Repository navigation
Description
Summary
When a @node(rerun_on_resume=True) node yields RequestInput more than once across rerun loops using the same interrupt_id (as shown in the official request_input_rerun sample), the dev-ui fails to render the input prompt for the second and subsequent requests. The workflow appears stuck even though the backend is correctly waiting for input.
Environment
google-adk2.0.0b1- Python 3.14
- macOS,
adk web(built-in dev-ui)
Reproduction
Use the official sample request_input_rerun/agent.py verbatim:
@node(rerun_on_resume=True)
def human_review(draft: str, ctx: Context):
resume_input = ctx.resume_inputs.get("human_review")
if not resume_input:
yield RequestInput(
interrupt_id="human_review",
message="Please review ... 'approve', 'reject', or feedback to revise.\n\n---\n{draft}\n---",
)
return
if resume_input == "reject":
yield Event(route="rejected")
elif resume_input == "approve":
yield Event(route="approved")
else:
yield Event(state={"feedback": resume_input}, route="revise")Steps:
- Start session with any complaint, e.g.
I received the wrong item. - At the first
adk_request_inputprompt, typePlease add a more empathetic tone(triggersrevise). - Wait for the second draft + the second
adk_request_inputevent.
Expected
The dev-ui renders the inline Enter your response... input box for the second adk_request_input, allowing the user to submit approve / reject / more feedback.
Actual
The second adk_request_input event shows up only as a collapsed badge — no message body, no input box. Typing into the main Type a message... box submits as a new user turn (#9) instead of resuming the interrupt, and the workflow restarts the graph from process_input.
Root cause analysis
Both RequestInput events are persisted with the same function_call.id == "human_review". The dev-ui treats a function_call.id as "already responded" once any matching function_response has been seen in the session, regardless of whether a newer function_call with the same id is now pending. So the second pending interrupt is silently classified as a historical/already-resolved request.
Notably, the framework itself does allow multiple interrupts from the same node when rerun_on_resume=True — _node_runner.py only raises when not node.rerun_on_resume and node_interrupted:
if is_interrupt and (
data_event_count > 0
or (not node.rerun_on_resume and node_interrupted)
):
raise ValueError(...)So the backend contract permits the loop, but the dev-ui contract does not.
Workaround
Make interrupt_id unique per run by appending ctx.run_id:
interrupt_id = f"human_review:{ctx.run_id}"
resume_input = ctx.resume_inputs.get(interrupt_id)
if not resume_input:
yield RequestInput(interrupt_id=interrupt_id, message=...)
returnWith this change the full loop (revise → approve → send_email) works end-to-end.
Suggested fixes (any one)
- Framework auto-disambiguation: transparently append
run_id/attempt_countto the user-suppliedinterrupt_idforrerun_on_resume=Truenodes when persisting the event. - Validation: when
rerun_on_resume=True, validate or warn that theinterrupt_idwas already used by a prior completed interrupt in the same node lineage. - Dev-ui fix: match
function_responseto the most recentfunction_callwith that id, instead of marking the id as "responded forever". - Docs/sample: at minimum, update the
request_input_rerunsample (and the "How To" code snippet) to use a uniqueinterrupt_idper run, since the current sample reproduces the bug.
Activity
surajksharma07 commentedon May 7, 2026
@ldfpku the dev-ui permanently marks a function_call.id as "already responded" after the first match, so reusing the same interrupt_id across rerun iterations silently drops every subsequent prompt regardless of how many times the node loops.
The interrupt_id=f"human_review:{ctx.run_id}" workaround you outlined looks like the right call here — could you do a more thorough pass and verify it holds across the full revise → approve → reject cycle and edge cases like session resumption mid-loop?
Once you're confident it's solid end-to-end, it'd also be worth checking that the official request_input_rerun sample gets updated to reflect the unique-id pattern — it currently reproduces the bug verbatim, so anyone copying from it will hit the same issue.
Happy to help scope the permanent framework-side fix once you've confirmed the workaround is stable.
xuanyang15 commentedon May 13, 2026
@wyf7107 Could you please take a look?
falcaozane commentedon May 22, 2026
yes facing the same issue in HIL implementation
LikhithST commentedon Jul 3, 2026
Hey! When I try the run_id is always '1' across multiple reruns, making it effectively same as a static str. So its still the same problem.
LikhithST commentedon Jul 4, 2026
Hey, I was able to achieve
RequestInputin loop by using Dynamic agent workflows method.Environment
Workaround
Below code demonstrates triggering of
RequestInputin loop by using Dynamic agent workflows method.Find out more here: https://adk.dev/graphs/dynamic/#human-input
As suggested here I split the function into two, Parent node
handle_processin dynamic workflow callingctx.run_nodemust setrerun_on_resume=True. The functionget_user_approvalthat calls theRequestInputneed not resume so@node(rerun_on_resume=False). By this we can completely avoid using theinterrupt_id.This is a workaround to the initial / llm suggested approach. But I am also not sure if we need to explicitly provide an
interrupt_Idbecause I observe that, in the above example callingRequestInputwithout argumentinterrupt_Id, the adk assigns an uniqueinterrupt_Idby itself, and since each of these newRequestInputfromget_user_approvalhas one to one mapping withhandle_processthe parent node ofget_user_approval, there is no problem of resolving unrelated interrupts.Image 1. ADK auto assigned
InterruptIdfor the first input{"amount": 350.0, "description": "Client dinner"}Image 2. ADK auto assigned
InterruptIdfor the second input{"amount": 150.0, "description": "Client dinner"}Image 3. Interrupt resolved for second input (the
idis same as secondInterruptId)Image 4. Interrupt resolved for first input (the
idis same as firstInterruptId)surajksharma07 commentedon Jul 13, 2026
@LikhithST — you're right that ctx.run_id stays static across reruns, so the workaround I suggested earlier doesn't actually work.
Your dynamic-workflow approach checks out against the source: RequestInput.interrupt_id already defaults to a fresh uuid.uuid4() when omitted (confirmed on latest main/v2.4.0), so routing the interactive step through ctx.run_node() on a separate node that never passes an explicit ID is a solid way to sidestep the collision entirely.
That said, the official request_input_rerun sample still hard-codes interrupt_id="human_review" unchanged, so it still reproduces this bug verbatim on v2.4.0.
And the actual dev-ui rendering bug (the JS layer treating a function_call.id as "resolved forever") doesn't look fixed anywhere — no PR or commit touches cli/browser/*.js.
Team is looking more into it.
Thanks!
fix(workflow): disambiguate RequestInput interrupt_id across rerun lo…