github.com/anthropics/anthropic-sdk-go@v1.70.1/betatoolrunner.go (about) 1 package anthropic 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "iter" 8 9 "github.com/anthropics/anthropic-sdk-go/internal/stainlessheader" 10 "github.com/anthropics/anthropic-sdk-go/option" 11 "golang.org/x/sync/errgroup" 12 ) 13 14 // BetaTool represents a tool that can be executed by the BetaToolRunner. 15 type BetaTool interface { 16 // Name returns the tool's name 17 Name() string 18 // Description returns the tool's description 19 Description() string 20 // InputSchema returns the JSON schema for the tool's input 21 InputSchema() BetaToolInputSchemaParam 22 // Execute runs the tool with raw JSON input and returns one or more result blocks. 23 Execute(ctx context.Context, input json.RawMessage) ([]BetaToolResultBlockParamContentUnion, error) 24 } 25 26 // BetaToolRunnerParams contains parameters for creating a BetaToolRunner or BetaToolRunnerStreaming. 27 type BetaToolRunnerParams struct { 28 BetaMessageNewParams 29 // MaxIterations limits the number of API calls. When set to 0 (the default), 30 // there is no limit and the runner continues until the model stops using tools. 31 MaxIterations int 32 } 33 34 // betaToolRunnerBase holds state and logic shared by BetaToolRunner and BetaToolRunnerStreaming. 35 type betaToolRunnerBase struct { 36 messageService *BetaMessageService 37 // Params contains the configuration for the tool runner. 38 // This field is exported so users can modify parameters directly. 39 Params BetaToolRunnerParams 40 toolMap map[string]BetaTool 41 iterationCount int 42 lastMessage *BetaMessage 43 completed bool 44 opts []option.RequestOption 45 err error 46 } 47 48 func newBetaToolRunnerBase(messageService *BetaMessageService, tools []BetaTool, params BetaToolRunnerParams, opts []option.RequestOption) betaToolRunnerBase { 49 toolMap := make(map[string]BetaTool) 50 apiTools := make([]BetaToolUnionParam, len(tools)) 51 52 for i, tool := range tools { 53 toolMap[tool.Name()] = tool 54 apiTools[i] = BetaToolUnionParam{ 55 OfTool: &BetaToolParam{ 56 Name: tool.Name(), 57 Description: String(tool.Description()), 58 InputSchema: tool.InputSchema(), 59 }, 60 } 61 } 62 63 // Add tools to the API params 64 params.BetaMessageNewParams.Tools = apiTools 65 params.Messages = append([]BetaMessageParam{}, params.Messages...) 66 67 opts = append([]option.RequestOption{stainlessheader.With(stainlessheader.BetaToolRunner)}, opts...) 68 69 return betaToolRunnerBase{ 70 messageService: messageService, 71 Params: params, 72 toolMap: toolMap, 73 opts: opts, 74 } 75 } 76 77 // LastMessage returns the most recent assistant message, or nil if no messages have been received yet. 78 func (b *betaToolRunnerBase) LastMessage() *BetaMessage { 79 return b.lastMessage 80 } 81 82 // AppendMessages adds messages to the conversation history. 83 // This is a convenience method equivalent to: 84 // 85 // runner.Params.Messages = append(runner.Params.Messages, messages...) 86 func (b *betaToolRunnerBase) AppendMessages(messages ...BetaMessageParam) { 87 b.Params.Messages = append(b.Params.Messages, messages...) 88 } 89 90 // Messages returns a copy of the current conversation history. 91 // The returned slice can be safely modified without affecting the runner's state. 92 func (b *betaToolRunnerBase) Messages() []BetaMessageParam { 93 result := make([]BetaMessageParam, len(b.Params.Messages)) 94 copy(result, b.Params.Messages) 95 return result 96 } 97 98 // IterationCount returns the number of API calls made so far. 99 // This is incremented each time a turn makes an API call. 100 func (b *betaToolRunnerBase) IterationCount() int { 101 return b.iterationCount 102 } 103 104 // IsCompleted returns true if the conversation has finished, either because 105 // the model stopped using tools or the maximum iteration limit was reached. 106 func (b *betaToolRunnerBase) IsCompleted() bool { 107 return b.completed 108 } 109 110 // Err returns the last error that occurred during iteration, if any. 111 // This is useful when using All() or AllStreaming() to check for errors 112 // after the iteration completes. 113 func (b *betaToolRunnerBase) Err() error { 114 return b.err 115 } 116 117 // adoptContainer carries the container the last turn ran in onto the next 118 // request: container-bound server tools reject a follow-up that omits it, so 119 // its id is forwarded unless the caller pinned one themselves. 120 func (b *betaToolRunnerBase) adoptContainer(message *BetaMessage) { 121 id := message.Container.ID 122 if id == "" { 123 return 124 } 125 container := &b.Params.Container 126 switch { 127 case container.OfContainers != nil: 128 if !container.OfContainers.ID.Valid() { 129 pinned := *container.OfContainers 130 pinned.ID = String(id) 131 container.OfContainers = &pinned 132 } 133 case !container.OfString.Valid(): 134 container.OfString = String(id) 135 } 136 } 137 138 // toolRunnerStep is what the runner does with a turn, decided by its stop reason. 139 type toolRunnerStep int 140 141 const ( 142 // stepRunTools answers the turn's client tool calls and continues, or stops 143 // when there are none. 144 stepRunTools toolRunnerStep = iota 145 // stepResume sends the unfinished turn back unchanged, without executing 146 // any tool calls it carries, so the server continues it. 147 stepResume 148 // stepStop ends the conversation with the turn as the final message, 149 // without executing its tool calls. 150 stepStop 151 ) 152 153 // determineNextStepFromStopReason classifies every generated stop reason 154 // explicitly so a new one has to be placed in a bucket here; values unknown at 155 // runtime stop like end_turn rather than erroring. 156 func determineNextStepFromStopReason(reason BetaStopReason) toolRunnerStep { 157 switch reason { 158 case BetaStopReasonToolUse: 159 return stepRunTools 160 case BetaStopReasonPauseTurn: 161 // A long-running server tool paused the turn; sending it back unchanged 162 // resumes it. 163 return stepResume 164 case BetaStopReasonCompaction: 165 // pause_after_compaction hands the turn back before the model answers; 166 // sending it back unchanged continues it. 167 return stepResume 168 case BetaStopReasonRefusal: 169 // A refusal-terminated turn is terminal: its tool calls belong to a dead 170 // conversation — executing them fires side effects the caller never 171 // confirmed and produces tool_results that cannot be coherently replayed. 172 return stepStop 173 case BetaStopReasonMaxTokens, BetaStopReasonModelContextWindowExceeded: 174 // A cut-off turn left its last call's arguments incomplete. 175 return stepStop 176 case BetaStopReasonEndTurn, BetaStopReasonStopSequence: 177 return stepStop 178 default: 179 // An unrecognized stop reason stops like end_turn. 180 return stepStop 181 } 182 } 183 184 // executeTools processes any tool use blocks in the given message and returns a tool result message. 185 // Returns: 186 // - (result, nil) if tools executed successfully 187 // - (nil, nil) if the turn did not stop for tool use or has no client tool calls 188 // - (nil, ctx.Err()) if context was cancelled 189 func (b *betaToolRunnerBase) executeTools(ctx context.Context, message *BetaMessage) (*BetaMessageParam, error) { 190 if determineNextStepFromStopReason(message.StopReason) != stepRunTools { 191 return nil, nil 192 } 193 194 // Tool calls before the last fallback block belong to the attempt that 195 // refused; the fallback middleware strips them from replayed history, so 196 // answering them would orphan the tool_result. 197 seam := -1 198 for i, block := range message.Content { 199 if block.Type == "fallback" { 200 seam = i 201 } 202 } 203 204 var toolUseBlocks []BetaToolUseBlock 205 206 // Find all tool use blocks in the message 207 for i, block := range message.Content { 208 if i > seam && block.Type == "tool_use" { 209 toolUseBlocks = append(toolUseBlocks, block.AsToolUse()) 210 } 211 } 212 213 if len(toolUseBlocks) == 0 { 214 return nil, nil 215 } 216 217 // Execute all tools in parallel using errgroup for proper cancellation handling 218 results := make([]BetaContentBlockParamUnion, len(toolUseBlocks)) 219 available := b.availableToolNames() 220 221 g, gctx := errgroup.WithContext(ctx) 222 for i, toolUse := range toolUseBlocks { 223 g.Go(func() error { 224 // Check for cancellation before executing tool 225 select { 226 case <-gctx.Done(): 227 return gctx.Err() 228 default: 229 } 230 result := b.executeToolUse(gctx, toolUse, available) 231 results[i] = BetaContentBlockParamUnion{OfToolResult: &result} 232 return nil // tool errors become result content, not Go errors 233 }) 234 } 235 236 if err := g.Wait(); err != nil { 237 return nil, err 238 } 239 240 // Create user message with tool results 241 userMessage := NewBetaUserMessage(results...) 242 return &userMessage, nil 243 } 244 245 func newBetaToolResultErrorBlockParam(toolUseID string, errorText string) BetaToolResultBlockParam { 246 return NewBetaToolResultTextBlockParam(toolUseID, errorText, true) 247 } 248 249 // availableToolNames returns the tool names currently offered to the model: 250 // every registered tool, minus names dropped by tool_removal blocks in 251 // role "system" messages, plus names re-enabled by later tool_addition 252 // blocks. Removal is only a hint — the model can still call a removed tool — 253 // so a removed tool must resolve to the same not-found result as one that 254 // was never registered. 255 func (b *betaToolRunnerBase) availableToolNames() map[string]struct{} { 256 available := make(map[string]struct{}, len(b.toolMap)) 257 for name := range b.toolMap { 258 available[name] = struct{}{} 259 } 260 for _, message := range b.Params.Messages { 261 if message.Role != BetaMessageParamRoleSystem { 262 continue 263 } 264 for _, block := range message.Content { 265 applyToolChange(block, available) 266 } 267 } 268 return available 269 } 270 271 // applyToolChange folds one system-message content block into the available 272 // tool-name set. The populated Of* variant is the discriminator; every other 273 // block type, including ones added after this SDK was generated, leaves the 274 // set untouched. 275 func applyToolChange(block BetaContentBlockParamUnion, available map[string]struct{}) { 276 switch { 277 case block.OfToolRemoval != nil: 278 if name, ok := removalRefName(block.OfToolRemoval.Tool); ok { 279 delete(available, name) 280 } 281 case block.OfToolAddition != nil: 282 if name, ok := additionRefName(block.OfToolAddition.Tool); ok { 283 available[name] = struct{}{} 284 } 285 } 286 } 287 288 // additionRefName and removalRefName resolve the referenced tool's name across 289 // the generated addition/removal tool unions. Only a tool_reference names a 290 // locally runnable tool; MCP references run server-side and unknown variants 291 // are ignored. 292 func additionRefName(u BetaRequestToolAdditionBlockToolUnionParam) (string, bool) { 293 switch { 294 case u.OfToolReference != nil: 295 return u.OfToolReference.Name, u.OfToolReference.Name != "" 296 default: 297 return "", false 298 } 299 } 300 301 func removalRefName(u BetaRequestToolRemovalBlockToolUnionParam) (string, bool) { 302 switch { 303 case u.OfToolReference != nil: 304 return u.OfToolReference.Name, u.OfToolReference.Name != "" 305 default: 306 return "", false 307 } 308 } 309 310 // executeToolUse executes a single tool use block and returns the result. 311 // available is the current set of offered tool names (see availableToolNames). 312 func (b *betaToolRunnerBase) executeToolUse(ctx context.Context, toolUse BetaToolUseBlock, available map[string]struct{}) BetaToolResultBlockParam { 313 _, allowed := available[toolUse.Name] 314 tool, exists := b.toolMap[toolUse.Name] 315 if !exists || !allowed { 316 return newBetaToolResultErrorBlockParam( 317 toolUse.ID, 318 fmt.Sprintf("Error: Tool '%s' not found", toolUse.Name), 319 ) 320 } 321 322 // Parse and execute the tool 323 inputBytes, err := json.Marshal(toolUse.Input) 324 if err != nil { 325 return newBetaToolResultErrorBlockParam( 326 toolUse.ID, 327 fmt.Sprintf("Error: Failed to marshal tool input: %v", err), 328 ) 329 } 330 331 content, err := tool.Execute(ctx, inputBytes) 332 if err != nil { 333 return newBetaToolResultErrorBlockParam( 334 toolUse.ID, 335 fmt.Sprintf("Error: %v", err), 336 ) 337 } 338 339 return BetaToolResultBlockParam{ 340 ToolUseID: toolUse.ID, 341 Content: content, 342 } 343 } 344 345 // BetaToolRunner manages the automatic conversation loop between the assistant and tools 346 // using non-streaming API calls. It implements an iterator pattern for processing 347 // conversation turns. 348 // 349 // A BetaToolRunner is NOT safe for concurrent use. All methods must be called 350 // from a single goroutine. However, tool handlers ARE called concurrently 351 // when multiple tools are invoked in a single turn - ensure your handlers 352 // are thread-safe. 353 type BetaToolRunner struct { 354 betaToolRunnerBase 355 } 356 357 // NewToolRunner creates a BetaToolRunner that automatically handles the loop between 358 // the model generating tool calls, executing those tool calls, and sending the 359 // results back to the model until a final answer is produced or the maximum 360 // number of iterations is reached. 361 func (r *BetaMessageService) NewToolRunner(tools []BetaTool, params BetaToolRunnerParams, opts ...option.RequestOption) *BetaToolRunner { 362 return &BetaToolRunner{ 363 betaToolRunnerBase: newBetaToolRunnerBase(r, tools, params, opts), 364 } 365 } 366 367 // NextMessage advances the conversation by one turn. It executes any pending tool calls 368 // from the previous message, then makes an API call to get the model's next response. 369 // 370 // Returns: 371 // - (message, nil) on success with the assistant's response 372 // - (nil, nil) when the conversation is complete (no more tool calls or max iterations reached) 373 // - (nil, error) if an error occurred during tool execution or API call 374 func (r *BetaToolRunner) NextMessage(ctx context.Context) (*BetaMessage, error) { 375 if r.completed { 376 return nil, nil 377 } 378 379 // Check iteration limit 380 if r.Params.MaxIterations > 0 && r.iterationCount >= r.Params.MaxIterations { 381 r.completed = true 382 return nil, nil 383 } 384 385 // Execute any pending tool calls from the last message 386 if r.lastMessage != nil { 387 toolMessage, err := r.executeTools(ctx, r.lastMessage) 388 if err != nil { 389 r.err = err 390 return nil, err 391 } 392 if toolMessage != nil { 393 r.Params.Messages = append(r.Params.Messages, *toolMessage) 394 } else if determineNextStepFromStopReason(r.lastMessage.StopReason) != stepResume { 395 r.completed = true 396 return nil, nil 397 } 398 } 399 400 // Make API call 401 r.iterationCount++ 402 messageParams := r.Params.BetaMessageNewParams 403 messageParams.Messages = r.Params.Messages 404 405 message, err := r.messageService.New(ctx, messageParams, r.opts...) 406 if err != nil { 407 r.err = err 408 return nil, fmt.Errorf("failed to get next message: %w", err) 409 } 410 411 r.lastMessage = message 412 r.Params.Messages = append(r.Params.Messages, message.ToParam()) 413 r.adoptContainer(message) 414 415 return message, nil 416 } 417 418 // RunToCompletion repeatedly calls NextMessage until the conversation is complete, 419 // either because the model stopped using tools or the maximum iteration limit was reached. 420 // 421 // Returns the final assistant message and any error that occurred. 422 func (r *BetaToolRunner) RunToCompletion(ctx context.Context) (*BetaMessage, error) { 423 for { 424 message, err := r.NextMessage(ctx) 425 if err != nil { 426 return nil, err 427 } 428 if message == nil { 429 return r.lastMessage, nil 430 } 431 } 432 } 433 434 // All returns an iterator that yields all messages until the conversation completes. 435 // This is a convenience method for iterating over the entire conversation. 436 // 437 // Example usage: 438 // 439 // for message, err := range runner.All(ctx) { 440 // if err != nil { 441 // return err 442 // } 443 // // process message 444 // } 445 func (r *BetaToolRunner) All(ctx context.Context) iter.Seq2[*BetaMessage, error] { 446 return func(yield func(*BetaMessage, error) bool) { 447 for { 448 message, err := r.NextMessage(ctx) 449 r.err = err 450 if message == nil { 451 if err != nil { 452 yield(nil, err) 453 } 454 return 455 } 456 if !yield(message, err) { 457 return 458 } 459 } 460 } 461 } 462 463 // BetaToolRunnerStreaming manages the automatic conversation loop between the assistant 464 // and tools using streaming API calls. It implements an iterator pattern for processing 465 // streaming events across conversation turns. 466 // 467 // A BetaToolRunnerStreaming is NOT safe for concurrent use. All methods must be called 468 // from a single goroutine. However, tool handlers ARE called concurrently 469 // when multiple tools are invoked in a single turn - ensure your handlers 470 // are thread-safe. 471 type BetaToolRunnerStreaming struct { 472 betaToolRunnerBase 473 } 474 475 // NewToolRunnerStreaming creates a BetaToolRunnerStreaming that automatically handles 476 // the loop between the model generating tool calls, executing those tool calls, and 477 // sending the results back to the model using streaming API calls until a final answer 478 // is produced or the maximum number of iterations is reached. 479 func (r *BetaMessageService) NewToolRunnerStreaming(tools []BetaTool, params BetaToolRunnerParams, opts ...option.RequestOption) *BetaToolRunnerStreaming { 480 return &BetaToolRunnerStreaming{ 481 betaToolRunnerBase: newBetaToolRunnerBase(r, tools, params, opts), 482 } 483 } 484 485 // NextStreaming advances the conversation by one turn with streaming. It executes any 486 // pending tool calls from the previous message, then makes a streaming API call. 487 // 488 // Returns an iterator that yields streaming events as they arrive. The iterator should 489 // be fully consumed to ensure the message is properly accumulated for subsequent turns. 490 // 491 // If an error occurs, it will be yielded as the second value in the iterator pair. 492 // Check IsCompleted() after consuming the iterator to determine if the conversation 493 // has finished. 494 func (r *BetaToolRunnerStreaming) NextStreaming(ctx context.Context) iter.Seq2[BetaRawMessageStreamEventUnion, error] { 495 return func(yield func(BetaRawMessageStreamEventUnion, error) bool) { 496 if r.completed { 497 return 498 } 499 500 // Check iteration limit 501 if r.Params.MaxIterations > 0 && r.iterationCount >= r.Params.MaxIterations { 502 r.completed = true 503 return 504 } 505 506 // Execute any pending tool calls from the last message 507 if r.lastMessage != nil { 508 toolMessage, err := r.executeTools(ctx, r.lastMessage) 509 if err != nil { 510 r.err = err 511 yield(BetaRawMessageStreamEventUnion{}, err) 512 return 513 } 514 if toolMessage != nil { 515 r.Params.Messages = append(r.Params.Messages, *toolMessage) 516 } else if determineNextStepFromStopReason(r.lastMessage.StopReason) != stepResume { 517 r.completed = true 518 return 519 } 520 } 521 522 // Make streaming API call 523 r.iterationCount++ 524 streamParams := r.Params.BetaMessageNewParams 525 streamParams.Messages = r.Params.Messages 526 527 stream := r.messageService.NewStreaming(ctx, streamParams, r.opts...) 528 defer stream.Close() 529 530 // We need to collect the final message from the stream for the next iteration 531 finalMessage := &BetaMessage{} 532 for stream.Next() { 533 event := stream.Current() 534 err := finalMessage.Accumulate(event) 535 if err != nil { 536 r.err = fmt.Errorf("failed to accumulate streaming event: %w", err) 537 yield(BetaRawMessageStreamEventUnion{}, r.err) 538 return 539 } 540 541 if !yield(event, nil) { 542 return 543 } 544 } 545 546 // Check for stream errors after the loop exits 547 if stream.Err() != nil { 548 r.err = stream.Err() 549 yield(BetaRawMessageStreamEventUnion{}, r.err) 550 return 551 } 552 553 r.lastMessage = finalMessage 554 r.Params.Messages = append(r.Params.Messages, finalMessage.ToParam()) 555 r.adoptContainer(finalMessage) 556 } 557 } 558 559 // AllStreaming returns an iterator of iterators, where each inner iterator yields 560 // streaming events for a single turn of the conversation. The outer iterator continues 561 // until the conversation completes. 562 // 563 // Example usage: 564 // 565 // for events, err := range runner.AllStreaming(ctx) { 566 // if err != nil { 567 // return err 568 // } 569 // for event, err := range events { 570 // if err != nil { 571 // return err 572 // } 573 // // process streaming event 574 // } 575 // } 576 func (r *BetaToolRunnerStreaming) AllStreaming(ctx context.Context) iter.Seq2[iter.Seq2[BetaRawMessageStreamEventUnion, error], error] { 577 return func(yield func(iter.Seq2[BetaRawMessageStreamEventUnion, error], error) bool) { 578 for !r.completed { 579 eventSeq := r.NextStreaming(ctx) 580 if !yield(eventSeq, nil) { 581 return 582 } 583 } 584 } 585 }