Skip to content

Commit a4d5bfe

Browse files
committed
Return error response for unknown tool calls instead of silently skipping
Two improvements: 1. Bug fix - unknown tool calls get an error response instead of being silently dropped. On main, when the model calls a tool that doesn't exist (the else branch), the call is skipped with no tool response message added to the session. This leaves a tool_call without a matching tool_response, which violates the LLM API contract and can cause errors or confused model behavior on the next turn. This commonly happens after a handoff: agent A sees tool calls from agent B in the conversation history and tries to use them. Now it gets a clear error telling it the tool isn't available. 2. Simpler structure - one availability check instead of two separate rejection paths. On main, the availability logic has three branches with two different rejection paths. The new code separates this into two sequential steps: check agentToolMap once (reject if missing), then pick the handler. This removes the nested if, deduplicates the rejection logic, and makes the flow read top-down: is it available ‚Üí how do we run it ‚Üí run it. Assisted-By: cagent
1 parent a1ae6ce commit a4d5bfe

File tree

2 files changed

+24
-33
lines changed

2 files changed

+24
-33
lines changed

pkg/runtime/runtime.go

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1437,29 +1437,27 @@ func (r *LocalRuntime) processToolCalls(ctx context.Context, sess *session.Sessi
14371437

14381438
slog.Debug("Processing tool call", "agent", a.Name(), "tool", toolCall.Function.Name, "session_id", sess.ID)
14391439

1440-
// Find the tool - first check runtime tools, then agent tools
1441-
var tool tools.Tool
1442-
var runTool func()
1440+
// Resolve the tool: it must be in the agent's tool set to be callable.
1441+
// After a handoff the model may hallucinate tools it saw in the
1442+
// conversation history from a previous agent; rejecting unknown
1443+
// tools with an error response lets it self-correct.
1444+
tool, available := agentToolMap[toolCall.Function.Name]
1445+
if !available {
1446+
slog.Warn("Tool call for unavailable tool", "agent", a.Name(), "tool", toolCall.Function.Name, "session_id", sess.ID)
1447+
errTool := tools.Tool{Name: toolCall.Function.Name}
1448+
r.addToolErrorResponse(ctx, sess, toolCall, errTool, events, a, fmt.Sprintf("Tool '%s' is not available. You can only use the tools provided to you.", toolCall.Function.Name))
1449+
callSpan.SetStatus(codes.Error, "tool not available")
1450+
callSpan.End()
1451+
continue
1452+
}
14431453

1454+
// Pick the handler: runtime-managed tools (transfer_task, handoff)
1455+
// have dedicated handlers; everything else goes through the toolset.
1456+
var runTool func()
14441457
if def, exists := r.toolMap[toolCall.Function.Name]; exists {
1445-
// Validate that the tool is actually available to this agent
1446-
if _, available := agentToolMap[toolCall.Function.Name]; !available {
1447-
slog.Warn("Tool call rejected: tool not available to agent", "agent", a.Name(), "tool", toolCall.Function.Name, "session_id", sess.ID)
1448-
r.addToolErrorResponse(ctx, sess, toolCall, def.tool, events, a, fmt.Sprintf("Tool '%s' is not available to this agent (%s).", toolCall.Function.Name, a.Name()))
1449-
callSpan.SetStatus(codes.Error, "tool not available to agent")
1450-
callSpan.End()
1451-
continue
1452-
}
1453-
tool = def.tool
1454-
runTool = func() { r.runAgentTool(callCtx, def.handler, sess, toolCall, def.tool, events, a) }
1455-
} else if t, exists := agentToolMap[toolCall.Function.Name]; exists {
1456-
tool = t
1457-
runTool = func() { r.runTool(callCtx, t, toolCall, events, sess, a) }
1458+
runTool = func() { r.runAgentTool(callCtx, def.handler, sess, toolCall, tool, events, a) }
14581459
} else {
1459-
// Tool not found - skip
1460-
callSpan.SetStatus(codes.Ok, "tool not found")
1461-
callSpan.End()
1462-
continue
1460+
runTool = func() { r.runTool(callCtx, tool, toolCall, events, sess, a) }
14631461
}
14641462

14651463
// Execute tool with approval check

pkg/runtime/runtime_test.go

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -862,44 +862,37 @@ func TestSummarize_EmptySession(t *testing.T) {
862862
require.Contains(t, warningMsg, "empty", "warning message should mention empty session")
863863
}
864864

865-
func TestProcessToolCalls_UnknownTool_NoToolResultMessage(t *testing.T) {
866-
// Build a runtime with a simple agent but no tools registered matching the call
865+
func TestProcessToolCalls_UnknownTool_ReturnsErrorResponse(t *testing.T) {
867866
root := agent.New("root", "You are a test agent", agent.WithModel(&mockProvider{}))
868867
tm := team.New(team.WithAgents(root))
869868

870869
rt, err := NewLocalRuntime(tm, WithSessionCompaction(false), WithModelStore(mockModelStore{}))
871870
require.NoError(t, err)
872-
873-
// Register default tools (contains only transfer_task) to ensure unknown tool isn't matched
874871
rt.registerDefaultTools()
875872

876873
sess := session.New(session.WithUserMessage("Start"))
877874

878-
// Simulate a model-issued tool call to a non-existent tool
879875
calls := []tools.ToolCall{{
880876
ID: "tool-unknown-1",
881877
Type: "function",
882878
Function: tools.FunctionCall{Name: "non_existent_tool", Arguments: "{}"},
883879
}}
884880

885881
events := make(chan Event, 10)
886-
887-
// No agentTools provided and runtime toolMap doesn't have this tool name
888882
rt.processToolCalls(t.Context(), sess, calls, nil, events)
889-
890-
// Drain events channel
891883
close(events)
892884
for range events {
893885
}
894886

895-
var sawToolMsg bool
887+
// The model must receive an error tool response so it can self-correct.
888+
var toolContent string
896889
for _, it := range sess.Messages {
897890
if it.IsMessage() && it.Message.Message.Role == chat.MessageRoleTool && it.Message.Message.ToolCallID == "tool-unknown-1" {
898-
sawToolMsg = true
899-
break
891+
toolContent = it.Message.Message.Content
900892
}
901893
}
902-
require.False(t, sawToolMsg, "no tool result should be added for unknown tool; this reproduces invalid sequencing state")
894+
require.NotEmpty(t, toolContent, "expected an error tool response for unknown tools")
895+
assert.Contains(t, toolContent, "not available")
903896
}
904897

905898
func TestEmitStartupInfo(t *testing.T) {

0 commit comments

Comments
 (0)