Friday, 7 August 2026

Streaming responses and processing



Here's a concrete example. Let's say the user asks:

"What is the weather in Mumbai and convert 1000 INR to USD?"

Claude will stream back two tool calls. Here's exactly how the events flow:


The complete stream, event by event:

1. message_start

json
{
  "type": "message_start",
  "message": {
    "id": "msg_01XY",
    "type": "message",
    "role": "assistant",
    "content": [],
    "usage": { "input_tokens": 245, "output_tokens": 0 }
  }
}

What your handler does: Message is beginning. Initialize an empty content[] array to collect blocks into.


2. content_block_start — first tool call opens

json
{
  "type": "content_block_start",
  "index": 0,
  "content_block": {
    "type": "tool_use",
    "id": "tool_01ABC",
    "name": "get_weather",
    "input": {}
  }
}

What your handler does: A tool_use block is opening at index 0. You know the tool name (get_weather) and its id, but input is empty. Create a slot at content[0] for this block.


3. content_block_delta — input JSON arrives in fragments

json
{ "type": "content_block_delta", "index": 0,
  "delta": { "type": "input_json_delta", "partial_json": "{\"ci" } }
json
{ "type": "content_block_delta", "index": 0,
  "delta": { "type": "input_json_delta", "partial_json": "ty\": " } }
json
{ "type": "content_block_delta", "index": 0,
  "delta": { "type": "input_json_delta", "partial_json": "\"Mumbai\"}" } }

What your handler does: Keep appending each fragment to content[0]'s input buffer. After all three arrive the buffer holds {"city": "Mumbai"} — but don't parse yet, wait for the block to close.


4. content_block_stop — first tool call complete

json
{
  "type": "content_block_stop",
  "index": 0
}

What your handler does: Block at index 0 is done. Now parse the accumulated JSON {"city": "Mumbai"}. This is the first safe moment to call get_weather.


5. content_block_start — second tool call opens

json
{
  "type": "content_block_start",
  "index": 1,
  "content_block": {
    "type": "tool_use",
    "id": "tool_02DEF",
    "name": "convert_currency",
    "input": {}
  }
}

What your handler does: A second tool_use block opens at index 1. Create a slot at content[1].


6. content_block_delta — second tool input fragments

json
{ "type": "content_block_delta", "index": 1,
  "delta": { "type": "input_json_delta", "partial_json": "{\"amount\": 1000," } }
json
{ "type": "content_block_delta", "index": 1,
  "delta": { "type": "input_json_delta", "partial_json": " \"from\": \"INR\"," } }
json
{ "type": "content_block_delta", "index": 1,
  "delta": { "type": "input_json_delta", "partial_json": " \"to\": \"USD\"}" } }

What your handler does: Append each fragment to content[1]'s buffer. Don't parse yet.


7. content_block_stop — second tool call complete

json
{
  "type": "content_block_stop",
  "index": 1
}

What your handler does: Block at index 1 is done. Now parse {"amount": 1000, "from": "INR", "to": "USD"}. Safe to call convert_currency now.


8. message_delta — stop reason arrives

json
{
  "type": "message_delta",
  "delta": {
    "stop_reason": "tool_use",
    "stop_sequence": null
  },
  "usage": { "output_tokens": 89 }
}

What your handler does: Record stop_reason: tool_use — this tells you Claude stopped because it needs tool results, not because it finished. Final token counts also arrive here.


9. message_stop — stream complete

json
{
  "type": "message_stop"
}

What your handler does: Stream is done. Your assembled content[] array now looks exactly like a non-streamed response. Execute both tools, send results back, continue the loop.


The assembled content array at the end:

json
[
  { "type": "tool_use", "id": "tool_01ABC", 
    "name": "get_weather", "input": { "city": "Mumbai" } },
  { "type": "tool_use", "id": "tool_02DEF", 
    "name": "convert_currency", "input": { "amount": 1000, "from": "INR", "to": "USD" } }
]

From this point, treat it exactly like a non-streamed response.


What Claude knows immediately vs what it builds gradually

When Claude decides to call a tool, it knows two things instantly:

  • Which tool to call (get_weather)
  • The tool's id (tool_01ABC)

But the input arguments need to be generated token by token, just like text. Claude doesn't know {"city": "Mumbai"} all at once — it generates each character sequentially.

So the protocol splits accordingly:

content_block_start  →  carries what's known upfront
                        (type, name, id)

content_block_delta  →  carries what's generated gradually
                        (input arguments, fragment by fragment)

Why this design makes sense

Your handler needs the tool name and id as early as possible — before the input is even complete. Here's why:

For logging: You can immediately log "Claude is calling get_weather" without waiting for the full input.

For UI feedback: You can show the user "Fetching weather..." the moment the tool name arrives, not after all fragments land.

For parallel preparation: In complex pipelines, knowing the tool name early lets you prepare resources before the arguments are fully assembled.

If the name and id also came through deltas, you'd have to buffer everything and wait until content_block_stop just to know which tool was called — which defeats the purpose of streaming entirely.


Simple mental model:

content_block_start   =   the envelope        (who is this for, what type)
content_block_delta   =   the letter inside   (the actual generated content)
content_block_stop    =   seal the envelope   (safe to read now)

The envelope arrives whole and instantly. The letter is written gradually, word by word.

No comments:

Post a Comment