From 2cadc25883d684e3f25e494989b35b348d16dc28 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Fri, 8 May 2026 21:54:28 -0700 Subject: [PATCH] fix(langgraph): extract __interrupt__ from values/updates events MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LangGraph emits interrupts as part of `values` and `updates` events under the special `__interrupt__` key, not as standalone `interrupt` events. The bridge was only handling the latter, so `agent.interrupt()` / `agent.interrupts()` never populated for real graphs and any UI gated on those signals (e.g. ) stayed hidden. Adds an `extractInterrupts` helper called from the values/updates branches that: - Mirrors `__interrupt__` payloads onto `interrupt$` (latest) and `interrupts$` (full list). - Clears both subjects when a values/updates event arrives without an `__interrupt__` key — LangGraph does not emit a separate "cleared" event, so the absence is the dismissal signal once a resume completes. Verified end-to-end against the canonical examples/chat demo: the approval welcome suggestion now surfaces , and all four actions (Accept / Edit / Respond / Ignore) resume the graph and dismiss the panel correctly. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../lib/internals/stream-manager.bridge.ts | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/libs/langgraph/src/lib/internals/stream-manager.bridge.ts b/libs/langgraph/src/lib/internals/stream-manager.bridge.ts index 7ff6558cb..99b66c712 100644 --- a/libs/langgraph/src/lib/internals/stream-manager.bridge.ts +++ b/libs/langgraph/src/lib/internals/stream-manager.bridge.ts @@ -420,6 +420,7 @@ export function createStreamManagerBridge( + payload: unknown, + subjects: StreamSubjects, +): void { + if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return; + const raw = (payload as Record)['__interrupt__']; + // Cast through unknown — Interrupt$ is parameterized over Bag's InterruptType, + // and the SDK delivers raw Interrupt payloads here. + if (Array.isArray(raw) && raw.length > 0) { + const list = raw as Interrupt[]; + subjects.interrupts$.next(list as unknown as Parameters[0]); + subjects.interrupt$.next(list[list.length - 1] as unknown as Parameters[0]); + return; + } + // Payload has no `__interrupt__` key. Clear any stale interrupt so the UI + // dismisses the panel after a resume completes (LangGraph does not emit a + // separate "cleared" event — the absence of `__interrupt__` in subsequent + // values/updates is the signal). No-op if interrupt$ was already empty. + if (subjects.interrupt$.value !== undefined) { + subjects.interrupt$.next(undefined); + subjects.interrupts$.next([]); + } +} + function extractEventData(event: StreamEvent): unknown { // Try event['data'] first (SDK format from normalizeSdkEvent) const d = event['data'];