Skip to content

Agent Handoff Behavior with transfer_to_agent (Runner & BaseAgent/LlmAgent) #714

@sherifkozman

Description

@sherifkozman

We're working on a multi-agent application using ADK v0.5.0 and have encountered some ambiguity regarding the expected behavior of agent handoffs, specifically when using actions.transfer_to_agent with the ADK Runner and PostgresSessionService.

Our Scenario & Observations:

We set up a minimal test case:

  1. A root agent (MinimalTransferOrchestratorDebug, a BaseAgent) is configured in the Runner.
  2. Upon receiving its first user message, this orchestrator yields a single Event. The Event.actions has transfer_to_agent set to the name of a target sub-agent (e.g., "CustomUserInfoAgent_NativeLLM").
  3. The target sub-agent is correctly listed in the root agent's sub_agents attribute during initialization.

Observed Behavior (via backend logs):

  • The MinimalTransferOrchestratorDebug runs its turn and successfully yields the event with the transfer_to_agent action.
  • Our application code (which wraps the Runner) logs that the Runner processed this event and recognized the transfer_to_agent request.
  • However, the target agent (CustomUserInfoAgent_NativeLLM) is not automatically invoked by the Runner in the same run_async() cycle. The run_async() call completes after processing the orchestrator's event.
  • If a second message is sent from the client (triggering a new run_async() call), the Runner's _find_agent_to_run method still selects the original orchestrator, not the target agent. This is because _find_agent_to_run appears to primarily use the author of the last non-user event to determine the next agent, and it doesn't seem to directly act on a transfer_to_agent value stored in a previous event's actions within the session history for BaseAgents.
  • We also tried setting the author of the transfer event to the target agent's name. This also did not result in the target agent running on the next turn; the _find_agent_to_run logic still defaulted to the root agent because the target (a BaseAgent) failed the _is_transferable_across_agent_tree check (which expects LlmAgent).
  • Modifying the target agent to be an LlmAgent still did not result in an automatic chained invocation by the Runner in the same turn.

This leads to our E2E tests timing out, as they expect the target agent to respond immediately after the transfer is signaled by the orchestrator.

Our Current Understanding/Hypothesis:
Based on documentation review (Multi-Agent Systems page) and preliminary Runner source code inspection (runners.py), it seems:

  • transfer_to_agent (when set in EventActions by a BaseAgent) primarily serves to record the intent for transfer in the session.
  • The Runner does not automatically chain execution to this new target agent within the same run_async() call that processed the transfer event.
  • The logic in _find_agent_to_run for determining the next agent to execute in a new run_async() call relies on event.author and specific conditions (like _is_transferable_across_agent_tree for LlmAgents) and may not directly use a transfer_to_agent from a previous event's actions to pick the next agent if the author doesn't align with these conditions.

Questions for Clarification:

  1. What is the precise intended mechanism by which the Runner should pick up and execute an agent specified in a previous event's actions.transfer_to_agent when the next run_async() call occurs (for both BaseAgent and LlmAgent targets)?
  2. Does the Runner support automatic chained invocation (i.e., the target agent runs immediately after the transferring agent's turn, within the same initial run_async() context/loop) when transfer_to_agent is used by a BaseAgent? Or is a new external stimulus/run_async() call always required?
  3. If a new run_async() call is required, how does _find_agent_to_run correctly identify the agent specified in the prior transfer_to_agent if the last event's author is still the transferring agent (which might not pass _is_transferable_across_agent_tree if it's a BaseAgent)?
  4. What is the recommended pattern for a BaseAgent (acting as an orchestrator) to reliably hand off control to another BaseAgent (a sub-agent) such that the sub-agent runs immediately without requiring an intervening user message? Is direct invocation (target_agent._run_async_impl(ctx)) from the orchestrator the only way, or is there a Runner-mediated approach?

We are trying to achieve a seamless user experience where sub-tasks handled by different agents can flow automatically. Any guidance on the canonical ADK patterns for this, especially concerning BaseAgent orchestration and the Runner's role in transfer_to_agent, would be immensely helpful.

Thank you!

Metadata

Metadata

Assignees

Labels

question[Component] This issue is asking a question or clarification

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions