answer-declined
Clojure (JVM) + cljgo · package net.clojars.muthuishere/toolnexus · SPEC §10 · clojure/src/toolnexus/client.cljc
(answer-declined id) ; :reason defaults to "declined"(answer-declined id reason)
;; => {:id id :ok false} ; no reason given;; => {:id id :ok false :reason "no today"} ; reason givenWraps a human’s refusal into the Answer a suspended run resumes with, carrying a reason string
that defaults to "declined". It is the decline counterpart to
answer-output, and both are canonical, shipped in all
seven ports (addendum A9): before these constructors existed, a host hand-built the Answer map
and a mistyped :ok/:reason key was a silent wrong-outcome bug rather than a construction error.
:reason is carried only on a decline (R1) — answer-output never sets it, because the loop’s
resume rule branches on :ok alone and never reads :reason to decide anything; it exists purely
so the model (and any log line) can see why the human said no.
When to use it
Section titled “When to use it”- The human explicitly refuses a request — “no, don’t do that”, “cancel”, a rejected confirmation.
- A suspension times out unanswered and the host decides to resolve it as a decline rather than
leave the run parked forever — pass a reason like
"expired"or"no reply within 5m". - A
:wait-forhook that cannot route the request at all (an unrecognised:kind, a channel that is down) should decline with a reason naming the problem, rather than throw or silently grant — the run keeps going and the model sees why the tool call was blocked.
Why this and not the alternative
Section titled “Why this and not the alternative”A declined answer is not a crash: the loop turns it into an error ToolResult fed back to the
model ("declined/expired: <prompt>"), and the run continues — the model can explain the situation
to the user instead of the whole run dying on a refusal.
Examples
Section titled “Examples”1. The smallest useful call — the default reason
Section titled “1. The smallest useful call — the default reason”(require '[toolnexus.client :as client])
(println)
(def a (client/answer-declined "sus-1"));; => {:id "sus-1" :ok false}
(assert (= {:id "sus-1" :ok false} a))(println "OK")Note there is no :reason key at all here — make-answer only assigns it when one is given, so a
bare decline is not {:reason "declined"} on the wire; it is simply :ok false with no reason.
2. The realistic case — a human explicitly says no, inside :wait-for
Section titled “2. The realistic case — a human explicitly says no, inside :wait-for”(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 "confirm_delete" :args {}}]} {:text "OK, I left the branches alone."}]))
(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))))}))))
(def confirm-delete-tool (tool/tool {:name "confirm_delete" :description "confirm branch deletion" :execute (fn ([_args] (client/suspend (client/make-request "approval" "Delete the stale branches?"))) ([_args ctx] (if (get-in ctx [:answer :ok]) (tool/success "deleted") (tool/failure (str "declined: " (get-in ctx [:answer :reason]))))))}))
(defn ask-human! [_prompt] {:approved? false})
(def llm (client/create-client {:base-url "http://mock.local" :style "openai" :model "gpt-4o-mini" :http-client (mock-http) :wait-for (fn [request] (let [reply (ask-human! (:prompt request))] (if (:approved? reply) (client/answer-output (:id request) (:value reply)) (client/answer-declined (:id request) "not today"))))}))
(def rr (client/run llm "Should I delete the stale branches?" {:toolkit (tool/toolkit [confirm-delete-tool])}))
(assert (= "done" (:status rr)))(assert (= "OK, I left the branches alone." (:text rr)))(println "OK")3. The full surface — an unroutable request, declined with a diagnostic reason
Section titled “3. The full surface — an unroutable request, declined with a diagnostic reason”(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 "mystery" :args {}}]} {:text "I could not route that request."}]))
(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))))}))))
(defn route-to-oauth! [_req] nil)(defn route-to-form! [_req] nil)
;; a suspending tool whose :kind is not one the host recognises(def mystery-tool (tool/tool {:name "mystery" :description "an unroutable kind" :execute (fn ([_args] (client/suspend (client/make-request "carrier-pigeon" "deliver a message"))) ([_args ctx] (tool/success "n/a")))}))
(def llm (client/create-client {:base-url "http://mock.local" :style "openai" :model "gpt-4o-mini" :http-client (mock-http) :wait-for (fn [request] (case (:kind request) "authorization" (route-to-oauth! request) "input" (route-to-form! request) ;; an unknown kind is declined, not thrown — the run keeps going (client/answer-declined (:id request) (str "unsupported suspension kind: " (:kind request)))))}))
(def rr (client/run llm "do the thing" {:toolkit (tool/toolkit [mystery-tool])}))(assert (= "done" (:status rr)))(assert (= "I could not route that request." (:text rr)))
;; a string-keyed Answer a host round-trips through JSON is normalised the same;; way regardless of which constructor built it(assert (= false (:ok (client/as-answer {"id" "x" "ok" false "reason" "no"}))))(println "OK")A string-keyed Answer a host round-trips through JSON is normalised the same way regardless of
which constructor built it — (client/as-answer {"id" "x" "ok" false "reason" "no"}) reads
:ok false correctly, which is exactly the bug (#89, ADR 0026 §5) that made a granted answer look
declined before normalisation existed.
See also
Section titled “See also”toolnexus.client/suspend— Return a Pending from a tool to park the run until someone answers.answer-output— The success counterpart: wraps a human’s typed string reply into anAnswer.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.