Delegate unregistered tool calls when a fallback is set
ci / vet-and-test (pull_request) Failing after 2m48s
ci / docker-checkout-hostgateway (pull_request) Failing after 31s

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:
2026-10-01 13:27:36 +00:00
co-authored by Cursor
parent e2815bbf94
commit a34689c78f
2 changed files with 36 additions and 1 deletions
+9 -1
View File
@@ -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{
+27
View File
@@ -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",