SPB Git

Repository Public

tool-runner.md 62.5 KB 1,734 lines markdown

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
Files main
Menu

Theme