Skip to content

fix(agents): route the turn after a paused single_turn sub-agent back to its delegator - #7605

Open
Somay-kousis wants to merge 2 commits into
google:mainfrom
Somay-kousis:fix/single-turn-sticky-routing
Open

Somay-kousis wants to merge 2 commits into
google:mainfrom
Somay-kousis:fix/single-turn-sticky-routing

Conversation

@Somay-kousis

Copy link
Copy Markdown

Link to Issue or Description of Change

1. Link to an existing issue (if applicable):

2. Or, if no issue exists, describe the change:

Problem:

A mode='single_turn' sub-agent that pauses on a tool confirmation takes every later turn. find_agent_to_run maps an agent inside a delegation back to its delegator through _resolve_task_delegator, but that helper only checks mode == "task". A paused single_turn agent is the last author, so it is picked as the agent to run: a new user message goes to it instead of the root, and after an approval it replies to the user directly while the root's call to it never gets a response.

Solution:

Check mode in DELEGATED_TASK_MODES (task and single_turn, from utils/_agent_mode.py) instead of == "task". The paused single_turn agent now resolves to its delegator on both paths that call the helper: the resume path (the user answers the confirmation; the delegator resumes the sub-agent and gets its result) and the event scan (the user sends a new message; the delegator answers it). Chat sub-agents keep their sticky routing.

Testing Plan

Unit Tests:

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

  • All unit tests pass locally.

  • test_find_agent_to_run_routes_single_turn_agent_to_its_delegator in tests/unittests/agents/test_agent_router.py, next to the existing task-mode test. Parametrized over the mode as a string and as AgentMode.SINGLE_TURN, a delegator that is the root or a nested chat agent, and whether the user answered the pending call.

  • test_run_async_paused_single_turn_agent_returns_turn_to_delegator in tests/unittests/test_runners.py, end to end through Runner: a root with a single_turn sub-agent whose tool needs confirmation. Parametrized over the next user turn (a new message, approve, reject) and ResumabilityConfig(is_resumable=False/True). It checks the tool ran only on approve, the root replies on turn 2, the sub-agent never gets a chat turn, and turn 3 goes to the root.

Before the fix (main at 4dbb55d):

FAILED tests/unittests/agents/test_agent_router.py::test_find_agent_to_run_routes_single_turn_agent_to_its_delegator[str-root_agent-False]
FAILED tests/unittests/agents/test_agent_router.py::test_find_agent_to_run_routes_single_turn_agent_to_its_delegator[str-root_agent-True]
FAILED tests/unittests/agents/test_agent_router.py::test_find_agent_to_run_routes_single_turn_agent_to_its_delegator[str-helper-False]
FAILED tests/unittests/agents/test_agent_router.py::test_find_agent_to_run_routes_single_turn_agent_to_its_delegator[str-helper-True]
FAILED tests/unittests/agents/test_agent_router.py::test_find_agent_to_run_routes_single_turn_agent_to_its_delegator[enum-root_agent-False]
FAILED tests/unittests/agents/test_agent_router.py::test_find_agent_to_run_routes_single_turn_agent_to_its_delegator[enum-root_agent-True]
FAILED tests/unittests/agents/test_agent_router.py::test_find_agent_to_run_routes_single_turn_agent_to_its_delegator[enum-helper-False]
FAILED tests/unittests/agents/test_agent_router.py::test_find_agent_to_run_routes_single_turn_agent_to_its_delegator[enum-helper-True]
FAILED tests/unittests/test_runners.py::test_run_async_paused_single_turn_agent_returns_turn_to_delegator[new_message-False]
FAILED tests/unittests/test_runners.py::test_run_async_paused_single_turn_agent_returns_turn_to_delegator[new_message-True]
FAILED tests/unittests/test_runners.py::test_run_async_paused_single_turn_agent_returns_turn_to_delegator[approve-False]
FAILED tests/unittests/test_runners.py::test_run_async_paused_single_turn_agent_returns_turn_to_delegator[approve-True]
FAILED tests/unittests/test_runners.py::test_run_async_paused_single_turn_agent_returns_turn_to_delegator[reject-False]
FAILED tests/unittests/test_runners.py::test_run_async_paused_single_turn_agent_returns_turn_to_delegator[reject-True]
14 failed in 6.10s

After the fix:

PASSED tests/unittests/agents/test_agent_router.py::test_find_agent_to_run_routes_single_turn_agent_to_its_delegator[str-root_agent-False]
PASSED tests/unittests/agents/test_agent_router.py::test_find_agent_to_run_routes_single_turn_agent_to_its_delegator[str-root_agent-True]
PASSED tests/unittests/agents/test_agent_router.py::test_find_agent_to_run_routes_single_turn_agent_to_its_delegator[str-helper-False]
PASSED tests/unittests/agents/test_agent_router.py::test_find_agent_to_run_routes_single_turn_agent_to_its_delegator[str-helper-True]
PASSED tests/unittests/agents/test_agent_router.py::test_find_agent_to_run_routes_single_turn_agent_to_its_delegator[enum-root_agent-False]
PASSED tests/unittests/agents/test_agent_router.py::test_find_agent_to_run_routes_single_turn_agent_to_its_delegator[enum-root_agent-True]
PASSED tests/unittests/agents/test_agent_router.py::test_find_agent_to_run_routes_single_turn_agent_to_its_delegator[enum-helper-False]
PASSED tests/unittests/agents/test_agent_router.py::test_find_agent_to_run_routes_single_turn_agent_to_its_delegator[enum-helper-True]
PASSED tests/unittests/test_runners.py::test_run_async_paused_single_turn_agent_returns_turn_to_delegator[new_message-False]
PASSED tests/unittests/test_runners.py::test_run_async_paused_single_turn_agent_returns_turn_to_delegator[new_message-True]
PASSED tests/unittests/test_runners.py::test_run_async_paused_single_turn_agent_returns_turn_to_delegator[approve-False]
PASSED tests/unittests/test_runners.py::test_run_async_paused_single_turn_agent_returns_turn_to_delegator[approve-True]
PASSED tests/unittests/test_runners.py::test_run_async_paused_single_turn_agent_returns_turn_to_delegator[reject-False]
PASSED tests/unittests/test_runners.py::test_run_async_paused_single_turn_agent_returns_turn_to_delegator[reject-True]
14 passed in 9.95s

pytest tests/unittests/agents tests/unittests/test_runners.py tests/unittests/runners tests/unittests/flows tests/unittests/workflow: this branch 3540 passed, 1 skipped, 5 xfailed, 1 xpassed; with main's _agent_router.py, 14 failed, 3526 passed, 1 skipped, 6 xfailed (the 14 are the new tests). The xpass is test_single_turn_child_agent_interrupt_and_resume in test_llm_agent_interruptions.py (xfail "Single-turn agent as subagent not supported yet"), which now passes. I left its marker alone; happy to drop it here if you want.

pyink --check and isort --check-only are clean on the changed files. mypy src/google/adk/agents/_agent_router.py: no issues, same as main.

Manual End-to-End (E2E) Tests:

The script from the issue: a chat root with a single_turn helper whose pay tool needs confirmation, on InMemoryRunner with fake models.

User types something else after the pause. Before:

=== scenario=new_message resumable=False
--- turn 1: user -> 'pay 5 please'
    root ['call helper']
    helper ['call pay']
    helper ['call adk_request_confirmation']
    helper ["response pay {'error': 'This tool call requires confirmation, please approve or reject.'}"]
--- turn 2: user -> 'unrelated message 2'
    helper ['HELPER: second reply']
--- turn 3: user -> 'unrelated message 3'
    helper ['HELPER: third reply']
root LLM calls: 1  helper LLM calls: 3

After:

=== scenario=new_message resumable=False
--- turn 1: user -> 'pay 5 please'
    root ['call helper']
    helper ['call pay']
    helper ['call adk_request_confirmation']
    helper ["response pay {'error': 'This tool call requires confirmation, please approve or reject.'}"]
--- turn 2: user -> 'unrelated message 2'
    root ['ROOT: second reply']
--- turn 3: user -> 'unrelated message 3'
    root ['ROOT: third reply']
root LLM calls: 3  helper LLM calls: 1

User approves. Before:

=== scenario=approve resumable=False
--- turn 1: user -> 'pay 5 please'
    root ['call helper']
    helper ['call pay']
    helper ['call adk_request_confirmation']
    helper ["response pay {'error': 'This tool call requires confirmation, please approve or reject.'}"]
--- turn 2: user -> 'approve confirmation'
    helper ["response pay {'paid': 5}"]
    helper ['HELPER: second reply']
--- turn 3: user -> 'unrelated message 3'
    helper ['HELPER: third reply']
root LLM calls: 1  helper LLM calls: 3

After:

=== scenario=approve resumable=False
--- turn 1: user -> 'pay 5 please'
    root ['call helper']
    helper ['call pay']
    helper ['call adk_request_confirmation']
    helper ["response pay {'error': 'This tool call requires confirmation, please approve or reject.'}"]
--- turn 2: user -> 'approve confirmation'
    helper ["response pay {'paid': 5}"]
    helper ['HELPER: second reply']
    root ["response helper {'result': 'HELPER: second reply'}"]
    root ['ROOT: second reply']
--- turn 3: user -> 'unrelated message 3'
    root ['ROOT: third reply']
root LLM calls: 3  helper LLM calls: 2

Approving after an unrelated message also works on this branch: the root answers the message, then the approval resumes helper and the root gets its result.

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

No open PR touches _agent_router.py. The task-mode mapping came in with c5ee35e.

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.

A single_turn sub-agent paused on a tool confirmation takes over every later turn

2 participants