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  }