Delegate unregistered tool calls when a fallback is set
Why: the direct chat path still executes model-emitted calls that are not in the profile allow-list, while squad keeps the unknown-tool error when the fallback is unset. Evidence: go test ./runtime/ passed, including TestLoopUnknownToolFallsThrough and the existing unknown-tool cases. Impact: Config.UnknownTool is optional. Nil preserves today's registry error. Refs: feature/agent-harness-p1 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+9
-1
@@ -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{
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user