---
title: 도구 러너 (SDK)
url: https://platform.claude.com/docs/ko/agents-and-tools/tool-use/tool-runner
description: SDK의 도구 러너를 사용하여 에이전트 루프, 오류 래핑, 타입 안전성을 자동으로 처리하세요.
---

"Tool runner"(도구 러너)는 에이전트 루프, 오류 래핑, 타입 안전성을 대신 처리해 주므로 직접 처리할 필요가 없습니다. 사람의 승인(human-in-the-loop), 사용자 정의 로깅 또는 조건부 실행이 필요한 경우에는 대신 [수동 루프](https://platform.claude.com/docs/ko/agents-and-tools/tool-use/handle-tool-calls)를 사용하세요.

도구 호출, 도구 결과, 대화 관리를 수동으로 처리하는 대신, 도구 러너는 자동으로 다음을 수행합니다:

* 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/BetaToolRunnerRunnableToolExample.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 헬퍼를 사용하여 도구를 정의한 다음, 도구 러너를 사용하여 실행하세요.

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

<Tabs exclude="shell">
  <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-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` 데코레이터는 함수 인수와 독스트링을 검사하여 JSON 스키마를 자동으로 도출합니다.
  </Tab>

  <Tab title="TypeScript">
    Zod 유효성 검사를 사용하는 타입 안전 도구 정의에는 `betaZodTool()`을, JSON Schema 기반 정의에는 `betaTool()`을 사용하세요.

    TypeScript는 도구를 정의하는 두 가지 방법을 제공합니다:

    **Zod 사용 (권장)** - Zod 유효성 검사를 사용하는 타입 안전 도구 정의에는 `betaZodTool()`을 사용하세요 (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);
      }
    }
    ```
  </Tab>

  <Tab title="C#">
    각 도구를 `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);
    }
    ```
  </Tab>

  <Tab title="Go">
    `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"]
      }
    }
    ```
  </Tab>

  <Tab title="Java">
    각 도구를 입력 클래스와 Claude가 도구를 호출할 때 실행되는 함수를 짝지은 `BetaRunnableTool`로 정의하세요. 도구 설명을 위해 입력 클래스에 `@JsonClassDescription`을, 매개변수 설명을 위해 각 public 필드에 `@JsonPropertyDescription`을 어노테이션으로 지정하세요. SDK는 클래스로부터 JSON 스키마, 도구 이름(스네이크 케이스로 변환된 클래스 이름), 입력 파싱을 도출하며, 도구에 `strict: true`([엄격한 도구 사용](https://platform.claude.com/docs/ko/agents-and-tools/tool-use/strict-tool-use))를 표시합니다.

    함수는 파싱된 입력을 해당 클래스의 인스턴스로 받아 `BetaToolResultBlockParam.Content`를 반환합니다. 텍스트를 반환하려면 `BetaToolResultBlockParam.Content.ofString()`으로 감싸세요. 함수가 람다이므로 다음 예제의 `WeatherService`와 같이 애플리케이션의 객체를 사용할 수 있습니다.

    ```java
    import com.anthropic.client.AnthropicClient;
    import com.anthropic.client.okhttp.AnthropicOkHttpClient;
    import com.anthropic.helpers.BetaRunnableTool;
    import com.anthropic.helpers.BetaToolRunner;
    import com.anthropic.models.beta.messages.BetaMessage;
    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;

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

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

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

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

    // 애플리케이션에 이미 있는 클래스를 대신하는 예시입니다.
    // 예를 들어 데이터베이스 클라이언트나 API 래퍼 등이 해당합니다.
    static class WeatherService {
        String currentWeather(String location, String unit) {
            return "{\"temperature\": \"20°C\", \"condition\": \"Sunny\"}";
        }
    }

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

        // 람다에서 weatherService를 사용할 수 있습니다.
        BetaRunnableTool getWeather = BetaRunnableTool.of(
                GetWeather.class,
                input -> BetaToolResultBlockParam.Content.ofString(
                        weatherService.currentWeather(input.location, input.unit)));

        BetaRunnableTool calculateSum = BetaRunnableTool.of(
                CalculateSum.class,
                input -> BetaToolResultBlockParam.Content.ofString(
                        String.valueOf(input.a + input.b)));

        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)
                        .addTool(calculateSum)
                        .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
    }
    ```
  </Tab>

  <Tab title="PHP">
    각 도구를 도구의 JSON 스키마 정의와 이를 실행하는 클로저를 짝지은 `BetaRunnableTool`로 정의하세요.

    ```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;
            }
        }
    }
    ```
  </Tab>

  <Tab title="Ruby">
    `Anthropic::BaseTool` 클래스를 사용하여 타입이 지정된 입력 스키마로 도구를 정의하세요.

    ```ruby
    require "anthropic"

    # 클라이언트를 초기화합니다
    client = Anthropic::Client.new

    # 입력 스키마를 정의합니다
    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

    # 도구를 정의합니다
    class GetWeather < Anthropic::BaseTool
      doc "Get the current weather in a given location"
      input_schema GetWeatherInput

      def call(input)
        # 실제 구현에서는 여기서 날씨 API를 호출합니다
        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

    # 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 스키마 형식으로 자동 변환합니다.
  </Tab>
</Tabs>

## 도구 러너 반복하기

도구 러너는 Claude의 메시지를 산출(yield)하는 이터러블입니다. 각 반복에서 러너는 Claude가 도구 사용을 요청했는지 확인합니다. 요청했다면 도구를 실행하고 결과를 자동으로 Claude에 다시 보낸 다음, 루프를 계속할 수 있도록 Claude의 다음 메시지를 산출합니다.

`break` 문을 사용하여 어느 반복에서든 루프를 종료할 수 있습니다. 러너는 Claude가 도구 사용이 없는 메시지를 반환할 때까지, 또는 설정한 경우 `max_iterations` (csharp, java, php: `maxIterations`; go: `MaxIterations`)에 도달할 때까지 반복합니다.

중간 메시지가 필요하지 않다면 최종 메시지를 직접 가져올 수 있습니다:

<Tabs exclude="shell">
  <Tab title="Python">
    `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)
    ```
  </Tab>

  <Tab title="TypeScript">
    러너를 `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);
      }
    }
    ```
  </Tab>

  <Tab title="C#">
    `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);
        }
    }
    ```
  </Tab>

  <Tab title="Go">
    `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)
    	}
    }
    ```
  </Tab>

  <Tab title="Java">
    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)
                    .addTool(calculateSum)
                    .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
    $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";
        }
    }
    ```
  </Tab>

  <Tab title="Ruby">
    `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 }
    ```
  </Tab>
</Tabs>

## 고급 사용법

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

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

2. 러너가 응답 메시지를 루프 본문에 산출합니다.

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

4. 루프 본문이 반환되면 러너는 메시지 기록을 수정했는지 확인합니다.

   * **메시지 기록을 수정하지 않은 경우:** 메시지에 도구 호출이 포함되어 있으면 러너는 어시스턴트 메시지와 도구 결과를 추가한 다음 계속합니다. 도구 호출이 없으면 루프가 종료됩니다.
   * **메시지 기록을 수정한 경우:** 러너는 자동 추가를 건너뛰고 사용자의 상태를 변경 없이 사용합니다. [메시지 기록 직접 관리하기](https://platform.claude.com/docs/ko/agents-and-tools/tool-use/tool-runner#taking-over-message-history)를 참조하세요.

```mermaid
sequenceDiagram
  accTitle: The tool runner loop
  accDescr: In each iteration, the tool runner sends a request with the current state to the Messages API. It receives the response message and yields it to your code. Your loop body runs, then the tool runner resumes. If the message history is unchanged and there are tool calls, it appends the assistant message and the tool results and continues. If there are none, it exits the loop. If the message history changed, it uses your state unchanged.
  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
```

### 메시지 기록 직접 관리하기

기본적으로 러너는 대화 상태를 대신 관리합니다. 각 도구 호출 턴 이후 어시스턴트 메시지와 도구 결과를 자체 메시지 기록에 추가합니다. 턴을 재시도(응답을 버리고 다시 전송)하거나, 후속 메시지를 삽입하거나, 도구 결과를 직접 구성하려는 경우 메시지 기록을 직접 관리하게 됩니다.

루프 본문 내부에서 러너의 메시지를 수정하면 직접 관리하게 됩니다. 정확한 방법은 SDK에 따라 다릅니다. 이어지는 언어별 탭을 참조하세요.

특정 반복에서 직접 관리하는 경우, 러너는 해당 턴의 어시스턴트 메시지나 도구 결과를 추가하지 않습니다. 대화를 유효하게 유지하는 책임은 사용자에게 있습니다. 어시스턴트 메시지와 도구 결과를 직접 추가하고(해당 턴을 반영하려는 경우), 도구 호출이 없을 때 루프가 여전히 종료될 수 있도록 상태를 조건부로 수정하며, 루프를 제한하기 위해 `max_iterations` (csharp, java, php: `maxIterations`; go: `MaxIterations`)를 전달하세요. 7개 SDK 모두 `max_iterations` (csharp, java, php: `maxIterations`; go: `MaxIterations`)를 지원합니다.

<Tabs exclude="shell">
  <Tab title="Python">
    `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()는 상태를 수정됨으로 표시하므로 러너는 이번 반복에서
            # 자동 추가를 건너뜁니다. 어시스턴트 메시지와 tool result를 직접 추가하고
            # 필요한 후속 메시지도 함께 추가하세요.
            runner.append_messages(
                message,
                tool_response,
                {"role": "user", "content": "Please be concise."},
            )
        # 도구 호출이 없으면 상태를 그대로 두어 루프가 종료되도록 합니다.
    ```

    메시지 기록을 직접 관리하지 않고 `max_tokens`와 같은 요청 매개변수를 변경하려면 `set_messages_params()`를 사용하세요. 러너는 여전히 어시스턴트 메시지와 도구 결과를 자동으로 추가합니다.

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

  <Tab title="TypeScript">
    `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}.`);
        // 예산을 늘립니다. setMessagesParams()가 상태를 수정됨으로 표시하므로
        // 러너는 잘린 메시지를 추가하지 않습니다. 다음 반복에서 더 큰 예산으로
        // 동일한 턴을 재시도합니다.
        runner.setMessagesParams((params) => ({ ...params, max_tokens: doubled }));
      }
      // 그 외에는 상태를 그대로 두어 러너가 자동으로 추가하고 계속 진행하게 합니다.
    }
    ```
  </Tab>

  <Tab title="C#">
    `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)
        {
            // 도구 호출 없음: 루프가 정상 종료되도록 상태를 그대로 둡니다.
            continue;
        }

        // 도구를 직접 실행하고 결과 블록을 구성합니다.
        var toolResult = new BetaToolResultBlockParam(toolUseBlock.ID)
        {
            Content = await getWeatherTool.ExecuteAsync(toolUseBlock, default),
        };

        // PushMessages()가 상태를 수정됨으로 표시하므로 러너는 자동 추가를 건너뜁니다.
        // 어시스턴트 턴과 도구 결과를 직접 제공한 다음 후속 메시지를 추가합니다.
        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">
    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
    	}

    	// Go 러너는 항상 어시스턴트 메시지와 도구 결과를 추가합니다.
    	// 여기서 변경한 매개변수는 다음 반복에 적용됩니다.
    	runner.Params.MaxTokens = 2048
    }
    ```
  </Tab>

  <Tab title="Java">
    `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)
                            .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 + ".");

            // setNextParams() 호출은 이 턴을 사용자 관리로 표시합니다: 러너는
            // 잘린 메시지를 자동으로 추가하지 않으므로, 다음 반복에서
            // 더 큰 예산으로 동일한 대화 접두사를 다시 전송합니다.
            runner.setNextParams(runner.params().toBuilder().maxTokens(doubled).build());
        }
        // 일반 턴에서는 변경 없음: 러너가 자동으로 추가하고 계속 진행합니다.
    }
    ```
  </Tab>

  <Tab title="PHP">
    `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";

            // 루프 내에서 setMessagesParams()를 호출하면 러너가 자동 추가를
            // 건너뜁니다. 잘린 메시지는 폐기되고, 다음 반복에서 더 큰 예산으로
            // 재시도합니다.
            // 키는 camelCase이며 toolRunner()의 명명된 매개변수와 일치합니다.
            $runner->setMessagesParams(['maxTokens' => $doubled]);
        }
    }
    ```
  </Tab>

  <Tab title="Ruby">
    단계별 제어에는 `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?"}]
    )

    # 러너를 한 번 진행합니다. 어시스턴트 메시지와 도구 결과는 next_message가 반환되기 전에
    # runner.params[:messages]에 추가됩니다.
    message = runner.next_message
    puts message.content

    # 계속하기 전에 후속 메시지를 주입합니다. feed_messages는 배열이 아닌 splat을 받습니다.
    runner.feed_messages({role: "user", content: "Also check Boston."})

    # 매개변수를 제자리에서 변경합니다. runner.params[:messages] 재할당은 each_message 또는
    # each_streaming 블록 내부에서 발생할 때만 메시지 기록을 대체합니다.
    runner.params.update(max_tokens: 2048)

    runner.run_until_finished
    ```
  </Tab>
</Tabs>

### 자동 컨텍스트 관리

장기 실행 에이전트 작업의 경우, TypeScript 및 Ruby 도구 러너는 자동 ["compaction"(압축)](https://platform.claude.com/docs/ko/build-with-claude/context-editing#client-side-compaction-sdk)을 지원합니다. 이는 토큰 사용량이 임계값을 초과하면 요약을 생성하여 대화가 컨텍스트 윈도우 제한을 넘어 계속될 수 있도록 합니다. 두 SDK 모두 이 클라이언트 측 옵션을 지원 중단하고 [서버 측 압축](https://platform.claude.com/docs/ko/build-with-claude/compaction-threshold)을 권장하며, 서버 측 압축은 `context_management` 요청 매개변수를 통해 모든 SDK의 도구 러너에서 작동합니다. Python SDK(v1.0 이상)와 Go, Java, C#, PHP 도구 러너에는 클라이언트 측 압축이 포함되어 있지 않습니다. 도구 러너에는 온디맨드 압축을 위한 `compact_before_next_turn()` (typescript, java, php: `compactBeforeNextTurn()`; csharp, go: `CompactBeforeNextTurn()`) 헬퍼가 있습니다. [루프에서 압축하기](https://platform.claude.com/docs/ko/build-with-claude/compaction-on-demand#compact-in-a-loop)를 참조하세요. 러너에서는 이 헬퍼 또는 `context_management` 압축 편집 중 하나만 사용하고, 둘 다 사용하지 마세요.

### 도구 실행 디버깅

도구가 예외를 던지면 도구 러너는 이를 잡아 `is_error: true`인 도구 결과로 Claude에 오류를 반환합니다. 도구 결과에는 전체 스택 트레이스가 아닌 예외의 메시지(Python에서는 타입과 메시지)가 담깁니다.

SDK가 로깅하는 내용은 언어별로 다릅니다. Python SDK는 도구가 처리되지 않은 예외를 발생시킬 때마다 표준 `logging` 모듈을 통해 스택 트레이스를 포함한 전체 예외를 로깅합니다. Python, TypeScript, Java SDK는 `ANTHROPIC_LOG` 환경 변수를 읽어 요청 및 응답 세부 정보를 포함하는 SDK 로깅을 활성화합니다:

```bash
# info 수준으로 로그 기록
export ANTHROPIC_LOG=info

# 더 자세한 출력을 위해 debug 수준으로 로그 기록
export ANTHROPIC_LOG=debug
```

Go, Ruby, C#, PHP SDK는 `ANTHROPIC_LOG`를 읽지 않습니다. Python 외에는 어떤 SDK도 실패한 도구를 로깅하지 않습니다. 도구가 실패한 이유를 확인하려면 반환하거나 다시 던지기 전에 도구 함수 내부에서 예외를 잡아 로깅하세요.

### 도구 오류 가로채기

기본적으로 도구 오류는 Claude에 다시 전달되며, Claude는 이에 적절히 응답할 수 있습니다. 그러나 오류를 감지하여 다르게 처리하고 싶을 수 있습니다. 예를 들어 실행을 조기에 중단하거나 사용자 정의 오류 처리를 구현하는 경우입니다.

<Tabs exclude="shell">
  <Tab title="Python">
    ```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는 dict입니다: {"role": "user", "content": [...]}
            # 도구 결과 중 오류가 있는지 확인합니다
            for block in tool_response["content"]:
                if block.get("is_error"):
                    # 옵션 1: 예외를 발생시켜 루프를 중단합니다
                    raise RuntimeError(f"Tool failed: {json.dumps(block['content'])}")

                    # 옵션 2: 로그를 남기고 계속 진행합니다(Claude가 처리하도록 함)
                    # logger.error(f"Tool error: {json.dumps(block['content'])}")

        # 메시지를 정상적으로 처리합니다
        print(message.content)
    ```

    <Note>
      루프에서 

      `runner.generate_tool_call_response()`

      를 호출하여 도구 결과를 가져오고, 러너가 이를 Claude에게 보내기 전에 오류가 있는지 확인하세요.
    </Note>
  </Tab>

  <Tab title="TypeScript">
    ```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") {
        // 도구 결과 중 오류가 있는지 확인합니다
        for (const block of toolResultMessage.content) {
          if (block.type === "tool_result" && block.is_error) {
            // 옵션 1: 예외를 던져 루프를 중단합니다
            throw new Error(`Tool failed: ${JSON.stringify(block.content)}`);

            // 옵션 2: 로그를 남기고 계속 진행합니다(Claude가 처리하도록 함)
            // console.error(`Tool error: ${JSON.stringify(block.content)}`);
          }
        }
      }

      // 메시지를 정상적으로 처리합니다
      console.log(message.content);
    }
    ```

    <Note>
      루프에서 

      `runner.generateToolResponse()`

      를 호출하여 도구 결과를 가져오고, 러너가 이를 Claude에게 보내기 전에 오류가 있는지 확인하세요.
    </Note>
  </Tab>

  <Tab title="C#">
    C# 도구 러너는 도구 결과가 Claude에 전송되기 전에 검사할 수 있는 훅을 노출하지 않습니다. 오류 내용을 제어하려면 도구 본문 내부에서 `BetaToolError`를 던지세요. 러너는 이를 `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)
            {
                // Claude가 실패를 확인하기 전에 먼저 살펴봐야 한다면 여기에서 로그를 남기세요.
                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());
    ```
  </Tab>

  <Tab title="Go">
    도구 오류를 Claude에 전송되기 전에 가로채는 기능은 현재 Go SDK에서 지원되지 않습니다. 러너는 핸들러에서 반환된 오류를 내부적으로 `is_error: true`인 도구 결과로 변환합니다. 오류 내용을 사용자 정의하려면 핸들러 내부에서 오류를 잡고 오류를 반환하는 대신 결과를 반환하세요.
  </Tab>

  <Tab title="Java">
    Java SDK에서는 현재 도구 오류가 Claude에게 전송되기 전에 가로채는 기능을 지원하지 않습니다. 러너는 도구 함수에서 발생한 모든 예외를 포착하여 `is_error: true`가 포함된 도구 결과로 자동 변환합니다. 오류 내용을 제어하려면 함수 내부에서 예외를 포착하고 자체 콘텐츠를 반환하세요.
  </Tab>

  <Tab title="PHP">
    PHP 도구 러너는 현재 도구 결과가 추가되기 전에 이를 노출하지 않습니다. 도구의 `run` 클로저에서 던져진 예외는 잡혀서 자동으로 `is_error: true`인 도구 결과로 Claude에 전송됩니다. 오류 내용을 검사하거나 교체하려면 [도구 결과 수정하기](https://platform.claude.com/docs/ko/agents-and-tools/tool-use/tool-runner#modifying-tool-results)에 나온 수동 `pushMessages()` 패턴을 사용하세요.
  </Tab>

  <Tab title="Ruby">
    ```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

      # next_message가 반환될 시점에는 러너가 이번 턴의 도구를 이미 실행하고
      # 그 결과를 마지막(user 역할) 메시지로 추가한 상태입니다. 다음 요청이 이를
      # 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]
            # 옵션 1: 예외를 발생시켜 루프를 중지합니다
            raise "Tool failed: #{block[:content]}"

            # 옵션 2: 로그를 남기고 계속합니다(Claude가 처리하도록 함)
            # logger.error("Tool error: #{block[:content]}")
          end
        end
      end

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

    <Note>
      Ruby 도구 러너에는 도구 결과를 반환하는 훅이 없습니다. 

      `runner.next_message`

      가 반환되면 도구 결과는 

      `runner.params[:messages]`

      의 마지막 항목이 되므로, 다음 요청이 이를 Claude에게 보내기 전에 그곳에서 확인하세요.
    </Note>
  </Tab>
</Tabs>

### 도구 결과 수정하기

도구 결과가 Claude에 다시 전송되기 전에 수정할 수 있습니다. 이는 도구 결과에 [프롬프트 캐싱](https://platform.claude.com/docs/ko/build-with-claude/prompt-caching)을 활성화하기 위해 `cache_control`과 같은 메타데이터를 추가하거나, 도구 출력을 변환하는 데 유용합니다.

<Tabs exclude="shell">
  <Tab title="Python">
    ```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는 dict입니다: {"role": "user", "content": [...]}
            # 캐시 제어를 추가하도록 도구 결과를 수정합니다
            for block in tool_response["content"]:
                if block["type"] == "tool_result":
                    # 이 도구 결과를 캐시하도록 cache_control을 추가합니다
                    block["cache_control"] = {"type": "ephemeral"}

            # 수정된 응답을 추가합니다(원본이 자동으로 추가되지 않도록 합니다)
            runner.append_messages(message, tool_response)

        print(message.content)
    ```

    <Note>
      `runner.generate_tool_call_response()`

      를 호출하여 도구 결과를 가져와 수정한 다음, 러너가 원본을 추가하지 않도록 

      `runner.append_messages()`

      에 전달하세요.
    </Note>
  </Tab>

  <Tab title="TypeScript">
    ```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") {
        // 캐시 제어를 추가하도록 도구 결과를 수정합니다
        for (const block of toolResultMessage.content) {
          if (block.type === "tool_result") {
            // 이 도구 결과를 캐시하기 위해 cache_control을 추가합니다
            block.cache_control = { type: "ephemeral" };
          }
        }
        // pushMessages 호출은 필요하지 않습니다: 러너가 assistant
        // 메시지와 (이제 변경된) 캐시된 도구 응답을 모두 자동으로 추가합니다.
      }

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

    <Note>
      `runner.generateToolResponse()`

      를 호출하여 도구 결과를 가져온 다음 제자리에서 수정하세요. 러너가 수정된 결과를 대신 추가합니다.
    </Note>
  </Tab>

  <Tab title="C#">
    도구 결과가 추가되기 전에 수정하는 기능(예: `cache_control` 추가)은 현재 C# SDK에서 지원되지 않습니다. 러너는 `tool_result` 블록을 내부적으로 구성하며 이를 변경할 수 있는 훅을 제공하지 않습니다.
  </Tab>

  <Tab title="Go">
    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),
    				// 내부 콘텐츠 블록에 cache_control을 설정합니다. 외부
    				// tool_result 블록의 cache_control은 현재
    				// 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)
    ```
  </Tab>

  <Tab title="Java">
    도구 결과에 `cache_control`을 설정하려면 반환되는 `BetaToolResultBlockParam.Content`를 `ofString()` 대신 `ofBlocks()`로 구성하고 내부 텍스트 블록에 `cacheControl`을 설정하세요. 러너는 현재 외부 `tool_result` 블록에 `cache_control`을 설정하는 기능을 지원하지 않습니다.

    ```java
    @JsonClassDescription("Look up reference documentation for a topic")
    static class SearchDocuments {
        @JsonPropertyDescription("The search query")
        public String query;
    }

    BetaRunnableTool searchDocuments = BetaRunnableTool.of(SearchDocuments.class, input -> {
        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())));
    });
    ```
  </Tab>

  <Tab title="PHP">
    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),
                    // 이 도구 결과를 캐시하려면 cache_control을 추가하세요
                    'cache_control' => ['type' => 'ephemeral'],
                ];
            }
        }

        if ($toolResults !== []) {
            // pushMessages()는 상태를 변경됨으로 표시하므로 runner는
            // 자동 추가를 건너뜁니다. assistant 메시지와 도구 결과를 추가하세요.
            $runner->pushMessages(
                ['role' => 'assistant', 'content' => $message->content],
                ['role' => 'user', 'content' => $toolResults],
            );
        }
        // 도구 호출 없음: 루프가 종료되도록 상태를 그대로 둡니다.
    }
    ```
  </Tab>

  <Tab title="Ruby">
    ```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

      # messages 배열에서 가장 최근의 도구 결과에 접근합니다
      # runner가 도구 결과를 자동으로 추가하지만, 직접 수정할 수도 있습니다
      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
            # 도구 결과를 수정하여 캐시 제어를 추가합니다
            block[:cache_control] = {type: "ephemeral"}
          end
        end
      end

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

    <Note>
      Ruby 도구 러너에는 도구 결과를 반환하는 훅이 없습니다. 

      `runner.next_message`

      가 반환되면 도구 결과는 

      `runner.params[:messages]`

      의 마지막 항목이 되므로, 다음 요청이 이를 Claude에게 보내기 전에 그곳에서 수정하세요.
    </Note>
  </Tab>
</Tabs>

<Tip>
  도구 결과에 `cache_control`을 추가하는 것은 도구가 후속 API 호출을 위해 캐시하려는 대량의 데이터(예: 문서 검색 결과)를 반환할 때 특히 유용합니다. 캐싱 전략에 대한 자세한 내용은 [프롬프트 캐싱](https://platform.claude.com/docs/ko/build-with-claude/prompt-caching)을 참조하세요.
</Tip>

## 스트리밍

"Streaming"(스트리밍)을 활성화하여 각 턴의 응답을 점진적으로 처리하세요. 각 반복은 이벤트를 반복할 수 있는 스트림 객체를 산출합니다.

<Tabs exclude="shell">
  <Tab title="Python">
    `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,
    )

    # 스트리밍 시 runner는 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
    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
    });

    // 스트리밍 시 runner는 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
    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();
    }
    ```
  </Tab>

  <Tab title="Go">
    `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()
    		}
    	}
    }
    ```
  </Tab>

  <Tab title="Java">
    `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)
                        .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
    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
    ```
  </Tab>
</Tabs>

## 다음 단계

<CardGroup cols={2}>
  <Card title="엄격한 도구 사용" icon="check" href="https://platform.claude.com/docs/ko/agents-and-tools/tool-use/strict-tool-use">
    문법 제약 샘플링으로 Claude의 도구 입력에 JSON Schema 준수를 강제하세요.
  </Card>

  <Card title="도구 호출 처리하기" icon="arrows-left-right" href="https://platform.claude.com/docs/ko/agents-and-tools/tool-use/handle-tool-calls">
    `tool_use` 블록을 파싱하고, `tool_result` 응답을 포맷하며, `is_error`로 오류를 처리하세요.
  </Card>

  <Card title="병렬 도구 사용" icon="grid" href="https://platform.claude.com/docs/ko/agents-and-tools/tool-use/parallel-tool-use">
    메시지 기록 가이드 및 문제 해결과 함께 병렬 도구 호출을 활성화, 포맷, 비활성화하세요.
  </Card>

  <Card title="도구 정의하기" icon="hammer" href="https://platform.claude.com/docs/ko/agents-and-tools/tool-use/define-tools">
    도구 스키마를 지정하고, 효과적인 설명을 작성하며, Claude가 도구를 호출하는 시점을 제어하세요.
  </Card>
</CardGroup>
