Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 18 additions & 20 deletions pkg/runtime/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -1437,29 +1437,27 @@ func (r *LocalRuntime) processToolCalls(ctx context.Context, sess *session.Sessi

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

// Find the tool - first check runtime tools, then agent tools
var tool tools.Tool
var runTool func()
// Resolve the tool: it must be in the agent's tool set to be callable.
// After a handoff the model may hallucinate tools it saw in the
// conversation history from a previous agent; rejecting unknown
// tools with an error response lets it self-correct.
tool, available := agentToolMap[toolCall.Function.Name]
if !available {
slog.Warn("Tool call for unavailable tool", "agent", a.Name(), "tool", toolCall.Function.Name, "session_id", sess.ID)
errTool := tools.Tool{Name: toolCall.Function.Name}
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))
callSpan.SetStatus(codes.Error, "tool not available")
callSpan.End()
continue
}

// Pick the handler: runtime-managed tools (transfer_task, handoff)
// have dedicated handlers; everything else goes through the toolset.
var runTool func()
if def, exists := r.toolMap[toolCall.Function.Name]; exists {
// Validate that the tool is actually available to this agent
if _, available := agentToolMap[toolCall.Function.Name]; !available {
slog.Warn("Tool call rejected: tool not available to agent", "agent", a.Name(), "tool", toolCall.Function.Name, "session_id", sess.ID)
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()))
callSpan.SetStatus(codes.Error, "tool not available to agent")
callSpan.End()
continue
}
tool = def.tool
runTool = func() { r.runAgentTool(callCtx, def.handler, sess, toolCall, def.tool, events, a) }
} else if t, exists := agentToolMap[toolCall.Function.Name]; exists {
tool = t
runTool = func() { r.runTool(callCtx, t, toolCall, events, sess, a) }
runTool = func() { r.runAgentTool(callCtx, def.handler, sess, toolCall, tool, events, a) }
} else {
// Tool not found - skip
callSpan.SetStatus(codes.Ok, "tool not found")
callSpan.End()
continue
runTool = func() { r.runTool(callCtx, tool, toolCall, events, sess, a) }
}

// Execute tool with approval check
Expand Down
19 changes: 6 additions & 13 deletions pkg/runtime/runtime_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -862,44 +862,37 @@ func TestSummarize_EmptySession(t *testing.T) {
require.Contains(t, warningMsg, "empty", "warning message should mention empty session")
}

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

rt, err := NewLocalRuntime(tm, WithSessionCompaction(false), WithModelStore(mockModelStore{}))
require.NoError(t, err)

// Register default tools (contains only transfer_task) to ensure unknown tool isn't matched
rt.registerDefaultTools()

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

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

events := make(chan Event, 10)

// No agentTools provided and runtime toolMap doesn't have this tool name
rt.processToolCalls(t.Context(), sess, calls, nil, events)

// Drain events channel
close(events)
for range events {
}

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

func TestEmitStartupInfo(t *testing.T) {
Expand Down