---
title: Исполнитель инструментов (SDK)
url: https://platform.claude.com/docs/ru/agents-and-tools/tool-use/tool-runner
description: Используйте исполнитель инструментов SDK для автоматической обработки агентного цикла, обёртывания ошибок и обеспечения типобезопасности.
---

«Tool runner» (исполнитель инструментов) берёт на себя агентный цикл, обёртывание ошибок и типобезопасность, чтобы вам не приходилось делать это самостоятельно. Если вам нужно одобрение с участием человека (human-in-the-loop), пользовательское логирование или условное выполнение, используйте вместо него [ручной цикл](https://platform.claude.com/docs/ru/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` для определения инструментов с аннотациями типов и строками документации (docstrings).

    <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">
    Используйте `betaZodTool()` для типобезопасных определений инструментов с валидацией Zod или `betaTool()` для определений на основе JSON Schema.

    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** — используйте `betaTool()` для типобезопасных определений инструментов без Zod:

    <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`, предоставив `Definition` с JSON-схемой и делегат `Run`, который выполняется, когда Claude вызывает инструмент.

    ```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">
    Определите каждый инструмент как `BetaRunnableTool`, который связывает класс входных данных с функцией, выполняемой, когда Claude вызывает инструмент. Аннотируйте класс входных данных с помощью `@JsonClassDescription` для описания инструмента, а каждое публичное поле — с помощью `@JsonPropertyDescription` для описаний параметров. SDK выводит из класса JSON-схему, имя инструмента (имя класса в snake\_case) и разбор входных данных, а также помечает инструмент как `strict: true` ([строгое использование инструментов](https://platform.claude.com/docs/ru/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">
    Определите каждый инструмент как `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;
            }
        }
    }
    ```
  </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

    # Использование средства запуска инструментов
    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. На каждой итерации исполнитель проверяет, запросил ли 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/ru/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`), чтобы ограничить цикл. Все семь 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() помечает состояние как изменённое, поэтому runner пропускает
            # автоматическое добавление на этой итерации. Добавьте сообщение ассистента и
            # результат инструмента самостоятельно, а также любые последующие сообщения.
            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 НЕ добавляет усечённое сообщение. Следующая итерация повторяет
        // тот же ход с увеличенным бюджетом.
        runner.setMessagesParams((params) => ({ ...params, max_tokens: doubled }));
      }
      // Иначе оставляем состояние нетронутым, чтобы runner автоматически добавил сообщение и продолжил.
    }
    ```
  </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 пропускает автодобавление.
        // Передайте ход ассистента и результат инструмента сами, затем добавьте продолжение.
        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 предоставляет параметры в виде публичного поля `Params`. Изменение `runner.Params` между вызовами `NextMessage(ctx)` применяется к следующему запросу 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 НЕ добавляет усечённое сообщение автоматически, и следующая итерация
            // повторно отправляет тот же префикс диалога с увеличенным бюджетом.
            runner.setNextParams(runner.params().toBuilder().maxTokens(doubled).build());
        }
        // Без изменений на обычном ходе: runner автоматически добавляет и продолжает.
    }
    ```
  </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`. Следующий шаблон вызывает `feed_messages` между вызовами `next_message`, что не является взятием управления на себя.

    ```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?"}]
    )

    # Выполнить один шаг runner. Сообщение ассистента и результат инструмента добавляются
    # в runner.params[:messages] до того, как next_message вернёт управление.
    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/ru/build-with-claude/context-editing#client-side-compaction-sdk), которое генерирует сводки, когда использование токенов превышает пороговое значение, чтобы диалог мог продолжаться за пределами ограничений контекстного окна. Оба SDK объявили эту клиентскую опцию устаревшей в пользу [серверного сжатия](https://platform.claude.com/docs/ru/build-with-claude/compaction-threshold), которое работает с исполнителем инструментов любого SDK через параметр запроса `context_management`. Python SDK (v1.0 и новее), а также исполнители инструментов Go, Java, C# и PHP не включают клиентское сжатие. У исполнителя инструментов есть вспомогательный метод `compact_before_next_turn()` (typescript, java, php: `compactBeforeNextTurn()`; csharp, go: `CompactBeforeNextTurn()`) для сжатия по требованию. См. [Сжатие в цикле](https://platform.claude.com/docs/ru/build-with-claude/compaction-on-demand#compact-in-a-loop). Используйте для исполнителя либо его, либо правку сжатия `context_management`, но не оба варианта одновременно.

### Отладка выполнения инструментов

Когда инструмент выбрасывает исключение, исполнитель инструментов перехватывает его и возвращает ошибку Claude в виде результата инструмента с `is_error: true`. Результат инструмента содержит сообщение исключения (в Python — его тип и сообщение), а не полную трассировку стека.

То, что логирует SDK, зависит от языка. Python SDK логирует полное исключение, включая трассировку стека, через стандартный модуль `logging` всякий раз, когда инструмент выбрасывает необработанное исключение. SDK для Python, TypeScript и Java читают переменную окружения `ANTHROPIC_LOG` для включения логирования SDK, которое включает детали запросов и ответов:

```bash
# Логирование на уровне info
export ANTHROPIC_LOG=info

# Логирование на уровне debug для более подробного вывода
export ANTHROPIC_LOG=debug
```

SDK для Go, Ruby, C# и PHP не читают `ANTHROPIC_LOG`. За пределами Python ни один SDK не логирует сбой инструмента: чтобы увидеть, почему инструмент завершился с ошибкой, перехватите и залогируйте исключение внутри функции инструмента перед возвратом или повторным выбрасыванием.

### Перехват ошибок инструментов

По умолчанию ошибки инструментов передаются обратно 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 — это словарь: {"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` изнутри тела инструмента. Исполнитель преобразует его в `tool_result` с `is_error: true` и указанным вами содержимым.

    ```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">
    Перехват ошибок инструментов до их отправки Claude в настоящее время не поддерживается в Java SDK. Исполнитель перехватывает любое исключение, выброшенное функцией инструмента, и автоматически преобразует его в результат инструмента с `is_error: true`. Чтобы управлять содержимым ошибки, перехватите исключение внутри функции и верните собственное содержимое.
  </Tab>

  <Tab title="PHP">
    Исполнитель инструментов PHP в настоящее время не предоставляет доступ к результатам инструментов до их добавления. Исключения, выброшенные из замыкания `run` инструмента, перехватываются и автоматически отправляются Claude как результаты инструментов с `is_error: true`. Чтобы проверить или заменить содержимое ошибки, используйте ручной шаблон `pushMessages()`, показанный в разделе [Изменение результатов инструментов](https://platform.claude.com/docs/ru/agents-and-tools/tool-use/tool-runner#modifying-tool-results).
  </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 runner уже выполнил инструменты этого хода и
      # добавил их результаты последним сообщением (с ролью 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. Это полезно для добавления метаданных, таких как `cache_control`, чтобы включить «prompt caching» ([кэширование подсказок](https://platform.claude.com/docs/ru/build-with-claude/prompt-caching)) для результатов инструментов, или для преобразования вывода инструмента.

<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 — это словарь: {"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 не нужен: runner автоматически добавляет и сообщение
        // ассистента, и (уже изменённый) закэшированный ответ инструмента.
      }

      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` с помощью `ofBlocks()` вместо `ofString()` и задайте `cacheControl` для внутреннего текстового блока. Исполнитель в настоящее время не поддерживает установку `cache_control` для внешнего блока `tool_result`.

    ```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 пропускает
            // автоматическое добавление. Добавьте сообщение ассистента и результаты инструментов.
            $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/ru/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/ru/agents-and-tools/tool-use/strict-tool-use">
    Обеспечьте соответствие входных данных инструментов Claude схеме JSON Schema с помощью сэмплирования с грамматическими ограничениями.
  </Card>

  <Card title="Обработка вызовов инструментов" icon="arrows-left-right" href="https://platform.claude.com/docs/ru/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/ru/agents-and-tools/tool-use/parallel-tool-use">
    Включайте, форматируйте и отключайте параллельные вызовы инструментов, с рекомендациями по истории сообщений и устранению неполадок.
  </Card>

  <Card title="Определение инструментов" icon="hammer" href="https://platform.claude.com/docs/ru/agents-and-tools/tool-use/define-tools">
    Задавайте схемы инструментов, пишите эффективные описания и управляйте тем, когда Claude вызывает ваши инструменты.
  </Card>
</CardGroup>
