Gracefully handle invalid tool calls instead of crashing
- Dominant language
- Go
- Stars
- 8.8k
- Forks
- 1k
- Avg merge
- 3d 18h
- Merged PRs (30d)
- 88
Description
**Describe the bug**
When an LLM calls a tool that is not registered with the current agent, the ADK crashes with a nil pointer dereference instead of gracefully handling the error. The `loopagent` accesses `event.Actions.Escalate` without checking if `event` is nil after an error is returned.
**To Reproduce**
Minimal reproduction code:
```go
package main
import (
"context"
"fmt"
"google.golang.org/adk/agent"
"google.golang.org/adk/agent/llmagent"
"google.golang.org/adk/agent/workflowagents/loopagent"
"google.golang.org/adk/model/gemini"
"google.golang.org/adk/runner"
"google.golang.org/adk/session"
"google.golang.org/adk/tool"
"google.golang.org/adk/tool/functiontool"
"google.golang.org/genai"
)
func main() {
ctx := context.Background()
// Create a simple tool
readFile, _ := functiontool.New(
functiontool.Config{
Name: "read_file",
Description: "Read a file",
},
func(ctx tool.Context, args struct{ Path string }) (string, error) {
return "file content", nil
},
)
// Create model
model, _ := gemini.New(ctx, "gemini-2.0-flash")
// Create agent with only read_file tool
// But system prompt mentions "apply_change" which doesn't exist
myAgent, _ := llmagent.New(llmagent.Config{
Name: "test-agent",
Description: "Test agent",
Model: model,
Instruction: `You have access to: read_file and apply_change tools.
Use apply_change to modify files.`, // Mentions non-existent tool
Tools: []tool.Tool{readFile}, // Only read_file is registered
})
// Wrap in loop agent
loopAgent := loopagent.New(loopagent.Config{
MaxIterations: 5,
SubAgents: []agent.Agent{myAgent},
})
// Create runner
sessionSvc := session.InMemoryService()
r, _ := runner.New(runner.Config{
AppName: "test",
Agent: loopAgent,
SessionService: sessionSvc,
})
sessionSvc.Create(ctx, &session.CreateRequest{
AppName: "test",
UserID: "user",
SessionID: "session1",
})
message := &genai.Content{
Role: "user",
Parts: []*genai.Part{genai.NewPartFromText("Please modify the config file")},
}
// This will panic when LLM tries to call "apply_change"
for event, err := range r.Run(ctx, "user", "session1", message, agent.RunConfig{}) {
if err != nil {
fmt.Printf("Error: %v\n", err)
}
if event != nil {
fmt.Printf("Event: %s\n", event.Author)
}
}
}
```
Steps to reproduce:
1. Install ADK: `go get google.golang.org/adk@v0.2.0`
2. Run the above code
3. When the LLM generates a function call for `apply_change` (which is mentioned in the prompt but not registered), the application panics
Error/stacktrace:
```
[ERROR] unknown tool: "apply_change"
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV code=0x1 addr=0xf0 pc=0x7ff683ffb421]
goroutine 1 [running]:
google.golang.org/adk/agent/workflowagents/loopagent.(*loopAgent).Run-fm.(*loopAgent).Run.func1-range1(0x0, {0x7ff6849d9b60?, 0xc000b9fd40?})
google.golang.org/adk@v0.2.0/agent/workflowagents/loopagent/agent.go:88 +0x61
google.golang.org/adk/agent.(*agent).Run.func1-range1(0xc000191c80?, {0x7ff6849d9b60?, 0xc000b9fd40?})
google.golang.org/adk@v0.2.0/agent/agent.go:189 +0xd2
google.golang.org/adk/agent/llmagent.(*llmAgent).run.func1-range1(0x0, {0x7ff6849d9b60, 0xc000b9fd40})
google.golang.org/adk@v0.2.0/agent/llmagent/llmagent.go:345 +0x8f
...
```
**Expected behavior**
When an LLM calls an unknown/unregistered tool, the ADK should:
1. Return an error response to the LLM indicating the tool doesn't exist
2. Allow the LLM to retry with a valid tool
3. Continue agent execution normally (not crash)
**Screenshots**
N/A
**Desktop (please complete the following information):**
- OS: Ubuntu 22.04.3 LTS (WSL2, kernel 6.6.87.2-microsoft-standard-WSL2)
- Go version: go1.25.1 linux/amd64
- ADK version: v0.2.0
**Model Information:**
- gemini-2.0-flash (but issue is model-agnostic - happens with any model that calls an unknown tool)
**Additional context**
**Root Cause:**
The bug is in `agent/workflowagents/loopagent/agent.go` at line 88:
```go
for event, err := range subAgent.Run(ctx) {
// TODO: ensure consistency -- if there's an error, return and close iterator...
if !yield(event, err) {
return
}
if event.Actions.Escalate { // LINE 88 - PANIC: event is nil when err != nil
shouldExit = true
}
}
```
When `handleFunctionCalls()` in `base_flow.go` detects an unknown tool, it returns `(nil, error)`. The error is yielded correctly, but then the code tries to access `event.Actions.Escalate` without checking if `event` is nil.
Note: There's already a TODO comment on line 83 acknowledging this needs to be fixed.
**Suggested Fix:**
```go
for event, err := range subAgent.Run(ctx) {
if !yield(event, err) {
return
}
if err != nil {
shouldExit = true
break
}
if event != nil && event.Actions.Escalate {
shouldExit = true
}
}
```
**Impact:**
This affects any application where:
- LLMs might hallucinate tool names
- Multi-agent systems have different tools per agent
- System prompts mention tools that aren't always registered
The crash is unrecoverable and terminates the entire agent execution.
Contributor guide
Research direction
Start in agent/workflowagents/loopagent/agent.go at line 88 and inspect how errors and nil events are handled after subAgent.Run. Check the unknown-tool error path in base_flow.go and reproduce it with the provided example. Done means an unregistered tool returns an error response, avoids a panic, and allows normal agent handling to continue.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- ai-infra-agents
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Stale
- Clarity
- Clearly specified
- Newbie friendliness
- 58/100