Why: Mutual exclusion alone does not preserve order. With parallel execution the host cannot keep order-sensitive tools (card streaming begin/append/commit, plan updates) correct by locking a mutex, because whichever goroutine grabs the lock first is not necessarily the first declared call - a real failure observed in the backend card-stream test. Evidence: go test ./... green; TestLoopSerialToolsKeepOrder asserts the serial calls execute in declaration order (s1->s2->s3) while parallel calls still fan out. Impact: New Config.SerialTools set. In parallel mode, matching calls run sequentially on the caller goroutine in model-declared order; others stay concurrent. Empty set keeps the previous all-parallel behavior; sequential mode is untouched. Refs: Dev task feature/agent-e1-parallel-fixes (F5).
73 lines
2.1 KiB
Go
73 lines
2.1 KiB
Go
package runtime
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"sync"
|
|
"testing"
|
|
|
|
"agent-native/runtime/llm"
|
|
"agent-native/runtime/tools"
|
|
)
|
|
|
|
// orderTool 记录自己的执行序号(并发安全)。
|
|
type orderTool struct {
|
|
name string
|
|
mu sync.Mutex
|
|
seq []string
|
|
}
|
|
|
|
func (o *orderTool) Name() string { return o.name }
|
|
func (o *orderTool) Description() string { return o.name }
|
|
func (o *orderTool) Parameters() json.RawMessage { return json.RawMessage(`{"type":"object"}`) }
|
|
func (o *orderTool) Execute(ctx context.Context, _ json.RawMessage) (tools.Result, error) {
|
|
id := o.name
|
|
if call, ok := ToolCallFromContext(ctx); ok {
|
|
id = call.ID
|
|
}
|
|
o.mu.Lock()
|
|
o.seq = append(o.seq, id)
|
|
o.mu.Unlock()
|
|
return tools.Result{Content: "ran " + id}, nil
|
|
}
|
|
|
|
// TestLoopSerialToolsKeepOrder 并行模式下 SerialTools 命中的调用按声明顺序执行
|
|
// (互斥不保序不够:卡片流等有先后依赖),未命中的并发执行。
|
|
func TestLoopSerialToolsKeepOrder(t *testing.T) {
|
|
cards := &orderTool{name: "card"}
|
|
reads := &orderTool{name: "read"}
|
|
reg := tools.NewRegistry(t.TempDir())
|
|
reg.Register(cards)
|
|
reg.Register(reads)
|
|
res, err := Run(context.Background(), Config{
|
|
Model: "fake", Tools: reg,
|
|
ToolExecution: ExecParallel,
|
|
SerialTools: map[string]bool{"card": true},
|
|
Stream: script(
|
|
llm.Message{Role: llm.RoleAssistant, StopReason: llm.StopToolCalls, ToolCalls: []llm.ToolCall{
|
|
{ID: "s1", Name: "card", Arguments: `{}`},
|
|
{ID: "p1", Name: "read", Arguments: `{}`},
|
|
{ID: "s2", Name: "card", Arguments: `{}`},
|
|
{ID: "s3", Name: "card", Arguments: `{}`},
|
|
}},
|
|
llm.Message{Role: llm.RoleAssistant, Content: "done", StopReason: llm.StopEnd},
|
|
),
|
|
}, llm.Message{Role: llm.RoleUser, Content: "go"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cards.mu.Lock()
|
|
got := append([]string(nil), cards.seq...)
|
|
cards.mu.Unlock()
|
|
if len(got) != 3 || got[0] != "s1" || got[1] != "s2" || got[2] != "s3" {
|
|
t.Fatalf("card executions must follow declaration order s1→s2→s3, got %v", got)
|
|
}
|
|
reads.mu.Lock()
|
|
nRead := len(reads.seq)
|
|
reads.mu.Unlock()
|
|
if nRead != 1 {
|
|
t.Fatalf("read executions = %d", nRead)
|
|
}
|
|
_ = res
|
|
}
|