CCD-F.2 · D2 + D1 + D8 · ProcessCCD-F D2 · 33%24 min build

Streaming and Tool-Use Integration.

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.

Loading your plan…
Mental modelArchitecture chooses the flow. Deterministic controls enforce policy. Structured state preserves truth.
Loop mascot illustrating Streaming and Tool-Use Integration.
Share
01 · System & parts

What this system is, and its 3 parts

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.

3Components
D2Primary domain
5Build steps
7Decision traps
4Concept links
Stack · Python or TypeScript SDK - a small set of custom toolsNeeds · Streaming basics - tool definitions - stop_reason
Streaming and Tool-Use Integration component architecture.
3 components. Each owns one concept.
01

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.

Configurationbuffers[event.index] += event.delta.partial_json on each input_json_delta; JSON.parse(buffers[index]) only at content_block_stop.
Concept: streaming →
02

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.

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

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.

Configurationif stop_reason 'tool_use': execute + continue. if stop_reason 'pause_turn': resend messages as-is, no extra prompt.
Concept: agentic-loops →
02 · Problem framing

The problem

What the user 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
03 · Data flow

One run, traced end to end

Streaming and Tool-Use Integration sequence diagram.
Streaming and Tool-Use Integration end-to-end flow.
04 · Build

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.

Concept: tool-calling →
Python
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"],
        },
    },
]
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.

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.

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.

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).

Concept: agentic-loops →
05 · Right call, wrong call

7 decisions the exam turns into distractors

01 · Where to buffer tool input JSON
Looks right

parse each input_json_delta chunk immediately

Actually right

keyed by content-block index, parsed only at content_block_stop

DEC-01
02 · Returning parallel tool results
Looks right

one user message per tool_result

Actually right

all tool_result blocks in one user message

DEC-02
03 · Handling pause_turn
Looks right

append a 'Continue.' user message

Actually right

resend the assistant turn as-is, no injected text

DEC-03
04 · Manual loop vs Tool Runner
Looks right

always default to the beta Tool Runner

Actually right

manual loop for full control over streaming + tool execution ordering

DEC-04
05 · Premature JSON parsing
Looks right

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

Actually right

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

CCDF-05
06 · Split tool results
Looks right

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

Actually right

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

CCDF-06
07 · pause_turn treated as termination
Looks right

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

Actually right

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

CCDF-07
Streaming and Tool-Use Integration failure map.
06 · Budget

Cost & latency

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

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

~1.5-2sAdded latency per tool round-trip

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

07 · Ship checklist

Check every gate before release

0/6 checked
  • streaming →
  • tool-calling →
  • agentic-loops →
08 · Practice

3 exam-pattern questions

Work through one question at a time, check the architecture, then move through the set.

Question 1 of 3Check the architecture

Your streaming handler calls JSON.parse() on every input_json_delta fragment and throws on most of them. What's the fix?

09 · 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.

Help someone build it

Share this scenario.

One share is one less team repeating the same architecture mistake.

Last reviewed ·