ToolResult
Python · package toolnexus · SPEC §1 · python/src/toolnexus/types.py
@dataclassclass ToolResult: output: str is_error: bool metadata: Optional[dict[str, Any]] = None parts: Optional[list[dict[str, Any]]] = NoneWhat 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,
metadata is free-form — except for one reserved key that turns a result into a suspension —
and parts carries non-text output (§1B ContentPart)
for a tool that returns an image, a document or an audio clip.
When to use it
Section titled “When to use it”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.
Why an error flag and not an exception
Section titled “Why an error flag and not an exception”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.
Why output stays required when there are parts
Section titled “Why output stays required when there are parts”Each entry in parts is a canonical part dict ({"type": …, "mimeType": …, "data": …}) — build
them with image/file/audio and to_dict, or write the
dict directly. Where the provider defines no shape for a part, a tool-derived part degrades to
a text placeholder with a warn-once rather than failing your run (§8A).
Examples
Section titled “Examples”1. Success and failure on the same tool
Section titled “1. Success and failure on the same tool”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)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.
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"] == 2assert res.metadata["ids"] == [1, 2]assert "Advanced usage" in res.output
print("ok:", res.metadata["title"])3. Non-text output — parts alongside output
Section titled “3. Non-text output — parts alongside output”from toolnexus import ToolResult, imagefrom toolnexus.content import to_dict
GOLDEN = open("examples/media/fixture.png.base64").read().strip()
def screenshot() -> ToolResult: part = image("examples/media/fixture.png") return ToolResult( # What the transcript, compaction and a text-only provider see. output=f"screenshot ({part.mimeType}, 82 bytes)", is_error=False, # What a vision model sees this turn. parts=[to_dict(part)], )
res = screenshot()assert res.output == "screenshot (image/png, 82 bytes)"assert res.is_error is Falseassert len(res.parts) == 1assert res.parts[0]["type"] == "image"assert res.parts[0]["mimeType"] == "image/png"assert res.parts[0]["data"] == GOLDENassert "path" not in res.parts[0] # a part never carries a path
# parts is optional: absent means a plain, pre-0.17-identical result.assert ToolResult(output="done", is_error=False).parts is None
print("ok:", res.output, "|", len(res.parts), "part")4. The reserved key — metadata["pending"] is a suspension
Section titled “4. 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 successreq = 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 ToolResultassert pending_of(ToolResult(output="done", is_error=False)) is None
print("ok:", req.kind, "|", pending_of(auth).kind)Fields
Section titled “Fields”| 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. |
parts |
list[dict[str, Any]] | None |
Non-text output — §1B ContentPart dicts. Absent ⇒ identical to a pre-0.17 result. |
See also
Section titled “See also”Tool— what returns thisToolContext— whatexecutereceivespending— build a suspending resultpending_of— read a suspension back off a resultContentPart— what goes inparts