---
title: Ejecutor de herramientas (SDK)
url: https://platform.claude.com/docs/es/agents-and-tools/tool-use/tool-runner
description: Usa el ejecutor de herramientas del SDK para manejar automáticamente el bucle agéntico, el encapsulamiento de errores y la seguridad de tipos.
---

El "tool runner" (ejecutor de herramientas) maneja el bucle agéntico, el encapsulamiento de errores y la seguridad de tipos para que tú no tengas que hacerlo. Cuando necesites aprobación humana en el bucle (human-in-the-loop), registro personalizado o ejecución condicional, usa el [bucle manual](https://platform.claude.com/docs/es/agents-and-tools/tool-use/handle-tool-calls) en su lugar.

En lugar de manejar manualmente las llamadas a herramientas, los resultados de herramientas y la gestión de la conversación, el ejecutor de herramientas automáticamente:

* Ejecuta las herramientas cuando Claude las llama
* Maneja el ciclo de solicitud/respuesta
* Gestiona el estado de la conversación
* Proporciona seguridad de tipos y validación

<Note>
  El ejecutor de herramientas está en beta y disponible en el [SDK de Python](https://github.com/anthropics/anthropic-sdk-python/blob/main/tools.md), el [SDK de TypeScript](https://github.com/anthropics/anthropic-sdk-typescript/blob/main/helpers.md#tool-helpers), el [SDK de C#](https://github.com/anthropics/anthropic-sdk-csharp/blob/main/examples/ToolRunnerExample/Program.cs), el [SDK de Go](https://github.com/anthropics/anthropic-sdk-go/blob/main/tools.md), el [SDK de Java](https://github.com/anthropics/anthropic-sdk-java/blob/main/anthropic-java-example/src/main/java/com/anthropic/example/BetaToolRunnerRunnableToolExample.java), el [SDK de PHP](https://github.com/anthropics/anthropic-sdk-php/blob/main/examples/beta/beta_tool_runner.php) y el [SDK de Ruby](https://github.com/anthropics/anthropic-sdk-ruby/blob/main/helpers.md#3-auto-looping-tool-runner-beta).
</Note>

## Uso básico

Define las herramientas usando los helpers del SDK y luego usa el ejecutor de herramientas para ejecutarlas.

Dependiendo de la firma de herramienta del SDK, una herramienta devuelve su resultado como una cadena o como bloques de contenido (bloques de texto, imagen o documento), por lo que una herramienta puede devolver resultados multimodales. Una cadena devuelta se convierte en un único bloque de contenido de texto. Para devolver datos estructurados, como un objeto JSON o un número, codifícalos primero como una cadena.

<Tabs exclude="shell">
  <Tab title="Python">
    Usa el decorador `@beta_tool` para definir herramientas con anotaciones de tipo y docstrings.

    <Note>
      Si estás usando el cliente asíncrono, reemplaza `@beta_tool` por `@beta_async_tool` y define la función con `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)
    ```

    El decorador `@beta_tool` inspecciona los argumentos de la función y el docstring para derivar el esquema JSON por ti.
  </Tab>

  <Tab title="TypeScript">
    Usa `betaZodTool()` para definiciones de herramientas con seguridad de tipos y validación con Zod, o `betaTool()` para definiciones basadas en JSON Schema.

    TypeScript ofrece dos enfoques para definir herramientas:

    **Usando Zod (recomendado)** - Usa `betaZodTool()` para definiciones de herramientas con seguridad de tipos y validación con Zod (requiere Zod 3.25.0 o superior):

    ```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);
      }
    }
    ```

    **Usando JSON Schema** - Usa `betaTool()` para definiciones de herramientas con seguridad de tipos sin Zod:

    <Note>
      La entrada generada por Claude no se valida en tiempo de ejecución. Realiza la validación dentro de la función `run` si es necesario.
    </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#">
    Define cada herramienta como un `BetaRunnableTool`, proporcionando una `Definition` con un esquema JSON y un delegado `Run` que se ejecuta cuando Claude llama a la herramienta.

    ```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">
    Define una herramienta con `toolrunner.NewBetaToolFromJSONSchema`. El tipo de entrada del handler es un struct con etiquetas `jsonschema:`. El SDK usa reflexión sobre él para generar el esquema 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)
    	}
    }
    ```

    Las etiquetas de struct `jsonschema:` generan el esquema de entrada. Por ejemplo, `CalculateSumInput` se convierte en:

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

  <Tab title="Java">
    Define cada herramienta como un `BetaRunnableTool` que empareja una clase de entrada con una función que se ejecuta cuando Claude llama a la herramienta. Anota la clase de entrada con `@JsonClassDescription` para la descripción de la herramienta, y cada campo público con `@JsonPropertyDescription` para las descripciones de los parámetros. El SDK deriva de la clase el esquema JSON, el nombre de la herramienta (el nombre de la clase en snake\_case) y el análisis de la entrada, y marca la herramienta con `strict: true` ([uso estricto de herramientas](https://platform.claude.com/docs/es/agents-and-tools/tool-use/strict-tool-use)).

    La función recibe la entrada analizada como una instancia de esa clase y devuelve un `BetaToolResultBlockParam.Content`. Para devolver texto, encapsúlalo con `BetaToolResultBlockParam.Content.ofString()`. Como la función es una lambda, puede usar objetos de tu aplicación, como el `WeatherService` del siguiente ejemplo.

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

    // Representa una clase que tu aplicación ya tiene,
    // como un cliente de base de datos o un wrapper de 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();

        // La lambda puede usar 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);
        }
    }
    ```

    El nombre de clase `CalculateSum` se convierte en el nombre de herramienta `calculate_sum`, y el SDK genera un esquema JSON a partir de los campos anotados:

    ```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">
    Define cada herramienta como un `BetaRunnableTool` que empareja la definición del esquema JSON de la herramienta con un closure que la ejecuta.

    ```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">
    Usa la clase `Anthropic::BaseTool` para definir herramientas con esquemas de entrada tipados.

    ```ruby
    require "anthropic"

    # Inicializa el cliente
    client = Anthropic::Client.new

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

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

      def call(input)
        # En una implementación completa, aquí llamarías a una API del clima
        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

    # Usa el ejecutor de herramientas
    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
    ```

    La clase `Anthropic::BaseTool` usa el método `doc` para la descripción de la herramienta e `input_schema` para definir los parámetros esperados. El SDK convierte esto automáticamente al formato de esquema JSON apropiado.
  </Tab>
</Tabs>

## Iterar sobre el ejecutor de herramientas

El ejecutor de herramientas es un iterable que produce mensajes de Claude. En cada iteración, el ejecutor verifica si Claude solicitó un uso de herramientas. Si es así, ejecuta la herramienta y envía el resultado de vuelta a Claude automáticamente, y luego produce el siguiente mensaje de Claude para continuar tu bucle.

Puedes terminar el bucle en cualquier iteración con una sentencia `break`. El ejecutor itera hasta que Claude devuelve un mensaje sin uso de herramientas, o hasta que alcanza `max_iterations` (csharp, java, php: `maxIterations`; go: `MaxIterations`) si lo configuraste.

Si no necesitas los mensajes intermedios, puedes obtener el mensaje final directamente:

<Tabs exclude="shell">
  <Tab title="Python">
    Usa `runner.until_done()` para obtener el mensaje final.

    ```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">
    Usa `await` sobre el ejecutor para obtener el mensaje final.

    ```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#">
    Usa `runner.RunUntilDoneAsync()` para obtener el mensaje final.

    ```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">
    Usa `runner.RunToCompletion(ctx)` para obtener el mensaje final.

    ```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">
    El SDK de Java no tiene un atajo `until_done()`. Itera hasta agotar el iterable y conserva el último mensaje.

    ```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">
    Usa `runUntilDone()` para obtener el mensaje final.

    ```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">
    Usa `runner.run_until_finished` para obtener todos los mensajes.

    ```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>

## Uso avanzado

Dentro del bucle, puedes leer cada mensaje de respuesta y modificar el estado del ejecutor antes de la siguiente llamada a la API. Cada iteración sigue este ciclo de vida:

1. El ejecutor envía una solicitud a la Messages API con su estado actual.

2. El ejecutor produce el mensaje de respuesta hacia el cuerpo de tu bucle.

3. Se ejecuta el cuerpo de tu bucle. Puedes leer el mensaje y, opcionalmente, modificar el estado del ejecutor.

4. Cuando el cuerpo de tu bucle retorna, el ejecutor verifica si modificaste su historial de mensajes.

   * **Si no modificaste el historial de mensajes:** Si el mensaje contiene llamadas a herramientas, el ejecutor agrega el mensaje del asistente y los resultados de las herramientas, y luego continúa. Si no hay llamadas a herramientas, el bucle termina.
   * **Si modificaste el historial de mensajes:** El ejecutor omite su agregado automático y usa tu estado sin cambios. Consulta [Tomar el control del historial de mensajes](https://platform.claude.com/docs/es/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
```

### Tomar el control del historial de mensajes

De forma predeterminada, el ejecutor gestiona el estado de la conversación por ti: después de cada turno con llamadas a herramientas, agrega el mensaje del asistente y cualquier resultado de herramienta a su propio historial de mensajes. Tomas el control del historial de mensajes cuando quieres reintentar un turno (descartar la respuesta y reenviar), inyectar un mensaje de seguimiento o construir el resultado de la herramienta tú mismo.

Tomas el control modificando los mensajes del ejecutor desde dentro del cuerpo del bucle. El método exacto depende del SDK. Consulta las pestañas por lenguaje que siguen.

Cuando tomas el control en una iteración, el ejecutor no agrega el mensaje del asistente ni los resultados de las herramientas de ese turno. Pasas a ser responsable de mantener la conversación válida: agrega tú mismo el mensaje del asistente y un resultado de herramienta (si quieres que el turno cuente), modifica el estado de forma condicional para que el bucle aún pueda terminar cuando no haya llamadas a herramientas, y pasa `max_iterations` (csharp, java, php: `maxIterations`; go: `MaxIterations`) para limitar el bucle. Los siete SDK admiten `max_iterations` (csharp, java, php: `maxIterations`; go: `MaxIterations`).

<Tabs exclude="shell">
  <Tab title="Python">
    Usa `generate_tool_call_response()` para inspeccionar o calcular el resultado de la herramienta. Llamar a `append_messages()` dentro del bucle le indica al ejecutor que estás gestionando el historial tú mismo, así que incluye el mensaje del asistente y el resultado de la herramienta en lo que agregues.

    ```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() marca el estado como modificado, así que el runner omite su
            # append automático para esta iteración. Agrega tú mismo el mensaje del asistente y
            # el tool result, más cualquier seguimiento.
            runner.append_messages(
                message,
                tool_response,
                {"role": "user", "content": "Please be concise."},
            )
        # Cuando no hay llamada a herramienta, deja el estado intacto para que el bucle termine.
    ```

    Para cambiar parámetros de la solicitud como `max_tokens` sin tomar el control del historial de mensajes, usa `set_messages_params()`. El ejecutor sigue agregando el mensaje del asistente y el resultado de la herramienta automáticamente.

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

  <Tab title="TypeScript">
    Usa `runner.params` para leer los parámetros actuales de la solicitud y `setMessagesParams()` para reemplazarlos. Llamar a `setMessagesParams()` o `pushMessages()` dentro del bucle le indica al ejecutor que estás gestionando el estado tú mismo: el mensaje del asistente y el resultado de la herramienta de esta iteración se descartan, y la siguiente solicitud se envía con tu estado.

    El siguiente ejemplo reintenta una respuesta truncada con un presupuesto de `max_tokens` mayor.

    ```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}.`);
        // Aumenta el presupuesto. setMessagesParams() marca el estado como modificado, así
        // que el runner NO agrega el mensaje truncado. La siguiente iteración reintenta
        // el mismo turno con el presupuesto mayor.
        runner.setMessagesParams((params) => ({ ...params, max_tokens: doubled }));
      }
      // Si no, deja el estado intacto para que el runner lo agregue automáticamente y siga.
    }
    ```
  </Tab>

  <Tab title="C#">
    Llamar a `SetParams()` o `PushMessages()` marca el estado como modificado, lo que hace que el ejecutor omita su agregado automático para ese turno. El ejecutor de C# aún ejecuta las herramientas correspondientes a ese turno y descarta sus resultados construidos automáticamente, por lo que una herramienta que también ejecutes tú mismo dentro del cuerpo del bucle se ejecuta dos veces a menos que lo tengas en cuenta. Cuando tomes el control, agrega tú mismo el mensaje del asistente y un resultado de herramienta. De lo contrario, la conversación no avanzará. El ejecutor de C# siempre termina cuando una respuesta no tiene llamadas a herramientas, así que condiciona cualquier mutación de estado a la presencia de un bloque `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)
        {
            // Sin llamada a herramienta: deja el estado intacto para que el bucle termine normalmente.
            continue;
        }

        // Ejecuta la herramienta tú mismo y construye el bloque de resultado.
        var toolResult = new BetaToolResultBlockParam(toolUseBlock.ID)
        {
            Content = await getWeatherTool.ExecuteAsync(toolUseBlock, default),
        };

        // PushMessages() marca el estado como modificado; el runner omite su auto-append.
        // Proporciona tú mismo el turno del asistente y el resultado de la herramienta, luego añade un seguimiento.
        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">
    El ejecutor de Go expone los parámetros como un campo público `Params`. Modificar `runner.Params` entre llamadas a `NextMessage(ctx)` se aplica a la siguiente solicitud a la API. A diferencia de otros SDK, el ejecutor de Go siempre agrega el mensaje del asistente y los resultados de herramientas de forma incondicional. Modificar `Params` no suprime ese paso.

    ```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
    	}

    	// El runner de Go siempre añade el mensaje del asistente y los resultados de herramientas.
    	// Los cambios de parámetros aquí se aplican a la siguiente iteración.
    	runner.Params.MaxTokens = 2048
    }
    ```
  </Tab>

  <Tab title="Java">
    Usa `runner.params()` para leer los parámetros actuales y `runner.setNextParams()` para reemplazarlos en la siguiente iteración. Cuando llamas a `setNextParams()` dentro del bucle, el ejecutor omite su agregado automático. El mensaje recién producido se descarta, y la siguiente iteración envía tus nuevos parámetros sin cambios.

    El siguiente ejemplo reintenta un turno que alcanzó el límite de tokens duplicando `max_tokens`. Mutar solo en la rama de `max_tokens` mantiene el bucle convergente: los turnos que se completan normalmente siguen su curso, y el ejecutor agrega automáticamente y termina cuando no hay más llamadas a herramientas.

    ```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 + ".");

            // Llamar a setNextParams() marca este turno como gestionado por el usuario: el
            // runner NO añade automáticamente el mensaje truncado, así que la siguiente
            // iteración reenvía el mismo prefijo de conversación con el presupuesto mayor.
            runner.setNextParams(runner.params().toBuilder().maxTokens(doubled).build());
        }
        // Sin mutación en un turno normal: el runner añade automáticamente y continúa.
    }
    ```
  </Tab>

  <Tab title="PHP">
    Usa `setMessagesParams()` y `pushMessages()` para modificar el estado del ejecutor, y `getParams()` para leerlo. Llamar a cualquiera de los dos setters dentro del bucle le indica al ejecutor que omita su agregado automático, de modo que la conversación continúa desde tu estado modificado.

    El siguiente ejemplo duplica `max_tokens` y reintenta cuando una respuesta queda cortada.

    ```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";

            // Llamar a setMessagesParams() dentro del bucle indica al runner que omita
            // su anexado automático. El mensaje truncado se descarta; la siguiente
            // iteración reintenta con el presupuesto mayor.
            // Las claves están en camelCase, coincidiendo con los parámetros nombrados de toolRunner().
            $runner->setMessagesParams(['maxTokens' => $doubled]);
        }
    }
    ```
  </Tab>

  <Tab title="Ruby">
    Usa `next_message` para un control paso a paso. Para cuando `next_message` retorna, el mensaje del asistente y el resultado de la herramienta de ese turno ya están agregados. Usa `feed_messages` para inyectar mensajes de seguimiento entre turnos, y `runner.params.update(...)` para cambiar los parámetros de la solicitud en el lugar.

    Tomas el control del historial de mensajes cuando, desde dentro de un bloque `each_message` o `each_streaming`, reasignas `runner.params[:messages]` o llamas a `feed_messages`. El siguiente patrón llama a `feed_messages` entre llamadas a `next_message`, lo cual no toma el control.

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

    # Avanza el runner un paso. El mensaje del asistente y el resultado de la herramienta se añaden
    # a runner.params[:messages] antes de que next_message retorne.
    message = runner.next_message
    puts message.content

    # Inyecta un seguimiento antes de continuar. feed_messages recibe un splat, no un arreglo.
    runner.feed_messages({role: "user", content: "Also check Boston."})

    # Cambia los parámetros in situ. Reasignar runner.params[:messages] toma el control del
    # historial de mensajes solo cuando ocurre dentro de un bloque each_message o each_streaming.
    runner.params.update(max_tokens: 2048)

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

### Gestión automática del contexto

Para tareas agénticas de larga duración, los ejecutores de herramientas de TypeScript y Ruby admiten la ["compaction" (compactación)](https://platform.claude.com/docs/es/build-with-claude/context-editing#client-side-compaction-sdk) automática, que genera resúmenes cuando el uso de tokens supera un umbral para que la conversación pueda continuar más allá de los límites de la "context window" (ventana de contexto). Ambos SDK han dejado obsoleta esta opción del lado del cliente en favor de la [compactación del lado del servidor](https://platform.claude.com/docs/es/build-with-claude/compaction-threshold), que funciona con el ejecutor de herramientas de cualquier SDK mediante el parámetro de solicitud `context_management`. El SDK de Python (v1.0 y posteriores) y los ejecutores de herramientas de Go, Java, C# y PHP no incluyen compactación del lado del cliente. El ejecutor de herramientas tiene un helper `compact_before_next_turn()` (typescript, java, php: `compactBeforeNextTurn()`; csharp, go: `CompactBeforeNextTurn()`) para la compactación bajo demanda. Consulta [Compactar en un bucle](https://platform.claude.com/docs/es/build-with-claude/compaction-on-demand#compact-in-a-loop). Usa este helper o una edición de compactación de `context_management` en un ejecutor, no ambos.

### Depurar la ejecución de herramientas

Cuando una herramienta lanza una excepción, el ejecutor de herramientas la captura y devuelve el error a Claude como un resultado de herramienta con `is_error: true`. El resultado de herramienta contiene el mensaje de la excepción (en Python, su tipo y mensaje), no el stack trace completo.

Lo que el SDK registra depende del lenguaje. El SDK de Python registra la excepción completa, incluido su stack trace, a través del módulo estándar `logging` cada vez que una herramienta lanza una excepción no manejada. Los SDK de Python, TypeScript y Java leen la variable de entorno `ANTHROPIC_LOG` para activar el registro del SDK, que incluye detalles de solicitudes y respuestas:

```bash
# Registrar en nivel info
export ANTHROPIC_LOG=info

# Registrar en nivel debug para una salida más detallada
export ANTHROPIC_LOG=debug
```

Los SDK de Go, Ruby, C# y PHP no leen `ANTHROPIC_LOG`. Fuera de Python, ningún SDK registra una herramienta fallida: para ver por qué falló una herramienta, captura y registra la excepción dentro de la función de la herramienta antes de retornar o relanzarla.

### Interceptar errores de herramientas

De forma predeterminada, los errores de herramientas se devuelven a Claude, que puede entonces responder de manera apropiada. Sin embargo, es posible que quieras detectar errores y manejarlos de forma diferente, por ejemplo, para detener la ejecución anticipadamente o implementar un manejo de errores personalizado.

<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 es un dict: {"role": "user", "content": [...]}
            # Comprueba si algún resultado de herramienta tiene un error
            for block in tool_response["content"]:
                if block.get("is_error"):
                    # Opción 1: Lanza una excepción para detener el bucle
                    raise RuntimeError(f"Tool failed: {json.dumps(block['content'])}")

                    # Opción 2: Registra y continúa (deja que Claude lo maneje)
                    # logger.error(f"Tool error: {json.dumps(block['content'])}")

        # Procesa el mensaje normalmente
        print(message.content)
    ```

    <Note>
      Llama a 

      `runner.generate_tool_call_response()`

       en el bucle para obtener los resultados de las herramientas y comprobar si contienen errores antes de que el ejecutor los envíe a 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") {
        // Comprueba si algún resultado de herramienta tiene un error
        for (const block of toolResultMessage.content) {
          if (block.type === "tool_result" && block.is_error) {
            // Opción 1: Lanza una excepción para detener el bucle
            throw new Error(`Tool failed: ${JSON.stringify(block.content)}`);

            // Opción 2: Registra y continúa (deja que Claude lo maneje)
            // console.error(`Tool error: ${JSON.stringify(block.content)}`);
          }
        }
      }

      // Procesa el mensaje normalmente
      console.log(message.content);
    }
    ```

    <Note>
      Llama a 

      `runner.generateToolResponse()`

       en el bucle para obtener los resultados de las herramientas y comprobar si contienen errores antes de que el ejecutor los envíe a Claude.
    </Note>
  </Tab>

  <Tab title="C#">
    El ejecutor de herramientas de C# no expone un hook para inspeccionar el resultado de la herramienta antes de que se envíe a Claude. Para controlar el contenido del error, lanza `BetaToolError` desde dentro del cuerpo de la herramienta. El ejecutor lo convierte en un `tool_result` con `is_error: true` y el contenido que proporciones.

    ```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)
            {
                // Registra aquí si necesitas inspeccionar el fallo antes de que Claude lo vea.
                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">
    Interceptar errores de herramientas antes de que se envíen a Claude no está admitido actualmente en el SDK de Go. El ejecutor convierte internamente un error devuelto por tu handler en un resultado de herramienta con `is_error: true`. Para personalizar el contenido del error, captura el error dentro de tu handler y devuelve un resultado en lugar de devolver el error.
  </Tab>

  <Tab title="Java">
    Actualmente, el SDK de Java no admite interceptar errores de herramientas antes de que se envíen a Claude. El ejecutor captura cualquier excepción lanzada desde la función de una herramienta y la convierte automáticamente en un resultado de herramienta con `is_error: true`. Para controlar el contenido del error, captura la excepción dentro de la función y devuelve tu propio contenido.
  </Tab>

  <Tab title="PHP">
    El ejecutor de herramientas de PHP no expone actualmente los resultados de herramientas antes de que se agreguen. Las excepciones lanzadas desde el closure `run` de una herramienta se capturan y se envían a Claude automáticamente como resultados de herramienta con `is_error: true`. Para inspeccionar o reemplazar el contenido del error, usa el patrón manual de `pushMessages()` que se muestra en [Modificar resultados de herramientas](https://platform.claude.com/docs/es/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

      # Cuando next_message retorna, el runner ya ejecutó las herramientas de este turno y
      # agregó sus resultados como el último mensaje (con rol user). Inspecciónalos aquí,
      # antes de que la siguiente solicitud se los envíe a 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]
            # Opción 1: Lanza una excepción para detener el bucle
            raise "Tool failed: #{block[:content]}"

            # Opción 2: Registra el error y continúa (deja que Claude lo maneje)
            # logger.error("Tool error: #{block[:content]}")
          end
        end
      end

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

    <Note>
      El ejecutor de herramientas de Ruby no tiene un hook que devuelva los resultados de las herramientas. Una vez que 

      `runner.next_message`

       retorna, estos son la última entrada de 

      `runner.params[:messages]`

      , así que compruébalos ahí antes de que la siguiente solicitud los envíe a Claude.
    </Note>
  </Tab>
</Tabs>

### Modificar resultados de herramientas

Puedes modificar los resultados de herramientas antes de que se envíen de vuelta a Claude. Esto es útil para agregar metadatos como `cache_control` para habilitar el "prompt caching" (almacenamiento en caché de prompts) en los resultados de herramientas, o para transformar la salida de la herramienta. Consulta [almacenamiento en caché de prompts](https://platform.claude.com/docs/es/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 es un dict: {"role": "user", "content": [...]}
            # Modifica el resultado de la herramienta para agregar control de caché
            for block in tool_response["content"]:
                if block["type"] == "tool_result":
                    # Agrega cache_control para almacenar en caché este resultado de la herramienta
                    block["cache_control"] = {"type": "ephemeral"}

            # Añade la respuesta modificada (esto evita que se añada automáticamente la original)
            runner.append_messages(message, tool_response)

        print(message.content)
    ```

    <Note>
      Llama a 

      `runner.generate_tool_call_response()`

       para obtener el resultado de la herramienta, modifícalo y pásalo a 

      `runner.append_messages()`

       para que el ejecutor no agregue el original.
    </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") {
        // Modifica el resultado de la herramienta para agregar control de caché
        for (const block of toolResultMessage.content) {
          if (block.type === "tool_result") {
            // Agrega cache_control para almacenar en caché este resultado de herramienta
            block.cache_control = { type: "ephemeral" };
          }
        }
        // No hace falta llamar a pushMessages: el runner agrega automáticamente tanto el mensaje
        // del asistente como la respuesta de herramienta en caché (ya modificada).
      }

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

    <Note>
      Llama a 

      `runner.generateToolResponse()`

       para obtener el resultado de la herramienta y luego modifícalo en el lugar. El ejecutor agrega el resultado modificado por ti.
    </Note>
  </Tab>

  <Tab title="C#">
    Modificar los resultados de herramientas antes de que se agreguen (por ejemplo, para añadir `cache_control`) no está admitido actualmente en el SDK de C#. El ejecutor construye el bloque `tool_result` internamente y no proporciona ningún hook para alterarlo.
  </Tab>

  <Tab title="Go">
    El ejecutor de Go no expone un hook para modificar el bloque `tool_result` externo. Sin embargo, puedes establecer `cache_control` en los bloques de contenido internos que devuelve tu handler.

    ```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),
    				// Establece cache_control en el bloque de contenido interno. El cache_control
    				// del bloque tool_result externo no se puede establecer actualmente
    				// mediante el runner de Go.
    				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">
    Para establecer `cache_control` en un resultado de herramienta, construye el `BetaToolResultBlockParam.Content` devuelto con `ofBlocks()` en lugar de `ofString()` y establece `cacheControl` en el bloque de texto interno. Actualmente, el ejecutor no admite establecer `cache_control` en el bloque `tool_result` externo.

    ```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">
    El ejecutor de herramientas de PHP no tiene un callback para mutar el bloque `tool_result` generado automáticamente. Para agregar campos como `cache_control`, construye el resultado de la herramienta tú mismo y agrégalo con push. Llamar a `pushMessages()` omite el agregado automático del ejecutor para ese turno.

    ```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),
                    // Agrega cache_control para almacenar en caché este resultado de herramienta
                    'cache_control' => ['type' => 'ephemeral'],
                ];
            }
        }

        if ($toolResults !== []) {
            // pushMessages() marca el estado como modificado, así que el runner omite su
            // anexado automático. Agrega el mensaje del asistente y los resultados de herramientas.
            $runner->pushMessages(
                ['role' => 'assistant', 'content' => $message->content],
                ['role' => 'user', 'content' => $toolResults],
            );
        }
        // Sin llamada a herramienta: deja el estado intacto para que el bucle termine.
    }
    ```
  </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

      # Accede a los resultados de herramientas más recientes del array de mensajes
      # El runner agrega automáticamente los resultados de herramientas, pero puedes modificarlos
      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
            # Modifica el resultado de la herramienta para agregar control de caché
            block[:cache_control] = {type: "ephemeral"}
          end
        end
      end

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

    <Note>
      El ejecutor de herramientas de Ruby no tiene un hook que devuelva los resultados de las herramientas. Una vez que 

      `runner.next_message`

       retorna, estos son la última entrada de 

      `runner.params[:messages]`

      , así que modifícalos ahí antes de que la siguiente solicitud los envíe a Claude.
    </Note>
  </Tab>
</Tabs>

<Tip>
  Agregar `cache_control` a los resultados de herramientas es particularmente útil cuando las herramientas devuelven grandes cantidades de datos (como resultados de búsqueda de documentos) que quieres almacenar en caché para llamadas posteriores a la API. Consulta [Almacenamiento en caché de prompts](https://platform.claude.com/docs/es/build-with-claude/prompt-caching) para más detalles sobre estrategias de caché.
</Tip>

## Streaming

Habilita el streaming para procesar la respuesta de cada turno de forma incremental. Cada iteración produce un objeto stream sobre el que puedes iterar para obtener eventos.

<Tabs exclude="shell">
  <Tab title="Python">
    Establece `stream=True` y usa `get_final_message()` para obtener el mensaje acumulado.

    ```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,
    )

    # Al usar streaming, el runner devuelve 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">
    Establece `stream: true` y usa `finalMessage()` para obtener el mensaje acumulado.

    ```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
    });

    // Al usar streaming, el runner devuelve 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#">
    Llama a `runner.Streaming()` para obtener una secuencia asíncrona anidada: un stream interno por cada llamada a la 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">
    Usa `NewToolRunnerStreaming` e itera sobre `runner.AllStreaming(ctx)`. Cada iteración externa produce un stream de eventos para una llamada a la 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">
    Llama a `runner.streaming()` para obtener un stream por cada turno. Cada `StreamResponse` debe cerrarse después de usarse.

    ```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">
    El streaming no está disponible actualmente con el ejecutor de herramientas de PHP.
  </Tab>

  <Tab title="Ruby">
    Usa `each_streaming` para iterar sobre los eventos de 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>

## Próximos pasos

<CardGroup cols={2}>
  <Card title="Uso estricto de herramientas" icon="check" href="https://platform.claude.com/docs/es/agents-and-tools/tool-use/strict-tool-use">
    Impón el cumplimiento de JSON Schema en las entradas de herramientas de Claude con muestreo restringido por gramática.
  </Card>

  <Card title="Manejar llamadas a herramientas" icon="arrows-left-right" href="https://platform.claude.com/docs/es/agents-and-tools/tool-use/handle-tool-calls">
    Analiza bloques `tool_use`, da formato a respuestas `tool_result` y maneja errores con `is_error`.
  </Card>

  <Card title="Uso de herramientas en paralelo" icon="grid" href="https://platform.claude.com/docs/es/agents-and-tools/tool-use/parallel-tool-use">
    Habilita, da formato y deshabilita las llamadas a herramientas en paralelo, con orientación sobre el historial de mensajes y solución de problemas.
  </Card>

  <Card title="Definir herramientas" icon="hammer" href="https://platform.claude.com/docs/es/agents-and-tools/tool-use/define-tools">
    Especifica esquemas de herramientas, escribe descripciones efectivas y controla cuándo Claude llama a tus herramientas.
  </Card>
</CardGroup>
