- 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 |
||
|---|---|---|
| .claude/skills/hence | skills: add hence skill definition; ignore zetl vault state | |
| .github/workflows | feat(identity): did:key cryptographic identity for agents | |
| architecture | review: SPEC-013 fresh-context adversarial review (pre-approval) | |
| bin | fix(security): SPEC-012 t12 — close setf/setq safety-variable bypass (BLOCKER-1) | |
| docs | spec: SPEC-013 skill definition port (draft) | |
| examples | fix(experiment): stub reasoner uses EQ symbol matching, not substring | |
| plugins | feat(plugin): imago/zai — GLM Coding Plan (Anthropic-compatible) | |
| specs | spec: SPEC-013 — inline draft definitions, add Orientation + BCP-14 (fix BLOCKER-3) | |
| src | fix(security): SPEC-012 t12 — close setf/setq safety-variable bypass (BLOCKER-1) | |
| test | fix(security): SPEC-012 t12 — close setf/setq safety-variable bypass (BLOCKER-1) | |
| theories | feat(SPEC-012): implement self-modification port | |
| .gitignore | skills: add hence skill definition; ignore zetl vault state | |
| imago.asd | feat(plugin): imago/zai — GLM Coding Plan (Anthropic-compatible) | |
| LICENSE | feat: initial implementation — M0–M5 + M9 (7/12 milestones) | |
| plan.spec-012.spl | fix(security): SPEC-012 t12 — close setf/setq safety-variable bypass (BLOCKER-1) | |
| plan.spec-013.spl | spec: SPEC-013 skill definition port (draft) | |
| plan.spl | feat(M10+M11): reasoner invariant filter + drainable shutdown release | |
| README.md | fix(security): SPEC-012 t12 — close setf/setq safety-variable bypass (BLOCKER-1) | |
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
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.
Built With
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-rscdylibgit 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.
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).
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.
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:vetoaborts 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-transportfor production, an in-memorymock-transportfor tests. - Reasoner integration is IPC-only. At
:on-tool-callthe 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-identitywith an Ed25519 keypair and adid:key:...DID. Gateways auth via the(auth-did ...)frame;:clean tzeros the private key before save.
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:
- Pre-filter — fast structural denylist (
unintern,delete-package,(setf (symbol-function ...) ...),eval,load, reader-macro mutators,sb-thread:interrupt-thread,defmethodagainst any safety-layer generic, etc.). - 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. - 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) |
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.
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
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
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 viaexamples/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.
Contributing
Contributions are welcome. The project is small enough that a PR-and-discuss flow works fine — no formal RFC process.
- Fork the project at https://codeberg.org/anuna/imago
- Create your feature branch (
git checkout -b feat/your-feature) - Make sure
bash bin/run-tests.sh allis green before opening a PR - Commit your changes (
git commit -m 'feat: ...') - 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.
License
Distributed under the Apache-2.0 License. See LICENSE for
more information.
Contact
Project Link: https://codeberg.org/anuna/imago
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 drivesplan.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:
architecture/ADR-001-image-runtime.md— why SBCL specificallyarchitecture/ADR-002-identity.md— why did:key for agent identity (and what's deferred)architecture/CHECKING.md— what:clean tactually does at save timeplan.spl— SPEC-011 implementation plan as defeasible-logic rules; query withhence plan board plan.spltest/m4-tests.lisp— the most readable end-to-end exercise of the runtimetest/m7-wss-tests.lisp— gateway round-trip over a real WebSocket on loopbackspecs/SPEC-012-self-modification-port.md— agent self-modification port specarchitecture/ADR-012-self-mod-adversarial-review.md— 11 bypass shapes the spec floor missed, and the IMPL+ amendments that close themarchitecture/ADR-013-self-mod-oq-decisions.md— OQ-001..004 resolutions (timeout, packages, printer bounds, defun rollback)architecture/EXPERIMENT-LOG.md— six goal-driven runs that shaped SPEC-012, with each finding mapped to the commit that fixed itplan.spec-012.spl— SPEC-012 implementation plantest/m12-tests.lisp— 22 test functions exercising the safety stack and recursion-safety properties