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-adk 2.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_input prompt, type Please add a more empathetic tone (triggers revise).
- Wait for the second draft + the second
adk_request_input event.
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=...)
return
With 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_count to the user-supplied interrupt_id for rerun_on_resume=True nodes when persisting the event.
- Validation: when
rerun_on_resume=True, validate or warn that the interrupt_id was already used by a prior completed interrupt in the same node lineage.
- Dev-ui fix: match
function_response to the most recent function_call with that id, instead of marking the id as "responded forever".
- Docs/sample: at minimum, update the
request_input_rerun sample (and the "How To" code snippet) to use a unique interrupt_id per run, since the current sample reproduces the bug.
Summary
When a
@node(rerun_on_resume=True)node yieldsRequestInputmore than once across rerun loops using the sameinterrupt_id(as shown in the officialrequest_input_rerunsample), 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.0b1adk web(built-in dev-ui)Reproduction
Use the official sample
request_input_rerun/agent.pyverbatim:Steps:
I received the wrong item.adk_request_inputprompt, typePlease add a more empathetic tone(triggersrevise).adk_request_inputevent.Expected
The dev-ui renders the inline
Enter your response...input box for the secondadk_request_input, allowing the user to submitapprove/reject/ more feedback.Actual
The second
adk_request_inputevent shows up only as a collapsed badge — no message body, no input box. Typing into the mainType a message...box submits as a new user turn (#9) instead of resuming the interrupt, and the workflow restarts the graph fromprocess_input.Root cause analysis
Both
RequestInputevents are persisted with the samefunction_call.id == "human_review". The dev-ui treats afunction_call.idas "already responded" once any matchingfunction_responsehas been seen in the session, regardless of whether a newerfunction_callwith 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.pyonly raises whennot node.rerun_on_resume and node_interrupted:So the backend contract permits the loop, but the dev-ui contract does not.
Workaround
Make
interrupt_idunique per run by appendingctx.run_id:With this change the full loop (
revise → approve → send_email) works end-to-end.Suggested fixes (any one)
run_id/attempt_countto the user-suppliedinterrupt_idforrerun_on_resume=Truenodes when persisting the event.rerun_on_resume=True, validate or warn that theinterrupt_idwas already used by a prior completed interrupt in the same node lineage.function_responseto the most recentfunction_callwith that id, instead of marking the id as "responded forever".request_input_rerunsample (and the "How To" code snippet) to use a uniqueinterrupt_idper run, since the current sample reproduces the bug.