diff --git a/runtime/loop.go b/runtime/loop.go index 7c4e037..5c560a8 100644 --- a/runtime/loop.go +++ b/runtime/loop.go @@ -74,6 +74,8 @@ type Config struct { Stop func(last llm.Message, info StopInfo) StopDecision // BeforeTool 返回错误时不执行工具,但仍写一条 IsError 的 tool 结果。 BeforeTool func(call llm.ToolCall) error + // UnknownTool 处理注册表里没有的调用。nil 时仍返回 unknown tool。 + UnknownTool func(call llm.ToolCall) tools.Result // AfterTool 只观察工具结果,不能改写。 AfterTool func(call llm.ToolCall, res llm.Message) // Reminders 在每次调用模型前提供提醒。正文包在 system-reminder 里, @@ -382,7 +384,13 @@ func runOne(ctx context.Context, cfg Config, call llm.ToolCall) llm.Message { } emit(cfg, Event{Type: "tool_execution_update", Tool: call.Name, Detail: call.Arguments}) res := tools.Result{Content: "no tools configured", IsError: true} - if cfg.Tools != nil { + known := cfg.Tools != nil && cfg.Tools.Has(call.Name) + switch { + case known: + res = cfg.Tools.Execute(ctx, call.Name, json.RawMessage(call.Arguments)) + case cfg.UnknownTool != nil: + res = cfg.UnknownTool(call) + case cfg.Tools != nil: res = cfg.Tools.Execute(ctx, call.Name, json.RawMessage(call.Arguments)) } msg := llm.Message{ diff --git a/runtime/loop_harness_test.go b/runtime/loop_harness_test.go index a4fadcd..6c32c2d 100644 --- a/runtime/loop_harness_test.go +++ b/runtime/loop_harness_test.go @@ -275,6 +275,33 @@ func TestLoopHistorySkipsGeneratedSystem(t *testing.T) { } } +func TestLoopUnknownToolFallsThrough(t *testing.T) { + res, err := Run(context.Background(), Config{ + Model: "fake", + UnknownTool: func(call llm.ToolCall) tools.Result { + return tools.Result{Content: "via " + call.Name} + }, + Stream: script( + llm.Message{Role: llm.RoleAssistant, StopReason: llm.StopToolCalls, ToolCalls: []llm.ToolCall{{ + ID: "c1", Name: "make_report", Arguments: `{}`, + }}}, + llm.Message{Role: llm.RoleAssistant, Content: "done", StopReason: llm.StopEnd}, + ), + }, llm.Message{Role: llm.RoleUser, Content: "go"}) + if err != nil { + t.Fatal(err) + } + found := false + for _, m := range res.Messages { + if m.Role == llm.RoleTool && m.Content == "via make_report" && !m.IsError { + found = true + } + } + if !found { + t.Fatalf("unknown tool was not delegated: %s", transcriptText(res.Messages)) + } +} + func TestLoopTurnCapKeepsTranscript(t *testing.T) { res, err := Run(context.Background(), Config{ Model: "fake",