Skip to content

output_key does not capture delegated sub-agent responses #3758

@sayanb

Description

@sayanb

Bug Report: output_key Does Not Capture Delegated Sub-Agent Responses

Bug Description

When a root agent with output_key configured delegates to a sub-agent, the sub-agent's response is not saved to session state. This contradicts the official documentation which states that output_key should capture "the agent's final textual response for a turn" regardless of which agent generated it (root or delegated sub-agent).

Actual behavior: Only responses directly from the root agent are saved to state[output_key]. Delegated sub-agent responses are silently ignored.

Expected behavior: ALL final responses (including delegated ones) should be saved to state[output_key].

Steps to Reproduce

Installation

pip install google-adk==1.19.0
# OR
uv add google-adk==1.19.0

Minimal Reproduction Code

from google.adk.agents import Agent
from google.adk.sessions import InMemorySessionService
from google.adk.runners import Runner

# Create sub-agent
def say_hello():
    return "Hello, there!"

greeting_agent = Agent(
    name="greeting_agent",
    model="gemini-2.0-flash",
    tools=[say_hello]
)

# Create root agent with output_key
root_agent = Agent(
    name="root_agent",
    model="gemini-2.0-flash",
    sub_agents=[greeting_agent],
    output_key="last_response"  # Should capture ALL responses
)

# Setup session and runner
session_service = InMemorySessionService()
runner = Runner(agent=root_agent, app_name="test", session_service=session_service)

# Turn 1: Direct response from root agent
await runner.run_async("user_1", "session_1", new_message="test direct")
# ✅ Saved to state["last_response"]

# Turn 2: Delegated response from greeting_agent
await runner.run_async("user_1", "session_1", new_message="hello")
# ❌ NOT saved to state["last_response"] - BUG!

# Check final state
session = await session_service.get_session("test", "user_1", "session_1")
print(session.state["last_response"])  # Still shows Turn 1 response, not Turn 2

Execution Steps

  1. Save the code above as test_output_key.py
  2. Set GOOGLE_API_KEY environment variable
  3. Run: python test_output_key.py
  4. Observe that state["last_response"] still contains Turn 1's response
  5. Expected: Should contain Turn 2's delegated greeting response

Observed Output

After running the minimal reproduction code:

# Turn 1 output
state["last_response"] = "This is a direct response from the root agent"  # ✅ Saved

# Turn 2 output
state["last_response"] = "This is a direct response from the root agent"  # ❌ NOT updated!
# Expected: state["last_response"] = "Hello, there!" (from greeting_agent)

Issue: When the root agent delegates to greeting_agent in Turn 2, the delegated response is not saved to state["last_response"]. The state retains the old value from Turn 1 instead of being updated with the greeting.

Expected Behavior

According to the official ADK tutorial:

The output_key configuration causes the agent to "automatically save the agent's final textual response for a turn" regardless of which agent generated it—root or delegated sub-agent.

Expected state after Turn 2:

state["last_response"] = "Hello, there!"  # Should be updated with greeting_agent's response

The output_key should capture delegated sub-agent responses, not just root agent responses.

Screenshots

N/A - This is a state management issue without visual components.

System Information

  • Operating System: Linux (tested on Ubuntu/Debian)
  • Python Version:
    $ python --version
    Python 3.12
  • ADK Version:
    $ pip show google-adk
    Version: 1.19.0

Model Configuration

  • Using LiteLLM: Yes (for OpenAI models in multi-agent setup)
  • Models Used:
    • Root agent: openai/gpt-4.1 (via LiteLLM)
    • Sub-agents: gemini-2.0-flash (native Gemini)

Additional Context

Root Cause

The bug is in google/adk/agents/llm_agent.py:784-816 in the __maybe_save_output_to_state() method:

def __maybe_save_output_to_state(self, event: Event):
    """Saves the model output to state if needed."""
    # skip if the event was authored by some other agent (e.g. current agent
    # transferred to another agent)
    if event.author != self.name:  # ← BUG: Skips delegated responses
      logger.debug(
          'Skipping output save for agent %s: event authored by %s',
          self.name,
          event.author,
      )
      return  # ← Exits early, never saves delegated responses!

    # Only executes if event.author == self.name
    if (
        self.output_key
        and event.is_final_response()
        and event.content
        and event.content.parts
    ):
      result = ''.join(
          part.text
          for part in event.content.parts
          if part.text and not part.thought
      )
      event.actions.state_delta[self.output_key] = result

Why it fails: When the root agent delegates to a sub-agent, event.author contains the sub-agent's name, not the root agent's name. The check event.author != self.name evaluates to True, causing the method to return early without saving to state.

Impact

  • Severity: Medium-High
  • Affects: All multi-agent systems using output_key with agent delegation
  • Workaround: Manually save responses to state within tools using tool_context.state[key] = value instead of relying on output_key

Implementation vs Documentation Discrepancy

The code comment in the source explicitly mentions this behavior: "skip if the event was authored by some other agent (e.g. current agent transferred to another agent)" - but this directly contradicts the documentation's promise that output_key captures responses "regardless of which agent generated it."

The code comment suggests this is intentional behavior, but the official tutorial explicitly demonstrates a test case where delegated responses should be saved to state via output_key. This creates confusion and breaks documented functionality.

Related Resources

Metadata

Metadata

Labels

services[Component] This issue is related to runtime services, e.g. sessions, memory, artifacts, etc

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions