Agent and AI engineering · Practical technical guide · 17 October 2026
A tool-result contract for agents: why raw tool output is the biggest context cost, how to enforce budgets in the tool runtime, return deterministic summaries with artifact handles, compact old results, and test that nothing is silently truncated.
The quiet cost of a large tool result
In most agent transcripts the largest blocks of text are not the instructions or the user's question. They are tool results: a JSON list of orders, a log excerpt, a web page, a query result. A result is read once by the model on the turn it arrives and then, in a typical loop that resends the conversation, read again on every later turn. A 12,000-token result returned on turn 2 of a 10-turn run is processed nine times, about 108,000 input tokens, unless prompt caching absorbs part of it.
Size also changes behaviour. Long results push instructions and earlier facts away from where the model attends most reliably, and a runtime that silently cuts a result at a byte limit can make a record look absent. Agent tool use architecture lists the basic shaping rules; this article turns them into a contract, an enforcement point and tests.
A result contract instead of a payload
Define one envelope that every tool result follows. The model sees a small, predictable structure; the full data lives elsewhere and is reachable through a handle.
| Field | Purpose |
|---|---|
| summary | Deterministic facts computed by the tool: counts, totals, ranges, top items |
| items | Projected records with only the fields the operation is about, capped in number |
| total | How many records matched, so the model knows what it is not seeing |
| truncated | True when items is shorter than total or a field was cut |
| handle | Reference to the full result in an artifact store, scoped to the run and user |
| next | A continuation instruction: which tool to call with which arguments for more |
MCP's tool results already support this shape: a tool may return structuredContent validated against an outputSchema, and it may return content items of type resource_link that point to a resource instead of embedding it. ADK and other frameworks have artifact services for the same purpose. The contract matters more than the transport.
Enforce budgets in the runtime, not in each tool
If every tool author decides their own limits, one forgets. Put the budget in the layer that calls tools and appends results to the conversation. It knows the model's context size, the current transcript size and the per-tool limits. It projects fields, caps items, writes the full payload to the artifact store, and computes the summary. Tools return raw data to the runtime; only the shaped envelope reaches the model.
Illustrative python, checked for syntax; adapt names and storage to your system.
import json, uuid
def estimate_tokens(text: str) -> int:
# Rough planning estimate; use the provider's token counter for billing decisions.
return max(1, len(text) // 4)
def shape_result(rows, fields, budget_tokens, store, scope, sort_key=None):
"""Project, summarize and cap a list result. Full rows go to the artifact store."""
ordered = sorted(rows, key=sort_key) if sort_key else list(rows)
handle = f"art-{uuid.uuid4().hex[:12]}"
store[handle] = {"scope": scope, "rows": ordered}
summary = {"count": len(ordered)}
if ordered and "amount" in ordered[0]:
amounts = [r["amount"] for r in ordered]
summary.update(total_amount=sum(amounts), max_amount=max(amounts))
items = []
envelope = {"summary": summary, "items": items, "total": len(ordered),
"truncated": False, "handle": handle}
for row in ordered:
candidate = {f: row.get(f) for f in fields}
if estimate_tokens(json.dumps({**envelope, "items": items + [candidate]})) > budget_tokens:
envelope["truncated"] = True
break
items.append(candidate)
if envelope["truncated"]:
envelope["next"] = (f"Call read_artifact(handle='{handle}', offset={len(items)}) "
"or narrow the query with a filter.")
return envelope
def read_artifact(store, handle, scope, offset=0, limit=20, fields=None):
entry = store.get(handle)
if entry is None or entry["scope"] != scope: # handles are not capabilities
raise PermissionError("unknown handle for this run")
rows = entry["rows"][offset:offset + limit]
return [{f: r.get(f) for f in fields} for r in rows] if fields else rowsTwo choices in that sketch are deliberate. The summary is computed by code, so totals and maxima are exact even when only the first items are shown; an outlier on page 40 still shows up as the maximum. And the handle check compares the stored scope with the caller's run and user, because a handle that any caller can redeem is a data leak waiting for a prompt injection to ask for it.
Summaries: deterministic first
It is tempting to have a cheaper model summarise every large result. That adds a second source of error the agent cannot see. Prefer summaries that code can compute exactly: counts by status, sums, minimum and maximum, the newest and oldest timestamps, the top five by a business-relevant key, the distinct values of a categorical field. Reserve model-written summaries for unstructured text such as a long document, and when you use them, keep the handle so the agent or a reviewer can check the source.
Compacting older results
Budgets per result are not enough for long runs; the transcript as a whole grows. A second rule replaces tool results older than a few turns with a one-line stub that keeps the summary and the handle: "orders query, 312 matches, total 48,210, handle art-9f2c; full data available via read_artifact". The model keeps the facts it needs to reason and can re-open the data if a later step depends on details. Make compaction deterministic and log what was replaced, so a replay of the run reproduces the same transcript. Context engineering architecture covers the wider budget across instructions, memory and retrieval.
Worked example: finding a refund on page three
A support agent calls list_orders(customer_id) for a customer with 312 orders. The raw response is about 90 KB of JSON. The runtime projects four fields (id, date, status, amount), sorts by date descending, and fits the newest few dozen items into a 1,500-token budget. The envelope reports total: 312, truncated: true, a summary with counts by status including "refund_pending: 1", and a next instruction.
The user asked why a refund from March has not arrived. The first page covers only recent months, but the summary shows one pending refund exists, so the agent calls list_orders again with a status filter instead of paging through 300 rows. Without the summary and the truncated flag, the likely outcome is a confident "I can't find a refund for this customer".
Failure modes
- Silent truncation. A byte cut in the middle of JSON or a list shortened without a flag makes data look absent. Always set the flag and say what was omitted.
- Fabricated unseen pages. Models sometimes describe records they never received. The total and the explicit continuation reduce this; evaluations should include questions whose answer is only off the first page.
- Redeemable handles across users. Scope every handle to the run and principal and check it on read.
- Unstable ordering. If the order changes between calls, offsets skip or repeat rows. Sort on a stable key and snapshot the full result behind the handle.
- Budgets that ignore tokenizers. Character-based estimates drift for code, non-Latin scripts and numbers. Calibrate the estimate against the provider token counter for your data.
How to verify
- Unit tests that every tool result passes through the shaper and that no envelope exceeds its budget.
- A test that the summary of a truncated result equals the summary computed over the full data.
- An evaluation set where the correct answer requires a filter or a second page, scored on answer correctness.
- A security test that a handle from one run is rejected in another.
- A transcript-size metric per run, alerting when a tool starts returning larger envelopes after a schema change.
Practice and related reading
Further reading:
Sources and scope
Checked against these sources on 17 October 2026. Examples and figures are illustrative, not measurements.