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