@langchain/langgraph-sdk 1.10.0 → 1.10.3-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -145,6 +145,16 @@ var StreamController = class {
145
145
  #threadEventUnsubscribe;
146
146
  #disposed = false;
147
147
  #pendingDisposeTimer = null;
148
+ /**
149
+ * Interrupt ids this controller has locally marked resolved via
150
+ * {@link respond} / {@link respondAll}. Used only to suppress
151
+ * *historical* SSE replay of those `input.requested` events (there
152
+ * is no `input.responded` protocol event). Live events after the
153
+ * resume barrier — or a post-resume reconcile against
154
+ * `state.tasks[].interrupts` — clear an id from this set so a
155
+ * still-pending server interrupt can reappear on
156
+ * `rootStore.interrupts`.
157
+ */
148
158
  #resolvedInterrupts = /* @__PURE__ */ new Set();
149
159
  /**
150
160
  * Set of interrupt IDs the server reports as currently *active* on
@@ -559,24 +569,8 @@ var StreamController = class {
559
569
  */
560
570
  if (Array.isArray(state?.tasks)) {
561
571
  const generationAtFetch = this.#submitGeneration;
562
- const activeInterrupts = [];
563
- const activeIds = /* @__PURE__ */ new Set();
564
- for (const task of state.tasks) {
565
- if (!Array.isArray(task?.interrupts)) continue;
566
- const checkpointNs = task?.checkpoint?.checkpoint_ns;
567
- const namespace = typeof checkpointNs === "string" && checkpointNs.length > 0 ? checkpointNs.split("|").filter((segment) => segment.length > 0) : [...ROOT_NAMESPACE];
568
- for (const interrupt of task.interrupts) {
569
- const typed = interrupt;
570
- const id = typed?.id;
571
- if (typeof id !== "string" || activeIds.has(id)) continue;
572
- activeIds.add(id);
573
- activeInterrupts.push(require_interrupts.normalizeInterruptForClient({
574
- id,
575
- value: typed?.value,
576
- namespace: Array.isArray(typed?.namespace) ? [...typed.namespace] : namespace
577
- }));
578
- }
579
- }
572
+ const { activeInterrupts, activeIds } = collectActiveInterruptsFromTasks(state.tasks);
573
+ for (const id of activeIds) this.#resolvedInterrupts.delete(id);
580
574
  this.rootStore.setState((s) => ({
581
575
  ...s,
582
576
  interrupts: activeInterrupts,
@@ -1729,8 +1723,12 @@ var StreamController = class {
1729
1723
  if (event.method !== "input.requested") return;
1730
1724
  const data = event.params.data;
1731
1725
  const interruptId = data?.interrupt_id;
1732
- if (typeof interruptId !== "string" || this.#resolvedInterrupts.has(interruptId)) return;
1726
+ if (typeof interruptId !== "string") return;
1733
1727
  const eventSeq = typeof event.seq === "number" ? event.seq : void 0;
1728
+ if (this.#resolvedInterrupts.has(interruptId)) {
1729
+ if (!(this.#hydratedActiveInterruptIds == null || typeof this.#interruptReplayThroughSeq === "number" && eventSeq != null && eventSeq > this.#interruptReplayThroughSeq)) return;
1730
+ this.#resolvedInterrupts.delete(interruptId);
1731
+ }
1734
1732
  const isUnknownInterrupt = this.#hydratedActiveInterruptIds != null && !this.#hydratedActiveInterruptIds.has(interruptId);
1735
1733
  if (isUnknownInterrupt && this.#interruptCommandPending) {
1736
1734
  this.#pendingInterruptEvents.push(event);
@@ -1794,8 +1792,12 @@ var StreamController = class {
1794
1792
  return [...ROOT_NAMESPACE];
1795
1793
  }
1796
1794
  /**
1797
- * Mark an interrupt resolved for replay filtering and mirror the
1798
- * removal into the root snapshot the framework hooks read.
1795
+ * Mark an interrupt resolved for historical-replay filtering and
1796
+ * mirror the removal into the root snapshot the framework hooks read.
1797
+ *
1798
+ * Tombstones are not permanent: a live `input.requested` after the
1799
+ * resume barrier, or {@link #reconcilePendingInterruptsFromServer},
1800
+ * clears the id when the server still reports it as pending.
1799
1801
  */
1800
1802
  #markInterruptResolvedInRootStore(interruptId) {
1801
1803
  this.#resolvedInterrupts.add(interruptId);
@@ -1810,6 +1812,38 @@ var StreamController = class {
1810
1812
  });
1811
1813
  }
1812
1814
  /**
1815
+ * Re-sync `rootStore.interrupts` from the server's authoritative
1816
+ * `state.tasks[].interrupts` after a resumed run settles.
1817
+ *
1818
+ * Sequential `respond()` of parallel interrupts optimistically clears
1819
+ * each id client-side. When the server still has siblings pending and
1820
+ * does not re-emit `input.requested` for them (session map already
1821
+ * holds the id), this restore keeps `stream.interrupts` truthful so
1822
+ * callers do not free-text `submit()` into an ambiguous resume.
1823
+ *
1824
+ * Uses the same transport-aware state fetch as {@link hydrate}, and
1825
+ * drops the result if the thread swapped or a new submit/respond
1826
+ * started while the fetch was in flight.
1827
+ */
1828
+ async #reconcilePendingInterruptsFromServer() {
1829
+ const threadId = this.#currentThreadId;
1830
+ if (threadId == null || this.#disposed) return;
1831
+ const generationAtFetch = this.#submitGeneration;
1832
+ try {
1833
+ const state = await this.#fetchHydrationState();
1834
+ if (this.#disposed || this.#currentThreadId !== threadId || this.#submitGeneration !== generationAtFetch) return;
1835
+ if (!Array.isArray(state?.tasks)) return;
1836
+ const { activeInterrupts, activeIds } = collectActiveInterruptsFromTasks(state.tasks);
1837
+ for (const id of activeIds) this.#resolvedInterrupts.delete(id);
1838
+ this.rootStore.setState((s) => ({
1839
+ ...s,
1840
+ interrupts: activeInterrupts,
1841
+ interrupt: activeInterrupts[0]
1842
+ }));
1843
+ this.#hydratedActiveInterruptIds = activeIds;
1844
+ } catch {}
1845
+ }
1846
+ /**
1813
1847
  * Resolve on the next root-namespace terminal lifecycle event
1814
1848
  * (`completed` / `failed` / `interrupted`) or on abort.
1815
1849
  *
@@ -1834,9 +1868,15 @@ var StreamController = class {
1834
1868
  * but before `respondInput` calls `#prepareForNextRun`; accepting that
1835
1869
  * terminal would unsubscribe the watcher before the resumed run's `failed`
1836
1870
  * terminal arrives.
1871
+ *
1872
+ * On `interrupted` / `completed`, reconciles pending interrupts from
1873
+ * server state so optimistic clears from {@link respond} cannot leave
1874
+ * `stream.interrupts` empty while the checkpoint still has siblings.
1837
1875
  */
1838
- #awaitResumedRunTerminal(signal) {
1839
- return this.#awaitRootTerminal(signal, { skipInterruptedUntilRunning: true });
1876
+ async #awaitResumedRunTerminal(signal) {
1877
+ const terminal = await this.#awaitRootTerminal(signal, { skipInterruptedUntilRunning: true });
1878
+ if (!signal.aborted && !this.#disposed && (terminal.event === "interrupted" || terminal.event === "completed")) await this.#reconcilePendingInterruptsFromServer();
1879
+ return terminal;
1840
1880
  }
1841
1881
  #awaitRootTerminal(signal, options) {
1842
1882
  return new Promise((resolve) => {
@@ -1928,6 +1968,38 @@ function extractAndCoerceMessagesWithFallback(values, messagesKey) {
1928
1968
  if (!Array.isArray(raw)) return null;
1929
1969
  return require_message_coercion.ensureMessageInstances(raw);
1930
1970
  }
1971
+ /**
1972
+ * Collect pending interrupts from a `threads.getState()` `tasks` array.
1973
+ *
1974
+ * Shared by {@link StreamController.hydrate} and the post-resume
1975
+ * reconcile path so both stay aligned on namespace derivation and
1976
+ * id dedupe.
1977
+ */
1978
+ function collectActiveInterruptsFromTasks(tasks) {
1979
+ const activeInterrupts = [];
1980
+ const activeIds = /* @__PURE__ */ new Set();
1981
+ for (const task of tasks) {
1982
+ if (!Array.isArray(task?.interrupts)) continue;
1983
+ const typedTask = task;
1984
+ const checkpointNs = typedTask.checkpoint?.checkpoint_ns;
1985
+ const namespace = typeof checkpointNs === "string" && checkpointNs.length > 0 ? checkpointNs.split("|").filter((segment) => segment.length > 0) : [...ROOT_NAMESPACE];
1986
+ for (const interrupt of typedTask.interrupts) {
1987
+ const typed = interrupt;
1988
+ const id = typed?.id;
1989
+ if (typeof id !== "string" || activeIds.has(id)) continue;
1990
+ activeIds.add(id);
1991
+ activeInterrupts.push(require_interrupts.normalizeInterruptForClient({
1992
+ id,
1993
+ value: typed?.value,
1994
+ namespace: Array.isArray(typed?.namespace) ? [...typed.namespace] : namespace
1995
+ }));
1996
+ }
1997
+ }
1998
+ return {
1999
+ activeInterrupts,
2000
+ activeIds
2001
+ };
2002
+ }
1931
2003
  //#endregion
1932
2004
  exports.ROOT_PUMP_CHANNELS = ROOT_PUMP_CHANNELS;
1933
2005
  exports.StreamController = StreamController;