Skip to content

ToolResult

Python · package toolnexus · SPEC §1 · python/src/toolnexus/types.py

@dataclass
class ToolResult:
output: str
is_error: bool
metadata: Optional[dict[str, Any]] = None

What 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.

Every time you write a tool. It is the return type of Tool.execute, so you construct one on every code path — success, failure, and everything in between.

output is always a str — it is what the model reads. Serialize structured data yourself (json.dumps) rather than expecting the loop to do it, so you control exactly what the model sees.

from toolnexus import ToolResult
CONFIG = {"region": "eu-west-1"}
def read_config(key: str) -> ToolResult:
if key not in CONFIG:
# Recoverable: the model can read this and try another key.
return ToolResult(output=f"No such config key: {key}", is_error=True)
return ToolResult(output=CONFIG[key], is_error=False)
found = read_config("region")
assert found.output == "eu-west-1"
assert found.is_error is False
missing = read_config("nope")
assert missing.is_error is True
print("ok:", found.output, "|", missing.output)

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.

from toolnexus import ToolResult
def search(q: str) -> ToolResult:
hits = [
{"id": 1, "title": "Getting started"},
{"id": 2, "title": "Advanced usage"},
]
return ToolResult(
# The model reads this. Make it legible, not just valid.
output="\n".join(f"#{h['id']} {h['title']}" for h in hits),
is_error=False,
# Your code reads this. The model never sees it.
metadata={"title": f"search: {q}", "count": len(hits), "ids": [h["id"] for h in hits]},
)
res = search("usage")
assert res.metadata["count"] == 2
assert res.metadata["ids"] == [1, 2]
assert "Advanced usage" in res.output
print("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 Request means “this tool cannot finish until something out-of-band happens” — the loop parks the run instead of returning. You rarely write this by hand; pending builds it for you.

from toolnexus import pending, auth_required, pending_of
# pending() returns a ToolResult carrying metadata["pending"] = Request.
res = pending(kind="input", prompt="Which environment?")
assert res.is_error is True # a parked call is not a success
req = pending_of(res)
assert req is not None, "pending_of reads the suspension back off the result"
assert req.kind == "input"
assert req.prompt == "Which environment?"
assert req.id, "an id is generated as the correlation key"
# auth_required is sugar for the login case.
auth = auth_required("https://example.com/login")
assert pending_of(auth).kind == "authorization"
assert pending_of(auth).url == "https://example.com/login"
# An ordinary result has no suspension.
from toolnexus import ToolResult
assert pending_of(ToolResult(output="done", is_error=False)) is None
print("ok:", req.kind, "|", pending_of(auth).kind)
Field Type What it is
output str The text handed to the model. Always a string — serialize structured data yourself.
is_error bool Whether the call failed. Fed back to the model, not raised.
metadata dict[str, Any] | None Free-form, for your code. Reserved: pending holds a §10 Request.