Handle
Clojure (JVM) + cljgo · package net.clojars.muthuishere/toolnexus · SPEC §7D · clojure/src/toolnexus/agents/runtime.cljc:10
;; a Handle is NOT a value you construct — it is created by `spawn` and;; referenced everywhere else by its id string, e.g. "root/coordinator.1/explore.2"(require '[toolnexus.agents.runtime :as rt])
(def h (rt/spawn runtime rt/root "writer")) ; => "root/writer.1", a deterministic, parent-scoped id
;; the two read-only views built on the Handle(rt/handles runtime) ; => tree-order snapshot, every handle under root;; => [{:id "root/writer.1" :state "idle" :tokens 15 :inbox 0} ...]
(rt/inspect runtime h) ; => the full detail view for ONE handle;; => {:id "root/writer.1" :state "idle" :tokens 15 :inbox 0;; :turns 2 :pool-tokens 4985 :pending nil}“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”
(clojure/src/toolnexus/agents/runtime.cljc:10-13). A Handle is what every
create-runtime verb operates on — spawn creates one, wake/
wait/interrupt/close/resume act on one by id, and handles/inspect are the read-only
windows onto its current state.
The four states
Section titled “The four states”idle— alive, no turn in flight. A freshly spawned handle starts here; a handle that settles (with a"done","incomplete","interrupted", or"error"result) returns here, never to"closed"on its own.running— a turn is in flight, admitted bywakethrough gate 1 (bounded inbox), gate 2 (per-parent concurrency, FIFO-queued past the cap) and gate 3 (a tree-wide cap on LLM calls actually in flight).suspended— parked on a pending §10Request(or a budget limit routed through"suspend"). The only way out isresumewith the matchingAnswer— waking or posting to a suspended handle just buffers, it never moves the state.closed— torn down byclose. Still queryable:inspectstill answers,waitstill returns the last recorded result, and a fresh handle may bespawnned in its place from the same registry entry. Close ≠ loss.
What inspect returns
Section titled “What inspect returns”(rt/inspect runtime id) is the detail view of one handle — view (:id, :state, :tokens,
:inbox) plus three extra fields (clojure/src/toolnexus/agents/runtime.cljc:588-593):
:turns—:turns-total, the handle’s lifetime turn count (grows across aresume, never resets).:pool-tokens— the token allowance this handle’s OWN carved budget pool still holds. This is the handle’s local carve, not the live ancestor-chain walkspawn/wakealso check — seeBudgetfor why the carve alone is not enough to catch a sibling’s overspend.:pending— the §10Requestthis handle is parked on, when:stateis"suspended";nilotherwise.
(rt/handles runtime) is the coarser, tree-order sibling: every handle under a given root (default
rt/root), each rendered through the same view — :id, :state, :tokens, :inbox — with no
:turns/:pool-tokens/:pending. Reach for handles to enumerate a tree or build a dashboard;
reach for inspect when you already have one id and want its full detail.
When to use it
Section titled “When to use it”- Branching on a handle’s
:stateafter a verb call, e.g. deciding whether a"suspended"handle needs a host to answer its pending request, or a"running"one just needs a longerwait. - Building an observability view — a dashboard listing every live sub-agent, its token spend, and its inbox depth, without touching any verb that would change state.
- Confirming a handle survived
close—inspecton a closed handle still answers with its last state and result, which is exactly the guarantee that makes “close ≠ loss” true in practice.
Why this and not the alternative
Section titled “Why this and not the alternative”Examples
Section titled “Examples”1. The smallest useful call — read one handle’s state after spawning it
Section titled “1. The smallest useful call — read one handle’s state after spawning it”(require '[toolnexus.agents.runtime :as rt] '[koine.json :as json])
(println)
(defn- text-response [t] {:choices [{:message {:role "assistant" :content t}}] :usage {:prompt_tokens 10 :completion_tokens 5 :total_tokens 15}})
(def runtime (rt/create-runtime {:registry {"w" {:name "w" :does "writes" :model "wm"}} :llm {:base-url "http://mock.local" :model "default"} :http-client (fn [_url _headers _body] {:status 200 :headers {} :body (json/write-str (text-response "done"))})}))
(let [h (rt/spawn runtime rt/root "w")] (assert (= "idle" (:state (rt/inspect runtime h))))) ; => "idle" — a fresh handle is idle, not running(println "OK")2. The realistic case — watch a handle move through running to idle
Section titled “2. The realistic case — watch a handle move through running to idle”(require '[toolnexus.agents.runtime :as rt] '[koine.json :as json])
(println)
(defn- text-response [t] {:choices [{:message {:role "assistant" :content t}}] :usage {:prompt_tokens 10 :completion_tokens 5 :total_tokens 15}})
(def runtime (rt/create-runtime {:registry {"w" {:name "w" :does "writes" :model "wm"}} :llm {:base-url "http://mock.local" :model "default"} :http-client (fn [_url _headers _body] {:status 200 :headers {} :body (json/write-str (text-response "done"))})}))
(let [h (rt/spawn runtime rt/root "w")] (rt/wake runtime h "go") (let [r (rt/wait runtime h)] (assert (= "done" (:status r)))) (assert (= "idle" (:state (rt/inspect runtime h))))) ; a settled handle returns to idle, never "closed"(println "OK")3. The full surface — inspecting a suspended handle’s pending request, then a whole tree
Section titled “3. The full surface — inspecting a suspended handle’s pending request, then a whole tree”(require '[toolnexus.agents.runtime :as rt] '[toolnexus.client :as client] '[toolnexus.tool :as tool] '[koine.json :as json])
(println)
(defn- calls-response [calls] {:choices [{:message {:role "assistant" :content nil :tool_calls (mapv (fn [c] {:id (:id c) :type "function" :function {:name (:name c) :arguments (json/write-str (or (:args c) {}))}}) calls)}}] :usage {:prompt_tokens 10 :completion_tokens 5 :total_tokens 15}})
(defn- text-response [t] {:choices [{:message {:role "assistant" :content t}}] :usage {:prompt_tokens 10 :completion_tokens 5 :total_tokens 15}})
(def script {"bm" [{:calls [{:id "c1" :name "task" :args {:agent "worker" :prompt "risky"}}]}] "wm" [{:calls [{:id "w1" :name "approve" :args {}}]}]})
(defn- mock-http [] (let [counts (atom {})] (fn [_url _headers body] (let [b (json/read-str body) model (:model b) n (get (swap! counts update model (fnil inc 0)) model) spec (get (get script model) (dec n))] {:status 200 :headers {} :body (json/write-str (if (seq (:calls spec)) (calls-response (:calls spec)) (text-response "done")))}))))
(def approve-tool (tool/tool {:name "approve" :description "needs approval" :execute (fn ([_args] (client/suspend (client/make-request "approval" "Approve the impossible"))) ([_args ctx] (tool/success "approved")))}))
(def runtime (rt/create-runtime {:registry {"boss" {:name "boss" :does "coordinates" :model "bm" :team ["worker"] :budget {:max-depth 5}} "worker" {:name "worker" :does "does the risky part" :model "wm" :tools [approve-tool]}} :llm {:base-url "http://mock.local" :model "default"} :http-client (mock-http)}))
;; after a §10 approval request suspends the handle mid-turn:(let [boss (rt/spawn runtime rt/root "boss")] (rt/wake runtime boss "go") (rt/wait runtime boss) (let [worker (str boss "/worker.1") detail (rt/inspect runtime worker)] (assert (= "suspended" (:state detail))) (assert (= "approval" (:kind (:pending detail)))) ; {:kind "approval" :prompt "…" :id "…" :data {...}}
;; enumerate the whole subtree rooted AT boss — includes boss itself, then its descendants (let [hs (rt/handles runtime boss)] (assert (= [boss worker] (mapv :id hs))) (assert (= "suspended" (:state (second hs)))))))(println "OK")See also
Section titled “See also”create-runtime— The six host verbs (spawn,post,wake,wait,interrupt,close) plusresumethat create and drive aHandle, and the state machine transitions they trigger.AgentDef— The registry entry aspawncall turns into a liveHandle.Budget— What:pool-tokensoninspect’s view actually measures, and why a live ancestor walk checks more than the local carve.