fix: prevent duplicate synthetic user event on single-turn agent resumption
Continuous Integration / Pre-commit Linter (push) Waiting to run
Continuous Integration / Mypy Check (Python 3.10) (push) Waiting to run
Continuous Integration / Mypy Check (Python 3.11) (push) Waiting to run
Continuous Integration / Mypy Check (Python 3.12) (push) Waiting to run
Continuous Integration / Mypy Check (Python 3.13) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.10) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.11) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.12) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.13) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.14) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.10) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.11) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.12) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.13) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.14) (push) Waiting to run
Copybara PR Handler / close-imported-pr (push) Waiting to run
Continuous Integration / Pre-commit Linter (push) Waiting to run
Continuous Integration / Mypy Check (Python 3.10) (push) Waiting to run
Continuous Integration / Mypy Check (Python 3.11) (push) Waiting to run
Continuous Integration / Mypy Check (Python 3.12) (push) Waiting to run
Continuous Integration / Mypy Check (Python 3.13) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.10) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.11) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.12) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.13) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.14) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.10) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.11) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.12) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.13) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.14) (push) Waiting to run
Copybara PR Handler / close-imported-pr (push) Waiting to run
When resuming a single-turn agent node from an HITL tool confirmation pause, prepare_llm_agent_input was unconditionally appending a synthetic user event, shadowing the user's FunctionResponse and causing an infinite confirmation loop. This change skips synthetic user input injection when resume_inputs are present. Co-authored-by: Shangjie Chen <deanchen@google.com> PiperOrigin-RevId: 969178203
This commit is contained in:
committed by
Copybara-Service
parent
d9f4d3d288
commit
e753651b7d
@@ -291,7 +291,9 @@ def prepare_llm_agent_input(
|
||||
"""Prepares the input for running LlmAgent as a node.
|
||||
|
||||
For ``single_turn`` mode, append a user-role event with the input
|
||||
directly to session.events (legacy behavior).
|
||||
directly to session.events (legacy behavior). When resuming with
|
||||
``resume_inputs``, skip appending to avoid injecting duplicate synthetic
|
||||
user events that shadow user function responses.
|
||||
|
||||
For ``task`` mode, the input is the parent's task-delegation FC
|
||||
args. Those are NOT appended here — the content-builder
|
||||
@@ -305,7 +307,17 @@ def prepare_llm_agent_input(
|
||||
For workflow nodes running in a sub-branch, stamp the input event with that
|
||||
branch. A private node input should not look like the shared root user turn.
|
||||
"""
|
||||
if node_input is None or agent.mode != 'single_turn':
|
||||
# Skip injection if:
|
||||
# 1. No input was provided.
|
||||
# 2. Agent is not single_turn (task mode handles its own leading turn).
|
||||
# 3. Resuming from pause/interrupt (e.g. HITL confirmation): node is re-run
|
||||
# with resume_inputs; injecting synthetic user input would shadow the
|
||||
# user's FunctionResponse on the branch tail and cause infinite loops.
|
||||
if (
|
||||
node_input is None
|
||||
or agent.mode != 'single_turn'
|
||||
or bool(ctx.resume_inputs)
|
||||
):
|
||||
return
|
||||
agent_input = to_user_content(node_input)
|
||||
user_event = Event(author='user', message=agent_input)
|
||||
|
||||
@@ -239,6 +239,39 @@ async def test_single_turn_input_event_inherits_branch_and_scope(
|
||||
assert event.isolation_scope == 'scope-1'
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_single_turn_input_skipped_when_resuming(
|
||||
request: pytest.FixtureRequest,
|
||||
):
|
||||
"""Resuming a single-turn agent node skips duplicate user input and retains initial input."""
|
||||
from google.adk.workflow._llm_agent_wrapper import prepare_llm_agent_input
|
||||
|
||||
agent = _make_agent(mode='single_turn')
|
||||
ic = await create_parent_invocation_context(request.function.__name__, agent)
|
||||
ic.branch = 'parent.worker@1'
|
||||
# Pre-populate session with turn 1's initial user input
|
||||
original_event = Event(
|
||||
author='user',
|
||||
branch='parent.worker@1',
|
||||
content=types.Content(
|
||||
role='user', parts=[types.Part(text='turn 1 initial input')]
|
||||
),
|
||||
)
|
||||
ic.session.events.append(original_event)
|
||||
|
||||
ctx = Context(
|
||||
invocation_context=ic, resume_inputs={'worker@1': {'confirmed': True}}
|
||||
)
|
||||
|
||||
initial_len = len(ic.session.events)
|
||||
prepare_llm_agent_input(agent, ctx, 'hello')
|
||||
|
||||
# Verify no duplicate user input was appended on resume
|
||||
assert len(ic.session.events) == initial_len
|
||||
# Verify the resumed node still sees the initial user input from turn 1
|
||||
assert ic.session.events[-1].content.parts[0].text == 'turn 1 initial input'
|
||||
|
||||
|
||||
# --- build_node auto-wrapping ---
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user