Skip to content

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.

  • 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 by wake through 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 §10 Request (or a budget limit routed through "suspend"). The only way out is resume with the matching Answer — waking or posting to a suspended handle just buffers, it never moves the state.
  • closed — torn down by close. Still queryable: inspect still answers, wait still returns the last recorded result, and a fresh handle may be spawnned in its place from the same registry entry. Close ≠ loss.

(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 a resume, 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 walk spawn/wake also check — see Budget for why the carve alone is not enough to catch a sibling’s overspend.
  • :pending — the §10 Request this handle is parked on, when :state is "suspended"; nil otherwise.

(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.

  • Branching on a handle’s :state after 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 longer wait.
  • 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 — inspect on a closed handle still answers with its last state and result, which is exactly the guarantee that makes “close ≠ loss” true in practice.

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")
  • create-runtime — The six host verbs (spawn, post, wake, wait, interrupt, close) plus resume that create and drive a Handle, and the state machine transitions they trigger.
  • AgentDef — The registry entry a spawn call turns into a live Handle.
  • Budget — What :pool-tokens on inspect’s view actually measures, and why a live ancestor walk checks more than the local carve.