The Agent That Read Page 1 of 40: Pagination in Tool Design

It reported three overdue invoices. There were sixty-one. The tool worked as documented — which is exactly why this bug is so common and so quiet.

An agent asked to find all overdue invoices for a customer reported three. There were sixty-one. The tool returned the first page, the agent read three results and a next_cursor field it didn't act on, and reported what it had as though it were everything.

Nothing errored. The tool behaved exactly as documented. This is one of the most common silent wrong-answer bugs in tool design, and it has a small set of fixes.

Why agents mishandle pagination specifically

Human API consumers loop until the cursor is exhausted because a developer wrote that loop once, deliberately. An agent decides afresh, every call, whether to fetch more — and that decision competes with an unhelpful incentive: it already has results that look like an answer.

Three factors make it worse:

  • The result looks complete. Three invoices with amounts and dates is a plausible answer to "find overdue invoices." Nothing about the shape signals truncation.
  • The cursor is easy to overlook. It's one field among many in a JSON blob, and its significance is a convention the model must infer.
  • Continuing costs turns. Fetching forty pages means forty tool calls, and an agent operating under implicit or explicit pressure to be efficient will stop early.

Fix the response shape first

The highest-leverage change is making incompleteness impossible to miss.

❌ Easy to misread:

{ "invoices": [ ... 3 items ... ], "next_cursor": "eyJvZmZ..." }

✅ Hard to misread:

{ "invoices": [ ... 3 items ... ],
  "returned": 3,
  "total_matched": 61,
  "complete": false,
  "next_cursor": "eyJvZmZ...",
  "note": "INCOMPLETE: 58 more results. Call again with cursor to continue." }

The total_matched count is what makes the gap legible — three versus sixty-one is a difference the model acts on. The explicit complete: false and the plain-language note reinforce it. This is not redundancy for its own sake; each field catches a different failure.

Then reduce the need to paginate

Aggregate server-side. Most "find all X" questions are really "how many X" or "what's the total of X." A count_invoices(filter) or sum_amounts(filter) tool answers exactly, in one call, with no pagination at all. Look at what your agent does with the full list — if it's counting or summing, give it the count or the sum.

Make page sizes generous. Human-facing APIs default to ten or twenty for UI reasons that don't apply here. A tool for an agent can return a hundred or five hundred items if they're small. Fewer round trips means fewer chances to stop early — bounded by context, so return compact records rather than full objects.

Support filtering that eliminates paging. If the agent is paging to find a subset, expose that subset as a filter parameter. Sixty-one results narrowed to the four that matter is better than sixty-one paged results.

Offer a bounded "all" variant. list_all_invoices(filter, max: 500) that loops internally and returns either the complete set or an explicit truncated: true with the count. One call, one clear answer, and the looping logic lives in code where it's deterministic.

Handle the genuinely large case

Sometimes the result set is too big for context regardless.

  • Return a reference, not the data. Write the results somewhere and hand back a handle plus a summary: {result_id, total: 4211, sample: [...5 items], summary: {...}}. Then give the agent tools to query the handle — filter it, aggregate it, fetch a slice. The agent reasons over a large set without carrying it.
  • Make the tool refuse. If a query would return an unusable number of results, return an error asking for a narrower filter rather than a truncated page. An error redirects the agent; a truncated page misleads it.

🔍 Auditing for this bug

It's easy to check and easy to miss, because the wrong answers look reasonable.

  1. Find every tool that returns a list.
  2. For each, ask what happens when there are more results than the page holds. If the answer is "returns the first page with a cursor," it's exposed.
  3. Write one eval case per tool with more results than fit, asserting the final answer accounts for all of them.
  4. In production, log the ratio of total_matched to returned on every list call. Any call where the agent stopped with a large gap is a potential silent wrong answer — and this ratio is the single cheapest instrument for finding them.

The takeaway

Pagination was designed for clients with hand-written loops. An agent re-decides every time, and a plausible partial result is exactly what stops it. Make incompleteness loud in the response, prefer aggregates and generous page sizes over paging, and hand back a queryable reference when the data is genuinely large. Then "find all" means all.

Keep reading

Similar posts

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