Skip to content

ag-ui-langgraph: multi-interrupt resume (__agui_resume_map__) rejected by LangGraph 1.x id-keyed resume requirement #2178

Description

@rickgentry

Summary

ag-ui-langgraph's multi-interrupt resume path is incompatible with LangGraph 1.x. When a thread has more than one pending interrupt (e.g. an AIMessage with several parallel tool calls that each call interrupt()), resuming via RunAgentInput.resume = [ResumeEntry, ...] raises before any interrupt() handler runs:

RuntimeError: When there are multiple pending interrupts, you must specify the interrupt id when resuming.

The single-interrupt path works; only the N>1 path is affected.

Root cause

LangGraphAgent._build_command_from_agui_resume (integrations/langgraph/python/ag_ui_langgraph/agent.py, ~L1181-1195) folds N>1 resume entries into:

Command(resume={"__agui_resume_map__": {interrupt_id: {"status": ..., "payload": ...}, ...}})

That value is passed straight to the graph (agent.py ~L626) with no unwrap. But LangGraph 1.x requires, when multiple interrupts are pending, that the resume dict be keyed by interrupt id (native keyed resume: Command(resume={interrupt_id: value})); a non-id key like "__agui_resume_map__" is rejected. So the documented behavior — README "Resuming via AG-UI standard resume[]" states that for multiple entries each interrupt() returns {"__agui_resume_map__": {...}} and the handler self-selects — can never be reached on LangGraph 1.x: the run errors before the handler executes.

Reproduce

  • A LangGraph create_react_agent (or any graph) whose turn fans out two tool calls, each calling interrupt().
  • Resume with RunAgentInput.resume = [ResumeEntry(interrupt_id=<id0>, status="resolved", payload=...), ResumeEntry(interrupt_id=<id1>, status="resolved", payload=...)].
  • Result: RuntimeError: When there are multiple pending interrupts, you must specify the interrupt id when resuming.

Confirmed on langgraph 1.0.7 with ag-ui-langgraph at commit d2049deb (its pyproject declares langgraph>=0.6.0,<2). I did not pin the exact langgraph version at which the id-keyed requirement was introduced, so I can't say precisely which point in the declared <2 range it starts failing — but it fails across langgraph 1.x. Note also test_interrupt_handling.py observes "Without a real LangGraph id we can't round-trip a resume answer", i.e. the N>1 resume round-trip isn't covered by the existing tests, which is likely why this went unnoticed.

Suggested fix

Have _build_command_from_agui_resume build the framework-native id-keyed resume for the N>1 case (and, uniformly, for the single case):

Command(resume={entry.interrupt_id: <payload-or-cancelled-sentinel> for entry in resume_entries})

so LangGraph routes each pending interrupt() its own value directly.

Behavior-change note: this changes the documented multi-interrupt handler contract — with the native-keyed form, each interrupt() receives its own payload directly, rather than the full __agui_resume_map__ that handlers currently self-select from. That is arguably a simplification (no handler-side map lookup), and it appears to be the only option compatible with LangGraph's id-keyed multi-interrupt resume — but it's a contract change worth a deliberate call + a README update.

Context

Found while adopting #1945's canonical RunFinished.outcome + RunAgentInput.resume[] protocol in a downstream LangGraph agent. We currently work around it by subclassing LangGraphAgent and overriding _build_command_from_agui_resume to emit the native id-keyed form; happy to open a PR against the base method if the behavior change above is acceptable.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions