The Four Message Block Types & the tool_use / tool_result Pairing Invariant
CoreWork with the Messages API's core request/response mechanics · Difficulty 3/5
Explanation
Four Block Types, One Enforced Pairing Rule
A Messages API conversation is not plain text -- each turn's content is a list of typed blocks, and exactly four block types do the work of a tool-use conversation:
| Block type | Appears on | Carries | Rule that governs it |
|---|---|---|---|
text | assistant | Claude's prose | If it sits alongside a tool_use block in the same turn, both must be preserved when the turn is appended to history -- dropping the text block corrupts the context Claude relied on |
tool_use | assistant | tool name, a unique id, and the input arguments | Must be answered by a tool_result in the immediately following user turn, matched by id |
tool_result | user | the matching tool_use_id, result content, optional is_error | The tool_use_id must match exactly -- this is how Claude reconnects a result to the call that produced it when a turn issues several calls at once |
thinking | assistant (extended thinking only) | Claude's internal reasoning, plus an opaque signature | Must be passed back to the API byte-for-byte unchanged on the next turn -- editing, summarizing, or dropping it invalidates the signature and the request is rejected |
The pairing invariant is the single most exam-relevant rule here: every tool_use block emitted in an assistant turn needs a matching `tool_result -- same id` -- sitting in the very next user turn, no exceptions. Skip a result, mismatch an id, or push a result out to a later turn instead of the next one, and the request fails validation on any of those. This is not something a better prompt fixes -- it is a structural requirement your code has to satisfy on every request.
# One assistant turn with two tool_use blocks -> both tool_use_id values must
# appear in tool_result blocks inside the SAME next user turn.
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": [
{"type": "tool_result", "tool_use_id": "toolu_01", "content": result_a},
{"type": "tool_result", "tool_use_id": "toolu_02", "content": result_b},
]})
# Splitting these two tool_result blocks across two separate user turns, or
# omitting one, both fail validation on the next request.The Thinking Signature Carry-Back Rule
When extended thinking is on and the conversation also uses tools, every thinking block the API returns carries a signature -- an opaque value that lets the API verify the reasoning it's receiving back is the exact reasoning it produced, not something edited or reconstructed. On the next turn, that block must go back byte-for-byte identical, signature included. Summarizing it to save tokens, rewriting it for readability, or quietly dropping it to shrink the context all break the signature match, and the API rejects the request outright rather than silently accepting a tampered block.
Redacted (encrypted) thinking blocks follow the identical rule. Their content is unreadable ciphertext -- you cannot inspect or summarize it even if you wanted to -- but that doesn't exempt them: they still have to be returned untouched, in the same position, on the next request. If the real motivation for wanting to touch a thinking block is context-budget pressure, the fix is context engineering (pruning, compaction, or a fresh session) applied to the *conversation as a whole*, not surgery on an individual thinking block.
Streaming Makes the tool_use Block a Moving Target
When a response streams, a `tool_use block does not arrive complete. Its input argument is assembled from a series of content_block_delta` events, each one appending another fragment of a JSON string that is not valid JSON until the block closes. Two disciplines follow directly from that:
- **Never parse or act on a `tool_use
block's input before itscontent_block_stopevent fires.** Attempting tojson.loads()` a partial string either throws on malformed JSON or, worse, occasionally parses something *syntactically* valid but semantically incomplete -- and then your code runs the tool with half its arguments missing. - On a dropped or interrupted stream, discard the entire partial assistant turn rather than saving whatever was accumulated to conversation history. A turn is only complete once
message_stophas arrived. If your handler commits history whenever its read loop happens to end -- rather than gating that commit onmessage_stopspecifically -- a network blip that drops the stream mid-`tool_use`-block leaves a partially-assembled block sitting in history. The next request then fails validation, and because the corrupted turn is now sitting several messages back, the error looks unrelated to its actual cause: a dropped stream from an earlier request. Retry the original request from the last known-complete turn instead of patching around the corrupted one.
# Gate the history commit on message_stop, not on the read loop simply ending.
blocks, complete = {}, False
try:
with client.messages.stream(model=model, max_tokens=4096,
messages=messages, tools=tools) as stream:
for event in stream:
if event.type == "content_block_start":
blocks[event.index] = init_block(event)
elif event.type == "content_block_delta":
apply_delta(blocks[event.index], event.delta)
elif event.type == "content_block_stop":
finalize_block(blocks[event.index]) # first safe point to parse tool_use.input
elif event.type == "message_stop":
complete = True
except (ConnectionError, TimeoutError):
pass # network drop -- fall through with complete == False
if complete:
messages.append({"role": "assistant", "content": assemble(blocks)})
else:
# Discard the partial turn entirely. Do not append a half-built block.
# Retry the original request from the last complete turn instead.
retry_request(messages, tools)Common exam traps
- Believing a `tool_use
block's input can be parsed as soon as the firstcontent_block_deltafor it arrives -- it can't, untilcontent_block_stop`. - Assuming a dropped stream just means "try again with what we have" -- the correct response is to throw away the partial turn, not persist it and retry on top of it.
- Forgetting that a redacted (encrypted) thinking block still has to be replayed unchanged, on the theory that since it's unreadable it must not matter -- the signature check applies regardless of whether the content is legible.
- Splitting multiple `tool_result` blocks from one assistant turn across more than one subsequent user turn instead of a single following turn.
This note covers the block-type taxonomy and stream-interruption handling; the question of *who* is responsible for executing a tool call within the loop, and the broader agent-loop mechanics, are covered in the tools/MCP domain -- this is the wire-level contract those loops have to satisfy underneath.
Key Takeaways
- Four block types govern a tool-use conversation: text, tool_use, tool_result, and thinking, each with its own carry-forward rule
- Every tool_use block must be answered by a tool_result block with a matching id in the immediately following user turn -- a mismatch, omission, or delay fails validation
- Thinking blocks (including redacted/encrypted ones) carry a signature and must be passed back completely unchanged on the next turn, or the request is rejected
- A streamed tool_use block's input is unparseable until content_block_stop fires -- acting on a partial JSON string is a real bug pattern
- On a dropped or interrupted stream, discard the partial assistant turn and retry rather than committing it to history
Glossary Terms
The structured units that make up Claude's response. Types include: `text` (plain text response), `tool_use` (a request to call a tool with specific inputs), `tool_result` (the caller's response to a tool request), and `thinking` (internal reasoning when extended thinking is enabled). A single response can contain multiple content blocks of mixed types.
A content block type in the user message that returns the output of a tool execution back to Claude. Must include the 'tool_use_id' matching the original tool_use block. Can be text, images, or error messages. Claude processes the result and continues reasoning.
A content block type in Claude's response indicating the model wants to call a specific tool. Contains 'id', 'name', and 'input' fields. The agent must execute the tool and return results in a tool_result content block for the conversation to continue.
Related Concepts