Format Tool Output for a Model, Not for a Log File

A bare empty array means "none exist" and "we couldn't check" at the same time, and the agent will pick the confident reading. Six changes to what your handlers return, in order of payoff.

Tool implementations usually return whatever the underlying API returned, lightly reshaped. That output was designed for a program that knows the schema or a human reading a log. The model is neither, and the difference shows up as wrong answers that look like reasoning failures.

Tool output is input to the model. It deserves the same attention as the prompt.

Six changes, in order of payoff

1. Return fields, not blobs

A raw API response carries dozens of fields the task doesn't need. They consume context, they dilute the relevant values, and they invite the model to reason about internal flags it can't interpret.

Return the fields the task uses. {id, status, total, currency, placed_at} beats a forty-field object, every time, and it makes truncation far less likely to bite.

2. Make units and formats explicit

{"amount": 4200} — cents or dollars? {"created": 1756137600} — the model will convert an epoch, usually correctly, and "usually" is not a property you want in a money path.

{"amount": "42.00", "currency": "EUR", "created": "2026-08-25T14:00:00Z"}

Any ambiguity a field name doesn't resolve becomes a guess. Name the unit in the field (amount_eur, duration_seconds) or format the value so it's unambiguous.

3. Distinguish empty from failed from filtered

Three very different situations that commonly arrive looking identical:

[]                                              ← ambiguous
{"results": [], "reason": "no orders match the filter"}
{"results": [], "reason": "search index unavailable — result unknown"}
{"results": [], "reason": "3 matched but were excluded by permissions"}

⚠️ An agent that reads a bare [] as "nothing exists" produces confident wrong answers when the truth was "we couldn't check." This is one of the most damaging and cheapest-to-fix issues in tool design.

4. Write errors as instructions

An error is a turn of the conversation. Error: 500 gives the model nothing to act on, so it retries or invents.

Rate limited by the orders API. Retry after 30s, or narrow the
date range to reduce the result size. Do not retry immediately.

State what happened, whether to retry, and what alternative exists. ✅ The distinction between retryable and terminal matters most — it's the difference between an agent that routes around a dead dependency and one that hammers it.

5. Truncate deliberately, and say so

Silent truncation is worse than a smaller limit. If output must be cut, cut it in a way the model can see:

{"results": [ ...20 of 340... ],
 "truncated": true, "total": 340,
 "hint": "narrow with status= or placed_after= to see the rest"}

Truncating in the middle of a JSON structure — which is what a naive character limit does — is the worst outcome: the model receives something that looks parseable and isn't.

6. Prefer a compact table for repeated records

For lists of similar items, a compact tabular form is easier to scan and cheaper than repeated JSON keys:

id       status     total    placed
INV-201  overdue    412.00   2026-06-01
INV-202  paid       88.50    2026-06-03
INV-203  overdue    1250.00  2026-06-04

Fewer tokens than the equivalent JSON, and comparisons across rows are more reliable. Keep JSON where structure genuinely nests; use tables where records are flat and repetitive.

🔍 The test

Take a tool result your agent received recently. Read it as though you know nothing beyond what's on screen — no schema, no docs.

  • Can you tell what every field means, including units?
  • Can you tell whether this is all the data or some of it?
  • If it's empty, can you tell whether that means "none exist" or "couldn't check"?
  • If it's an error, do you know what to do next?

Any "no" is a bug in the tool, not in the model. Fixing it is usually a few lines in the handler and removes a whole class of failure.

What not to do

  • ❌ Pass the raw API response through "so the model has everything." Everything is noise, and noise is paid for on every subsequent turn.
  • ❌ Return prose descriptions of structured data. "I found three orders, the first of which…" is longer, less parseable, and loses the exact values.
  • ❌ Include internal identifiers the model can't use. If it can't be passed to another tool, it's context tax.

The takeaway

Every byte a tool returns enters the model's context and shapes the next decision. Return only the fields the task needs, make units and completeness explicit, distinguish empty from failed, and write errors as instructions about what to do next. It's a small amount of work in the handler that eliminates failures which are otherwise diagnosed — expensively, and wrongly — as the model being unreliable.

Keep reading

Similar posts

Matched on shared tags and category — the more bars, the stronger the overlap with what you just read.