도구 러너

도구 러너 (Tool runner, SDK)

도구 러너는 에이전틱 루프, 오류 감싸기, 타입 안전성을 대신 처리해 주니까 직접 다룰 필요가 없어요. 사람의 승인(HITL)이 필요하거나, 커스텀 로깅이나 조건부 실행이 필요하다면 수동 루프를 쓰세요.

출처: 문서

본문

도구 러너는 에이전틱 루프, 오류 감싸기, 타입 안전성을 대신 처리해 주니 직접 하지 않아도 돼요. 인간 검토(인-더-루프) 승인, 커스텀 로깅, 조건부 실행이 필요할 때는 수동 루프를 쓰세요.

도구 호출, 도구 결과, 대화 관리를 수동으로 다루는 대신 도구 러너가 자동으로:

  • Claude가 호출하면 도구를 실행해요
  • 요청/응답 주기를 처리해요
  • 대화 상태를 관리해요
  • 타입 안전성과 검증을 제공해요
도구 러너는 베타이며 [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)에서 사용할 수 있어요.

기본 사용법 (Basic usage)

SDK 헬퍼로 도구를 정의한 뒤, 도구 러너로 실행하세요.

SDK의 도구 시그니처에 따라 도구는 결과를 문자열이나 콘텐츠 블록(텍스트, 이미지, 문서 블록)으로 반환하므로, 도구가 멀티모달 결과를 반환할 수 있어요. 반환된 문자열은 단일 텍스트 콘텐츠 블록이 돼요. JSON 객체나 숫자 같은 구조화 데이터를 반환하려면 먼저 문자열로 인코딩하세요.

`@beta_tool` 데코레이터로 타입 힌트와 docstring을 가진 도구를 정의하세요.
<Note>
  async 클라이언트를 쓴다면 `@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-5-5",
    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)
```

`@beta_tool` 데코레이터가 함수 인자와 docstring을 검사해 JSON 스키마를 자동으로 유도해요.
Zod 검증이 있는 타입 안전 도구 정의에는 `betaZodTool()`을, JSON Schema 기반 정의에는 `betaTool()`을 쓰세요.
TypeScript는 도구 정의에 두 가지 접근 방식을 제공해요:

**Zod 사용(권장)** - `betaZodTool()`로 Zod 검증이 있는 타입 안전한 도구 정의(Zod 3.25.0 이상 필요):

```typescript
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-5-5",
  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);
  }
}
```

**JSON Schema 사용** - Zod 없이 타입 안전한 도구 정의에 `betaTool()`을 쓰세요:

<Note>
  Claude가 생성한 입력은 런타임에 검증되지 않아요. 필요하면 `run` 함수 안에서 검증을 수행하세요.
</Note>

```typescript
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-5-5",
  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);
  }
}
```
각 도구를 `BetaRunnableTool`로 정의하고, JSON 스키마가 있는 `Definition`과 Claude가 도구를 호출할 때 실행되는 `Run` 델리게이트를 제공하세요.
```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.ClaudeOpus5_5,
        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);
}
```
`toolrunner.NewBetaToolFromJSONSchema`로 도구를 정의하세요. 핸들러의 입력 타입은 `jsonschema:` 태그가 있는 구조체예요. SDK가 그것을 리플렉션해 JSON 스키마를 생성해요.
```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.ModelClaudeOpus5_5,
				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)
	}
}
```

`jsonschema:` 구조체 태그가 입력 스키마를 생성해요. 예를 들어 `CalculateSumInput`은 이렇게 돼요:

```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"]
  }
}
```
각 도구를 `Supplier`을 구현하는 클래스로 정의하세요. 도구 설명에는 클래스를 `@JsonClassDescription`으로, 각 public 필드에는 `@JsonPropertyDescription`으로 매개변수 설명을 주석 처리하세요. SDK가 클래스에서 JSON 스키마, 도구 이름(스네이크 케이스 클래스 이름), 입력 파싱을 유도하고, 도구를 `strict: true`로 표시해요([엄격한 도구 사용](https://platform.claude.com/docs/en/agents-and-tools/tool-use/strict-tool-use)).
```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_5_5)
                    .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);
    }
}
```

클래스 이름 `CalculateSum`이 도구 이름 `calculate_sum`이 되고, SDK가 주석 달린 필드에서 JSON 스키마를 생성해요:

```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
  },
  "strict": true
}
```
각 도구를 `BetaRunnableTool`로 정의해서 도구의 JSON 스키마 정의와 그것을 실행하는 클로저를 짝지어 주세요.
```php
<?php

use Anthropic\Client;
use Anthropic\Beta\Messages\BetaTextBlock;
use Anthropic\Beta\Messages\BetaToolUseBlock;
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_5_5,
    tools: [$getWeather, $calculateSum],
);

foreach ($runner as $message) {
    foreach ($message->content as $block) {
        switch (true) {
            case $block instanceof BetaTextBlock:
                echo $block->text, "\n";
                break;
            case $block instanceof BetaToolUseBlock:
                echo "[Tool call: {$block->name}]\n";
                break;
        }
    }
}
```
`Anthropic::BaseTool` 클래스로 타입이 있는 입력 스키마를 가진 도구를 정의하세요.
```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-5-5",
  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
```

`Anthropic::BaseTool` 클래스는 `doc` 메서드로 도구 설명을, `input_schema`로 예상 매개변수를 정의해요. SDK가 이를 자동으로 적절한 JSON 스키마 형식으로 변환해요.

도구 러너 반복하기 (Iterating over the tool runner)

도구 러너는 Claude의 메시지를 내놓는(yield) 이터러블이에요. 각 반복에서 러너는 Claude가 도구 사용을 요청했는지 확인해요. 그렇다면 도구를 실행하고 결과를 자동으로 Claude에게 보낸 뒤, 다음 메시지를 내놓아 루프가 계속되게 해요.

break 문으로 어느 반복에서든 루프를 끝낼 수 있어요. 러너는 Claude가 도구 사용 없는 메시지를 반환하거나, 설정했다면 max_iterations에 도달할 때까지 반복해요.

중간 메시지가 필요 없다면 마지막 메시지를 직접 얻을 수 있어요:

최종 메시지를 얻으려면 `runner.until_done()`을 쓰세요.
```python
client = anthropic.Anthropic()
# ...
runner = client.beta.messages.tool_runner(
    model="claude-opus-5-5",
    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)
```
최종 메시지를 얻으려면 러너를 `await`하세요.
```typescript
const client = new Anthropic();
// ...
const runner = client.beta.messages.toolRunner({
  model: "claude-opus-5-5",
  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);
  }
}
```
최종 메시지를 얻으려면 `runner.RunUntilDoneAsync()`를 쓰세요.
```csharp
var client = new AnthropicClient();
// ...
var runner = client.Beta.Messages.ToolRunner(
    new MessageCreateParams
    {
        Model = Model.ClaudeOpus5_5,
        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);
    }
}
```
최종 메시지를 얻으려면 `runner.RunToCompletion(ctx)`를 쓰세요.
```go
client := anthropic.NewClient()
ctx := context.Background()
// ...
runner := client.Beta.Messages.NewToolRunner(
	[]anthropic.BetaTool{getWeather},
	anthropic.BetaToolRunnerParams{
		BetaMessageNewParams: anthropic.BetaMessageNewParams{
			Model:     anthropic.ModelClaudeOpus5_5,
			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)
	}
}
```
Java SDK에는 `until_done()` 단축키가 없어요. 다 쓸 때까지 반복하고 마지막 메시지를 유지하세요.
```java
AnthropicClient client = AnthropicOkHttpClient.fromEnv();

BetaToolRunner runner = client.beta()
        .messages()
        .toolRunner(MessageCreateParams.builder()
                .model(Model.CLAUDE_OPUS_5_5)
                .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()));
}
```
최종 메시지를 얻으려면 `runUntilDone()`을 쓰세요.
```php
$client = new Client();
// ...
$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_5_5,
    tools: [$getWeather, $calculateSum],
);

$finalMessage = $runner->runUntilDone();
foreach ($finalMessage->content as $block) {
    if ($block->type === 'text') {
        echo $block->text, "\n";
    }
}
```
모든 메시지를 얻으려면 `runner.run_until_finished`를 쓰세요.
```ruby
client = Anthropic::Client.new
# ...
runner = client.beta.messages.tool_runner(
  model: "claude-opus-5-5",
  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 }
```

고급 사용법 (Advanced usage)

루프 안에서 각 응답 메시지를 읽고 다음 API 호출 전에 러너의 상태를 수정할 수 있어요. 각 반복은 이 수명 주기를 따라요:

  1. 러너가 현재 상태로 Messages API에 요청을 보내요.

  2. 러너가 응답 메시지를 루프 본문에 내놓아요.

  3. 루프 본문이 실행돼요. 메시지를 읽고 선택적으로 러너의 상태를 수정할 수 있어요.

  4. 루프 본문이 돌아가면 러너가 메시지 기록을 수정했는지 확인해요.

    • 메시지 기록을 수정하지 않았다면: 메시지에 도구 호출이 있으면 러너가 어시스턴트 메시지와 도구 결과를 추가한 뒤 계속해요. 도구 호출이 없으면 루프가 종료돼요.
    • 메시지 기록을 수정했다면: 러너가 자동 추가를 건너뛰고 우리 상태를 그대로 사용해요. 메시지 기록 인계받기를 참고하세요.
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: If tool calls, append assistant<br/>message + tool results and continue.<br/>If none, exit the loop
    else Message history changed
      TR->>TR: Use your state unchanged
    end
  end

메시지 기록 인계받기 (Taking over message history)

기본적으로 러너가 대화 상태를 관리해 줘요. 도구 호출 턴이 끝날 때마다 어시스턴트 메시지와 모든 도구 결과를 자체 메시지 기록에 추가해요. 턴을 재시도하려 하거나(응답을 버리고 다시 보내기), 후속 메시지를 주입하거나, 도구 결과를 직접 만들 때 메시지 기록을 인계받아요.

루프 본문 안에서 러너의 messages를 수정해 인계받아요. 정확한 방법은 SDK에 따라 달라져요. 다음 언어별 탭을 보세요.

한 반복에 대해 인계받으면 러너는 그 턴의 어시스턴트 메시지나 도구 결과를 추가하지 않아요. 대화가 유효하게 유지되는 건 우리 책임이에요. (턴이 계산되게 하려면) 어시스턴트 메시지와 도구 결과를 직접 추가하고, 도구 호출이 없을 때 루프가 여전히 종료될 수 있도록 상태를 조건부로 수정하며, 루프에 상한을 주려면 max_iterations를 전달하세요. 일곱 개 SDK 모두 max_iterations를 지원해요.

`generate_tool_call_response()`로 도구 결과를 검사하거나 계산하세요. 루프 안에서 `append_messages()`를 호출하면 러너에게 기록을 직접 관리하고 있다고 알려 주니, 추가하는 것에 어시스턴트 메시지와 도구 결과를 포함하세요.
```python
runner = client.beta.messages.tool_runner(
    model="claude-opus-5-5",
    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.
```

메시지 기록을 인계받지 않고 `max_tokens` 같은 요청 매개변수를 바꾸려면 `set_messages_params()`를 쓰세요. 러너는 어시스턴트 메시지와 도구 결과를 여전히 자동으로 추가해요.

```python
for message in runner:
    runner.set_messages_params(lambda params: {**params, "max_tokens": 2048})
```
현재 요청 매개변수를 읽으려면 `runner.params`를, 교체하려면 `setMessagesParams()`를 쓰세요. 루프 안에서 `setMessagesParams()`나 `pushMessages()`를 호출하면 러너에게 상태를 직접 관리하고 있다고 알려 줘요. 이 반복의 어시스턴트 메시지와 도구 결과는 버려지고, 다음 요청은 우리 상태로 나가요.
다음 예시는 잘린 응답을 더 큰 `max_tokens` 예산으로 재시도해요.

```typescript
const runner = client.beta.messages.toolRunner({
  model: "claude-opus-5-5",
  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.
}
```
`SetParams()`나 `PushMessages()`를 호출하면 상태가 수정된 것으로 표시되어 러너가 그 턴의 자동 추가를 건너뛰어요. C# 러너는 그 턴에 일치한 도구를 여전히 실행하고 자동 생성된 결과를 버리므로, 루프 본문 안에서 직접 실행하는 도구는 감안하지 않으면 두 번 실행돼요. 인계받을 때는 어시스턴트 메시지와 도구 결과를 직접 푸시하세요. 그렇지 않으면 대화가 전진하지 않아요. C# 러너는 응답에 도구 호출이 없으면 항상 종료하므로, 상태 변이를 `tool_use` 블록 존재에 조건화하세요.
```csharp
var runner = client.Beta.Messages.ToolRunner(
    new MessageCreateParams
    {
        Model = Model.ClaudeOpus5_5,
        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." }
    );
}
```
Go 러너는 public `Params` 필드로 매개변수를 노출해요. `NextMessage(ctx)` 호출 사이에 `runner.Params`를 수정하면 다음 API 요청에 적용돼요. 다른 SDK와 달리 Go 러너는 어시스턴트 메시지와 도구 결과를 항상 무조건 추가해요. `Params`를 수정해도 그 단계를 없애지 않아요.
```go
runner := client.Beta.Messages.NewToolRunner(
	[]anthropic.BetaTool{getWeather},
	anthropic.BetaToolRunnerParams{
		BetaMessageNewParams: anthropic.BetaMessageNewParams{
			Model:     anthropic.ModelClaudeOpus5_5,
			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
}
```
현재 매개변수를 읽으려면 `runner.params()`를, 다음 반복에 교체하려면 `runner.setNextParams()`를 쓰세요. 루프 안에서 `setNextParams()`를 호출하면 러너가 자동 추가를 건너뛰어요. 방금 내놓은 메시지는 버려지고, 다음 반복은 새 매개변수를 그대로 보내요.
다음 예시는 토큰 한계에 닿은 턴을 `max_tokens`를 두 배로 늘려 재시도해요. `max_tokens` 분기에서만 변이하면 루프가 수렴해요. 정상 완료된 턴은 빠져나가고, 러너가 자동 추가하다가 도구 호출이 없으면 종료하거든요.

```java
BetaToolRunner runner = client.beta()
        .messages()
        .toolRunner(ToolRunnerCreateParams.builder()
                .initialMessageParams(MessageCreateParams.builder()
                        .model(Model.CLAUDE_OPUS_5_5)
                        .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.
}
```
러너 상태를 수정하려면 `setMessagesParams()`와 `pushMessages()`를, 읽으려면 `getParams()`를 쓰세요. 루프 안에서 두 세터 중 하나를 호출하면 러너에게 자동 추가를 건너뛰라고 알려 주니, 대화가 수정된 상태에서 이어져요.
다음 예시는 응답이 잘렸을 때 `max_tokens`를 두 배로 늘리고 재시도해요.

```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_5_5,
    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]);
    }
}
```
단계별 제어에는 `next_message`를 쓰세요. `next_message`가 반환될 때쯤이면 그 턴의 어시스턴트 메시지와 도구 결과가 이미 추가됐어요. 턴 사이에 후속 메시지를 주입하려면 `feed_messages`를, 요청 매개변수를 제자리에서 바꾸려면 `runner.params.update(...)`를 쓰세요.
`each_message`나 `each_streaming` 블록 안에서 `runner.params[:messages]`를 재할당하거나 `feed_messages`를 호출하면 메시지 기록을 인계받아요. 다음 패턴은 `next_message` 호출 사이에 `feed_messages`를 호출하는데, 이는 인계가 아니에요.

```ruby
runner = client.beta.messages.tool_runner(
  model: "claude-opus-5-5",
  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] takes over
# message history only when it happens inside an each_message or each_streaming block.
runner.params.update(max_tokens: 2048)

runner.run_until_finished
```

자동 컨텍스트 관리 (Automatic context management)

오래 돌아가는 에이전틱 작업에서 TypeScript와 Ruby 도구 러너는 자동 컴팩션을 지원해요. 토큰 사용량이 임계값을 넘으면 요약을 생성해서 컨텍스트 창 한계를 넘어 대화를 이어가게 해 줘요. 두 SDK는 이 클라이언트 쪽 옵션을 폐기하고, context_management 요청 매개변수로 모든 SDK의 도구 러너와 동작하는 서버 쪽 컴팩션을 선호해요. Python SDK(v1.0 이상)와 Go, Java, C#, PHP 도구 러너는 클라이언트 쪽 컴팩션을 포함하지 않아요. Python, TypeScript, C#, Go, Java, PHP, Ruby 도구 러너는 주문형 컴팩션용 compact_before_next_turn() 헬퍼를 갖고 있어요(각 언어의 케이싱으로). 루프 안에서 컴팩션을 참고하세요. 러너에서 그것이나 context_management 컴팩션 편집을 쓰되, 둘 다는 쓰지 마세요.

도구 실행 디버깅 (Debugging tool execution)

도구가 예외를 던지면 도구 러너가 그것을 잡아 is_error: true인 도구 결과로 Claude에게 오류를 반환해요. 도구 결과는 예외의 메시지(Python에서는 타입과 메시지)를 담지, 전체 스택 트레이스는 담지 않아요.

SDK가 로깅하는 것은 언어마다 달라요. Python SDK는 도구가 처리되지 않은 예외를 일으킬 때마다 표준 logging 모듈을 통해 전체 예외(스택 트레이스 포함)를 로깅해요. Python, TypeScript, Java SDK는 ANTHROPIC_LOG 환경 변수를 읽어 요청·응답 상세를 포함한 SDK 로깅을 켜요:

# Log at info level
export ANTHROPIC_LOG=info

# Log at debug level for more verbose output
export ANTHROPIC_LOG=debug

Go, Ruby, C#, PHP SDK는 ANTHROPIC_LOG를 읽지 않아요. Python 밖에서는 어떤 SDK도 실패한 도구를 로깅하지 않아요. 도구가 왜 실패했는지 보려면 도구 함수 안에서 예외를 잡아 반환하거나 다시 던지기 전에 로깅하세요.

도구 오류 가로채기 (Intercepting tool errors)

기본적으로 도구 오류는 Claude에게 전달되고, Claude가 적절히 응답할 수 있어요. 하지만 오류를 감지해 다르게 처리하고 싶을 수 있어요. 예를 들어 실행을 일찍 멈추거나 커스텀 오류 처리를 구현하는 등이죠.

Python과 TypeScript SDK에서는 도구 응답 메서드(Python의 generate_tool_call_response(), TypeScript의 generateToolResponse())로 도구 결과를 가로채 Claude에게 보내기 전에 오류를 확인할 수 있어요. 다른 SDK는 그 훅을 노출하지 않아요. 해당 탭들이 가장 가까운 대안을 설명해요:

```python client = anthropic.Anthropic() # ... runner = client.beta.messages.tool_runner( model="claude-opus-5-5", 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)
```
```typescript const client = new Anthropic(); // ... const runner = client.beta.messages.toolRunner({ model: "claude-opus-5-5", 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 && typeof toolResultMessage.content !== "string") {
    // 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);
}
```
C# 도구 러너는 Claude에게 보내기 전에 도구 결과를 검사하는 훅을 노출하지 않아요. 오류 콘텐츠를 제어하려면 도구 본문 안에서 `BetaToolError`를 던지세요. 러너가 그것을 우리가 제공한 content와 함께 `is_error: true`인 `tool_result`로 변환해요.
```csharp
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.ClaudeOpus5_5,
        MaxTokens = 1024,
        Messages =
        [
            new() { Role = Role.User, Content = "What's the weather in San Francisco?" },
        ],
    },
    [getWeatherTool]
);

Console.WriteLine(await runner.RunUntilDoneAsync());
```
Claude에게 보내기 전에 도구 오류를 가로채는 것은 Go SDK에서 현재 지원되지 않아요. 러너는 핸들러가 반환하는 오류를 내부적으로 `is_error: true`인 도구 결과로 변환해요. 오류 콘텐츠를 커스텀하려면 핸들러 안에서 오류를 잡고 오류를 반환하는 대신 결과를 반환하세요. Claude에게 보내기 전에 도구 오류를 가로채는 것은 Java SDK에서 현재 지원되지 않아요. 러너는 도구의 `get()` 메서드가 던지는 모든 예외를 잡아 자동으로 `is_error: true`인 도구 결과로 변환해요. 오류 콘텐츠를 제어하려면 도구 안에서 예외를 잡고 커스텀 문자열을 반환하세요. PHP 도구 러너는 현재 도구 결과가 추가되기 전에 노출하지 않아요. 도구의 `run` 클로저에서 던진 예외는 잡혀 `is_error: true`인 도구 결과로 자동으로 Claude에게 보내져요. 오류 콘텐츠를 검사하거나 교체하려면 [도구 결과 수정하기](#modifying-tool-results)에 나온 수동 `pushMessages()` 패턴을 쓰세요. ```ruby client = Anthropic::Client.new # ... runner = client.beta.messages.tool_runner( model: "claude-opus-5-5", max_tokens: 1024, tools: [MyTool.new], messages: [{role: "user", content: "Run my_tool with the query 'hello'."}] )
loop do
  message = runner.next_message
  break unless message

  # By the time next_message returns, the runner has run this turn's tools and
  # appended their results as the last (user-role) message. Inspect them here,
  # before the next request sends them to Claude.
  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
  break if message.stop_reason != :tool_use
end
```

도구 결과 수정하기 (Modifying tool results)

Claude에게 다시 보내기 전에 도구 결과를 수정할 수 있어요. 도구 결과에 cache_control 같은 메타데이터를 추가해 프롬프트 캐싱을 활성화하거나, 도구 출력을 변환할 때 유용해요.

Python과 TypeScript SDK에서는 도구 응답 메서드로 도구 결과를 얻은 뒤 러너가 진행하기 전에 수정하세요. 수정된 결과를 명시적으로 추가할지, 제자리에서 변이할지는 SDK에 따라 달라요. 각 탭의 코드 주석을 참고하세요.

```python client = anthropic.Anthropic() # ... runner = client.beta.messages.tool_runner( model="claude-opus-5-5", 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)
```
```typescript const client = new Anthropic(); // ... const runner = client.beta.messages.toolRunner({ model: "claude-opus-5-5", 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);
}
```
추가되기 전에 도구 결과를 수정하는 것(예: `cache_control` 추가)은 C# SDK에서 현재 지원되지 않아요. 러너가 `tool_result` 블록을 내부에서 구성하고 그것을 바꿀 훅을 제공하지 않아요. Go 러너는 바깥쪽 `tool_result` 블록을 수정하는 훅을 노출하지 않아요. 하지만 핸들러가 반환하는 안쪽 콘텐츠 블록에는 `cache_control`을 설정할 수 있어요.
```go
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.ModelClaudeOpus5_5,
			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)
```
도구 결과에 `cache_control`을 설정하려면 `String` 대신 `BetaToolResultBlockParam.Content`를 도구에서 반환하고 안쪽 텍스트 블록에 `cacheControl`을 설정하세요. 러너는 현재 바깥쪽 `tool_result` 블록의 `cache_control` 설정을 지원하지 않아요.
```java
@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())));
    }
}
```
PHP 도구 러너는 자동 생성된 `tool_result` 블록을 바꾸는 콜백이 없어요. `cache_control` 같은 필드를 추가하려면 도구 결과를 직접 만들고 푸시하세요. `pushMessages()`를 호출하면 그 턴의 러너 자동 추가를 건너뛰어요.
```php
$client = new Client();
// ...
$runner = $client->beta->messages->toolRunner(
    maxTokens: 1024,
    messages: [
        ['role' => 'user', 'content' => 'Search for information about the climate of San Francisco.'],
    ],
    model: Model::CLAUDE_OPUS_5_5,
    tools: [$searchDocuments],
);

foreach ($runner as $message) {
    $toolResults = [];
    foreach ($message->content as $block) {
        if ($block instanceof \Anthropic\Beta\Messages\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.
}
```
```ruby client = Anthropic::Client.new # ... runner = client.beta.messages.tool_runner( model: "claude-opus-5-5", 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
```
도구가 큰 데이터(문서 검색 결과 같은)를 반환해서 후속 API 호출에 캐시하고 싶을 때 도구 결과에 `cache_control`을 추가하면 특히 유용해요. 캐싱 전략에 대한 더 자세한 내용은 [프롬프트 캐싱](https://platform.claude.com/docs/en/build-with-claude/prompt-caching)을 참고하세요.

스트리밍 (Streaming)

스트리밍을 켜면 각 턴의 응답을 점진적으로 처리할 수 있어요. 각 반복은 이벤트를 반복할 수 있는 스트림 객체를 내놓아요.

`stream=True`를 설정하고 누적된 메시지를 얻으려면 `get_final_message()`을 쓰세요.
```python
client = anthropic.Anthropic()
# ...
runner = client.beta.messages.tool_runner(
    model="claude-opus-5-5",
    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())
```
`stream: true`를 설정하고 누적된 메시지를 얻으려면 `finalMessage()`를 쓰세요.
```typescript
const client = new Anthropic();
// ...
const runner = client.beta.messages.toolRunner({
  model: "claude-opus-5-5",
  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);
```
`runner.Streaming()`을 호출하면 중첩된 async 시퀀스를 얻어요. 각 API 호출에 대해 하나의 내부 스트림이에요.
```csharp
var client = new AnthropicClient();
// ...
var runner = client.Beta.Messages.ToolRunner(
    new MessageCreateParams
    {
        Model = Model.ClaudeOpus5_5,
        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();
}
```
`NewToolRunnerStreaming`을 쓰고 `runner.AllStreaming(ctx)`를 반복하세요. 각 바깥 반복은 한 API 호출에 대한 이벤트 스트림을 내놓아요.
```go
client := anthropic.NewClient()
ctx := context.Background()
// ...
runner := client.Beta.Messages.NewToolRunnerStreaming(
	[]anthropic.BetaTool{calculateSum},
	anthropic.BetaToolRunnerParams{
		BetaMessageNewParams: anthropic.BetaMessageNewParams{
			Model:     anthropic.ModelClaudeOpus5_5,
			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()
		}
	}
}
```
각 턴의 스트림을 얻으려면 `runner.streaming()`을 호출하세요. 각 `StreamResponse`는 사용 후 닫아야 해요.
```java
void main() {
    AnthropicClient client = AnthropicOkHttpClient.fromEnv();

    BetaToolRunner runner = client.beta()
            .messages()
            .toolRunner(MessageCreateParams.builder()
                    .model(Model.CLAUDE_OPUS_5_5)
                    .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));
        }
    }
}
```
스트리밍은 현재 PHP 도구 러너에서 사용할 수 없어요. 스트리밍 이벤트를 반복하려면 `each_streaming`을 쓰세요.
```ruby
client = Anthropic::Client.new
# ...
runner = client.beta.messages.tool_runner(
  model: "claude-opus-5-5",
  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
```

더 알아보기 (Learn more)