5
0
Fork
You've already forked imago
0
A small, hackable agent runtime for SBCL Common Lisp.
  • Common Lisp 92.9%
  • Shell 7.1%
Anuna 02 d4efac38ab spec: SPEC-013 — inline draft definitions, add Orientation + BCP-14 (fix BLOCKER-3)
The assembled spec was a hollow index: sections 4–9 held only summary
tables linking to [[#REQ-###]]/[[#CON-###]]/[[#TEST-###]]/etc., but the
definitions lived only in docs/spec-013-drafts/, leaving ~64 dead
in-document anchors. Per the SPEC-013 review, this blocked genuine review.
Inlines all 63 artifact definitions verbatim from Hugo's drafts —
12 REQ, 6 NFR, 6 CON, 4 ADR, 28 TEST, 7 OBS, plus the format survey —
under their sections, keeping the summary tables as the orientation layer.
Adds the protocol-required Orientation block (intent/metaphor/ASCII
structure/decisions/load-bearing/open) and the BCP-14 conformance
declaration.
No design decision, threshold, or requirement wording changed: status
stays draft, the three OQs stay unresolved, and REQ-010/ADR-003/CON-001
(the safety-model artifacts flagged in the review) are byte-identical to
the drafts — those remain Hugo's calls. In-document artifact anchors now
resolve; remaining zetl dead-links are external concept pages (expected
backlog).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014hpPqDSqzzecsDaJNuTN8c 
2026年07月04日 21:20:26 +10:00
.claude/skills/hence skills: add hence skill definition; ignore zetl vault state 2026年07月04日 20:20:24 +10:00
.github/workflows feat(identity): did:key cryptographic identity for agents 2026年04月25日 23:50:47 +10:00
architecture review: SPEC-013 fresh-context adversarial review (pre-approval) 2026年07月04日 21:10:51 +10:00
bin fix(security): SPEC-012 t12 — close setf/setq safety-variable bypass (BLOCKER-1) 2026年07月04日 21:00:51 +10:00
docs spec: SPEC-013 skill definition port (draft) 2026年05月05日 13:54:39 +10:00
examples fix(experiment): stub reasoner uses EQ symbol matching, not substring 2026年04月28日 15:23:07 +10:00
plugins feat(plugin): imago/zai — GLM Coding Plan (Anthropic-compatible) 2026年04月28日 13:05:09 +10:00
specs spec: SPEC-013 — inline draft definitions, add Orientation + BCP-14 (fix BLOCKER-3) 2026年07月04日 21:20:26 +10:00
src fix(security): SPEC-012 t12 — close setf/setq safety-variable bypass (BLOCKER-1) 2026年07月04日 21:00:51 +10:00
test fix(security): SPEC-012 t12 — close setf/setq safety-variable bypass (BLOCKER-1) 2026年07月04日 21:00:51 +10:00
theories feat(SPEC-012): implement self-modification port 2026年04月28日 12:00:30 +10:00
.gitignore skills: add hence skill definition; ignore zetl vault state 2026年07月04日 20:20:24 +10:00
imago.asd feat(plugin): imago/zai — GLM Coding Plan (Anthropic-compatible) 2026年04月28日 13:05:09 +10:00
LICENSE feat: initial implementation — M0–M5 + M9 (7/12 milestones) 2026年04月25日 20:15:14 +10:00
plan.spec-012.spl fix(security): SPEC-012 t12 — close setf/setq safety-variable bypass (BLOCKER-1) 2026年07月04日 21:00:51 +10:00
plan.spec-013.spl spec: SPEC-013 skill definition port (draft) 2026年05月05日 13:54:39 +10:00
plan.spl feat(M10+M11): reasoner invariant filter + drainable shutdown release 2026年04月25日 20:55:08 +10:00
README.md fix(security): SPEC-012 t12 — close setf/setq safety-variable bypass (BLOCKER-1) 2026年07月04日 21:00:51 +10:00

Apache 2.0 License Status: Experimental Common Lisp SBCL 2.6+ Tests LOC


anuna-imago

A small, hackable agent runtime for SBCL Common Lisp.
Build your agent at the REPL · save it as a single binary · redefine any function in flight without restart.

Try it · Built-in tools · Roadmap · Report a bug

Table of Contents
  1. About The Project
  2. Getting Started
  3. Usage
  4. Project layout
  5. API Reference
  6. Roadmap
  7. Contributing
  8. License
  9. Contact
  10. Acknowledgments

About The Project

Agent frameworks tend to embed assumptions about what models can't do — planning modules, prompt-pipeline curation, output parsers — and those assumptions age badly as model capability climbs. anuna-imago commits to operational scaffolding only: supervision, identity, audit, capability routing, image distribution, runtime safety invariants. If a layer turns out to encode an obsolete assumption, you redefine it at the live REPL instead of filing a framework migration ticket.

The implementation is constrained to roughly 2000 lines of Common Lisp. The runtime is supervised processes. The wire is CBCL. The substrate is fully redefinable in flight.

(back to top)

Built With

(back to top)

Getting Started

Prerequisites

  • SBCL 2.6+
    brew install sbcl # macOS
    sudo apt-get install sbcl libffi-dev # Debian/Ubuntu
    
  • Quicklisp (one-time)
    curl -O https://beta.quicklisp.org/quicklisp.lisp
    sbcl --no-userinit --no-sysinit --load quicklisp.lisp \
     --eval '(quicklisp-quickstart:install)' --quit
    
  • Optional, only for the M6 CBCL FFI tests — cbcl-rs cdylib
    git clone https://codeberg.org/anuna/cbcl-rs ../cbcl-rs
    ( cd ../cbcl-rs && cargo build --release -p cbcl-ffi )
    

Installation

# Clone into anuna-imago/ to match the package name and directory tree below.
git clone https://codeberg.org/anuna/imago anuna-imago
cd anuna-imago
bash bin/run-tests.sh all # 17 suites, 290+ checks, ~30s
bash bin/build-echo-image.sh # produces ./echo-agent (~63 MB full-agent profile)

No install step. The system loads via ASDF from this directory; nothing gets dropped into /usr/local.

(back to top)

Usage

Try it

./echo-agent --version
# anuna-imago 0.1.0
./echo-agent --echo "hello world"
# echo: hello world
echo "ping" | ./echo-agent --serve
# echo: ping
# [drain on EOF]
./echo-agent --serve 60 # serve for 60s, drain on SIGTERM/SIGINT

The echo agent uses a stub provider — no API key required. Swap in the real Anthropic provider and you have a working LLM-backed agent in another ~10 lines of customisation (see Build your own agent).

(back to top)

Redefine in flight

The headline claim — "redefine any function in flight, then save" — works because methods are late-bound and the SBCL heap is the artifact.

(in-package :anuna-imago)
;; Build and start an agent in a live REPL.
(defparameter *sup* (make-supervisor 'live-sup))
(defparameter *agent* (build-echo-agent))
(spawn-agent! *sup* *agent*)
(getf (ask-agent *agent* "hi") :text)
;; => "echo: hi"
;; Redefine a provider method. CLOS swaps the dispatch in place — the
;; running supervisor, the spawned agent, its mailbox: all untouched.
(defmethod provider-stream! ((p stub-provider) agent message)
 (declare (ignore agent))
 (let* ((c (if (listp message) (getf message :content) message))
 (frame (list :text (format nil "shouted: ~A!" (string-upcase c))))
 (cell (cons nil (list frame))))
 (lambda () (pop (cdr cell)))))
(getf (ask-agent *agent* "hi") :text)
;; => "shouted: HI!" ; same process, no restart
;; Snapshot the live image. The redefined method is in the saved heap.
(send! (agent-mailbox *agent*) :shutdown)
(drain-supervisor! *sup*)
(save-image! "shouty-agent" :toplevel 'agent-main)
$ ./shouty-agent --echo "hello"
shouted: HELLO!

The patch survives because the binary is the heap. There's no source tree to redeploy and no rebuild step — the running image was edited and then frozen. The same path works for any harness method: tool dispatch, hook handlers, supervisor restart policy.

(back to top)

Mental model

A running agent is one OS process holding an SBCL image:

┌──────────────────────────────────────────────────────────┐
│ agent process │
│ │
│ ┌────────────┐ │
│ │ supervisor │ one-for-one restart policy │
│ └─────┬──────┘ │
│ │ spawns / monitors │
│ ▼ │
│ ┌────────────┐ ┌────────────┐ ┌─────────────┐ │
│ │ worker │ │ worker │ │ gateway │ │
│ │ (agent + │ │ (agent + │ │ (CBCL │ │
│ │ turn-loop)│ │ turn-loop)│ │ router) │ │
│ └─────┬──────┘ └────────────┘ └──────┬──────┘ │
│ │ inbox │ │
│ ▼ ▼ │
│ ┌────────────┐ ┌──────────────┐ │
│ │ mailbox │◄──── ask / reply ────►│ WebSocket │ │
│ └────────────┘ └──────────────┘ │
└──────────────────────────────────────────────────────────┘

The turn loop is the heart. For each inbound ask:

agent turn-loop hook chain tool registry
 │ ask │ │ │
 ├──────────────►│ provider-stream! │ │
 │ ├──── HTTP ─────► │ │
 │ │◄── tool_use ── │ │
 │ │ run-hook │ │
 │ │ :on-tool-call │ │
 │ ├──────────────────►│ Spindle reasoner: │
 │ │ │ (forbidden ...) ? │
 │ │◄──────────────────┤ -∂ → pass │
 │ │ dispatch-tool! │ │
 │ ├──────────────────────────────────────►│
 │ │◄── result ────────────────────────────┤
 │ reply │ │
 │◄──────────────┤ :tool-results │

Six seams to know about:

  • Methods are defmethod — redefine at the REPL, the next call uses the new version.
  • Hooks at :on-user-input, :on-tool-call, :on-tool-result, :on-turn-complete, :on-agent-spawn, :on-agent-crash. First handler to return :veto aborts the chain.
  • Providers are CLOS classes implementing provider-stream!. Stubs for tests, Anthropic in production; CON-005 contract preserved so Bedrock and Vertex drivers fit the same shape (not yet implemented).
  • Transports are abstract — wss-transport for production, an in-memory mock-transport for tests.
  • Reasoner integration is IPC-only. At :on-tool-call the harness asks a Spindle defeasible-logic theory whether the call is (forbidden ...); a +Δ or +∂ verdict vetoes before the handler runs.
  • Identity is per-agent: each agent can carry an agent-identity with an Ed25519 keypair and a did:key:... DID. Gateways auth via the (auth-did ...) frame; :clean t zeros the private key before save.

(back to top)

Built-in tools

Ten tools auto-register when imago loads. They cover introspection on the harness itself plus a few mundane utilities. Agents that don't list them in :tools don't expose them to the LLM, so registration is harmless.

Name Returns
harness-list-tools All registered tool names
harness-describe-tool {description, permission, schema} for a name
harness-list-hooks Hook keys + handler counts
harness-version Harness version string
harness-now Current UTC time, ISO-8601
harness-describe-agent Calling agent's id, capability, system prompt, tools, state
harness-query-receipts Last N receipt-log entries with :limit
harness-uuid Fresh UUID v4
harness-stats Process metrics: uptime, mailbox depth, tool count
harness-query-theory Inspect a Spindle theory query (does NOT veto)
;; Wire all ten into an agent:
(make-instance 'agent ... :tools *builtin-tool-names*)

Opt-in: fileops

File operations are gated behind an explicit installer. The reasoner is expected to gate dangerous calls — at minimum, (forbidden harness-write-file "/etc/...") style rules — before exposing these to an agent.

(install-fileops-tools!) ; registers the three below
(make-instance 'agent ...
 :tools (append *builtin-tool-names* *fileops-tool-names*))
Name Permission Returns
harness-read-file :read UTF-8 content + :length + :truncated (truncates at :max-chars, default 1MB)
harness-write-file :write {:status :ok :length ...}; :append t to append
harness-list-directory :read Sorted file + subdirectory names

What's deliberately not shipped: HTTP fetch, web search, shell exec. Those are exactly the "agent framework" abstractions this project's bitter-lesson stance refuses — schema-volatile across providers, and better served by MCP servers or per-project tool modules.

Plugins (opt-in ASDF subsystems)

Capabilities that don't belong in the core 2k LOC harness ship as plugins under plugins/. Load with (ql:quickload :imago/<plugin>); main :imago system stays unaware.

Plugin Load with Provides
imago/openrouter (ql:quickload :imago/openrouter) OpenAI-compatible driver for any OpenRouter-served model. Pass the slug as :model, e.g. "z-ai/glm-5.1", "openai/gpt-4o", "anthropic/claude-opus-4-7"
imago/zai (ql:quickload :imago/zai) Z.ai GLM Coding Plan provider (Anthropic-compatible). Thin wrapper over the existing Anthropic driver pointed at api.z.ai/api/anthropic. Optional opencode auth.json key reader
;; --- Example A: any model via OpenRouter ----------------------------
(ql:quickload :imago/openrouter)
;; OPENROUTER_API_KEY in env, or :api-key here.
(let ((provider (anuna-imago:make-openrouter-provider
 :model "z-ai/glm-5.1"))) ; or "openai/gpt-4o", "anthropic/claude-opus-4-7", ...
 (make-instance 'anuna-imago:agent
 :provider provider
 :system-prompt "You are concise."
 :tools '(anuna-imago:harness-list-tools)))
;; --- Example B: Z.ai GLM Coding Plan (Anthropic-compatible) ---------
(ql:quickload :imago/zai)
(let ((provider (anuna-imago:make-zai-coding-provider
 :model "glm-5.1" ; or glm-5-turbo / glm-4.7 / glm-4.5-air
 ;; Pull the key from opencode's auth.json so you don't
 ;; have to re-paste it. Pass NIL to skip and use
 ;; ANTHROPIC_API_KEY env instead.
 :opencode-slug "zai-coding-plan")))
 (make-instance 'anuna-imago:agent
 :provider provider
 :system-prompt "You are concise."
 :tools '(anuna-imago:harness-list-tools)))

Both plugins reuse the existing provider-stream! contract — the same agent definition, supervision, hook chain, and self-modification port work unchanged with any served model.

Multi-turn experiment runner

examples/glm-self-mod-experiment.lisp is a research instrument with two modes, driven by bin/run-experiment.sh:

Mode What Question it answers
Prescribed (default) Fixed six-prompt protocol: tool-discovery → plan → implement → verify → forbidden-probe → rollback Do the safety mechanisms catch the obvious attacks? Comparable across runs.
Goal-driven (--goal "...") Agent is told a goal in its system prompt and driven by Continue. each turn until it replies GOAL ACHIEVED: or hits --turns (default 8) Can the model decompose a real task using the port? Open-ended; not directly comparable.
./bin/run-experiment.sh # prescribed
./bin/run-experiment.sh --model glm-4.7 # different model
./bin/run-experiment.sh --goal "Define a memoize macro and use it
 to wrap a slow function."
./bin/run-experiment.sh --goal "..." --turns 12 # longer run
./bin/run-experiment.sh --dry-run --goal "..." # show what would run

The script reads your Z.ai key from ~/.local/share/opencode/auth.json under the zai-coding-plan slug — no env vars to set. Reports land under /tmp/imago-experiments/<timestamp>-glm{-goal}-experiment.md with: starting/ending state, per-turn deltas (audit log appendices, origin-index updates, rollback-register pushes, reasoner-trace facts correlated by form-id), audit-log tally, falsification checklist, and failure modes to scan for.

Both modes use a floor-only stub reasoner that mimics the shipped theories/self-modification-floor.spl, so no live Spindle service is required.

What goals can actually work: the agent can defun / defclass / defmacro freely, but most of the harness's core dispatch surface is in *safety-layer-symbols* — so goals like "add logging to all tool calls" will hit :vetoed because the obvious implementation mentions dispatch-tool!. Goals that fit the v0.1 floor:

  • "Add persistent memory to yourself." (Empirically achieved by GLM 5.1 in 6 turns: 18 symbols redefined, 19 rollback records, 3 legitimate vetoes — see architecture/EXPERIMENT-LOG.md.)
  • "Build a small library of string utilities (palindrome, capitalize words, etc.) and demonstrate calling them on test inputs."
  • "Define a memoize macro and apply it to a slow function you also define."
  • "Implement a simple in-memory key-value cache as a defclass with put/get methods, and exercise it."

When the floor fires, the rejected reply carries a :hint field naming the category of the offending mention — e.g. EVAL-CLASS for any of eval / read / load / compile — and suggests an alternative class of operations. The harness-list-safety-layer tool also accepts :by-category t to return categorised symbols with rationale up front. Without these, agents tend to retry from the same forbidden category one symbol at a time; with them, GLM 5.1 typically pivots to a working approach in 1-2 vetoes.

Wrapping/replacing existing harness behavior is largely deferred to a future harness-advise tool (ADR-005, v0.2). The empirical journey that shaped this surface — six goal-driven runs, four follow-up fixes — is logged in architecture/EXPERIMENT-LOG.md.

Opt-in: self-modification port

A harness-eval tool — submit a Common Lisp source form, get it evaluated in the harness's runtime — plus five introspection siblings live in examples/self-modifying.lisp under explicit author opt-in. The file is not loaded by ASDF; the author must (load ...) it and call (install-self-modification-tools!) after loading a defeasible-logic theory. Three layers gate every harness-eval call:

  1. Pre-filter — fast structural denylist (unintern, delete-package, (setf (symbol-function ...) ...), eval, load, reader-macro mutators, sb-thread:interrupt-thread, defmethod against any safety-layer generic, etc.).
  2. Reasoner — the form is lifted to facts (operator, target, defmethod-targets, free-symbols incl. body-buried) and the active Spindle theory is queried for (forbidden eval-call ...). The shipped floor invariants block any mention of any safety-layer symbol.
  3. Handler — evaluates in a worker thread under a timeout (default 1 s, max 30 s), captures method-set diffs for rollback, writes a verbatim audit-log entry, updates a queryable origin index.

The full surface is specified in specs/SPEC-012-self-modification-port.md; the in-context adversarial review of CON-002/003 found 11 bypass shapes and is recorded in architecture/ADR-012-self-mod-adversarial-review.md. Operators can (unregister-tool! 'harness-eval) from the REPL at any time and re-snapshot the image without it.

;; Opt-in pattern:
(load "examples/self-modifying.lisp")
(let* ((floor (uiop:read-file-string "theories/self-modification-floor.spl"))
 (handle (load-theory floor)))
 (install-invariant-filter! :theory-handle handle)
 (install-self-modification-tools!)) ; → :ok | :no-active-theory

Six tools register together (*self-modification-tool-names*). The five siblings give the LLM agent enough runtime context to use harness-eval without trial-and-error against the safety stack:

Name Permission Returns
harness-eval :eval The evaluated value, or a structured rejection / veto / error / timeout plist (see CON-001 prose in the tool's :description)
harness-list-safety-layer :read The forbidden symbol set, optionally filtered by :prefix — "what NOT to redefine"
harness-redefine-history :read Per-symbol summary {:symbol :event-count :latest}, or with :symbol the most-recent N events for that symbol
harness-list-rollbacks :read Lightweight summary of *rollback-register* — index, kind, symbol, installed-at, rolled-back
harness-rollback :execute Re-installs the prior method/fdefinition for :index. Returns `{:status :ok
harness-query-self-mod-receipts :read Recent harness-eval audit-log entries (distinct from harness-query-receipts, which reads the SPEC-011 ASK/reply log)

(back to top)

Quick start: scaffold a new project

bash bin/new-agent.sh my-agent ../my-agent
cd ../my-agent
bash bin/build.sh
./my-agent --echo "hi"

This vendors imago into ../my-agent/imago/, generates a starter project with a build script, smoke test, and demonstrative agent file. See bin/new-agent.sh for what gets stamped where.

Build your own agent

Three things to customise: tools, system prompt, provider.

;; my-agent.lisp
(in-package #:anuna-imago)
;; 1. Define tools the LLM can call.
(define-tool current-time
 :description "Return the current ISO-8601 UTC timestamp."
 :schema ()
 :handler (lambda (args) (declare (ignore args)) (iso-8601-now)))
(define-tool greet
 :description "Greet someone by name."
 :schema ((:name :type :string :required-p t :description "Person to greet"))
 :handler (lambda (args) (format nil "Hello, ~A." (getf args :name))))
;; 2. Wire an agent that uses them, against a real provider.
(defun my-toplevel ()
 (let* ((provider (make-anthropic-provider)) ; reads ANTHROPIC_API_KEY
 (sup (make-supervisor 'my-sup))
 (agent (make-instance 'agent
 :id 'clock
 :capability "time:lookup"
 :provider provider
 :system-prompt "You are a clock agent."
 :tools '(current-time greet))))
 (spawn-agent! sup agent)
 (sleep 0.05)
 (let ((reply (ask-agent agent "What time is it? Greet Hugo while you're at it.")))
 (format t "~%~A~%" (getf reply :text))
 (dolist (r (getf reply :tool-results))
 (format t " [~A] → ~A~%" (getf r :name) (getf r :value))))
 (send! (agent-mailbox agent) :shutdown)
 (drain-supervisor! sup)))

Save it as a binary:

sbcl --no-userinit --no-sysinit \
 --load ~/quicklisp/setup.lisp \
 --eval "(push (truename \".\") asdf:*central-registry*)" \
 --eval "(asdf:load-system :imago)" \
 --eval "(load \"my-agent.lisp\")" \
 --eval "(anuna-imago:save-image! \"my-agent\"
 :toplevel 'anuna-imago::my-toplevel
 :executable t)"
ANTHROPIC_API_KEY=sk-... ./my-agent

./my-agent is now a self-contained ~63 MB binary you can scp to any machine without SBCL installed. :clean t (default) flushes credentials, private keys, async pools, and open log handles before the save — see architecture/CHECKING.md for the checklist.

To make the agent reachable from a CBCL router, build a gateway over a wss-transport. Pass either a bearer-token string or an agent-identity (Ed25519 + did:key) for the :identity slot — the auth handshake dispatches on type:

(let* ((id (generate-identity)) ; fresh did:key identity
 (tr (make-wss-transport "wss://router.example/agent/v1"))
 (gw (make-gateway :id 'my-gw
 :transport tr
 :identity id ; → (auth-did <DID> ...)
 :capability "echo:say"
 :agent agent)))
 (register-identity-for-clean! id) ; private key stripped on save
 (gateway-connect! gw)
 (gateway-start-pumps! gw))

test/m7-tests.lisp shows the full lifecycle against a mock router; test/m7-wss-tests.lisp exercises the same surface against a real WebSocket round-trip on loopback.

(back to top)

Project layout

anuna-imago/
├── bin/
│ ├── build-echo-image.sh save-lisp-and-die wrapper
│ └── run-tests.sh test runner
├── src/
│ ├── packages.lisp package definitions
│ ├── main.lisp agent-main entry + --serve loop
│ │
│ ├── mailbox.lisp ┐
│ ├── supervisor.lisp │ supervised actor primitives
│ ├── agent.lisp ┘
│ │
│ ├── hooks.lisp hook registry, sync + fire-and-forget
│ ├── tools.lisp define-tool, JSON Schema as CL data
│ ├── builtin-tools.lisp harness-{list,describe,uuid,stats,...}
│ ├── fileops-tools.lisp opt-in harness-{read,write,list-dir}
│ ├── turn-loop.lisp default per-message loop
│ │
│ ├── receipt-log.lisp content-addressed audit log
│ ├── save-image.lisp save-image! + :clean checklist
│ │
│ ├── gateway.lisp CBCL Router Client — receiver side
│ ├── producer-gateway.lisp CBCL Router Client — producer side
│ ├── identity.lisp Ed25519 + did:key, sign/verify
│ ├── wss-transport.lisp websocket-driver-backed transport
│ ├── cbcl-ffi.lisp CBCL parser via FFI to cbcl-rs
│ ├── reasoner.lisp Spindle IPC + invariant filter
│ ├── self-modification.lisp SPEC-012 harness-eval (handler,
│ │ prefilter, lift, origin index, rollback)
│ │
│ └── providers/
│ ├── stub.lisp canned-response provider for tests
│ └── anthropic.lisp Messages API + mockable HTTP
│
├── test/ milestone suites (M1–M11, M12) plus
│ M7-WSS, M7-Producer, builtin-tools,
│ fileops-tools, identity
├── examples/
│ ├── echo.lisp reference echo agent
│ └── self-modifying.lisp opt-in install-self-modification-tools!
│ (NOT registered by ASDF — REQ-001)
├── theories/
│ └── self-modification-floor.spl SPEC-012 floor invariants (Spindle)
├── specs/
│ └── SPEC-012-self-modification-port.md agent self-modification port
├── architecture/
│ ├── ADR-001-image-runtime.md why SBCL (not ECL)
│ ├── ADR-002-identity.md why did:key (not did:web/plc)
│ ├── ADR-012-self-mod-adversarial-review.md
│ │ 11 bypass shapes + IMPL+ amendments
│ ├── ADR-013-self-mod-oq-decisions.md
│ │ OQ-001..004 resolutions
│ └── CHECKING.md :clean t audit checklist
├── plugins/ opt-in subsystems — load via
│ ├── openrouter/ (ql:quickload :imago/<plugin>)
│ │ ├── openrouter.lisp OpenRouter (OpenAI-compatible) driver
│ │ └── test.lisp stubbed-HTTP test suite
│ └── zai/
│ ├── zai.lisp Z.ai GLM Coding Plan (Anthropic-compat)
│ └── test.lisp stubbed-HTTP test suite
├── plan.spl SPEC-011 implementation plan (hence)
├── plan.spec-012.spl SPEC-012 implementation plan (hence)
└── .github/workflows/ci.yml matrix CI + LOC-budget gate

(back to top)

API Reference

Compact signature reference for the surface a user-facing agent author calls. The full export list is in src/packages.lisp; docstrings live with the definitions in src/.

Agents & supervision

(make-instance 'agent :id ... :capability ... :provider ... :system-prompt ...
 :tools '(...))  agent
(agent-id|agent-capability|agent-mailbox|agent-provider
 |agent-tools|agent-system-prompt|agent-theory|agent-state) agent
*current-agent* ; bound during a turn
(make-supervisor id &key max-restarts within-seconds parent) → supervisor
(add-child! supervisor id start-fn) → id
(start-supervisor! supervisor) → supervisor
(spawn-agent! supervisor agent) → agent
(drain-supervisor! supervisor &key timeout) → supervisor
(force-restart! supervisor child-id) → child-id | nil
(list-children supervisor)  ((:id ... :state ... :restarts ...) ...)
(child-state-of supervisor child-id)  keyword | nil
(sup-state supervisor) → :stopped | :running | :draining | :failed

Mailboxes & messaging

(make-mailbox)  mailbox
(send! target message)  message ; defgeneric
(receive! mailbox &key timeout)  message | :timeout | :closed
(peek-mailbox mailbox)  message | nil
(mailbox-depth mailbox)  integer
(close-mailbox! mailbox)
;; SEND! is a generic; default method is on MAILBOX. GATEWAY adds a method
;; that forwards as a reply over the wire — same call site, two backends.
(make-ask content &key reply-to meta)  ask-plist
(ask-message-p msg)  boolean
(ask-content msg)  string
(ask-reply-to msg)  mailbox-or-gateway
(make-reply text &key tool-results meta)  reply-plist
(ask-agent agent content &key timeout)  reply-plist ; convenience

Hooks

(register-hook key handler)  handle
(remove-hook handle)  t | nil
(run-hook key agent &rest args) → value | :veto | nil
(list-hooks &optional key)
(clear-all-hooks)
*hook-keys* ; :on-user-input :on-tool-call :on-tool-result
 ; :on-turn-complete :on-agent-spawn :on-agent-crash
*excluded-hook-keys* ; :on-prompt-build :on-stream-token (registration errors)

Tools

;; Registration
(define-tool name :description ... :permission ... :schema ... :handler ...)
(register-tool! tool)  name
(unregister-tool! name)  t | nil
(find-tool name) → tool | nil
(list-tools)  (name ...)
(clear-all-tools)
(dispatch-tool! name args-plist)  handler-return
;; Schema → provider format
(schema->json-schema schema)  CL alist tree
(json-schema->schema json-data)  schema ; round-trip
(tool->anthropic-descriptor tool)
  ((:name ... :description ... :input_schema ...))
;; Built-ins (auto-registered when imago loads)
*builtin-tool-names* ; harness-list-tools, harness-describe-tool,
 ; harness-list-hooks, harness-version, harness-now,
 ; harness-describe-agent, harness-query-receipts,
 ; harness-uuid, harness-stats, harness-query-theory
(install-builtin-tools!) ; idempotent re-install
;; Fileops (opt-in)
*fileops-tool-names* ; harness-read-file, harness-write-file,
 ; harness-list-directory
*fileops-max-read-chars* ; default 1048576
(install-fileops-tools!)
(uninstall-fileops-tools!)

Providers

(provider-name provider)  string
(provider-stream! provider agent message)  stream-handle
(stream-next-frame! stream)
  (:text STRING) | (:tool-use ID NAME ARGS) | (:error PLIST) | :done
;; Stub (canned responses for tests)
(make-stub-provider &key responder)  provider
;; Anthropic
(make-anthropic-provider &key api-key model base-url max-tokens)  provider
(build-request provider message agent)  hash-table
(auth-headers provider)  ((header . value) ...)
*anthropic-http-post* (url &key headers content)  string ; mockable

Gateway & transport

;; Receiver gateway — agent serves asks for its registered capability.
(make-gateway :id ... :transport ... :identity ... :capability ...
 :agent ... :receipt-log ... :heartbeat-interval ...)  gateway
(gateway-connect! gateway &key timeout)  t
(gateway-start-pumps! gateway) ; recv + heartbeat threads
(gateway-disconnect! gateway)
(gateway-state gateway)
  :disconnected | :authenticating | :ready | :draining | :failed
;; Producer gateway — agent issues asks via the router and awaits replies.
(make-producer-gateway :id ... :transport ... :identity ...
 :heartbeat-interval ...)  producer-gateway
(producer-connect! gw &key timeout)  t
(producer-start-pumps! gw)
(producer-ask! gw capability body)  (values mailbox receipt-id)
(producer-call! gw capability body &key timeout)
  reply-string | :timeout | (:error :rejected ...)
(producer-disconnect! gw)
(producer-state gw)  :disconnected | :authenticating | :ready | :draining | :failed
(producer-in-flight-count gw)  integer
;; Transport protocol (defgenerics; specialise to add new transports)
(transport-open! tr)
(transport-send! tr string)
(transport-recv! tr &key timeout)  string | :timeout | :closed
(transport-close! tr)
(transport-connected-p tr)  boolean
(make-wss-transport url &key headers)  wss-transport
*wss-open-timeout-seconds* ; default 10
(make-mock-transport)  mock-transport
(mock-feed! tr string) ; inject inbound (for tests)
(mock-drain! tr &key timeout) ; read what gateway sent

Identity (did:key, Ed25519)

(generate-identity)  agent-identity
(identity-did identity)  "did:key:z..."
(identity-public-key-bytes identity)  octet-vector (32 bytes)
(identity-private-key identity)  ironclad-priv | nil ; nil after :clean
(sign-string identity string) → octet-vector (64 bytes)
(verify-signature did string signature-bytes) → boolean
;; DID encoding helpers (W3C did:key, Ed25519 multicodec 0xed01)
(encode-did-key 32-byte-pubkey) → "did:key:z..."
(parse-did-key did-string) → 32-byte-pubkey | nil
(bytes->hex octet-vector) / (hex->bytes string)
(base58btc-encode octet-vector) / (base58btc-decode string)
;; :clean integration — strips private key before save-image!
(register-identity-for-clean! identity)
(clear-identity-private-key! identity)
;; Auth handshake helper
(make-did-auth-payload did iso-timestamp)  string ; signed payload
(make-did-auth-frame identity)
  "(auth-did <did> <iso-ts> <hex-sig>)"
;; Wire identity into an agent or gateway:
(make-instance 'agent ... :identity (generate-identity))
(agent-did agent) ; convenience
(make-gateway ... :identity (generate-identity)) ; uses (auth-did ...) instead of (auth ...)

Reasoner

*reasoner-ipc-call* (op &rest args) ; injectable IPC; tests stub
*active-theory-handle* ; bound by install-...!
(load-theory text-or-path)  handle
(assert-fact! handle fact)  ack
(retract-fact! handle fact)  ack
(query handle goal)
  (:tag :+delta|:+partial-delta|:-delta|:-partial-delta
 :derivation ... :time-ms ...)
(what-if handle goal facts)  proof-result
(why-not handle goal)  counter-derivation
(proof-result-positive-p result)  generalised-boolean
(install-invariant-filter! :theory-handle handle)  hook-handle
(uninstall-invariant-filter!)
;; Wires invariant-filter-hook on :on-tool-call. Reasoner verdicts of
;; +Δ or +∂ on (forbidden TOOL ARGS) → :VETO before dispatch.

Receipt log

(open-receipt-log path)  log
(append-receipt! log &key receipt-id direction dialect verb body
 agent-id producer-id status)  entry-plist
(read-receipts path)  (entry-plist ...)
(close-receipt-log! log)
(content-hash body)  hex-string ; sb-md5
(iso-8601-now)  "YYYY-MM-DDTHH:MM:SSZ"
(register-receipt-log-for-clean! log) ; for :clean t flush

Self-modification (SPEC-012, opt-in)

;; Registration — load examples/self-modifying.lisp first; not in ASDF.
(install-self-modification-tools! &key audit-log-path)
  :ok | :no-active-theory | :already-installed
(uninstall-self-modification-tools!)  :ok
;; Tool dispatch (the handler returns a plist; never raises)
;; CON-001 contract:
;; (:status :ok :phase :evaluated :value ... :stdout ... :elapsed-ms ...)
;; (:status :rejected :phase :pre-filter :rule ... :reason ...)
;; (:status :vetoed :phase :reasoner :goal ... :derivation ... :time-ms ...)
;; (:status :error :phase :evaluation :condition-type ... :message ...)
;; (:status :timeout :phase :evaluation :elapsed-ms ...)
;; Pre-filter (CON-002 + ADR-012 §A1)
*prefilter-denylist* ; alist op → rule keyword
(%harness-eval-prefilter form)  :pass | (:status :rejected ...)
;; Lift (CON-003 + ADR-012 §A4)
(%lift-form form)
 → (:operator ... :target ... :qualifier ... :specialisers ...
 :free-symbols ... :defmethod-targets ... :defgeneric-targets ...
 :defun-targets ...)
;; Safety-layer set — defeasible mentions/2 floor (ADR-012 §A1)
*safety-layer-symbols*
;; Origin index (CON-005)
*redefine-history* ; hash-table sym → events
(redefine-history symbol) → (event ...)
(last-redefinition symbol) → event | nil
(all-redefined-symbols)  (sym ...)
;; Rollback register (CON-006 + ADR-013 OQ-004 union shape)
*rollback-register* ; vector of records
(rollback! index)  :ok | :no-such-record |
 :already-rolled-back
(rollback-records)  (record ...)
(find-rollback-records-for symbol)  (record ...)
;; Result-printing bound (ADR-013 OQ-003)
*harness-eval-result-truncate-bytes* ; default 4096
;; Audit log instance — opened by install-self-modification-tools!
*harness-eval-audit-log* ; receipt-log instance | nil

Image distribution

(save-image! path &key toplevel clean executable) ; DOES NOT RETURN
(pre-save-clean!) ; manually trigger checklist
*clean-checklist* ; :close-receipt-log :shutdown-hook-async-pool
 ; :drop-credentials :force-gc
(register-credential-eraser! thunk)
*boot-time* ; universal-time at load
*version* ; "0.1.0"
(agent-main) ; toplevel for saved images

(back to top)

Roadmap

  • M0 Project scaffold (asd, packages, ADR-001-image-runtime)
  • M1 Supervisor + mailbox + agent CLOS
  • M2 Hook system with stance enforcement
  • M3 define-tool + JSON Schema as CL data
  • M4 Turn loop + stub provider + echo example
  • M5 Append-only content-addressed receipt log
  • M6 CBCL parser via FFI to cbcl-rs
  • M7 Gateway with transport abstraction
  • M7-WSS Production WebSocket transport
  • M8 Anthropic provider (non-streaming)
  • M9 Image distribution (save-image! + :clean)
  • M10 Reasoner IPC + invariant filter
  • M11 Drainable shutdown + release smoke
  • Built-in introspection tools (10 default, auto-registered)
  • Producer-gateway (cross-process agent composition)
  • Opt-in fileops tools (install-fileops-tools!)
  • did:key cryptographic identity (Ed25519, ADR-002)
  • SPEC-012 self-modification port — implemented (harness-eval, opt-in via examples/self-modifying.lisp; ADR-012 / ADR-013)
  • SPEC-012 t10 — save-image! survival integration test (TEST-016)
  • SPEC-012 t11 — full SPEC-012 test corpus (TEST-010/017 + NFR benchmarks TEST-018..TEST-022, 1000-form FP corpus under test/fixtures/)
  • SPEC-012 t12 — final adversarial review (SELF-MOD-REVIEW.md); fixed BLOCKER-1 setf/setq safety-variable bypass (ADR-014)
  • R4 frame-level signing on every CBCL message (needs cbcl-rs FFI)
  • Streaming SSE for the Anthropic provider
  • CI matrix M6 step (cbcl-rs cdylib build)
  • Bedrock provider driver
  • Vertex provider driver

By the numbers: ~4100 LOC harness, ~3760 LOC tests, ~63 MB image (full-agent profile incl. provider + WSS + identity), 165 ms p90 cold start, 17 test suites ×ばつ 430+ checks, all green.

See the open issues for proposed features and known issues.

(back to top)

Contributing

Contributions are welcome. The project is small enough that a PR-and-discuss flow works fine — no formal RFC process.

  1. Fork the project at https://codeberg.org/anuna/imago
  2. Create your feature branch (git checkout -b feat/your-feature)
  3. Make sure bash bin/run-tests.sh all is green before opening a PR
  4. Commit your changes (git commit -m 'feat: ...')
  5. Push to the branch and open a pull request

The LOC budget gate at 3300 lines (in .github/workflows/ci.yml) is a soft signal — going over warrants a discussion of whether the addition is paying for itself. The cap has been raised four times as features landed; each bump comes with a commit message explaining what made it necessary.

(back to top)

License

Distributed under the Apache-2.0 License. See LICENSE for more information.

(back to top)

Contact

Project Link: https://codeberg.org/anuna/imago

(back to top)

Acknowledgments

  • SBCL — the runtime that makes image-as-artifact + live redefinition real
  • websocket-driver — clean WS protocol implementation, server + client
  • dexador — HTTP+SSE used by the Anthropic provider
  • com.inuoe.jzon — fast, modern JSON for CL
  • ironclad — Ed25519 sign/verify behind the did:key identity layer
  • cbcl-rs — Rust parser binding, inherits Lean-verified oracle parity
  • hence — the defeasible-logic task planner that drives plan.spl
  • Best-README-Template — the structural template this README follows

The bitter-lesson stance owes its framing to recent critiques of agent frameworks; the "amputable substrate" framing is from anuna's prior architectural work on operational scaffolding versus capability augmentation.

Further reading, in roughly the order you'd want to read them:

(back to top)