answer-output
Clojure (JVM) + cljgo · package net.clojars.muthuishere/toolnexus · SPEC §10 · clojure/src/toolnexus/client.cljc
(answer-output id output)(answer-output id output is-error)
;; => {:id id :ok true :data {:output "…"}};; => {:id id :ok true :data {:output "…" :isError true}} ; is-error true
;; a non-string output THROWS — it never silently degrades to ""(answer-output "sus-1" {:city "Chennai"});; => ExceptionInfo "toolnexus: Answer output must be a string"Wraps a human’s typed string reply into the Answer a suspended run resumes with — the success
counterpart to answer-declined. It exists so a host
building an Answer by hand never has a key to get wrong (ADR 0026): before this constructor
shipped, the recognised payload shape had to be reconstructed at every call site, and a mistyped
key there is silent — the loop would treat a malformed answer as an unrecognised one rather than
failing loudly.
output must be a string. A non-string argument throws immediately rather than being coerced
or blanked to "": degrading a wrong answer to an empty string is exactly how a bad payload used
to reach the model quietly, with no error and no log — the class of defect ADR 0026 exists to
close.
is-error (default false) sets :isError true in the payload, the same flag a
ToolResult uses to mark its own output as a failure — a host
resuming a tool that itself failed while the human was answering a different question can carry
that failure through in the same shape.
When to use it
Section titled “When to use it”- Resuming a
"input"-kind suspension where the human typed or picked something the tool needs — a city, a confirmation code, a chosen account — and you have it as a string in hand. - Inside a
:wait-forhook, to build the return value once you have resolved a human’s reply (from a chat message, a form submission, a durable queue) into a plain string. - Not for an
"authorization"suspension where the answer carries no meaningful payload — seeauth-required, whose retry arity typically checks(:ok answer)alone and never reads:data.
Why this and not the alternative
Section titled “Why this and not the alternative”Hand-building {:id id :ok true :data {:output s}} yourself works too — answer-output is not a
new mechanism, just the one call site that cannot typo a key or forget the string check. Prefer it
over hand-rolling the map for exactly the reason ADR 0026 exists.
Examples
Section titled “Examples”1. The smallest useful call — answer a plain question
Section titled “1. The smallest useful call — answer a plain question”(require '[toolnexus.client :as client])
(println)
(def a (client/answer-output "sus-1" "Chennai"));; => {:id "sus-1" :ok true :data {:output "Chennai"}}
(assert (= {:id "sus-1" :ok true :data {:output "Chennai"}} a))(println "OK")2. The realistic case — inside a :wait-for hook, resolving a queued human reply
Section titled “2. The realistic case — inside a :wait-for hook, resolving a queued human reply”(require '[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 (atom [{:calls [{:id "c1" :name "ask_city" :args {}}]} {:text "The weather in Chennai is sunny."}]))
(defn- mock-http [] (let [n (atom 0)] (fn [_url _headers _body] (let [i (swap! n inc) spec (get @script (dec i))] {:status 200 :headers {} :body (json/write-str (if (seq (:calls spec)) (calls-response (:calls spec)) (text-response (:text spec))))}))))
;; a suspending tool: no answer yet -> park; ctx carries the answer on retry(def ask-city-tool (tool/tool {:name "ask_city" :description "which city" :execute (fn ([_args] (client/suspend (client/make-request "input" "Which city?"))) ([_args ctx] (tool/success (str (get-in ctx [:answer :data :output])))))}))
(defn block-until-chat-reply! [_id] "Chennai")
(def llm (client/create-client {:base-url "http://mock.local" :style "openai" :model "gpt-4o-mini" :http-client (mock-http) :wait-for (fn [request] ;; blocks until a human answers via chat, then wraps the reply (let [reply (block-until-chat-reply! (:id request))] (client/answer-output (:id request) reply)))}))
(def rr (client/run llm "Which city should I check the weather for?" {:toolkit (tool/toolkit [ask-city-tool])}))
(assert (= "done" (:status rr)))(assert (= "The weather in Chennai is sunny." (:text rr)))(println "OK")3. The full surface — marking the resumed answer itself as an error result
Section titled “3. The full surface — marking the resumed answer itself as an error result”(require '[toolnexus.client :as client])
(println)
;; the tool being resumed will see :isError true on the answer's :data,;; so its retry arity can distinguish "granted, but the value failed validation";; from "granted, and the value is good"(def a (client/answer-output "sus-2" "not-a-real-city" true));; => {:id "sus-2" :ok true :data {:output "not-a-real-city" :isError true}}(assert (= {:id "sus-2" :ok true :data {:output "not-a-real-city" :isError true}} a))
;; a non-string output is refused rather than silently coerced(def refused (try (client/answer-output "sus-3" 42) (catch Throwable e (ex-message e))));; => "toolnexus: Answer output must be a string"(assert (= "toolnexus: Answer output must be a string" refused))(println "OK")See also
Section titled “See also”toolnexus.client/suspend— Return a Pending from a tool to park the run until someone answers.answer-declined— The refusal counterpart: wraps a human’s decline into anAnswercarrying a:reason.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.