B3.3Bloom · AnNot started

Trace tool-use loop / stop_reason

Reading depth

What you'll learn

Tool use is a back-and-forth loop. The model says 'run this tool', you run it and reply with the result, repeat until the model says it's done (`stop_reason: end_turn`).

  • Tool use is a loop. `stop_reason: tool_use` → run tool, return result. `stop_reason: end_turn` → done.
  • Parallel tools: multiple `tool_use` blocks in one turn → multiple `tool_result` blocks in matched-ID order.
  • tool_result must reference the prior tool_use_id; mismatches are rejected.

Tool use in the Claude API is a *loop*, not a single call. The model emits a `tool_use` content block; your code executes the tool and returns a `tool_result` block in the next user turn; the model continues. The loop terminates when the model's `stop_reason` is `end_turn` (text-only response, no more tool calls) or it hits a stop sequence / max tokens. Until then, you're still in the loop.

The two `stop_reason` values that matter most: `tool_use` means the model wants you to run a tool and bring the result back — control returns to your code, which executes and replies with a `tool_result` block. `end_turn` means the model is done and produced its final answer — exit the loop. Confusing these is the canonical loop bug: treating `tool_use` as the final response (you ship the placeholder text 'I'll look that up...' as the answer) or treating `end_turn` as a tool intent (you keep looping forever).

The loop has a parallel-tool-use shape too. The model can emit multiple `tool_use` blocks in a single turn; your code executes them (often in parallel) and returns *multiple* `tool_result` blocks (in matching `tool_use_id`s) in the next user turn. The `stop_reason` is still `tool_use`, just with multiple parallel calls.

Three operational invariants. (1) `tool_result` blocks must reference the `tool_use_id` from the prior assistant turn — the API rejects unmatched IDs. (2) The full message history (all prior tool_use / tool_result pairs) is replayed on every turn — this is how the model maintains coherence across the loop; cost grows with loop depth. (3) Errors in tool execution are returned as `tool_result` blocks with `is_error: true` — the model can read the error and decide whether to retry, try a different tool, or apologise.

Key points

  • Tool use is a loop. `stop_reason: tool_use` → run tool, return result. `stop_reason: end_turn` → done.
  • Parallel tools: multiple `tool_use` blocks in one turn → multiple `tool_result` blocks in matched-ID order.
  • tool_result must reference the prior tool_use_id; mismatches are rejected.
  • Full history replays each turn — cost grows with loop depth.
  • Errors return as `is_error: true` tool_result blocks; the model can adapt.

Examples

Single-call loop

user: 'What's the weather in NYC?' → model: tool_use(get_weather, {city: 'NYC'}), stop_reason: tool_use → your code runs get_weather, returns tool_result(id, '72°F sunny') → model: 'It's 72°F and sunny in NYC.', stop_reason: end_turn. Loop done.

Parallel-call loop

user: 'Compare weather in NYC and LA.' → model: tool_use(get_weather, NYC) + tool_use(get_weather, LA), stop_reason: tool_use → your code runs both, returns two tool_result blocks in one user turn → model: comparison text, end_turn.

Pitfalls

  • Treating `stop_reason: tool_use` as the final response and shipping the placeholder text. The text in that turn is incidental to the tool intent.
  • Returning a `tool_result` with a different `tool_use_id` than the model emitted. API rejects.
  • Treating tool-use loops as cheap. The full history replays each turn; deep loops cost real tokens.
  • Throwing on tool error instead of returning `is_error: true`. The model can recover from errors it can see.

Source notes: 00-academy-basics/notes/04-claude-api.md

Ask Claude

Build a prompt with this lesson + your question, copy it, and open the Claude Project in a new tab.