Toolnexus.ToolResult
Elixir · package toolnexus · SPEC §1 · elixir/lib/toolnexus/types.ex
defmodule Toolnexus.ToolResult do defstruct output: "", is_error: false, parts: nil, metadata: nilendWhat every execute returns. Four fields, and the whole tool-calling loop is built on them:
output is the text handed back to the model, is_error says whether the call failed, parts
carries non-text output (an image, a PDF, an audio clip), and metadata is free-form — except for
one reserved key that turns a result into a suspension.
output stays required even when parts is present. It is what the transcript, compaction,
token estimation and any text-only provider see, so a tool returning an image sets output to a
description ("screenshot, 1280x720 png") and parts to the image. A result that sets no parts
is byte-identical to one from before parts existed.
Unlike Toolnexus.Tool, nothing here is enforced: every field has a default, so %ToolResult{} is
valid and means “empty success”.
When to use it
Section titled “When to use it”Every time you write a tool. It is the return type of
Toolnexus.Tool’s execute, so you construct one on every code path.
Why an error flag and not a raise
Section titled “Why an error flag and not a raise”output is always a String.t() — it is what the model reads. Serialize structured data yourself
(Jason.encode!/1) rather than expecting the loop to do it.
Examples
Section titled “Examples”1. Success and failure, with the shorthand constructors
Section titled “1. Success and failure, with the shorthand constructors”ok/1 and error/1 cover the two ordinary cases without writing the struct out.
alias Toolnexus.ToolResult
config = %{"region" => "eu-west-1"}
read_config = fn key -> case Map.fetch(config, key) do # Recoverable: the model can read this and try another key. :error -> ToolResult.error("No such config key: #{key}") {:ok, v} -> ToolResult.ok(v) endend
found = read_config.("region")true = found.output == "eu-west-1"false = found.is_error
missing = read_config.("nope")true = missing.is_error
IO.puts("ok: #{found.output} | #{missing.output}")2. Structured output and metadata
Section titled “2. Structured output and metadata”output must be a string, so serialize deliberately. metadata rides alongside for your code —
the model never sees it, which makes it the right place for bookkeeping.
alias Toolnexus.ToolResult
search = fn q -> hits = [%{id: 1, title: "Getting started"}, %{id: 2, title: "Advanced usage"}]
%ToolResult{ # The model reads this. Make it legible, not just valid. output: Enum.map_join(hits, "\n", fn h -> "##{h.id} #{h.title}" end), is_error: false, # Your code reads this. The model never sees it. metadata: %{title: "search: #{q}", count: length(hits), ids: Enum.map(hits, & &1.id)} }end
res = search.("usage")true = res.metadata.count == 2true = res.metadata.ids == [1, 2]true = String.contains?(res.output, "Advanced usage")
IO.puts("ok: #{res.metadata.title}")3. The reserved key — metadata.pending is a suspension
Section titled “3. The reserved key — metadata.pending is a suspension”metadata is free-form with one exception. A :pending key holding a %Toolnexus.Request{} means
“this tool cannot finish until something out-of-band happens” — the loop parks the run instead of
returning. ToolResult.pending?/1 is how you detect it.
alias Toolnexus.{ToolResult, Request}
# A suspension is a ToolResult whose metadata carries a Request.req = %Request{id: "pnd-1", kind: "input", prompt: "Which environment?"}res = %ToolResult{output: req.prompt, is_error: true, metadata: %{pending: req}}
# A parked call is not a success.true = res.is_errortrue = ToolResult.pending?(res)
# Read the request back off the result by matching the metadata.%{pending: %Request{} = got} = res.metadatatrue = got.kind == "input"true = got.prompt == "Which environment?"true = got.id == "pnd-1"
# The authorization shape carries a URL instead of expecting typed input.auth_req = %Request{id: "pnd-2", kind: "authorization", prompt: "Log in", url: "https://example.com/login"}auth = %ToolResult{output: auth_req.prompt, is_error: true, metadata: %{pending: auth_req}}true = ToolResult.pending?(auth)true = auth.metadata.pending.url == "https://example.com/login"
# An ordinary result has no suspension.false = ToolResult.pending?(ToolResult.ok("done"))
IO.puts("ok: #{got.kind} | #{auth_req.kind}")4. Non-text output — parts alongside output
Section titled “4. Non-text output — parts alongside output”A tool that produces an image still owes the model a sentence. output is what the transcript and
the compactor read; parts is what a vision-capable provider actually gets.
alias Toolnexus.{ContentPart, ToolResult}
repo = System.get_env("TOOLNEXUS_REPO") || "."fixture = Path.join(repo, "examples/media/fixture.png")
screenshot = fn -> case ContentPart.image(fixture) do # The non-bang form inside execute: an unreadable file is an outcome the model can # recover from, not a crash that ends the run. {:error, e} -> ToolResult.error("screenshot: #{Exception.message(e)}") {:ok, part} -> %ToolResult{output: ContentPart.summary(part), is_error: false, parts: [part]} endend
res = screenshot.()false = res.is_error# output is never empty just because the payload is binary.true = res.output == "image (image/png, 82 bytes)"true = length(res.parts) == 1true = hd(res.parts).mime_type == "image/png"
# parts cannot collide with a §10 suspension: pending?/1 matches on metadata only,# so a result carrying an image is still an ordinary, finished result.false = ToolResult.pending?(res)true = res.metadata == nil
IO.puts("ok: #{res.output}")Fields
Section titled “Fields”| Field | Type | Default | What it is |
|---|---|---|---|
output |
String.t() |
"" |
The text handed to the model. Serialize structured data yourself. |
is_error |
boolean() |
false |
Whether the call failed. Fed back to the model, not raised. |
parts |
[ContentPart.t()] | nil |
nil |
Non-text output — see Toolnexus.ContentPart. nil ⇒ byte-identical to a pre-parts result. |
metadata |
map() | nil |
nil |
Free-form, for your code. Reserved: :pending holds a §10 Request. |
Functions
Section titled “Functions”| Function | What it does |
|---|---|
ToolResult.ok/1 |
A success result with the given text. |
ToolResult.error/1 |
An error result with the given text. |
ToolResult.pending?/1 |
True when metadata.pending holds a %Request{} — i.e. this is a §10 suspension. |
See also
Section titled “See also”Toolnexus.Tool— what returns thisToolnexus.Context— whatexecutereceivesToolnexus.Request— the suspension request shapeToolnexus.ContentPart— whatpartsholds