Skip to content

llmagent: a task agent that finishes with a null result never returns it to the coordinator聽#1738

Open

Description

@karolpiotrowicz

馃敶 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:

  1. Save the test below as agent/llmagent/repro_null_task_result_test.go. It uses the package's existing scriptedLLM test helper.
  2. 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

  1. dhakad22klx commented on Oct 5, 2026

    @dhakad22klx
    Contributor

    Hi @karolpiotrowicz, i would like to take this issue. Please assign.

  2. dhakad22klx commented on Oct 6, 2026

    @dhakad22klx
    Contributor

    Hi @Vivek1106-04 , if you could have dropped a comment that you gonna raise PR - it could be better. Fine tho.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workinghelp wantedExtra attention is needed

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions