AgentRuntime.resume — resume a suspended sub-agent
Clojure (JVM) + cljgo · package net.clojars.muthuishere/toolnexus · SPEC §7D · clojure/src/toolnexus/client.cljc
Route an Answer to the deepest suspended sub-agent handle and resume it in place.
What to use instead
Section titled “What to use instead”Resume the run yourself from the transcript. run hands back everything needed: when no
:wait-for is configured, a suspension ends the run cleanly with :status "pending", the
Request on :pending, and :messages holding every call up to and including the parked one.
Persist those two things, and when the human answers, start a new run whose :wait-for already
knows the answer.
(require '[toolnexus.client :as client])
;; --- pass 1: no :wait-for, so the run returns instead of blocking -------------(def llm (client/create-client {:base-url "https://api.anthropic.com" :style "anthropic" :model "claude-sonnet-4-5"}))
(let [r (client/run llm "Deploy release/1.4 to production" {:toolkit toolkit})] (when (= "pending" (:status r)) (save-job! "job-77" {:request (:pending r) ; id, kind, prompt, url, data :messages (:messages r)}) ; the transcript so far (ask-the-human! (:pending r))))
;; --- pass 2: minutes or days later, in a different process -------------------(defn resume-job! [job-id answer-data approved?] (let [{:keys [request messages]} (load-job! job-id) ;; a :wait-for that answers the ONE request this job is parked on resumed (client/create-client {:base-url "https://api.anthropic.com" :style "anthropic" :model "claude-sonnet-4-5" :wait-for (fn [req] (if (= (:id req) (:id request)) (client/make-answer (:id req) approved? answer-data (when-not approved? "declined by operator")) ;; a NEW suspension in this pass: park it again (client/make-answer (:id req) false nil "not this job's request")))})] (client/run resumed "Continue." {:toolkit toolkit :history messages})))Two things make this work rather than merely look like it works. The transcript in :messages is
the provider’s own message shape, so it goes straight back in as :history with no translation.
And the model re-issues the tool call on the new run, at which point the tool suspends again with a
fresh request id — which is why the :wait-for above compares against the saved request and
declines anything else rather than answering blindly.
The cost, stated plainly: the second pass replays the turn rather than continuing mid-turn, so you pay for the prompt again and any side effects your tools performed before the parked call are not repeated only because the transcript records them as done. Keep tools that run alongside a suspending one idempotent.
If the human is reachable from the same thread, none of this is necessary — configure
:wait-for on the first client and the loop blocks, resolves,
and re-executes the tool once, in place.
See also
Section titled “See also”toolnexus.client/suspend— Return a Pending from a tool to park the run until someone answers.toolnexus.client/auth-required— The auth-shaped suspension: hand back a URL, resume once the user has granted access.toolnexus.client/create-client— The single hook where the host resolves a suspension — in-process prompt or durable queue, same contract.toolnexus.client/pending-of— Detect that a RunResult is parked rather than finished, and get the Request that parked it.