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
parts: Optional[list[dict[str, Any]]] = None

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

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.

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

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. Non-text output — parts alongside output

Section titled “3. Non-text output — parts alongside output”
from toolnexus import ToolResult, image
from 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 False
assert len(res.parts) == 1
assert res.parts[0]["type"] == "image"
assert res.parts[0]["mimeType"] == "image/png"
assert res.parts[0]["data"] == GOLDEN
assert "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 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.
parts list[dict[str, Any]] | None Non-text output — §1B ContentPart dicts. Absent ⇒ identical to a pre-0.17 result.