What Is the Claude SDK Tool Runner?
When building applications with Claude's tool use capabilities, developers traditionally had to implement a manual loop: send a request to Claude, detect tool calls in the response, execute those tools, and feed the results back. While straightforward in concept, this loop becomes complex when you factor in error handling, context management, and multi-turn conversations.
The Tool Runner is a beta feature available in the Python, TypeScript, and Ruby SDKs that automates this entire loop. Instead of managing the back-and-forth yourself, the SDK handles tool execution, result passing, and conversation flow automatically.
Here's what Tool Runner brings to the table:
- Automatic loop management: Each time Claude calls a tool, the runner executes it and feeds the result back without manual intervention
- Type-safe input validation: Combined with Tool Helpers, tool inputs are validated against their schemas before execution
- Automatic compaction: When token usage exceeds a threshold, the runner generates conversation summaries to keep context manageable
- Streaming support: Receive responses in real-time while the agentic loop runs
The Traditional Tool Loop vs. Tool Runner
Let's first look at the traditional pattern to understand what Tool Runner replaces.
import anthropic
client = anthropic.Anthropic()
tools = [
{
"name": "get_weather",
"description": "Get current weather for a city",
"input_schema": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "City name"}
},
"required": ["city"]
}
}
]
def get_weather(city: str) -> str:
return f"Weather in {city}: Sunny, 72°F"
# Manual loop
messages = [{"role": "user", "content": "What's the weather in Tokyo and Osaka?"}]
while True:
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
tools=tools,
messages=messages,
)
if response.stop_reason == "end_turn":
break
for block in response.content:
if block.type == "tool_use":
result = get_weather(block.input["city"])
messages.append({"role": "assistant", "content": response.content})
messages.append({
"role": "user",
"content": [{
"type": "tool_result",
"tool_use_id": block.id,
"content": result,
}]
})
final_text = next(b.text for b in response.content if hasattr(b, "text"))
print(final_text)This works, but loop management, message appending, and error handling are all on you. Tool Runner eliminates this boilerplate entirely.
Defining Tools with Tool Helpers
Before using the Tool Runner, let's see how Tool Helpers make tool definitions type-safe and concise.
Python (Pydantic-Based)
import anthropic
from pydantic import BaseModel, Field
client = anthropic.Anthropic()
class GetWeatherInput(BaseModel):
city: str = Field(description="City to get weather for")
unit: str = Field(default="celsius", description="Temperature unit (celsius/fahrenheit)")
@anthropic.tool(name="get_weather", description="Get current weather for a specified city")
def get_weather(input: GetWeatherInput) -> str:
return f"Weather in {input.city}: Sunny, 22°C ({input.unit})"
# Automatically generates JSON Schema from the Pydantic model
print(get_weather.to_params())
# Output:
# {
# "name": "get_weather",
# "description": "Get current weather for a specified city",
# "input_schema": {
# "type": "object",
# "properties": {
# "city": {"type": "string", "description": "City to get weather for"},
# "unit": {"type": "string", "default": "celsius", ...}
# },
# "required": ["city"]
# }
# }TypeScript (Zod-Based)
import Anthropic from "@anthropic-ai/sdk";
import { z } from "zod";
const client = new Anthropic();
const GetWeatherInput = z.object({
city: z.string().describe("City to get weather for"),
unit: z.enum(["celsius", "fahrenheit"]).default("celsius").describe("Temperature unit"),
});
const weatherTool = client.betaZodTool({
name: "get_weather",
description: "Get current weather for a specified city",
schema: GetWeatherInput,
execute: async (input) => {
// input is automatically typed based on the Zod schema
return `Weather in ${input.city}: Sunny, 22°C (${input.unit})`;
},
});With Tool Helpers, invalid inputs are caught at validation time rather than causing runtime errors deep in your application logic.
Building Agentic Loops with Tool Runner
Python Implementation
import anthropic
from pydantic import BaseModel, Field
client = anthropic.Anthropic()
class SearchInput(BaseModel):
query: str = Field(description="Search query")
class CalculateInput(BaseModel):
expression: str = Field(description="Math expression to evaluate")
@anthropic.tool(name="web_search", description="Search the web for information")
def web_search(input: SearchInput) -> str:
return f"Search results for '{input.query}': Latest information found..."
@anthropic.tool(name="calculate", description="Evaluate a math expression")
def calculate(input: CalculateInput) -> str:
try:
result = eval(input.expression) # Use safe_eval in production
return f"Result: {input.expression} = {result}"
except Exception as e:
return f"Calculation error: {e}"
# Run the agentic loop with Tool Runner
result = client.messages.tool_runner(
model="claude-sonnet-4-6",
max_tokens=4096,
tools=[web_search, calculate],
messages=[{
"role": "user",
"content": "What is Japan's population? Calculate what percentage of the world population that represents."
}],
)
# result is iterable — process each message in sequence
for message in result:
print(f"[{message.role}]")
for block in message.content:
if hasattr(block, "text"):
print(block.text)
elif block.type == "tool_use":
print(f" → Tool call: {block.name}({block.input})")TypeScript Implementation
import Anthropic from "@anthropic-ai/sdk";
import { z } from "zod";
const client = new Anthropic();
const searchTool = client.betaZodTool({
name: "web_search",
description: "Search the web for information",
schema: z.object({ query: z.string().describe("Search query") }),
execute: async (input) => {
return `Search results for '${input.query}': Latest information found...`;
},
});
const calcTool = client.betaZodTool({
name: "calculate",
description: "Evaluate a math expression",
schema: z.object({ expression: z.string().describe("Math expression") }),
execute: async (input) => {
try {
const result = Function(`"use strict"; return (${input.expression})`)();
return `Result: ${input.expression} = ${result}`;
} catch (e) {
return `Calculation error: ${e}`;
}
},
});
// Run the agentic loop with Tool Runner
const runner = client.messages.toolRunner({
model: "claude-sonnet-4-6",
max_tokens: 4096,
tools: [searchTool, calcTool],
messages: [{
role: "user",
content: "What is Japan's population? Calculate what percentage of the world population that represents.",
}],
});
// Process each message as it arrives
for await (const message of runner) {
console.log(`[${message.role}]`);
for (const block of message.content) {
if ("text" in block) {
console.log(block.text);
} else if (block.type === "tool_use") {
console.log(` → Tool call: ${block.name}(${JSON.stringify(block.input)})`);
}
}
}Managing Context with Automatic Compaction
In long-running agent sessions, tool call results accumulate and token counts grow rapidly. The Tool Runner's automatic compaction feature generates conversation summaries when token usage exceeds a configurable threshold, keeping everything within the context window.
# Python: Tool Runner with automatic compaction
result = client.messages.tool_runner(
model="claude-sonnet-4-6",
max_tokens=4096,
tools=[web_search, calculate],
messages=messages,
compaction={
"enabled": True,
"threshold_tokens": 50000, # Trigger summarization above this threshold
},
)// TypeScript: Tool Runner with automatic compaction
const runner = client.messages.toolRunner({
model: "claude-sonnet-4-6",
max_tokens: 4096,
tools: [searchTool, calcTool],
messages,
compaction: {
enabled: true,
thresholdTokens: 50000,
},
});When compaction triggers, older messages are replaced with a summary while preserving key information, allowing the conversation to continue seamlessly.
Enforcing Schema Compliance with strict: true
Adding strict: true to your tool definitions enables Structured Outputs for tool calls, guaranteeing that Claude's generated inputs always match your specified schema exactly.
tools = [
{
"name": "create_user",
"description": "Create a new user account",
"strict": True, # Enable strict schema validation
"input_schema": {
"type": "object",
"properties": {
"name": {"type": "string"},
"email": {"type": "string", "format": "email"},
"age": {"type": "integer", "minimum": 0}
},
"required": ["name", "email", "age"],
"additionalProperties": False # Required when strict is true
}
}
]With strict: true, you can eliminate type mismatches and missing fields entirely, significantly reducing runtime errors during tool execution.
Wrapping Up
The Claude SDK Tool Runner and Tool Helpers dramatically simplify the process of building agentic applications. By delegating loop management, error handling, and context management to the SDK, you can focus on what matters most: your application's business logic and the tools that power it.
For a deeper dive into tool definitions and advanced patterns, check out the complete guide to Claude API tool use. If you're working with TypeScript, the type-safe production guide for the TypeScript SDK covers SDK-specific best practices. For cost management strategies, see the token counting and cost optimization guide.