Skip to content

[Bug]: prepare_stream regenerate heuristic incorrectly triggers on interrupt resume, destroying interrupt state #1743

@uesleilima

Description

@uesleilima

Pre-flight Checklist

  • I have searched existing issues and this hasn't been reported yet.
  • I am using the latest version AG-UI.

Describe the Bug

The prepare_stream method in agent.py uses a message-count heuristic to detect "regenerate last response" requests: if the checkpoint has more messages than the frontend sent, it assumes the user wants to time-travel back.

This heuristic is wrong for interrupt resumes. When a LangGraph interrupt() fires, the checkpoint contains an AI message (the tool call that triggered the interrupt) that the frontend never received or displayed. When the frontend later resumes with Command(resume=...), the checkpoint's extra AI message makes prepare_stream think this is a regenerate request.

The result: prepare_stream enters the prepare_regenerate_stream path, which time-travels back and destroys the interrupt state, making it impossible to resume from the interrupt.

Root Cause

agent.py, prepare_stream, line ~460:

non_system_messages = [msg for msg in langchain_messages if not isinstance(msg, SystemMessage)]
if len(agent_state.values.get("messages", [])) > len(non_system_messages):
    # ... triggers regenerate path

When an interrupt is active:

  • Checkpoint has: [HumanMessage, AIMessage(tool_calls=[...])] → 2 messages
  • Frontend sends: [HumanMessage] → 1 message (AI tool-call was never shown)
  • 2 > 1True → regenerate path triggered incorrectly

The has_active_interrupts check at line ~495 comes after the regenerate check, so it never gets a chance to run.

Steps to Reproduce

  1. Create a LangGraph agent that uses interrupt() for human-in-the-loop:
from langgraph.types import interrupt

async def approval_node(state):
    answer = interrupt({"question": "Approve?", "options": ["yes", "no"]})
    return {"approved": answer}
  1. Send a message that triggers the interrupt.
  2. The agent pauses at the interrupt, frontend receives the interrupt event.
  3. Frontend sends a resume request with forwarded_props.command.resume = "yes".
  4. prepare_stream compares message counts → checkpoint has more → enters regenerate path.
  5. Interrupt state is destroyed. The resume value is lost.

Expected Behavior

prepare_stream should detect active interrupts with a resume value before the regenerate heuristic, and bypass the message-count check entirely in that case.

Suggested fix — early return for interrupt resumes:

async def prepare_stream(self, input, agent_state, config):
    ...
    interrupts = self._collect_interrupts(agent_state.tasks)
    has_active_interrupts = len(interrupts) > 0
    resume_input = forwarded_props.get('command', {}).get('resume', None)

    # Interrupt resume must bypass the regenerate heuristic
    if resume_input is not None and has_active_interrupts:
        # Set up state, create Command(resume=...), return stream
        ...

    # Only now check for regenerate
    if len(agent_state.values.get("messages", [])) > len(non_system_messages):
        ...

Environment

ag-ui-langgraph: 0.0.35
ag-ui-protocol:  0.1.18
langgraph:       0.4.7
langchain-core:  0.3.59
Python:          3.13

Related Issues

Additional Context

Workaround: override prepare_stream and detect the interrupt-resume case before the base class's message-count check, then bypass directly to the Command(resume=...) streaming path.

Happy to send a PR with the fix and a regression test if assigned.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions