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.

Delta Accumulator
index-keyed JSON bufferBuffers 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 →
Tool Executor
concurrent - single-batch returnRuns 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 →Loop Controller
stop_reason dispatcherRe-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 →
The problem
- Show text as it's generated, even on turns that also call tools.
- Execute a tool only once its full input has arrived, never on a partial JSON fragment.
- Run independent tool calls concurrently, not one at a time.
- Calling json.loads() on each input_json_delta chunk as it arrives throws on every partial fragment until the block closes.
- Splitting parallel tool_result blocks across multiple user messages trains the model to stop batching tool calls, silently degrading throughput.
- Ignoring stop_reason: 'pause_turn' on a long server-tool turn truncates the response instead of resuming it.
- ✓ 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
One run, traced end to end


5 steps to production
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 →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"],
},
},
]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 →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 →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 →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 →7 decisions the exam turns into distractors
parse each input_json_delta chunk immediately
keyed by content-block index, parsed only at content_block_stop
one user message per tool_result
all tool_result blocks in one user message
append a 'Continue.' user message
resend the assistant turn as-is, no injected text
always default to the beta Tool Runner
manual loop for full control over streaming + tool execution ordering
Code calls json.loads() on every input_json_delta chunk as it streams in.
Buffer fragments per content-block index; parse only once content_block_stop fires for that index.
Each tool_result is sent back in its own separate user message.
Batch every tool_result from one response into a single user message.
Loop returns to the caller on stop_reason 'pause_turn', truncating the answer.
Resend the assistant turn as-is to resume; only end_turn (or the iteration cap) ends the loop.

Cost & latency
Growing history across two tool round-trips before end_turn; tool defs cached after the first turn.
New stream open + tool execution time; concurrent execution keeps this from multiplying per tool.
Check every gate before release
3 exam-pattern questions
Work through one question at a time, check the architecture, then move through the set.
Your streaming handler calls JSON.parse() on every input_json_delta fragment and throws on most of them. What's the fix?
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.
