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

**Sub-marker:** CCD-F.2
**Domains:** CCDVF-D2 · Applications & Integration, CCDVF-D1 · Agents & Workflows, CCDVF-D8 · Tools & MCPs
**Exam weight:** CCD-F D2 (33%) + D1 (14.7%)
**Build time:** 24 minutes
**Source:** 🟡 Developer-cert production pattern
**Canonical:** https://claudearchitectcertification.com/scenarios/streaming-and-tool-use-integration
**Last reviewed:** 2026-07-12

## In plain English

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.

## Exam impact

This is where D2 (Applications & Integration, 33.1%) and D1 (Agents & Workflows, 14.7%) meet: a streaming service that also runs an agentic tool loop. D8 (Tools & MCPs, 10.6%) covers the tool-definition half. The exam tests whether you know streaming and tool_use are not independent features you can bolt on separately.

## The problem

### What the customer needs
- 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.

### Why naive approaches fail
- 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.

### 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

## Concepts in play

- 🟢 **Streaming** (`streaming`), SSE event routing
- 🟢 **Tool calling** (`tool-calling`), Tool registry + execution
- 🟢 **tool_choice** (`tool-choice`), auto for open-ended turns
- 🟡 **Agentic loops** (`agentic-loops`), Loop resumption on tool_use/pause_turn

## Components

### 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`

## Build steps

### 1. 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.

**Python:**

```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"],
        },
    },
]
```

**TypeScript:**

```typescript
const tools: Anthropic.Tool[] = [
  {
    name: "lookup_order",
    description:
      "Look up an order by order_id. Use this when the user asks about " +
      "order status, shipping, or contents. Returns a 404-shaped error if " +
      "order_id is not found.",
    input_schema: {
      type: "object",
      properties: { order_id: { type: "string" } },
      required: ["order_id"],
    },
  },
];
```

Concept: `tool-calling`

### 2. 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.

**Python:**

```python
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])
```

**TypeScript:**

```typescript
function routeStreamEvents(stream: AsyncIterable<Anthropic.MessageStreamEvent>) {
  const buffers: Record<number, string> = {};
  const toolUses: Record<number, { id: string; name: string; input?: unknown }> = {};

  return (async function* () {
    for await (const event of stream) {
      if (event.type === "content_block_start" && event.content_block.type === "tool_use") {
        toolUses[event.index] = { id: event.content_block.id, name: event.content_block.name };
        buffers[event.index] = "";
      } else if (event.type === "content_block_delta" && event.delta.type === "input_json_delta") {
        buffers[event.index] += event.delta.partial_json;
      } else if (event.type === "content_block_delta" && event.delta.type === "text_delta") {
        yield { kind: "text" as const, text: event.delta.text };
      } else if (event.type === "content_block_stop" && event.index in toolUses) {
        toolUses[event.index].input = JSON.parse(buffers[event.index]);
        yield { kind: "tool_use" as const, call: toolUses[event.index] };
      }
    }
  })();
}
```

Concept: `streaming`

### 3. 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.

**Python:**

```python
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
```

**TypeScript:**

```typescript
async function executeAndContinue(
  messages: Anthropic.MessageParam[],
  toolCalls: { id: string; name: string; input: unknown }[],
  responseContent: Anthropic.ContentBlock[],
) {
  const results = await Promise.all(toolCalls.map(executeTool));
  const toolResults = toolCalls.map((t, i) => ({
    type: "tool_result" as const,
    tool_use_id: t.id,
    content: results[i],
  }));
  messages.push({ role: "assistant", content: responseContent });
  messages.push({ role: "user", content: toolResults }); // single batch
  return messages;
}
```

Concept: `tool-calling`

### 4. 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.

**Python:**

```python
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)
```

**TypeScript:**

```typescript
async function handlePauseTurn(messages: Anthropic.MessageParam[], response: Anthropic.Message) {
  messages.push({ 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, messages });
}
```

Concept: `agentic-loops`

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

**Python:**

```python
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"}
```

**TypeScript:**

```typescript
async function runStreamingToolLoop(userText: string, maxIter = 10) {
  let messages: Anthropic.MessageParam[] = [{ role: "user", content: userText }];
  for (let i = 0; i < maxIter; i++) {
    const { response, toolCalls } = await streamAndRoute(messages, sendToClient);
    if (response.stop_reason === "end_turn") return response;
    if (response.stop_reason === "tool_use") {
      messages = await executeAndContinue(messages, toolCalls, response.content);
      continue;
    }
    if (response.stop_reason === "pause_turn") {
      await handlePauseTurn(messages, response);
      continue;
    }
  }
  return { status: "iteration_cap" };
}
```

Concept: `agentic-loops`

## Decision matrix

| Decision | Right answer | Wrong answer | Why |
|---|---|---|---|
| Where to buffer tool input JSON | keyed by content-block index, parsed only at content_block_stop | parse each input_json_delta chunk immediately | Fragments are not valid JSON on their own; interleaved blocks corrupt a single shared buffer. |
| Returning parallel tool results | all tool_result blocks in one user message | one user message per tool_result | Splitting results across messages silently trains the model to stop issuing parallel tool calls. |
| Handling pause_turn | resend the assistant turn as-is, no injected text | append a 'Continue.' user message | The API detects the trailing server_tool_use block and resumes on its own; extra text just adds noise. |
| Manual loop vs Tool Runner | manual loop for full control over streaming + tool execution ordering | always default to the beta Tool Runner | The 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. |

## Failure modes

| Anti-pattern | Failure | Fix |
|---|---|---|
| CCDF-05 · Premature JSON parsing | 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. |
| CCDF-06 · Split tool results | Each tool_result is sent back in its own separate user message. | Batch every tool_result from one response into a single user message. |
| CCDF-07 · pause_turn treated as termination | 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. |

## Implementation checklist

- [ ] Tool input buffered by content-block index, parsed only at content_block_stop (`streaming`)
- [ ] Parallel tool_use blocks executed concurrently (`tool-calling`)
- [ ] All tool_result blocks for a turn returned in a single user message
- [ ] pause_turn resumes without an injected user message (`agentic-loops`)
- [ ] Loop has an iteration cap as a safety net, not the primary control
- [ ] Text deltas render to the client with no added latency from tool routing

## Cost &amp; 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.

## Domain weights

- **CCDVF-D2 · Applications & Integration (33.1%):** Delta Accumulator + streaming integration
- **CCDVF-D1 · Agents & Workflows (14.7%):** Loop Controller resumption logic
- **CCDVF-D8 · Tools & MCPs (10.6%):** Tool Executor + tool definitions

## Practice questions

### Q1. 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.

### Q2. 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.

### Q3. 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.

## FAQ

### Q1. 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.

### Q2. 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.

### Q3. 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.

### Q4. 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.

## Production readiness

- [ ] 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

---

**Source:** https://claudearchitectcertification.com/scenarios/streaming-and-tool-use-integration
**Last reviewed:** 2026-07-12

**Evidence tiers**, 🟢 official Anthropic doc · 🟡 partial doc / inferred · 🟠 community-derived · 🔴 disputed.
