Skip to content

RequestInput with constant interrupt_id breaks dev-ui input prompt on @node(rerun_on_resume=True) loops #5617

@ldfpku

Description

@ldfpku

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:

  1. Start session with any complaint, e.g. I received the wrong item.
  2. At the first adk_request_input prompt, type Please add a more empathetic tone (triggers revise).
  3. 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)

  1. 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.
  2. 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.
  3. Dev-ui fix: match function_response to the most recent function_call with that id, instead of marking the id as "responded forever".
  4. 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.

Metadata

Metadata

Labels

web[Component] This issue will be transferred to adk-web

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions