Skip to content

toolnexus.tool/success

Clojure (JVM) + cljgo · package net.clojars.muthuishere/toolnexus · SPEC §1 · clojure/src/toolnexus/tool.cljc

(toolnexus.tool/success output) ; => {:output "…" :isError false}
(toolnexus.tool/success output metadata) ; => {:output "…" :isError false :metadata {…}}
(toolnexus.tool/failure output) ; => {:output "…" :isError true}
(toolnexus.tool/failure output metadata) ; => {:output "…" :isError true :metadata {…}}

There is no ToolResult type in this port. A result is a plain map with two keys and an optional third, built by one of these two functions. output is stred on the way in — the model reads text, so (success 42) yields "42" rather than a number that each host would print differently. metadata is omitted from the map entirely when it is nil, so (success "hi") is exactly {:output "hi" :isError false} and nothing else.

They are named success and failure, not ok and err. cljgo’s clojure.core has ok and err; the JVM’s does not. Defining ok would shadow a core name on one host only — a hazard that fires on one runtime and not the other, which is precisely the class of drift this port exists to prevent. The suite asserts it: no public name in toolnexus.tool resolves in clojure.core, on either host.

failure is not an error-handling mechanism for your code. A tool error is a value handed back to the model, which is meant to read it and try something else. Throwing works too — toolnexus.tool/execute converts a throw into (failure (ex-message e)) — but then the model gets whatever the exception happened to say. Call failure when you have a message worth writing.

  • Every :execute you write by hand. toolnexus.tool/tool does not normalize return values; the map you hand back is the map the loop sees.
  • Telling the model it went wrong, in words you chose — a validation failure, a missing argument, a 404 from an upstream service.
  • Attaching out-of-band data via metadata. The loop never shows metadata to the model; it is read by the port itself (suspension) and by your own observability code.
Key Type Always present What it is
:output string yes The text the model sees. Non-strings are stred.
:isError boolean yes false from success, true from failure.
:metadata map no Present only when you passed one. {:pending Request} is the reserved §10 use.

1. Both results, and why output is always text

Section titled “1. Both results, and why output is always text”
(require '[toolnexus.tool :as tool])
(tool/success "hi") ;; => {:output "hi" :isError false}
(tool/failure "no such row") ;; => {:output "no such row" :isError true}
(:output (tool/success 42)) ;; => "42" — a number would print differently per host
(:metadata (tool/success "x" {:name "s"})) ;; => {:name "s"}
(contains? (tool/success "x") :metadata) ;; => false — omitted, not nil

2. A failure is a value the loop keeps going with

Section titled “2. A failure is a value the loop keeps going with”
(require '[toolnexus.tool :as tool])
(def lookup
(tool/tool {:name "lookup"
:description "Look a customer up by id"
:execute (fn [args]
(if-let [id (:id args)]
(tool/success (str "customer " id))
;; the model can read this and call again with an id
(tool/failure "missing required argument: id")))}))
(def tk (tool/toolkit [lookup]))
(tool/execute tk "lookup" {}) ;; => {:output "missing required argument: id" :isError true}
(tool/execute tk "lookup" {:id "42"}) ;; => {:output "customer 42" :isError false}

Nothing here throws, and nothing above the tool has to catch. That is the point of §0.8: the toolkit boundary converts every failure mode — a thrown exception, an unknown tool name, a dead MCP server — into this same two-key map.

SPEC §10 does not add a return type for “I need the human first”. A suspension is an ordinary failure result whose metadata holds a :pending Request, which is why every tool source can suspend without changing :execute’s signature.

(require '[toolnexus.tool :as tool]
'[toolnexus.client :as client])
(def approval-request
(client/make-request "input" "Approve the refund?" {}))
(def result (tool/failure "Waiting for a response." {:pending approval-request}))
;; => {:output "Waiting for a response." :isError true :metadata {:pending {...}}}
(client/pending-of result) ;; => the Request — this result IS a suspension
(client/pending-of (tool/success "done")) ;; => nil

The client loop branches on metadata.pending alone and never on :isError, so the flag stays true here (the call did not produce a usable answer) without the run being counted as a tool error — the tool observability event carries isError:false plus a pending marker so error-rate metrics and circuit breakers do not fire.