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_iterationscaps 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_resultwithis_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.