google / google/adk-go

Gracefully handle invalid tool calls instead of crashing

Open
#423 1 comment 2 reactions 0 assignees View on GitHub
bug
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.