@langchain/langgraph-sdk 1.9.15 → 1.9.17

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.
Files changed (108) hide show
  1. package/dist/client/base.cjs +70 -4
  2. package/dist/client/base.cjs.map +1 -1
  3. package/dist/client/base.d.cts +3 -0
  4. package/dist/client/base.d.cts.map +1 -1
  5. package/dist/client/base.d.ts +3 -0
  6. package/dist/client/base.d.ts.map +1 -1
  7. package/dist/client/base.js +70 -4
  8. package/dist/client/base.js.map +1 -1
  9. package/dist/client/threads/index.cjs +4 -2
  10. package/dist/client/threads/index.cjs.map +1 -1
  11. package/dist/client/threads/index.d.cts.map +1 -1
  12. package/dist/client/threads/index.d.ts.map +1 -1
  13. package/dist/client/threads/index.js +4 -2
  14. package/dist/client/threads/index.js.map +1 -1
  15. package/dist/stream/controller.cjs +496 -46
  16. package/dist/stream/controller.cjs.map +1 -1
  17. package/dist/stream/controller.d.cts +15 -0
  18. package/dist/stream/controller.d.cts.map +1 -1
  19. package/dist/stream/controller.d.ts +15 -0
  20. package/dist/stream/controller.d.ts.map +1 -1
  21. package/dist/stream/controller.js +517 -46
  22. package/dist/stream/controller.js.map +1 -1
  23. package/dist/stream/discovery/index.cjs +2 -0
  24. package/dist/stream/discovery/index.js +3 -0
  25. package/dist/stream/discovery/namespace-from-history.cjs +207 -0
  26. package/dist/stream/discovery/namespace-from-history.cjs.map +1 -0
  27. package/dist/stream/discovery/namespace-from-history.js +204 -0
  28. package/dist/stream/discovery/namespace-from-history.js.map +1 -0
  29. package/dist/stream/discovery/subagents.cjs +56 -1
  30. package/dist/stream/discovery/subagents.cjs.map +1 -1
  31. package/dist/stream/discovery/subagents.d.cts +31 -0
  32. package/dist/stream/discovery/subagents.d.cts.map +1 -1
  33. package/dist/stream/discovery/subagents.d.ts +31 -0
  34. package/dist/stream/discovery/subagents.d.ts.map +1 -1
  35. package/dist/stream/discovery/subagents.js +56 -1
  36. package/dist/stream/discovery/subagents.js.map +1 -1
  37. package/dist/stream/discovery/subgraphs.cjs +24 -0
  38. package/dist/stream/discovery/subgraphs.cjs.map +1 -1
  39. package/dist/stream/discovery/subgraphs.d.cts +13 -0
  40. package/dist/stream/discovery/subgraphs.d.cts.map +1 -1
  41. package/dist/stream/discovery/subgraphs.d.ts +13 -0
  42. package/dist/stream/discovery/subgraphs.d.ts.map +1 -1
  43. package/dist/stream/discovery/subgraphs.js +24 -0
  44. package/dist/stream/discovery/subgraphs.js.map +1 -1
  45. package/dist/stream/index.cjs +1 -0
  46. package/dist/stream/index.js +1 -0
  47. package/dist/stream/message-coercion.cjs +101 -0
  48. package/dist/stream/message-coercion.cjs.map +1 -0
  49. package/dist/stream/message-coercion.d.ts +1 -0
  50. package/dist/stream/message-coercion.js +98 -0
  51. package/dist/stream/message-coercion.js.map +1 -0
  52. package/dist/stream/message-metadata-tracker.cjs +92 -0
  53. package/dist/stream/message-metadata-tracker.cjs.map +1 -1
  54. package/dist/stream/message-metadata-tracker.d.cts +23 -0
  55. package/dist/stream/message-metadata-tracker.d.cts.map +1 -1
  56. package/dist/stream/message-metadata-tracker.d.ts +23 -0
  57. package/dist/stream/message-metadata-tracker.d.ts.map +1 -1
  58. package/dist/stream/message-metadata-tracker.js +92 -0
  59. package/dist/stream/message-metadata-tracker.js.map +1 -1
  60. package/dist/stream/message-reconciliation.cjs +2 -2
  61. package/dist/stream/message-reconciliation.cjs.map +1 -1
  62. package/dist/stream/message-reconciliation.js +2 -2
  63. package/dist/stream/message-reconciliation.js.map +1 -1
  64. package/dist/stream/optimistic-input.cjs +86 -0
  65. package/dist/stream/optimistic-input.cjs.map +1 -0
  66. package/dist/stream/optimistic-input.d.ts +1 -0
  67. package/dist/stream/optimistic-input.js +86 -0
  68. package/dist/stream/optimistic-input.js.map +1 -0
  69. package/dist/stream/projections/messages.cjs +24 -14
  70. package/dist/stream/projections/messages.cjs.map +1 -1
  71. package/dist/stream/projections/messages.js +21 -11
  72. package/dist/stream/projections/messages.js.map +1 -1
  73. package/dist/stream/projections/tool-calls.cjs +22 -10
  74. package/dist/stream/projections/tool-calls.cjs.map +1 -1
  75. package/dist/stream/projections/tool-calls.js +22 -10
  76. package/dist/stream/projections/tool-calls.js.map +1 -1
  77. package/dist/stream/projections/values.cjs +2 -2
  78. package/dist/stream/projections/values.cjs.map +1 -1
  79. package/dist/stream/projections/values.js +1 -1
  80. package/dist/stream/projections/values.js.map +1 -1
  81. package/dist/stream/root-message-projection.cjs +130 -3
  82. package/dist/stream/root-message-projection.cjs.map +1 -1
  83. package/dist/stream/root-message-projection.js +130 -3
  84. package/dist/stream/root-message-projection.js.map +1 -1
  85. package/dist/stream/submit-coordinator.cjs +100 -6
  86. package/dist/stream/submit-coordinator.cjs.map +1 -1
  87. package/dist/stream/submit-coordinator.d.cts.map +1 -1
  88. package/dist/stream/submit-coordinator.d.ts +0 -1
  89. package/dist/stream/submit-coordinator.d.ts.map +1 -1
  90. package/dist/stream/submit-coordinator.js +100 -6
  91. package/dist/stream/submit-coordinator.js.map +1 -1
  92. package/dist/stream/tool-calls.cjs +32 -0
  93. package/dist/stream/tool-calls.cjs.map +1 -1
  94. package/dist/stream/tool-calls.js +32 -1
  95. package/dist/stream/tool-calls.js.map +1 -1
  96. package/dist/stream/types.d.cts +43 -0
  97. package/dist/stream/types.d.cts.map +1 -1
  98. package/dist/stream/types.d.ts +43 -0
  99. package/dist/stream/types.d.ts.map +1 -1
  100. package/dist/ui/index.d.cts +1 -1
  101. package/dist/ui/index.d.ts +1 -1
  102. package/dist/ui/messages.cjs +4 -50
  103. package/dist/ui/messages.cjs.map +1 -1
  104. package/dist/ui/messages.d.cts.map +1 -1
  105. package/dist/ui/messages.d.ts.map +1 -1
  106. package/dist/ui/messages.js +3 -48
  107. package/dist/ui/messages.js.map +1 -1
  108. package/package.json +4 -4
@@ -1,5 +1,7 @@
1
- const require_messages = require("../ui/messages.cjs");
1
+ require("../_virtual/_rolldown/runtime.cjs");
2
+ const require_message_coercion = require("./message-coercion.cjs");
2
3
  const require_tools = require("../client/stream/handles/tools.cjs");
4
+ require("./constants.cjs");
3
5
  const require_headless_tools = require("../headless-tools.cjs");
4
6
  const require_store = require("./store.cjs");
5
7
  const require_channel_registry = require("./channel-registry.cjs");
@@ -8,11 +10,15 @@ const require_interrupts = require("../ui/interrupts.cjs");
8
10
  const require_namespace = require("./namespace.cjs");
9
11
  const require_subagents = require("./discovery/subagents.cjs");
10
12
  const require_subgraphs = require("./discovery/subgraphs.cjs");
13
+ require("./discovery/index.cjs");
14
+ const require_namespace_from_history = require("./discovery/namespace-from-history.cjs");
11
15
  const require_message_metadata_tracker = require("./message-metadata-tracker.cjs");
12
16
  const require_lifecycle_loading_tracker = require("./lifecycle-loading-tracker.cjs");
13
17
  const require_root_message_projection = require("./root-message-projection.cjs");
18
+ const require_optimistic_input = require("./optimistic-input.cjs");
14
19
  const require_submit_coordinator = require("./submit-coordinator.cjs");
15
20
  const require_tool_calls = require("./tool-calls.cjs");
21
+ let uuid = require("uuid");
16
22
  require("@langchain/core/messages");
17
23
  //#region src/stream/controller.ts
18
24
  /**
@@ -48,6 +54,43 @@ function lifecycleReason(event) {
48
54
  if (event === "interrupted") return "interrupt";
49
55
  return null;
50
56
  }
57
+ /**
58
+ * Decide whether a hydrated thread is *active* (a run is executing or
59
+ * paused awaiting resume) from the `getState()` snapshot alone — no
60
+ * extra request.
61
+ *
62
+ * Why this gate exists: a finished thread does not need either of the
63
+ * always-on SSE pumps. Subagent/subgraph cards are already seeded from
64
+ * the `getState()` messages and a single bounded `getHistory()` page, so
65
+ * opening the depth-1 content pump + the wildcard lifecycle watcher only
66
+ * to replay a completed run and then idle forever is pure waste. We open
67
+ * the pumps eagerly only when the thread is active; otherwise they come
68
+ * up on the first local `submit()` (the existing deferred-pump path) or
69
+ * a thread swap that lands on an active thread.
70
+ *
71
+ * The gate is deliberately conservative: we only conclude *idle* when
72
+ * the state proves it. A thread is treated as active unless `next` is a
73
+ * present, empty array AND no task carries a pending interrupt:
74
+ * - `next` missing / not an array: unknown shape (a server or custom
75
+ * client may omit it). Treat as active so an already-running
76
+ * server-side run is still observed on reconnect — never silently
77
+ * disable streaming on an unfamiliar `getState` shape.
78
+ * - `next.length > 0`: the checkpoint still has nodes to execute, i.e.
79
+ * a run is mid-flight or paused at an interrupt.
80
+ * - `next` is `[]` but a `tasks[].interrupts` is non-empty: the thread
81
+ * is interrupted and a resume (which starts a run) must be observable.
82
+ * - `next` is `[]` and no pending interrupts: a completed run → idle.
83
+ */
84
+ function isThreadStateActive(state) {
85
+ if (state == null) return true;
86
+ if (!Array.isArray(state.next)) return true;
87
+ if (state.next.length > 0) return true;
88
+ if (Array.isArray(state.tasks)) for (const task of state.tasks) {
89
+ const interrupts = task?.interrupts;
90
+ if (Array.isArray(interrupts) && interrupts.length > 0) return true;
91
+ }
92
+ return false;
93
+ }
51
94
  const ROOT_NAMESPACE = [];
52
95
  /**
53
96
  * Channel set covered by the always-on root subscription. Exported so
@@ -133,11 +176,37 @@ var StreamController = class {
133
176
  * request would 404 and surface a spurious error to the UI).
134
177
  */
135
178
  #selfCreatedThreadIds = /* @__PURE__ */ new Set();
179
+ /**
180
+ * In-flight per-subagent namespace resolutions, keyed by tool-call
181
+ * id. De-dupes concurrent {@link resolveSubagentNamespace} calls so
182
+ * re-renders / multiple consumers of the same subagent don't issue
183
+ * parallel `getHistory` walks.
184
+ */
185
+ #namespaceResolves = /* @__PURE__ */ new Map();
186
+ /**
187
+ * In-flight hydrate-time discovery seed ({@link #seedDiscoveryFromHistory}):
188
+ * a single bounded `getHistory` page that bulk-promotes every
189
+ * still-default subagent namespace and seeds subgraph hosts. Per-card
190
+ * {@link resolveSubagentNamespace} calls await this shared promise
191
+ * instead of each firing their own `getHistory` walk, so opening N
192
+ * cards right after reconnect costs one history read, not N. Re-armed
193
+ * per hydrate cycle and cleared once it settles.
194
+ */
195
+ #discoverySeedPromise;
196
+ #scopedHistorySeeds = /* @__PURE__ */ new Map();
136
197
  #rootEventListeners = /* @__PURE__ */ new Set();
137
198
  #rootBus;
138
199
  #activeRunId;
139
200
  #localRunDepth = 0;
140
201
  /**
202
+ * `true` once a root `values` event has been applied for the current
203
+ * optimistic run. Reset to `false` in {@link #beginOptimistic} and
204
+ * read in {@link #settleOptimistic}: when a run terminates without
205
+ * the server ever echoing a `values` snapshot, optimistically-merged
206
+ * non-message keys are rolled back to their pre-submit values.
207
+ */
208
+ #sawValuesForRun = false;
209
+ /**
141
210
  * Single-shot hydration promise. Exposed via `hydrationPromise`
142
211
  * so Suspense wrappers can throw it until the first hydrate
143
212
  * completes (resolve) or fails (reject). Reset whenever a new
@@ -172,7 +241,8 @@ var StreamController = class {
172
241
  return () => {
173
242
  this.#rootEventListeners.delete(listener);
174
243
  };
175
- }
244
+ },
245
+ trySeedFromHistory: (params) => this.#trySeedProjectionFromHistory(params)
176
246
  };
177
247
  this.registry = new require_channel_registry.ChannelRegistry(this.#rootBus);
178
248
  this.subagentStore = this.#subagents.store;
@@ -210,6 +280,7 @@ var StreamController = class {
210
280
  },
211
281
  waitForRootPumpReady: () => this.#rootPumpReady,
212
282
  awaitNextTerminal: (signal) => this.#awaitNextTerminal(signal),
283
+ awaitResumedRunTerminal: (signal) => this.#awaitResumedRunTerminal(signal),
213
284
  onSubmitStart: () => {
214
285
  this.#hydratedActiveInterruptIds = null;
215
286
  this.#submitGeneration += 1;
@@ -217,7 +288,9 @@ var StreamController = class {
217
288
  onRunStart: () => this.#markLocalRunStart(),
218
289
  onRunCreated: (runId) => this.#notifyCreated(runId),
219
290
  onRunCompleted: (reason, runId) => this.#notifyCompleted(reason, runId),
220
- onRunEnd: () => this.#markLocalRunEnd()
291
+ onRunEnd: () => this.#markLocalRunEnd(),
292
+ beginOptimistic: (input) => this.#beginOptimistic(input),
293
+ settleOptimistic: (handle, event) => this.#settleOptimistic(handle, event)
221
294
  });
222
295
  this.#hydrationPromise = this.#createHydrationPromise();
223
296
  /**
@@ -284,6 +357,8 @@ var StreamController = class {
284
357
  const target = threadId === void 0 ? this.#currentThreadId : threadId;
285
358
  const changed = target !== this.#currentThreadId;
286
359
  this.#currentThreadId = target ?? null;
360
+ this.#discoverySeedPromise = void 0;
361
+ this.#scopedHistorySeeds.clear();
287
362
  this.rootStore.setState((s) => ({
288
363
  ...s,
289
364
  threadId: this.#currentThreadId
@@ -340,9 +415,11 @@ var StreamController = class {
340
415
  }));
341
416
  let hydrationError;
342
417
  let threadExists = false;
418
+ let threadActive = true;
343
419
  try {
344
420
  const state = await this.#options.client.threads.getState(this.#currentThreadId);
345
421
  threadExists = state != null;
422
+ threadActive = isThreadStateActive(state);
346
423
  if (state?.values != null) {
347
424
  /**
348
425
  * `threads.getState()` returns the legacy `ThreadState` shape
@@ -354,11 +431,43 @@ var StreamController = class {
354
431
  */
355
432
  const checkpointId = state.checkpoint?.checkpoint_id;
356
433
  const parentCheckpointId = state.parent_checkpoint?.checkpoint_id ?? void 0;
434
+ /**
435
+ * Carry the checkpoint `step` from `getState()` metadata so the
436
+ * root message projection treats this seed as the authoritative
437
+ * latest superstep. The content pump's reconnect replay emits
438
+ * older checkpoints (lower step); marking the seed's step lets
439
+ * the projection reject those as stale instead of letting them
440
+ * remove the seeded message tail (the final assistant turn).
441
+ */
442
+ const seedStep = state.metadata?.step;
357
443
  const syntheticCheckpoint = typeof checkpointId === "string" ? {
358
444
  id: checkpointId,
359
- ...parentCheckpointId != null ? { parent_id: parentCheckpointId } : {}
445
+ ...parentCheckpointId != null ? { parent_id: parentCheckpointId } : {},
446
+ ...typeof seedStep === "number" ? { step: seedStep } : {}
360
447
  } : void 0;
361
448
  this.#applyValues(state.values, syntheticCheckpoint);
449
+ /**
450
+ * Seed subagent discovery from checkpoint messages so deep-agent
451
+ * cards render on refresh without waiting for SSE replay. Zero
452
+ * extra HTTP (reuses the `getState` payload); mirrors the
453
+ * interrupt-seeding below. `#subagents` was cleared in
454
+ * `#teardownThread`, and `seedFromCheckpointMessages` is
455
+ * idempotent, so this is safe on re-hydrate.
456
+ */
457
+ const seedMessages = state.values[this.#messagesKey];
458
+ if (Array.isArray(seedMessages)) this.#subagents.seedFromCheckpointMessages(seedMessages);
459
+ }
460
+ /**
461
+ * Converge to server truth: drop any optimistic messages the
462
+ * server state does not contain (`pending` / `failed` that were
463
+ * never persisted — e.g. a failed run's user message). Echoed
464
+ * ids were flipped to `"sent"` by `#applyValues` above and so are
465
+ * excluded from `unpersistedOptimisticIds()`.
466
+ */
467
+ const unpersisted = this.#messageMetadata.unpersistedOptimisticIds();
468
+ if (unpersisted.size > 0) {
469
+ this.#rootMessages.dropOptimisticMessages(unpersisted);
470
+ this.#messageMetadata.forget(unpersisted);
362
471
  }
363
472
  /**
364
473
  * Sync the visible interrupt list to the server's authoritative
@@ -412,27 +521,253 @@ var StreamController = class {
412
521
  else this.#resolveHydration();
413
522
  }
414
523
  /**
415
- * P0 fix: open the shared subscription on mount so in-flight
416
- * server-side runs are observed even when no local `submit()` is
417
- * active. The transport replays the run from `seq=0` on a rotating
418
- * subscribe, so late-joining is free once the subscription exists.
419
- * `isLoading` transitions are driven by the persistent root
420
- * lifecycle listener registered in `#startRootPump`.
524
+ * Open the shared subscription on mount so in-flight server-side
525
+ * runs are observed even when no local `submit()` is active — BUT
526
+ * only when the thread is actually active (see
527
+ * {@link isThreadStateActive}). A finished thread's cards are seeded
528
+ * from `getState()` + the bounded `getHistory()` below, so opening
529
+ * the depth-1 content pump just to replay a completed run and idle
530
+ * forever is pure waste. When idle we take the deferred path: the
531
+ * pump (and watcher) come up on the first local `submit()` via
532
+ * {@link #startDeferredRootPump}, exactly like a self-created thread.
533
+ * The transport replays from `seq=0` on the deferred subscribe, so
534
+ * nothing is missed.
421
535
  */
422
- const thread = this.#ensureThread(this.#currentThreadId);
536
+ const thread = this.#ensureThread(this.#currentThreadId, !threadActive);
423
537
  /**
424
- * Start the wildcard lifecycle watcher up-front for existing
425
- * threads. The root content pump runs at `depth: 1`, which covers
426
- * root-namespace and one-deep events but not arbitrarily-nested
427
- * subagent / subgraph lifecycle — the dedicated watcher handles
428
- * those.
538
+ * Start the wildcard lifecycle watcher up-front for existing,
539
+ * active threads. The root content pump runs at `depth: 1`, which
540
+ * covers root-namespace and one-deep events but not arbitrarily-
541
+ * nested subagent / subgraph lifecycle — the dedicated watcher
542
+ * handles those.
429
543
  *
430
- * For self-created (new) threads we skip — the watcher would 404
431
- * against a not-yet-existent thread. `submitRun` / `respondInput`
432
- * call `startLifecycleWatcher` on first submission to cover that
433
- * case.
544
+ * Skipped when:
545
+ * - the thread is idle/finished — there are no live events to
546
+ * watch; discovery is seeded from history below, and the watcher
547
+ * starts with the deferred pump on the first `submit()`.
548
+ * - the thread is self-created (new) — the watcher would 404
549
+ * against a not-yet-existent thread; `submitRun` / `respondInput`
550
+ * call `startLifecycleWatcher` on first submission instead.
434
551
  */
435
- if (threadExists) thread.startLifecycleWatcher();
552
+ if (threadExists && threadActive) thread.startLifecycleWatcher();
553
+ if (threadExists) {
554
+ /**
555
+ * Seed subgraph discovery and promote subagent execution
556
+ * namespaces from a single bounded `getHistory` page. Subgraph
557
+ * structure is not present in the root checkpoint messages
558
+ * (unlike subagents), so it can only be reconstructed from
559
+ * history. Fire-and-forget — not awaited into the hydration
560
+ * promise, so Suspense / first paint stay unblocked; cards fill
561
+ * in progressively when it resolves.
562
+ *
563
+ * Held in `#discoverySeedPromise` so lazy per-card
564
+ * {@link resolveSubagentNamespace} calls coalesce onto this single
565
+ * read instead of each firing their own `getHistory` walk.
566
+ */
567
+ const seed = this.#seedDiscoveryFromHistory(this.#currentThreadId).finally(() => {
568
+ if (this.#discoverySeedPromise === seed) this.#discoverySeedPromise = void 0;
569
+ });
570
+ this.#discoverySeedPromise = seed;
571
+ }
572
+ }
573
+ /**
574
+ * One bounded, non-blocking `getHistory` read at hydrate that seeds
575
+ * subgraph hosts and bulk-promotes still-default subagent execution
576
+ * namespaces. O(1) in requests regardless of subagent/subgraph count.
577
+ */
578
+ async #seedDiscoveryFromHistory(threadId) {
579
+ try {
580
+ const history = await require_namespace_from_history.getHistoryPage(this.#options.client, threadId, { limit: 20 });
581
+ if (this.#disposed || this.#currentThreadId !== threadId) return;
582
+ this.#primeScopedHistorySeedsFromHistory(threadId, history);
583
+ const hosts = require_namespace_from_history.collectSubgraphHostNamespaces(history);
584
+ this.#subgraphs.seedFromHistory(hosts);
585
+ const defaultOnlyIds = [...this.#subagents.snapshot.values()].filter(namespaceIsDefaultOnly).map((entry) => entry.id);
586
+ if (defaultOnlyIds.length > 0) {
587
+ const map = require_namespace_from_history.mapSubagentNamespaces(history, defaultOnlyIds, this.#messagesKey);
588
+ for (const [id, segment] of map) this.#subagents.applyExecutionNamespace(id, segment);
589
+ }
590
+ } catch {}
591
+ }
592
+ /**
593
+ * Lazily resolve a single subagent's execution namespace from
594
+ * checkpoint history. Intended call site: the first scoped
595
+ * `useMessages` / `useToolCalls` mount for a subagent whose namespace
596
+ * is still the default `tools:<toolCallId>`. A fallback for the
597
+ * hydrate-time bulk seed ({@link #seedDiscoveryFromHistory}) — most
598
+ * subagents are already promoted by the time a panel opens.
599
+ *
600
+ * Skips ids already promoted past default-only (SSE replay or a prior
601
+ * resolve). Concurrent calls for the same id share one `getHistory`
602
+ * walk via {@link #namespaceResolves}.
603
+ *
604
+ * @param toolCallId - Parent `task` tool-call id (the subagent's discovery key).
605
+ */
606
+ async resolveSubagentNamespace(toolCallId) {
607
+ if (this.#disposed) return;
608
+ const threadId = this.#currentThreadId;
609
+ if (threadId == null) return;
610
+ if (!namespaceIsDefaultOnly(this.#subagents.snapshot.get(toolCallId))) return;
611
+ const inflight = this.#namespaceResolves.get(toolCallId);
612
+ if (inflight != null) return inflight;
613
+ const run = (async () => {
614
+ try {
615
+ /**
616
+ * Coalesce onto the hydrate-time discovery seed. That single
617
+ * bounded `getHistory` page bulk-promotes every default-only
618
+ * subagent, so when many cards mount at once (the common
619
+ * reconnect case) they all await this one read instead of each
620
+ * firing their own walk. Re-check after it settles: usually the
621
+ * bulk seed already promoted us and no further fetch is needed.
622
+ */
623
+ const seed = this.#discoverySeedPromise;
624
+ if (seed != null) {
625
+ await seed;
626
+ if (this.#disposed || this.#currentThreadId !== threadId) return;
627
+ if (!namespaceIsDefaultOnly(this.#subagents.snapshot.get(toolCallId))) return;
628
+ }
629
+ const map = await require_namespace_from_history.resolveSubagentNamespaces(this.#options.client, threadId, [toolCallId], { messagesKey: this.#messagesKey });
630
+ if (this.#disposed || this.#currentThreadId !== threadId) return;
631
+ const segment = map.get(toolCallId);
632
+ if (segment != null) this.#subagents.applyExecutionNamespace(toolCallId, segment);
633
+ } catch {} finally {
634
+ this.#namespaceResolves.delete(toolCallId);
635
+ }
636
+ })();
637
+ this.#namespaceResolves.set(toolCallId, run);
638
+ return run;
639
+ }
640
+ /**
641
+ * Try to satisfy a scoped selector projection from checkpoint history
642
+ * instead of opening a scoped `/events` replay.
643
+ *
644
+ * This is only valid while the root pump is deferred, which means hydrate
645
+ * has classified the thread as idle/stale. Active and interrupted threads
646
+ * must keep using SSE so ongoing work and resumes are observed. For an idle
647
+ * thread, though, a late-mounted subagent card only needs the latest scoped
648
+ * checkpoint snapshot; opening `/events` just asks the server to replay work
649
+ * that already finished and can be slow for namespaces discovered from
650
+ * history.
651
+ *
652
+ * Returns `true` when the projection was handled without `/events`. That can
653
+ * mean either the store was seeded from namespace-specific history, or the
654
+ * projection targeted a default subagent namespace that should be skipped
655
+ * because hydrate promoted it to its execution namespace. Returns `false`
656
+ * when the caller should fall back to the normal subscription path.
657
+ */
658
+ async #trySeedProjectionFromHistory(params) {
659
+ const threadId = this.#currentThreadId;
660
+ if (this.#disposed || threadId == null || params.namespace.length === 0 || !this.#rootPumpDeferred || this.#selfCreatedThreadIds.has(threadId)) return false;
661
+ if (await this.#skipDefaultSubagentProjection(params.namespace, threadId)) return true;
662
+ if (this.#disposed || this.#currentThreadId !== threadId || !this.#rootPumpDeferred) return false;
663
+ const seed = await this.#getScopedHistorySeed(threadId, params.namespace);
664
+ if (seed == null || this.#disposed || this.#currentThreadId !== threadId || !this.#rootPumpDeferred) return false;
665
+ if (await this.#skipDefaultSubagentProjection(params.namespace, threadId)) return true;
666
+ if (params.kind === "messages") {
667
+ params.store.setValue(seed.messages);
668
+ return true;
669
+ }
670
+ params.store.setValue(seed.toolCalls);
671
+ return true;
672
+ }
673
+ /**
674
+ * Suppress subscriptions for placeholder subagent namespaces once hydrate has
675
+ * resolved the real execution namespace.
676
+ *
677
+ * Deep-agent discovery first creates cards at `tools:<toolCallId>`. The
678
+ * actual worker history usually lives under a different checkpoint namespace
679
+ * such as `tools:<uuid>`, and hydrate resolves that mapping from the bounded
680
+ * root history seed. React/Vue/Svelte/Angular selector effects can mount
681
+ * while that seed is still in flight, so this helper waits for it and then
682
+ * returns `true` when the original placeholder namespace is stale. Returning
683
+ * `true` tells the projection runtime not to open an `/events` subscription
684
+ * for the wrong namespace; the framework will re-render with the promoted
685
+ * card namespace and acquire the real projection.
686
+ */
687
+ async #skipDefaultSubagentProjection(namespace, threadId) {
688
+ const toolCallId = defaultSubagentToolCallId(namespace);
689
+ if (toolCallId == null) return false;
690
+ if (!namespaceIsDefaultOnly(this.#subagents.snapshot.get(toolCallId))) return false;
691
+ const seed = this.#discoverySeedPromise;
692
+ if (seed != null) await seed;
693
+ if (this.#disposed || this.#currentThreadId !== threadId) return true;
694
+ return !namespaceIsDefaultOnly(this.#subagents.snapshot.get(toolCallId));
695
+ }
696
+ /**
697
+ * Load and cache the latest checkpoint snapshot for one scoped namespace.
698
+ *
699
+ * `useMessages(stream, subagent)` and `useToolCalls(stream, subagent)` often
700
+ * mount together. Both need the same namespace-specific history page, so the
701
+ * controller keeps an in-flight promise per `threadId + checkpoint_ns`.
702
+ * The cache may already be primed by the hydrate-time discovery history page;
703
+ * otherwise this method performs a narrow `checkpoint_ns` read and derives
704
+ * both projection snapshots from that one response:
705
+ *
706
+ * - `messages` are coerced with the stream-local message coercion rules, so
707
+ * serialized `content_blocks` and tool-call metadata hydrate correctly.
708
+ * - `toolCalls` are reconstructed from AI tool calls plus matching
709
+ * ToolMessages, enough for finished/stale card panels without replaying
710
+ * the `tools` channel.
711
+ *
712
+ * Returns `null` when history does not contain usable values, or the request
713
+ * fails. Callers treat that as a signal to fall back to `/events` so custom
714
+ * servers or unusual state shapes still work.
715
+ */
716
+ #getScopedHistorySeed(threadId, namespace) {
717
+ const checkpointNs = require_namespace.namespaceKey(namespace);
718
+ const key = `${threadId}|${checkpointNs}`;
719
+ const existing = this.#scopedHistorySeeds.get(key);
720
+ if (existing != null) return existing;
721
+ const seed = (async () => {
722
+ try {
723
+ const values = (await require_namespace_from_history.getHistoryPage(this.#options.client, threadId, {
724
+ limit: 1,
725
+ checkpoint: { checkpoint_ns: checkpointNs }
726
+ }))[0]?.values;
727
+ if (values == null || typeof values !== "object") return null;
728
+ const messages = extractAndCoerceMessagesWithFallback(values, this.#messagesKey);
729
+ if (messages == null) return null;
730
+ return {
731
+ messages,
732
+ toolCalls: require_tool_calls.seedToolCallsFromMessages(namespace, messages)
733
+ };
734
+ } catch {
735
+ return null;
736
+ }
737
+ })();
738
+ this.#scopedHistorySeeds.set(key, seed);
739
+ return seed;
740
+ }
741
+ /**
742
+ * Reuse the hydrate-time discovery history page as scoped projection data
743
+ * when it already contains checkpoint values for a namespace.
744
+ *
745
+ * The discovery read is required to resolve subagent execution namespaces and
746
+ * subgraph hosts. That same page often includes the latest values for those
747
+ * namespaces, so priming `#scopedHistorySeeds` here lets later
748
+ * `useMessages(stream, subagent)` / `useToolCalls(stream, subagent)` mounts
749
+ * hydrate from memory instead of issuing an immediate second `getHistory`
750
+ * request. If a namespace is not present in the bounded page,
751
+ * `#getScopedHistorySeed` still falls back to a targeted `checkpoint_ns`
752
+ * history read.
753
+ */
754
+ #primeScopedHistorySeedsFromHistory(threadId, history) {
755
+ for (const state of history) {
756
+ const checkpointNs = state.checkpoint?.checkpoint_ns;
757
+ if (typeof checkpointNs !== "string" || checkpointNs.length === 0) continue;
758
+ const namespace = checkpointNs.split("\0").filter((segment) => segment.length > 0);
759
+ if (namespace.length === 0) continue;
760
+ const key = `${threadId}|${require_namespace.namespaceKey(namespace)}`;
761
+ if (this.#scopedHistorySeeds.has(key)) continue;
762
+ const values = state.values;
763
+ if (values == null || typeof values !== "object") continue;
764
+ const messages = extractAndCoerceMessagesWithFallback(values, this.#messagesKey);
765
+ if (messages == null) continue;
766
+ this.#scopedHistorySeeds.set(key, Promise.resolve({
767
+ messages,
768
+ toolCalls: require_tool_calls.seedToolCallsFromMessages(namespace, messages)
769
+ }));
770
+ }
436
771
  }
437
772
  /**
438
773
  * Submit input to the active thread.
@@ -597,15 +932,18 @@ var StreamController = class {
597
932
  namespace: options.namespace ?? [...ROOT_NAMESPACE]
598
933
  } : this.#resolveInterruptForResume();
599
934
  if (resolved == null) throw new Error("No pending interrupt to respond to.");
935
+ const thread = this.#thread;
600
936
  try {
601
- await this.#thread.respondInput({
602
- namespace: resolved.namespace,
603
- interrupt_id: resolved.interruptId,
604
- response: require_hitl_interrupt_payload.normalizeHitlResponseForServer(response),
605
- config: options?.config,
606
- metadata: options?.metadata
937
+ await this.#submitter.dispatchResume(async () => {
938
+ await thread.respondInput({
939
+ namespace: resolved.namespace,
940
+ interrupt_id: resolved.interruptId,
941
+ response: require_hitl_interrupt_payload.normalizeHitlResponseForServer(response),
942
+ config: options?.config,
943
+ metadata: options?.metadata
944
+ });
945
+ this.#markInterruptResolvedInRootStore(resolved.interruptId);
607
946
  });
608
- this.#markInterruptResolvedInRootStore(resolved.interruptId);
609
947
  } catch (error) {
610
948
  if (this.#disposed && isAbortLikeError(error)) return;
611
949
  throw error;
@@ -656,19 +994,22 @@ var StreamController = class {
656
994
  if (this.#disposed || this.#thread == null) throw new Error("No active thread to respond to.");
657
995
  const entries = Object.entries(responsesById);
658
996
  if (entries.length === 0) throw new Error("respondAll() requires at least one response.");
659
- const pending = this.#thread.interrupts;
997
+ const thread = this.#thread;
998
+ const pending = thread.interrupts;
660
999
  const responses = entries.map(([interruptId, response]) => ({
661
1000
  interrupt_id: interruptId,
662
1001
  response: require_hitl_interrupt_payload.normalizeHitlResponseForServer(response),
663
1002
  namespace: pending.find((entry) => entry.interruptId === interruptId)?.namespace ?? [...ROOT_NAMESPACE]
664
1003
  }));
665
1004
  try {
666
- await this.#thread.respondInput({
667
- responses,
668
- config: options?.config,
669
- metadata: options?.metadata
1005
+ await this.#submitter.dispatchResume(async () => {
1006
+ await thread.respondInput({
1007
+ responses,
1008
+ config: options?.config,
1009
+ metadata: options?.metadata
1010
+ });
1011
+ for (const { interrupt_id: interruptId } of responses) this.#markInterruptResolvedInRootStore(interruptId);
670
1012
  });
671
- for (const { interrupt_id: interruptId } of responses) this.#markInterruptResolvedInRootStore(interruptId);
672
1013
  } catch (error) {
673
1014
  if (this.#disposed && isAbortLikeError(error)) return;
674
1015
  throw error;
@@ -865,6 +1206,7 @@ var StreamController = class {
865
1206
  this.#lifecycleLoading.reset();
866
1207
  this.#subagents.reset();
867
1208
  this.#subgraphs.reset();
1209
+ this.#scopedHistorySeeds.clear();
868
1210
  this.#activeRunId = void 0;
869
1211
  this.#localRunDepth = 0;
870
1212
  this.#messageMetadata.reset();
@@ -1141,9 +1483,67 @@ var StreamController = class {
1141
1483
  * @param raw - Raw `values` channel payload.
1142
1484
  * @param checkpoint - Optional checkpoint envelope paired with the values event.
1143
1485
  */
1486
+ /**
1487
+ * Apply a submit input optimistically to the root projection before
1488
+ * the server responds. Mints stable ids for id-less messages (so the
1489
+ * server echo reconciles by id), appends them to the projection, and
1490
+ * shallow-merges non-message input keys into `values`.
1491
+ *
1492
+ * Returns the dispatch payload (id-injected) for the coordinator to
1493
+ * send, plus an {@link OptimisticHandle} for terminal reconciliation.
1494
+ * Returns `undefined` when optimistic UI is disabled or there is
1495
+ * nothing to echo, in which case the coordinator dispatches the raw
1496
+ * input unchanged.
1497
+ *
1498
+ * @param input - Raw input passed to `submit()`.
1499
+ */
1500
+ #beginOptimistic(input) {
1501
+ if (this.#options.optimistic === false) return void 0;
1502
+ if (input == null || typeof input !== "object" || Array.isArray(input)) return;
1503
+ const prepared = require_optimistic_input.prepareOptimisticInput(input, this.#messagesKey, () => (0, uuid.v7)());
1504
+ const extraKeys = Object.keys(prepared.extraValues);
1505
+ if (prepared.echoedIds.length === 0 && extraKeys.length === 0) return;
1506
+ const currentValues = this.rootStore.getSnapshot().values;
1507
+ const restoreKeys = extraKeys.map((key) => ({
1508
+ key,
1509
+ hadKey: Object.prototype.hasOwnProperty.call(currentValues, key),
1510
+ prevValue: currentValues[key]
1511
+ }));
1512
+ this.#sawValuesForRun = false;
1513
+ this.#rootMessages.appendOptimistic(prepared.optimisticMessages, prepared.extraValues);
1514
+ if (prepared.echoedIds.length > 0) this.#messageMetadata.markPending(prepared.echoedIds);
1515
+ return {
1516
+ dispatchInput: prepared.dispatchInput,
1517
+ handle: {
1518
+ echoedIds: prepared.echoedIds,
1519
+ restoreKeys
1520
+ }
1521
+ };
1522
+ }
1523
+ /**
1524
+ * Reconcile optimistic state when a run terminates.
1525
+ *
1526
+ * - Messages: any echoed id still `"pending"` (never echoed by the
1527
+ * server) is flipped to `"sent"` on success/interrupt, or
1528
+ * `"failed"` on failure/abort. Ids the server already echoed were
1529
+ * flipped to `"sent"` in {@link #applyValues} and are untouched.
1530
+ * - Non-message keys: rolled back to their pre-submit values when no
1531
+ * server `values` event landed during the run (otherwise the
1532
+ * server snapshot already reconciled them). Skipped on abort,
1533
+ * where a superseding run (or `stop()`) owns subsequent state.
1534
+ *
1535
+ * @param handle - Handle returned by {@link #beginOptimistic}.
1536
+ * @param event - Terminal lifecycle event for the run.
1537
+ */
1538
+ #settleOptimistic(handle, event) {
1539
+ const failed = event === "failed" || event === "aborted";
1540
+ this.#messageMetadata.resolvePending(handle.echoedIds, failed ? "failed" : "sent");
1541
+ if (event !== "aborted" && !this.#sawValuesForRun) this.#rootMessages.restoreValueKeys(handle.restoreKeys);
1542
+ }
1144
1543
  #applyValues(raw, checkpoint) {
1145
1544
  if (raw == null || typeof raw !== "object" || Array.isArray(raw)) return;
1146
1545
  const state = raw;
1546
+ this.#sawValuesForRun = true;
1147
1547
  /**
1148
1548
  * Surface parent_checkpoint per-message when the values event
1149
1549
  * carries the lightweight checkpoint envelope (populated by
@@ -1158,7 +1558,7 @@ var StreamController = class {
1158
1558
  let nextValues;
1159
1559
  let nextMessages;
1160
1560
  if (Array.isArray(maybeMessages)) {
1161
- const coerced = require_messages.ensureMessageInstances(maybeMessages);
1561
+ const coerced = require_message_coercion.ensureMessageInstances(maybeMessages);
1162
1562
  nextValues = {
1163
1563
  ...state,
1164
1564
  [this.#messagesKey]: coerced
@@ -1168,15 +1568,18 @@ var StreamController = class {
1168
1568
  nextValues = state;
1169
1569
  nextMessages = [];
1170
1570
  }
1171
- this.#rootMessages.applyValues(nextValues, nextMessages);
1172
- if (nextMessages.length > 0) this.rootStore.setState((s) => {
1173
- const toolCalls = require_tool_calls.reconcileToolCallsFromMessages(s.toolCalls, nextMessages);
1174
- if (toolCalls === s.toolCalls) return s;
1175
- return {
1176
- ...s,
1177
- toolCalls
1178
- };
1179
- });
1571
+ this.#rootMessages.applyValues(nextValues, nextMessages, { step: checkpoint?.step });
1572
+ if (nextMessages.length > 0) {
1573
+ this.#messageMetadata.resolvePending(nextMessages.map((m) => m.id).filter((id) => typeof id === "string"), "sent");
1574
+ this.rootStore.setState((s) => {
1575
+ const toolCalls = require_tool_calls.reconcileToolCallsFromMessages(s.toolCalls, nextMessages);
1576
+ if (toolCalls === s.toolCalls) return s;
1577
+ return {
1578
+ ...s,
1579
+ toolCalls
1580
+ };
1581
+ });
1582
+ }
1180
1583
  }
1181
1584
  /**
1182
1585
  * Mirror root protocol interrupts into the root snapshot.
@@ -1235,8 +1638,25 @@ var StreamController = class {
1235
1638
  * @param signal - Abort signal for the local submit lifecycle.
1236
1639
  */
1237
1640
  #awaitNextTerminal(signal) {
1641
+ return this.#awaitRootTerminal(signal, { skipInterruptedUntilRunning: false });
1642
+ }
1643
+ /**
1644
+ * Resolve on the resumed run's root terminal lifecycle.
1645
+ *
1646
+ * Unlike {@link #awaitNextTerminal}, ignores `interrupted` events until a
1647
+ * root `running` lifecycle has been observed. Headless-tool flows can emit
1648
+ * a stale `interrupted` for the run being resumed after `input.requested`
1649
+ * but before `respondInput` calls `#prepareForNextRun`; accepting that
1650
+ * terminal would unsubscribe the watcher before the resumed run's `failed`
1651
+ * terminal arrives.
1652
+ */
1653
+ #awaitResumedRunTerminal(signal) {
1654
+ return this.#awaitRootTerminal(signal, { skipInterruptedUntilRunning: true });
1655
+ }
1656
+ #awaitRootTerminal(signal, options) {
1238
1657
  return new Promise((resolve) => {
1239
1658
  let settled = false;
1659
+ let sawRunning = false;
1240
1660
  function finish(result) {
1241
1661
  if (settled) return;
1242
1662
  settled = true;
@@ -1251,12 +1671,19 @@ var StreamController = class {
1251
1671
  if (event.method !== "lifecycle") return;
1252
1672
  if (!require_namespace.isRootNamespace(event.params.namespace)) return;
1253
1673
  const lifecycle = event.params.data;
1674
+ if (lifecycle?.event === "running") {
1675
+ sawRunning = true;
1676
+ return;
1677
+ }
1254
1678
  if (lifecycle?.event === "completed") setTimeout(() => finish({ event: "completed" }), 0);
1255
1679
  else if (lifecycle?.event === "failed") setTimeout(() => finish({
1256
1680
  event: "failed",
1257
1681
  error: lifecycle.error
1258
1682
  }), 0);
1259
- else if (lifecycle?.event === "interrupted") setTimeout(() => finish({ event: "interrupted" }), 0);
1683
+ else if (lifecycle?.event === "interrupted") {
1684
+ if (options.skipInterruptedUntilRunning && !sawRunning) return;
1685
+ setTimeout(() => finish({ event: "interrupted" }), 0);
1686
+ }
1260
1687
  };
1261
1688
  const unsubscribeRoot = this.#rootBus.subscribe(onEvent);
1262
1689
  const unsubscribeThread = this.#thread?.onEvent(onEvent);
@@ -1283,6 +1710,23 @@ var StreamController = class {
1283
1710
  }
1284
1711
  };
1285
1712
  /**
1713
+ * True when a subagent still sits on its default `tools:<toolCallId>`
1714
+ * namespace — i.e. no execution namespace has been observed (via SSE
1715
+ * replay) or resolved (via history) yet. Used to gate lazy namespace
1716
+ * resolution so already-promoted subagents aren't re-fetched.
1717
+ */
1718
+ function namespaceIsDefaultOnly(entry) {
1719
+ if (entry == null) return false;
1720
+ return entry.namespace.length === 1 && entry.namespace[0] === `tools:${entry.id}`;
1721
+ }
1722
+ function defaultSubagentToolCallId(namespace) {
1723
+ if (namespace.length !== 1) return void 0;
1724
+ const segment = namespace[0];
1725
+ if (!segment.startsWith("tools:")) return void 0;
1726
+ const id = segment.slice(6);
1727
+ return id.length > 0 ? id : void 0;
1728
+ }
1729
+ /**
1286
1730
  * Extract and coerce the configured messages key from a values object.
1287
1731
  *
1288
1732
  * @param values - State values object to read from.
@@ -1291,7 +1735,13 @@ var StreamController = class {
1291
1735
  function extractAndCoerceMessages(values, messagesKey) {
1292
1736
  const raw = values[messagesKey];
1293
1737
  if (!Array.isArray(raw)) return [];
1294
- return require_messages.ensureMessageInstances(raw);
1738
+ return require_message_coercion.ensureMessageInstances(raw);
1739
+ }
1740
+ function extractAndCoerceMessagesWithFallback(values, messagesKey) {
1741
+ let raw = values[messagesKey];
1742
+ if (!Array.isArray(raw) && messagesKey !== "messages") raw = values.messages;
1743
+ if (!Array.isArray(raw)) return null;
1744
+ return require_message_coercion.ensureMessageInstances(raw);
1295
1745
  }
1296
1746
  //#endregion
1297
1747
  exports.ROOT_PUMP_CHANNELS = ROOT_PUMP_CHANNELS;