Certopolis

Claude Certified Architect – Foundations

Tool runner vs manual agent loop in the Claude SDK

The tool runner (client.beta.messages.tool_runner in Python) is an SDK helper that runs the tool-use loop for you: it calls your tools, sends the results back, and keeps the conversation state. Write a manual loop instead when you need human approval before a tool runs, custom logging, or conditional execution.

What the tool runner does

You define tools with the SDK helpers, such as the @beta_tool decorator in Python (it builds the JSON schema from type hints and the docstring) or betaZodTool() in TypeScript. The runner then runs tools when Claude calls them, handles the request and response cycle, manages conversation state, and validates inputs.

It is in beta and available in the Python, TypeScript, C#, Go, Java, PHP and Ruby SDKs. It runs inside your own process and only calls the tools you define. In Python, runner.until_done() returns the final message.

When the loop ends

The runner loops until Claude returns a message with no tool use, or until it reaches max_iterations if you set it (maxIterations in C#, Java and PHP; MaxIterations in Go). You can also break out of the loop at any iteration.

Errors and taking control

If a tool throws, the runner catches it and returns it to Claude as a tool result with is_error: true. The result carries the exception's message (in Python, its type and message), not the full stack trace, and the loop keeps going.

To act on a result before Claude sees it, call generate_tool_call_response() inside the loop in Python, inspect the result, and break or change it. Calling append_messages() inside the loop tells the runner you are managing history yourself: it stops appending the assistant message and tool results for that turn, so you must append them and should pass max_iterations to bound the loop.

If you need full control over every request, use the manual loop: call the Messages API, check stop_reason, run the tools yourself, and append the tool_result blocks.

Where the exam catches people

  • The runner has no default turn limit: only max_iterations caps the number of turns, otherwise it runs until Claude stops calling tools.
  • A tool exception does not escape the runner and end the loop: it becomes a tool_result with is_error: true, so you have to inspect results yourself to stop on a specific failure.
  • Claude does not see the stack trace of a failed tool, only the exception's message (and type in Python).
  • Calling append_messages() only to add a steering note breaks the conversation, because the runner then skips appending that turn's assistant message and tool results.

Written from Anthropic's documentation

Try 3 questions on this

Pick an answer to see why each option is right or wrong.

  1. You drive your own functions with client.beta.messages.tool_runner(...) and iterate over the runner in a for loop that has no exit of its own, so nothing in your code tells the runner to stop. A task can keep looping for as long as Claude keeps requesting tools, and you want the number of turns it takes bounded. Which approach should you take?

    Right. In the game that’s Renown for your Agent Architecture and Orchestration building.

    Not that one: C is right. In the game your Agent Architecture and Orchestration building would lose a little Renown, and this question would come back later.

  2. Your Python service drives three internal tools with the SDK tool runner. A teammate proposes leaving every failure as an uncaught exception, arguing that Claude will read the full stack trace in the tool result and correct its own arguments on the next turn. Your loop has no error handling of its own. How should you evaluate this proposal?

    Right. In the game that’s Renown for your Agent Architecture and Orchestration building.

    Not that one: B is right. In the game your Agent Architecture and Orchestration building would lose a little Renown, and this question would come back later.

    Show the full explanation and doc quotes

    The result carries the exception's message, and in Python its type as well, not the full stack trace. The trace stays on your side, where the Python SDK logs it through the standard logging module. So the teammate is right that the loop keeps running and Claude sees the failure, and wrong that Claude sees the trace.

    Written from platform.claude.com/docs/en/agents-and-tools/tool-use/tool-runner

  3. Your service drives an agent loop with the Anthropic Python SDK's tool runner (client.beta.messages.tool_runner). One tool raises an exception when a downstream billing API returns a hard decline. Today the loop continues and Claude retries the call with different arguments. Your service must stop the loop on that specific failure and surface it to its caller. Which approach should you take?

    Right. In the game that’s Renown for your Agent Architecture and Orchestration building.

    Not that one: B is right. In the game your Agent Architecture and Orchestration building would lose a little Renown, and this question would come back later.

    Show the full explanation and doc quotes

    To act on a failure before it reaches Claude, the Python and TypeScript SDKs expose a tool response method -- generate_tool_call_response() in Python -- which yields the tool result inside the loop body, where the caller can inspect it and break.

    Written from platform.claude.com/docs/en/agents-and-tools/tool-use/tool-runner