Files
BlackBean/pigo/internal/agenttool/batch_executor.go
T
2026-08-14 23:41:57 +08:00

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
}