Inject the active tool call into the Execute context #43
@@ -709,6 +709,22 @@ func noteToolHook(cfg Config, call llm.ToolCall, msg llm.Message) {
|
|||||||
cfg.Hooks.Emit(hooks.Call{Event: ev, Tool: call.Name, Args: call.Arguments, Result: msg.Content, IsError: msg.IsError})
|
cfg.Hooks.Emit(hooks.Call{Event: ev, Tool: call.Name, Args: call.Arguments, Result: msg.Content, IsError: msg.IsError})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// toolCallKey 是把当前工具调用注入 Execute ctx 的私有键:并行执行下工具实现
|
||||||
|
// 需要知道自己服务于哪次调用(配对过程事件、按调用记账),经
|
||||||
|
// ToolCallFromContext 读取;旧宿主/直接构造的 ctx 返回 ok=false。
|
||||||
|
type toolCallKey struct{}
|
||||||
|
|
||||||
|
// WithToolCall 把 call 挂进 ctx(内核 runOne 在执行前注入)。
|
||||||
|
func WithToolCall(ctx context.Context, call llm.ToolCall) context.Context {
|
||||||
|
return context.WithValue(ctx, toolCallKey{}, call)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToolCallFromContext 取出当前正在执行的调用;不存在时 ok=false。
|
||||||
|
func ToolCallFromContext(ctx context.Context) (llm.ToolCall, bool) {
|
||||||
|
call, ok := ctx.Value(toolCallKey{}).(llm.ToolCall)
|
||||||
|
return call, ok
|
||||||
|
}
|
||||||
|
|
||||||
func runOne(ctx context.Context, cfg Config, call llm.ToolCall) llm.Message {
|
func runOne(ctx context.Context, cfg Config, call llm.ToolCall) llm.Message {
|
||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return cancelledToolMessage(call, err)
|
return cancelledToolMessage(call, err)
|
||||||
@@ -757,6 +773,7 @@ func runOne(ctx context.Context, cfg Config, call llm.ToolCall) llm.Message {
|
|||||||
defer endSpan()
|
defer endSpan()
|
||||||
res := tools.Result{Content: "no tools configured", IsError: true}
|
res := tools.Result{Content: "no tools configured", IsError: true}
|
||||||
known := cfg.Tools != nil && cfg.Tools.Has(call.Name)
|
known := cfg.Tools != nil && cfg.Tools.Has(call.Name)
|
||||||
|
ctx = WithToolCall(ctx, call)
|
||||||
switch {
|
switch {
|
||||||
case known:
|
case known:
|
||||||
res = cfg.Tools.Execute(ctx, call.Name, json.RawMessage(call.Arguments))
|
res = cfg.Tools.Execute(ctx, call.Name, json.RawMessage(call.Arguments))
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user