Skip to content

message_as_output=True unconditionally set, destroying structured event.output for single_turn agents with output_schema #6089

@wi5nuu

Description

@wi5nuu

🔴 Required Information

Describe the Bug

When a single_turn LlmAgent is configured with an output_schema, the validated structured output is lost before it reaches downstream consumers.

The issue occurs because process_llm_agent_output() in _llm_agent_wrapper.py unconditionally sets:

event.node_info.message_as_output = True

even when the agent is producing structured output through output_schema.

Later, _consume_event_queue() in runners.py interprets message_as_output=True as a signal that the message content itself represents the node output and clears event.output to avoid duplication.

As a result, the structured output stored in event.output is overwritten with None.

The implementation also appears to contradict the documented intent in _consume_event_queue(), whose comments explicitly describe message_as_output as applying only to the "no output_schema" case.

Steps to Reproduce

  1. Create an LlmAgent with:

    • mode="single_turn" (or use it as a workflow node)
    • an output_schema based on a Pydantic model
  2. Execute the agent through a runner instance:

runner = InMemoryRunner(agent=agent)

events = await runner.run_async(...)

or:

runner = Runner(...)

events = await runner.run_async(...)
  1. Inspect the emitted events.

  2. Observe that:

event.output

is always None, even when the model successfully produces valid structured output.

Expected Behavior

When an output_schema is configured and validation succeeds:

event.output

should contain the validated structured result.

Downstream consumers should be able to access the structured output through the event stream.

Observed Behavior

For single_turn agents using output_schema:

event.output

is always None.

The structured output is generated correctly but is later removed by _consume_event_queue() because message_as_output was set unconditionally.

Environment Details

  • ADK Library Version: Latest main branch
  • Desktop OS: N/A (identified via static code analysis)
  • Python Version: N/A

Model Information

  • Using LiteLLM: No
  • Model: Any model used with output_schema

🟡 Optional Information

Additional Context

The issue involves two locations.

1. src/google/adk/workflow/_llm_agent_wrapper.py

process_llm_agent_output() sets:

event.output = output
event.node_info.message_as_output = True

The flag is enabled regardless of whether the output is plain text or a validated structured result.

2. src/google/adk/runners.py

_consume_event_queue() contains logic similar to:

if not event.partial:
    if event.node_info.message_as_output and event.content is not None:
        event = event.model_copy()
        event.output = None

This removes the structured output before the event is returned.

Documentation / Implementation Mismatch

The comment in _consume_event_queue() (approximately lines 788–794) states:

When an LlmAgent node uses message_as_output (no output_schema), the wrapper sets both event.content and event.output to the same text. event.output is cleared here to avoid rendering the same value twice.

This comment explicitly limits the behavior to the no output_schema case.

However, the current implementation sets message_as_output = True regardless of whether an output_schema exists.

Suggested Fix

Only mark the message as the output when no structured output schema is configured:

event.output = output

if not agent.output_schema:
    event.node_info.message_as_output = True

Minimal Reproduction

from pydantic import BaseModel
from google.adk.agents import LlmAgent
from google.adk.runners import InMemoryRunner

class WeatherOutput(BaseModel):
    temperature: float
    condition: str

agent = LlmAgent(
    name="weather_agent",
    model="gemini-2.0-flash",
    output_schema=WeatherOutput,
    instruction="Return weather data as structured output.",
)

runner = InMemoryRunner(agent=agent)

events = await runner.run_async(
    user_id="test",
    session_id="test",
    new_message=types.Content(
        parts=[types.Part(text="What is the weather in Tokyo?")]
    ),
)

for event in events:
    print(event.output)  # Always None

Frequency

  • Always reproducible (100%)

Metadata

Metadata

Assignees

Labels

core[Component] This issue is related to the core interface and implementation

Type

No fields configured for Bug.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions