Liking cljdoc? Tell your friends :D

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

agent-toolclj/s

(agent-tool rt def-name)

The axiom's other direction: an AgentDef AS a Tool, droppable into any toolkit's :tools.

{name, description: does, inputSchema:{prompt}, execute: run its loop} — returning ONLY the agent's final text plus {:agent :turns :total-tokens}. The caller sees a tool; what is behind it is a whole agent with its own soul, its own tool view and its own budget, and it cannot tell the difference. That is §7A/§7B's symmetry closed locally.

The axiom's other direction: an AgentDef AS a Tool, droppable into any
toolkit's `:tools`.

`{name, description: does, inputSchema:{prompt}, execute: run its loop}` —
returning ONLY the agent's final text plus `{:agent :turns :total-tokens}`.
The caller sees a tool; what is behind it is a whole agent with its own soul,
its own tool view and its own budget, and it cannot tell the difference. That
is §7A/§7B's symmetry closed locally.
sourceraw docstring

closeclj/s

(close rt id)
(close rt id {:keys [force reason] :as opts})

Graceful shutdown, LEAF-FIRST: stop accepting, close children first, let a running turn finish bounded by :shutdown-ms (then escalate to an abort), run :on-close, notify waiters, and KEEP THE FINAL STATE QUERYABLE.

close ≠ loss. inspect still answers, wait still returns the recorded result, and a successor may be spawned from the checkpoint. Stop-all is (close rt runtime/root).

{:force true} skips the grace period and aborts immediately.

Graceful shutdown, LEAF-FIRST: stop accepting, close children first, let a
running turn finish bounded by `:shutdown-ms` (then escalate to an abort), run
`:on-close`, notify waiters, and KEEP THE FINAL STATE QUERYABLE.

close ≠ loss. `inspect` still answers, `wait` still returns the recorded
result, and a successor may be spawned from the checkpoint. Stop-all is
`(close rt runtime/root)`.

`{:force true}` skips the grace period and aborts immediately.
sourceraw docstring

create-runtimeclj/s

(create-runtime opts)

Build a runtime.

:registry agent definitions by name — the task tool resolves targets here (REQUIRED for delegation) :llm {:base-url :style :model :api-key} for every handle's client; a def's own :model overrides :model unless it is "inherit" :http-client the LLM transport (fn [url headers body] response) — the hermetic-test seam, and the thing the turn gate wraps. Same shape as koine.http/post-json :clock the time source; default system-clock :store the ONE conversation store for every handle (conversation id = handle id); default in-memory :inbox-cap gate 1 — inbox capacity per handle (default 8) :max-concurrent-turns gate 3 — concurrent LLM calls tree-wide (default 8) :shutdown-ms graceful-close bound before close escalates to an interrupt (default 200) :hooks §8 lifecycle callbacks applied to EVERY agent, unless that agent's def sets its own. Forwarded verbatim :on-metric §8 observability sink, same resolution, resolved INDEPENDENTLY of :hooks :on-budget the optional §7D host budget callback, (fn [info] "stop"|"extend"|"suspend"). Consulted ONLY when a limit would stop a turn; absent ⇒ the limit always stops it, byte-identically. See budget-decision!

An AgentDef is a map:

{:name :does :soul :model :tools :team :budget :wait-for :on-spawn :on-close :hooks :on-metric}

:on-spawn is (fn [runtime handle-id]), :on-close is (fn [runtime handle-id reason]) and :wait-for is (fn [request] answer). A handle here is an id, so a lifecycle callback that wants to DO anything needs the runtime as well — hence two arguments where the other ports pass one object.

A Budget is a map of any of :max-turns :max-tokens :max-tool-calls :max-wall-ms :max-children :max-concurrent :max-depth. Money is deliberately absent — it is vendor data, and a host converts tokens to money itself.

Build a runtime.

  :registry             agent definitions by name — the `task` tool resolves
                        targets here (REQUIRED for delegation)
  :llm                  {:base-url :style :model :api-key} for every handle's
                        client; a def's own `:model` overrides `:model` unless
                        it is "inherit"
  :http-client          the LLM transport (fn [url headers body] response) —
                        the hermetic-test seam, and the thing the turn gate
                        wraps. Same shape as `koine.http/post-json`
  :clock                the time source; default `system-clock`
  :store                the ONE conversation store for every handle
                        (conversation id = handle id); default in-memory
  :inbox-cap            gate 1 — inbox capacity per handle (default 8)
  :max-concurrent-turns gate 3 — concurrent LLM calls tree-wide (default 8)
  :shutdown-ms          graceful-close bound before `close` escalates to an
                        interrupt (default 200)
  :hooks                §8 lifecycle callbacks applied to EVERY agent, unless
                        that agent's def sets its own. Forwarded verbatim
  :on-metric            §8 observability sink, same resolution, resolved
                        INDEPENDENTLY of `:hooks`
  :on-budget            the optional §7D host budget callback,
                        `(fn [info] "stop"|"extend"|"suspend")`. Consulted
                        ONLY when a limit would stop a turn; absent ⇒ the limit
                        always stops it, byte-identically. See `budget-decision!`

An AgentDef is a map:

  {:name :does :soul :model :tools :team :budget
   :wait-for :on-spawn :on-close :hooks :on-metric}

`:on-spawn` is `(fn [runtime handle-id])`, `:on-close` is
`(fn [runtime handle-id reason])` and `:wait-for` is `(fn [request] answer)`.
A handle here is an id, so a lifecycle callback that wants to DO anything
needs the runtime as well — hence two arguments where the other ports pass
one object.

A Budget is a map of any of `:max-turns :max-tokens :max-tool-calls
:max-wall-ms :max-children :max-concurrent :max-depth`. Money is deliberately
absent — it is vendor data, and a host converts tokens to money itself.
sourceraw docstring

execute-turn!clj/s

(execute-turn! rt id input one-shot-wait-for)

ONE turn: the handle's client runs the §8 loop over its conversation.

The caller has already admitted the handle and drained its inbox into input (both inside the verb's transaction), so what is left here is genuinely just the Run. Failures cross the handle boundary as RESULTS — never as exceptions — for the parent's model to judge.

On a durable pending the stored transcript is REWOUND to its pre-turn snapshot: a persisted §10 placeholder would make the resumed parent believe it already delegated, and skip re-invoking task. Idempotency for delegated work comes from task-key reattachment, not from reading a transcript.

ONE turn: the handle's client runs the §8 loop over its conversation.

The caller has already admitted the handle and drained its inbox into `input`
(both inside the verb's transaction), so what is left here is genuinely just
the Run. Failures cross the handle boundary as RESULTS — never as exceptions —
for the parent's model to judge.

On a durable pending the stored transcript is REWOUND to its pre-turn
snapshot: a persisted §10 placeholder would make the resumed parent believe it
already delegated, and skip re-invoking `task`. Idempotency for delegated work
comes from task-key reattachment, not from reading a transcript.
sourceraw docstring

gate-statsclj/s

(gate-stats rt)

Turn-gate observability: LLM calls in flight and the high-water mark. A fixture asserts gate 3 with :max-observed, which is the only honest way to test a limit — a test that never reaches the cap proves nothing.

Turn-gate observability: LLM calls in flight and the high-water mark. A
fixture asserts gate 3 with `:max-observed`, which is the only honest way to
test a limit — a test that never reaches the cap proves nothing.
sourceraw docstring

handlesclj/s

(handles rt)
(handles rt id)

Read-only snapshot of every handle in tree order, root excluded.

NOT named listclojure.core/list exists on both hosts and shadowing a core name is how a whole namespace gets rejected by cljgo's interop scan.

Read-only snapshot of every handle in tree order, root excluded.

NOT named `list` — `clojure.core/list` exists on both hosts and shadowing a
core name is how a whole namespace gets rejected by cljgo's interop scan.
sourceraw docstring

inspectclj/s

(inspect rt id)

Read-only detail view of one handle.

Read-only detail view of one handle.
sourceraw docstring

interruptclj/s

(interrupt rt id)

Abort the in-flight Run → idle, with the DRAINED INBOX ITEMS RESTORED. It is never a kill: the handle stays alive, its transcript intact, ready to be woken again. On a suspended handle it cancels the pending Request → idle — the operator's escape hatch from a suspension nobody is going to answer.

Waiters receive a uniform interrupted result, never an exception.

Landing is cooperative here (checked either side of each LLM round trip), so an interrupt takes effect at the next checkpoint rather than mid-socket. §7D says only abort LATENCY may differ between ports; the outcome does not.

Abort the in-flight Run → `idle`, with the DRAINED INBOX ITEMS RESTORED. It is
never a kill: the handle stays alive, its transcript intact, ready to be woken
again. On a `suspended` handle it cancels the pending Request → `idle` — the
operator's escape hatch from a suspension nobody is going to answer.

Waiters receive a uniform `interrupted` result, never an exception.

Landing is cooperative here (checked either side of each LLM round trip), so an
interrupt takes effect at the next checkpoint rather than mid-socket. §7D says
only abort LATENCY may differ between ports; the outcome does not.
sourceraw docstring

postclj/s

(post rt id item)

Append an item to a handle's inbox. NO state transition — an inbox item is data waiting for a turn, not a trigger.

Gate 1 is LOUD: at capacity the post is REJECTED SYNCHRONOUSLY to the sender. Silently dropping it (or growing without bound) are the two failure modes this gate exists to make impossible.

item = {:from "root/coordinator.1"|"external"|"clock" :channel "peer"|"timer"|"external" :text "…"}.

Returns {:ok true} or {:ok false :error "…"}.

Append an item to a handle's inbox. NO state transition — an inbox item is
data waiting for a turn, not a trigger.

Gate 1 is LOUD: at capacity the post is REJECTED SYNCHRONOUSLY to the sender.
Silently dropping it (or growing without bound) are the two failure modes this
gate exists to make impossible.

`item` = `{:from "root/coordinator.1"|"external"|"clock"
           :channel "peer"|"timer"|"external" :text "…"}`.

Returns `{:ok true}` or `{:ok false :error "…"}`.
sourceraw docstring

release-child-slot!clj/s

(release-child-slot! rt id)

Gate 2's other half: free this Run's slot and TRANSFER it to queued sibling wakes, FIFO, re-checking budgets at dequeue time (a wake queued five minutes ago may be over budget by the time its slot arrives).

Gate 2's other half: free this Run's slot and TRANSFER it to queued sibling
wakes, FIFO, re-checking budgets at dequeue time (a wake queued five minutes
ago may be over budget by the time its slot arrives).
sourceraw docstring

resumeclj/s

(resume rt answer)

Route an Answer to the DEEPEST suspended handle and cascade upward.

The deepest handle resumes from its checkpoint (turns and usage GROW, never reset), then each suspended parent re-runs; the parent's re-invoked task reattaches to the child that already resumed and never spawns a duplicate. Parked levels burn zero tokens while they wait.

Throws only when there is nothing suspended — the root is the one place §7D permits a throw to the host.

Route an Answer to the DEEPEST suspended handle and cascade upward.

The deepest handle resumes from its checkpoint (turns and usage GROW, never
reset), then each suspended parent re-runs; the parent's re-invoked `task`
reattaches to the child that already resumed and never spawns a duplicate.
Parked levels burn zero tokens while they wait.

Throws only when there is nothing suspended — the root is the one place §7D
permits a throw to the host.
sourceraw docstring

rootclj/s

The runtime root handle's id. close(root) is stop-all; spawn from it is how a host makes its first agent.

The runtime root handle's id. `close(root)` is stop-all; `spawn` from it is
how a host makes its first agent.
sourceraw docstring

run-agentclj/s

(run-agent rt def-name prompt)
(run-agent rt parent-id def-name prompt)

One-shot: spawn a handle for def-name, wake it with prompt, wait, close. The §7D Level-1 .run(prompt).

A pending result is NOT closed — a suspended handle still has an Answer coming, and closing it would discard the checkpoint that resume needs.

One-shot: spawn a handle for `def-name`, wake it with `prompt`, wait, close.
The §7D Level-1 `.run(prompt)`.

A `pending` result is NOT closed — a suspended handle still has an Answer
coming, and closing it would discard the checkpoint that `resume` needs.
sourceraw docstring

spawnclj/s

(spawn rt parent-id def-name)
(spawn rt parent-id def-name budget)

Create a child handle under parent-id with a DETERMINISTIC, parent-scoped id (root/coordinator.1/explore.2) — never random, so two runs of one fixture produce the same trace.

maxDepth, maxChildren and the live budget walk are all checked HERE. The id is returned to the spawner alone: handles are capabilities — post and wake what you hold, wait only on what you spawned.

Returns the child's id, or {:error "…"} (see verb-error?).

Create a child handle under `parent-id` with a DETERMINISTIC, parent-scoped id
(`root/coordinator.1/explore.2`) — never random, so two runs of one fixture
produce the same trace.

`maxDepth`, `maxChildren` and the live budget walk are all checked HERE. The id
is returned to the spawner alone: handles are capabilities — post and wake
what you hold, wait only on what you spawned.

Returns the child's id, or `{:error "…"}` (see `verb-error?`).
sourceraw docstring

statusesclj/s

The CLOSED result-status vocabulary (§7D). Identical strings in every port — a host that switches on these must never meet a seventh value.

done the turn produced a final answer pending a §10 durable suspension; resume with resume incomplete a §7D limit stopped the run, and the text NAMES the limit interrupted the turn was aborted; the handle is idle and alive closed the handle was closed timeout a wait deadline expired — the child KEEPS RUNNING error the run failed; failures cross a handle boundary as results

The CLOSED result-status vocabulary (§7D). Identical strings in every port —
a host that switches on these must never meet a seventh value.

  done         the turn produced a final answer
  pending      a §10 durable suspension; resume with `resume`
  incomplete   a §7D limit stopped the run, and the text NAMES the limit
  interrupted  the turn was aborted; the handle is idle and alive
  closed       the handle was closed
  timeout      a `wait` deadline expired — the child KEEPS RUNNING
  error        the run failed; failures cross a handle boundary as results
sourceraw docstring

system-clockclj/s

(system-clock)

The default clock: real wall time, real sleeps.

:set-timeout is run-async! + sleep! rather than a timer object, because a timer object is java.util.Timer on one host and a time.Timer on the other, and neither is reachable from portable Clojure. The thread is a daemon on the JVM and a goroutine on cljgo, so a pending timer never holds a consumer's process open.

The default clock: real wall time, real sleeps.

`:set-timeout` is `run-async!` + `sleep!` rather than a timer object, because a
timer object is `java.util.Timer` on one host and a `time.Timer` on the other,
and neither is reachable from portable Clojure. The thread is a daemon on the
JVM and a goroutine on cljgo, so a pending timer never holds a consumer's
process open.
sourceraw docstring

task-toolclj/s

(task-tool rt parent-id def-map)

task {agent, prompt} = spawn→wake→wait→close, fused into one tool call.

The child runs on a FRESH transcript and the parent gains exactly one tool message; the child's usage rolls up into the parent's. The description advertises ONLY the caller's team, sorted by name and composed from each agent's :does — an out-of-team target is an error that lists the team, never a silent reach into the registry.

A re-invoked call REATTACHES to the existing child by task key (agent+prompt): settled ⇒ its recorded result, suspended ⇒ its pending (or an inline resume when the retry carries the Answer), running ⇒ await. Reattachment — not transcript inspection, not a completion cache — is the required idempotency mechanism, and it is what makes an upward resume cascade safe.

`task {agent, prompt}` = spawn→wake→wait→close, fused into one tool call.

The child runs on a FRESH transcript and the parent gains exactly one tool
message; the child's usage rolls up into the parent's. The description
advertises ONLY the caller's team, sorted by name and composed from each
agent's `:does` — an out-of-team target is an error that lists the team, never
a silent reach into the registry.

A re-invoked call REATTACHES to the existing child by task key (agent+prompt):
settled ⇒ its recorded result, suspended ⇒ its pending (or an inline resume
when the retry carries the Answer), running ⇒ await. Reattachment — not
transcript inspection, not a completion cache — is the required idempotency
mechanism, and it is what makes an upward resume cascade safe.
sourceraw docstring

traceclj/s

(trace rt)

The transition trace — the §0 conformance artifact. Every state transition, every refused verb, every escalation, in the order they committed.

The transition trace — the §0 conformance artifact. Every state transition,
every refused verb, every escalation, in the order they committed.
sourceraw docstring

verb-error?clj/s

(verb-error? x)

True for the {:error "…"} shape a verb returns instead of a handle. Verbs return failures, they do not throw — only the root may throw to the host.

True for the `{:error "…"}` shape a verb returns instead of a handle. Verbs
return failures, they do not throw — only the root may throw to the host.
sourceraw docstring

virtual-clockclj/s

(virtual-clock)
(virtual-clock start-ms)

A clock whose time only moves when a test moves it.

Same two keys as system-clock, plus :advance!((:advance! c) 250) moves time forward and fires every timer that came due, in DEADLINE order (insertion order breaks ties), synchronously on the caller's thread. Deterministic by construction: nothing about a trace recorded under this clock depends on how fast a machine is.

A clock whose time only moves when a test moves it.

Same two keys as `system-clock`, plus `:advance!` — `((:advance! c) 250)` moves
time forward and fires every timer that came due, in DEADLINE order (insertion
order breaks ties), synchronously on the caller's thread. Deterministic by
construction: nothing about a trace recorded under this clock depends on how
fast a machine is.
sourceraw docstring

waitclj/s

(wait rt id)
(wait rt id {:keys [timeout-ms by]})

Block until this handle's NEXT result — or answer immediately with its LAST one when it is already settled (idle with a recorded result, suspended with its pending, closed). Registration order is unobservable.

:timeout-ms yields an explicit timeout result and the CHILD KEEPS RUNNING — a wait deadline is the waiter's deadline, never the child's. The timer goes through the injectable clock, so a fixture on a virtual clock produces the same trace every time.

:by enforces the capability rule: only the spawner may wait on a handle.

Block until this handle's NEXT result — or answer immediately with its LAST
one when it is already settled (idle with a recorded result, suspended with its
pending, closed). Registration order is unobservable.

`:timeout-ms` yields an explicit `timeout` result and the CHILD KEEPS RUNNING —
a wait deadline is the waiter's deadline, never the child's. The timer goes
through the injectable clock, so a fixture on a virtual clock produces the same
trace every time.

`:by` enforces the capability rule: only the spawner may wait on a handle.
sourceraw docstring

wakeclj/s

(wake rt id)
(wake rt id prompt)

idle → running. The turn's input is prompt plus the WHOLE drained inbox, coalesced into one block.

Admission is ATOMIC with the verb: the budget walk, the concurrency slot, the state flip and the drain all commit together. Over the parent's :max-concurrent the wake QUEUES FIFO and a completing sibling transfers its slot — a queued wake is deferred, never dropped.

Waking a suspended handle is a no-op: items buffer, and only the Answer to its pending Request may move it. Waking a running handle is a no-op too — the inbox drains on its next turn.

Returns {:ok true} or {:ok false :error "…"} and does NOT block; use wait for the result.

`idle → running`. The turn's input is `prompt` plus the WHOLE drained inbox,
coalesced into one block.

Admission is ATOMIC with the verb: the budget walk, the concurrency slot, the
state flip and the drain all commit together. Over the parent's
`:max-concurrent` the wake QUEUES FIFO and a completing sibling transfers its
slot — a queued wake is deferred, never dropped.

Waking a `suspended` handle is a no-op: items buffer, and only the Answer to
its pending Request may move it. Waking a `running` handle is a no-op too —
the inbox drains on its next turn.

Returns `{:ok true}` or `{:ok false :error "…"}` and does NOT block; use
`wait` for the result.
sourceraw 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