ag-ui-protocol / ag-ui-protocol/ag-ui

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

Abierto
#2,178 2 comentarios 0 reacciones 0 asignados Ver en GitHub
bug Integration
Lenguaje dominante
Python
Estrellas
15.9k
Forks
1.4k
Merge medio
1 d 17 h
PR fusionados (30 d)
163

Descripción

## 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:

```python
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=, status="resolved", payload=...), ResumeEntry(interrupt_id=, 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):

```python
Command(resume={entry.interrupt_id: 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.

Guía de contribución

Abrir la guía de contribución

Evaluación

Este issue todavía no se ha evaluado.

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.