87 lines
2.9 KiB
Go
87 lines
2.9 KiB
Go
// This file implements batch tool execution (US-005): a batch of tool calls
|
|
// from one assistant message is run either sequentially or in parallel, mirroring
|
|
// pi's semantics.
|
|
//
|
|
// - sequential mode runs each call prepare→execute→finalize in order and stops
|
|
// early if the context is aborted.
|
|
// - parallel mode preserves ordering by index-backfilling results, running the
|
|
// allowed calls in goroutines. (prepare is not separately staged here because
|
|
// executeToolCall keeps prepare+execute together per call; ordering is still
|
|
// guaranteed by writing each result to its source index.)
|
|
//
|
|
// The whole batch signals termination only when every finalized result has
|
|
// terminate=true, matching pi.
|
|
package agenttool
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
|
|
"github.com/smallnest/pigo/internal/agentcore"
|
|
)
|
|
|
|
// ForceSequential, when true, makes the whole batch run serially regardless of
|
|
// per-tool ExecutionMode.
|
|
type BatchConfig struct {
|
|
ToolExecutorConfig
|
|
ForceSequential bool
|
|
}
|
|
|
|
// ExecuteToolCalls runs a batch of tool calls belonging to one assistant
|
|
// message. It returns the tool-result messages in source order and whether the
|
|
// whole batch requests termination (only when every result terminates).
|
|
func ExecuteToolCalls(ctx context.Context, cfg BatchConfig, calls []agentcore.AgentToolCall, emit agentcore.EmitFunc) ([]agentcore.ToolResultMessage, bool) {
|
|
if len(calls) == 0 {
|
|
return nil, false
|
|
}
|
|
|
|
results := make([]agentcore.ToolResultMessage, len(calls))
|
|
terminates := make([]bool, len(calls))
|
|
|
|
if cfg.ForceSequential || batchRequiresSequential(cfg.Registry, calls) {
|
|
for i, call := range calls {
|
|
if ctx.Err() != nil {
|
|
// Abort: fill the remaining calls with aborted error results so
|
|
// every tool call still gets a result message.
|
|
for j := i; j < len(calls); j++ {
|
|
results[j] = errorToolResult(calls[j], "tool call aborted")
|
|
terminates[j] = false
|
|
}
|
|
break
|
|
}
|
|
results[i], terminates[i] = executeToolCall(ctx, cfg.ToolExecutorConfig, call, emit)
|
|
}
|
|
} else {
|
|
var wg sync.WaitGroup
|
|
for i, call := range calls {
|
|
wg.Add(1)
|
|
go func(i int, call agentcore.AgentToolCall) {
|
|
defer wg.Done()
|
|
results[i], terminates[i] = executeToolCall(ctx, cfg.ToolExecutorConfig, call, emit)
|
|
}(i, call)
|
|
}
|
|
wg.Wait()
|
|
}
|
|
|
|
// Whole batch terminates only when every result terminates (pi semantics).
|
|
allTerminate := true
|
|
for _, t := range terminates {
|
|
if !t {
|
|
allTerminate = false
|
|
break
|
|
}
|
|
}
|
|
return results, allTerminate
|
|
}
|
|
|
|
// batchRequiresSequential reports whether any tool in the batch declares
|
|
// ExecutionMode sequential, which forces the whole batch to run serially.
|
|
func batchRequiresSequential(reg *ToolRegistry, calls []agentcore.AgentToolCall) bool {
|
|
for _, call := range calls {
|
|
if tool, ok := reg.Get(call.Name); ok && tool.ExecutionMode() == agentcore.ToolExecutionSequential {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|