# 工具运行器（SDK）

使用 SDK 的工具运行器抽象自动处理 agentic 循环、错误包装和类型安全。

---

工具运行器处理 agentic 循环、错误包装和类型安全，因此你无需自己处理。当你需要人在回路审批、自定义日志记录或条件执行时，请改用[手动循环](/docs/en/agents-and-tools/tool-use/handle-tool-calls)。

工具运行器提供了与 Claude 一起运行工具的开箱即用解决方案。工具运行器可以简化大多数工具使用实现。工具运行器自动处理工具调用、工具结果和对话管理，而无需手动处理：

- 当 Claude 调用工具时运行工具
- 处理请求/响应周期
- 管理对话状态
- 提供类型安全和验证

<Note>
工具运行器目前处于测试阶段，可在 [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) 和 [Ruby SDK](https://github.com/anthropics/anthropic-sdk-ruby/blob/main/helpers.md#3-auto-looping-tool-runner-beta) 中使用。
</Note>

## 基本用法

使用 SDK 辅助函数定义工具，然后使用工具运行器运行它们。

<Tabs>
<Tab title="Python">

使用 `@beta_tool` 装饰器通过类型提示和文档字符串定义工具。

<Note>
如果你使用异步客户端，请将 `@beta_tool` 替换为 `@beta_async_tool`，并使用 `async def` 定义函数。
</Note>

```python
import json
from anthropic import Anthropic, beta_tool

client = Anthropic()


@beta_tool
def get_weather(location: str, unit: str = "fahrenheit") -> str:
    """Get the current weather in a given location.

    Args:
        location: The city and state, e.g. San Francisco, CA
        unit: Temperature unit, either 'celsius' or 'fahrenheit'
    """
    return json.dumps({"temperature": "20°C", "condition": "Sunny"})


@beta_tool
def calculate_sum(a: int, b: int) -> str:
    """Add two numbers together.

    Args:
        a: First number
        b: Second number
    """
    return str(a + b)


runner = client.beta.messages.tool_runner(
    model="claude-opus-4-7",
    max_tokens=1024,
    tools=[get_weather, calculate_sum],
    messages=[
        {
            "role": "user",
            "content": "What's the weather like in Paris? Also, what's 15 + 27?",
        }
    ],
)
for message in runner:
    print(message)
```

The `@beta_tool` decorator inspects the function arguments and docstring to derive the JSON schema for you.

</Tab>
<Tab title="TypeScript">

Use `betaZodTool()` for type-safe tool definitions with Zod validation, or `betaTool()` for JSON Schema-based definitions.

TypeScript offers two approaches for defining tools:

**Using Zod (recommended)** - Use `betaZodTool()` for type-safe tool definitions with Zod validation (requires Zod 3.25.0 or higher):

```typescript hidelines={1}
import Anthropic from "@anthropic-ai/sdk";
import { betaZodTool } from "@anthropic-ai/sdk/helpers/beta/zod";
import { z } from "zod";

const client = new Anthropic();

const getWeatherTool = betaZodTool({
  name: "get_weather",
  description: "Get the current weather in a given location",
  inputSchema: z.object({
    location: z.string().describe("The city and state, e.g. San Francisco, CA"),
    unit: z.enum(["celsius", "fahrenheit"]).default("fahrenheit").describe("Temperature unit")
  }),
  run: async (input) => {
    return JSON.stringify({ temperature: "20°C", condition: "Sunny" });
  }
});

const finalMessage = await client.beta.messages.toolRunner({
  model: "claude-opus-4-7",
  max_tokens: 1024,
  tools: [getWeatherTool],
  messages: [{ role: "user", content: "What's the weather like in Paris?" }]
});

for (const block of finalMessage.content) {
  if (block.type === "text") {
    console.log(block.text);
  }
}
```

**Using JSON Schema** - Use `betaTool()` for type-safe tool definitions without Zod:

<Note>
The input generated by Claude is not validated at runtime. Perform validation inside the `run` function if needed.
</Note>

```typescript hidelines={1}
import Anthropic from "@anthropic-ai/sdk";
import { betaTool } from "@anthropic-ai/sdk/helpers/beta/json-schema";

const client = new Anthropic();

const calculateSumTool = betaTool({
  name: "calculate_sum",
  description: "Add two numbers together",
  inputSchema: {
    type: "object",
    properties: {
      a: { type: "number", description: "First number" },
      b: { type: "number", description: "Second number" }
    },
    required: ["a", "b"]
  },
  run: async (input) => {
    return String(input.a + input.b);
  }
});

const finalMessage = await client.beta.messages.toolRunner({
  model: "claude-opus-4-7",
  max_tokens: 1024,
  tools: [calculateSumTool],
  messages: [{ role: "user", content: "What's 15 + 27?" }]
});

for (const block of finalMessage.content) {
  if (block.type === "text") {
    console.log(block.text);
  }
}
```

</Tab>
<Tab title="C#">

Define each tool as a `BetaRunnableTool`, providing a `Definition` with a JSON schema and a `Run` delegate that runs when Claude calls the tool.

```csharp
using System.Text.Json;
using Anthropic;
using Anthropic.Helpers.Beta;
using Anthropic.Models.Beta.Messages;
using MessageCreateParams = Anthropic.Models.Beta.Messages.MessageCreateParams;
using InputSchema = Anthropic.Models.Beta.Messages.InputSchema;
using Role = Anthropic.Models.Beta.Messages.Role;
using Model = Anthropic.Models.Messages.Model;

var client = new AnthropicClient();

var getWeatherTool = new BetaRunnableTool
{
    Name = "get_weather",
    Definition = new BetaTool
    {
        Name = "get_weather",
        Description = "Get the current weather in a given location.",
        InputSchema = new InputSchema
        {
            Properties = new Dictionary<string, JsonElement>
            {
                ["location"] = JsonSerializer.SerializeToElement(
                    new { type = "string", description = "The city and state, e.g. San Francisco, CA" }
                ),
            },
            Required = ["location"],
        },
    },
    Run = (toolUse, _) =>
    {
        var location = toolUse.Input["location"].GetString();
        return Task.FromResult<BetaToolResultBlockParamContent>(
            $"Weather in {location}: 20°C, sunny"
        );
    },
};

var calculateSumTool = new BetaRunnableTool
{
    Name = "calculate_sum",
    Definition = new BetaTool
    {
        Name = "calculate_sum",
        Description = "Add two numbers together.",
        InputSchema = new InputSchema
        {
            Properties = new Dictionary<string, JsonElement>
            {
                ["a"] = JsonSerializer.SerializeToElement(new { type = "number" }),
                ["b"] = JsonSerializer.SerializeToElement(new { type = "number" }),
            },
            Required = ["a", "b"],
        },
    },
    Run = (toolUse, _) =>
    {
        var a = toolUse.Input["a"].GetDouble();
        var b = toolUse.Input["b"].GetDouble();
        return Task.FromResult<BetaToolResultBlockParamContent>($"{a + b}");
    },
};

var runner = client.Beta.Messages.ToolRunner(
    new MessageCreateParams
    {
        Model = Model.ClaudeOpus4_7,
        MaxTokens = 1024,
        Messages =
        [
            new()
            {
                Role = Role.User,
                Content = "What's the weather like in Paris? Also, what's 15 + 27?",
            },
        ],
    },
    [getWeatherTool, calculateSumTool]
);

await foreach (var message in runner)
{
    Console.WriteLine(message);
}
```

</Tab>
<Tab title="Go">

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.

```go
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/anthropics/anthropic-sdk-go"
	"github.com/anthropics/anthropic-sdk-go/toolrunner"
)

type GetWeatherInput struct {
	Location string `json:"location" jsonschema:"required,description=The city and state, e.g. San Francisco, CA"`
	Unit     string `json:"unit,omitempty" jsonschema:"enum=celsius,enum=fahrenheit,description=Temperature unit"`
}

type CalculateSumInput struct {
	A int `json:"a" jsonschema:"required,description=First number"`
	B int `json:"b" jsonschema:"required,description=Second number"`
}

func main() {
	client := anthropic.NewClient()
	ctx := context.Background()

	getWeather, err := toolrunner.NewBetaToolFromJSONSchema(
		"get_weather",
		"Get the current weather in a given location.",
		func(ctx context.Context, input GetWeatherInput) (anthropic.BetaToolResultBlockParamContentUnion, error) {
			return anthropic.BetaToolResultBlockParamContentUnion{
				OfText: &anthropic.BetaTextBlockParam{Text: "20°C, Sunny"},
			}, nil
		},
	)
	if err != nil {
		log.Fatal(err)
	}

	calculateSum, err := toolrunner.NewBetaToolFromJSONSchema(
		"calculate_sum",
		"Add two numbers together.",
		func(ctx context.Context, input CalculateSumInput) (anthropic.BetaToolResultBlockParamContentUnion, error) {
			return anthropic.BetaToolResultBlockParamContentUnion{
				OfText: &anthropic.BetaTextBlockParam{Text: fmt.Sprintf("%d", input.A+input.B)},
			}, nil
		},
	)
	if err != nil {
		log.Fatal(err)
	}

	runner := client.Beta.Messages.NewToolRunner(
		[]anthropic.BetaTool{getWeather, calculateSum},
		anthropic.BetaToolRunnerParams{
			BetaMessageNewParams: anthropic.BetaMessageNewParams{
				Model:     anthropic.ModelClaudeOpus4_7,
				MaxTokens: 1024,
				Messages: []anthropic.BetaMessageParam{
					anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock(
						"What's the weather like in Paris? Also, what's 15 + 27?",
					)),
				},
			},
		},
	)

	for message, err := range runner.All(ctx) {
		if err != nil {
			log.Fatal(err)
		}
		fmt.Println(message)
	}
}
```

The `jsonschema:` struct tags generate the input schema. For example, `CalculateSumInput` becomes:

```json
{
  "name": "calculate_sum",
  "description": "Add two numbers together.",
  "input_schema": {
    "type": "object",
    "properties": {
      "a": { "type": "integer", "description": "First number" },
      "b": { "type": "integer", "description": "Second number" }
    },
    "required": ["a", "b"]
  }
}
```

</Tab>
<Tab title="Java">

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.

```java
import com.anthropic.client.AnthropicClient;
import com.anthropic.client.okhttp.AnthropicOkHttpClient;
import com.anthropic.helpers.BetaToolRunner;
import com.anthropic.models.beta.messages.BetaMessage;
import com.anthropic.models.beta.messages.MessageCreateParams;
import com.anthropic.models.messages.Model;
import com.fasterxml.jackson.annotation.JsonClassDescription;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import java.util.function.Supplier;

@JsonClassDescription("Get the current weather in a given location")
static class GetWeather implements Supplier<String> {
    @JsonPropertyDescription("The city and state, e.g. San Francisco, CA")
    public String location;

    @JsonPropertyDescription("Temperature unit, either 'celsius' or 'fahrenheit'")
    public String unit;

    @Override
    public String get() {
        return "{\"temperature\": \"20°C\", \"condition\": \"Sunny\"}";
    }
}

@JsonClassDescription("Add two numbers together")
static class CalculateSum implements Supplier<String> {
    @JsonPropertyDescription("First number")
    public double a;

    @JsonPropertyDescription("Second number")
    public double b;

    @Override
    public String get() {
        return String.valueOf(a + b);
    }
}

void main() {
    AnthropicClient client = AnthropicOkHttpClient.fromEnv();

    BetaToolRunner runner = client.beta()
            .messages()
            .toolRunner(MessageCreateParams.builder()
                    .model(Model.CLAUDE_OPUS_4_7)
                    .maxTokens(1024)
                    .addBeta("structured-outputs-2025-11-13")
                    .addUserMessage("What's the weather like in Paris? Also, what's 15 + 27?")
                    .addTool(GetWeather.class)
                    .addTool(CalculateSum.class)
                    .build());

    for (BetaMessage message : runner) {
        IO.println(message);
    }
}
```

The class name `CalculateSum` becomes the tool name `calculate_sum`, and the SDK generates a JSON schema from the annotated fields:

```json
{
  "name": "calculate_sum",
  "description": "Add two numbers together",
  "input_schema": {
    "type": "object",
    "properties": {
      "a": { "description": "First number", "type": "number" },
      "b": { "description": "Second number", "type": "number" }
    },
    "required": ["a", "b"],
    "additionalProperties": false
  }
}
```

</Tab>
<Tab title="PHP">

Define each tool as a `BetaRunnableTool` that pairs the tool's JSON schema definition with a closure that runs it.

```php
<?php

use Anthropic\Client;
use Anthropic\Lib\Tools\BetaRunnableTool;
use Anthropic\Messages\Model;

$client = new Client();

$getWeather = new BetaRunnableTool(
    definition: [
        'name' => 'get_weather',
        'description' => 'Get the current weather in a given location.',
        'input_schema' => [
            'type' => 'object',
            'properties' => [
                'location' => [
                    'type' => 'string',
                    'description' => 'The city and state, e.g. San Francisco, CA',
                ],
                'unit' => [
                    'type' => 'string',
                    'enum' => ['celsius', 'fahrenheit'],
                ],
            ],
            'required' => ['location'],
        ],
    ],
    run: fn (array $input): string => json_encode([
        'temperature' => '20°C',
        'condition' => 'Sunny',
    ]),
);

$calculateSum = new BetaRunnableTool(
    definition: [
        'name' => 'calculate_sum',
        'description' => 'Add two numbers together.',
        'input_schema' => [
            'type' => 'object',
            'properties' => [
                'a' => ['type' => 'number', 'description' => 'First number'],
                'b' => ['type' => 'number', 'description' => 'Second number'],
            ],
            'required' => ['a', 'b'],
        ],
    ],
    run: fn (array $input): string => (string) ($input['a'] + $input['b']),
);

$runner = $client->beta->messages->toolRunner(
    maxTokens: 1024,
    messages: [
        ['role' => 'user', 'content' => "What's the weather like in Paris? Also, what's 15 + 27?"],
    ],
    model: Model::CLAUDE_OPUS_4_7,
    tools: [$getWeather, $calculateSum],
);

foreach ($runner as $message) {
    foreach ($message->content as $block) {
        if ($block->type === 'text') {
            echo $block->text, "\n";
        } elseif ($block->type === 'tool_use') {
            echo "[Tool call: {$block->name}]\n";
        }
    }
}
```

</Tab>
<Tab title="Ruby">

Use the `Anthropic::BaseTool` class to define tools with typed input schemas.

```ruby
require "anthropic"

# Initialize client
client = Anthropic::Client.new

# Define input schema
class GetWeatherInput < Anthropic::BaseModel
  required :location, String, doc: "The city and state, e.g. San Francisco, CA"
  optional :unit, Anthropic::InputSchema::EnumOf["celsius", "fahrenheit"],
           doc: "Temperature unit"
end

# Define tool
class GetWeather < Anthropic::BaseTool
  doc "Get the current weather in a given location"
  input_schema GetWeatherInput

  def call(input)
    # In a full implementation, you'd call a weather API here
    JSON.generate({temperature: "20°C", condition: "Sunny"})
  end
end

class CalculateSumInput < Anthropic::BaseModel
  required :a, Integer, doc: "First number"
  required :b, Integer, doc: "Second number"
end

class CalculateSum < Anthropic::BaseTool
  doc "Add two numbers together"
  input_schema CalculateSumInput

  def call(input)
    (input.a + input.b).to_s
  end
end

# Use the tool runner
runner = client.beta.messages.tool_runner(
  model: "claude-opus-4-7",
  max_tokens: 1024,
  tools: [GetWeather.new, CalculateSum.new],
  messages: [
    {role: "user", content: "What's the weather like in Paris? Also, what's 15 + 27?"}
  ]
)

runner.each_message do |message|
  message.content.each do |block|
    puts block.text if block.type == :text
  end
end
```

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.

</Tab>
</Tabs>

工具函数必须返回内容块或内容块数组，包括文本、图像或文档块。这允许工具返回丰富的多模态响应。返回的字符串会转换为文本内容块。如果你想向 Claude 返回结构化的 JSON 对象，请在返回之前将其编码为 JSON 字符串。数字、布尔值或其他非字符串基元也必须转换为字符串。

## 遍历工具运行器

工具运行器是一个可迭代对象，它产出来自 Claude 的消息。这通常被称为"工具调用循环"。每次迭代时，运行器检查 Claude 是否请求了工具使用。如果是，它会调用工具并将结果自动发送回 Claude，然后产出 Claude 的下一条消息以继续你的循环。

你可以随时使用 `break` 语句结束循环。运行器会循环直到 Claude 返回一条没有工具使用的消息。

如果你不需要中间消息，可以直接获取最终消息：

<Tabs>
<Tab title="Python">

使用 `runner.until_done()` 获取最终消息。

```python hidelines={1..18}
import anthropic
from anthropic import beta_tool

client = anthropic.Anthropic()


@beta_tool
def get_weather(location: str) -> str:
    """Get the current weather in a given location."""
    return "20°C, Sunny"


@beta_tool
def calculate_sum(a: int, b: int) -> str:
    """Add two numbers together."""
    return str(a + b)


runner = client.beta.messages.tool_runner(
    model="claude-opus-4-7",
    max_tokens=1024,
    tools=[get_weather, calculate_sum],
    messages=[
        {
            "role": "user",
            "content": "What's the weather like in Paris? Also, what's 15 + 27?",
        }
    ],
)
final_message = runner.until_done()
for block in final_message.content:
    if block.type == "text":
        print(block.text)
```

</Tab>
<Tab title="TypeScript">

`await` 运行器以获取最终消息。

```typescript hidelines={1..13}
import Anthropic from "@anthropic-ai/sdk";
import { betaZodTool } from "@anthropic-ai/sdk/helpers/beta/zod";
import { z } from "zod";

const client = new Anthropic();

const getWeatherTool = betaZodTool({
  name: "get_weather",
  description: "Get the current weather in a given location",
  inputSchema: z.object({ location: z.string() }),
  run: async () => JSON.stringify({ temperature: "20°C", condition: "Sunny" })
});

const runner = client.beta.messages.toolRunner({
  model: "claude-opus-4-7",
  max_tokens: 1024,
  tools: [getWeatherTool],
  messages: [{ role: "user", content: "What's the weather like in Paris?" }]
});

const finalMessage = await runner;
for (const block of finalMessage.content) {
  if (block.type === "text") {
    console.log(block.text);
  }
}
```

</Tab>
<Tab title="C#">

使用 `runner.RunUntilDoneAsync()` 获取最终消息。

```csharp hidelines={1..33}
using System.Text.Json;
using Anthropic;
using Anthropic.Helpers.Beta;
using Anthropic.Models.Beta.Messages;
using MessageCreateParams = Anthropic.Models.Beta.Messages.MessageCreateParams;
using InputSchema = Anthropic.Models.Beta.Messages.InputSchema;
using Role = Anthropic.Models.Beta.Messages.Role;
using Model = Anthropic.Models.Messages.Model;

var client = new AnthropicClient();

var getWeatherTool = new BetaRunnableTool
{
    Name = "get_weather",
    Definition = new BetaTool
    {
        Name = "get_weather",
        Description = "Get the current weather in a given location.",
        InputSchema = new InputSchema
        {
            Properties = new Dictionary<string, JsonElement>
            {
                ["location"] = JsonSerializer.SerializeToElement(new { type = "string" }),
            },
            Required = ["location"],
        },
    },
    Run = (toolUse, _) =>
        Task.FromResult<BetaToolResultBlockParamContent>(
            $"Weather in {toolUse.Input["location"].GetString()}: 20°C, sunny"
        ),
};

var runner = client.Beta.Messages.ToolRunner(
    new MessageCreateParams
    {
        Model = Model.ClaudeOpus4_7,
        MaxTokens = 1024,
        Messages =
        [
            new()
            {
                Role = Role.User,
                Content = "What's the weather like in Paris?",
            },
        ],
    },
    [getWeatherTool]
);

var finalMessage = await runner.RunUntilDoneAsync();
foreach (var block in finalMessage.Content)
{
    if (block.TryPickText(out var textBlock))
    {
        Console.WriteLine(textBlock.Text);
    }
}
```

</Tab>
<Tab title="Go">

使用 `runner.RunToCompletion(ctx)` 获取最终消息。

```go hidelines={1..32,-1}
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/anthropics/anthropic-sdk-go"
	"github.com/anthropics/anthropic-sdk-go/toolrunner"
)

type GetWeatherInput struct {
	Location string `json:"location" jsonschema:"required,description=The city and state"`
}

func main() {
	client := anthropic.NewClient()
	ctx := context.Background()

	getWeather, err := toolrunner.NewBetaToolFromJSONSchema(
		"get_weather",
		"Get the current weather in a given location.",
		func(ctx context.Context, input GetWeatherInput) (anthropic.BetaToolResultBlockParamContentUnion, error) {
			return anthropic.BetaToolResultBlockParamContentUnion{
				OfText: &anthropic.BetaTextBlockParam{Text: "20°C, Sunny"},
			}, nil
		},
	)
	if err != nil {
		log.Fatal(err)
	}

	runner := client.Beta.Messages.NewToolRunner(
		[]anthropic.BetaTool{getWeather},
		anthropic.BetaToolRunnerParams{
			BetaMessageNewParams: anthropic.BetaMessageNewParams{
				Model:     anthropic.ModelClaudeOpus4_7,
				MaxTokens: 1024,
				Messages: []anthropic.BetaMessageParam{
					anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock(
						"What's the weather like in Paris?",
					)),
				},
			},
		},
	)

	finalMessage, err := runner.RunToCompletion(ctx)
	if err != nil {
		log.Fatal(err)
	}
	for _, block := range finalMessage.Content {
		if textBlock, ok := block.AsAny().(anthropic.BetaTextBlock); ok {
			fmt.Println(textBlock.Text)
		}
	}
}
```

</Tab>
<Tab title="Java">

Java SDK 没有 `until_done()` 快捷方式。迭代至耗尽并保留最后一条消息。

```java hidelines={1..39,-1}
import com.anthropic.client.AnthropicClient;
import com.anthropic.client.okhttp.AnthropicOkHttpClient;
import com.anthropic.helpers.BetaToolRunner;
import com.anthropic.models.beta.messages.BetaContentBlock;
import com.anthropic.models.beta.messages.BetaMessage;
import com.anthropic.models.beta.messages.MessageCreateParams;
import com.anthropic.models.messages.Model;
import com.fasterxml.jackson.annotation.JsonClassDescription;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import java.util.function.Supplier;

@JsonClassDescription("Get the current weather in a given location")
static class GetWeather implements Supplier<String> {
    @JsonPropertyDescription("The city and state, e.g. San Francisco, CA")
    public String location;

    @Override
    public String get() {
        return "20°C, Sunny";
    }
}

@JsonClassDescription("Add two numbers together")
static class CalculateSum implements Supplier<String> {
    @JsonPropertyDescription("First number")
    public double a;

    @JsonPropertyDescription("Second number")
    public double b;

    @Override
    public String get() {
        return String.valueOf(a + b);
    }
}

void main() {
    AnthropicClient client = AnthropicOkHttpClient.fromEnv();

    BetaToolRunner runner = client.beta()
            .messages()
            .toolRunner(MessageCreateParams.builder()
                    .model(Model.CLAUDE_OPUS_4_7)
                    .maxTokens(1024)
                    .addBeta("structured-outputs-2025-11-13")
                    .addUserMessage("What's the weather like in Paris? Also, what's 15 + 27?")
                    .addTool(GetWeather.class)
                    .addTool(CalculateSum.class)
                    .build());

    BetaMessage finalMessage = null;
    for (BetaMessage message : runner) {
        finalMessage = message;
    }
    for (BetaContentBlock block : finalMessage.content()) {
        block.text().ifPresent(textBlock -> IO.println(textBlock.text()));
    }
}
```

</Tab>
<Tab title="PHP">

使用 `runUntilDone()` 获取最终消息。

```php hidelines={1..30}
<?php

use Anthropic\Client;
use Anthropic\Lib\Tools\BetaRunnableTool;
use Anthropic\Messages\Model;

$client = new Client();

$getWeather = new BetaRunnableTool(
    definition: [
        'name' => 'get_weather',
        'description' => 'Get the current weather in a given location.',
        'input_schema' => [
            'type' => 'object',
            'properties' => ['location' => ['type' => 'string']],
            'required' => ['location'],
        ],
    ],
    run: fn (array $input): string => '20°C, Sunny',
);

$calculateSum = new BetaRunnableTool(
    definition: [
        'name' => 'calculate_sum',
        'description' => 'Add two numbers together.',
        'input_schema' => ['type' => 'object', 'properties' => ['a' => ['type' => 'number'], 'b' => ['type' => 'number']], 'required' => ['a', 'b']],
    ],
    run: fn (array $input): string => (string) ($input['a'] + $input['b']),
);

$runner = $client->beta->messages->toolRunner(
    maxTokens: 1024,
    messages: [
        ['role' => 'user', 'content' => "What's the weather like in Paris? Also, what's 15 + 27?"],
    ],
    model: Model::CLAUDE_OPUS_4_7,
    tools: [$getWeather, $calculateSum],
);

$finalMessage = $runner->runUntilDone();
foreach ($finalMessage->content as $block) {
    if ($block->type === 'text') {
        echo $block->text, "\n";
    }
}
```

</Tab>
<Tab title="Ruby">

使用 `runner.run_until_finished` 获取所有消息。

```ruby hidelines={1..29}
require "anthropic"

client = Anthropic::Client.new

class GetWeatherInput < Anthropic::BaseModel
  required :location, String
end

class GetWeather < Anthropic::BaseTool
  doc "Get the current weather in a given location"
  input_schema GetWeatherInput
  def call(input)
    "Weather in #{input.location}: 20°C, Sunny"
  end
end

class CalculateSumInput < Anthropic::BaseModel
  required :a, Integer
  required :b, Integer
end

class CalculateSum < Anthropic::BaseTool
  doc "Add two numbers together"
  input_schema CalculateSumInput
  def call(input)
    (input.a + input.b).to_s
  end
end

runner = client.beta.messages.tool_runner(
  model: "claude-opus-4-7",
  max_tokens: 1024,
  tools: [GetWeather.new, CalculateSum.new],
  messages: [
    {role: "user", content: "What's the weather like in Paris? Also, what's 15 + 27?"}
  ]
)

all_messages = runner.run_until_finished
all_messages.each { |msg| puts msg.content }
```

</Tab>
</Tabs>

## 高级用法

在循环内，你可以读取每条响应消息并在下一次 API 调用之前修改运行器的状态。每次迭代遵循以下生命周期：

1. 运行器使用其当前状态向 Messages API 发送请求。
2. 运行器将响应消息产出到你的循环体。
3. 你的循环体运行。你可以读取消息并选择性地修改运行器的状态。
4. 当你的循环体返回时，运行器检查你是否修改了其消息历史。
   - **如果你没有修改消息历史：** 运行器将助手消息追加到其状态。如果消息包含工具调用，运行器会运行它们并追加结果。如果没有工具调用，循环退出。
   - **如果你修改了消息历史：** 运行器跳过其自动追加并使用你的状态不变。请参阅[接管消息历史](#taking-over-message-history)。

```mermaid
sequenceDiagram
  participant U as Your code
  participant TR as ToolRunner
  participant API as Messages API

  loop For each iteration
    TR->>API: Send request with current state
    API-->>TR: Response message
    TR-->>U: Yield message
    note over U: Your loop body runs
    U->>TR: Resume
    alt Message history unchanged
      TR->>TR: Append assistant message,<br/>run tools, append results<br/>(exit if no tool calls)
    else Message history changed
      TR->>TR: Use your state unchanged
    end
  end
```

### 接管消息历史

默认情况下，运行器为你管理对话状态：每个回合后，它将助手消息和任何工具结果追加到自己的消息历史中。当你想重试一个回合（丢弃响应并重新发送）、注入后续消息或自己构建工具结果时，你会接管消息历史。

你通过从循环体内修改运行器的消息来接管。具体方法取决于 SDK；请参阅下面的各语言选项卡。

当你接管一个迭代时，运行器不会追加该回合的助手消息或工具结果。你负责保持对话有效：自己追加助手消息和工具结果（如果你希望该回合有效），有条件地修改状态以便在没有工具调用时循环仍能退出，并传递 `max_iterations` 来限制循环。所有七个 SDK 都支持 `max_iterations`。

<Tabs>
<Tab title="Python">

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.

````python
runner = client.beta.messages.tool_runner(
    model="claude-opus-4-7",
    max_tokens=1024,
    max_iterations=10,
    tools=[get_weather],
    messages=[{"role": "user", "content": "What's the weather in San Francisco?"}],
)

for message in runner:
    tool_response = runner.generate_tool_call_response()
    if tool_response is not None:
        # append_messages() flags state as modified, so the runner skips its
        # automatic append for this iteration. Append the assistant message and
        # tool result yourself, plus any follow-up.
        runner.append_messages(
            message,
            tool_response,
            {"role": "user", "content": "Please be concise."},
        )
    # When there's no tool call, leave state untouched so the loop exits.
````

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.

````python
for message in runner:
    runner.set_messages_params(lambda params: {**params, "max_tokens": 2048})
````

</Tab>
<Tab title="TypeScript">

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.

The following example retries a truncated response with a larger `max_tokens` budget.

````typescript
const runner = client.beta.messages.toolRunner({
  model: "claude-opus-4-7",
  max_tokens: 1024,
  max_iterations: 10,
  tools: [getWeatherTool],
  messages: [
    {
      role: "user",
      content: "Give me a detailed weather report for every major US city."
    }
  ]
});

const MAX_TOKEN_CEILING = 8192;

for await (const message of runner) {
  if (message.stop_reason === "max_tokens") {
    const current = runner.params.max_tokens;
    if (current >= MAX_TOKEN_CEILING) {
      console.warn(`Hit ceiling (${MAX_TOKEN_CEILING}); stopping.`);
      break;
    }
    const doubled = Math.min(current * 2, MAX_TOKEN_CEILING);
    console.log(`Response truncated at ${current} tokens; retrying with ${doubled}.`);
    // Bump the budget. setMessagesParams() flags state as modified, so the
    // runner does NOT append the truncated message. The next iteration retries
    // the same turn with the larger budget.
    runner.setMessagesParams((params) => ({ ...params, max_tokens: doubled }));
  }
  // Otherwise leave state untouched so the runner auto-appends and continues.
}
````

</Tab>
<Tab title="C#">

Calling `SetParams()` or `PushMessages()` flags state as modified, which causes the runner to skip its auto-append for that turn. 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.

````csharp
var runner = client.Beta.Messages.ToolRunner(
    new MessageCreateParams
    {
        Model = Model.ClaudeOpus4_7,
        MaxTokens = 1024,
        Messages = [new() { Role = Role.User, Content = "What's the weather in San Francisco?" }],
    },
    [getWeatherTool],
    maxIterations: 10
);

await foreach (var message in runner)
{
    var toolUseBlock = message
        .Content.Select(block => block.TryPickToolUse(out var toolUse) ? toolUse : null)
        .FirstOrDefault(toolUse => toolUse is not null);

    if (toolUseBlock is null)
    {
        // No tool call: leave state untouched so the loop exits normally.
        continue;
    }

    // Run the tool yourself and build the result block.
    var toolResult = new BetaToolResultBlockParam(toolUseBlock.ID)
    {
        Content = await getWeatherTool.ExecuteAsync(toolUseBlock, default),
    };

    // PushMessages() flags state as modified; the runner skips its auto-append.
    // Supply the assistant turn and the tool result yourself, then add a follow-up.
    runner.PushMessages(
        new()
        {
            Role = Role.Assistant,
            Content = new BetaMessageParamContent(
                JsonSerializer.SerializeToElement(
                    message.Content.Select(block => block.Json).ToArray()
                )
            ),
        },
        new()
        {
            Role = Role.User,
            Content = new List<BetaContentBlockParam> { toolResult },
        },
        new() { Role = Role.User, Content = "Please be concise in your response." }
    );
}
````

</Tab>
<Tab title="Go">

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.

````go
runner := client.Beta.Messages.NewToolRunner(
	[]anthropic.BetaTool{getWeather},
	anthropic.BetaToolRunnerParams{
		BetaMessageNewParams: anthropic.BetaMessageNewParams{
			Model:     anthropic.ModelClaudeOpus4_7,
			MaxTokens: 1024,
			Messages: []anthropic.BetaMessageParam{
				anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock(
					"What's the weather in San Francisco?",
				)),
			},
		},
		MaxIterations: 10,
	},
)

for {
	message, err := runner.NextMessage(ctx)
	if err != nil {
		log.Fatal(err)
	}
	if message == nil {
		break // conversation complete
	}

	// The Go runner always appends the assistant message and tool results.
	// Param changes here apply to the next iteration.
	runner.Params.MaxTokens = 2048
}
````

</Tab>
<Tab title="Java">

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.

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.

````java
BetaToolRunner runner = client.beta()
        .messages()
        .toolRunner(ToolRunnerCreateParams.builder()
                .initialMessageParams(MessageCreateParams.builder()
                        .model(Model.CLAUDE_OPUS_4_7)
                        .maxTokens(1024)
                        .addBeta("structured-outputs-2025-11-13")
                        .addUserMessage("Give me a detailed weather report for every major US city.")
                        .addTool(GetWeather.class)
                        .build())
                .maxIterations(10L)
                .build());

long ceiling = 8192;

for (BetaMessage message : runner) {
    if (BetaStopReason.MAX_TOKENS.equals(message.stopReason().orElse(null))) {
        long current = runner.params().maxTokens();
        if (current >= ceiling) {
            IO.println("Hit ceiling (" + ceiling + "), accepting truncated response.");
            break;
        }
        long doubled = Math.min(current * 2, ceiling);
        IO.println("Response truncated at " + current + " tokens, retrying with " + doubled + ".");

        // Calling setNextParams() flags this turn as user-managed: the runner
        // does NOT auto-append the truncated message, so the next iteration
        // re-sends the same conversation prefix with the larger budget.
        runner.setNextParams(runner.params().toBuilder().maxTokens(doubled).build());
    }
    // No mutation on a normal turn: the runner auto-appends and continues.
}
````

</Tab>
<Tab title="PHP">

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.

The following example doubles `max_tokens` and retries when a response is cut off.

````php
use Anthropic\Beta\Messages\BetaStopReason;

$runner = $client->beta->messages->toolRunner(
    maxTokens: 1024,
    messages: [
        ['role' => 'user', 'content' => 'Give a detailed weather report for every major US city.'],
    ],
    model: Model::CLAUDE_OPUS_4_7,
    tools: [$getWeather],
    maxIterations: 10,
);

$maxTokenCeiling = 8192;

foreach ($runner as $message) {
    if ($message->stopReason === BetaStopReason::MAX_TOKENS->value) {
        $current = $runner->getParams()['maxTokens'];

        if ($current >= $maxTokenCeiling) {
            echo "Hit ceiling ({$maxTokenCeiling}), accepting truncated response.\n";
            break;
        }

        $doubled = min($current * 2, $maxTokenCeiling);
        echo "Response truncated at {$current} tokens, retrying with {$doubled}.\n";

        // Calling setMessagesParams() inside the loop tells the runner to skip
        // its automatic append. The truncated message is discarded; the next
        // iteration retries with the larger budget.
        // Keys are camelCase, matching the toolRunner() named parameters.
        $runner->setMessagesParams(['maxTokens' => $doubled]);
    }
}
````

</Tab>
<Tab title="Ruby">

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.

You take over message history when you reassign `runner.params[:messages]`, or call `feed_messages` from inside an `each_message` block. The following pattern calls `feed_messages` between `next_message` calls, which does not take over.

````ruby
runner = client.beta.messages.tool_runner(
  model: "claude-opus-4-7",
  max_tokens: 1024,
  max_iterations: 10,
  tools: [GetWeather.new],
  messages: [{role: "user", content: "What's the weather in San Francisco?"}]
)

# Step the runner once. The assistant message and tool result are appended
# to runner.params[:messages] before next_message returns.
message = runner.next_message
puts message.content

# Inject a follow-up before continuing. feed_messages takes a splat, not an array.
runner.feed_messages({role: "user", content: "Also check Boston."})

# Change parameters in place. Reassigning runner.params[:messages] would tell
# the runner to skip its automatic append on the next turn.
runner.params.update(max_tokens: 2048)

runner.run_until_finished
````

</Tab>
</Tabs>

### 自动上下文管理

对于长时间运行的 agentic 任务，工具运行器支持自动[压缩](/docs/en/build-with-claude/context-editing#client-side-compaction-sdk)，当 token 使用量超过阈值时生成摘要，以便对话可以继续超出上下文窗口限制。

### 调试工具执行

当工具抛出异常时，工具运行器会捕获它并将错误作为 `is_error: true` 的工具结果返回给 Claude。默认情况下，只包含异常消息，不包含完整的堆栈跟踪。

要查看完整的堆栈跟踪和调试信息，请设置 `ANTHROPIC_LOG` 环境变量：

```bash
# 查看包含工具错误的 info 级别日志
export ANTHROPIC_LOG=info

# 查看更详细的 debug 级别日志
export ANTHROPIC_LOG=debug
```

启用后，SDK 会将完整的异常详情记录到你的语言的标准日志设施中，包括工具失败时的完整堆栈跟踪。

### 拦截工具错误

默认情况下，工具错误会传回 Claude，Claude 可以相应地做出响应。但是，你可能希望检测错误并以不同方式处理它们，例如提前停止执行或实现自定义错误处理。

使用工具响应方法在工具结果发送给 Claude 之前拦截并检查错误：

<Tabs>
<Tab title="Python">

```python hidelines={1..13}
import anthropic
import json
from anthropic import beta_tool

client = anthropic.Anthropic()


@beta_tool
def my_tool(query: str) -> str:
    """A sample tool."""
    return f"Result for: {query}"


runner = client.beta.messages.tool_runner(
    model="claude-opus-4-7",
    max_tokens=1024,
    tools=[my_tool],
    messages=[{"role": "user", "content": "Run my_tool with the query 'hello'."}],
)

for message in runner:
    tool_response = runner.generate_tool_call_response()

    if tool_response is not None:
        # tool_response is a dict: {"role": "user", "content": [...]}
        # Check if any tool result has an error
        for block in tool_response["content"]:
            if block.get("is_error"):
                # Option 1: Raise an exception to stop the loop
                raise RuntimeError(f"Tool failed: {json.dumps(block['content'])}")

                # Option 2: Log and continue (let Claude handle it)
                # logger.error(f"Tool error: {json.dumps(block['content'])}")

    # Process the message normally
    print(message.content)
```

</Tab>
<Tab title="TypeScript">

```typescript hidelines={1..13}
import Anthropic from "@anthropic-ai/sdk";
import { betaZodTool } from "@anthropic-ai/sdk/helpers/beta/zod";
import { z } from "zod";

const client = new Anthropic();

const myTool = betaZodTool({
  name: "my_tool",
  description: "A sample tool",
  inputSchema: z.object({ query: z.string() }),
  run: async (input) => `Result for: ${input.query}`
});

const runner = client.beta.messages.toolRunner({
  model: "claude-opus-4-7",
  max_tokens: 1024,
  tools: [myTool],
  messages: [{ role: "user", content: "Run my_tool with the query 'hello'." }]
});

for await (const message of runner) {
  const toolResultMessage = await runner.generateToolResponse();

  if (toolResultMessage) {
    // Check if any tool result has an error
    for (const block of toolResultMessage.content) {
      if (block.type === "tool_result" && block.is_error) {
        // Option 1: Throw to stop the loop
        throw new Error(`Tool failed: ${JSON.stringify(block.content)}`);

        // Option 2: Log and continue (let Claude handle it)
        // console.error(`Tool error: ${JSON.stringify(block.content)}`);
      }
    }
  }

  // Process the message normally
  console.log(message.content);
}
```

</Tab>
<Tab title="C#">

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.

```csharp hidelines={1..14}
using System.Text.Json;
using Anthropic;
using Anthropic.Helpers.Beta;
using Anthropic.Models.Beta.Messages;
using MessageCreateParams = Anthropic.Models.Beta.Messages.MessageCreateParams;
using InputSchema = Anthropic.Models.Beta.Messages.InputSchema;
using Role = Anthropic.Models.Beta.Messages.Role;
using Model = Anthropic.Models.Messages.Model;

static Task<string> CallExternalWeatherService(string location, CancellationToken ct) =>
    throw new HttpRequestException("simulated outage");

var client = new AnthropicClient();

var getWeatherTool = new BetaRunnableTool
{
    Name = "get_weather",
    Definition = new BetaTool
    {
        Name = "get_weather",
        Description = "Get the current weather in a given location.",
        InputSchema = new InputSchema
        {
            Properties = new Dictionary<string, JsonElement>
            {
                ["location"] = JsonSerializer.SerializeToElement(new { type = "string" }),
            },
            Required = ["location"],
        },
    },
    Run = async (toolUse, cancellationToken) =>
    {
        try
        {
            return await CallExternalWeatherService(
                toolUse.Input["location"].GetString()!,
                cancellationToken
            );
        }
        catch (HttpRequestException ex)
        {
            // Log here if you need to inspect the failure before Claude sees it.
            throw new BetaToolError($"Weather service unavailable: {ex.Message}");
        }
    },
};

var runner = client.Beta.Messages.ToolRunner(
    new MessageCreateParams
    {
        Model = Model.ClaudeOpus4_7,
        MaxTokens = 1024,
        Messages =
        [
            new() { Role = Role.User, Content = "What's the weather in San Francisco?" },
        ],
    },
    [getWeatherTool]
);

Console.WriteLine(await runner.RunUntilDoneAsync());
```

</Tab>
<Tab title="Go">

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.

</Tab>
<Tab title="Java">

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.

</Tab>
<Tab title="PHP">

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](#modifying-tool-results).

</Tab>
<Tab title="Ruby">

```ruby hidelines={1..16}
require "anthropic"

client = Anthropic::Client.new

class MyToolInput < Anthropic::BaseModel
  required :query, String
end

class MyTool < Anthropic::BaseTool
  doc "A sample tool"
  input_schema MyToolInput
  def call(input)
    "Result for: #{input.query}"
  end
end

runner = client.beta.messages.tool_runner(
  model: "claude-opus-4-7",
  max_tokens: 1024,
  tools: [MyTool.new],
  messages: [{role: "user", content: "Run my_tool with the query 'hello'."}]
)

runner.each_message do |message|
  # Get the tool response to check for errors
  # Note: The runner automatically handles tool execution and appends results
  # This is just for error checking/logging purposes
  tool_results = runner.params[:messages].last

  if tool_results && tool_results[:role] == :user && tool_results[:content].is_a?(Array)
    tool_results[:content].each do |block|
      if block[:type] == :tool_result && block[:is_error]
        # Option 1: Raise an exception to stop the loop
        raise "Tool failed: #{block[:content]}"

        # Option 2: Log and continue (let Claude handle it)
        # logger.error("Tool error: #{block[:content]}")
      end
    end
  end

  puts message.content
end
```

</Tab>
</Tabs>

### 修改工具结果

你可以在工具结果发送回 Claude 之前修改它们。这对于添加元数据（如 `cache_control`）以在工具结果上启用[提示缓存](/docs/en/build-with-claude/prompt-caching)或转换工具输出很有用。

使用工具响应方法获取工具结果，然后在运行器继续之前修改它。你是显式追加修改后的结果还是原地修改取决于 SDK；请参阅每个选项卡中的代码注释。

<Tabs>
<Tab title="Python">

```python hidelines={1..12}
import anthropic
from anthropic import beta_tool

client = anthropic.Anthropic()


@beta_tool
def search_documents(query: str) -> str:
    """Search documents for relevant information."""
    return f"Found 3 documents matching: {query}"


runner = client.beta.messages.tool_runner(
    model="claude-opus-4-7",
    max_tokens=1024,
    tools=[search_documents],
    messages=[
        {
            "role": "user",
            "content": "Search for information about the climate of San Francisco",
        }
    ],
)

for message in runner:
    tool_response = runner.generate_tool_call_response()

    if tool_response is not None:
        # tool_response is a dict: {"role": "user", "content": [...]}
        # Modify the tool result to add cache control
        for block in tool_response["content"]:
            if block["type"] == "tool_result":
                # Add cache_control to cache this tool result
                block["cache_control"] = {"type": "ephemeral"}

        # Append the modified response (this prevents auto-append of the original)
        runner.append_messages(message, tool_response)

    print(message.content)
```

</Tab>
<Tab title="TypeScript">

```typescript hidelines={1..13}
import Anthropic from "@anthropic-ai/sdk";
import { betaZodTool } from "@anthropic-ai/sdk/helpers/beta/zod";
import { z } from "zod";

const client = new Anthropic();

const searchDocuments = betaZodTool({
  name: "search_documents",
  description: "Search documents for relevant information",
  inputSchema: z.object({ query: z.string() }),
  run: async (input) => `Found 3 documents matching: ${input.query}`
});

const runner = client.beta.messages.toolRunner({
  model: "claude-opus-4-7",
  max_tokens: 1024,
  tools: [searchDocuments],
  messages: [
    { role: "user", content: "Search for information about the climate of San Francisco" }
  ]
});

for await (const message of runner) {
  const toolResultMessage = await runner.generateToolResponse();

  if (toolResultMessage && typeof toolResultMessage.content !== "string") {
    // Modify the tool result to add cache control
    for (const block of toolResultMessage.content) {
      if (block.type === "tool_result") {
        // Add cache_control to cache this tool result
        block.cache_control = { type: "ephemeral" };
      }
    }
    // No pushMessages call needed: the runner auto-appends both the assistant
    // message and the (now-mutated) cached tool response.
  }

  console.log(message.content);
}
```

</Tab>
<Tab title="C#">

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.

</Tab>
<Tab title="Go">

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.

```go hidelines={1..19,-1}
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/anthropics/anthropic-sdk-go"
	"github.com/anthropics/anthropic-sdk-go/toolrunner"
)

type SearchDocumentsInput struct {
	Query string `json:"query" jsonschema:"required,description=Search query"`
}

func main() {
	client := anthropic.NewClient()
	ctx := context.Background()

	searchDocuments, err := toolrunner.NewBetaToolFromJSONSchema(
		"search_documents",
		"Search documents for relevant information.",
		func(ctx context.Context, input SearchDocumentsInput) (anthropic.BetaToolResultBlockParamContentUnion, error) {
			return anthropic.BetaToolResultBlockParamContentUnion{
				OfText: &anthropic.BetaTextBlockParam{
					Text: fmt.Sprintf("Found 3 documents matching: %s", input.Query),
					// Set cache_control on the inner content block. The outer
					// tool_result block's cache_control is not currently
					// settable through the Go runner.
					CacheControl: anthropic.NewBetaCacheControlEphemeralParam(),
				},
			}, nil
		},
	)
	if err != nil {
		log.Fatal(err)
	}

	runner := client.Beta.Messages.NewToolRunner(
		[]anthropic.BetaTool{searchDocuments},
		anthropic.BetaToolRunnerParams{
			BetaMessageNewParams: anthropic.BetaMessageNewParams{
				Model:     anthropic.ModelClaudeOpus4_7,
				MaxTokens: 1024,
				Messages: []anthropic.BetaMessageParam{
					anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock(
						"Search for information about the climate of San Francisco",
					)),
				},
			},
		},
	)

	finalMessage, err := runner.RunToCompletion(ctx)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(finalMessage)
}
```

</Tab>
<Tab title="Java">

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.

```java hidelines={1..14,31..48}
import com.anthropic.client.AnthropicClient;
import com.anthropic.client.okhttp.AnthropicOkHttpClient;
import com.anthropic.helpers.BetaToolRunner;
import com.anthropic.models.beta.messages.BetaCacheControlEphemeral;
import com.anthropic.models.beta.messages.BetaMessage;
import com.anthropic.models.beta.messages.BetaTextBlockParam;
import com.anthropic.models.beta.messages.BetaToolResultBlockParam;
import com.anthropic.models.beta.messages.MessageCreateParams;
import com.anthropic.models.messages.Model;
import com.fasterxml.jackson.annotation.JsonClassDescription;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import java.util.List;
import java.util.function.Supplier;

@JsonClassDescription("Look up reference documentation for a topic")
static class SearchDocuments implements Supplier<BetaToolResultBlockParam.Content> {
    @JsonPropertyDescription("The search query")
    public String query;

    @Override
    public BetaToolResultBlockParam.Content get() {
        String largeResult = "..."; // a long document worth caching
        return BetaToolResultBlockParam.Content.ofBlocks(List.of(
                BetaToolResultBlockParam.Content.Block.ofText(
                        BetaTextBlockParam.builder()
                                .text(largeResult)
                                .cacheControl(BetaCacheControlEphemeral.builder().build())
                                .build())));
    }
}

void main() {
    AnthropicClient client = AnthropicOkHttpClient.fromEnv();

    BetaToolRunner runner = client.beta()
            .messages()
            .toolRunner(MessageCreateParams.builder()
                    .model(Model.CLAUDE_OPUS_4_7)
                    .maxTokens(1024)
                    .addBeta("structured-outputs-2025-11-13")
                    .addUserMessage("Search the docs for prompt caching.")
                    .addTool(SearchDocuments.class)
                    .build());

    for (BetaMessage message : runner) {
        IO.println(message);
    }
}
```

</Tab>
<Tab title="PHP">

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.

```php hidelines={1..22}
<?php

use Anthropic\Beta\Messages\BetaToolUseBlock;
use Anthropic\Client;
use Anthropic\Lib\Tools\BetaRunnableTool;
use Anthropic\Messages\Model;

$client = new Client();

$searchDocuments = new BetaRunnableTool(
    definition: [
        'name' => 'search_documents',
        'description' => 'Search documents for relevant information.',
        'input_schema' => [
            'type' => 'object',
            'properties' => ['query' => ['type' => 'string']],
            'required' => ['query'],
        ],
    ],
    run: fn (array $input): string => "Found 3 documents matching: {$input['query']}",
);

$runner = $client->beta->messages->toolRunner(
    maxTokens: 1024,
    messages: [
        ['role' => 'user', 'content' => 'Search for information about the climate of San Francisco.'],
    ],
    model: Model::CLAUDE_OPUS_4_7,
    tools: [$searchDocuments],
);

foreach ($runner as $message) {
    $toolResults = [];
    foreach ($message->content as $block) {
        if ($block instanceof BetaToolUseBlock) {
            $toolResults[] = [
                'type' => 'tool_result',
                'tool_use_id' => $block->id,
                'content' => $searchDocuments->run($block->input),
                // Add cache_control to cache this tool result
                'cache_control' => ['type' => 'ephemeral'],
            ];
        }
    }

    if ($toolResults !== []) {
        // pushMessages() flags state as mutated, so the runner skips its
        // automatic append. Push the assistant message and tool results.
        $runner->pushMessages(
            ['role' => 'assistant', 'content' => $message->content],
            ['role' => 'user', 'content' => $toolResults],
        );
    }
    // No tool call: leave state untouched so the loop exits.
}
```

</Tab>
<Tab title="Ruby">

```ruby hidelines={1..16}
require "anthropic"

client = Anthropic::Client.new

class SearchDocumentsInput < Anthropic::BaseModel
  required :query, String
end

class SearchDocuments < Anthropic::BaseTool
  doc "Search documents for relevant information"
  input_schema SearchDocumentsInput
  def call(input)
    "Found 3 documents matching: #{input.query}"
  end
end

runner = client.beta.messages.tool_runner(
  model: "claude-opus-4-7",
  max_tokens: 1024,
  tools: [SearchDocuments.new],
  messages: [{role: "user", content: "Search for information about the climate of San Francisco"}]
)

loop do
  message = runner.next_message
  break unless message

  # Access the most recent tool results from the messages array
  # The runner automatically adds tool results, but you can modify them
  tool_results_message = runner.params[:messages].last

  if tool_results_message && tool_results_message[:role] == :user && tool_results_message[:content].is_a?(Array)
    tool_results_message[:content].each do |block|
      if block[:type] == :tool_result
        # Modify the tool result to add cache control
        block[:cache_control] = {type: "ephemeral"}
      end
    end
  end

  puts message.content
  break if message.stop_reason != :tool_use
end
```

</Tab>
</Tabs>

<Tip>
当工具返回大量数据（如文档搜索结果）时，向工具结果添加 `cache_control` 特别有用，你希望为后续 API 调用缓存这些数据。有关缓存策略的更多详情，请参阅[提示缓存](/docs/en/build-with-claude/prompt-caching)。
</Tip>

## 流式传输

启用流式传输以增量处理每个回合的响应。每次迭代产出一个流对象，你可以遍历它来获取事件。

<Tabs>
<Tab title="Python">

设置 `stream=True` 并使用 `get_final_message()` 获取累积的消息。

```python hidelines={1..12}
import anthropic
from anthropic import beta_tool

client = anthropic.Anthropic()


@beta_tool
def calculate_sum(a: int, b: int) -> str:
    """Add two numbers together."""
    return str(a + b)


runner = client.beta.messages.tool_runner(
    model="claude-opus-4-7",
    max_tokens=1024,
    tools=[calculate_sum],
    messages=[{"role": "user", "content": "What is 15 + 27?"}],
    stream=True,
)

# When streaming, the runner returns BetaMessageStream
for message_stream in runner:
    for event in message_stream:
        print("event:", event)
    print("message:", message_stream.get_final_message())

print(runner.until_done())
```

</Tab>
<Tab title="TypeScript">

设置 `stream: true` 并使用 `finalMessage()` 获取累积的消息。

```typescript hidelines={1..13}
import Anthropic from "@anthropic-ai/sdk";
import { betaZodTool } from "@anthropic-ai/sdk/helpers/beta/zod";
import { z } from "zod";

const client = new Anthropic();

const getWeatherTool = betaZodTool({
  name: "get_weather",
  description: "Get the current weather in a given location",
  inputSchema: z.object({ location: z.string() }),
  run: async () => JSON.stringify({ temperature: "20°C", condition: "Sunny" })
});

const runner = client.beta.messages.toolRunner({
  model: "claude-opus-4-7",
  max_tokens: 1024,
  messages: [{ role: "user", content: "What is the weather in San Francisco?" }],
  tools: [getWeatherTool],
  stream: true
});

// When streaming, the runner returns BetaMessageStream
for await (const messageStream of runner) {
  for await (const event of messageStream) {
    console.log("event:", event);
  }
  console.log("message:", await messageStream.finalMessage());
}

console.log(await runner);
```

</Tab>
<Tab title="C#">

调用 `runner.Streaming()` 获取嵌套的异步序列：每次 API 调用一个内部流。

```csharp hidelines={1..36}
using System.Text.Json;
using Anthropic;
using Anthropic.Helpers.Beta;
using Anthropic.Models.Beta.Messages;
using MessageCreateParams = Anthropic.Models.Beta.Messages.MessageCreateParams;
using InputSchema = Anthropic.Models.Beta.Messages.InputSchema;
using Role = Anthropic.Models.Beta.Messages.Role;
using Model = Anthropic.Models.Messages.Model;

var client = new AnthropicClient();

var calculateSumTool = new BetaRunnableTool
{
    Name = "calculate_sum",
    Definition = new BetaTool
    {
        Name = "calculate_sum",
        Description = "Add two numbers together.",
        InputSchema = new InputSchema
        {
            Properties = new Dictionary<string, JsonElement>
            {
                ["a"] = JsonSerializer.SerializeToElement(new { type = "number" }),
                ["b"] = JsonSerializer.SerializeToElement(new { type = "number" }),
            },
            Required = ["a", "b"],
        },
    },
    Run = (toolUse, _) =>
    {
        var a = toolUse.Input["a"].GetDouble();
        var b = toolUse.Input["b"].GetDouble();
        return Task.FromResult<BetaToolResultBlockParamContent>($"{a + b}");
    },
};

var runner = client.Beta.Messages.ToolRunner(
    new MessageCreateParams
    {
        Model = Model.ClaudeOpus4_7,
        MaxTokens = 1024,
        Messages =
        [
            new() { Role = Role.User, Content = "What is 15 + 27?" },
        ],
    },
    [calculateSumTool]
);

await foreach (var stream in runner.Streaming())
{
    await foreach (var streamEvent in stream)
    {
        if (
            streamEvent.TryPickContentBlockDelta(out var deltaEvent)
            && deltaEvent.Delta.TryPickText(out var textDelta)
        )
        {
            Console.Write(textDelta.Text);
        }
    }
    Console.WriteLine();
}
```

</Tab>
<Tab title="Go">

使用 `NewToolRunnerStreaming` 并遍历 `runner.AllStreaming(ctx)`。每次外部迭代产出一次 API 调用的事件流。

```go hidelines={1..33,-1}
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/anthropics/anthropic-sdk-go"
	"github.com/anthropics/anthropic-sdk-go/toolrunner"
)

type CalculateSumInput struct {
	A int `json:"a" jsonschema:"required,description=First number"`
	B int `json:"b" jsonschema:"required,description=Second number"`
}

func main() {
	client := anthropic.NewClient()
	ctx := context.Background()

	calculateSum, err := toolrunner.NewBetaToolFromJSONSchema(
		"calculate_sum",
		"Add two numbers together.",
		func(ctx context.Context, input CalculateSumInput) (anthropic.BetaToolResultBlockParamContentUnion, error) {
			return anthropic.BetaToolResultBlockParamContentUnion{
				OfText: &anthropic.BetaTextBlockParam{Text: fmt.Sprintf("%d", input.A+input.B)},
			}, nil
		},
	)
	if err != nil {
		log.Fatal(err)
	}

	runner := client.Beta.Messages.NewToolRunnerStreaming(
		[]anthropic.BetaTool{calculateSum},
		anthropic.BetaToolRunnerParams{
			BetaMessageNewParams: anthropic.BetaMessageNewParams{
				Model:     anthropic.ModelClaudeOpus4_7,
				MaxTokens: 1024,
				Messages: []anthropic.BetaMessageParam{
					anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("What is 15 + 27?")),
				},
			},
		},
	)

	for events, err := range runner.AllStreaming(ctx) {
		if err != nil {
			log.Fatal(err)
		}
		for event, err := range events {
			if err != nil {
				log.Fatal(err)
			}
			switch eventVariant := event.AsAny().(type) {
			case anthropic.BetaRawContentBlockDeltaEvent:
				switch deltaVariant := eventVariant.Delta.AsAny().(type) {
				case anthropic.BetaTextDelta:
					fmt.Print(deltaVariant.Text)
				case anthropic.BetaInputJSONDelta:
					fmt.Print(deltaVariant.PartialJSON)
				}
			case anthropic.BetaRawMessageStopEvent:
				fmt.Println()
			}
		}
	}
}
```

</Tab>
<Tab title="Java">

调用 `runner.streaming()` 获取每个回合的流。每个 `StreamResponse` 使用后必须关闭。

```java hidelines={1..25}
import com.anthropic.client.AnthropicClient;
import com.anthropic.client.okhttp.AnthropicOkHttpClient;
import com.anthropic.core.http.StreamResponse;
import com.anthropic.helpers.BetaToolRunner;
import com.anthropic.models.beta.messages.BetaRawMessageStreamEvent;
import com.anthropic.models.beta.messages.MessageCreateParams;
import com.anthropic.models.messages.Model;
import com.fasterxml.jackson.annotation.JsonClassDescription;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import java.util.function.Supplier;

@JsonClassDescription("Add two numbers together")
static class CalculateSum implements Supplier<String> {
    @JsonPropertyDescription("First number")
    public double a;

    @JsonPropertyDescription("Second number")
    public double b;

    @Override
    public String get() {
        return String.valueOf(a + b);
    }
}

void main() {
    AnthropicClient client = AnthropicOkHttpClient.fromEnv();

    BetaToolRunner runner = client.beta()
            .messages()
            .toolRunner(MessageCreateParams.builder()
                    .model(Model.CLAUDE_OPUS_4_7)
                    .maxTokens(1024)
                    .addBeta("structured-outputs-2025-11-13")
                    .addUserMessage("What is 15 + 27?")
                    .addTool(CalculateSum.class)
                    .build());

    for (StreamResponse<BetaRawMessageStreamEvent> stream : runner.streaming()) {
        try (stream) {
            stream.stream().forEach(event -> IO.println("event: " + event));
        }
    }
}
```

</Tab>
<Tab title="PHP">

PHP 工具运行器目前不支持流式传输。

</Tab>
<Tab title="Ruby">

使用 `each_streaming` 遍历流式事件。

```ruby hidelines={1..17}
require "anthropic"

client = Anthropic::Client.new

class CalculateSumInput < Anthropic::BaseModel
  required :a, Integer
  required :b, Integer
end

class CalculateSum < Anthropic::BaseTool
  doc "Add two numbers together"
  input_schema CalculateSumInput
  def call(input)
    (input.a + input.b).to_s
  end
end

runner = client.beta.messages.tool_runner(
  model: "claude-opus-4-7",
  max_tokens: 1024,
  tools: [CalculateSum.new],
  messages: [{role: "user", content: "What is 15 + 27?"}]
)

runner.each_streaming do |stream|
  stream.each do |event|
    case event
    when Anthropic::Streaming::TextEvent
      print event.text
    when Anthropic::Streaming::InputJsonEvent
      print event.partial_json
    end
  end
  puts
end
```

</Tab>
</Tabs>

## 后续步骤

- 有关手动控制工具调用循环，请参阅[处理工具调用](/docs/en/agents-and-tools/tool-use/handle-tool-calls)。
- 有关并发运行多个工具，请参阅[并行工具使用](/docs/en/agents-and-tools/tool-use/parallel-tool-use)。
- 有关完整的工具使用工作流，请参阅[定义工具](/docs/en/agents-and-tools/tool-use/define-tools)。