CCD-F.2 · CCDVF-D2 + CCDVF-D1 + CCDVF-D8 · Process

Streaming and Tool-Use Integration.

This is what happens when your streaming chat app also needs to call tools mid-response: look something up, run a calculation, hit an internal API, before it can finish answering. The two features compose awkwardly if you build them separately. Tool input arrives as fragments of JSON spread across many stream events, and you can't call the tool until every fragment has arrived. This scenario is the backend loop that gets both right at once: render text as it streams, wait for a complete tool call before executing it, and resume the stream after the tool result comes back.

24 min build·3 components·4 concepts

A backend service that combines Messages API streaming with the tool_use loop. Tool input JSON streams in fragments via input_json_delta events keyed by content-block index; a fragment is only safe to parse after its content_block_stop. Parallel tool_use blocks execute concurrently and return in a single user turn (never split across messages). stop_reason drives every branch, including pause_turn for server-side tool iteration limits.

D2 + D1 combined
SourceDeveloper-cert production pattern
What do the colours mean?
Green
Official Anthropic doc or API contract
Yellow
Partial doc / inferred
Orange
Community-derived
Red
Disputed / changes frequently
Stack
Python or TypeScript SDK - a small set of custom tools
Needs
Streaming basics - tool definitions - stop_reason
Exam
CCD-F D2 (33%) + D1 (14.7%). 33.1% CCDVF-D2 · 14.7% CCDVF-D1 · 10.6% CCDVF-D8.
Loop the mascot - painterly hero illustration for the Streaming and Tool-Use Integration scenario.
End-to-end flowCCD-F D2 (33%) + D1 (14.7%)
01 · Problem framing

The problem

What the customer needs

  1. Show text as it's generated, even on turns that also call tools.
  2. Execute a tool only once its full input has arrived, never on a partial JSON fragment.
  3. Run independent tool calls concurrently, not one at a time.

Why naive approaches fail

  1. Calling json.loads() on each input_json_delta chunk as it arrives throws on every partial fragment until the block closes.
  2. Splitting parallel tool_result blocks across multiple user messages trains the model to stop batching tool calls, silently degrading throughput.
  3. Ignoring stop_reason: 'pause_turn' on a long server-tool turn truncates the response instead of resuming it.
Definition of done
  • Every tool call executes against a complete, successfully parsed JSON input
  • Parallel tool_use blocks in one response execute concurrently and return in one user message
  • pause_turn resumes automatically without an injected 'Continue' message
  • Text renders to the client with no added latency from the tool-handling logic
02 · Architecture

The system

03 · Component detail

What each part does

3 components, each owns a concept. Click any card to drill into the underlying primitive.

Delta Accumulator

index-keyed JSON buffer

Buffers input_json_delta fragments per content-block index. Never attempts to parse a tool's input until that block's content_block_stop event has fired.

Configuration

buffers[event.index] += event.delta.partial_json on each input_json_delta; JSON.parse(buffers[index]) only at content_block_stop.

Concept: streaming

Tool Executor

concurrent - single-batch return

Runs every completed tool_use block from a response concurrently, then returns all tool_result blocks in one user message so parallel tool use keeps working on the next turn.

Configuration

results = await Promise.all(toolUses.map(execute)); messages.push({role: 'user', content: results}) - one message, every result.

Concept: tool-calling

Loop Controller

stop_reason dispatcher

Re-opens a new stream after tool_use, and resumes automatically (no injected user text) after pause_turn by resending the accumulated assistant + user history.

Configuration

if stop_reason 'tool_use': execute + continue. if stop_reason 'pause_turn': resend messages as-is, no extra prompt.

Concept: agentic-loops
04 · One concrete run

Data flow

05 · Build it

5 steps to production

01

Define a small, precisely-described tool set

Tool count and description quality drive routing accuracy under streaming just as much as in a non-streaming call. Keep the set small and each description explicit about when to call it.

Define a small, precisely-described tool set
tools = [
    {
        "name": "lookup_order",
        "description": (
            "Look up an order by order_id. Use this when the user asks about "
            "order status, shipping, or contents. Returns 404-shaped error if "
            "order_id is not found."
        ),
        "input_schema": {
            "type": "object",
            "properties": {"order_id": {"type": "string"}},
            "required": ["order_id"],
        },
    },
]
↪ Concept: tool-calling
02

Accumulate tool input keyed by content-block index

A response can interleave multiple content blocks. Buffer input_json_delta text per index and only parse it once that block's content_block_stop event arrives.

Accumulate tool input keyed by content-block index
def route_stream_events(stream):
    buffers = {}
    tool_uses = {}
    for event in stream:
        if event.type == "content_block_start" and event.content_block.type == "tool_use":
            tool_uses[event.index] = {"id": event.content_block.id, "name": event.content_block.name}
            buffers[event.index] = ""
        elif event.type == "content_block_delta" and event.delta.type == "input_json_delta":
            buffers[event.index] += event.delta.partial_json
        elif event.type == "content_block_delta" and event.delta.type == "text_delta":
            yield ("text", event.delta.text)
        elif event.type == "content_block_stop" and event.index in tool_uses:
            tool_uses[event.index]["input"] = json.loads(buffers[event.index])
            yield ("tool_use", tool_uses[event.index])
↪ Concept: streaming
03

Execute completed tool calls concurrently, return as one batch

Every tool_use block gathered from a single response executes concurrently. All of their results go back in exactly one user message, preserving Claude's ability to keep issuing parallel calls.

Execute completed tool calls concurrently, return as one batch
async def execute_and_continue(messages, tool_calls, response_content):
    results = await asyncio.gather(*(execute_tool(t) for t in tool_calls))
    tool_results = [
        {"type": "tool_result", "tool_use_id": t["id"], "content": r}
        for t, r in zip(tool_calls, results)
    ]
    messages.append({"role": "assistant", "content": response_content})
    messages.append({"role": "user", "content": tool_results})  # single batch
    return messages
↪ Concept: tool-calling
04

Resume on pause_turn without inventing a user message

A long server-tool turn can stop with pause_turn. Resend the trailing assistant response as-is; the API detects the pending server_tool_use and resumes automatically.

Resume on pause_turn without inventing a user message
def handle_pause_turn(messages, response):
    messages.append({"role": "assistant", "content": response.content})
    # Do NOT append an extra "Continue." user message - just re-request.
    return client.messages.create(model="claude-sonnet-5", max_tokens=4096,
                                   tools=tools, messages=messages)
↪ Concept: agentic-loops
05

Drive the full loop, capping iterations

Wrap steps 2-4 in a loop that opens a new stream after every tool_use turn and terminates on end_turn, with an iteration cap as a safety net (not the primary control).

Drive the full loop, capping iterations
async def run_streaming_tool_loop(user_text, max_iter=10):
    messages = [{"role": "user", "content": user_text}]
    for _ in range(max_iter):
        response = None
        tool_calls = []
        async for kind, payload in stream_and_route(messages):
            if kind == "text":
                send_to_client(payload)
            elif kind == "tool_use":
                tool_calls.append(payload)
            elif kind == "final":
                response = payload
        if response.stop_reason == "end_turn":
            return response
        if response.stop_reason == "tool_use":
            messages = await execute_and_continue(messages, tool_calls, response.content)
            continue
        if response.stop_reason == "pause_turn":
            response = await handle_pause_turn(messages, response)
            continue
    return {"status": "iteration_cap"}
↪ Concept: agentic-loops
06 · Configuration decisions

The four decisions

DecisionRight answerWrong answerWhy
Where to buffer tool input JSONkeyed by content-block index, parsed only at content_block_stopparse each input_json_delta chunk immediatelyFragments are not valid JSON on their own; interleaved blocks corrupt a single shared buffer.
Returning parallel tool resultsall tool_result blocks in one user messageone user message per tool_resultSplitting results across messages silently trains the model to stop issuing parallel tool calls.
Handling pause_turnresend the assistant turn as-is, no injected textappend a 'Continue.' user messageThe API detects the trailing server_tool_use block and resumes on its own; extra text just adds noise.
Manual loop vs Tool Runnermanual loop for full control over streaming + tool execution orderingalways default to the beta Tool RunnerThe Tool Runner is convenient but its streaming variant does not auto-resume pause_turn as of current SDK versions; a manual loop handles it explicitly.
07 · Failure modes

Where it breaks

3 failure pairs. Each maps to an exam-style question - the naive move on the left, the disciplined fix on the right.

Premature JSON parsing

Code calls json.loads() on every input_json_delta chunk as it streams in.

CCDF-05
✅ Fix

Buffer fragments per content-block index; parse only once content_block_stop fires for that index.

Split tool results

Each tool_result is sent back in its own separate user message.

CCDF-06
✅ Fix

Batch every tool_result from one response into a single user message.

pause_turn treated as termination

Loop returns to the caller on stop_reason == 'pause_turn', truncating the answer.

CCDF-07
✅ Fix

Resend the assistant turn as-is to resume; only end_turn (or the iteration cap) ends the loop.

08 · Budget

Cost & latency

Per-turn tokens (with 2 tool round-trips)
~2,600 input - 900 output

Growing history across two tool round-trips before end_turn; tool defs cached after the first turn.

Added latency per tool round-trip
~1.5-2s

New stream open + tool execution time; concurrent execution keeps this from multiplying per tool.

09 · Ship gates

Ship checklist

Two passes. Build-time gates verify the code; run-time gates verify the system in production.

Build-time

  1. Tool input buffered by content-block index, parsed only at content_block_stopstreaming
  2. Parallel tool_use blocks executed concurrentlytool-calling
  3. All tool_result blocks for a turn returned in a single user message
  4. pause_turn resumes without an injected user messageagentic-loops
  5. Loop has an iteration cap as a safety net, not the primary control
  6. Text deltas render to the client with no added latency from tool routing

Run-time

  • Test with a tool whose input JSON exceeds one SSE chunk (forces multi-fragment accumulation)
  • Test two independent tools called in parallel in one response
  • Test pause_turn resumption end-to-end with a long server-tool run
  • Alert on iteration-cap hits as a routing/description quality signal
  • Verify tool_result batching survives a code review checklist, not just a manual test
10 · Question patterns

Five exam-pattern questions

Your streaming handler calls `JSON.parse()` on every `input_json_delta` fragment and throws on most of them. What's the fix?
Buffer the fragments per content-block index and parse only once that block's content_block_stop event has fired. A single input_json_delta chunk is not valid JSON on its own.
After adding a second tool, the model stops issuing parallel tool calls even though the tools are independent. You send each tool_result back in its own user message. Why does this matter?
Splitting tool_result blocks across multiple messages silently trains Claude to stop batching tool calls in one response. Return every tool_result from a turn in a single user message.
A long research turn using server-side web search stops with `stop_reason: 'pause_turn'`. What should the client do?
Resend the conversation with the assistant's paused response appended as-is - no extra user message. The API detects the pending server_tool_use block and resumes automatically.
11 · FAQ

Frequently asked

Should I use the SDK's Tool Runner instead of a manual loop for streaming + tools?
The Tool Runner is a solid default for non-streaming or simple streaming cases, but its streaming variant does not automatically resume pause_turn in current SDK releases - it ends the loop and returns the paused message. A manual loop handles this explicitly, which is why this scenario builds one.
Do I need to stream tool_use content blocks to the UI at all?
Usually no. Stream text blocks to the UI for perceived responsiveness; tool_use blocks are backend bookkeeping. Some UIs show a 'using tool X' indicator, which you can emit as soon as content_block_start reports a tool_use block, before its input is even complete.
What happens if two tools are called in the same response but one fails?
Return a tool_result with is_error: true and an informative message for the failed one, alongside normal results for the others, all in the same batched user message. Claude will typically retry differently or explain the failure rather than silently proceeding.
How many tool round-trips should the loop allow before giving up?
Cap it (e.g. 10) as a safety net, but treat repeatedly hitting the cap as a bug signal, not something to fix by raising the number - it usually means a missing tool_result append or an ambiguous tool description causing alternation.
Last reviewed: 2026-07-12·Refresh cadence: 90 days
CCD-F.2 · CCDVF-D2 · Applications & Integration

Streaming and Tool-Use Integration, complete.

You've covered the full ten-section breakdown for this primitive, definition, mechanics, code, false positives, comparison, decision tree, exam patterns, and FAQ. One technical primitive down on the path to CCA-F.

More platforms →