Results the model can act on
Lesson 3 of 5 in Tools and Tool Use: The Contract Between Model and World.
Half the contract is what goes into a tool. The other half — the half almost everyone forgets — is what comes back out. A tool result is a prompt. It lands in the context window, the model reads every token of it, and whatever it says becomes the ground truth for the rest of the loop.
Two design rules follow. First, results should be compact and relevant: return the six fields the model needs, not the forty-field database row, the raw HTML page, or the full HTTP envelope. A bloated result taxes every later step — it crowds out earlier turns, buries the signal, and you pay for those tokens on every subsequent model call in the loop. Second — and this is where most agents quietly break — failure must come back as data the model can act on, not as an exception it can only apologise for.
Broken: throw an exception
The customer asks about order ORD-2026-999999, which does not exist. The tool throws, and the runtime dumps what the developer would want to see:
OrderNotFoundException: no row for id ORD-2026-999999
at OrderRepository.findById (repository.py:212)
at LookupOrderTool.run (tools.py:87)
The model reads this and has nowhere to go. It does not know if the ID format was wrong, the order is too old, or the service is down. So it does what models do with dead ends: retries the identical call, or gives up gracefully — “I apologise, I encountered an error while looking up your order.” The user learns nothing; the loop is over.
Fixed: return “no results” as data
Same situation, but the tool treats not found as a first-class result:
{
"status": "not_found",
"order_id": "ORD-2026-999999",
"hint": "No order has this ID. Order IDs appear in the confirmation email as ORD-YYYY-NNNNNN. If the customer cannot find it, use search_orders with their email address."
}
Now the model has a next move — in fact it has two, spelled out in the result itself. It can ask the customer to check the confirmation email, or pivot to search_orders. Same backend, same missing row; the difference is that one design ends the loop and the other steers it.
The empty case: zero results
Never return an empty string, null, or [] alone — the model cannot tell “nothing exists” from “the tool broke.” Say what was searched, confirm that zero matches is a valid answer, and suggest the best next move: broaden the query, try a different tool, or ask the user for more detail.
The error case: the operation failed
Classify it for the model: is this retryable (timeout, rate limit — say when), fixable (bad argument — say which one and what valid looks like), or terminal (no permission — say who to escalate to)? Each class implies a different next action, and only the result text can tell the model which class it is in.
The too-many case: 10,000 results
Truncation should be honest and steerable: return the top handful, state the total count, and say how to narrow (“1,214 matches; showing 5 most recent. Add a date range or customer email to narrow.”). Silent truncation makes the model reason confidently from a sample it believes is complete.
The verbose case: the result is technically correct but huge
Resist passing raw upstream responses through. Strip pagination metadata, internal UUIDs the model will never reuse, base64 blobs, and repeated boilerplate. A practical heuristic: read the result and ask, “which of these tokens change what the model does next?” Return those.
Key terms: tool result, context window, token, agent loop
Interactive checkpoint quiz (2 questions) — open this page in a browser to take it.