Pre-flight Checklist
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 > 1 → True → 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
- 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}
- Send a message that triggers the interrupt.
- The agent pauses at the interrupt, frontend receives the interrupt event.
- Frontend sends a resume request with
forwarded_props.command.resume = "yes".
prepare_stream compares message counts → checkpoint has more → enters regenerate path.
- 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.
Pre-flight Checklist
Describe the Bug
The
prepare_streammethod inagent.pyuses 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 withCommand(resume=...), the checkpoint's extra AI message makesprepare_streamthink this is a regenerate request.The result:
prepare_streamenters theprepare_regenerate_streampath, 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:When an interrupt is active:
[HumanMessage, AIMessage(tool_calls=[...])]→ 2 messages[HumanMessage]→ 1 message (AI tool-call was never shown)2 > 1→True→ regenerate path triggered incorrectlyThe
has_active_interruptscheck at line ~495 comes after the regenerate check, so it never gets a chance to run.Steps to Reproduce
interrupt()for human-in-the-loop:forwarded_props.command.resume = "yes".prepare_streamcompares message counts → checkpoint has more → enters regenerate path.Expected Behavior
prepare_streamshould 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:
Environment
Related Issues
RUN_STARTEDinhas_active_interruptspath (different root cause, sameprepare_streamarea)'NoneType' object is not a mappingerror in ag-ui-langgraph #702 —NoneTypeis not a mapping crash inset_message_in_progress(anotheragent.pybug)Additional Context
Workaround: override
prepare_streamand detect the interrupt-resume case before the base class's message-count check, then bypass directly to theCommand(resume=...)streaming path.Happy to send a PR with the fix and a regression test if assigned.