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 {…}}
(toolnexus.tool/with-parts result parts) ; => result + {:parts [ContentPart …]};; an EMPTY or absent parts list leaves the result byte-identical to what it wasThere is no ToolResult type in this port. A result is a plain map with two keys and two optional
ones, 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.
When to use it
Section titled “When to use it”- Every
:executeyou write by hand.toolnexus.tool/tooldoes 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.
- Returning an image, a PDF or an audio clip —
with-partsputs it under:parts, while:outputstays the text the transcript sees. - 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.
Why this and not the alternative
Section titled “Why this and not the alternative”What you get back
Section titled “What you get back”| 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. |
:parts |
vector | no | Non-text output — §1B ContentParts. Present only when with-parts was given a non-empty list. |
:metadata |
map | no | Present only when you passed one. {:pending Request} is the reserved §10 use. |
Examples
Section titled “Examples”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 nil2. 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.
3. Metadata carries a suspension
Section titled “3. Metadata carries a suspension”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")) ;; => nilThe 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.
4. Non-text output goes in :parts — and :output stays required
Section titled “4. Non-text output goes in :parts — and :output stays required”:output is not optional when a part is present: it is what the transcript, compaction, token
estimation and any text-only provider see. A tool returning an image sets :output to a
description and :parts to the image. with-parts uses cond->, so a part-free result is
byte-identical to what success / failure produced before :parts existed — nothing in an
existing text-only tool moves by a byte.
(require '[toolnexus.tool :as tool] '[toolnexus.content :as content] '[clojure.string :as str] '[koine.fs :as fs])
(defn find-up [rel] (->> (iterate (fn [p] (str "../" p)) rel) (take 8) (filter fs/exists?) first))
(def png (find-up "examples/media/fixture.png"))
(def shot (tool/tool {:name "screenshot" :description "Take a screenshot" :execute (fn [_] (let [p (content/image-file png)] ;; :output describes it; :parts carries it (tool/with-parts (tool/success (content/summarize-part p)) [p])))}))
(def r ((:execute shot) {}))(println "output:" (:output r))(assert (= "image (image/png, 82 bytes)" (:output r)))(assert (false? (:isError r)))(assert (= 1 (count (:parts r))))(assert (= "image/png" (:mimeType (first (:parts r)))))
;; An empty or absent parts list changes NOTHING — the compatibility story in one line.(assert (= {:output "hi" :isError false} (tool/with-parts (tool/success "hi") [])))(assert (= {:output "hi" :isError false} (tool/with-parts (tool/success "hi") nil)))(assert (false? (contains? (tool/with-parts (tool/success "hi") []) :parts)))
;; A failure carries parts too — a rendering tool that failed halfway still has the frame.(def bad (tool/with-parts (tool/failure "render timed out") [(content/text-part "partial")]))(assert (true? (:isError bad)))(assert (= 1 (count (:parts bad))))
(println "OK")See also
Section titled “See also”toolnexus.content/attach— build the parts:partscarriestoolnexus.tool/tool— the Tool whose:executereturns this map- The context argument — the optional third argument to
execute toolnexus.client/suspend— build the suspension result for youtoolnexus.client/pending-of— read the Request back out