Visitar URL original
fix(agents): route next turn to the target of an unfinished transfer_to_agent by erickleal-azos · Pull Request #7459 · google/adk-python · GitHub
Skip to content

fix(agents): route next turn to the target of an unfinished transfer_to_agent - #7459

Open
erickleal-azos wants to merge 1 commit into
google:mainfrom
erickleal-azos:fix/route-unfinished-transfer
Open

erickleal-azos wants to merge 1 commit into
google:mainfrom
erickleal-azos:fix/route-unfinished-transfer

Conversation

@erickleal-azos

Copy link
Copy Markdown

Link to Issue

Problem:

If the target of transfer_to_agent fails before yielding any event (for example a 503 on its first model call), the transfer is already persisted but the target never authored an event. find_agent_to_run returns the author of the newest eligible event, so the next user message goes back to the transferring agent, whose history says the transfer succeeded. The conversation stays stuck there.

The node-failure event makes it worse: it has no content and inherits the transferring agent as its author, so it becomes the newest eligible event. See the issue for the full reproduction.

Solution:

In agents/_agent_router.py::find_agent_to_run:

  1. Skip error events without content when scanning. They are not replies, and their author may be inherited from the parent context.
  2. When a scanned event carries actions.transfer_to_agent, return the target if it is one of the author's transfer targets (_get_transfer_targets(author)) and is_transferable_across_agent_tree(target) holds. Otherwise fall through to the existing author-based logic.

This follows the direction of the existing TODO: use wait_for_output to decide the agent to run: route by who should produce the next output, not only by who wrote last.

Why the target is resolved among the author's transfer targets: it is the same set the transfer tool and scheduler allow. A transfer the scheduler would have rejected (for example one forbidden by disallow_transfer_to_parent or disallow_transfer_to_peers) therefore never reroutes the session.

Why it is safe:

Situation Behaviour
Target replied normally Unchanged: the target's own events are newer than the transfer and are found first
Target failed before its first event Routed to the target (the fix)
Chain root -> A -> B, B fails Routed to B (the newest transfer wins)
sub -> root, root failed before replying Routed to the root (also the behaviour today)
Transfer restricted by disallow_transfer_to_parent / disallow_transfer_to_peers Unchanged: not among the author's transfer targets
Target not transferable across the tree, or no longer in the tree Unchanged: falls through to the existing logic
Error event that carries content (e.g. MAX_TOKENS partial reply) Unchanged: still counts as a reply
Resumable pending function response, or a Workflow root Unchanged: both return before the scan

No change to event persistence, LLM contents, or the transfer_to_agent tool.

Testing Plan

Unit Tests:

  • I have added or updated unit tests for my change.
  • All unit tests pass locally.

New tests in tests/unittests/agents/test_agent_router.py (routing unit tests, using the existing _make_agent_tree()):

Test Fails without the fix
test_find_agent_to_run_routes_to_target_of_unfinished_transfer yes
test_find_agent_to_run_ignores_contentless_error_event_after_transfer yes
test_find_agent_to_run_ignores_contentless_error_event_without_transfer yes
test_find_agent_to_run_routes_to_root_on_unfinished_transfer_to_parent yes
test_find_agent_to_run_routes_to_last_target_of_chained_transfer yes
test_find_agent_to_run_routes_by_author_of_error_event_with_content guard
test_find_agent_to_run_prefers_later_reply_over_older_transfer guard
test_find_agent_to_run_unfinished_transfer_to_non_transferable_target guard
test_find_agent_to_run_ignores_transfer_rejected_by_peer_restriction guard
test_find_agent_to_run_ignores_transfer_rejected_by_parent_restriction guard

New end-to-end test in tests/unittests/workflow/test_agent_transfer.py, using a real Runner and a MockModel that raises ServerError(503) on the target:

  • test_transfer_target_failing_before_first_event_owns_next_turn[True/False] (resumable and non-resumable): the first turn raises, then with a healthy model the second turn is answered by the target and the root model is not called again. Fails without the fix.

Without the fix, 7 of these fail (5 routing + 2 end-to-end); with it, all pass:

$ pytest tests/unittests/agents/test_agent_router.py \
         tests/unittests/workflow/test_agent_transfer.py \
         tests/unittests/test_runners.py \
         tests/unittests/live/test__runner_utils.py -q
261 passed, 91 warnings in 5.47s

Manual End-to-End (E2E) Tests:

Setup: the repro.py from issue #7458 (a root LlmAgent with one sub-agent, mock models, the sub-agent raising ServerError(503) on the first turn only). Run it with python repro.py against this branch and against main.

Before (main):

>>> TRANSFER please
    raised 503
>>> hello?
    root_agent: [root] answered

After (this PR):

>>> TRANSFER please
    raised 503
>>> hello?
    sub_agent: [sub] answered

The relevant line is the answer to hello?: it now comes from sub_agent, the target of the earlier transfer.

Checklist

  • I have read the CONTRIBUTING.md document.
  • I have performed a self-review of my own code.
  • I have commented my code, particularly in hard-to-understand areas.
  • I have added tests that prove my fix is effective or that my feature works.
  • New and existing unit tests pass locally with my changes.
  • I have manually tested my changes end-to-end.
  • Any dependent changes have been merged and published in downstream modules.

Additional context

Out of scope for this PR, to keep it to one concern. Possible follow-ups:

  • Author node-failure events with the failing node instead of the inherited event_author (NodeRunner._enrich_event). This does not replace the routing change, because cancellations and process crashes record no error event.
  • Optionally surface the failure to the model as a short note in the contents.
  • adk-docs: state that a persisted transfer is final, and recommend HttpRetryOptions / on_model_error_callback for transient errors.

…to_agent

When the target of transfer_to_agent fails before yielding any event, the
transfer is already persisted but the target never authored an event, so
find_agent_to_run sent the next turn back to the transferring agent. Route to
the target of the latest valid transfer when it has not replied since, and skip
content-less error events, whose author is inherited from the parent context.

Fixes google#7458
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Committed transfer_to_agent is lost when the target agent fails before yielding: the next turn is routed back to the transferring agent, whose history says the transfer succeeded

2 participants