The same diagrams that illustrate the architecture pages, collected here so a single URL shows the whole system. Each one is authored as text insrc/data/diagrams.ts and rendered in the browser, no exported images, so they stay diffable and cannot rot.
Registered, Active, Deactivated, Failed, and what activation and deactivation actually do.
Capability ledger
sequenceDiagram
autonumber
participant H as Host
participant RT as PluginRuntime
participant P as Plugin
participant R as Registry
participant L as Ledger
H->>RT: enable(id)
RT->>P: initialize(ctx)
P-->>RT: true
RT->>P: capabilities()
P-->>RT: Capability[]
loop each capability, declaration order
RT->>R: install(services)
R-->>RT: Contribution handle
RT->>L: record(owner, Contribution)
end
Note over H,L: Active - every contribution reachable
H->>RT: disable(id)
RT->>P: shutdown()
RT->>L: unwind(owner), reverse install order
L->>R: remove() per Contribution
Note over H,L: Deactivated - nothing reachable
Declare, install, record; then shutdown and unwind in reverse install order.
Where a plugin runs is an isolation decision, not a size decision.
Command binding
flowchart TB
declare["Plugin declares CommandSpec<br/>root · help · man · subtree · handlers"]
install["Ledger installs into CommandRegistry"]
merge["Host merges the subtree into the command tree<br/>executable leaf action = plugin.{id}.{path}"]
drawer["Drawer + completions"]
dispatch["Dispatch resolves the handler<br/>from the live registry"]
inert["Leaf with no handler<br/>documented, not executable"]
toggle["enable / disable rebuilds the tree, no restart"]
declare --> install
install --> merge
merge --> drawer
merge --> dispatch
merge --> inert
toggle -.-> merge
The host derives the action and resolves the handler from the live registry at dispatch time.
External plugin handshake
sequenceDiagram
autonumber
participant H as amber harness
participant PM as PluginManager
participant P as plugin process
H->>PM: enable(id)
PM->>PM: discover manifest.json
Note over H,PM: enabled - tools registered, namespace merged
H->>PM: tool call
PM->>P: spawn executable
PM->>P: initialize protocol_version, settings, workspace
P-->>PM: protocol_version, ok
alt compatible
PM->>P: tool.call name, args
P-->>PM: ok, output, meta
PM-->>H: tool envelope
else version mismatch or ok false
Note over H,P: unregistered - incompatible
end
H->>PM: disable(id)
PM->>P: shutdown
P-->>PM: exit
Discovery, spawn on the first tool call, the version handshake, and the shutdown notification.
Command system
Command tree structure
flowchart TB
json["completions.json<br/>the single source of truth"]
schema["node = { help, man, action, children }<br/>the last leaf of a branch carries the action"]
json --> schema
schema --> index["SettingRegistry indexes every node<br/>help · man · children · choices · ranges"]
index --> keyed["namespaces keyed by full display path<br/>get.model is not set.model"]
keyed --> resolve["resolve_key<br/>exact → get.key → set.key"]
resolve --> unknown["unresolved → empty<br/>the caller reports an unknown command"]
completions.json is the single source of truth; namespaces are keyed by full display path.
Command dispatch
flowchart TD
line["Enter: /set policy read"] --> strip["strip the leading slash, trim, tokenize"]
strip --> walk["walk the commands tree, consuming every token<br/>that names a documented child"]
walk --> deepest{"any node matched?"}
deepest -->|no| unknown["unknown command (try /help)"]
deepest -->|yes| node["node = deepest documented match<br/>remaining tokens joined as the argument"]
node --> has{"node carries an action?"}
has -->|no| man["print the node man page<br/>documented, not executable"]
has -->|yes| reg{"action registered?"}
reg -->|no| man
reg -->|yes| dispatch["dispatch(action, arg)"]
dispatch --> safe["exceptions caught<br/>command error: ... to the status line"]
One keypress to one closure. The tree decides the action; the handler never names a command.
Command feeds
flowchart TB
tree["completions.json<br/>documented namespaces"]
feeds["feed leaves generated at runtime<br/>model list · policy rules · providers · jobs<br/>plugins · MCP servers · plugin commands"]
tree --> merge
feeds --> merge["merge_completions_json<br/>deep merge: static fields preserved,<br/>children unioned, feeds never clobber docs"]
merge --> actions["each leaf carries a generated action<br/>and the feed registers its closure"]
actions --> surface["drawer · completions · dispatch<br/>one tree, three readers"]
Dynamic values arrive as merged feed leaves, never as hardcoded C++ lists.
Engine
Agent loop
flowchart TD
run["Agent::run(prompt)"] --> seed["ensure_system_prompt<br/>history always starts with a system message"]
seed --> loop{"iteration under max_tool_iterations<br/>and not done?"}
loop -->|no| stop["hard stop<br/>loop detected, or tools kept failing"]
loop -->|yes| gate{"compression gate<br/>context utilisation over threshold?"}
gate -->|compress| rebuild["compression pipeline<br/>context rebuilt: clear + push"]
gate -->|keep| assemble
rebuild --> assemble["assemble the prompt copy<br/>head + tail blocks, after the gate"]
assemble --> llm["LLM call, streamed via SSE"]
llm --> parse["parse tool calls"]
parse -->|none| confirm["silent confirm turn<br/>are you finished?"]
confirm -->|done| reply["return the final reply"]
confirm -->|continue| loop
parse -->|calls| dispatch["dispatch tool calls<br/>parallel execution, ordered results"]
dispatch --> push["push tool-role messages"]
push --> loop
One turn of the ReAct loop: gate, assemble, call, dispatch, confirm.
Tool dispatch
flowchart TD
calls["tool calls from the LLM reply"] --> validate{"known tool,<br/>valid JSON arguments?"}
validate -->|no| denied["denied<br/>tool not found / invalid JSON"]
validate -->|yes| dup{"duplicate of an identical call<br/>in the last assistant turn?"}
dup -->|yes| denied
dup -->|no| policy{"PolicyStore level<br/>read / write / yolo"}
policy -->|read mode blocks it| denied
policy -->|needs approval| gate{"approval gate"}
gate -->|denied| denied
gate -->|allowed| exec
policy -->|allowed| exec["execute in parallel via std::async"]
exec --> collect["collect results"]
collect --> ordered["append in original order<br/>exactly one tool-role message per call"]
denied --> ordered
ordered --> envelope["feedback envelope<br/>status ok / error / denied"]
Every tool call, and every way it can be denied, ending in one ordered envelope per call.
Compression pipeline
sequenceDiagram
autonumber
participant A as Agent
participant C as Context (live)
participant L as LLM
A->>C: read the snapshot
C-->>A: messages (Context stays untouched)
A->>A: collapse consecutive tool-call loops
Note over A,L: both requests share a content-identical prefix,<br/>so the second extends the first's KV cache
A->>L: classify request
L-->>A: classification json - core / context / prune
A->>L: extract request, replaying the classify prefix
L-->>A: extraction json - memories + skills
A->>A: apply classification, enforce headroom
A->>C: clear() then push() the compressed set
Note over A,C: the rebuild is the only live-context change,<br/>and only on success; a failure leaves it untouched
Classify then extract, sharing one prefix, and why the cycle costs no full prefill.
Context hash chain
flowchart TB
subgraph ops["the only mutation operations"]
push["push(msg): seal and append<br/>h = FNV(prev_hash, msg)"]
pop["pop(): remove the top, LIFO<br/>restores the previous hash in O(1)"]
clear["clear(): remove all<br/>used only by the compression rebuild"]
get["get_all(): read-only view<br/>recomputes and asserts the chain"]
end
push --> chain["parallel deque of hashes"]
pop --> chain
clear --> chain
chain --> verify["any in-place mutation breaks a link<br/>and asserts in a debug build"]
get --> verify
verify --> owner["single ownership, not locking<br/>one writer thread: everyone else reads snapshots"]
A pure stack: four operations, a hash chain, and single ownership instead of locking.
Prompt assembly
flowchart TB
ctx["sealed Context, read-only"] --> gate{"compression gate"}
gate -->|compressed| rebuild["context rebuilt<br/>clear + push"]
gate -->|kept| copy
rebuild --> copy["prompt copy<br/>the Context itself is never mutated"]
copy --> head["head blocks<br/>inserted right after the system prompt"]
copy --> tail["tail blocks<br/>appended after the conversation"]
head --> order["within each: ascending prompt_priority<br/>ties keep insertion order"]
tail --> order
order --> sources["sources: retrieved memories · skill metadata<br/>session brief · plugin PromptBlockCapability"]
sources --> omit["a block that renders empty is omitted"]
sources --> isolated["a plugin block that throws<br/>cannot break the turn"]
Injected blocks are assembled after the compression gate, never before it.
Error recovery
flowchart TD
each["evaluated every iteration"] --> tool{"3 or more identical<br/>tool call sets in a row?"}
tool -->|yes| stopTool["hard stop: loop detected"]
tool -->|no| text{"identical reply text<br/>repeating?"}
text -->|second time| steerText["inject a steering message"]
text -->|fifth time| stopText["hard stop"]
text -->|no| fail{"consecutive tool failures"}
fail -->|3| steerFail["inject a recovery steer"]
fail -->|6| stopFail["hard stop: tool calls kept failing"]
steerText --> hist["steer pushed to history_<br/>the model sees it on the next turn"]
steerFail --> hist
hist --> each
Three failure modes, two responses each: steer first, hard stop if it persists.
Wire
Provider wire path
flowchart TB
provider["provider plugin<br/>flavor + presets + optional dialect factory"] --> select["Config::flavor set at selection"]
select --> make["make_dialect(flavor)<br/>resolved exactly once per client"]
make -->|known flavor| dialect
make -->|unknown flavor| fallback["openai fallback<br/>a typo never breaks a session"]
fallback --> dialect["Dialect implementation<br/>endpoints · auth · body shape"]
dialect --> request["build request body and headers"]
request --> transport["HTTP transport via libcurl<br/>retry · timeout · cancel check"]
transport --> endpoint["OpenAI-compatible endpoint"]
endpoint --> decode["stream decoder: SSE framing"]
decode --> chunks["StreamChunk per event → AgentHooks"]
decode --> usage["TokenUsage mapping"]
decode --> errors["is_retryable classification<br/>typed ApiError"]
request --> normalize["tool calls normalized to the internal shape<br/>at the dialect edge, buffered and streamed alike"]
Provider plugin to endpoint, with the Dialect port resolving exactly once in the middle.
MCP session lifecycle
sequenceDiagram
autonumber
participant U as User
participant H as amber, the MCP host
participant T as Transport
participant S as MCP server
U->>H: /mcp connect
H->>T: open transport, stdio or streamable HTTP
T->>S: spawn the process, or POST
H->>S: initialize - protocol negotiation
S-->>H: server capabilities
H->>S: tools/list
S-->>H: tools, paginated
H->>S: resources/list, prompts/list
S-->>H: resources, prompts
Note over H,S: adapters map all three onto the Tool port
S-->>H: notifications/listChanged
U->>H: invoke a server tool
H->>S: tools/call
S-->>H: result
U->>H: disconnect
H->>S: shutdown
flowchart TD
call["tool call"] --> mode{"agent mode"}
mode -->|read| block["read mode blocks<br/>every non-read-only tool"]
mode -->|write or yolo| classify{"shell_classify<br/>bash only"}
classify -->|read-only, or benign in-workspace write| run["runs free"]
classify -->|destructive, or outside the workspace| policy
classify -->|not a bash call| policy{"stored or session rule?"}
policy -->|granted| run
policy -->|no rule| handler{"approval handler installed?"}
handler -->|no handler| deny["denied: fail-safe"]
handler -->|non-TTY with no --yes| deny
handler -->|TTY| dialog["approval dialog<br/>prompted once per scope: kind or folder"]
dialog -->|approve| grant["session grant recorded"]
grant --> run["tool executes<br/>own process group, stdin to /dev/null"]
dialog -->|deny| deny
block --> deny
Mode, policy and approval, ending in a fail-safe deny when no approver exists.
Path confinement
flowchart TD
input["path from the tool call"] --> empty{"empty?"}
empty -->|yes| reject
empty -->|no| root["resolve the root<br/>cached, then $AMBER_WORKSPACE, then cwd"]
root --> normalize["lexical normalisation<br/>no filesystem access during resolution"]
normalize --> escape{"escapes the root?<br/>dot-dot traversal or an absolute path outside"}
escape -->|yes| reject["rejected"]
escape -->|no| prefix{"slash-terminated prefix match<br/>/ws2 is not inside /ws"}
prefix -->|no| reject
prefix -->|yes| ok["confined absolute path returned"]
ok --> io["only now does any file I/O happen"]
Purely lexical, checked before any file I/O, with slash-terminated prefix matching.
Subsystems
Sub-agent execution
flowchart TB
task["task tool call"] --> mode{"execution mode"}
mode -->|serial| serial["one at a time<br/>sequential requests share the system-prompt prefix<br/>so provider prompt caches stay warm"]
mode -->|parallel| parallel["concurrent, capped by max()"]
serial --> build
parallel --> build["each sub-agent gets its own Context<br/>and a custom system prompt"]
build --> iters["max_iterations cap, default 20"]
iters --> result["result returned to the parent agent"]
Serial for cache warmth, parallel for throughput: each with its own Context.
Search backends
flowchart TD
query["search tool call<br/>mode argument"] --> pick{"mode"}
pick -->|semantic| sem["SemanticBackend<br/>dependency-free lexical index<br/>cached per root and glob"]
pick -->|anything else| grep["GrepBackend<br/>wraps grep -rnI, stateless"]
sem --> fresh["a backend is created fresh per execute()"]
grep --> fresh
fresh --> format["results formatted into the tool envelope"]
One port, two backends, and a grep fallback for anything unrecognised.
Memory extraction
flowchart TD
cycle["compression cycle"] --> response["CompressionResponse<br/>segments · memory_ops · skill_ops"]
response --> apply["apply_compression_result"]
apply --> mem["apply_memory_ops<br/>upsert, evidence starts at promote_threshold"]
apply --> skills["apply_skill_ops<br/>upsert or deprecate, with a trigger phrase"]
mem --> store
skills --> store["JsonMemoryStore"]
store --> dedup["dedup by content hash<br/>a repeat upsert just bumps evidence"]
store --> recall["MemoryRetriever builds the system-prompt suffix<br/>recall is injected into the prompt copy"]
Learning happens only inside the compression cycle, driven by the model’s judgment.
A third client outside the engine: observe, score, aggregate, report.
Test layers
flowchart TB
unit["1 · Unit<br/>a capability against a fake PluginServices"] --> integ
integ["2 · Hermetic integration<br/>PluginRuntime + fake LLM client<br/>no network, ever"] --> dialect
dialect["3 · Dialect, for providers<br/>pure request / parse / stream fixtures"] --> reg
reg["4 · Regression: make test<br/>contributions must not leak into other tests"]
guard["any test that activates a plugin redirects<br/>XDG_CONFIG_HOME to a scratch directory"] -.-> integ
The four layers a plugin contribution has to survive, and the redirection guard.