# API result truncation: stop agents making bad changes

Agents do not need a malicious tool to make a damaging change. A tool that quietly returns only part of its answer is enough. Give an agent a search result that looks complete, ask it to remove what the search did not find, and it will often produce a perfectly logical mistake from false premises.

The fix is not a longer system prompt telling the agent to be careful. The tool must state, in a stable machine-readable form, whether it completed the requested work, what it omitted, why it omitted it, and how the caller can continue. If the tool cannot make that statement, the agent must not use absence in the result as permission for a broad change.

## Partial data and empty data are different claims

An empty result says that the tool found no matching items within the scope it actually examined. A complete empty result says that the tool examined the full requested scope and found nothing. Those are different claims, and most API contracts collapse them into the same `[]`.

That collapse causes a specific bad inference:

1. The agent asks for all service accounts without a current owner.
2. The API returns an empty array after scanning its first page, stopping at a result cap, or omitting records the token cannot read.
3. The agent concludes that every account has an owner.
4. It changes a related control, report, or cleanup job based on that conclusion.

The agent did not need to misunderstand English. The tool gave it an answer whose shape implied more than the server knew.

A tool should distinguish at least four states. A complete query can return items. A complete query can return no items. An incomplete query can return some items. An incomplete query can return no items. Treating only the third state as noteworthy misses the most dangerous case: an empty response that persuades an agent a problem does not exist.

Permissions make this worse. Many services deliberately hide inaccessible objects by returning an empty collection or a filtered collection rather than a permission error. That behavior may be reasonable for a human-facing interface. It is unacceptable evidence for an autonomous cleanup task unless the API says exactly what visibility the caller had.

Do not use prose such as `Some results may be missing` as the contract. It gives an agent no reliable branch to take, and it gives an engineer no testable condition. A field named `complete` with a boolean value looks boring. Boring is the point.

## A successful HTTP response can still be an incomplete answer

HTTP status codes describe the exchange between client and server. They do not, by themselves, prove that a search, inventory, or export covered the requested domain.

RFC 9110 defines the semantics of HTTP status codes. A `200 OK` says the request succeeded according to the method's semantics. It does not say that a search covered every page, every partition, every permission domain, or every record before a deadline. Teams often read more into `200` than the protocol promises.

Consider this response:

```json
HTTP/1.1 200 OK
Content-Type: application/json

{
  "items": [],
  "next_cursor": null
}
```

It looks final. But `next_cursor: null` only says this particular pagination mechanism has no next page. It says nothing about a backend result cap, an expired search job, a source that failed, records excluded by policy, or an API that silently limits lookback time.

A response becomes usable evidence when the contract says what `complete` covers. For an account search, that might mean all accounts visible to the calling principal at a stated snapshot. For a code search, it may mean all indexed files in a stated revision, while explicitly excluding ignored files and unindexed generated output. The scope must be concrete enough that a caller can decide whether it matches the proposed action.

Do not solve this by returning a `500` for every partial response. Partial results can be useful. A dashboard can show them. An agent can summarize them. A human can inspect them. The mistake is presenting a partial result as an authoritative answer to a question that requires completeness.

Use an error when the requested operation promises an atomic or complete answer and cannot fulfill that promise. Use a successful response with explicit incompleteness when the partial data itself has legitimate use. The client needs a deterministic distinction, not an argument about whether `200` felt optimistic.

## Put completeness metadata beside every result

A result contract should expose completeness as structured data that appears whether the item list is full, short, or empty. Do not make callers infer it from item counts, a missing header, or a sentence in a `message` field.

This shape works for a collection search:

```json
{
  "items": [
    {"id": "svc-184", "owner": null}
  ],
  "complete": false,
  "truncated": true,
  "incomplete_reasons": [
    {
      "code": "RESULT_LIMIT_REACHED",
      "message": "The query stopped after the configured result limit.",
      "limit": 1000
    }
  ],
  "next_cursor": "eyJvZmZzZXQiOjEwMDB9",
  "scope": {
    "resource": "service_accounts",
    "visibility": "resources readable by this credential",
    "snapshot": "2025-03-08T14:20:11Z"
  },
  "warnings": []
}
```

The exact field names matter less than their meaning and consistency. `complete` is the decision field. `truncated` describes one important route to incompleteness, but it should not become a catch-all. A permission filter is not truncation. A timed-out federated search is not pagination. If you overload one flag, callers lose the reason they need to recover safely.

Keep `warnings` separate from `incomplete_reasons`. A warning may tell the caller that a deprecated field appeared, a value was normalized, or a requested sort fell back to a default. An incomplete reason says the response cannot support claims about the unreturned portion of the requested scope. That distinction decides whether an agent can proceed.

Also avoid a bare `has_more` flag as the only signal. It usually answers a narrow pagination question. An agent that sees `has_more: false` may reasonably infer that the collection ended, even when a server-side cap or inaccessible shard prevented a full scan. `has_more` can stay, but it must not carry the entire burden of completeness.

For a single-resource read, use the same discipline. A response with omitted fields should say whether the server omitted them because the caller did not request them, the caller lacks access, the data source failed, or the value is genuinely absent. JSON omission is compact, but it is ambiguous.

## Pagination needs a stable boundary, not a bigger page size

Pagination is safe for agents only when the API makes continuation reliable and explains what changes can invalidate it. Raising the page limit postpones the bug. It does not remove it.

Offset pagination is especially prone to bad conclusions. An agent reads records 0 through 99, deletes or creates an object, then reads 100 through 199. If the underlying order changed, it can skip a record or process one twice. For an advisory report, that may be tolerable. For a change plan, it can be disastrous.

Cursor pagination is usually better because the server can encode a position in an ordered result set. It still needs a contract. State whether the cursor freezes a snapshot, how long it remains valid, and whether changing filters, sort order, or authorization makes the cursor invalid. If a cursor expires, do not restart the scan invisibly and return a merged answer. Return an explicit incomplete state or force the client to begin again.

A useful collection response gives the caller enough information to finish deliberately:

```json
{
  "items": ["item-001", "item-002"],
  "complete": false,
  "next_cursor": "cD0y",
  "page": {
    "returned": 2,
    "requested_size": 2,
    "ordering": "id ascending",
    "snapshot": "search-7f9c"
  },
  "incomplete_reasons": [
    {"code": "MORE_PAGES_AVAILABLE"}
  ]
}
```

The caller should continue until it receives `complete: true`, not merely until it receives a short page. Short pages happen for many reasons. Some APIs return them because a partition is temporarily sparse, because an internal worker stopped early, or because the service limits response size by bytes rather than object count.

Do not ask the language model to remember this loop in prose. Put pagination behavior in the tool implementation. A high-level `search_all` tool can collect pages, preserve the snapshot, cap its own work, and report whether it reached a terminal state. If it hits its own cap, it must return `complete: false` and say that the client-side cap caused it.

That last case gets missed often. Engineers correctly add metadata to the API, then build an agent wrapper with `max_pages=10` and discard the fact that it stopped at ten. The wrapper has become the source of incompleteness. The outermost tool contract owns the disclosure.

## Time limits, failed shards, and permissions need their own reasons

A search can finish its HTTP request while parts of its work did not finish. Distributed services commonly fan a query out to several indexes or tenants. If one source times out and the service returns matches from the others, the result may be useful, but it is incomplete.

Represent the cause in a code a program can branch on. Human text belongs alongside it, not in place of it. Keep codes few, stable, and documented. For example:

- `MORE_PAGES_AVAILABLE` means the caller can request the next page.
- `RESULT_LIMIT_REACHED` means the service enforced a cap before it exhausted matches.
- `TIME_BUDGET_EXCEEDED` means the search stopped before all planned work completed.
- `SOURCE_UNAVAILABLE` means an identified source did not answer.
- `VISIBILITY_RESTRICTED` means the caller's authorization excluded part of the requested domain.

Do not conceal `VISIBILITY_RESTRICTED` behind a generic success response. Security teams sometimes prefer indistinguishable replies because they do not want to reveal which object exists. That concern is legitimate. The API can report that visibility limits prevent a complete inventory without naming hidden objects. It must not let the caller mistake a partial inventory for an exhaustive one.

The same rule applies to rate limits and quotas. If an API reads the first portion of a request before it exhausts a budget, report the returned data and the budget condition. A retry might finish later, but a retry is a new attempt. The agent should not combine two attempts into a claim of completeness unless the API gives it a stable snapshot or the task tolerates drift.

A deadline should be an input as well as an output. When an agent asks for a broad inventory, let it set a time budget and receive the amount of work completed. That makes the tradeoff visible. A ten-second reconnaissance search may be fine before a human review. It is weak evidence for deleting every resource the search did not see.

## Absence is weak evidence for destructive changes

An agent may safely use partial data to draft a report, identify candidates, or ask a human to inspect a small target. It should not use partial data to infer that a resource is unused, unowned, duplicate, or safe to remove.

The difference is the direction of the claim. Finding a record with `owner: null` is positive evidence about that record, subject to field freshness. Finding no unowned records is a universal claim about the search domain. Universal claims require complete coverage of a defined scope.

This failure often arrives disguised as an efficiency improvement. A team gives an agent a tool called `list_inactive_projects`, then lets it archive every returned project or, worse, archive every project absent from a second list. The tool has a maximum result count. A large organization crosses that count months later. Nobody changed the agent prompt, but the agent's meaning changed from "operate on the inventory" to "operate on an arbitrary prefix of the inventory."

Build action tools so they demand evidence rather than accepting a narrative. An archive operation can require the IDs selected by a prior complete inventory and a snapshot token that binds the selection to the read. If the inventory was incomplete, the tool rejects the operation. This moves the safety check to a place where a model cannot hand-wave around it.

For actions that cannot use snapshot tokens, require an explicit scope and recheck each target at execution time. That does not prove the original search was exhaustive, but it prevents one stale list from authorizing unrelated mutations. Keep the action narrow enough that a reviewer can understand the target set.

The popular alternative is to tell the agent, "Never delete anything unless you are certain." It feels sensible and fails in practice. Certainty is a word in a prompt. `complete: false` is a condition a tool can enforce.

## Tool schemas should force an agent to confront uncertainty

An MCP tool or any agent-facing wrapper should return a typed envelope, not an attractive block of prose. The model can read prose, but surrounding software needs fields that it can validate, log, gate, and test.

A practical response type might look like this:

```json
{
  "status": "partial",
  "data": {
    "repositories": [
      {"id": "repo-a", "default_branch": "main"}
    ]
  },
  "completeness": {
    "complete": false,
    "reasons": ["TIME_BUDGET_EXCEEDED"],
    "continuation": {
      "kind": "retry_with_deadline",
      "minimum_seconds": 30
    }
  },
  "warnings": [
    {
      "code": "STALE_INDEX",
      "message": "Search index may lag the source repository."
    }
  ]
}
```

Do not use `status: "success"` for this response. It encourages simple clients to discard the metadata. `partial` tells the caller that it received usable data with a constraint. If your protocol must use a single success status, make `complete` mandatory and require action-capable clients to examine it before mutation.

The continuation field should describe a real recovery route. `next_cursor` is appropriate for another page. `retry_after` fits a rate limit. `narrow_query` may fit a server cap. Do not provide a continuation that merely repeats the same query and hopes the universe behaves differently.

Agent instructions should establish a small, strict rule set:

- The agent may cite an empty collection as proof of absence only when `complete` is true.
- The agent may use a partial response to propose a bounded read-only investigation.
- The agent must surface `incomplete_reasons` before requesting approval for any action that relies on the result.
- The agent must not invent a missing continuation token or claim that a retry succeeded without its result.

These rules are short because the data carries the detail. A prompt cannot recover information that a tool chose not to report.

## Warnings need ownership and an expiry path

Warnings become wallpaper when every response emits vague caution. Keep them specific, attributable, and actionable. A warning that never changes the caller's next choice should usually become documentation or disappear.

For example, `STALE_INDEX` should identify the indexed source and, when possible, its observed revision or update time. The agent can then decide to inspect the source of record before changing code. `PARTIAL_FIELD_SET` should say which fields the server omitted and whether the caller can request them. `DEFAULT_SCOPE_APPLIED` should state the scope that the server selected, because defaults are a frequent source of accidental broad actions.

Do not turn warnings into blockers by accident. A caller needs a clear severity rule. Completeness metadata governs whether the result supports a claim about the whole scope. Warnings govern confidence, freshness, or interpretation. A tool may return `complete: true` with a staleness warning. That result can still enumerate every item in an index while remaining unsuitable for a change that requires live state.

Give warnings stable codes and test consumers against them. Avoid tests that only assert a friendly message. Messages change when a writer improves wording; the decision rule should not.

Also decide who owns a warning after it ships. If an operations team sees the same warning on every call for six months, it will stop reading it. Either repair the underlying condition, promote it to a hard failure where appropriate, or remove it if it does not affect a decision. Permanent yellow lights teach people and agents to ignore yellow lights.

## Tests must exercise the dangerous empty response

Most test suites cover a normal page of results and a server error. They skip the response that causes the worst inference: `items: []` plus incompleteness.

Write contract tests for each reason code. Verify that the API returns the metadata for populated and empty lists, that SDKs preserve it, and that the agent wrapper does not flatten it into text. A regression in any layer can turn an honest server response into a misleading tool result.

Use cases like these in a test fixture:

```json
{
  "case": "empty first page with more pages",
  "response": {
    "items": [],
    "complete": false,
    "truncated": false,
    "incomplete_reasons": ["MORE_PAGES_AVAILABLE"],
    "next_cursor": "cursor-2"
  },
  "expected_agent_decision": "continue_search"
}
```

Then test a mutation request after that fixture. The expected decision should be `refuse_or_request_review`, not `perform_cleanup`. Make the policy visible in the test name. Future maintainers will otherwise treat the guard as an overly cautious edge case and remove it to make an automation demo smoother.

Test pagination under mutation too. Insert, delete, and reorder records between pages. Expire a cursor. Make one shard fail after another has returned results. Remove one permission halfway through a scan. Your tool should either preserve a documented snapshot or report that it cannot claim completeness. A test that only uses a static fake database cannot detect the lies that appear in production.

Property tests help here. Generate collections larger than every configured limit, vary page sizes, and assert one invariant: a client may label a collection complete only after it has accounted for every item in its declared snapshot. The test does not need a language model. This is ordinary interface correctness.

## Human approval should reveal the missing evidence

Human control works only when the approval shows the decision a person is being asked to make. "Allow agent action" is not an approval. It is a request to accept an opaque chain of assumptions.

When a tool reports incomplete data, show the proposed action, the target scope, the reason the evidence is incomplete, and the recovery option. A useful prompt says that the inventory timed out after returning 842 resources and asks whether to retry with a longer deadline, restrict the action to the returned IDs, or abandon the change. The reviewer can then make a real tradeoff.

Sallyport keeps the credential out of an agent process when it performs HTTP and SSH actions, and its activity records can show the resulting calls. That containment and trace are useful when a reviewer must reconstruct a bad decision. They do not turn an ambiguous API answer into evidence, so the tool response still needs to carry its completeness state.

Avoid approval fatigue by reserving approval for consequential ambiguity. A tool should handle routine continuation, such as fetching a documented next page, without repeatedly interrupting a person. It should stop when it reaches a policy boundary: an expired snapshot, restricted visibility, an action based on absence, or a proposed mutation outside the evidence it collected.

The first engineering task is small: find every API wrapper that can return a list, an aggregate, or a search result, then add an explicit complete state to its outermost response. Start with empty results and capped searches. Those are where confident agents manufacture the cleanest wrong answers.
