The Anthropic Messages API rejects a request when any tool_use block in the history is not answered by a tool_result block carrying the same tool_use_id, in the immediately following user message, ahead of every other block in that message. The index it reports is usually far behind the current turn: messages.251 in one report, messages.216 in another, messages.8 in a third, on requests whose new content sat at the tail. The same invariant rejects requests on OpenAI Chat Completions, the OpenAI Responses API, Amazon Bedrock Converse and Gemini, under three other published wordings plus one with no stable public string, so a matcher written against the error text fires on one provider only. Two public reports had every result present and still took the 400, because another block sat between the results; the contiguous version of the same content returns 200. Retrying is useless, because a deterministically invalid array reproduces the identical error, and in one report the recovery command itself failed with the same 400 at the same index. The repair is a two-pass precondition that runs before every request: move every tool_result ahead of the other blocks in its message, then inject a tool_result with is_error true for each unanswered tool_use id. Add that pass to your client this week and log one aggregated warning per repair, so you can see which of the six causes you actually have.
The error, verbatim, as filed against the Claude Code repository on 18 July 2025:
API Error: 400 {"type":"error","error":{"type":"invalid_request_error","message":"messages.251: `tool_use` ids were found without `tool_result` blocks immediately after: toolu_01NQv98A7JdU62r8UxsPZecG. Each `tool_use` block must have a corresponding `tool_result` block in the next message."}}The number worth staring at is 251. That request carried its new content at the tail of the array, and the position the API objected to sits hundreds of messages back. Two other reports name messages.8 and messages.216. The API validates history far behind the current turn, so the request that fails is not the request that broke the array.
The common reading of tool_use ids were found without tool_result blocks is that a result went missing and the caller should retry. Neither half is safe to assume: in two filed reports every result was present and matched by id, just not in the position the API checks, and an array that violated the invariant once violates it identically on attempt two.
The fix is a repair pass that runs before every request, not an exception handler after the first 400. Broader agent architecture lives in the AI agents pillar.
The contract in one sentence, and why the index is not where you broke it
Every tool_use block is answered by a tool_result block carrying the same tool_use_id, in the immediately following user message, ahead of any other content block in that message.
Anthropic states the last two directly. On adjacency: "Tool result blocks must immediately follow their corresponding tool use blocks in the message history." On ordering: "In the user message containing tool results, the tool_result blocks must come FIRST in the content array. Any text must come AFTER all tool results."
The block lives on a user message, and Anthropic's docs are explicit that the Claude API "integrates tools directly into the user and assistant message structure" instead of separate tool or function roles. Code adapted from an OpenAI example will not produce a valid array however carefully the ids line up.
Anthropic's published example of an empty tool result carries only type and tool_use_id, so a result with no content is a valid answer: you never have to fabricate output. The defect that produced the orphan landed earlier, possibly in a process that has since exited. Take the index out of the error, slice that region, and read what wrote it.
| Field | Required | What it accepts |
|---|---|---|
tool_use_id | Yes | The id of the tool use request this is a result for |
content | No | A string, or a list of text, image, document or search_result blocks |
is_error | No | true if the tool execution resulted in an error |
One invariant, five surfaces, so match on the invariant
Every major tool-calling API enforces it; the wording is what differs.
GitHub issue search returns roughly 1,350 issues for the Anthropic wording, about 280 inside the Claude Code repository, against roughly 700, 375 and 123 for three of the others. Those are issue counts, not incident counts.
Five surfaces, four published strings, one rule. Repair logic keyed on a substring match against the error text fires on one provider and stops working the day someone adds a Bedrock fallback. Key it on the invariant: parse the array, index the ids, answer the question yourself. Same triage discipline as MCP error -32000.
The Responses API layers a second pairing rule on top: OpenAI's documentation states that any reasoning items returned with tool calls must also be passed back with the tool call outputs. Anthropic has no equivalent, which is one more break to plan for when migrating agents between model families.
| Surface | Rejection | Source |
|---|---|---|
| Anthropic Messages API | "tool_use ids were found without tool_result blocks immediately after: toolu_01NQv98A7JdU62r8UxsPZecG" | anthropics/claude-code#3886, July 2025 |
| OpenAI Chat Completions | "An assistant message with 'tool_calls' must be followed by tool messages responding to each 'tool_call_id'. The following tool_call_ids did not have response messages: call_hSm..." | langchain-ai/langchainjs#6621, August 2024 |
| OpenAI Responses API | "No tool output found for function call call_00000000" | agno-agi/agno#9361, August 2026 |
| Amazon Bedrock Converse | "ValidationException: Expected toolResult blocks at messages.2.content for the following Ids: tooluse_xxx, tooluse_yyy" | agno-agi/agno#6242, January 2026 |
| Google Gemini (Interactions) | Unpaired function_call steps rejected, no stable public string | agno-agi/agno#9373, August 2026 |
The error does not always mean a result is missing
An August 2026 report against pydantic-ai states the rule the error actually enforces: every tool_result answering an assistant turn has to sit in the leading run of the next user turn, and a result pushed behind any other block is reported as missing. It confirmed both shapes against the live Bedrock API.
[toolResult, text, toolResult, text, toolResult, text] # 400 [toolResult, toolResult, toolResult, text, text, text] # 200
The trigger was availability announcements interleaved between results, which makes the failure intermittent: load one capability at a time and a single result still leads, so it passes.
A second report, filed in August 2026 against an IDE agent extension, ships the array with it: an assistant message with two tool_use blocks, followed by a user message ordered [tool_result(first id), image, tool_result(second id), text, text]. Both were answered. The API named only the second id, at messages.216. Check block order inside the answering message first, then go looking for a result that never arrived.
Two related rules also reject a turn whose results are all present: a tool_result answering a computer use or browser use member block must echo its tool_use block's toolset_name, and a user message answering a turn that also called a server tool must carry nothing but tool_result blocks, since text after the results draws a 400 naming the unresolved server tool.
Six ways a correctly written loop still ships an invalid array
The loop code is usually fine. The transcript is not: every cause below is a history-construction defect away from the call site.
Interrupts leave the clearest paper trail. In the April 2025 report the user hit Escape after an auto-compaction, the cancel did not take, and an unpaired toolu_vrtx_015oxzdhxTvyks3Djx4FZE3h stayed in the transcript. The Claude Code changelog at 2.1.218 records fixing one such path, "an unpaired tool_use block left in the transcript when a tool aborted mid-response", and its name for the placeholder it writes instead is a synthetic tool denial. The common production interrupt is a rejected approval rather than a keystroke, so build the injection into your human-in-the-loop approval gate.
Compaction removes one half of the pair. An August 2026 issue against the OpenHands agent SDK describes it exactly: View.append_event applies a condensation by removing forgotten events by id, which can drop an action event while keeping its observation event. ToolCallMatchingProperty and ObservationUniquenessProperty exist to catch that, but run only through enforce_properties on the rebuild path, which the incremental path skips for performance. Checkpoint resume is the same defect on a different clock: a replay that lands the assistant turn but not the results yields an array that was valid when written and invalid when read, which is why the repair belongs at read time. That seam is inherent to durable execution, and Claude Code carries CLAUDE_CODE_RESUME_INTERRUPTED_TURN for it.
Id rewriting breaks pairs in transit, because any gateway that normalises ids has to rewrite both halves at once. A validator that pattern-matches a prefix breaks on the first failover.
Parallel batches fail loudly on Bedrock. The January 2026 agno report traced it to one assistant message carrying two toolUse blocks answered by two separate user messages, and measured four agents on parallel queries at 0 of 10, 0 of 3, 0 of 3 and 0 of 3, against 5 of 5 both for a single call and for the same agent instructed to run serially. On the direct Anthropic API the same shape is documented as behavioural rather than fatal, since a separate user message per result teaches the model to avoid parallel calls.
| Cause | What the array ends up looking like | Repair |
|---|---|---|
| Interrupt or failed cancel | tool_use with no answering block | Inject a tool_result with is_error true |
| Compaction evicts one half | Action event dropped, observation kept | Snap the boundary to a pair-safe index |
| Summarization rewrites the tail | Transcript reserialised into prose plus loose items | Rebuild pairing afterwards |
| Checkpoint resume replays a turn | tool_use replayed, result not yet written | Repair at read time, never write back |
| Cross-provider handoff rewrites ids | tool_calls[].id rewritten, tool_call_id not | Rewrite both halves in one pass |
| Parallel batch split or partly answered | Results in separate messages, or behind a block | One message, results ahead of any text |
| Surface | Id prefix | Constraint |
|---|---|---|
| Anthropic direct, Vertex, Bedrock | toolu_, toolu_vrtx_, tooluse_ | None published |
| OpenAI Chat Completions | call_ | Maximum length 40 |
| OpenAI Responses | fc_ on the item, call_ on the call id | Separate call_id required |
| Google Gemini | Any format | None |
| Mistral | None | Alphanumeric, exactly 9 characters |
Why retrying is the wrong instinct
A retry is the right reflex for a 429 and the wrong one here. Attempt two returns the identical 400 at the identical index, and the agent locks up instead of degrading. The IDE extension report puts it plainly: this breaks the task permanently and seems irrecoverable from the interface.
The April 2025 report shows the built-in escape hatch failing the same way: /compact issued its own request against the same broken array and returned the same error at messages.8. Any recovery path that has to talk to the model first inherits the poison.
An August 2026 agno issue settles where the defect lives: one poisoned session reproduces across three formatters, the OpenAI chat formatter, the Claude message utility and the Gemini interactions builder, each appending the assistant half with no check that a result follows.
It is the inverse of the failure it gets confused with: an agent that loops on the same failed tool call calls too much and progresses too little, while this one cannot call at all. Replace the retry policy with a precondition and one aggregated warning per pass carrying the ids repaired.
The repair pass: index the ids, then inject or drop
Walk the array, collect every tool call id with a recorded result, and for each tool_use id not in that set inject a synthetic result or drop the block. A proposed patch against agno, open and unmerged, names the same two moves: collect_recorded_tool_call_ids() for the index, counting any tool-role message with an id as an answer, and a MISSING_TOOL_RESULT_PLACEHOLDER constant for the injected text.
Inject rather than drop, unless the call provably never ran: an earlier agno patch for the Responses formatter recorded what dropping costs, since removing the assistant half turned a user -> assistant(tool_calls) -> user sequence into user, user and the model lost all trace the call was attempted. Anthropic publishes the shape for a call you chose not to run, which is the one to reuse for an interrupted call:
{
"type": "tool_result",
"tool_use_id": "toolu_02",
"is_error": true,
"content": "Not executed: the preceding write_file call failed."
}Set is_error because Anthropic recommends it for a call that produced no result, not because the validator demands it: the field is optional. Keep the content a neutral factual marker, not an instruction, so it cannot trigger a spurious re-run three turns later.
def repair_tool_pairs(messages):
answered = {
block["tool_use_id"]
for m in messages if m["role"] == "user" and isinstance(m["content"], list)
for block in m["content"] if block.get("type") == "tool_result"
}
out, stubs = [], []
for m in messages:
if stubs and m["role"] == "user" and isinstance(m["content"], list):
m = {**m, "content": stubs + m["content"]} # one answering message, results first
elif stubs:
out.append({"role": "user", "content": stubs})
stubs = []
out.append(m)
if m["role"] == "assistant" and isinstance(m["content"], list):
stubs = [
{"type": "tool_result", "tool_use_id": b["id"], "is_error": True,
"content": "Not executed: the run was interrupted before this call completed."}
for b in m["content"]
if b.get("type") == "tool_use" and b["id"] not in answered
]
return out + ([{"role": "user", "content": stubs}] if stubs else [])Merge the stubs into the answering message rather than appending them as their own: a separate message splits the batch across two user messages, the shape Bedrock rejects outright.
The ordering pass fixes the case where nothing is actually absent:
def results_first(user_message):
blocks = user_message["content"]
if not isinstance(blocks, list):
return user_message
results = [b for b in blocks if b.get("type") == "tool_result"]
rest = [b for b in blocks if b.get("type") != "tool_result"]
return {**user_message, "content": results + rest}Run results_first over every user message, then repair_tool_pairs over the array, on every request. Write nothing back: runs already on disk carry the defect, and a read-time repair fixes them without a migration.
Where the invariant has to live inside your compaction routine
Compaction is the cause you cannot fix at the call site: the code that removes messages has no idea what a tool pair is. Compact only at turn boundaries and treat the pair as one indivisible unit in your eviction policy.
The agno helper returns "the largest index b <= requested_index such that every assistant tool_call kept in messages[:b] has its result message also in messages[:b]". Anthropic's server-side context editing goes further and does not break the pair at all, behind the anthropic-beta: context-management-2025-06-27 header:
context_management={
"edits": [
{
"type": "clear_tool_uses_20250919",
"trigger": {"type": "input_tokens", "value": 30000},
"keep": {"type": "tool_uses", "value": 3},
"clear_at_least": {"type": "input_tokens", "value": 5000},
"exclude_tools": ["web_search"],
"clear_tool_inputs": False,
}
]
}Same trick as writing an oversized tool result to a file and leaving a path reference behind: the block stays, only its content changes. We covered the token economics of eviction separately; pairing is the constraint that post does not impose. Impose it.
| Mechanism | What it does about pairing |
|---|---|
Anthropic clear_tool_uses_20250919 | Replaces cleared result content with placeholder text and keeps the tool_use block; clear_tool_inputs defaults to false |
agno safe_truncation_index | Snaps a requested truncation boundary down to the largest pair-safe index |
agno filter_tool_calls | Strips matching entries out of the assistant tool_calls array when it drops old results |
LangChain trim_messages | Exposes start_on and end_on and documents the invariant, but repairs nothing for you |
On-prem: your own gateway does not enforce what the hosted API enforces
The finding that changes the answer inside a private perimeter is a negative one. vLLM's Anthropic-compatible /v1/messages endpoint converts tool_use blocks into OpenAI-style tool_calls and tool_result blocks into tool messages, with no pairing check, no orphan detection and no 400 path anywhere in vllm/entrypoints/anthropic/serving.py. The OpenAI-compatible path is the same: vllm/entrypoints/openai/chat_completion/serving.py calls make_tool_call_id() to mint an id the model did not supply, and none of the request validators in the matching protocol.py check that a tool_call_id has a partner. The invariant is a hosted-API contract, not a property of the wire format.
Both directions of a failover bite. A history your private endpoint accepted all week will 400 the instant a request goes to the hosted API, and an orchestrator built against the hosted shape can hit a chat template locally that does not expect it. For a regulated team running a private endpoint in development or as a fallback, the failover is itself the trigger.
The local failure mode is worse than a 400, because there is no error. The chat template renders the orphaned tool_use into the prompt and the model carries on against a transcript that says a tool was called and never answered. Output degrades and nothing logs, which needs the same treatment as an agent that reports done and did nothing: a check that reads the artifact, not the model's account. What does fail loudly is misleading: the file's only template-level guard, _detect_merge_inline_system, catches jinja2.TemplateError from a probe render at startup, purely to decide whether system messages need merging. Nothing catches it on a real request, so a history the chat template chokes on surfaces as a Jinja error rather than a provider error naming a message index, much like empty tool calls from a vLLM parser.
An id-rewriting gateway is a second local trigger. agno's per-provider configuration exists because the chat surface caps ids at 40 characters, the Responses surface needs a separate call_id, and Mistral demands exactly 9 alphanumeric characters. Rewrite the assistant tool_calls[].id without the tool message's tool_call_id and you have manufactured the orphan yourself.
Run the repair pass on both sides of every failover path, and assert the invariant in CI rather than trusting either endpoint. This week, write one fixture: a transcript with a deliberately orphaned tool_use, plus a user message whose tool_result sits behind a text block. Feed it through your serialiser and assert that every result comes back answered and leading. That test is the difference between a failover that degrades and a session that cannot send anything.
FAQ
Quick answers to the questions this post tends to raise.



