Skip to content

[Python][Live] transfer_to_agent is ignored when its function response is not content.parts[0] #6541

Description

@haydeem2

Bug Description:

In Live mode (BaseLlmFlow.run_live()), agent transfer is gated on whether
transfer_to_agent is the first function response in event.content.parts[0].

When the model issues parallel function calls and a non-transfer tool's
response is merged first, ADK:

  1. Executes both tools successfully
  2. Merges both function responses into one event
  3. Correctly sets event.actions.transfer_to_agent on the merged event
  4. Does not start the child agent, because the transfer gate only inspects
    parts[0]

Reversing the merged response order (transfer first) makes transfer work. A lone
transfer_to_agent call also works. This is an order-dependent Live transfer
bug, not a failure to execute or merge parallel tools.

The non-Live async path uses function_response_event.actions.transfer_to_agent
directly and does not exhibit this parts[0] dependency.

Steps to Reproduce:

  1. Install ADK 1.34.0 and download the reproducer (see Minimal Reproduction Code
    below).
  2. Run python adk_live_transfer_order_repro.py.
  3. Observe Layer 1: merged event has parts[0]=set_state,
    parts[1]=transfer_to_agent, and actions.transfer_to_agent set — but the
    stock predicate misses the transfer.
  4. Observe Layer 2: with [set_state, transfer_to_agent] parallel order, the
    child agent does not start (XFAIL on stock ADK 1.34.0).
  5. Re-run mentally / via script with reversed order [transfer_to_agent, set_state] — child starts (PASS).

Expected Behavior:

Transfer should be order-independent: if the merged event contains a
transfer_to_agent function response andevent.actions.transfer_to_agent
names a target, the child agent should start exactly once.

Merged response orderExpected
[set_state, transfer_to_agent]Child starts once
[transfer_to_agent, set_state]Child starts once
lone transfer_to_agentChild starts once
parallel non-transfer toolsNo child starts

Observed Behavior:

Merged response orderStock ADK 1.34.0
[set_state, transfer_to_agent]Child does not start
[transfer_to_agent, set_state]Child starts
lone transfer_to_agentChild starts

Example output from the reproducer on stock google-adk==1.34.0:

Layer 1: 15 passed
Layer 2: 7 passed, 1 xfail, 1 skipped
XFAIL [set_state, transfer] child starts in run_live (known stock ADK bug)
RESULT: OK on stock ADK — bug reproduced

Root cause in BaseLlmFlow.run_live() (v1.34.0,
base_llm_flow.py#L578-L614):

if (
event.contentandevent.content.partsandevent.content.parts[0].function_responseandevent.content.parts[0].function_response.name=="transfer_to_agent"
):
transfer_to_agent=event.actions.transfer_to_agentiftransfer_to_agent:
... # start child run_live()

The same parts[0] predicate remains in v2.6.0
(base_llm_flow.py#L681-L717).

Environment Details:

  • ADK Library Version (pip show google-adk): 1.34.0 (also present in 2.6.0
    and current main as of 2026-07-31)
  • Desktop OS: macOS (reproducer also runs on Linux; not Windows-tested)
  • Python Version (python -V): 3.12.8 (reproducer requires 3.11+)

Model Information:

  • Are you using LiteLLM: No
  • Which model is being used: gemini-live-2.5-flash-native-audio (Gemini 2.5
    Live native audio) in production via direct ADK Live + Vertex AI Agent Engine.
    The attached reproducer uses a MockModel (no GCP / API key) to force
    parallel response order deterministically.

Regression:

Did this work in a previous version of ADK? If so, which one?

Not confirmed on older ADK versions for this specific parts[0] predicate. The
issue is present on 1.34.0, 2.6.0, and main (checked 2026-07-31).
Related Live transfer fixes (#5238,
#5536) addressed different
mechanisms (history attribution and resumption-handle inheritance) and do not
remove the parts[0] check.

Logs:

Please attach relevant logs. Wrap them in code blocks (```) or attach a
text file.

ADK Live transfer_to_agent response-order reproducer
google-adk: 1.34.0
Python: 3.12.8
--- Layer 1: event-level probe (merge + predicates) ---
PASS [set_state, transfer] parts[0] is set_state
PASS [set_state, transfer] parts[1] is transfer_to_agent
PASS [set_state, transfer] action names specialist
PASS [set_state, transfer] stock predicate misses transfer
PASS [set_state, transfer] patched predicate detects transfer
→ 15 passed, 0 failed, 0 xfail, 0 skipped
--- Layer 2: run_live() regression (MockModel) ---
XFAIL [set_state, transfer] child starts in run_live (known stock ADK bug)
PASS [transfer, set_state] child starts
PASS lone transfer: child starts
→ 7 passed, 0 failed, 1 xfail, 1 skipped
RESULT: OK on stock ADK — bug reproduced

Screenshots / Video:

N/A

Additional Context:

Parallel Live tool execution merges responses correctly in
merge_parallel_function_response_events()
— all parts are appended and actions (including transfer_to_agent) are merged.
The bug is solely in the Live transfer gate inside run_live().

Proposed fix (scan all function responses and require the transfer action):

has_transfer_response=any(
response.name=="transfer_to_agent"forresponseinevent.get_function_responses()
)
ifhas_transfer_responseandevent.actions.transfer_to_agent:
... # existing child run_live() logic unchanged

Useful references:

Full repo (optional): https://github.com/haydeem2/adk-live-transfer-order-repro

Minimal Reproduction Code:

Gist:https://gist.github.com/haydeem2/4608a4a4e306a20ae9b5dfaa54338a34

python3 -m venv .venv
source .venv/bin/activate
pip install "google-adk==1.34.0"
curl -fsSL -o adk_live_transfer_order_repro.py \
"https://gist.githubusercontent.com/haydeem2/4608a4a4e306a20ae9b5dfaa54338a34/raw/adk_live_transfer_order_repro.py"
python adk_live_transfer_order_repro.py

Minimal merged-event proof (Layer 1 only — no run_live()):

fromgoogle.adk.events.eventimportEventfromgoogle.adk.events.event_actionsimportEventActionsfromgoogle.adk.flows.llm_flows.functionsimportmerge_parallel_function_response_eventsfromgoogle.genaiimporttypesdefresponse_event(name, *, transfer_target=None):
actions=EventActions()
iftransfer_target:
actions.transfer_to_agent=transfer_targetreturnEvent(
author="coordinator_agent",
content=types.Content(
role="user",
parts=[types.Part(function_response=types.FunctionResponse(
id=f"{name}-id", name=name, response={"result": "ok"},
))],
),
actions=actions,
)
merged=merge_parallel_function_response_events([
response_event("set_state"),
response_event("transfer_to_agent", transfer_target="specialist_agent"),
])
assertmerged.content.parts[0].function_response.name=="set_state"assertmerged.content.parts[1].function_response.name=="transfer_to_agent"assertmerged.actions.transfer_to_agent=="specialist_agent"# Stock run_live() gate checks parts[0] only → transfer missed

How often has this issue occurred?:

  • Rare

In production the symptom is rare: the model must batch transfer_to_agent with
another tool and have the non-transfer response land in parts[0]. Prompting
for a lone transfer reduces frequency further. When those conditions align, the
bug is deterministic (100% in the attached reproducer).

Metadata

Metadata

Assignees

Labels

live[Component] This issue is related to live, voice and video chat

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions