Files
Agent-Native/runtime/loop_callctx_test.go
T
weichen a5da18b710
ci / vet-and-test (pull_request) Successful in 50s
ci / docker-checkout-hostgateway (pull_request) Successful in 1s
Inject the active tool call into the Execute context
Why: With parallel tool execution the host-side tool wrapper can no
longer infer which invocation it is serving from a single in-flight
slot; tools need the call identity (ID/name/arguments) to pair process
state and per-call bookkeeping. Context injection keeps the tools
registry signature unchanged.

Evidence: go test ./... green; TestLoopToolSeesOwnCallInContext runs
two parallel calls and asserts each reads its own ID from ctx.

Impact: Additive WithToolCall/ToolCallFromContext helpers; runOne now
wraps the ctx before Execute. Callers that never inject keep ok=false
and are unaffected.

Refs: Dev task feature/agent-e1-parallel-fixes (F5 parallel wiring).
2026-10-10 08:48:26 +00:00

70 lines
2.0 KiB
Go

package runtime
import (
"context"
"encoding/json"
"sync"
"testing"
"agent-native/runtime/llm"
"agent-native/runtime/tools"
)
// ctxEchoTool 把自己从 ctx 读到的调用 ID 记下来(并发安全),验证内核
// 在 Execute 前注入了当前调用。
type ctxEchoTool struct {
mu sync.Mutex
ids []string
bad int
}
func (c *ctxEchoTool) Name() string { return "echo" }
func (c *ctxEchoTool) Description() string { return "echo" }
func (c *ctxEchoTool) Parameters() json.RawMessage { return json.RawMessage(`{"type":"object"}`) }
func (c *ctxEchoTool) Execute(ctx context.Context, _ json.RawMessage) (tools.Result, error) {
call, ok := ToolCallFromContext(ctx)
c.mu.Lock()
defer c.mu.Unlock()
if !ok || call.ID == "" || call.Name != "echo" {
c.bad++
return tools.Result{Content: "call missing from context", IsError: true}, nil
}
c.ids = append(c.ids, call.ID)
return tools.Result{Content: "ok " + call.ID}, nil
}
// TestLoopToolSeesOwnCallInContext 并行执行两个调用,各自读到自己的 ID。
func TestLoopToolSeesOwnCallInContext(t *testing.T) {
tool := &ctxEchoTool{}
reg := tools.NewRegistry(t.TempDir())
reg.Register(tool)
res, err := Run(context.Background(), Config{
Model: "fake",
Tools: reg,
ToolExecution: ExecParallel,
Stream: script(
llm.Message{Role: llm.RoleAssistant, StopReason: llm.StopToolCalls, ToolCalls: []llm.ToolCall{
{ID: "pa", Name: "echo", Arguments: `{}`},
{ID: "pb", Name: "echo", Arguments: `{}`},
}},
llm.Message{Role: llm.RoleAssistant, Content: "done", StopReason: llm.StopEnd},
),
}, llm.Message{Role: llm.RoleUser, Content: "go"})
if err != nil {
t.Fatal(err)
}
tool.mu.Lock()
defer tool.mu.Unlock()
if tool.bad != 0 || len(tool.ids) != 2 {
t.Fatalf("bad=%d ids=%v", tool.bad, tool.ids)
}
seen := map[string]bool{}
for _, id := range tool.ids {
seen[id] = true
}
if !seen["pa"] || !seen["pb"] {
t.Fatalf("ids = %v", tool.ids)
}
_ = res
}