Liking cljdoc? Tell your friends :D

toolnexus.agents.compaction

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})}})
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})}})
raw docstring

toolnexus.agents.home

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.
raw docstring

toolnexus.agents.runtime

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.

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.
raw docstring

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close