Toolnexus.ToolResult
Elixir · package toolnexus · SPEC §1 · elixir/lib/toolnexus/types.ex
defmodule Toolnexus.ToolResult do defstruct output: "", is_error: false, metadata: nilendWhat every execute returns. Three 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, and
metadata is free-form — except for one reserved key that turns a result into a suspension.
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}")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. |
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 shape