Repository navigation
llmagent: a task agent that finishes with a null result never returns it to the coordinator聽#1738
Description
馃敶 Required Information
Describe the Bug:
A task-mode agent whose OutputSchema is a nullable non-object type can finish with a null result, and when it does, the coordinator never receives the result and is never resumed. No error is reported.
When the task agent's OutputSchema is not an object, finish_task wraps it under a required result key (finish_task_tool.go#L76-L83). Since #1566, a schema with Nullable: true accepts finish_task({"result": null}), and the task model is told "Task completed.". runTask then copies that nil into the event's output (llm_agent_wrapper.go#L704-L706). The coordinator treats a nil output as "the task did not finish" and returns without a function response (llm_agent_wrapper.go#L597-L618), so the delegation stays open.
Links are pinned to main at 65761912.
Steps to Reproduce:
- Save the test below as
agent/llmagent/repro_null_task_result_test.go. It uses the package's existingscriptedLLMtest helper. - Run
go test -count=1 -run TestReproNullableTaskResultClosesDelegation -v ./agent/llmagent/.
Reproduction test
package llmagent_test
import (
"testing"
"google.golang.org/genai"
"google.golang.org/adk/v2/agent"
"google.golang.org/adk/v2/agent/llmagent"
"google.golang.org/adk/v2/model"
"google.golang.org/adk/v2/runner"
"google.golang.org/adk/v2/session"
)
func fcTurn(id, name string, args map[string]any) *model.LLMResponse {
return &model.LLMResponse{Content: &genai.Content{Role: "model", Parts: []*genai.Part{{
FunctionCall: &genai.FunctionCall{ID: id, Name: name, Args: args},
}}}}
}
// A task agent whose OutputSchema is a nullable string finishes with null.
// The delegation should close (the coordinator receives a FunctionResponse).
func TestReproNullableTaskResultClosesDelegation(t *testing.T) {
taskLLM := &scriptedLLM{turns: []*model.LLMResponse{
fcTurn("finish-1", "finish_task", map[string]any{"result": nil}),
// Only consumed if the null call is rejected and the model retries.
fcTurn("finish-2", "finish_task", map[string]any{"result": "retry-value"}),
}}
task, err := llmagent.New(llmagent.Config{
Name: "doer", Description: "d", Model: taskLLM, Mode: llmagent.ModeTask,
OutputSchema: &genai.Schema{Type: genai.TypeString, Nullable: genai.Ptr(true)},
})
if err != nil {
t.Fatal(err)
}
coordLLM := &scriptedLLM{turns: []*model.LLMResponse{
fcTurn("fc-1", "doer", map[string]any{"request": "x"}),
{Content: &genai.Content{Role: "model", Parts: []*genai.Part{genai.NewPartFromText("coordinator resumed")}}},
}}
coord, err := llmagent.New(llmagent.Config{Name: "coord", Model: coordLLM, Mode: llmagent.ModeChat, SubAgents: []agent.Agent{task}})
if err != nil {
t.Fatal(err)
}
svc := session.InMemoryService()
if _, err := svc.Create(t.Context(), &session.CreateRequest{AppName: "app", UserID: "u", SessionID: "s"}); err != nil {
t.Fatal(err)
}
r, err := runner.New(runner.Config{Agent: coord, SessionService: svc, AppName: "app"})
if err != nil {
t.Fatal(err)
}
var delegationFR *genai.FunctionResponse
for ev, err := range r.Run(t.Context(), "u", "s", genai.NewContentFromText("go", "user"), agent.RunConfig{}) {
if err != nil {
t.Fatalf("run: %v", err)
}
if ev.Content == nil {
continue
}
for _, p := range ev.Content.Parts {
if p.FunctionCall != nil {
t.Logf("author=%s FC %s args=%v", ev.Author, p.FunctionCall.Name, p.FunctionCall.Args)
}
if p.FunctionResponse != nil {
t.Logf("author=%s FR %s resp=%v", ev.Author, p.FunctionResponse.Name, p.FunctionResponse.Response)
if p.FunctionResponse.Name == "doer" {
delegationFR = p.FunctionResponse
}
}
if p.Text != "" {
t.Logf("author=%s text=%q", ev.Author, p.Text)
}
}
}
t.Logf("coordinator LLM calls=%d task LLM calls=%d", coordLLM.callIdx, taskLLM.callIdx)
if delegationFR == nil {
t.Errorf("no FunctionResponse closed the delegation to doer")
}
}Expected Behavior:
Any finish_task call that passes validation closes the delegation: the coordinator receives a function response for the task agent and is resumed. A null result should either be delivered to the coordinator as null, or be rejected so the model retries. Which of the two is right is part of the fix.
Observed Behavior:
On main at 6576191:
author=coord FC doer args=map[request:x]
author=doer FC finish_task args=map[result:<nil>]
author=doer FR finish_task resp=map[result:Task completed.]
coordinator LLM calls=1 task LLM calls=1
no FunctionResponse closed the delegation to doer
--- FAIL: TestReproNullableTaskResultClosesDelegation
Environment Details:
- ADK Library Version: main at 6576191 (
google.golang.org/adk/v2) - OS: Linux
- Go Version: 1.26.6
Model Information:
- Which model is being used: N/A (scripted test model)
馃煛 Optional Information
Regression:
Yes. Before #1566, a nullable schema still rejected null, so the model retried with a non-null value and the coordinator received it. #1566 is a correct fix for nullable fields in general. This is a path it did not cover.
Additional context:
Nullable: false has no test either: treating it like Nullable: true in internal/utils passes the whole suite. A test pinning that case would be a useful addition alongside the fix.
Activity
dhakad22klx commentedon Oct 5, 2026
Hi @karolpiotrowicz, i would like to take this issue. Please assign.
dhakad22klx commentedon Oct 6, 2026
Hi @Vivek1106-04 , if you could have dropped a comment that you gonna raise PR - it could be better. Fine tho.