Context compaction (SPEC.md §7F) — keep a long-lived agent under budget.
A long-lived agent grows its transcript until it overflows the model's window.
compactor returns a §8 :before-llm hook that summarizes the older transcript
and keeps a recent tail. It rides the seam that already exists: the loop applies
a :before-llm message rewrite by REPLACING the working transcript, and that
vector flows on into the run result and the conversation store. So compaction is
a pure messages -> messages helper and adds no loop behavior — it is the
canonical use of the :before-llm hook.
Three invariants (SPEC §7F):
:max-tokens the hook is a NO-OP — it returns nil and the run is
byte-identical to a run with no compactor.user turn, so no
tool message is ever orphaned from the assistant carrying its
tool_call_id.Messages are the same maps toolnexus.client builds: :role, :content, and
where present :tool_calls / :tool_call_id.
(client/create {:hooks {:before-llm (compaction/compactor
{:max-tokens 120000
:summarize my-summarizer})}})
Context compaction (SPEC.md §7F) — keep a long-lived agent under budget.
A long-lived agent grows its transcript until it overflows the model's window.
`compactor` returns a §8 `:before-llm` hook that summarizes the older transcript
and keeps a recent tail. It rides the seam that already exists: the loop applies
a `:before-llm` message rewrite by REPLACING the working transcript, and that
vector flows on into the run result and the conversation store. So compaction is
a pure `messages -> messages` helper and adds no loop behavior — it is the
canonical use of the `:before-llm` hook.
Three invariants (SPEC §7F):
1. At or below `:max-tokens` the hook is a NO-OP — it returns nil and the run is
byte-identical to a run with no compactor.
2. Tool-pair safety — the retained tail always begins at a `user` turn, so no
`tool` message is ever orphaned from the `assistant` carrying its
`tool_call_id`.
3. The leading system prompt (identity / soul / skills) is preserved verbatim;
only the body between it and the tail is summarized.
Messages are the same maps `toolnexus.client` builds: `:role`, `:content`, and
where present `:tool_calls` / `:tool_call_id`.
(client/create {:hooks {:before-llm (compaction/compactor
{:max-tokens 120000
:summarize my-summarizer})}})Agent home — the persona surface (SPEC.md §7E).
A persona is an identity that lives in FILES, keeps durable MEMORY it can edit,
and runs on a HEARTBEAT so it can act unprompted. All three ride seams that
already ship: the composed soul is just a system prompt, the memory tool is a
plain Tool, and the heartbeat is post + wake on the §7D runtime's
INJECTABLE CLOCK. This layer adds no runtime behavior — it is a directory
convention plus composition, not new machinery.
(def ava (home/from-dir "./personas/ava"))
;; one-shot, or as a tool in someone else's toolkit
(def rt (rt/create-runtime {:registry {(:name ava) ava} :llm llm}))
(rt/run-agent rt (:name ava) "what is on my plate?")
(rt/agent-tool rt (:name ava))
;; …or give it its own clock
(def started (home/start-agent ava {:llm llm} {:every-ms 1800000
:on-beat println}))
((:stop started))
Everything here is also usable against a plain client with no runtime at all —
a composed soul is a :system-prompt, and the memory tool is a tool like any
other.
Agent home — the persona surface (SPEC.md §7E).
A persona is an identity that lives in FILES, keeps durable MEMORY it can edit,
and runs on a HEARTBEAT so it can act unprompted. All three ride seams that
already ship: the composed soul is just a system prompt, the memory tool is a
plain `Tool`, and the heartbeat is `post` + `wake` on the §7D runtime's
INJECTABLE CLOCK. This layer adds no runtime behavior — it is a directory
convention plus composition, not new machinery.
(def ava (home/from-dir "./personas/ava"))
;; one-shot, or as a tool in someone else's toolkit
(def rt (rt/create-runtime {:registry {(:name ava) ava} :llm llm}))
(rt/run-agent rt (:name ava) "what is on my plate?")
(rt/agent-tool rt (:name ava))
;; …or give it its own clock
(def started (home/start-agent ava {:llm llm} {:every-ms 1800000
:on-beat println}))
((:stop started))
Everything here is also usable against a plain client with no runtime at all —
a composed soul is a `:system-prompt`, and the memory tool is a tool like any
other.The agent runtime substrate — SPEC.md §7D.
One axiom: an Agent IS a Tool — (system prompt × a filtered toolkit view ×
the §8 loop), invocable, returning ONLY its final text plus
{:agent :turns :total-tokens}. Everything below is the machinery that makes
that safe to do recursively: a tree of handles, three loud backpressure gates,
hierarchical budgets, and §10 suspension that escalates one hop at a time.
A Handle is a live agent: a state machine (idle → running → idle|suspended| closed, and suspended → running ONLY via the Answer to its pending Request),
an inbox held as AGENT state (never a language mailbox), a carved budget, and a
deterministic parent-scoped id (root/coordinator.1/explore.2). The runtime
exposes exactly six host verbs — spawn post wake wait interrupt
close — plus the read-only handles / inspect views, and owns the
cross-cutting infrastructure: ONE conversation store for every handle
(conversation id = handle id, so transcripts genuinely survive turns), an
injectable clock, and the handle table.
SPEC pins TRANSITIONS, never scheduling. Conformance is identical per-handle
transition traces on a virtual clock — which is why trace is a first-class
return value here and why every timer goes through :clock.
(def rt (runtime/create-runtime
{:registry {"writer" {:name "writer" :does "writes" :soul "…"}}
:llm {:base-url "http://127.0.0.1:9999" :model "m"}}))
(def h (runtime/spawn rt runtime/root "writer"))
(runtime/wake rt h "draft the intro")
(:text (runtime/wait rt h))
THREE THINGS THIS PORT DOES DIFFERENTLY, all forced and all recorded:
root/writer.1;
the mutable tree lives in one atom. Every verb is then a compare-and-set
transaction over the whole table, which is how "admission is atomic with
the verb" is implemented rather than merely asserted. It also keeps the
capability rule honest: holding the string IS holding the capability.wait blocks; wake does not. §7D's contract is written in promises
because JS has no other choice. Clojure does: wake starts the turn on a
koine.process/run-async! thread and returns, wait derefs a promise. The
observable contract (next-or-last result, timeout leaves the child running)
is unchanged.interrupt
therefore lands between attempts, and the SPEC is explicit that only
abort LATENCY may differ: the outcome (idle + restored inbox + an
interrupted result to waiters) is identical.No future (a library may not hold its consumer's process open), no java.*,
no reader conditionals.
The agent runtime substrate — SPEC.md §7D.
One axiom: **an Agent IS a Tool** — (system prompt × a filtered toolkit view ×
the §8 loop), invocable, returning ONLY its final text plus
`{:agent :turns :total-tokens}`. Everything below is the machinery that makes
that safe to do recursively: a tree of handles, three loud backpressure gates,
hierarchical budgets, and §10 suspension that escalates one hop at a time.
A Handle is a live agent: a state machine (`idle → running → idle|suspended|
closed`, and `suspended → running` ONLY via the Answer to its pending Request),
an inbox held as AGENT state (never a language mailbox), a carved budget, and a
deterministic parent-scoped id (`root/coordinator.1/explore.2`). The runtime
exposes exactly six host verbs — `spawn` `post` `wake` `wait` `interrupt`
`close` — plus the read-only `handles` / `inspect` views, and owns the
cross-cutting infrastructure: ONE conversation store for every handle
(conversation id = handle id, so transcripts genuinely survive turns), an
injectable clock, and the handle table.
SPEC pins TRANSITIONS, never scheduling. Conformance is identical per-handle
transition traces on a virtual clock — which is why `trace` is a first-class
return value here and why every timer goes through `:clock`.
(def rt (runtime/create-runtime
{:registry {"writer" {:name "writer" :does "writes" :soul "…"}}
:llm {:base-url "http://127.0.0.1:9999" :model "m"}}))
(def h (runtime/spawn rt runtime/root "writer"))
(runtime/wake rt h "draft the intro")
(:text (runtime/wait rt h))
THREE THINGS THIS PORT DOES DIFFERENTLY, all forced and all recorded:
1. **Handles are ids, not objects.** A handle is the string `root/writer.1`;
the mutable tree lives in one atom. Every verb is then a compare-and-set
transaction over the whole table, which is how "admission is atomic with
the verb" is *implemented* rather than merely asserted. It also keeps the
capability rule honest: holding the string IS holding the capability.
2. **`wait` blocks; `wake` does not.** §7D's contract is written in promises
because JS has no other choice. Clojure does: `wake` starts the turn on a
`koine.process/run-async!` thread and returns, `wait` derefs a promise. The
observable contract (next-or-last result, timeout leaves the child running)
is unchanged.
3. **Cancellation is cooperative**, checked either side of every LLM round
trip — the same tier §7D's table gives python and java. `interrupt`
therefore lands *between* attempts, and the SPEC is explicit that only
abort LATENCY may differ: the outcome (`idle` + restored inbox + an
`interrupted` result to waiters) is identical.
No `future` (a library may not hold its consumer's process open), no java.*,
no reader conditionals.cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |