@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,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;
@@ -188,6 +279,7 @@ var StreamController = class {
188
279
  },
189
280
  waitForRootPumpReady: () => this.#rootPumpReady,
190
281
  awaitNextTerminal: (signal) => this.#awaitNextTerminal(signal),
282
+ awaitResumedRunTerminal: (signal) => this.#awaitResumedRunTerminal(signal),
191
283
  onSubmitStart: () => {
192
284
  this.#hydratedActiveInterruptIds = null;
193
285
  this.#submitGeneration += 1;
@@ -195,7 +287,9 @@ var StreamController = class {
195
287
  onRunStart: () => this.#markLocalRunStart(),
196
288
  onRunCreated: (runId) => this.#notifyCreated(runId),
197
289
  onRunCompleted: (reason, runId) => this.#notifyCompleted(reason, runId),
198
- onRunEnd: () => this.#markLocalRunEnd()
290
+ onRunEnd: () => this.#markLocalRunEnd(),
291
+ beginOptimistic: (input) => this.#beginOptimistic(input),
292
+ settleOptimistic: (handle, event) => this.#settleOptimistic(handle, event)
199
293
  });
200
294
  this.#hydrationPromise = this.#createHydrationPromise();
201
295
  /**
@@ -262,6 +356,8 @@ var StreamController = class {
262
356
  const target = threadId === void 0 ? this.#currentThreadId : threadId;
263
357
  const changed = target !== this.#currentThreadId;
264
358
  this.#currentThreadId = target ?? null;
359
+ this.#discoverySeedPromise = void 0;
360
+ this.#scopedHistorySeeds.clear();
265
361
  this.rootStore.setState((s) => ({
266
362
  ...s,
267
363
  threadId: this.#currentThreadId
@@ -318,9 +414,11 @@ var StreamController = class {
318
414
  }));
319
415
  let hydrationError;
320
416
  let threadExists = false;
417
+ let threadActive = true;
321
418
  try {
322
419
  const state = await this.#options.client.threads.getState(this.#currentThreadId);
323
420
  threadExists = state != null;
421
+ threadActive = isThreadStateActive(state);
324
422
  if (state?.values != null) {
325
423
  /**
326
424
  * `threads.getState()` returns the legacy `ThreadState` shape
@@ -332,11 +430,43 @@ var StreamController = class {
332
430
  */
333
431
  const checkpointId = state.checkpoint?.checkpoint_id;
334
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;
335
442
  const syntheticCheckpoint = typeof checkpointId === "string" ? {
336
443
  id: checkpointId,
337
- ...parentCheckpointId != null ? { parent_id: parentCheckpointId } : {}
444
+ ...parentCheckpointId != null ? { parent_id: parentCheckpointId } : {},
445
+ ...typeof seedStep === "number" ? { step: seedStep } : {}
338
446
  } : void 0;
339
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);
340
470
  }
341
471
  /**
342
472
  * Sync the visible interrupt list to the server's authoritative
@@ -390,27 +520,253 @@ var StreamController = class {
390
520
  else this.#resolveHydration();
391
521
  }
392
522
  /**
393
- * P0 fix: open the shared subscription on mount so in-flight
394
- * server-side runs are observed even when no local `submit()` is
395
- * active. The transport replays the run from `seq=0` on a rotating
396
- * subscribe, so late-joining is free once the subscription exists.
397
- * `isLoading` transitions are driven by the persistent root
398
- * 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.
399
534
  */
400
- const thread = this.#ensureThread(this.#currentThreadId);
535
+ const thread = this.#ensureThread(this.#currentThreadId, !threadActive);
401
536
  /**
402
- * Start the wildcard lifecycle watcher up-front for existing
403
- * threads. The root content pump runs at `depth: 1`, which covers
404
- * root-namespace and one-deep events but not arbitrarily-nested
405
- * subagent / subgraph lifecycle — the dedicated watcher handles
406
- * 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.
407
542
  *
408
- * For self-created (new) threads we skip — the watcher would 404
409
- * against a not-yet-existent thread. `submitRun` / `respondInput`
410
- * call `startLifecycleWatcher` on first submission to cover that
411
- * 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.
412
550
  */
413
- 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
+ }
414
770
  }
415
771
  /**
416
772
  * Submit input to the active thread.
@@ -575,15 +931,18 @@ var StreamController = class {
575
931
  namespace: options.namespace ?? [...ROOT_NAMESPACE]
576
932
  } : this.#resolveInterruptForResume();
577
933
  if (resolved == null) throw new Error("No pending interrupt to respond to.");
934
+ const thread = this.#thread;
578
935
  try {
579
- await this.#thread.respondInput({
580
- namespace: resolved.namespace,
581
- interrupt_id: resolved.interruptId,
582
- response: normalizeHitlResponseForServer(response),
583
- config: options?.config,
584
- metadata: options?.metadata
936
+ await this.#submitter.dispatchResume(async () => {
937
+ await thread.respondInput({
938
+ namespace: resolved.namespace,
939
+ interrupt_id: resolved.interruptId,
940
+ response: normalizeHitlResponseForServer(response),
941
+ config: options?.config,
942
+ metadata: options?.metadata
943
+ });
944
+ this.#markInterruptResolvedInRootStore(resolved.interruptId);
585
945
  });
586
- this.#markInterruptResolvedInRootStore(resolved.interruptId);
587
946
  } catch (error) {
588
947
  if (this.#disposed && isAbortLikeError(error)) return;
589
948
  throw error;
@@ -634,19 +993,22 @@ var StreamController = class {
634
993
  if (this.#disposed || this.#thread == null) throw new Error("No active thread to respond to.");
635
994
  const entries = Object.entries(responsesById);
636
995
  if (entries.length === 0) throw new Error("respondAll() requires at least one response.");
637
- const pending = this.#thread.interrupts;
996
+ const thread = this.#thread;
997
+ const pending = thread.interrupts;
638
998
  const responses = entries.map(([interruptId, response]) => ({
639
999
  interrupt_id: interruptId,
640
1000
  response: normalizeHitlResponseForServer(response),
641
1001
  namespace: pending.find((entry) => entry.interruptId === interruptId)?.namespace ?? [...ROOT_NAMESPACE]
642
1002
  }));
643
1003
  try {
644
- await this.#thread.respondInput({
645
- responses,
646
- config: options?.config,
647
- metadata: options?.metadata
1004
+ await this.#submitter.dispatchResume(async () => {
1005
+ await thread.respondInput({
1006
+ responses,
1007
+ config: options?.config,
1008
+ metadata: options?.metadata
1009
+ });
1010
+ for (const { interrupt_id: interruptId } of responses) this.#markInterruptResolvedInRootStore(interruptId);
648
1011
  });
649
- for (const { interrupt_id: interruptId } of responses) this.#markInterruptResolvedInRootStore(interruptId);
650
1012
  } catch (error) {
651
1013
  if (this.#disposed && isAbortLikeError(error)) return;
652
1014
  throw error;
@@ -843,6 +1205,7 @@ var StreamController = class {
843
1205
  this.#lifecycleLoading.reset();
844
1206
  this.#subagents.reset();
845
1207
  this.#subgraphs.reset();
1208
+ this.#scopedHistorySeeds.clear();
846
1209
  this.#activeRunId = void 0;
847
1210
  this.#localRunDepth = 0;
848
1211
  this.#messageMetadata.reset();
@@ -1119,9 +1482,67 @@ var StreamController = class {
1119
1482
  * @param raw - Raw `values` channel payload.
1120
1483
  * @param checkpoint - Optional checkpoint envelope paired with the values event.
1121
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
+ }
1122
1542
  #applyValues(raw, checkpoint) {
1123
1543
  if (raw == null || typeof raw !== "object" || Array.isArray(raw)) return;
1124
1544
  const state = raw;
1545
+ this.#sawValuesForRun = true;
1125
1546
  /**
1126
1547
  * Surface parent_checkpoint per-message when the values event
1127
1548
  * carries the lightweight checkpoint envelope (populated by
@@ -1146,15 +1567,18 @@ var StreamController = class {
1146
1567
  nextValues = state;
1147
1568
  nextMessages = [];
1148
1569
  }
1149
- this.#rootMessages.applyValues(nextValues, nextMessages);
1150
- if (nextMessages.length > 0) this.rootStore.setState((s) => {
1151
- const toolCalls = reconcileToolCallsFromMessages(s.toolCalls, nextMessages);
1152
- if (toolCalls === s.toolCalls) return s;
1153
- return {
1154
- ...s,
1155
- toolCalls
1156
- };
1157
- });
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
+ }
1158
1582
  }
1159
1583
  /**
1160
1584
  * Mirror root protocol interrupts into the root snapshot.
@@ -1213,8 +1637,25 @@ var StreamController = class {
1213
1637
  * @param signal - Abort signal for the local submit lifecycle.
1214
1638
  */
1215
1639
  #awaitNextTerminal(signal) {
1640
+ return this.#awaitRootTerminal(signal, { skipInterruptedUntilRunning: false });
1641
+ }
1642
+ /**
1643
+ * Resolve on the resumed run's root terminal lifecycle.
1644
+ *
1645
+ * Unlike {@link #awaitNextTerminal}, ignores `interrupted` events until a
1646
+ * root `running` lifecycle has been observed. Headless-tool flows can emit
1647
+ * a stale `interrupted` for the run being resumed after `input.requested`
1648
+ * but before `respondInput` calls `#prepareForNextRun`; accepting that
1649
+ * terminal would unsubscribe the watcher before the resumed run's `failed`
1650
+ * terminal arrives.
1651
+ */
1652
+ #awaitResumedRunTerminal(signal) {
1653
+ return this.#awaitRootTerminal(signal, { skipInterruptedUntilRunning: true });
1654
+ }
1655
+ #awaitRootTerminal(signal, options) {
1216
1656
  return new Promise((resolve) => {
1217
1657
  let settled = false;
1658
+ let sawRunning = false;
1218
1659
  function finish(result) {
1219
1660
  if (settled) return;
1220
1661
  settled = true;
@@ -1229,12 +1670,19 @@ var StreamController = class {
1229
1670
  if (event.method !== "lifecycle") return;
1230
1671
  if (!isRootNamespace(event.params.namespace)) return;
1231
1672
  const lifecycle = event.params.data;
1673
+ if (lifecycle?.event === "running") {
1674
+ sawRunning = true;
1675
+ return;
1676
+ }
1232
1677
  if (lifecycle?.event === "completed") setTimeout(() => finish({ event: "completed" }), 0);
1233
1678
  else if (lifecycle?.event === "failed") setTimeout(() => finish({
1234
1679
  event: "failed",
1235
1680
  error: lifecycle.error
1236
1681
  }), 0);
1237
- else if (lifecycle?.event === "interrupted") setTimeout(() => finish({ event: "interrupted" }), 0);
1682
+ else if (lifecycle?.event === "interrupted") {
1683
+ if (options.skipInterruptedUntilRunning && !sawRunning) return;
1684
+ setTimeout(() => finish({ event: "interrupted" }), 0);
1685
+ }
1238
1686
  };
1239
1687
  const unsubscribeRoot = this.#rootBus.subscribe(onEvent);
1240
1688
  const unsubscribeThread = this.#thread?.onEvent(onEvent);
@@ -1261,6 +1709,23 @@ var StreamController = class {
1261
1709
  }
1262
1710
  };
1263
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
+ /**
1264
1729
  * Extract and coerce the configured messages key from a values object.
1265
1730
  *
1266
1731
  * @param values - State values object to read from.
@@ -1271,6 +1736,12 @@ function extractAndCoerceMessages(values, messagesKey) {
1271
1736
  if (!Array.isArray(raw)) return [];
1272
1737
  return ensureMessageInstances(raw);
1273
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
+ }
1274
1745
  //#endregion
1275
1746
  export { ROOT_PUMP_CHANNELS, StreamController };
1276
1747