Skip to content

A2A sub-agent completion ends parent agent flow without returning to LLM #5977

@DamienJScott

Description

@DamienJScott

🔴 Required Information

Describe the Bug:
When using RemoteA2aAgent registered as a sub_agent on an LlmAgent, the parent agent's flow terminates immediately after the A2A sub-agent completes, without returning control to the parent LLM for summarization or further processing. According to the ADK A2A documentation (adk.dev/a2a/intro/), interacting with a remote agent should feel like "interacting with a local tool or function" where "the parent agent maintains full control" and "responses are returned as standard tool/function call return values." However, the actual behavior is that the LLM wrapper's chat-mode loop detects event.actions.transfer_to_agent and sets transferred = True, causing the outer loop to return immediately instead of re-entering the LLM with the sub-agent's result.

Steps to Reproduce:

  1. Create an LlmAgent with a RemoteA2aAgent registered via sub_agents=[remote_agent]
  2. Ensure the parent LLM's system prompt instructs it to use transfer_to_agent when it needs to delegate to the sub-agent
  3. Run the parent agent with a query that triggers the transfer_to_agent call
  4. Observe that after the A2A sub-agent completes and returns its response, the entire flow ends — the parent LLM never gets another turn to process or summarize the sub-agent's output

Expected Behavior:
After the A2A sub-agent completes, control should return to the parent LLM with the sub-agent's result as a FunctionResponse (similar to how task delegation via AgentTool works). The parent LLM should then see the result and be able to summarize, analyze, or continue the conversation based on the sub-agent's output. This matches the ADK A2A documentation: "Parent LLM sees results as regular Tool Output."

Observed Behavior:
The flow terminates immediately after the A2A sub-agent completes. The _llm_agent_wrapper.py chat-mode loop (run_llm_agent_as_node) detects event.actions.transfer_to_agent and:

  1. Sets transferred = True
  2. Breaks the inner loop
  3. Outer loop checks if not had_task_fc or transferred: return
  4. Returns immediately — the parent LLM never re-enters to process the result

Environment Details:

  • ADK Library Version: 2.0 (latest as of 2026-06-05)
  • Desktop OS: Windows
  • Python Version: 3.13

Model Information:

  • Are you using LiteLLM: Yes
  • Which model is being used: qwen 3.6 plus (via Model Gateway)

🟡 Optional Information

Regression:
N/A — This appears to be the intended behavior of transfer_to_agent (scheduler-level routing), but it conflicts with the A2A documentation which describes sub-agents as tool-like call-and-return interactions. This is the first version of ADK with A2A support that we have tested.

Logs:

# Source code analysis from _llm_agent_wrapper.py (lines ~415-445):

while True:
    had_task_fc = False
    transferred = False
    run_method = agent.run_live(ic) if is_live else agent.run_async(ic)
    async with aclosing(run_method) as run_iter:
        async for event in run_iter:
            yield event
            task_fcs = _extract_task_delegation_fcs(event, tools_dict)
            for fc in task_fcs:
                output = await _dispatch_task_fc(agent, fc, ctx)
                yield _synthesize_task_fr_event(fc, output)
            if task_fcs:
                had_task_fc = True
                break
            if event.actions.transfer_to_agent:
                target_name = event.actions.transfer_to_agent
                if target_name != agent.name:
                    transferred = True
                    break
    if not had_task_fc or transferred:
        # LLM finished without delegating (or transferred away);
        # nothing more for this wrapper to do.
        return
    # Otherwise: loop back to re-enter agent.run_async so the LLM
    # sees the synthesized FR(s) and can emit follow-up actions.

Screenshots / Video:
N/A — This is a control flow issue, not a visual/rendering bug.

Additional Context:
The root cause is a semantic mismatch between transfer_to_agent and the documented A2A call-and-return model:

  • transfer_to_agent is designed as a scheduler routing signal (handoff control to another agent permanently)
  • A2A sub-agents are documented as call-and-return (like a tool call, parent resumes after result)

The _dynamic_node_scheduler.py while True loop correctly handles transfer_to_agent by routing to the target agent and continuing. However, _llm_agent_wrapper.py's outer loop interprets transferred = True as "flow is done, return to caller." This works for true handoff scenarios but breaks the A2A use case where the parent should see the result and continue.

For contrast, AgentTool (wrapping a sub-agent as a function tool) correctly returns a FunctionResponse to the LLM, which then autonomously decides next steps — this is the behavior described in the A2A docs.

Minimal Reproduction Code:

from google.adk.agents.llm_agent import LlmAgent
from google.adk.agents.remote_a2a_agent import RemoteA2aAgent
from google.adk.runners import Runner
from google.adk.sessions.in_memory_session_service import InMemorySessionService

# Create a remote A2A agent
remote_agent = RemoteA2aAgent(
    name="sub_agent",
    agent_card="http://localhost:8080/.well-known/agent-card.json",
    description="A test sub-agent",
)

# Create parent LlmAgent with sub_agent
parent_agent = LlmAgent(
    name="parent_agent",
    model=model,
    instruction="You are a helpful assistant. When you need help, transfer to the sub_agent.",
    tools=[],
    sub_agents=[remote_agent],
)

runner = Runner(
    app_name="test",
    agent=parent_agent,
    session_service=InMemorySessionService(),
)

# Run — after sub_agent completes, flow ends immediately
async for event in runner.run_async(
    user_id="test",
    session_id="test-session",
    new_message=new_message,
):
    print(event)
    if event.is_final_response():
        # This fires after sub_agent completes,
        # but parent LLM never got to see the result
        break

How often has this issue occurred?:

  • Always (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