github.com/anthropics/anthropic-sdk-go@v1.70.1/tools.md (about)

     1  # Tool Helpers
     2  
     3  The SDK provides helper functions for defining tools and automatically running the conversation loop between Claude and your tools until Claude produces a final response.
     4  
     5  ## Defining Tools
     6  
     7  Use the `toolrunner` package to create a `BetaTool` that combines the tool definition with its execution handler. There are three ways to create tools:
     8  
     9  - `NewBetaToolFromJSONSchema` - Automatically generates schema from a struct with `jsonschema` tags (recommended)
    10  - `NewBetaToolFromBytes` - Creates a tool from JSON schema bytes
    11  - `NewBetaTool` - Creates a tool from an explicit `BetaToolInputSchemaParam`
    12  
    13  The generic type parameter is automatically inferred from your handler function's signature, so you don't need to specify it explicitly.
    14  
    15  ### Automatic Schema Generation from Structs (Recommended)
    16  
    17  The easiest approach is to use `NewBetaToolFromJSONSchema`, which automatically generates the schema from your struct using `jsonschema` tags:
    18  
    19  ```go
    20  type GetWeatherInput struct {
    21  	City  string `json:"city" jsonschema:"required,description=The city name"`
    22  	Units string `json:"units,omitempty" jsonschema:"enum=celsius,enum=fahrenheit,description=Temperature units"`
    23  }
    24  
    25  weatherTool, err := toolrunner.NewBetaToolFromJSONSchema(
    26  	"get_weather",
    27  	"Get current weather for a city",
    28  	func(ctx context.Context, input GetWeatherInput) (anthropic.BetaToolResultBlockParamContentUnion, error) {
    29  		return anthropic.BetaToolResultBlockParamContentUnion{
    30  			OfText: &anthropic.BetaTextBlockParam{
    31  				Text: fmt.Sprintf("Weather in %s: 72°F, sunny", input.City),
    32  			},
    33  		}, nil
    34  	},
    35  )
    36  ```
    37  
    38  ### Using JSON Bytes
    39  
    40  You can provide the schema as JSON bytes using `NewBetaToolFromBytes`:
    41  
    42  ```go
    43  type GetWeatherInput struct {
    44  	City string `json:"city"`
    45  }
    46  
    47  weatherTool, err := toolrunner.NewBetaToolFromBytes(
    48  	"get_weather",
    49  	"Get current weather for a city",
    50  	[]byte(`{
    51  		"type": "object",
    52  		"properties": {
    53  			"city": {"type": "string", "description": "The city name"}
    54  		},
    55  		"required": ["city"]
    56  	}`),
    57  	func(ctx context.Context, input GetWeatherInput) (anthropic.BetaToolResultBlockParamContentUnion, error) {
    58  		// Your handler here
    59  	},
    60  )
    61  ```
    62  
    63  ### Using an Explicit Schema
    64  
    65  For full control, use `NewBetaTool` with a `BetaToolInputSchemaParam` directly:
    66  
    67  ```go
    68  weatherTool := toolrunner.NewBetaTool(
    69  	"get_weather",
    70  	"Get current weather for a city",
    71  	anthropic.BetaToolInputSchemaParam{
    72  		Properties: map[string]any{
    73  			"city": map[string]any{
    74  				"type":        "string",
    75  				"description": "The city name",
    76  			},
    77  		},
    78  	},
    79  	handler,
    80  )
    81  ```
    82  
    83  ### Raw JSON Input
    84  
    85  If you prefer to handle JSON parsing yourself, use `json.RawMessage` or `[]byte` as the input type:
    86  
    87  ```go
    88  rawTool, err := toolrunner.NewBetaToolFromBytes(
    89  	"process_data",
    90  	"Process raw JSON data",
    91  	schemaBytes,
    92  	func(ctx context.Context, input json.RawMessage) (anthropic.BetaToolResultBlockParamContentUnion, error) {
    93  		// Parse the JSON yourself
    94  		var data map[string]any
    95  		json.Unmarshal(input, &data)
    96  		// ...
    97  	},
    98  )
    99  
   100  ## Tool Runner
   101  
   102  The `BetaToolRunner` automatically handles the conversation loop between Claude and your tools. On each iteration, it:
   103  
   104  1. Sends the current messages to Claude
   105  2. If the message's `StopReason` is `tool_use`, executes the requested tools in parallel and adds the results to the conversation
   106  3. If it is `pause_turn` or `compaction`, sends the turn back unchanged so the server can resume it
   107  4. Repeats until Claude stops for any other reason (e.g. `end_turn`, `max_tokens`), returning that final message without running tools
   108  
   109  ### Basic Usage
   110  
   111  ```go
   112  tools := []anthropic.BetaTool{weatherTool}
   113  
   114  runner := client.Beta.Messages.NewToolRunner(tools, anthropic.BetaToolRunnerParams{
   115  	BetaMessageNewParams: anthropic.BetaMessageNewParams{
   116  		Model:     anthropic.ModelClaudeSonnet4_5_20250929,
   117  		MaxTokens: 1024,
   118  		Messages: []anthropic.BetaMessageParam{
   119  			anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("What's the weather in Tokyo?")),
   120  		},
   121  	},
   122  })
   123  
   124  // Run the entire conversation to completion
   125  message, err := runner.RunToCompletion(context.Background())
   126  ```
   127  
   128  ### Iterating Over Messages
   129  
   130  Use `All()` to iterate over each message in the conversation:
   131  
   132  ```go
   133  for message, err := range runner.All(ctx) {
   134  	if err != nil {
   135  		log.Fatal(err)
   136  	}
   137  	for _, block := range message.Content {
   138  		switch b := block.AsAny().(type) {
   139  		case anthropic.BetaTextBlock:
   140  			fmt.Println("[assistant]:", b.Text)
   141  		case anthropic.BetaToolUseBlock:
   142  			fmt.Printf("[tool call]: %s(%v)\n", b.Name, b.Input)
   143  		}
   144  	}
   145  }
   146  ```
   147  
   148  ### Step-by-Step Iteration
   149  
   150  For more control, use `NextMessage()` to advance one turn at a time:
   151  
   152  ```go
   153  for {
   154  	message, err := runner.NextMessage(ctx)
   155  	if err != nil {
   156  		log.Fatal(err)
   157  	}
   158  	if message == nil {
   159  		break // Conversation complete
   160  	}
   161  	// Process the message...
   162  }
   163  ```
   164  
   165  ### Streaming
   166  
   167  Use `BetaToolRunnerStreaming` via `NewToolRunnerStreaming()` for streaming responses:
   168  
   169  ```go
   170  runner := client.Beta.Messages.NewToolRunnerStreaming(tools, anthropic.BetaToolRunnerParams{
   171  	BetaMessageNewParams: anthropic.BetaMessageNewParams{
   172  		Model:     anthropic.ModelClaudeSonnet4_5_20250929,
   173  		MaxTokens: 1024,
   174  		Messages: []anthropic.BetaMessageParam{
   175  			anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("What's the weather in Tokyo?")),
   176  		},
   177  	},
   178  })
   179  
   180  for eventsIterator := range runner.AllStreaming(ctx) {
   181  	for event, err := range eventsIterator {
   182  		if err != nil {
   183  			log.Fatal(err)
   184  		}
   185  		switch e := event.AsAny().(type) {
   186  		case anthropic.BetaRawContentBlockDeltaEvent:
   187  			switch delta := e.Delta.AsAny().(type) {
   188  			case anthropic.BetaTextDelta:
   189  				fmt.Print(delta.Text)
   190  			}
   191  		}
   192  	}
   193  }
   194  ```
   195  
   196  Or use `NextStreaming()` for step-by-step streaming:
   197  
   198  ```go
   199  for !runner.IsCompleted() {
   200  	for event, err := range runner.NextStreaming(ctx) {
   201  		// Handle streaming events...
   202  	}
   203  }
   204  ```
   205  
   206  ## Configuration
   207  
   208  ### Max Iterations
   209  
   210  Limit the number of API calls to prevent runaway loops. When set to 0 (the default), there is no limit and the runner continues until the model's stop reason ends the loop:
   211  
   212  ```go
   213  runner := client.Beta.Messages.NewToolRunner(tools, anthropic.BetaToolRunnerParams{
   214  	// ...
   215  	MaxIterations: 10, // Stop after 10 API calls (0 = no limit)
   216  })
   217  ```
   218  
   219  ### Modifying Parameters Mid-Conversation
   220  
   221  The `Params` field is exported, so you can modify parameters directly:
   222  
   223  ```go
   224  // Update maximum tokens
   225  runner.Params.MaxTokens = 2048
   226  
   227  // Update maximum iterations
   228  runner.Params.MaxIterations = 10
   229  
   230  // Update system prompt
   231  runner.Params.System = []anthropic.BetaTextBlockParam{
   232  	{Text: "You are a helpful assistant."},
   233  }
   234  
   235  // Add messages to the conversation (direct field access)
   236  runner.Params.Messages = append(runner.Params.Messages, anthropic.NewBetaUserMessage(
   237  	anthropic.NewBetaTextBlock("Now check the weather in London too"),
   238  ))
   239  
   240  // Or use the convenience method
   241  runner.AppendMessages(anthropic.NewBetaUserMessage(
   242  	anthropic.NewBetaTextBlock("Now check the weather in London too"),
   243  ))
   244  ```
   245  
   246  ### Inspecting State
   247  
   248  ```go
   249  // Get most recent assistant message
   250  lastMsg := runner.LastMessage()
   251  
   252  // Get full conversation history (returns a copy)
   253  messages := runner.Messages()
   254  
   255  // Check iteration count
   256  count := runner.IterationCount()
   257  
   258  // Check if completed
   259  if runner.IsCompleted() {
   260  	// ...
   261  }
   262  ```
   263  
   264  ## Error Handling
   265  
   266  Tool execution errors are automatically converted to error results and sent back to Claude, allowing it to recover or try a different approach:
   267  
   268  ```go
   269  func handler(ctx context.Context, input MyInput) (anthropic.BetaToolResultBlockParamContentUnion, error) {
   270  	if input.City == "" {
   271  		return anthropic.BetaToolResultBlockParamContentUnion{}, errors.New("city is required")
   272  	}
   273  	// ...
   274  }
   275  ```
   276  
   277  The error message will be sent to Claude as a tool result with `is_error: true`.
   278  
   279  ## Parallel Tool Execution
   280  
   281  When Claude requests multiple tool calls in a single message, they are executed in parallel using an `errgroup`. This provides:
   282  
   283  - Concurrent execution for better performance
   284  - Proper context cancellation handling
   285  - Results returned in the correct order
   286  
   287  ## Managed-agents sessions
   288  
   289  The same `anthropic.BetaTool` shape works for managed-agents sessions. Two helpers cover the self-hosted side:
   290  
   291  - `client.Beta.Sessions.Events.NewToolRunner(ctx, sessionID, anthropic.SessionToolRunnerOptions{...})` — the sessions-side counterpart to `client.Beta.Messages.NewToolRunner`. The session id is a positional argument (matching `list`/`send`/`stream` on the events resource); the options struct carries the tool registry and tuning knobs. It attaches to a session's event stream, dispatches the registered tools on both `agent.tool_use` (builtin tools, answered with `user.tool_result`) and `agent.custom_tool_use` (user-defined function tools, answered with `user.custom_tool_result`), and stops after the session is idle past `MaxIdle`. It does *only* that — no work claiming, lease heartbeating, or skill download.
   292  - `environments.NewEnvironmentWorker(client, environments.EnvironmentWorkerOptions{...})` (in `github.com/anthropics/anthropic-sdk-go/lib/environments`) — the full self-hosted runner: it composes `environments.WorkPoller` (claim work) with a per-session `SessionToolRunner`, sets up the workdir + downloads the session agent's skills, heartbeats the work-item lease in parallel, force-stops the work on exit, and loops. A single `EnvironmentKey` authorizes everything — both the work-poll calls and the per-session calls. `worker.Run(ctx)` drives the poll loop (requires `EnvironmentID` + `EnvironmentKey`); `worker.HandleItem(ctx, environments.HandleItemOptions{...})` runs that same per-item flow (skills + run + heartbeat + force-stop) once for a work item you have already claimed yourself. Each `HandleItemOptions` field — `WorkID` / `EnvironmentID` / `SessionID` / `EnvironmentKey` — falls back to `ANTHROPIC_WORK_ID` / `ANTHROPIC_ENVIRONMENT_ID` / `ANTHROPIC_SESSION_ID` / `ANTHROPIC_ENVIRONMENT_KEY` when left empty (and `EnvironmentKey` also falls back to the worker's own `EnvironmentKey` option), so inside an `ant worker poll --on-work` hook (which exports all of them) it is just `worker.HandleItem(ctx, environments.HandleItemOptions{})`. If you are iterating `environments.WorkPoller` yourself, pass the claimed item through: `worker.HandleItem(ctx, environments.HandleItemOptions{WorkID: work.ID, EnvironmentID: work.EnvironmentID, SessionID: work.Data.ID, EnvironmentKey: environmentKey})`.
   293  
   294  The standard `agent_toolset_20260401` tools (`bash`, `read`, `write`, `edit`, `glob`, `grep`), the workdir/skills `AgentToolContext`, and the skill-download helper live in `github.com/anthropics/anthropic-sdk-go/tools/agenttoolset`; `agenttoolset.BetaAgentToolset20260401(env)` returns them as a plain `[]anthropic.BetaTool` you can filter or extend. The file tools confine to the workdir (symlink-aware) and are safe without a sandbox; `bash` is unrestricted and should run inside one.
   295  
   296  ## Examples
   297  
   298  See the [examples](./examples) directory for complete working examples:
   299  
   300  - [examples/tool-runner](./examples/tool-runner) - Basic tool runner usage
   301  - [examples/tool-runner-streaming](./examples/tool-runner-streaming) - Streaming with tool runner
   302  - [examples/managed-agents-self-hosted-sandbox-worker](./examples/managed-agents-self-hosted-sandbox-worker) - Self-hosted environment worker