---
title: Exécuteur d'outils (SDK)
url: https://platform.claude.com/docs/fr/agents-and-tools/tool-use/tool-runner
description: Utilisez l'exécuteur d'outils du SDK pour gérer automatiquement la boucle agentique, l'encapsulation des erreurs et la sûreté de typage.
---

Le « tool runner » (exécuteur d'outils) gère la boucle agentique, l'encapsulation des erreurs et la sûreté de typage pour que vous n'ayez pas à le faire. Lorsque vous avez besoin d'une approbation humaine dans la boucle, d'une journalisation personnalisée ou d'une exécution conditionnelle, utilisez plutôt la [boucle manuelle](https://platform.claude.com/docs/fr/agents-and-tools/tool-use/handle-tool-calls).

Au lieu de gérer manuellement les appels d'outils, les résultats d'outils et la gestion de la conversation, l'exécuteur d'outils effectue automatiquement les opérations suivantes :

* Exécute les outils lorsque Claude les appelle
* Gère le cycle requête/réponse
* Gère l'état de la conversation
* Fournit la sûreté de typage et la validation

<Note>
  L'exécuteur d'outils est en bêta et disponible dans le [SDK Python](https://github.com/anthropics/anthropic-sdk-python/blob/main/tools.md), le [SDK TypeScript](https://github.com/anthropics/anthropic-sdk-typescript/blob/main/helpers.md#tool-helpers), le [SDK C#](https://github.com/anthropics/anthropic-sdk-csharp/blob/main/examples/ToolRunnerExample/Program.cs), le [SDK Go](https://github.com/anthropics/anthropic-sdk-go/blob/main/tools.md), le [SDK Java](https://github.com/anthropics/anthropic-sdk-java/blob/main/anthropic-java-example/src/main/java/com/anthropic/example/BetaToolRunnerRunnableToolExample.java), le [SDK PHP](https://github.com/anthropics/anthropic-sdk-php/blob/main/examples/beta/beta_tool_runner.php) et le [SDK Ruby](https://github.com/anthropics/anthropic-sdk-ruby/blob/main/helpers.md#3-auto-looping-tool-runner-beta).
</Note>

## Utilisation de base

Définissez les outils à l'aide des helpers du SDK, puis utilisez l'exécuteur d'outils pour les exécuter.

Selon la signature d'outil du SDK, un outil renvoie son résultat sous forme de chaîne de caractères ou de blocs de contenu (blocs de texte, d'image ou de document), de sorte qu'un outil peut renvoyer des résultats multimodaux. Une chaîne renvoyée devient un unique bloc de contenu texte. Pour renvoyer des données structurées, telles qu'un objet JSON ou un nombre, encodez-les d'abord sous forme de chaîne.

<Tabs exclude="shell">
  <Tab title="Python">
    Utilisez le décorateur `@beta_tool` pour définir des outils avec des annotations de type et des docstrings.

    <Note>
      Si vous utilisez le client asynchrone, remplacez `@beta_tool` par `@beta_async_tool` et définissez la fonction avec `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)
    ```

    Le décorateur `@beta_tool` inspecte les arguments de la fonction et la docstring pour en déduire le schéma JSON à votre place.
  </Tab>

  <Tab title="TypeScript">
    Utilisez `betaZodTool()` pour des définitions d'outils typées avec validation Zod, ou `betaTool()` pour des définitions basées sur JSON Schema.

    TypeScript propose deux approches pour définir des outils :

    **Avec Zod (recommandé)** - Utilisez `betaZodTool()` pour des définitions d'outils typées avec validation Zod (nécessite Zod 3.25.0 ou supérieur) :

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

    **Avec JSON Schema** - Utilisez `betaTool()` pour des définitions d'outils typées sans Zod :

    <Note>
      L'entrée générée par Claude n'est pas validée à l'exécution. Effectuez la validation dans la fonction `run` si nécessaire.
    </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#">
    Définissez chaque outil comme un `BetaRunnableTool`, en fournissant une `Definition` avec un schéma JSON et un délégué `Run` qui s'exécute lorsque Claude appelle l'outil.

    ```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">
    Définissez un outil avec `toolrunner.NewBetaToolFromJSONSchema`. Le type d'entrée du handler est une struct avec des tags `jsonschema:`. Le SDK utilise la réflexion sur celle-ci pour générer le schéma 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)
    	}
    }
    ```

    Les tags de struct `jsonschema:` génèrent le schéma d'entrée. Par exemple, `CalculateSumInput` devient :

    ```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">
    Définissez chaque outil comme un `BetaRunnableTool` qui associe une classe d'entrée à une fonction qui s'exécute lorsque Claude appelle l'outil. Annotez la classe d'entrée avec `@JsonClassDescription` pour la description de l'outil, et chaque champ public avec `@JsonPropertyDescription` pour les descriptions des paramètres. Le SDK déduit de la classe le schéma JSON, le nom de l'outil (nom de classe en snake\_case) et l'analyse de l'entrée, et marque l'outil avec `strict: true` ([utilisation d'outils stricte](https://platform.claude.com/docs/fr/agents-and-tools/tool-use/strict-tool-use)).

    La fonction reçoit l'entrée analysée sous forme d'instance de cette classe et renvoie un `BetaToolResultBlockParam.Content`. Pour renvoyer du texte, encapsulez-le avec `BetaToolResultBlockParam.Content.ofString()`. Comme la fonction est une lambda, elle peut utiliser des objets de votre application, comme le `WeatherService` de l'exemple suivant.

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

    // Remplace une classe que votre application possède déjà,
    // comme un client de base de données ou un wrapper d'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 peut utiliser 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);
        }
    }
    ```

    Le nom de classe `CalculateSum` devient le nom d'outil `calculate_sum`, et le SDK génère un schéma JSON à partir des champs annotés :

    ```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">
    Définissez chaque outil comme un `BetaRunnableTool` qui associe la définition du schéma JSON de l'outil à une closure qui l'exécute.

    ```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">
    Utilisez la classe `Anthropic::BaseTool` pour définir des outils avec des schémas d'entrée typés.

    ```ruby
    require "anthropic"

    # Initialiser le client
    client = Anthropic::Client.new

    # Définir le schéma d'entrée
    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

    # Définir l'outil
    class GetWeather < Anthropic::BaseTool
      doc "Get the current weather in a given location"
      input_schema GetWeatherInput

      def call(input)
        # Dans une implémentation complète, vous appelleriez ici une API météo.
        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

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

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

    La classe `Anthropic::BaseTool` utilise la méthode `doc` pour la description de l'outil et `input_schema` pour définir les paramètres attendus. Le SDK convertit automatiquement cela au format de schéma JSON approprié.
  </Tab>
</Tabs>

## Itérer sur l'exécuteur d'outils

L'exécuteur d'outils est un itérable qui produit les messages de Claude. À chaque itération, l'exécuteur vérifie si Claude a demandé une utilisation d'outils. Si c'est le cas, il exécute l'outil et renvoie automatiquement le résultat à Claude, puis produit le message suivant de Claude pour poursuivre votre boucle.

Vous pouvez terminer la boucle à n'importe quelle itération avec une instruction `break`. L'exécuteur boucle jusqu'à ce que Claude renvoie un message sans utilisation d'outil, ou jusqu'à ce qu'il atteigne `max_iterations` (csharp, java, php: `maxIterations`; go: `MaxIterations`) si vous l'avez défini.

Si vous n'avez pas besoin des messages intermédiaires, vous pouvez obtenir directement le message final :

<Tabs exclude="shell">
  <Tab title="Python">
    Utilisez `runner.until_done()` pour obtenir le message 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">
    Appliquez `await` à l'exécuteur pour obtenir le message 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#">
    Utilisez `runner.RunUntilDoneAsync()` pour obtenir le message 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">
    Utilisez `runner.RunToCompletion(ctx)` pour obtenir le message 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">
    Le SDK Java ne dispose pas de raccourci `until_done()`. Itérez jusqu'à épuisement et conservez le dernier message.

    ```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">
    Utilisez `runUntilDone()` pour obtenir le message 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">
    Utilisez `runner.run_until_finished` pour obtenir tous les messages.

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

## Utilisation avancée

Dans la boucle, vous pouvez lire chaque message de réponse et modifier l'état de l'exécuteur avant le prochain appel API. Chaque itération suit ce cycle de vie :

1. L'exécuteur envoie une requête à l'API Messages avec son état actuel.

2. L'exécuteur produit le message de réponse pour le corps de votre boucle.

3. Le corps de votre boucle s'exécute. Vous pouvez lire le message et éventuellement modifier l'état de l'exécuteur.

4. Lorsque le corps de votre boucle se termine, l'exécuteur vérifie si vous avez modifié son historique de messages.

   * **Si vous n'avez pas modifié l'historique de messages :** si le message contient des appels d'outils, l'exécuteur ajoute le message de l'assistant et les résultats d'outils, puis continue. S'il n'y a pas d'appels d'outils, la boucle se termine.
   * **Si vous avez modifié l'historique de messages :** l'exécuteur ignore son ajout automatique et utilise votre état tel quel. Consultez [Prendre le contrôle de l'historique de messages](https://platform.claude.com/docs/fr/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
```

### Prendre le contrôle de l'historique de messages

Par défaut, l'exécuteur gère l'état de la conversation pour vous : après chaque tour comportant un appel d'outil, il ajoute le message de l'assistant et les éventuels résultats d'outils à son propre historique de messages. Vous prenez le contrôle de l'historique de messages lorsque vous souhaitez réessayer un tour (rejeter la réponse et renvoyer la requête), injecter un message de suivi ou construire vous-même le résultat d'outil.

Vous prenez le contrôle en modifiant les messages de l'exécuteur depuis le corps de la boucle. La méthode exacte dépend du SDK. Consultez les onglets par langage ci-dessous.

Lorsque vous prenez le contrôle pour une itération, l'exécuteur n'ajoute ni le message de l'assistant ni les résultats d'outils de ce tour. Il vous incombe alors de maintenir la validité de la conversation : ajoutez vous-même le message de l'assistant et un résultat d'outil (si vous voulez que le tour compte), modifiez l'état de manière conditionnelle afin que la boucle puisse toujours se terminer lorsqu'il n'y a pas d'appels d'outils, et passez `max_iterations` (csharp, java, php: `maxIterations`; go: `MaxIterations`) pour borner la boucle. Les sept SDK prennent tous en charge `max_iterations` (csharp, java, php: `maxIterations`; go: `MaxIterations`).

<Tabs exclude="shell">
  <Tab title="Python">
    Utilisez `generate_tool_call_response()` pour inspecter ou calculer le résultat d'outil. Appeler `append_messages()` dans la boucle indique à l'exécuteur que vous gérez vous-même l'historique ; incluez donc le message de l'assistant et le résultat d'outil dans ce que vous ajoutez.

    ```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() marque l'état comme modifié, donc le runner ignore son
            # ajout automatique pour cette itération. Ajoutez vous-même le message de
            # l'assistant et le résultat d'outil, plus tout suivi éventuel.
            runner.append_messages(
                message,
                tool_response,
                {"role": "user", "content": "Please be concise."},
            )
        # Sans appel d'outil, laissez l'état intact pour que la boucle se termine.
    ```

    Pour modifier des paramètres de requête tels que `max_tokens` sans prendre le contrôle de l'historique de messages, utilisez `set_messages_params()`. L'exécuteur ajoute toujours automatiquement le message de l'assistant et le résultat d'outil.

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

  <Tab title="TypeScript">
    Utilisez `runner.params` pour lire les paramètres de requête actuels et `setMessagesParams()` pour les remplacer. Appeler `setMessagesParams()` ou `pushMessages()` dans la boucle indique à l'exécuteur que vous gérez vous-même l'état : le message de l'assistant et le résultat d'outil de cette itération sont abandonnés, et la requête suivante est envoyée avec votre état.

    L'exemple suivant réessaie une réponse tronquée avec un budget `max_tokens` plus élevé.

    ```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}.`);
        // Augmentez le budget. setMessagesParams() marque l'état comme modifié, donc le
        // runner n'ajoute PAS le message tronqué. L'itération suivante réessaie
        // le même tour avec le budget plus élevé.
        runner.setMessagesParams((params) => ({ ...params, max_tokens: doubled }));
      }
      // Sinon, laissez l'état intact pour que le runner l'ajoute automatiquement et continue.
    }
    ```
  </Tab>

  <Tab title="C#">
    Appeler `SetParams()` ou `PushMessages()` marque l'état comme modifié, ce qui amène l'exécuteur à ignorer son ajout automatique pour ce tour. L'exécuteur C# exécute tout de même les outils correspondants pour ce tour et rejette leurs résultats construits automatiquement ; un outil que vous exécutez également vous-même dans le corps de la boucle s'exécute donc deux fois, sauf si vous en tenez compte. Lorsque vous prenez le contrôle, ajoutez vous-même le message de l'assistant et un résultat d'outil. Sinon, la conversation ne progressera pas. L'exécuteur C# se termine toujours lorsqu'une réponse ne contient aucun appel d'outil ; conditionnez donc toute mutation d'état à la présence d'un bloc `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)
        {
            // Aucun appel d'outil : laissez l'état intact pour que la boucle se termine normalement.
            continue;
        }

        // Exécutez l'outil vous-même et construisez le bloc de résultat.
        var toolResult = new BetaToolResultBlockParam(toolUseBlock.ID)
        {
            Content = await getWeatherTool.ExecuteAsync(toolUseBlock, default),
        };

        // PushMessages() marque l'état comme modifié ; le runner ignore son ajout automatique.
        // Fournissez vous-même le tour de l'assistant et le résultat d'outil, puis ajoutez un suivi.
        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">
    L'exécuteur Go expose les paramètres sous forme d'un champ public `Params`. Modifier `runner.Params` entre les appels à `NextMessage(ctx)` s'applique à la prochaine requête API. Contrairement aux autres SDK, l'exécuteur Go ajoute toujours le message de l'assistant et les résultats d'outils de manière inconditionnelle. Modifier `Params` ne supprime pas cette étape.

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

    	// Le runner Go ajoute toujours le message de l'assistant et les résultats d'outils.
    	// Les modifications de paramètres ici s'appliquent à l'itération suivante.
    	runner.Params.MaxTokens = 2048
    }
    ```
  </Tab>

  <Tab title="Java">
    Utilisez `runner.params()` pour lire les paramètres actuels et `runner.setNextParams()` pour les remplacer pour l'itération suivante. Lorsque vous appelez `setNextParams()` dans la boucle, l'exécuteur ignore son ajout automatique. Le message qui vient d'être produit est rejeté, et l'itération suivante envoie vos nouveaux paramètres tels quels.

    L'exemple suivant réessaie un tour ayant atteint la limite de tokens en doublant `max_tokens`. Ne muter que dans la branche `max_tokens` permet à la boucle de converger : les tours qui se terminent normalement passent sans modification, et l'exécuteur effectue l'ajout automatique et se termine lorsqu'il n'y a plus d'appels d'outils.

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

            // L'appel à setNextParams() marque ce tour comme géré par l'utilisateur : le runner
            // n'ajoute PAS automatiquement le message tronqué, donc l'itération suivante
            // renvoie le même préfixe de conversation avec le budget plus élevé.
            runner.setNextParams(runner.params().toBuilder().maxTokens(doubled).build());
        }
        // Aucune mutation sur un tour normal : le runner ajoute automatiquement et continue.
    }
    ```
  </Tab>

  <Tab title="PHP">
    Utilisez `setMessagesParams()` et `pushMessages()` pour modifier l'état de l'exécuteur, et `getParams()` pour le lire. Appeler l'un ou l'autre de ces setters dans la boucle indique à l'exécuteur d'ignorer son ajout automatique, de sorte que la conversation se poursuit à partir de votre état modifié.

    L'exemple suivant double `max_tokens` et réessaie lorsqu'une réponse est tronquée.

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

            // Appeler setMessagesParams() dans la boucle indique au runner d'ignorer
            // son ajout automatique. Le message tronqué est supprimé ; l'itération
            // suivante réessaie avec le budget plus élevé.
            // Les clés sont en camelCase, correspondant aux paramètres nommés de toolRunner().
            $runner->setMessagesParams(['maxTokens' => $doubled]);
        }
    }
    ```
  </Tab>

  <Tab title="Ruby">
    Utilisez `next_message` pour un contrôle pas à pas. Au moment où `next_message` retourne, le message de l'assistant et le résultat d'outil de ce tour sont déjà ajoutés. Utilisez `feed_messages` pour injecter des messages de suivi entre les tours, et `runner.params.update(...)` pour modifier les paramètres de requête sur place.

    Vous prenez le contrôle de l'historique de messages lorsque, depuis un bloc `each_message` ou `each_streaming`, vous réaffectez `runner.params[:messages]` ou appelez `feed_messages`. Le modèle suivant appelle `feed_messages` entre les appels à `next_message`, ce qui ne prend pas le contrôle.

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

    # Avance le runner d'un pas. Le message de l'assistant et le résultat d'outil sont ajoutés
    # à runner.params[:messages] avant que next_message ne retourne.
    message = runner.next_message
    puts message.content

    # Injecte un suivi avant de continuer. feed_messages prend un splat, pas un tableau.
    runner.feed_messages({role: "user", content: "Also check Boston."})

    # Modifie les paramètres en place. Réassigner runner.params[:messages] ne remplace
    # l'historique des messages que si cela se produit dans un bloc each_message ou each_streaming.
    runner.params.update(max_tokens: 2048)

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

### Gestion automatique du contexte

Pour les tâches agentiques de longue durée, les exécuteurs d'outils TypeScript et Ruby prennent en charge la [compaction](https://platform.claude.com/docs/fr/build-with-claude/context-editing#client-side-compaction-sdk) automatique, qui génère des résumés lorsque l'utilisation de tokens dépasse un seuil afin que la conversation puisse se poursuivre au-delà des limites de la « context window » (fenêtre de contexte). Les deux SDK ont déprécié cette option côté client au profit de la [compaction côté serveur](https://platform.claude.com/docs/fr/build-with-claude/compaction-threshold), qui fonctionne avec l'exécuteur d'outils de chaque SDK via le paramètre de requête `context_management`. Le SDK Python (v1.0 et versions ultérieures) ainsi que les exécuteurs d'outils Go, Java, C# et PHP n'incluent pas la compaction côté client. L'exécuteur d'outils dispose d'un helper `compact_before_next_turn()` (typescript, java, php: `compactBeforeNextTurn()`; csharp, go: `CompactBeforeNextTurn()`) pour la compaction à la demande. Consultez [Compacter dans une boucle](https://platform.claude.com/docs/fr/build-with-claude/compaction-on-demand#compact-in-a-loop). Utilisez soit ce helper, soit une modification de compaction `context_management` sur un exécuteur, mais pas les deux.

### Déboguer l'exécution des outils

Lorsqu'un outil lève une exception, l'exécuteur d'outils l'intercepte et renvoie l'erreur à Claude sous forme de résultat d'outil avec `is_error: true`. Le résultat d'outil contient le message de l'exception (en Python, son type et son message), et non la trace de pile complète.

Ce que le SDK journalise dépend du langage. Le SDK Python journalise l'exception complète, y compris sa trace de pile, via le module standard `logging` chaque fois qu'un outil lève une exception non gérée. Les SDK Python, TypeScript et Java lisent la variable d'environnement `ANTHROPIC_LOG` pour activer la journalisation du SDK, qui inclut le détail des requêtes et des réponses :

```bash
# Journaliser au niveau info
export ANTHROPIC_LOG=info

# Journaliser au niveau debug pour une sortie plus détaillée
export ANTHROPIC_LOG=debug
```

Les SDK Go, Ruby, C# et PHP ne lisent pas `ANTHROPIC_LOG`. En dehors de Python, aucun SDK ne journalise un outil en échec : pour savoir pourquoi un outil a échoué, interceptez et journalisez l'exception dans la fonction de l'outil avant de la renvoyer ou de la relancer.

### Intercepter les erreurs d'outils

Par défaut, les erreurs d'outils sont renvoyées à Claude, qui peut alors répondre de manière appropriée. Cependant, vous pourriez vouloir détecter les erreurs et les traiter différemment, par exemple pour arrêter l'exécution prématurément ou implémenter une gestion d'erreurs personnalisée.

<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 est un dict : {"role": "user", "content": [...]}
            # Vérifier si un résultat d'outil contient une erreur.
            for block in tool_response["content"]:
                if block.get("is_error"):
                    # Option 1 : lever une exception pour arrêter la boucle.
                    raise RuntimeError(f"Tool failed: {json.dumps(block['content'])}")

                    # Option 2 : journaliser et continuer (laisser Claude gérer).
                    # logger.error(f"Tool error: {json.dumps(block['content'])}")

        # Traiter le message normalement.
        print(message.content)
    ```

    <Note>
      Appelez 

      `runner.generate_tool_call_response()`

       dans la boucle pour obtenir les résultats d'outils et y rechercher des erreurs avant que l'exécuteur ne les envoie à 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") {
        // Vérifier si un résultat d'outil contient une erreur
        for (const block of toolResultMessage.content) {
          if (block.type === "tool_result" && block.is_error) {
            // Option 1 : lever une exception pour arrêter la boucle
            throw new Error(`Tool failed: ${JSON.stringify(block.content)}`);

            // Option 2 : journaliser et continuer (laisser Claude gérer l'erreur)
            // console.error(`Tool error: ${JSON.stringify(block.content)}`);
          }
        }
      }

      // Traiter le message normalement
      console.log(message.content);
    }
    ```

    <Note>
      Appelez 

      `runner.generateToolResponse()`

       dans la boucle pour obtenir les résultats d'outils et y rechercher des erreurs avant que l'exécuteur ne les envoie à Claude.
    </Note>
  </Tab>

  <Tab title="C#">
    L'exécuteur d'outils C# n'expose pas de hook pour inspecter le résultat d'outil avant son envoi à Claude. Pour contrôler le contenu de l'erreur, levez `BetaToolError` depuis le corps de l'outil. L'exécuteur le convertit en `tool_result` avec `is_error: true` et le contenu que vous fournissez.

    ```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)
            {
                // Journalisez ici si vous devez inspecter l'échec avant que Claude ne le voie.
                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">
    L'interception des erreurs d'outils avant leur envoi à Claude n'est actuellement pas prise en charge dans le SDK Go. L'exécuteur convertit en interne une erreur renvoyée par votre handler en résultat d'outil avec `is_error: true`. Pour personnaliser le contenu de l'erreur, interceptez l'erreur dans votre handler et renvoyez un résultat au lieu de renvoyer l'erreur.
  </Tab>

  <Tab title="Java">
    L'interception des erreurs d'outils avant leur envoi à Claude n'est actuellement pas prise en charge dans le SDK Java. L'exécuteur intercepte toute exception levée par la fonction d'un outil et la convertit automatiquement en un résultat d'outil avec `is_error: true`. Pour contrôler le contenu de l'erreur, interceptez l'exception dans la fonction et renvoyez votre propre contenu.
  </Tab>

  <Tab title="PHP">
    L'exécuteur d'outils PHP n'expose actuellement pas les résultats d'outils avant leur ajout. Les exceptions levées depuis la closure `run` d'un outil sont interceptées et envoyées automatiquement à Claude sous forme de résultats d'outils avec `is_error: true`. Pour inspecter ou remplacer le contenu de l'erreur, utilisez le modèle manuel `pushMessages()` présenté dans [Modifier les résultats d'outils](https://platform.claude.com/docs/fr/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

      # Lorsque next_message retourne, le runner a exécuté les outils de ce tour et
      # ajouté leurs résultats comme dernier message (rôle user). Inspectez-les ici,
      # avant que la requête suivante ne les envoie à Claude.
      tool_results = runner.params[:messages].last

      if tool_results && tool_results[:role] == :user && tool_results[:content].is_a?(Array)
        tool_results[:content].each do |block|
          if block[:type] == :tool_result && block[:is_error]
            # Option 1 : lever une exception pour arrêter la boucle.
            raise "Tool failed: #{block[:content]}"

            # Option 2 : journaliser et continuer (laisser Claude gérer).
            # logger.error("Tool error: #{block[:content]}")
          end
        end
      end

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

    <Note>
      L'exécuteur d'outils Ruby n'a pas de hook qui renvoie les résultats d'outils. Une fois que 

      `runner.next_message`

       a renvoyé son résultat, ils constituent la dernière entrée de 

      `runner.params[:messages]`

       ; vérifiez-les donc à cet endroit avant que la requête suivante ne les envoie à Claude.
    </Note>
  </Tab>
</Tabs>

### Modifier les résultats d'outils

Vous pouvez modifier les résultats d'outils avant qu'ils ne soient renvoyés à Claude. Cela est utile pour ajouter des métadonnées telles que `cache_control` afin d'activer le « prompt caching » (mise en cache des prompts) sur les résultats d'outils, ou pour transformer la sortie de l'outil. Consultez la page sur la [mise en cache des prompts](https://platform.claude.com/docs/fr/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 est un dict : {"role": "user", "content": [...]}
            # Modifier le résultat de l'outil pour ajouter le contrôle du cache.
            for block in tool_response["content"]:
                if block["type"] == "tool_result":
                    # Ajouter cache_control pour mettre en cache ce résultat d'outil.
                    block["cache_control"] = {"type": "ephemeral"}

            # Ajouter la réponse modifiée (empêche l'ajout automatique de l'originale).
            runner.append_messages(message, tool_response)

        print(message.content)
    ```

    <Note>
      Appelez 

      `runner.generate_tool_call_response()`

       pour obtenir le résultat d'outil, modifiez-le, puis passez-le à 

      `runner.append_messages()`

       afin que l'exécuteur n'ajoute pas l'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") {
        // Modifier le résultat de l'outil pour ajouter le contrôle du cache.
        for (const block of toolResultMessage.content) {
          if (block.type === "tool_result") {
            // Ajouter cache_control pour mettre en cache ce résultat d'outil.
            block.cache_control = { type: "ephemeral" };
          }
        }
        // Aucun appel à pushMessages n'est nécessaire : le runner ajoute automatiquement le message
        // de l'assistant et la réponse d'outil mise en cache (désormais modifiée).
      }

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

    <Note>
      Appelez 

      `runner.generateToolResponse()`

       pour obtenir le résultat d'outil, puis modifiez-le sur place. L'exécuteur ajoute le résultat modifié pour vous.
    </Note>
  </Tab>

  <Tab title="C#">
    La modification des résultats d'outils avant leur ajout (par exemple, pour ajouter `cache_control`) n'est actuellement pas prise en charge dans le SDK C#. L'exécuteur construit le bloc `tool_result` en interne et ne fournit aucun hook pour le modifier.
  </Tab>

  <Tab title="Go">
    L'exécuteur Go n'expose pas de hook pour modifier le bloc `tool_result` externe. Vous pouvez toutefois définir `cache_control` sur les blocs de contenu internes que votre handler renvoie.

    ```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),
    				// Définissez cache_control sur le bloc de contenu interne. Le cache_control
    				// du bloc tool_result externe n'est actuellement pas
    				// configurable via le runner 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">
    Pour définir `cache_control` sur un résultat d'outil, construisez le `BetaToolResultBlockParam.Content` renvoyé avec `ofBlocks()` au lieu de `ofString()` et définissez `cacheControl` sur le bloc de texte interne. L'exécuteur ne prend actuellement pas en charge la définition de `cache_control` sur le bloc `tool_result` externe.

    ```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">
    L'exécuteur d'outils PHP ne dispose d'aucun callback pour muter le bloc `tool_result` généré automatiquement. Pour ajouter des champs tels que `cache_control`, construisez vous-même le résultat d'outil et ajoutez-le. Appeler `pushMessages()` ignore l'ajout automatique de l'exécuteur pour ce tour.

    ```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),
                    // Ajoutez cache_control pour mettre en cache ce résultat d'outil.
                    'cache_control' => ['type' => 'ephemeral'],
                ];
            }
        }

        if ($toolResults !== []) {
            // pushMessages() marque l'état comme modifié, donc le runner ignore son
            // ajout automatique. Ajoutez le message de l'assistant et les résultats d'outils.
            $runner->pushMessages(
                ['role' => 'assistant', 'content' => $message->content],
                ['role' => 'user', 'content' => $toolResults],
            );
        }
        // Aucun appel d'outil : laissez l'état intact pour que la boucle se 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

      # Accédez aux résultats d'outils les plus récents depuis le tableau messages.
      # Le runner ajoute automatiquement les résultats d'outils, mais vous pouvez les modifier.
      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
            # Modifiez le résultat d'outil pour ajouter le contrôle du cache.
            block[:cache_control] = {type: "ephemeral"}
          end
        end
      end

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

    <Note>
      L'exécuteur d'outils Ruby n'a pas de hook qui renvoie les résultats d'outils. Une fois que 

      `runner.next_message`

       a renvoyé son résultat, ils constituent la dernière entrée de 

      `runner.params[:messages]`

       ; modifiez-les donc à cet endroit avant que la requête suivante ne les envoie à Claude.
    </Note>
  </Tab>
</Tabs>

<Tip>
  L'ajout de `cache_control` aux résultats d'outils est particulièrement utile lorsque les outils renvoient de grandes quantités de données (comme des résultats de recherche documentaire) que vous souhaitez mettre en cache pour les appels API suivants. Consultez [Mise en cache des prompts](https://platform.claude.com/docs/fr/build-with-claude/prompt-caching) pour plus de détails sur les stratégies de mise en cache.
</Tip>

## Streaming

Activez le streaming pour traiter la réponse de chaque tour de manière incrémentale. Chaque itération produit un objet de flux sur lequel vous pouvez itérer pour obtenir les événements.

<Tabs exclude="shell">
  <Tab title="Python">
    Définissez `stream=True` et utilisez `get_final_message()` pour obtenir le message accumulé.

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

    # En streaming, le runner renvoie 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">
    Définissez `stream: true` et utilisez `finalMessage()` pour obtenir le message accumulé.

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

    // En streaming, le runner renvoie 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#">
    Appelez `runner.Streaming()` pour obtenir une séquence asynchrone imbriquée : un flux interne pour chaque appel 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">
    Utilisez `NewToolRunnerStreaming` et itérez sur `runner.AllStreaming(ctx)`. Chaque itération externe produit un flux d'événements pour un appel 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">
    Appelez `runner.streaming()` pour obtenir un flux pour chaque tour. Chaque `StreamResponse` doit être fermé après utilisation.

    ```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">
    Le streaming n'est actuellement pas disponible avec l'exécuteur d'outils PHP.
  </Tab>

  <Tab title="Ruby">
    Utilisez `each_streaming` pour itérer sur les événements 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>

## Étapes suivantes

<CardGroup cols={2}>
  <Card title="Utilisation d'outils stricte" icon="check" href="https://platform.claude.com/docs/fr/agents-and-tools/tool-use/strict-tool-use">
    Imposez la conformité au JSON Schema sur les entrées d'outils de Claude grâce à l'échantillonnage contraint par grammaire.
  </Card>

  <Card title="Gérer les appels d'outils" icon="arrows-left-right" href="https://platform.claude.com/docs/fr/agents-and-tools/tool-use/handle-tool-calls">
    Analysez les blocs `tool_use`, formatez les réponses `tool_result` et gérez les erreurs avec `is_error`.
  </Card>

  <Card title="Utilisation d'outils en parallèle" icon="grid" href="https://platform.claude.com/docs/fr/agents-and-tools/tool-use/parallel-tool-use">
    Activez, formatez et désactivez les appels d'outils parallèles, avec des conseils sur l'historique de messages et le dépannage.
  </Card>

  <Card title="Définir des outils" icon="hammer" href="https://platform.claude.com/docs/fr/agents-and-tools/tool-use/define-tools">
    Spécifiez les schémas d'outils, rédigez des descriptions efficaces et contrôlez quand Claude appelle vos outils.
  </Card>
</CardGroup>
