PrepGenAICerts

The Four Message Block Types & the tool_use / tool_result Pairing Invariant

Core

Work with the Messages API's core request/response mechanics · Difficulty 3/5

0%
message-blockstool-usethinking-signaturestreaming

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 typeAppears onCarriesRule that governs it
textassistantClaude's proseIf 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_useassistanttool name, a unique id, and the input argumentsMust be answered by a tool_result in the immediately following user turn, matched by id
tool_resultuserthe matching tool_use_id, result content, optional is_errorThe 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
thinkingassistant (extended thinking only)Claude's internal reasoning, plus an opaque signatureMust 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:

  1. **Never parse or act on a `tool_use block's input before its content_block_stop event fires.** Attempting to json.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.
  2. 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_stop has arrived. If your handler commits history whenever its read loop happens to end -- rather than gating that commit on message_stop specifically -- 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 first content_block_delta for it arrives -- it can't, until content_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

Related Concepts

PrepGenAICerts.com is an independent third-party exam-prep platform for the Claude Certified Architect (CCA-F) certification. We are not affiliated with, endorsed by, or acting on behalf of Anthropic PBC.

Note: New premium upgrades are temporarily paused while we resolve an issue with our payment provider. Existing premium members retain full access.