@langchain/langgraph-sdk 1.9.16 → 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 +451 -32
  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 +472 -32
  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 +28 -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 +28 -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 +1 -1
@@ -1,20 +1,47 @@
1
- import { ensureMessageInstances } from "../ui/messages.js";
1
+ import { ensureMessageInstances } from "./message-coercion.js";
2
2
  import { ToolCallAssembler } from "../client/stream/handles/tools.js";
3
+ import "./constants.js";
3
4
  import { resolveInterruptTargetForHeadlessResume } from "../headless-tools.js";
4
5
  import { StreamStore } from "./store.js";
5
6
  import { ChannelRegistry } from "./channel-registry.js";
6
7
  import { normalizeHitlResponseForServer } from "../ui/hitl-interrupt-payload.js";
7
8
  import { normalizeInterruptForClient } from "../ui/interrupts.js";
8
- import { isInternalWorkNamespace, isLegacySubagentNamespace, isRootNamespace } from "./namespace.js";
9
+ import { isInternalWorkNamespace, isLegacySubagentNamespace, isRootNamespace, namespaceKey } from "./namespace.js";
9
10
  import { SubagentDiscovery } from "./discovery/subagents.js";
10
11
  import { SubgraphDiscovery } from "./discovery/subgraphs.js";
12
+ import "./discovery/index.js";
13
+ import { collectSubgraphHostNamespaces, getHistoryPage, mapSubagentNamespaces, resolveSubagentNamespaces } from "./discovery/namespace-from-history.js";
11
14
  import { MessageMetadataTracker } from "./message-metadata-tracker.js";
12
15
  import { LifecycleLoadingTracker } from "./lifecycle-loading-tracker.js";
13
16
  import { RootMessageProjection } from "./root-message-projection.js";
17
+ import { prepareOptimisticInput } from "./optimistic-input.js";
14
18
  import { EMPTY_QUEUE, SubmitCoordinator } from "./submit-coordinator.js";
15
- import { reconcileToolCallsFromMessages, upsertToolCall } from "./tool-calls.js";
19
+ import { reconcileToolCallsFromMessages, seedToolCallsFromMessages, upsertToolCall } from "./tool-calls.js";
20
+ import { v7 } from "uuid";
16
21
  import "@langchain/core/messages";
17
22
  //#region src/stream/controller.ts
23
+ /**
24
+ * Framework-agnostic controller for the experimental v2 stream.
25
+ *
26
+ * Responsibilities:
27
+ * - Owns at most one {@link ThreadStream} at a time (swapped on
28
+ * `hydrate(newThreadId)` or `dispose`).
29
+ * - Exposes three always-on observable surfaces via {@link StreamStore}:
30
+ * - `rootStore` : root values/messages/toolCalls/interrupts/…
31
+ * - `subagentStore` : discovery map of subagents (no content)
32
+ * - `subgraphStore` : discovery map of subgraphs (no content)
33
+ * - Owns a {@link ChannelRegistry} that framework selector hooks
34
+ * (`useMessages`, `useToolCalls`, `useExtension`, `useChannel`)
35
+ * use to lazily open per-namespace subscriptions.
36
+ * - Imperative run surface: `submit`, `stop`, `respond`, `joinStream`.
37
+ *
38
+ * A single multi-channel subscription (`values`, `lifecycle`, `input`,
39
+ * `messages`, `tools`) powers every always-on projection and both
40
+ * discovery runners. Selector hooks add their own (deduped)
41
+ * subscriptions on top — so even a UI with many subagents only opens
42
+ * one extra subscription per `(channels, namespace)` actually
43
+ * rendered on screen.
44
+ */
18
45
  function isAbortLikeError(error) {
19
46
  if (error == null || typeof error !== "object") return false;
20
47
  const maybeError = error;
@@ -26,6 +53,43 @@ function lifecycleReason(event) {
26
53
  if (event === "interrupted") return "interrupt";
27
54
  return null;
28
55
  }
56
+ /**
57
+ * Decide whether a hydrated thread is *active* (a run is executing or
58
+ * paused awaiting resume) from the `getState()` snapshot alone — no
59
+ * extra request.
60
+ *
61
+ * Why this gate exists: a finished thread does not need either of the
62
+ * always-on SSE pumps. Subagent/subgraph cards are already seeded from
63
+ * the `getState()` messages and a single bounded `getHistory()` page, so
64
+ * opening the depth-1 content pump + the wildcard lifecycle watcher only
65
+ * to replay a completed run and then idle forever is pure waste. We open
66
+ * the pumps eagerly only when the thread is active; otherwise they come
67
+ * up on the first local `submit()` (the existing deferred-pump path) or
68
+ * a thread swap that lands on an active thread.
69
+ *
70
+ * The gate is deliberately conservative: we only conclude *idle* when
71
+ * the state proves it. A thread is treated as active unless `next` is a
72
+ * present, empty array AND no task carries a pending interrupt:
73
+ * - `next` missing / not an array: unknown shape (a server or custom
74
+ * client may omit it). Treat as active so an already-running
75
+ * server-side run is still observed on reconnect — never silently
76
+ * disable streaming on an unfamiliar `getState` shape.
77
+ * - `next.length > 0`: the checkpoint still has nodes to execute, i.e.
78
+ * a run is mid-flight or paused at an interrupt.
79
+ * - `next` is `[]` but a `tasks[].interrupts` is non-empty: the thread
80
+ * is interrupted and a resume (which starts a run) must be observable.
81
+ * - `next` is `[]` and no pending interrupts: a completed run → idle.
82
+ */
83
+ function isThreadStateActive(state) {
84
+ if (state == null) return true;
85
+ if (!Array.isArray(state.next)) return true;
86
+ if (state.next.length > 0) return true;
87
+ if (Array.isArray(state.tasks)) for (const task of state.tasks) {
88
+ const interrupts = task?.interrupts;
89
+ if (Array.isArray(interrupts) && interrupts.length > 0) return true;
90
+ }
91
+ return false;
92
+ }
29
93
  const ROOT_NAMESPACE = [];
30
94
  /**
31
95
  * Channel set covered by the always-on root subscription. Exported so
@@ -111,11 +175,37 @@ var StreamController = class {
111
175
  * request would 404 and surface a spurious error to the UI).
112
176
  */
113
177
  #selfCreatedThreadIds = /* @__PURE__ */ new Set();
178
+ /**
179
+ * In-flight per-subagent namespace resolutions, keyed by tool-call
180
+ * id. De-dupes concurrent {@link resolveSubagentNamespace} calls so
181
+ * re-renders / multiple consumers of the same subagent don't issue
182
+ * parallel `getHistory` walks.
183
+ */
184
+ #namespaceResolves = /* @__PURE__ */ new Map();
185
+ /**
186
+ * In-flight hydrate-time discovery seed ({@link #seedDiscoveryFromHistory}):
187
+ * a single bounded `getHistory` page that bulk-promotes every
188
+ * still-default subagent namespace and seeds subgraph hosts. Per-card
189
+ * {@link resolveSubagentNamespace} calls await this shared promise
190
+ * instead of each firing their own `getHistory` walk, so opening N
191
+ * cards right after reconnect costs one history read, not N. Re-armed
192
+ * per hydrate cycle and cleared once it settles.
193
+ */
194
+ #discoverySeedPromise;
195
+ #scopedHistorySeeds = /* @__PURE__ */ new Map();
114
196
  #rootEventListeners = /* @__PURE__ */ new Set();
115
197
  #rootBus;
116
198
  #activeRunId;
117
199
  #localRunDepth = 0;
118
200
  /**
201
+ * `true` once a root `values` event has been applied for the current
202
+ * optimistic run. Reset to `false` in {@link #beginOptimistic} and
203
+ * read in {@link #settleOptimistic}: when a run terminates without
204
+ * the server ever echoing a `values` snapshot, optimistically-merged
205
+ * non-message keys are rolled back to their pre-submit values.
206
+ */
207
+ #sawValuesForRun = false;
208
+ /**
119
209
  * Single-shot hydration promise. Exposed via `hydrationPromise`
120
210
  * so Suspense wrappers can throw it until the first hydrate
121
211
  * completes (resolve) or fails (reject). Reset whenever a new
@@ -150,7 +240,8 @@ var StreamController = class {
150
240
  return () => {
151
241
  this.#rootEventListeners.delete(listener);
152
242
  };
153
- }
243
+ },
244
+ trySeedFromHistory: (params) => this.#trySeedProjectionFromHistory(params)
154
245
  };
155
246
  this.registry = new ChannelRegistry(this.#rootBus);
156
247
  this.subagentStore = this.#subagents.store;
@@ -196,7 +287,9 @@ var StreamController = class {
196
287
  onRunStart: () => this.#markLocalRunStart(),
197
288
  onRunCreated: (runId) => this.#notifyCreated(runId),
198
289
  onRunCompleted: (reason, runId) => this.#notifyCompleted(reason, runId),
199
- onRunEnd: () => this.#markLocalRunEnd()
290
+ onRunEnd: () => this.#markLocalRunEnd(),
291
+ beginOptimistic: (input) => this.#beginOptimistic(input),
292
+ settleOptimistic: (handle, event) => this.#settleOptimistic(handle, event)
200
293
  });
201
294
  this.#hydrationPromise = this.#createHydrationPromise();
202
295
  /**
@@ -263,6 +356,8 @@ var StreamController = class {
263
356
  const target = threadId === void 0 ? this.#currentThreadId : threadId;
264
357
  const changed = target !== this.#currentThreadId;
265
358
  this.#currentThreadId = target ?? null;
359
+ this.#discoverySeedPromise = void 0;
360
+ this.#scopedHistorySeeds.clear();
266
361
  this.rootStore.setState((s) => ({
267
362
  ...s,
268
363
  threadId: this.#currentThreadId
@@ -319,9 +414,11 @@ var StreamController = class {
319
414
  }));
320
415
  let hydrationError;
321
416
  let threadExists = false;
417
+ let threadActive = true;
322
418
  try {
323
419
  const state = await this.#options.client.threads.getState(this.#currentThreadId);
324
420
  threadExists = state != null;
421
+ threadActive = isThreadStateActive(state);
325
422
  if (state?.values != null) {
326
423
  /**
327
424
  * `threads.getState()` returns the legacy `ThreadState` shape
@@ -333,11 +430,43 @@ var StreamController = class {
333
430
  */
334
431
  const checkpointId = state.checkpoint?.checkpoint_id;
335
432
  const parentCheckpointId = state.parent_checkpoint?.checkpoint_id ?? void 0;
433
+ /**
434
+ * Carry the checkpoint `step` from `getState()` metadata so the
435
+ * root message projection treats this seed as the authoritative
436
+ * latest superstep. The content pump's reconnect replay emits
437
+ * older checkpoints (lower step); marking the seed's step lets
438
+ * the projection reject those as stale instead of letting them
439
+ * remove the seeded message tail (the final assistant turn).
440
+ */
441
+ const seedStep = state.metadata?.step;
336
442
  const syntheticCheckpoint = typeof checkpointId === "string" ? {
337
443
  id: checkpointId,
338
- ...parentCheckpointId != null ? { parent_id: parentCheckpointId } : {}
444
+ ...parentCheckpointId != null ? { parent_id: parentCheckpointId } : {},
445
+ ...typeof seedStep === "number" ? { step: seedStep } : {}
339
446
  } : void 0;
340
447
  this.#applyValues(state.values, syntheticCheckpoint);
448
+ /**
449
+ * Seed subagent discovery from checkpoint messages so deep-agent
450
+ * cards render on refresh without waiting for SSE replay. Zero
451
+ * extra HTTP (reuses the `getState` payload); mirrors the
452
+ * interrupt-seeding below. `#subagents` was cleared in
453
+ * `#teardownThread`, and `seedFromCheckpointMessages` is
454
+ * idempotent, so this is safe on re-hydrate.
455
+ */
456
+ const seedMessages = state.values[this.#messagesKey];
457
+ if (Array.isArray(seedMessages)) this.#subagents.seedFromCheckpointMessages(seedMessages);
458
+ }
459
+ /**
460
+ * Converge to server truth: drop any optimistic messages the
461
+ * server state does not contain (`pending` / `failed` that were
462
+ * never persisted — e.g. a failed run's user message). Echoed
463
+ * ids were flipped to `"sent"` by `#applyValues` above and so are
464
+ * excluded from `unpersistedOptimisticIds()`.
465
+ */
466
+ const unpersisted = this.#messageMetadata.unpersistedOptimisticIds();
467
+ if (unpersisted.size > 0) {
468
+ this.#rootMessages.dropOptimisticMessages(unpersisted);
469
+ this.#messageMetadata.forget(unpersisted);
341
470
  }
342
471
  /**
343
472
  * Sync the visible interrupt list to the server's authoritative
@@ -391,27 +520,253 @@ var StreamController = class {
391
520
  else this.#resolveHydration();
392
521
  }
393
522
  /**
394
- * P0 fix: open the shared subscription on mount so in-flight
395
- * server-side runs are observed even when no local `submit()` is
396
- * active. The transport replays the run from `seq=0` on a rotating
397
- * subscribe, so late-joining is free once the subscription exists.
398
- * `isLoading` transitions are driven by the persistent root
399
- * lifecycle listener registered in `#startRootPump`.
523
+ * Open the shared subscription on mount so in-flight server-side
524
+ * runs are observed even when no local `submit()` is active — BUT
525
+ * only when the thread is actually active (see
526
+ * {@link isThreadStateActive}). A finished thread's cards are seeded
527
+ * from `getState()` + the bounded `getHistory()` below, so opening
528
+ * the depth-1 content pump just to replay a completed run and idle
529
+ * forever is pure waste. When idle we take the deferred path: the
530
+ * pump (and watcher) come up on the first local `submit()` via
531
+ * {@link #startDeferredRootPump}, exactly like a self-created thread.
532
+ * The transport replays from `seq=0` on the deferred subscribe, so
533
+ * nothing is missed.
400
534
  */
401
- const thread = this.#ensureThread(this.#currentThreadId);
535
+ const thread = this.#ensureThread(this.#currentThreadId, !threadActive);
402
536
  /**
403
- * Start the wildcard lifecycle watcher up-front for existing
404
- * threads. The root content pump runs at `depth: 1`, which covers
405
- * root-namespace and one-deep events but not arbitrarily-nested
406
- * subagent / subgraph lifecycle — the dedicated watcher handles
407
- * those.
537
+ * Start the wildcard lifecycle watcher up-front for existing,
538
+ * active threads. The root content pump runs at `depth: 1`, which
539
+ * covers root-namespace and one-deep events but not arbitrarily-
540
+ * nested subagent / subgraph lifecycle — the dedicated watcher
541
+ * handles those.
408
542
  *
409
- * For self-created (new) threads we skip — the watcher would 404
410
- * against a not-yet-existent thread. `submitRun` / `respondInput`
411
- * call `startLifecycleWatcher` on first submission to cover that
412
- * case.
543
+ * Skipped when:
544
+ * - the thread is idle/finished — there are no live events to
545
+ * watch; discovery is seeded from history below, and the watcher
546
+ * starts with the deferred pump on the first `submit()`.
547
+ * - the thread is self-created (new) — the watcher would 404
548
+ * against a not-yet-existent thread; `submitRun` / `respondInput`
549
+ * call `startLifecycleWatcher` on first submission instead.
413
550
  */
414
- if (threadExists) thread.startLifecycleWatcher();
551
+ if (threadExists && threadActive) thread.startLifecycleWatcher();
552
+ if (threadExists) {
553
+ /**
554
+ * Seed subgraph discovery and promote subagent execution
555
+ * namespaces from a single bounded `getHistory` page. Subgraph
556
+ * structure is not present in the root checkpoint messages
557
+ * (unlike subagents), so it can only be reconstructed from
558
+ * history. Fire-and-forget — not awaited into the hydration
559
+ * promise, so Suspense / first paint stay unblocked; cards fill
560
+ * in progressively when it resolves.
561
+ *
562
+ * Held in `#discoverySeedPromise` so lazy per-card
563
+ * {@link resolveSubagentNamespace} calls coalesce onto this single
564
+ * read instead of each firing their own `getHistory` walk.
565
+ */
566
+ const seed = this.#seedDiscoveryFromHistory(this.#currentThreadId).finally(() => {
567
+ if (this.#discoverySeedPromise === seed) this.#discoverySeedPromise = void 0;
568
+ });
569
+ this.#discoverySeedPromise = seed;
570
+ }
571
+ }
572
+ /**
573
+ * One bounded, non-blocking `getHistory` read at hydrate that seeds
574
+ * subgraph hosts and bulk-promotes still-default subagent execution
575
+ * namespaces. O(1) in requests regardless of subagent/subgraph count.
576
+ */
577
+ async #seedDiscoveryFromHistory(threadId) {
578
+ try {
579
+ const history = await getHistoryPage(this.#options.client, threadId, { limit: 20 });
580
+ if (this.#disposed || this.#currentThreadId !== threadId) return;
581
+ this.#primeScopedHistorySeedsFromHistory(threadId, history);
582
+ const hosts = collectSubgraphHostNamespaces(history);
583
+ this.#subgraphs.seedFromHistory(hosts);
584
+ const defaultOnlyIds = [...this.#subagents.snapshot.values()].filter(namespaceIsDefaultOnly).map((entry) => entry.id);
585
+ if (defaultOnlyIds.length > 0) {
586
+ const map = mapSubagentNamespaces(history, defaultOnlyIds, this.#messagesKey);
587
+ for (const [id, segment] of map) this.#subagents.applyExecutionNamespace(id, segment);
588
+ }
589
+ } catch {}
590
+ }
591
+ /**
592
+ * Lazily resolve a single subagent's execution namespace from
593
+ * checkpoint history. Intended call site: the first scoped
594
+ * `useMessages` / `useToolCalls` mount for a subagent whose namespace
595
+ * is still the default `tools:<toolCallId>`. A fallback for the
596
+ * hydrate-time bulk seed ({@link #seedDiscoveryFromHistory}) — most
597
+ * subagents are already promoted by the time a panel opens.
598
+ *
599
+ * Skips ids already promoted past default-only (SSE replay or a prior
600
+ * resolve). Concurrent calls for the same id share one `getHistory`
601
+ * walk via {@link #namespaceResolves}.
602
+ *
603
+ * @param toolCallId - Parent `task` tool-call id (the subagent's discovery key).
604
+ */
605
+ async resolveSubagentNamespace(toolCallId) {
606
+ if (this.#disposed) return;
607
+ const threadId = this.#currentThreadId;
608
+ if (threadId == null) return;
609
+ if (!namespaceIsDefaultOnly(this.#subagents.snapshot.get(toolCallId))) return;
610
+ const inflight = this.#namespaceResolves.get(toolCallId);
611
+ if (inflight != null) return inflight;
612
+ const run = (async () => {
613
+ try {
614
+ /**
615
+ * Coalesce onto the hydrate-time discovery seed. That single
616
+ * bounded `getHistory` page bulk-promotes every default-only
617
+ * subagent, so when many cards mount at once (the common
618
+ * reconnect case) they all await this one read instead of each
619
+ * firing their own walk. Re-check after it settles: usually the
620
+ * bulk seed already promoted us and no further fetch is needed.
621
+ */
622
+ const seed = this.#discoverySeedPromise;
623
+ if (seed != null) {
624
+ await seed;
625
+ if (this.#disposed || this.#currentThreadId !== threadId) return;
626
+ if (!namespaceIsDefaultOnly(this.#subagents.snapshot.get(toolCallId))) return;
627
+ }
628
+ const map = await resolveSubagentNamespaces(this.#options.client, threadId, [toolCallId], { messagesKey: this.#messagesKey });
629
+ if (this.#disposed || this.#currentThreadId !== threadId) return;
630
+ const segment = map.get(toolCallId);
631
+ if (segment != null) this.#subagents.applyExecutionNamespace(toolCallId, segment);
632
+ } catch {} finally {
633
+ this.#namespaceResolves.delete(toolCallId);
634
+ }
635
+ })();
636
+ this.#namespaceResolves.set(toolCallId, run);
637
+ return run;
638
+ }
639
+ /**
640
+ * Try to satisfy a scoped selector projection from checkpoint history
641
+ * instead of opening a scoped `/events` replay.
642
+ *
643
+ * This is only valid while the root pump is deferred, which means hydrate
644
+ * has classified the thread as idle/stale. Active and interrupted threads
645
+ * must keep using SSE so ongoing work and resumes are observed. For an idle
646
+ * thread, though, a late-mounted subagent card only needs the latest scoped
647
+ * checkpoint snapshot; opening `/events` just asks the server to replay work
648
+ * that already finished and can be slow for namespaces discovered from
649
+ * history.
650
+ *
651
+ * Returns `true` when the projection was handled without `/events`. That can
652
+ * mean either the store was seeded from namespace-specific history, or the
653
+ * projection targeted a default subagent namespace that should be skipped
654
+ * because hydrate promoted it to its execution namespace. Returns `false`
655
+ * when the caller should fall back to the normal subscription path.
656
+ */
657
+ async #trySeedProjectionFromHistory(params) {
658
+ const threadId = this.#currentThreadId;
659
+ if (this.#disposed || threadId == null || params.namespace.length === 0 || !this.#rootPumpDeferred || this.#selfCreatedThreadIds.has(threadId)) return false;
660
+ if (await this.#skipDefaultSubagentProjection(params.namespace, threadId)) return true;
661
+ if (this.#disposed || this.#currentThreadId !== threadId || !this.#rootPumpDeferred) return false;
662
+ const seed = await this.#getScopedHistorySeed(threadId, params.namespace);
663
+ if (seed == null || this.#disposed || this.#currentThreadId !== threadId || !this.#rootPumpDeferred) return false;
664
+ if (await this.#skipDefaultSubagentProjection(params.namespace, threadId)) return true;
665
+ if (params.kind === "messages") {
666
+ params.store.setValue(seed.messages);
667
+ return true;
668
+ }
669
+ params.store.setValue(seed.toolCalls);
670
+ return true;
671
+ }
672
+ /**
673
+ * Suppress subscriptions for placeholder subagent namespaces once hydrate has
674
+ * resolved the real execution namespace.
675
+ *
676
+ * Deep-agent discovery first creates cards at `tools:<toolCallId>`. The
677
+ * actual worker history usually lives under a different checkpoint namespace
678
+ * such as `tools:<uuid>`, and hydrate resolves that mapping from the bounded
679
+ * root history seed. React/Vue/Svelte/Angular selector effects can mount
680
+ * while that seed is still in flight, so this helper waits for it and then
681
+ * returns `true` when the original placeholder namespace is stale. Returning
682
+ * `true` tells the projection runtime not to open an `/events` subscription
683
+ * for the wrong namespace; the framework will re-render with the promoted
684
+ * card namespace and acquire the real projection.
685
+ */
686
+ async #skipDefaultSubagentProjection(namespace, threadId) {
687
+ const toolCallId = defaultSubagentToolCallId(namespace);
688
+ if (toolCallId == null) return false;
689
+ if (!namespaceIsDefaultOnly(this.#subagents.snapshot.get(toolCallId))) return false;
690
+ const seed = this.#discoverySeedPromise;
691
+ if (seed != null) await seed;
692
+ if (this.#disposed || this.#currentThreadId !== threadId) return true;
693
+ return !namespaceIsDefaultOnly(this.#subagents.snapshot.get(toolCallId));
694
+ }
695
+ /**
696
+ * Load and cache the latest checkpoint snapshot for one scoped namespace.
697
+ *
698
+ * `useMessages(stream, subagent)` and `useToolCalls(stream, subagent)` often
699
+ * mount together. Both need the same namespace-specific history page, so the
700
+ * controller keeps an in-flight promise per `threadId + checkpoint_ns`.
701
+ * The cache may already be primed by the hydrate-time discovery history page;
702
+ * otherwise this method performs a narrow `checkpoint_ns` read and derives
703
+ * both projection snapshots from that one response:
704
+ *
705
+ * - `messages` are coerced with the stream-local message coercion rules, so
706
+ * serialized `content_blocks` and tool-call metadata hydrate correctly.
707
+ * - `toolCalls` are reconstructed from AI tool calls plus matching
708
+ * ToolMessages, enough for finished/stale card panels without replaying
709
+ * the `tools` channel.
710
+ *
711
+ * Returns `null` when history does not contain usable values, or the request
712
+ * fails. Callers treat that as a signal to fall back to `/events` so custom
713
+ * servers or unusual state shapes still work.
714
+ */
715
+ #getScopedHistorySeed(threadId, namespace) {
716
+ const checkpointNs = namespaceKey(namespace);
717
+ const key = `${threadId}|${checkpointNs}`;
718
+ const existing = this.#scopedHistorySeeds.get(key);
719
+ if (existing != null) return existing;
720
+ const seed = (async () => {
721
+ try {
722
+ const values = (await getHistoryPage(this.#options.client, threadId, {
723
+ limit: 1,
724
+ checkpoint: { checkpoint_ns: checkpointNs }
725
+ }))[0]?.values;
726
+ if (values == null || typeof values !== "object") return null;
727
+ const messages = extractAndCoerceMessagesWithFallback(values, this.#messagesKey);
728
+ if (messages == null) return null;
729
+ return {
730
+ messages,
731
+ toolCalls: seedToolCallsFromMessages(namespace, messages)
732
+ };
733
+ } catch {
734
+ return null;
735
+ }
736
+ })();
737
+ this.#scopedHistorySeeds.set(key, seed);
738
+ return seed;
739
+ }
740
+ /**
741
+ * Reuse the hydrate-time discovery history page as scoped projection data
742
+ * when it already contains checkpoint values for a namespace.
743
+ *
744
+ * The discovery read is required to resolve subagent execution namespaces and
745
+ * subgraph hosts. That same page often includes the latest values for those
746
+ * namespaces, so priming `#scopedHistorySeeds` here lets later
747
+ * `useMessages(stream, subagent)` / `useToolCalls(stream, subagent)` mounts
748
+ * hydrate from memory instead of issuing an immediate second `getHistory`
749
+ * request. If a namespace is not present in the bounded page,
750
+ * `#getScopedHistorySeed` still falls back to a targeted `checkpoint_ns`
751
+ * history read.
752
+ */
753
+ #primeScopedHistorySeedsFromHistory(threadId, history) {
754
+ for (const state of history) {
755
+ const checkpointNs = state.checkpoint?.checkpoint_ns;
756
+ if (typeof checkpointNs !== "string" || checkpointNs.length === 0) continue;
757
+ const namespace = checkpointNs.split("\0").filter((segment) => segment.length > 0);
758
+ if (namespace.length === 0) continue;
759
+ const key = `${threadId}|${namespaceKey(namespace)}`;
760
+ if (this.#scopedHistorySeeds.has(key)) continue;
761
+ const values = state.values;
762
+ if (values == null || typeof values !== "object") continue;
763
+ const messages = extractAndCoerceMessagesWithFallback(values, this.#messagesKey);
764
+ if (messages == null) continue;
765
+ this.#scopedHistorySeeds.set(key, Promise.resolve({
766
+ messages,
767
+ toolCalls: seedToolCallsFromMessages(namespace, messages)
768
+ }));
769
+ }
415
770
  }
416
771
  /**
417
772
  * Submit input to the active thread.
@@ -850,6 +1205,7 @@ var StreamController = class {
850
1205
  this.#lifecycleLoading.reset();
851
1206
  this.#subagents.reset();
852
1207
  this.#subgraphs.reset();
1208
+ this.#scopedHistorySeeds.clear();
853
1209
  this.#activeRunId = void 0;
854
1210
  this.#localRunDepth = 0;
855
1211
  this.#messageMetadata.reset();
@@ -1126,9 +1482,67 @@ var StreamController = class {
1126
1482
  * @param raw - Raw `values` channel payload.
1127
1483
  * @param checkpoint - Optional checkpoint envelope paired with the values event.
1128
1484
  */
1485
+ /**
1486
+ * Apply a submit input optimistically to the root projection before
1487
+ * the server responds. Mints stable ids for id-less messages (so the
1488
+ * server echo reconciles by id), appends them to the projection, and
1489
+ * shallow-merges non-message input keys into `values`.
1490
+ *
1491
+ * Returns the dispatch payload (id-injected) for the coordinator to
1492
+ * send, plus an {@link OptimisticHandle} for terminal reconciliation.
1493
+ * Returns `undefined` when optimistic UI is disabled or there is
1494
+ * nothing to echo, in which case the coordinator dispatches the raw
1495
+ * input unchanged.
1496
+ *
1497
+ * @param input - Raw input passed to `submit()`.
1498
+ */
1499
+ #beginOptimistic(input) {
1500
+ if (this.#options.optimistic === false) return void 0;
1501
+ if (input == null || typeof input !== "object" || Array.isArray(input)) return;
1502
+ const prepared = prepareOptimisticInput(input, this.#messagesKey, () => v7());
1503
+ const extraKeys = Object.keys(prepared.extraValues);
1504
+ if (prepared.echoedIds.length === 0 && extraKeys.length === 0) return;
1505
+ const currentValues = this.rootStore.getSnapshot().values;
1506
+ const restoreKeys = extraKeys.map((key) => ({
1507
+ key,
1508
+ hadKey: Object.prototype.hasOwnProperty.call(currentValues, key),
1509
+ prevValue: currentValues[key]
1510
+ }));
1511
+ this.#sawValuesForRun = false;
1512
+ this.#rootMessages.appendOptimistic(prepared.optimisticMessages, prepared.extraValues);
1513
+ if (prepared.echoedIds.length > 0) this.#messageMetadata.markPending(prepared.echoedIds);
1514
+ return {
1515
+ dispatchInput: prepared.dispatchInput,
1516
+ handle: {
1517
+ echoedIds: prepared.echoedIds,
1518
+ restoreKeys
1519
+ }
1520
+ };
1521
+ }
1522
+ /**
1523
+ * Reconcile optimistic state when a run terminates.
1524
+ *
1525
+ * - Messages: any echoed id still `"pending"` (never echoed by the
1526
+ * server) is flipped to `"sent"` on success/interrupt, or
1527
+ * `"failed"` on failure/abort. Ids the server already echoed were
1528
+ * flipped to `"sent"` in {@link #applyValues} and are untouched.
1529
+ * - Non-message keys: rolled back to their pre-submit values when no
1530
+ * server `values` event landed during the run (otherwise the
1531
+ * server snapshot already reconciled them). Skipped on abort,
1532
+ * where a superseding run (or `stop()`) owns subsequent state.
1533
+ *
1534
+ * @param handle - Handle returned by {@link #beginOptimistic}.
1535
+ * @param event - Terminal lifecycle event for the run.
1536
+ */
1537
+ #settleOptimistic(handle, event) {
1538
+ const failed = event === "failed" || event === "aborted";
1539
+ this.#messageMetadata.resolvePending(handle.echoedIds, failed ? "failed" : "sent");
1540
+ if (event !== "aborted" && !this.#sawValuesForRun) this.#rootMessages.restoreValueKeys(handle.restoreKeys);
1541
+ }
1129
1542
  #applyValues(raw, checkpoint) {
1130
1543
  if (raw == null || typeof raw !== "object" || Array.isArray(raw)) return;
1131
1544
  const state = raw;
1545
+ this.#sawValuesForRun = true;
1132
1546
  /**
1133
1547
  * Surface parent_checkpoint per-message when the values event
1134
1548
  * carries the lightweight checkpoint envelope (populated by
@@ -1153,15 +1567,18 @@ var StreamController = class {
1153
1567
  nextValues = state;
1154
1568
  nextMessages = [];
1155
1569
  }
1156
- this.#rootMessages.applyValues(nextValues, nextMessages);
1157
- if (nextMessages.length > 0) this.rootStore.setState((s) => {
1158
- const toolCalls = reconcileToolCallsFromMessages(s.toolCalls, nextMessages);
1159
- if (toolCalls === s.toolCalls) return s;
1160
- return {
1161
- ...s,
1162
- toolCalls
1163
- };
1164
- });
1570
+ this.#rootMessages.applyValues(nextValues, nextMessages, { step: checkpoint?.step });
1571
+ if (nextMessages.length > 0) {
1572
+ this.#messageMetadata.resolvePending(nextMessages.map((m) => m.id).filter((id) => typeof id === "string"), "sent");
1573
+ this.rootStore.setState((s) => {
1574
+ const toolCalls = reconcileToolCallsFromMessages(s.toolCalls, nextMessages);
1575
+ if (toolCalls === s.toolCalls) return s;
1576
+ return {
1577
+ ...s,
1578
+ toolCalls
1579
+ };
1580
+ });
1581
+ }
1165
1582
  }
1166
1583
  /**
1167
1584
  * Mirror root protocol interrupts into the root snapshot.
@@ -1292,6 +1709,23 @@ var StreamController = class {
1292
1709
  }
1293
1710
  };
1294
1711
  /**
1712
+ * True when a subagent still sits on its default `tools:<toolCallId>`
1713
+ * namespace — i.e. no execution namespace has been observed (via SSE
1714
+ * replay) or resolved (via history) yet. Used to gate lazy namespace
1715
+ * resolution so already-promoted subagents aren't re-fetched.
1716
+ */
1717
+ function namespaceIsDefaultOnly(entry) {
1718
+ if (entry == null) return false;
1719
+ return entry.namespace.length === 1 && entry.namespace[0] === `tools:${entry.id}`;
1720
+ }
1721
+ function defaultSubagentToolCallId(namespace) {
1722
+ if (namespace.length !== 1) return void 0;
1723
+ const segment = namespace[0];
1724
+ if (!segment.startsWith("tools:")) return void 0;
1725
+ const id = segment.slice(6);
1726
+ return id.length > 0 ? id : void 0;
1727
+ }
1728
+ /**
1295
1729
  * Extract and coerce the configured messages key from a values object.
1296
1730
  *
1297
1731
  * @param values - State values object to read from.
@@ -1302,6 +1736,12 @@ function extractAndCoerceMessages(values, messagesKey) {
1302
1736
  if (!Array.isArray(raw)) return [];
1303
1737
  return ensureMessageInstances(raw);
1304
1738
  }
1739
+ function extractAndCoerceMessagesWithFallback(values, messagesKey) {
1740
+ let raw = values[messagesKey];
1741
+ if (!Array.isArray(raw) && messagesKey !== "messages") raw = values.messages;
1742
+ if (!Array.isArray(raw)) return null;
1743
+ return ensureMessageInstances(raw);
1744
+ }
1305
1745
  //#endregion
1306
1746
  export { ROOT_PUMP_CHANNELS, StreamController };
1307
1747