Shaping tool results: the half of the tool contract nobody specifies.
A tool definition costs a few hundred tokens and you pay for it once per turn; a single unshaped tool result can cost forty thousand and you pay for it on every turn that follows. Teams spend weeks trimming schemas and then return whatever the upstream API happened to send — which is not only the largest line on the bill but the single largest untrusted text block in the context window. The result deserves a contract as strict as the arguments: a byte budget the harness enforces, a projection the tool author chooses, and a handle for everything that did not fit.
The asymmetry: schemas are bounded, results are not.
Both halves of a tool live in the same context window, and almost every optimisation effort goes to the half that cannot surprise you.
- A schema has a known worst case. You wrote it. It is 200–500 tokens, it does not change between runs, and it sits in the stable prefix where prompt caching makes it nearly free after the first call.
- A result has no worst case at all.
list_issuesreturns eleven items in your test fixture and 1,400 in the customer account that filed the ticket. The size is chosen by data you do not control, on a day you are not watching. - Results land after the cache boundary and are re-sent forever. Every tool result is appended to the transcript and re-submitted on each subsequent step. A 40,000-token payload fetched at step 3 is still being paid for at step 12 — the quadratic growth agent cost control describes, sourced almost entirely from results rather than definitions.
- The published wins are result-side wins. The Google Drive-to-Salesforce workflow that dropped from 150,000 tokens to 2,000 did so by keeping intermediate data out of context, as code as action covers. Anthropic's Tool Search Tool trims the definitions and reports around 85% savings on that half — real, and a different half.
- Long results degrade the model before they exhaust the window. Retrieval accuracy inside a long context falls well before the advertised limit, per effective vs advertised context. A 40k-token dump does not just cost money; it makes the model worse at using the 800 tokens that mattered.
Put the ratio somewhere you will see it: in a mature agent, tool definitions are typically single-digit percentages of tokens consumed and tool results are the majority. Optimisation attention is usually allocated the other way round.
The result is the most privileged untrusted text you will ever inject.
This is the reason shaping is not merely a cost exercise. Arguments flow from the model outward and are validated at the boundary; results flow inward, unvalidated, and arrive wearing the system's authority.
- Everything the model reads that it did not write comes through here. Issue bodies, web pages, file contents, email, CRM notes, another agent's output. Every documented indirect prompt-injection chain has a tool result somewhere in the middle — the mechanism prompt-injection defense starts from.
- Unshaped means unbounded attacker budget. If a tool returns a full document body, an attacker who controls that document controls an arbitrarily long span of your context. A 200-token projection of the same document caps what they can say inside your prompt at 200 tokens.
- The fields you did not need are the fields that hurt. API responses carry HTML descriptions, user-supplied labels, embedded URLs and free-text notes because programs ignore what they do not read. A model cannot ignore a field. It pays for it and it may act on it.
- Provenance has to survive the trip. Wrap returned content in an explicit, consistent delimiter and label it as data from a named source. It is not a defence on its own — the instruction hierarchy is advisory, not enforced — but a model asked to distinguish content from instruction cannot do it at all if the transcript does not mark the boundary.
- Shaping is a control you can measure. "Maximum attacker-controlled tokens per tool call" is a number you can compute per tool from its projection and budget, and it belongs in the same review as its permissions.
Four moves, in order of how much they pay and how rarely they are used.
These compose, and the ordering matters: projecting first means everything downstream operates on a smaller, cleaner object.
- Project — an explicit field allowlist, chosen by the tool author. Decide which fields a model could plausibly need to make the next decision and drop the rest at the boundary. This is the single largest win and the one almost nobody implements, because the default — hand back the upstream JSON — requires writing no code. A GitHub issue is roughly 40 fields; an agent triaging issues needs about six.
- Rank, then truncate. Truncation is only safe if the tail you drop is the least useful part, which requires ordering the collection before cutting it. Truncating an unsorted list is a coin flip on whether the answer survived.
- Paginate with a stable cursor. Offsets over a mutating collection silently duplicate and skip records across turns. Return an opaque cursor and say plainly how many remain, so continuing is a decision the model makes rather than a guess.
- Hand back a reference. Store the full payload outside the transcript, return a handle plus a short summary, and give the model a second tool that re-opens the handle with a query. This is the pattern that makes 40k-token artifacts tractable — the same asymmetry code as action exploits, available without a sandbox.
- Aggregate, when the model only needs the shape. "1,412 issues, 87% opened in the last 30 days, top three labels" answers "is this repository healthy" in thirty tokens. Returning rows to answer a question about counts is a category error the tool author is better placed to catch than the model.
The one-line test for a projection: could the model take the next action with this field removed? If yes, remove it — and if you find yourself unsure for most fields, the tool is doing too much and the fix is in tool granularity, not here.
Silent truncation is how shaping turns into a correctness bug.
Every failure below comes from the same root: the model was handed a partial answer that was formatted like a complete one, and nothing in the transcript said otherwise.
- Never cut mid-structure. Slicing a JSON string at a byte offset produces a fragment the model will still try to parse, and it will confabulate the closing shape. Truncate on record boundaries and re-serialise; if that is impossible, return the omission notice instead of the fragment.
- An unmarked cut is a lie about completeness. "Processed all matching records" is a claim a model will make from a truncated list, because a truncated list looks exactly like a short list. The marker has to be inside the result the model reads, not in a log line.
- Put the omission and the remedy in the same sentence.
"showing 20 of 1412; call again with cursor=… for the next page"tells the model what happened and what to do."[truncated]"tells it only that it has failed, which is the same defect error messages as prompts catalogues. - Budget the tool, not the request. One tool returning 30k tokens has consumed the whole step. Per-tool ceilings enforced by the harness beat per-tool discipline enforced by convention, because the tool that blows the budget is usually the one written last by someone who never read this page.
- Log what you dropped. When an eval regresses, "the result was truncated at 4k and the matching record was at index 63" is the entire diagnosis, and you can only make it if the span recorded the pre-shaping size and the cut point.
One envelope for every tool beats a clever shape per tool.
Consistency is worth more than per-tool optimality, because the model's recovery behaviour is learned from the shape of what it reads. A uniform envelope means one set of habits covers every tool you will ever add.
# Same five keys from every tool, always in this order. { "status": "partial", # ok | partial | empty | error "summary": "20 of 1412 open issues, sorted by last update", "data": [ /* projected records only — six fields, not forty */ ], "omitted": {"count": 1392, "reason": "result_budget"}, "next": "cursor:eyJvIjoyMH0" # null when nothing remains }
summaryis read first and is often read alone. Write it for a model that will act on that line without parsingdata, because on a long transcript that is exactly what happens.status: "empty"is notstatus: "error". Conflating them is why agents retry a correct query five times. Zero results is a finding; say so and stop.omittedmakes the invisible visible. The model can now reason about whether 1,392 unseen records could change its answer — a judgement it cannot make when the omission is unrecorded.- The envelope belongs to the harness. Enforce it in the layer that dispatches tool calls, so a new tool inherits budgets, markers and cursors without its author opting in. On the server side of MCP tool design the same rule applies, one layer further out.
- Prose beats JSON for anything the model only reads. Structured output earns its brackets when something parses it. If only the model consumes the result, a compact sentence carries the same information for fewer tokens and fewer chances to mis-parse.
Fitting it to the budget you actually have.
Shaping is a per-tool decision made against a whole-run budget, and the numbers should be written down rather than discovered in production.
- Start from the run, not the call. Decide what fraction of the window may be tool results at the end of a long run — a third is a defensible starting point — then divide by expected calls to get a per-call ceiling. Context budgeting is where this arithmetic lives.
- Measure before you tune. Record pre-shaping and post-shaping token counts per tool on every span. The two or three tools responsible for most of your bill will be obvious within a day, and they are rarely the ones anyone suspected.
- Shape before compaction, not instead of it. Compaction is a lossy recovery from having admitted too much; shaping stops it entering. Both are needed, and doing the first well makes the second rare.
- Re-shape when the model changes. A projection tuned for a model that tolerated terse fields can under-inform a different one. Budgets and projections are configuration, not constants, and they belong in the same review as prompts.
- Treat a new tool's result as a new attack surface. Before it ships, ask what an adversary who controls the upstream data can write into your context and how many tokens they get. If the answer is "the whole document", the projection is missing.
Do this in the order that pays: put one envelope and one harness-enforced per-tool ceiling in front of every tool this week, record pre- and post-shaping token counts on every span, then write explicit field allowlists for the two tools that turn out to dominate the bill. You will usually take the majority of your result tokens out on the first pass, and the same edit caps how much an attacker-controlled document can say inside your prompt. Arguments get a schema because the model is not trusted to produce them; results deserve one for exactly the same reason, in the other direction.
Related: schemas, contracts and defaults for the half of the contract that is already specified, tool-design anti-patterns for the shapes this replaces, context engineering for the wider budget, and context compaction for what you still need when shaping is not enough.