Repository Public
1---2title: Tool runner (SDK)3url: https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-runner4description: Use the SDK's tool runner to handle the agentic loop, error wrapping, and type safety automatically.5---67The tool runner handles the agentic loop, error wrapping, and type safety so you don't have to. When you need human-in-the-loop approval, custom logging, or conditional execution, use the [manual loop](https://platform.claude.com/docs/en/agents-and-tools/tool-use/handle-tool-calls) instead.89Instead of manually handling tool calls, tool results, and conversation management, the tool runner automatically:1011* Runs tools when Claude calls them12* Handles the request/response cycle13* Manages conversation state14* Provides type safety and validation1516<Note>17 The tool runner is in beta and available in the [Python SDK](https://github.com/anthropics/anthropic-sdk-python/blob/main/tools.md), [TypeScript SDK](https://github.com/anthropics/anthropic-sdk-typescript/blob/main/helpers.md#tool-helpers), [C# SDK](https://github.com/anthropics/anthropic-sdk-csharp/blob/main/examples/ToolRunnerExample/Program.cs), [Go SDK](https://github.com/anthropics/anthropic-sdk-go/blob/main/tools.md), [Java SDK](https://github.com/anthropics/anthropic-sdk-java/blob/main/anthropic-java-example/src/main/java/com/anthropic/example/BetaToolRunnerExample.java), [PHP SDK](https://github.com/anthropics/anthropic-sdk-php/blob/main/examples/beta/beta_tool_runner.php), and [Ruby SDK](https://github.com/anthropics/anthropic-sdk-ruby/blob/main/helpers.md#3-auto-looping-tool-runner-beta).18</Note>1920## Basic usage2122Define tools using the SDK helpers, then use the tool runner to run them.2324Depending on the SDK's tool signature, a tool returns its result as a string or as content blocks (text, image, or document blocks), so a tool can return multimodal results. A returned string becomes a single text content block. To return structured data, such as a JSON object or a number, encode it as a string first.2526<Tabs>27 <Tab title="Python">28 Use the `@beta_tool` decorator to define tools with type hints and docstrings.2930 <Note>31 If you're using the async client, replace `@beta_tool` with `@beta_async_tool` and define the function with `async def`.32 </Note>3334 ```python35 import json36 from anthropic import Anthropic, beta_tool3738 client = Anthropic()394041 @beta_tool42 def get_weather(location: str, unit: str = "fahrenheit") -> str:43 """Get the current weather in a given location.4445 Args:46 location: The city and state, e.g. San Francisco, CA47 unit: Temperature unit, either 'celsius' or 'fahrenheit'48 """49 return json.dumps({"temperature": "20°C", "condition": "Sunny"})505152 @beta_tool53 def calculate_sum(a: int, b: int) -> str:54 """Add two numbers together.5556 Args:57 a: First number58 b: Second number59 """60 return str(a + b)616263 runner = client.beta.messages.tool_runner(64 model="claude-opus-5",65 max_tokens=1024,66 tools=[get_weather, calculate_sum],67 messages=[68 {69 "role": "user",70 "content": "What's the weather like in Paris? Also, what's 15 + 27?",71 }72 ],73 )74 for message in runner:75 print(message)76 ```7778 The `@beta_tool` decorator inspects the function arguments and docstring to derive the JSON schema for you.79 </Tab>8081 <Tab title="TypeScript">82 Use `betaZodTool()` for type-safe tool definitions with Zod validation, or `betaTool()` for JSON Schema-based definitions.8384 TypeScript offers two approaches for defining tools:8586 **Using Zod (recommended)** - Use `betaZodTool()` for type-safe tool definitions with Zod validation (requires Zod 3.25.0 or higher):8788 ```typescript89 import Anthropic from "@anthropic-ai/sdk";90 import { betaZodTool } from "@anthropic-ai/sdk/helpers/beta/zod";91 import { z } from "zod";9293 const client = new Anthropic();9495 const getWeatherTool = betaZodTool({96 name: "get_weather",97 description: "Get the current weather in a given location",98 inputSchema: z.object({99 location: z.string().describe("The city and state, e.g. San Francisco, CA"),100 unit: z.enum(["celsius", "fahrenheit"]).default("fahrenheit").describe("Temperature unit")101 }),102 run: async (input) => {103 return JSON.stringify({ temperature: "20°C", condition: "Sunny" });104 }105 });106107 const finalMessage = await client.beta.messages.toolRunner({108 model: "claude-opus-5",109 max_tokens: 1024,110 tools: [getWeatherTool],111 messages: [{ role: "user", content: "What's the weather like in Paris?" }]112 });113114 for (const block of finalMessage.content) {115 if (block.type === "text") {116 console.log(block.text);117 }118 }119 ```120121 **Using JSON Schema** - Use `betaTool()` for type-safe tool definitions without Zod:122123 <Note>124 The input generated by Claude is not validated at runtime. Perform validation inside the `run` function if needed.125 </Note>126127 ```typescript128 import Anthropic from "@anthropic-ai/sdk";129 import { betaTool } from "@anthropic-ai/sdk/helpers/beta/json-schema";130131 const client = new Anthropic();132133 const calculateSumTool = betaTool({134 name: "calculate_sum",135 description: "Add two numbers together",136 inputSchema: {137 type: "object",138 properties: {139 a: { type: "number", description: "First number" },140 b: { type: "number", description: "Second number" }141 },142 required: ["a", "b"]143 },144 run: async (input) => {145 return String(input.a + input.b);146 }147 });148149 const finalMessage = await client.beta.messages.toolRunner({150 model: "claude-opus-5",151 max_tokens: 1024,152 tools: [calculateSumTool],153 messages: [{ role: "user", content: "What's 15 + 27?" }]154 });155156 for (const block of finalMessage.content) {157 if (block.type === "text") {158 console.log(block.text);159 }160 }161 ```162 </Tab>163164 <Tab title="C#">165 Define each tool as a `BetaRunnableTool`, providing a `Definition` with a JSON schema and a `Run` delegate that runs when Claude calls the tool.166167 ```csharp168 using System.Text.Json;169 using Anthropic;170 using Anthropic.Helpers.Beta;171 using Anthropic.Models.Beta.Messages;172 using MessageCreateParams = Anthropic.Models.Beta.Messages.MessageCreateParams;173 using InputSchema = Anthropic.Models.Beta.Messages.InputSchema;174 using Role = Anthropic.Models.Beta.Messages.Role;175 using Model = Anthropic.Models.Messages.Model;176177 var client = new AnthropicClient();178179 var getWeatherTool = new BetaRunnableTool180 {181 Name = "get_weather",182 Definition = new BetaTool183 {184 Name = "get_weather",185 Description = "Get the current weather in a given location.",186 InputSchema = new InputSchema187 {188 Properties = new Dictionary<string, JsonElement>189 {190 ["location"] = JsonSerializer.SerializeToElement(191 new { type = "string", description = "The city and state, e.g. San Francisco, CA" }192 ),193 },194 Required = ["location"],195 },196 },197 Run = (toolUse, _) =>198 {199 var location = toolUse.Input["location"].GetString();200 return Task.FromResult<BetaToolResultBlockParamContent>(201 $"Weather in {location}: 20°C, sunny"202 );203 },204 };205206 var calculateSumTool = new BetaRunnableTool207 {208 Name = "calculate_sum",209 Definition = new BetaTool210 {211 Name = "calculate_sum",212 Description = "Add two numbers together.",213 InputSchema = new InputSchema214 {215 Properties = new Dictionary<string, JsonElement>216 {217 ["a"] = JsonSerializer.SerializeToElement(new { type = "number" }),218 ["b"] = JsonSerializer.SerializeToElement(new { type = "number" }),219 },220 Required = ["a", "b"],221 },222 },223 Run = (toolUse, _) =>224 {225 var a = toolUse.Input["a"].GetDouble();226 var b = toolUse.Input["b"].GetDouble();227 return Task.FromResult<BetaToolResultBlockParamContent>($"{a + b}");228 },229 };230231 var runner = client.Beta.Messages.ToolRunner(232 new MessageCreateParams233 {234 Model = Model.ClaudeOpus5,235 MaxTokens = 1024,236 Messages =237 [238 new()239 {240 Role = Role.User,241 Content = "What's the weather like in Paris? Also, what's 15 + 27?",242 },243 ],244 },245 [getWeatherTool, calculateSumTool]246 );247248 await foreach (var message in runner)249 {250 Console.WriteLine(message);251 }252 ```253 </Tab>254255 <Tab title="Go">256 Define a tool with `toolrunner.NewBetaToolFromJSONSchema`. The handler's input type is a struct with `jsonschema:` tags. The SDK reflects on it to generate the JSON schema.257258 ```go259 package main260261 import (262 "context"263 "fmt"264 "log"265266 "github.com/anthropics/anthropic-sdk-go"267 "github.com/anthropics/anthropic-sdk-go/toolrunner"268 )269270 type GetWeatherInput struct {271 Location string `json:"location" jsonschema:"required,description=The city and state, e.g. San Francisco, CA"`272 Unit string `json:"unit,omitempty" jsonschema:"enum=celsius,enum=fahrenheit,description=Temperature unit"`273 }274275 type CalculateSumInput struct {276 A int `json:"a" jsonschema:"required,description=First number"`277 B int `json:"b" jsonschema:"required,description=Second number"`278 }279280 func main() {281 client := anthropic.NewClient()282 ctx := context.Background()283284 getWeather, err := toolrunner.NewBetaToolFromJSONSchema(285 "get_weather",286 "Get the current weather in a given location.",287 func(ctx context.Context, input GetWeatherInput) (anthropic.BetaToolResultBlockParamContentUnion, error) {288 return anthropic.BetaToolResultBlockParamContentUnion{289 OfText: &anthropic.BetaTextBlockParam{Text: "20°C, Sunny"},290 }, nil291 },292 )293 if err != nil {294 log.Fatal(err)295 }296297 calculateSum, err := toolrunner.NewBetaToolFromJSONSchema(298 "calculate_sum",299 "Add two numbers together.",300 func(ctx context.Context, input CalculateSumInput) (anthropic.BetaToolResultBlockParamContentUnion, error) {301 return anthropic.BetaToolResultBlockParamContentUnion{302 OfText: &anthropic.BetaTextBlockParam{Text: fmt.Sprintf("%d", input.A+input.B)},303 }, nil304 },305 )306 if err != nil {307 log.Fatal(err)308 }309310 runner := client.Beta.Messages.NewToolRunner(311 []anthropic.BetaTool{getWeather, calculateSum},312 anthropic.BetaToolRunnerParams{313 BetaMessageNewParams: anthropic.BetaMessageNewParams{314 Model: anthropic.ModelClaudeOpus5,315 MaxTokens: 1024,316 Messages: []anthropic.BetaMessageParam{317 anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock(318 "What's the weather like in Paris? Also, what's 15 + 27?",319 )),320 },321 },322 },323 )324325 for message, err := range runner.All(ctx) {326 if err != nil {327 log.Fatal(err)328 }329 fmt.Println(message)330 }331 }332 ```333334 The `jsonschema:` struct tags generate the input schema. For example, `CalculateSumInput` becomes:335336 ```json337 {338 "name": "calculate_sum",339 "description": "Add two numbers together.",340 "input_schema": {341 "type": "object",342 "properties": {343 "a": { "type": "integer", "description": "First number" },344 "b": { "type": "integer", "description": "Second number" }345 },346 "required": ["a", "b"]347 }348 }349 ```350 </Tab>351352 <Tab title="Java">353 Define each tool as a class implementing `Supplier<String>`. Annotate the class with `@JsonClassDescription` for the tool description, and each public field with `@JsonPropertyDescription` for parameter descriptions. The SDK derives the JSON schema, tool name (snake-cased class name), and input parsing from the class, and marks the tool with `strict: true` ([strict tool use](https://platform.claude.com/docs/en/agents-and-tools/tool-use/strict-tool-use)).354355 ```java356 import com.anthropic.client.AnthropicClient;357 import com.anthropic.client.okhttp.AnthropicOkHttpClient;358 import com.anthropic.helpers.BetaToolRunner;359 import com.anthropic.models.beta.messages.BetaMessage;360 import com.anthropic.models.beta.messages.MessageCreateParams;361 import com.anthropic.models.messages.Model;362 import com.fasterxml.jackson.annotation.JsonClassDescription;363 import com.fasterxml.jackson.annotation.JsonPropertyDescription;364 import java.util.function.Supplier;365366 @JsonClassDescription("Get the current weather in a given location")367 static class GetWeather implements Supplier<String> {368 @JsonPropertyDescription("The city and state, e.g. San Francisco, CA")369 public String location;370371 @JsonPropertyDescription("Temperature unit, either 'celsius' or 'fahrenheit'")372 public String unit;373374 @Override375 public String get() {376 return "{\"temperature\": \"20°C\", \"condition\": \"Sunny\"}";377 }378 }379380 @JsonClassDescription("Add two numbers together")381 static class CalculateSum implements Supplier<String> {382 @JsonPropertyDescription("First number")383 public double a;384385 @JsonPropertyDescription("Second number")386 public double b;387388 @Override389 public String get() {390 return String.valueOf(a + b);391 }392 }393394 void main() {395 AnthropicClient client = AnthropicOkHttpClient.fromEnv();396397 BetaToolRunner runner = client.beta()398 .messages()399 .toolRunner(MessageCreateParams.builder()400 .model(Model.CLAUDE_OPUS_5)401 .maxTokens(1024)402 .addBeta("structured-outputs-2025-11-13")403 .addUserMessage("What's the weather like in Paris? Also, what's 15 + 27?")404 .addTool(GetWeather.class)405 .addTool(CalculateSum.class)406 .build());407408 for (BetaMessage message : runner) {409 IO.println(message);410 }411 }412 ```413414 The class name `CalculateSum` becomes the tool name `calculate_sum`, and the SDK generates a JSON schema from the annotated fields:415416 ```json417 {418 "name": "calculate_sum",419 "description": "Add two numbers together",420 "input_schema": {421 "type": "object",422 "properties": {423 "a": { "description": "First number", "type": "number" },424 "b": { "description": "Second number", "type": "number" }425 },426 "required": ["a", "b"],427 "additionalProperties": false428 },429 "strict": true430 }431 ```432 </Tab>433434 <Tab title="PHP">435 Define each tool as a `BetaRunnableTool` that pairs the tool's JSON schema definition with a closure that runs it.436437 ```php438 <?php439440 use Anthropic\Client;441 use Anthropic\Beta\Messages\BetaTextBlock;442 use Anthropic\Beta\Messages\BetaToolUseBlock;443 use Anthropic\Lib\Tools\BetaRunnableTool;444 use Anthropic\Messages\Model;445446 $client = new Client();447448 $getWeather = new BetaRunnableTool(449 definition: [450 'name' => 'get_weather',451 'description' => 'Get the current weather in a given location.',452 'input_schema' => [453 'type' => 'object',454 'properties' => [455 'location' => [456 'type' => 'string',457 'description' => 'The city and state, e.g. San Francisco, CA',458 ],459 'unit' => [460 'type' => 'string',461 'enum' => ['celsius', 'fahrenheit'],462 ],463 ],464 'required' => ['location'],465 ],466 ],467 run: fn (array $input): string => json_encode([468 'temperature' => '20°C',469 'condition' => 'Sunny',470 ]),471 );472473 $calculateSum = new BetaRunnableTool(474 definition: [475 'name' => 'calculate_sum',476 'description' => 'Add two numbers together.',477 'input_schema' => [478 'type' => 'object',479 'properties' => [480 'a' => ['type' => 'number', 'description' => 'First number'],481 'b' => ['type' => 'number', 'description' => 'Second number'],482 ],483 'required' => ['a', 'b'],484 ],485 ],486 run: fn (array $input): string => (string) ($input['a'] + $input['b']),487 );488489 $runner = $client->beta->messages->toolRunner(490 maxTokens: 1024,491 messages: [492 ['role' => 'user', 'content' => "What's the weather like in Paris? Also, what's 15 + 27?"],493 ],494 model: Model::CLAUDE_OPUS_5,495 tools: [$getWeather, $calculateSum],496 );497498 foreach ($runner as $message) {499 foreach ($message->content as $block) {500 switch (true) {501 case $block instanceof BetaTextBlock:502 echo $block->text, "\n";503 break;504 case $block instanceof BetaToolUseBlock:505 echo "[Tool call: {$block->name}]\n";506 break;507 }508 }509 }510 ```511 </Tab>512513 <Tab title="Ruby">514 Use the `Anthropic::BaseTool` class to define tools with typed input schemas.515516 ```ruby517 require "anthropic"518519 # Initialize client520 client = Anthropic::Client.new521522 # Define input schema523 class GetWeatherInput < Anthropic::BaseModel524 required :location, String, doc: "The city and state, e.g. San Francisco, CA"525 optional :unit, Anthropic::InputSchema::EnumOf["celsius", "fahrenheit"],526 doc: "Temperature unit"527 end528529 # Define tool530 class GetWeather < Anthropic::BaseTool531 doc "Get the current weather in a given location"532 input_schema GetWeatherInput533534 def call(input)535 # In a full implementation, you'd call a weather API here536 JSON.generate({temperature: "20°C", condition: "Sunny"})537 end538 end539540 class CalculateSumInput < Anthropic::BaseModel541 required :a, Integer, doc: "First number"542 required :b, Integer, doc: "Second number"543 end544545 class CalculateSum < Anthropic::BaseTool546 doc "Add two numbers together"547 input_schema CalculateSumInput548549 def call(input)550 (input.a + input.b).to_s551 end552 end553554 # Use the tool runner555 runner = client.beta.messages.tool_runner(556 model: "claude-opus-5",557 max_tokens: 1024,558 tools: [GetWeather.new, CalculateSum.new],559 messages: [560 {role: "user", content: "What's the weather like in Paris? Also, what's 15 + 27?"}561 ]562 )563564 runner.each_message do |message|565 message.content.each do |block|566 puts block.text if block.type == :text567 end568 end569 ```570571 The `Anthropic::BaseTool` class uses the `doc` method for the tool description and `input_schema` to define the expected parameters. The SDK automatically converts this to the appropriate JSON schema format.572 </Tab>573</Tabs>574575## Iterating over the tool runner576577The tool runner is an iterable that yields messages from Claude. On each iteration, the runner checks whether Claude requested a tool use. If so, it runs the tool and sends the result back to Claude automatically, then yields the next message from Claude to continue your loop.578579You can end the loop at any iteration with a `break` statement. The runner loops until Claude returns a message without a tool use, or until it reaches `max_iterations` if you set it.580581If you don't need intermediate messages, you can get the final message directly:582583<Tabs>584 <Tab title="Python">585 Use `runner.until_done()` to get the final message.586587 ```python588 client = anthropic.Anthropic()589 # ...590 runner = client.beta.messages.tool_runner(591 model="claude-opus-5",592 max_tokens=1024,593 tools=[get_weather, calculate_sum],594 messages=[595 {596 "role": "user",597 "content": "What's the weather like in Paris? Also, what's 15 + 27?",598 }599 ],600 )601 final_message = runner.until_done()602 for block in final_message.content:603 if block.type == "text":604 print(block.text)605 ```606 </Tab>607608 <Tab title="TypeScript">609 `await` the runner to get the final message.610611 ```typescript612 const client = new Anthropic();613 // ...614 const runner = client.beta.messages.toolRunner({615 model: "claude-opus-5",616 max_tokens: 1024,617 tools: [getWeatherTool],618 messages: [{ role: "user", content: "What's the weather like in Paris?" }]619 });620621 const finalMessage = await runner;622 for (const block of finalMessage.content) {623 if (block.type === "text") {624 console.log(block.text);625 }626 }627 ```628 </Tab>629630 <Tab title="C#">631 Use `runner.RunUntilDoneAsync()` to get the final message.632633 ```csharp634 var client = new AnthropicClient();635 // ...636 var runner = client.Beta.Messages.ToolRunner(637 new MessageCreateParams638 {639 Model = Model.ClaudeOpus5,640 MaxTokens = 1024,641 Messages =642 [643 new()644 {645 Role = Role.User,646 Content = "What's the weather like in Paris?",647 },648 ],649 },650 [getWeatherTool]651 );652653 var finalMessage = await runner.RunUntilDoneAsync();654 foreach (var block in finalMessage.Content)655 {656 if (block.TryPickText(out var textBlock))657 {658 Console.WriteLine(textBlock.Text);659 }660 }661 ```662 </Tab>663664 <Tab title="Go">665 Use `runner.RunToCompletion(ctx)` to get the final message.666667 ```go668 client := anthropic.NewClient()669 ctx := context.Background()670 // ...671 runner := client.Beta.Messages.NewToolRunner(672 []anthropic.BetaTool{getWeather},673 anthropic.BetaToolRunnerParams{674 BetaMessageNewParams: anthropic.BetaMessageNewParams{675 Model: anthropic.ModelClaudeOpus5,676 MaxTokens: 1024,677 Messages: []anthropic.BetaMessageParam{678 anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock(679 "What's the weather like in Paris?",680 )),681 },682 },683 },684 )685686 finalMessage, err := runner.RunToCompletion(ctx)687 if err != nil {688 log.Fatal(err)689 }690 for _, block := range finalMessage.Content {691 if textBlock, ok := block.AsAny().(anthropic.BetaTextBlock); ok {692 fmt.Println(textBlock.Text)693 }694 }695 ```696 </Tab>697698 <Tab title="Java">699 The Java SDK has no `until_done()` shortcut. Iterate to exhaustion and keep the last message.700701 ```java702 AnthropicClient client = AnthropicOkHttpClient.fromEnv();703704 BetaToolRunner runner = client.beta()705 .messages()706 .toolRunner(MessageCreateParams.builder()707 .model(Model.CLAUDE_OPUS_5)708 .maxTokens(1024)709 .addBeta("structured-outputs-2025-11-13")710 .addUserMessage("What's the weather like in Paris? Also, what's 15 + 27?")711 .addTool(GetWeather.class)712 .addTool(CalculateSum.class)713 .build());714715 BetaMessage finalMessage = null;716 for (BetaMessage message : runner) {717 finalMessage = message;718 }719 for (BetaContentBlock block : finalMessage.content()) {720 block.text().ifPresent(textBlock -> IO.println(textBlock.text()));721 }722 ```723 </Tab>724725 <Tab title="PHP">726 Use `runUntilDone()` to get the final message.727728 ```php729 $client = new Client();730 // ...731 $runner = $client->beta->messages->toolRunner(732 maxTokens: 1024,733 messages: [734 ['role' => 'user', 'content' => "What's the weather like in Paris? Also, what's 15 + 27?"],735 ],736 model: Model::CLAUDE_OPUS_5,737 tools: [$getWeather, $calculateSum],738 );739740 $finalMessage = $runner->runUntilDone();741 foreach ($finalMessage->content as $block) {742 if ($block->type === 'text') {743 echo $block->text, "\n";744 }745 }746 ```747 </Tab>748749 <Tab title="Ruby">750 Use `runner.run_until_finished` to get all messages.751752 ```ruby753 client = Anthropic::Client.new754 # ...755 runner = client.beta.messages.tool_runner(756 model: "claude-opus-5",757 max_tokens: 1024,758 tools: [GetWeather.new, CalculateSum.new],759 messages: [760 {role: "user", content: "What's the weather like in Paris? Also, what's 15 + 27?"}761 ]762 )763764 all_messages = runner.run_until_finished765 all_messages.each { |msg| puts msg.content }766 ```767 </Tab>768</Tabs>769770## Advanced usage771772Within the loop, you can read each response message and modify the runner's state before the next API call. Each iteration follows this lifecycle:7737741. The runner sends a request to the Messages API with its current state.7757762. The runner yields the response message to your loop body.7777783. Your loop body runs. You can read the message and optionally modify the runner's state.7797804. When your loop body returns, the runner checks whether you modified its message history.781782 * **If you did not modify message history:** If the message contains tool calls, the runner appends the assistant message and the tool results, then continues. If there are no tool calls, the loop exits.783 * **If you modified message history:** The runner skips its automatic append and uses your state unchanged. See [Taking over message history](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-runner#taking-over-message-history).784785```mermaid786sequenceDiagram787 participant U as Your code788 participant TR as ToolRunner789 participant API as Messages API790791 loop For each iteration792 TR->>API: Send request with current state793 API-->>TR: Response message794 TR-->>U: Yield message795 note over U: Your loop body runs796 U->>TR: Resume797 alt Message history unchanged798 TR->>TR: If tool calls, append assistant<br/>message + tool results and continue.<br/>If none, exit the loop799 else Message history changed800 TR->>TR: Use your state unchanged801 end802 end803```804805### Taking over message history806807By default, the runner manages conversation state for you: after each tool-call turn, it appends the assistant message and any tool results to its own message history. You take over message history when you want to retry a turn (discard the response and resend), inject a follow-up message, or build the tool result yourself.808809You take over by modifying the runner's messages from inside the loop body. The exact method depends on the SDK. See the per-language tabs that follow.810811When you take over for an iteration, the runner does not append the assistant message or tool results from that turn. You become responsible for keeping the conversation valid: append the assistant message and a tool result yourself (if you want the turn to count), modify state conditionally so the loop can still exit when there are no tool calls, and pass `max_iterations` to bound the loop. All seven SDKs support `max_iterations`.812813<Tabs>814 <Tab title="Python">815 Use `generate_tool_call_response()` to inspect or compute the tool result. Calling `append_messages()` inside the loop tells the runner you're managing history yourself, so include the assistant message and tool result in what you append.816817 ```python818 runner = client.beta.messages.tool_runner(819 model="claude-opus-5",820 max_tokens=1024,821 max_iterations=10,822 tools=[get_weather],823 messages=[{"role": "user", "content": "What's the weather in San Francisco?"}],824 )825826 for message in runner:827 tool_response = runner.generate_tool_call_response()828 if tool_response is not None:829 # append_messages() flags state as modified, so the runner skips its830 # automatic append for this iteration. Append the assistant message and831 # tool result yourself, plus any follow-up.832 runner.append_messages(833 message,834 tool_response,835 {"role": "user", "content": "Please be concise."},836 )837 # When there's no tool call, leave state untouched so the loop exits.838 ```839840 To change request parameters such as `max_tokens` without taking over message history, use `set_messages_params()`. The runner still appends the assistant message and tool result automatically.841842 ```python843 for message in runner:844 runner.set_messages_params(lambda params: {**params, "max_tokens": 2048})845 ```846 </Tab>847848 <Tab title="TypeScript">849 Use `runner.params` to read the current request parameters and `setMessagesParams()` to replace them. Calling `setMessagesParams()` or `pushMessages()` inside the loop tells the runner you're managing state yourself: the assistant message and tool result from this iteration are dropped, and the next request goes out with your state.850851 The following example retries a truncated response with a larger `max_tokens` budget.852853 ```typescript854 const runner = client.beta.messages.toolRunner({855 model: "claude-opus-5",856 max_tokens: 1024,857 max_iterations: 10,858 tools: [getWeatherTool],859 messages: [860 {861 role: "user",862 content: "Give me a detailed weather report for every major US city."863 }864 ]865 });866867 const MAX_TOKEN_CEILING = 8192;868869 for await (const message of runner) {870 if (message.stop_reason === "max_tokens") {871 const current = runner.params.max_tokens;872 if (current >= MAX_TOKEN_CEILING) {873 console.warn(`Hit ceiling (${MAX_TOKEN_CEILING}); stopping.`);874 break;875 }876 const doubled = Math.min(current * 2, MAX_TOKEN_CEILING);877 console.log(`Response truncated at ${current} tokens; retrying with ${doubled}.`);878 // Bump the budget. setMessagesParams() flags state as modified, so the879 // runner does NOT append the truncated message. The next iteration retries880 // the same turn with the larger budget.881 runner.setMessagesParams((params) => ({ ...params, max_tokens: doubled }));882 }883 // Otherwise leave state untouched so the runner auto-appends and continues.884 }885 ```886 </Tab>887888 <Tab title="C#">889 Calling `SetParams()` or `PushMessages()` flags state as modified, which causes the runner to skip its auto-append for that turn. The C# runner still runs the matched tools for that turn and discards their auto-built results, so a tool you also run yourself inside the loop body runs twice unless you account for it. When you take over, push the assistant message and a tool result yourself. Otherwise the conversation won't make forward progress. The C# runner always exits when a response has no tool calls, so condition any state mutation on the presence of a `tool_use` block.890891 ```csharp892 var runner = client.Beta.Messages.ToolRunner(893 new MessageCreateParams894 {895 Model = Model.ClaudeOpus5,896 MaxTokens = 1024,897 Messages = [new() { Role = Role.User, Content = "What's the weather in San Francisco?" }],898 },899 [getWeatherTool],900 maxIterations: 10901 );902903 await foreach (var message in runner)904 {905 var toolUseBlock = message906 .Content.Select(block => block.TryPickToolUse(out var toolUse) ? toolUse : null)907 .FirstOrDefault(toolUse => toolUse is not null);908909 if (toolUseBlock is null)910 {911 // No tool call: leave state untouched so the loop exits normally.912 continue;913 }914915 // Run the tool yourself and build the result block.916 var toolResult = new BetaToolResultBlockParam(toolUseBlock.ID)917 {918 Content = await getWeatherTool.ExecuteAsync(toolUseBlock, default),919 };920921 // PushMessages() flags state as modified; the runner skips its auto-append.922 // Supply the assistant turn and the tool result yourself, then add a follow-up.923 runner.PushMessages(924 new()925 {926 Role = Role.Assistant,927 Content = new BetaMessageParamContent(928 JsonSerializer.SerializeToElement(929 message.Content.Select(block => block.Json).ToArray()930 )931 ),932 },933 new()934 {935 Role = Role.User,936 Content = new List<BetaContentBlockParam> { toolResult },937 },938 new() { Role = Role.User, Content = "Please be concise in your response." }939 );940 }941 ```942 </Tab>943944 <Tab title="Go">945 The Go runner exposes parameters as a public `Params` field. Modifying `runner.Params` between calls to `NextMessage(ctx)` applies to the next API request. Unlike other SDKs, the Go runner always appends the assistant message and tool results unconditionally. Modifying `Params` does not suppress that step.946947 ```go948 runner := client.Beta.Messages.NewToolRunner(949 []anthropic.BetaTool{getWeather},950 anthropic.BetaToolRunnerParams{951 BetaMessageNewParams: anthropic.BetaMessageNewParams{952 Model: anthropic.ModelClaudeOpus5,953 MaxTokens: 1024,954 Messages: []anthropic.BetaMessageParam{955 anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock(956 "What's the weather in San Francisco?",957 )),958 },959 },960 MaxIterations: 10,961 },962 )963964 for {965 message, err := runner.NextMessage(ctx)966 if err != nil {967 log.Fatal(err)968 }969 if message == nil {970 break // conversation complete971 }972973 // The Go runner always appends the assistant message and tool results.974 // Param changes here apply to the next iteration.975 runner.Params.MaxTokens = 2048976 }977 ```978 </Tab>979980 <Tab title="Java">981 Use `runner.params()` to read the current parameters and `runner.setNextParams()` to replace them for the next iteration. When you call `setNextParams()` inside the loop, the runner skips its automatic append. The just-yielded message is discarded, and the next iteration sends your new params unchanged.982983 The following example retries a turn that hit the token limit by doubling `max_tokens`. Mutating only on the `max_tokens` branch keeps the loop converging: turns that complete normally fall through, and the runner auto-appends and exits when there are no more tool calls.984985 ```java986 BetaToolRunner runner = client.beta()987 .messages()988 .toolRunner(ToolRunnerCreateParams.builder()989 .initialMessageParams(MessageCreateParams.builder()990 .model(Model.CLAUDE_OPUS_5)991 .maxTokens(1024)992 .addBeta("structured-outputs-2025-11-13")993 .addUserMessage("Give me a detailed weather report for every major US city.")994 .addTool(GetWeather.class)995 .build())996 .maxIterations(10L)997 .build());998999 long ceiling = 8192;10001001 for (BetaMessage message : runner) {1002 if (BetaStopReason.MAX_TOKENS.equals(message.stopReason().orElse(null))) {1003 long current = runner.params().maxTokens();1004 if (current >= ceiling) {1005 IO.println("Hit ceiling (" + ceiling + "), accepting truncated response.");1006 break;1007 }1008 long doubled = Math.min(current * 2, ceiling);1009 IO.println("Response truncated at " + current + " tokens, retrying with " + doubled + ".");10101011 // Calling setNextParams() flags this turn as user-managed: the runner1012 // does NOT auto-append the truncated message, so the next iteration1013 // re-sends the same conversation prefix with the larger budget.1014 runner.setNextParams(runner.params().toBuilder().maxTokens(doubled).build());1015 }1016 // No mutation on a normal turn: the runner auto-appends and continues.1017 }1018 ```1019 </Tab>10201021 <Tab title="PHP">1022 Use `setMessagesParams()` and `pushMessages()` to modify the runner's state, and `getParams()` to read it. Calling either setter inside the loop tells the runner to skip its automatic append, so the conversation continues from your modified state instead.10231024 The following example doubles `max_tokens` and retries when a response is cut off.10251026 ```php1027 use Anthropic\Beta\Messages\BetaStopReason;10281029 $runner = $client->beta->messages->toolRunner(1030 maxTokens: 1024,1031 messages: [1032 ['role' => 'user', 'content' => 'Give a detailed weather report for every major US city.'],1033 ],1034 model: Model::CLAUDE_OPUS_5,1035 tools: [$getWeather],1036 maxIterations: 10,1037 );10381039 $maxTokenCeiling = 8192;10401041 foreach ($runner as $message) {1042 if ($message->stopReason === BetaStopReason::MAX_TOKENS->value) {1043 $current = $runner->getParams()['maxTokens'];10441045 if ($current >= $maxTokenCeiling) {1046 echo "Hit ceiling ({$maxTokenCeiling}), accepting truncated response.\n";1047 break;1048 }10491050 $doubled = min($current * 2, $maxTokenCeiling);1051 echo "Response truncated at {$current} tokens, retrying with {$doubled}.\n";10521053 // Calling setMessagesParams() inside the loop tells the runner to skip1054 // its automatic append. The truncated message is discarded; the next1055 // iteration retries with the larger budget.1056 // Keys are camelCase, matching the toolRunner() named parameters.1057 $runner->setMessagesParams(['maxTokens' => $doubled]);1058 }1059 }1060 ```1061 </Tab>10621063 <Tab title="Ruby">1064 Use `next_message` for step-by-step control. By the time `next_message` returns, the assistant message and tool result for that turn are already appended. Use `feed_messages` to inject follow-up messages between turns, and `runner.params.update(...)` to change request parameters in place.10651066 You take over message history when, from inside an `each_message` or `each_streaming` block, you reassign `runner.params[:messages]` or call `feed_messages`. The following pattern calls `feed_messages` between `next_message` calls, which does not take over.10671068 ```ruby1069 runner = client.beta.messages.tool_runner(1070 model: "claude-opus-5",1071 max_tokens: 1024,1072 max_iterations: 10,1073 tools: [GetWeather.new],1074 messages: [{role: "user", content: "What's the weather in San Francisco?"}]1075 )10761077 # Step the runner once. The assistant message and tool result are appended1078 # to runner.params[:messages] before next_message returns.1079 message = runner.next_message1080 puts message.content10811082 # Inject a follow-up before continuing. feed_messages takes a splat, not an array.1083 runner.feed_messages({role: "user", content: "Also check Boston."})10841085 # Change parameters in place. Reassigning runner.params[:messages] takes over1086 # message history only when it happens inside an each_message or each_streaming block.1087 runner.params.update(max_tokens: 2048)10881089 runner.run_until_finished1090 ```1091 </Tab>1092</Tabs>10931094### Automatic context management10951096For long-running agentic tasks, the TypeScript and Ruby tool runners support automatic [compaction](https://platform.claude.com/docs/en/build-with-claude/context-editing#client-side-compaction-sdk), which generates summaries when token usage exceeds a threshold so the conversation can continue beyond context window limits. Both SDKs have deprecated this client-side option in favor of [server-side compaction](https://platform.claude.com/docs/en/build-with-claude/compaction), which works with every SDK's tool runner through the `context_management` request parameter. The Python SDK (v1.0 and later) and the Go, Java, C#, and PHP tool runners don't include client-side compaction.10971098### Debugging tool execution10991100When a tool throws an exception, the tool runner catches it and returns the error to Claude as a tool result with `is_error: true`. The tool result carries the exception's message (in Python, its type and message), not the full stack trace.11011102What the SDK logs is language-specific. The Python SDK logs the full exception, including its stack trace, through the standard `logging` module whenever a tool raises an unhandled exception. The Python, TypeScript, and Java SDKs read the `ANTHROPIC_LOG` environment variable to turn on the SDK's logging, which includes request and response detail:11031104```bash1105# Log at info level1106export ANTHROPIC_LOG=info11071108# Log at debug level for more verbose output1109export ANTHROPIC_LOG=debug1110```11111112The Go, Ruby, C#, and PHP SDKs don't read `ANTHROPIC_LOG`. Outside Python, no SDK logs a failed tool: to see why a tool failed, catch and log the exception inside the tool function before returning or rethrowing it.11131114### Intercepting tool errors11151116By default, tool errors are passed back to Claude, which can then respond appropriately. However, you might want to detect errors and handle them differently, for example, to stop execution early or implement custom error handling.11171118In the Python and TypeScript SDKs, use the tool response method (`generate_tool_call_response()` in Python, `generateToolResponse()` in TypeScript) to intercept tool results and check for errors before they're sent to Claude. The other SDKs don't expose that hook. Their tabs describe the closest alternative:11191120<Tabs>1121 <Tab title="Python">1122 ```python1123 client = anthropic.Anthropic()1124 # ...1125 runner = client.beta.messages.tool_runner(1126 model="claude-opus-5",1127 max_tokens=1024,1128 tools=[my_tool],1129 messages=[{"role": "user", "content": "Run my_tool with the query 'hello'."}],1130 )11311132 for message in runner:1133 tool_response = runner.generate_tool_call_response()11341135 if tool_response is not None:1136 # tool_response is a dict: {"role": "user", "content": [...]}1137 # Check if any tool result has an error1138 for block in tool_response["content"]:1139 if block.get("is_error"):1140 # Option 1: Raise an exception to stop the loop1141 raise RuntimeError(f"Tool failed: {json.dumps(block['content'])}")11421143 # Option 2: Log and continue (let Claude handle it)1144 # logger.error(f"Tool error: {json.dumps(block['content'])}")11451146 # Process the message normally1147 print(message.content)1148 ```1149 </Tab>11501151 <Tab title="TypeScript">1152 ```typescript1153 const client = new Anthropic();1154 // ...1155 const runner = client.beta.messages.toolRunner({1156 model: "claude-opus-5",1157 max_tokens: 1024,1158 tools: [myTool],1159 messages: [{ role: "user", content: "Run my_tool with the query 'hello'." }]1160 });11611162 for await (const message of runner) {1163 const toolResultMessage = await runner.generateToolResponse();11641165 if (toolResultMessage && typeof toolResultMessage.content !== "string") {1166 // Check if any tool result has an error1167 for (const block of toolResultMessage.content) {1168 if (block.type === "tool_result" && block.is_error) {1169 // Option 1: Throw to stop the loop1170 throw new Error(`Tool failed: ${JSON.stringify(block.content)}`);11711172 // Option 2: Log and continue (let Claude handle it)1173 // console.error(`Tool error: ${JSON.stringify(block.content)}`);1174 }1175 }1176 }11771178 // Process the message normally1179 console.log(message.content);1180 }1181 ```1182 </Tab>11831184 <Tab title="C#">1185 The C# tool runner doesn't expose a hook for inspecting the tool result before it's sent to Claude. To control error content, throw `BetaToolError` from inside the tool body. The runner converts it to a `tool_result` with `is_error: true` and the content you supply.11861187 ```csharp1188 var client = new AnthropicClient();11891190 var getWeatherTool = new BetaRunnableTool1191 {1192 Name = "get_weather",1193 Definition = new BetaTool1194 {1195 Name = "get_weather",1196 Description = "Get the current weather in a given location.",1197 InputSchema = new InputSchema1198 {1199 Properties = new Dictionary<string, JsonElement>1200 {1201 ["location"] = JsonSerializer.SerializeToElement(new { type = "string" }),1202 },1203 Required = ["location"],1204 },1205 },1206 Run = async (toolUse, cancellationToken) =>1207 {1208 try1209 {1210 return await CallExternalWeatherService(1211 toolUse.Input["location"].GetString()!,1212 cancellationToken1213 );1214 }1215 catch (HttpRequestException ex)1216 {1217 // Log here if you need to inspect the failure before Claude sees it.1218 throw new BetaToolError($"Weather service unavailable: {ex.Message}");1219 }1220 },1221 };12221223 var runner = client.Beta.Messages.ToolRunner(1224 new MessageCreateParams1225 {1226 Model = Model.ClaudeOpus5,1227 MaxTokens = 1024,1228 Messages =1229 [1230 new() { Role = Role.User, Content = "What's the weather in San Francisco?" },1231 ],1232 },1233 [getWeatherTool]1234 );12351236 Console.WriteLine(await runner.RunUntilDoneAsync());1237 ```1238 </Tab>12391240 <Tab title="Go">1241 Intercepting tool errors before they're sent to Claude is not currently supported in the Go SDK. The runner converts an error returned from your handler into a tool result with `is_error: true` internally. To customize the error content, catch the error inside your handler and return a result instead of returning the error.1242 </Tab>12431244 <Tab title="Java">1245 Intercepting tool errors before they're sent to Claude is not currently supported in the Java SDK. The runner catches any exception thrown from a tool's `get()` method and converts it into a tool result with `is_error: true` automatically. To control the error content, catch the exception inside your tool and return a custom string.1246 </Tab>12471248 <Tab title="PHP">1249 The PHP tool runner does not currently expose tool results before they are appended. Exceptions thrown from a tool's `run` closure are caught and sent to Claude as tool results with `is_error: true` automatically. To inspect or replace error content, use the manual `pushMessages()` pattern shown in [Modifying tool results](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-runner#modifying-tool-results).1250 </Tab>12511252 <Tab title="Ruby">1253 ```ruby1254 client = Anthropic::Client.new1255 # ...1256 runner = client.beta.messages.tool_runner(1257 model: "claude-opus-5",1258 max_tokens: 1024,1259 tools: [MyTool.new],1260 messages: [{role: "user", content: "Run my_tool with the query 'hello'."}]1261 )12621263 loop do1264 message = runner.next_message1265 break unless message12661267 # By the time next_message returns, the runner has run this turn's tools and1268 # appended their results as the last (user-role) message. Inspect them here,1269 # before the next request sends them to Claude.1270 tool_results = runner.params[:messages].last12711272 if tool_results && tool_results[:role] == :user && tool_results[:content].is_a?(Array)1273 tool_results[:content].each do |block|1274 if block[:type] == :tool_result && block[:is_error]1275 # Option 1: Raise an exception to stop the loop1276 raise "Tool failed: #{block[:content]}"12771278 # Option 2: Log and continue (let Claude handle it)1279 # logger.error("Tool error: #{block[:content]}")1280 end1281 end1282 end12831284 puts message.content1285 break if message.stop_reason != :tool_use1286 end1287 ```1288 </Tab>1289</Tabs>12901291### Modifying tool results12921293You can modify tool results before they're sent back to Claude. This is useful for adding metadata such as `cache_control` to enable [prompt caching](https://platform.claude.com/docs/en/build-with-claude/prompt-caching) on tool results, or for transforming the tool output.12941295In the Python and TypeScript SDKs, use the tool response method to get the tool result, then modify it before the runner proceeds. Whether you explicitly append the modified result or mutate it in place depends on the SDK. See the code comments in each tab.12961297<Tabs>1298 <Tab title="Python">1299 ```python1300 client = anthropic.Anthropic()1301 # ...1302 runner = client.beta.messages.tool_runner(1303 model="claude-opus-5",1304 max_tokens=1024,1305 tools=[search_documents],1306 messages=[1307 {1308 "role": "user",1309 "content": "Search for information about the climate of San Francisco",1310 }1311 ],1312 )13131314 for message in runner:1315 tool_response = runner.generate_tool_call_response()13161317 if tool_response is not None:1318 # tool_response is a dict: {"role": "user", "content": [...]}1319 # Modify the tool result to add cache control1320 for block in tool_response["content"]:1321 if block["type"] == "tool_result":1322 # Add cache_control to cache this tool result1323 block["cache_control"] = {"type": "ephemeral"}13241325 # Append the modified response (this prevents auto-append of the original)1326 runner.append_messages(message, tool_response)13271328 print(message.content)1329 ```1330 </Tab>13311332 <Tab title="TypeScript">1333 ```typescript1334 const client = new Anthropic();1335 // ...1336 const runner = client.beta.messages.toolRunner({1337 model: "claude-opus-5",1338 max_tokens: 1024,1339 tools: [searchDocuments],1340 messages: [1341 { role: "user", content: "Search for information about the climate of San Francisco" }1342 ]1343 });13441345 for await (const message of runner) {1346 const toolResultMessage = await runner.generateToolResponse();13471348 if (toolResultMessage && typeof toolResultMessage.content !== "string") {1349 // Modify the tool result to add cache control1350 for (const block of toolResultMessage.content) {1351 if (block.type === "tool_result") {1352 // Add cache_control to cache this tool result1353 block.cache_control = { type: "ephemeral" };1354 }1355 }1356 // No pushMessages call needed: the runner auto-appends both the assistant1357 // message and the (now-mutated) cached tool response.1358 }13591360 console.log(message.content);1361 }1362 ```1363 </Tab>13641365 <Tab title="C#">1366 Modifying tool results before they're appended (for example, to add `cache_control`) is not currently supported in the C# SDK. The runner constructs the `tool_result` block internally and provides no hook to alter it.1367 </Tab>13681369 <Tab title="Go">1370 The Go runner does not expose a hook to modify the outer `tool_result` block. You can, however, set `cache_control` on the inner content blocks your handler returns.13711372 ```go1373 client := anthropic.NewClient()1374 ctx := context.Background()13751376 searchDocuments, err := toolrunner.NewBetaToolFromJSONSchema(1377 "search_documents",1378 "Search documents for relevant information.",1379 func(ctx context.Context, input SearchDocumentsInput) (anthropic.BetaToolResultBlockParamContentUnion, error) {1380 return anthropic.BetaToolResultBlockParamContentUnion{1381 OfText: &anthropic.BetaTextBlockParam{1382 Text: fmt.Sprintf("Found 3 documents matching: %s", input.Query),1383 // Set cache_control on the inner content block. The outer1384 // tool_result block's cache_control is not currently1385 // settable through the Go runner.1386 CacheControl: anthropic.NewBetaCacheControlEphemeralParam(),1387 },1388 }, nil1389 },1390 )1391 if err != nil {1392 log.Fatal(err)1393 }13941395 runner := client.Beta.Messages.NewToolRunner(1396 []anthropic.BetaTool{searchDocuments},1397 anthropic.BetaToolRunnerParams{1398 BetaMessageNewParams: anthropic.BetaMessageNewParams{1399 Model: anthropic.ModelClaudeOpus5,1400 MaxTokens: 1024,1401 Messages: []anthropic.BetaMessageParam{1402 anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock(1403 "Search for information about the climate of San Francisco",1404 )),1405 },1406 },1407 },1408 )14091410 finalMessage, err := runner.RunToCompletion(ctx)1411 if err != nil {1412 log.Fatal(err)1413 }1414 fmt.Println(finalMessage)1415 ```1416 </Tab>14171418 <Tab title="Java">1419 To set `cache_control` on a tool result, return `BetaToolResultBlockParam.Content` from the tool instead of `String` and set `cacheControl` on the inner text block. The runner does not currently support setting `cache_control` on the outer `tool_result` block.14201421 ```java1422 @JsonClassDescription("Look up reference documentation for a topic")1423 static class SearchDocuments implements Supplier<BetaToolResultBlockParam.Content> {1424 @JsonPropertyDescription("The search query")1425 public String query;14261427 @Override1428 public BetaToolResultBlockParam.Content get() {1429 String largeResult = "..."; // a long document worth caching1430 return BetaToolResultBlockParam.Content.ofBlocks(List.of(1431 BetaToolResultBlockParam.Content.Block.ofText(1432 BetaTextBlockParam.builder()1433 .text(largeResult)1434 .cacheControl(BetaCacheControlEphemeral.builder().build())1435 .build())));1436 }1437 }1438 ```1439 </Tab>14401441 <Tab title="PHP">1442 The PHP tool runner has no callback to mutate the auto-generated `tool_result` block. To add fields such as `cache_control`, build the tool result yourself and push it. Calling `pushMessages()` skips the runner's auto-append for that turn.14431444 ```php1445 $client = new Client();1446 // ...1447 $runner = $client->beta->messages->toolRunner(1448 maxTokens: 1024,1449 messages: [1450 ['role' => 'user', 'content' => 'Search for information about the climate of San Francisco.'],1451 ],1452 model: Model::CLAUDE_OPUS_5,1453 tools: [$searchDocuments],1454 );14551456 foreach ($runner as $message) {1457 $toolResults = [];1458 foreach ($message->content as $block) {1459 if ($block instanceof \Anthropic\Beta\Messages\BetaToolUseBlock) {1460 $toolResults[] = [1461 'type' => 'tool_result',1462 'tool_use_id' => $block->id,1463 'content' => $searchDocuments->run($block->input),1464 // Add cache_control to cache this tool result1465 'cache_control' => ['type' => 'ephemeral'],1466 ];1467 }1468 }14691470 if ($toolResults !== []) {1471 // pushMessages() flags state as mutated, so the runner skips its1472 // automatic append. Push the assistant message and tool results.1473 $runner->pushMessages(1474 ['role' => 'assistant', 'content' => $message->content],1475 ['role' => 'user', 'content' => $toolResults],1476 );1477 }1478 // No tool call: leave state untouched so the loop exits.1479 }1480 ```1481 </Tab>14821483 <Tab title="Ruby">1484 ```ruby1485 client = Anthropic::Client.new1486 # ...1487 runner = client.beta.messages.tool_runner(1488 model: "claude-opus-5",1489 max_tokens: 1024,1490 tools: [SearchDocuments.new],1491 messages: [{role: "user", content: "Search for information about the climate of San Francisco"}]1492 )14931494 loop do1495 message = runner.next_message1496 break unless message14971498 # Access the most recent tool results from the messages array1499 # The runner automatically adds tool results, but you can modify them1500 tool_results_message = runner.params[:messages].last15011502 if tool_results_message && tool_results_message[:role] == :user && tool_results_message[:content].is_a?(Array)1503 tool_results_message[:content].each do |block|1504 if block[:type] == :tool_result1505 # Modify the tool result to add cache control1506 block[:cache_control] = {type: "ephemeral"}1507 end1508 end1509 end15101511 puts message.content1512 break if message.stop_reason != :tool_use1513 end1514 ```1515 </Tab>1516</Tabs>15171518<Tip>1519 Adding `cache_control` to tool results is particularly useful when tools return large amounts of data (such as document search results) that you want to cache for subsequent API calls. See [Prompt caching](https://platform.claude.com/docs/en/build-with-claude/prompt-caching) for more details on caching strategies.1520</Tip>15211522## Streaming15231524Enable streaming to process each turn's response incrementally. Each iteration yields a stream object that you can iterate for events.15251526<Tabs>1527 <Tab title="Python">1528 Set `stream=True` and use `get_final_message()` to get the accumulated message.15291530 ```python1531 client = anthropic.Anthropic()1532 # ...1533 runner = client.beta.messages.tool_runner(1534 model="claude-opus-5",1535 max_tokens=1024,1536 tools=[calculate_sum],1537 messages=[{"role": "user", "content": "What is 15 + 27?"}],1538 stream=True,1539 )15401541 # When streaming, the runner returns BetaMessageStream1542 for message_stream in runner:1543 for event in message_stream:1544 print("event:", event)1545 print("message:", message_stream.get_final_message())15461547 print(runner.until_done())1548 ```1549 </Tab>15501551 <Tab title="TypeScript">1552 Set `stream: true` and use `finalMessage()` to get the accumulated message.15531554 ```typescript1555 const client = new Anthropic();1556 // ...1557 const runner = client.beta.messages.toolRunner({1558 model: "claude-opus-5",1559 max_tokens: 1024,1560 messages: [{ role: "user", content: "What is the weather in San Francisco?" }],1561 tools: [getWeatherTool],1562 stream: true1563 });15641565 // When streaming, the runner returns BetaMessageStream1566 for await (const messageStream of runner) {1567 for await (const event of messageStream) {1568 console.log("event:", event);1569 }1570 console.log("message:", await messageStream.finalMessage());1571 }15721573 console.log(await runner);1574 ```1575 </Tab>15761577 <Tab title="C#">1578 Call `runner.Streaming()` to get a nested async sequence: one inner stream for each API call.15791580 ```csharp1581 var client = new AnthropicClient();1582 // ...1583 var runner = client.Beta.Messages.ToolRunner(1584 new MessageCreateParams1585 {1586 Model = Model.ClaudeOpus5,1587 MaxTokens = 1024,1588 Messages =1589 [1590 new() { Role = Role.User, Content = "What is 15 + 27?" },1591 ],1592 },1593 [calculateSumTool]1594 );15951596 await foreach (var stream in runner.Streaming())1597 {1598 await foreach (var streamEvent in stream)1599 {1600 if (1601 streamEvent.TryPickContentBlockDelta(out var deltaEvent)1602 && deltaEvent.Delta.TryPickText(out var textDelta)1603 )1604 {1605 Console.Write(textDelta.Text);1606 }1607 }1608 Console.WriteLine();1609 }1610 ```1611 </Tab>16121613 <Tab title="Go">1614 Use `NewToolRunnerStreaming` and iterate `runner.AllStreaming(ctx)`. Each outer iteration yields a stream of events for one API call.16151616 ```go1617 client := anthropic.NewClient()1618 ctx := context.Background()1619 // ...1620 runner := client.Beta.Messages.NewToolRunnerStreaming(1621 []anthropic.BetaTool{calculateSum},1622 anthropic.BetaToolRunnerParams{1623 BetaMessageNewParams: anthropic.BetaMessageNewParams{1624 Model: anthropic.ModelClaudeOpus5,1625 MaxTokens: 1024,1626 Messages: []anthropic.BetaMessageParam{1627 anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("What is 15 + 27?")),1628 },1629 },1630 },1631 )16321633 for events, err := range runner.AllStreaming(ctx) {1634 if err != nil {1635 log.Fatal(err)1636 }1637 for event, err := range events {1638 if err != nil {1639 log.Fatal(err)1640 }1641 switch eventVariant := event.AsAny().(type) {1642 case anthropic.BetaRawContentBlockDeltaEvent:1643 switch deltaVariant := eventVariant.Delta.AsAny().(type) {1644 case anthropic.BetaTextDelta:1645 fmt.Print(deltaVariant.Text)1646 case anthropic.BetaInputJSONDelta:1647 fmt.Print(deltaVariant.PartialJSON)1648 }1649 case anthropic.BetaRawMessageStopEvent:1650 fmt.Println()1651 }1652 }1653 }1654 ```1655 </Tab>16561657 <Tab title="Java">1658 Call `runner.streaming()` to get a stream for each turn. Each `StreamResponse` must be closed after use.16591660 ```java1661 void main() {1662 AnthropicClient client = AnthropicOkHttpClient.fromEnv();16631664 BetaToolRunner runner = client.beta()1665 .messages()1666 .toolRunner(MessageCreateParams.builder()1667 .model(Model.CLAUDE_OPUS_5)1668 .maxTokens(1024)1669 .addBeta("structured-outputs-2025-11-13")1670 .addUserMessage("What is 15 + 27?")1671 .addTool(CalculateSum.class)1672 .build());16731674 for (StreamResponse<BetaRawMessageStreamEvent> stream : runner.streaming()) {1675 try (stream) {1676 stream.stream().forEach(event -> IO.println("event: " + event));1677 }1678 }1679 }1680 ```1681 </Tab>16821683 <Tab title="PHP">1684 Streaming is not currently available with the PHP tool runner.1685 </Tab>16861687 <Tab title="Ruby">1688 Use `each_streaming` to iterate over streaming events.16891690 ```ruby1691 client = Anthropic::Client.new1692 # ...1693 runner = client.beta.messages.tool_runner(1694 model: "claude-opus-5",1695 max_tokens: 1024,1696 tools: [CalculateSum.new],1697 messages: [{role: "user", content: "What is 15 + 27?"}]1698 )16991700 runner.each_streaming do |stream|1701 stream.each do |event|1702 case event1703 when Anthropic::Streaming::TextEvent1704 print event.text1705 when Anthropic::Streaming::InputJsonEvent1706 print event.partial_json1707 end1708 end1709 puts1710 end1711 ```1712 </Tab>1713</Tabs>17141715## Next steps17161717<CardGroup cols={2}>1718 <Card title="Strict tool use" icon="check" href="https://platform.claude.com/docs/en/agents-and-tools/tool-use/strict-tool-use">1719 Enforce JSON Schema compliance on Claude's tool inputs with grammar-constrained sampling.1720 </Card>17211722 <Card title="Handle tool calls" icon="arrows-left-right" href="https://platform.claude.com/docs/en/agents-and-tools/tool-use/handle-tool-calls">1723 Parse `tool_use` blocks, format `tool_result` responses, and handle errors with `is_error`.1724 </Card>17251726 <Card title="Parallel tool use" icon="grid" href="https://platform.claude.com/docs/en/agents-and-tools/tool-use/parallel-tool-use">1727 Enable, format, and disable parallel tool calls, with message-history guidance and troubleshooting.1728 </Card>17291730 <Card title="Define tools" icon="hammer" href="https://platform.claude.com/docs/en/agents-and-tools/tool-use/define-tools">1731 Specify tool schemas, write effective descriptions, and control when Claude calls your tools.1732 </Card>1733</CardGroup>1734