Skip to content

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

Closed

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.

Activity

  1. added
    web[Component] This issue will be transferred to adk-web
    on May 7, 2026
  2. added theissue type on May 7, 2026
  3. removed their assignment
    on May 7, 2026
  4. surajksharma07 commented on May 7, 2026

    @surajksharma07
    Collaborator

    @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.

  5. added
    request clarification[Status] The maintainer need clarification or more information from the author
    v2Affects only 2.0 version
    on May 7, 2026
  6. xuanyang15 commented on May 13, 2026

    @xuanyang15
    Collaborator

    @wyf7107 Could you please take a look?

  7. falcaozane commented on May 22, 2026

    @falcaozane

    yes facing the same issue in HIL implementation

  8. LikhithST commented on Jul 3, 2026

    @LikhithST

    @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.

    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.

  9. LikhithST commented on Jul 4, 2026

    @LikhithST

    Hey, I was able to achieve RequestInput in loop by using Dynamic agent workflows method.

    Environment

    • google-adk 2.3.0
    • Python 3.10.12
    • Ubuntu 22.04.5 LTS, adk web (built-in dev-ui)

    Workaround

    Below code demonstrates triggering of RequestInput in loop by using Dynamic agent workflows method.

    from typing import Any
    from google.adk import Context
    from google.adk.events import RequestInput
    from google.adk.workflow import node
    
    
    @node(rerun_on_resume=False)
    async def get_user_approval(ctx: Context, node_input: Any):
        """Yields a RequestInput to pause the workflow and wait for user input."""
        yield RequestInput(message="Please approve this request (Yes/No)")
    
    
    @node(rerun_on_resume=True)
    async def handle_process(ctx: Context, node_input: Any):
        """The orchestrator calling the interactive step."""
        user_response = await ctx.run_node(get_user_approval)
    
        if user_response.lower() == "yes":
            return "Approved"
        return "Denied"
    

    Find out more here: https://adk.dev/graphs/dynamic/#human-input

    As suggested here I split the function into two, Parent node handle_process in dynamic workflow calling ctx.run_node must set rerun_on_resume=True. The function get_user_approval that calls the RequestInput need not resume so @node(rerun_on_resume=False). By this we can completely avoid using the interrupt_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_Id because I observe that, in the above example calling RequestInput without argument interrupt_Id, the adk assigns an unique interrupt_Id by itself, and since each of these new RequestInput from get_user_approval has one to one mapping with handle_process the parent node of get_user_approval, there is no problem of resolving unrelated interrupts.

    Image 1. ADK auto assigned InterruptId for the first input {"amount": 350.0, "description": "Client dinner"} Image

    Image 2. ADK auto assigned InterruptId for the second input {"amount": 150.0, "description": "Client dinner"} Image

    Image 3. Interrupt resolved for second input (the id is same as second InterruptId) Image

    Image 4. Interrupt resolved for first input (the id is same as first InterruptId) Image

  10. surajksharma07 commented on Jul 13, 2026

    @surajksharma07
    Collaborator

    @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!

  11. added
    spam[Status] Issues suspected of having comments which are spam
    on Jul 21, 2026
  12. deleted a comment from adk-bot on Jul 22, 2026
  13. removed
    spam[Status] Issues suspected of having comments which are spam
    on Jul 22, 2026
  14. added a commit that references this issue on Oct 14, 2026
    ed81e1b
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

needs review[Status] The PR/issue is awaiting review from the maintainerrequest clarification[Status] The maintainer need clarification or more information from the authorv2Affects only 2.0 versionweb[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