A tool that searches a codebase, queries a database, or lists files has no upper bound on how much it could return — a search can match three lines or thirty thousand. Something has to decide how much actually comes back, and that decision gets made somewhere whether or not the tool's author made it on purpose. If the tool doesn't decide, the decision falls to whatever's downstream: a hard framework limit that cuts the response off mid-record, an agent that has to read and discard most of a huge payload just to find the two lines it needed, or a context window that fills with rows nobody asked for. None of those are decisions — they're what happens by default when nobody made one. Sizing tool output well means the tool itself decides how much to return, on purpose, before that output exists anywhere the agent has to reason about it.
Context window management is about what an agent does with tool output that's already landed in its context — what to keep, summarize, or let age out over a long session (see context window management for long-running agents). It starts from the assumption that the output already arrived at whatever size it arrived at. Tool output sizing is upstream of that: it's about the tool deciding, before any response is sent, how much to return in the first place — a decision made once per call, by the tool's own code, not something the agent has to manage after the fact. Tool error messages, in turn, are about a call that failed outright (see designing tool error messages agents can actually recover from); a tool returning ten thousand rows didn't fail — it succeeded at doing something unhelpful, which is a different problem with a different fix.
| Failure mode | Why it's worse than it looks |
|---|---|
| Cutting a response off at N characters/bytes with no marker | The agent has no way to know it saw a fragment, not the whole answer — a truncated JSON array can even still parse, just with fewer elements, so nothing looks wrong |
| Returning "the first N results" with no total count | The agent can't tell three results are all there are from three results being the first page of three thousand — both look identical |
| Offset-based pagination on a live, changing dataset | Rows inserted or deleted between page requests shift every subsequent offset, so an agent paging through can silently skip or repeat records without any call reporting an error |
| A hard framework-level response-size limit the tool author didn't know about | The truncation point is arbitrary relative to the data's structure — it can land mid-record, mid-line, or mid-field, corrupting the last item rather than just omitting later ones |
The pattern across all four: something got left out, and the response gives no signal that anything is missing. A response that says "here are 20 of 3,000, here's a cursor for more" is more useful than one that quietly hands back 20 and looks, structurally, exactly like a complete result would.
An unbounded tool call is a liability regardless of how rare huge results actually are, because the one time it's huge is the time it blows up whatever's downstream. The fix isn't to avoid limits — it's to make the limit visible in the response itself: a default page size, the total count (or a clearly labeled estimate when an exact count is expensive), and a cursor or token for continuing. An agent that gets back {"results": [...20 items], "total": 3841, "next_cursor": "..."} can decide whether 20 was enough, ask for more, or tell the user the search matched thousands and needs narrowing — three real options a silent 20-item array doesn't offer.
Offset-based paging (skip=40&limit=20) is simple to implement and wrong on any dataset that changes between calls — the meaning of "offset 40" shifts as rows are added or removed ahead of it. Cursor-based paging (after=<opaque-token-from-last-response>) anchors to a specific position in the data rather than a numeric offset, so it keeps working correctly even as the underlying data changes mid-iteration. This matters more for agent tools than for a human-facing UI, because an agent iterating pages in a loop has no human in front of the screen to notice a row looks repeated or missing — it will trust the pagination contract completely, so the contract has to actually hold.
It's tempting to have a tool summarize a huge result before returning it — condense a long log, collapse a large diff, roll up a big query into aggregates. That's the right call when the tool has information the agent doesn't yet have and summarizing doesn't lose anything the agent would have needed (a log tool returning error-level lines plus a count of suppressed info-level ones). It's the wrong call when the tool doesn't know what the agent is actually looking for — a diff tool that pre-collapses "unimportant" hunks based on a guess about what's unimportant can hide exactly the three lines that mattered for this particular task. When the tool can't tell what matters, the honest move is to return a bounded, navigable slice of the real data — not a summary that quietly substitutes the tool author's guess for the agent's judgment.
The test to apply: find a tool in the agent's kit with no documented upper bound on response size, and ask what actually happens on the day it matches everything — a 30,000-line grep, a full-table scan, a directory listing on the repo root. If the honest answer involves a framework timeout, an OOM, or a response that just gets cut off somewhere, that's an unmade sizing decision waiting to surface at the worst possible call.
Sizing what a tool returns is a narrower problem than describing what a tool does — see writing tool descriptions agents actually use correctly for the broader contract a tool makes with the agent calling it.