@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.
- package/dist/client/base.cjs +70 -4
- package/dist/client/base.cjs.map +1 -1
- package/dist/client/base.d.cts +3 -0
- package/dist/client/base.d.cts.map +1 -1
- package/dist/client/base.d.ts +3 -0
- package/dist/client/base.d.ts.map +1 -1
- package/dist/client/base.js +70 -4
- package/dist/client/base.js.map +1 -1
- package/dist/client/threads/index.cjs +4 -2
- package/dist/client/threads/index.cjs.map +1 -1
- package/dist/client/threads/index.d.cts.map +1 -1
- package/dist/client/threads/index.d.ts.map +1 -1
- package/dist/client/threads/index.js +4 -2
- package/dist/client/threads/index.js.map +1 -1
- package/dist/stream/controller.cjs +451 -32
- package/dist/stream/controller.cjs.map +1 -1
- package/dist/stream/controller.d.cts +15 -0
- package/dist/stream/controller.d.cts.map +1 -1
- package/dist/stream/controller.d.ts +15 -0
- package/dist/stream/controller.d.ts.map +1 -1
- package/dist/stream/controller.js +472 -32
- package/dist/stream/controller.js.map +1 -1
- package/dist/stream/discovery/index.cjs +2 -0
- package/dist/stream/discovery/index.js +3 -0
- package/dist/stream/discovery/namespace-from-history.cjs +207 -0
- package/dist/stream/discovery/namespace-from-history.cjs.map +1 -0
- package/dist/stream/discovery/namespace-from-history.js +204 -0
- package/dist/stream/discovery/namespace-from-history.js.map +1 -0
- package/dist/stream/discovery/subagents.cjs +56 -1
- package/dist/stream/discovery/subagents.cjs.map +1 -1
- package/dist/stream/discovery/subagents.d.cts +31 -0
- package/dist/stream/discovery/subagents.d.cts.map +1 -1
- package/dist/stream/discovery/subagents.d.ts +31 -0
- package/dist/stream/discovery/subagents.d.ts.map +1 -1
- package/dist/stream/discovery/subagents.js +56 -1
- package/dist/stream/discovery/subagents.js.map +1 -1
- package/dist/stream/discovery/subgraphs.cjs +24 -0
- package/dist/stream/discovery/subgraphs.cjs.map +1 -1
- package/dist/stream/discovery/subgraphs.d.cts +13 -0
- package/dist/stream/discovery/subgraphs.d.cts.map +1 -1
- package/dist/stream/discovery/subgraphs.d.ts +13 -0
- package/dist/stream/discovery/subgraphs.d.ts.map +1 -1
- package/dist/stream/discovery/subgraphs.js +24 -0
- package/dist/stream/discovery/subgraphs.js.map +1 -1
- package/dist/stream/index.cjs +1 -0
- package/dist/stream/index.js +1 -0
- package/dist/stream/message-coercion.cjs +101 -0
- package/dist/stream/message-coercion.cjs.map +1 -0
- package/dist/stream/message-coercion.d.ts +1 -0
- package/dist/stream/message-coercion.js +98 -0
- package/dist/stream/message-coercion.js.map +1 -0
- package/dist/stream/message-metadata-tracker.cjs +92 -0
- package/dist/stream/message-metadata-tracker.cjs.map +1 -1
- package/dist/stream/message-metadata-tracker.d.cts +23 -0
- package/dist/stream/message-metadata-tracker.d.cts.map +1 -1
- package/dist/stream/message-metadata-tracker.d.ts +23 -0
- package/dist/stream/message-metadata-tracker.d.ts.map +1 -1
- package/dist/stream/message-metadata-tracker.js +92 -0
- package/dist/stream/message-metadata-tracker.js.map +1 -1
- package/dist/stream/message-reconciliation.cjs +2 -2
- package/dist/stream/message-reconciliation.cjs.map +1 -1
- package/dist/stream/message-reconciliation.js +2 -2
- package/dist/stream/message-reconciliation.js.map +1 -1
- package/dist/stream/optimistic-input.cjs +86 -0
- package/dist/stream/optimistic-input.cjs.map +1 -0
- package/dist/stream/optimistic-input.d.ts +1 -0
- package/dist/stream/optimistic-input.js +86 -0
- package/dist/stream/optimistic-input.js.map +1 -0
- package/dist/stream/projections/messages.cjs +24 -14
- package/dist/stream/projections/messages.cjs.map +1 -1
- package/dist/stream/projections/messages.js +21 -11
- package/dist/stream/projections/messages.js.map +1 -1
- package/dist/stream/projections/tool-calls.cjs +22 -10
- package/dist/stream/projections/tool-calls.cjs.map +1 -1
- package/dist/stream/projections/tool-calls.js +22 -10
- package/dist/stream/projections/tool-calls.js.map +1 -1
- package/dist/stream/projections/values.cjs +2 -2
- package/dist/stream/projections/values.cjs.map +1 -1
- package/dist/stream/projections/values.js +1 -1
- package/dist/stream/projections/values.js.map +1 -1
- package/dist/stream/root-message-projection.cjs +130 -3
- package/dist/stream/root-message-projection.cjs.map +1 -1
- package/dist/stream/root-message-projection.js +130 -3
- package/dist/stream/root-message-projection.js.map +1 -1
- package/dist/stream/submit-coordinator.cjs +28 -6
- package/dist/stream/submit-coordinator.cjs.map +1 -1
- package/dist/stream/submit-coordinator.d.cts.map +1 -1
- package/dist/stream/submit-coordinator.d.ts +0 -1
- package/dist/stream/submit-coordinator.d.ts.map +1 -1
- package/dist/stream/submit-coordinator.js +28 -6
- package/dist/stream/submit-coordinator.js.map +1 -1
- package/dist/stream/tool-calls.cjs +32 -0
- package/dist/stream/tool-calls.cjs.map +1 -1
- package/dist/stream/tool-calls.js +32 -1
- package/dist/stream/tool-calls.js.map +1 -1
- package/dist/stream/types.d.cts +43 -0
- package/dist/stream/types.d.cts.map +1 -1
- package/dist/stream/types.d.ts +43 -0
- package/dist/stream/types.d.ts.map +1 -1
- package/dist/ui/index.d.cts +1 -1
- package/dist/ui/index.d.ts +1 -1
- package/dist/ui/messages.cjs +4 -50
- package/dist/ui/messages.cjs.map +1 -1
- package/dist/ui/messages.d.cts.map +1 -1
- package/dist/ui/messages.d.ts.map +1 -1
- package/dist/ui/messages.js +3 -48
- package/dist/ui/messages.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
|
|
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;
|
|
@@ -218,7 +288,9 @@ var StreamController = class {
|
|
|
218
288
|
onRunStart: () => this.#markLocalRunStart(),
|
|
219
289
|
onRunCreated: (runId) => this.#notifyCreated(runId),
|
|
220
290
|
onRunCompleted: (reason, runId) => this.#notifyCompleted(reason, runId),
|
|
221
|
-
onRunEnd: () => this.#markLocalRunEnd()
|
|
291
|
+
onRunEnd: () => this.#markLocalRunEnd(),
|
|
292
|
+
beginOptimistic: (input) => this.#beginOptimistic(input),
|
|
293
|
+
settleOptimistic: (handle, event) => this.#settleOptimistic(handle, event)
|
|
222
294
|
});
|
|
223
295
|
this.#hydrationPromise = this.#createHydrationPromise();
|
|
224
296
|
/**
|
|
@@ -285,6 +357,8 @@ var StreamController = class {
|
|
|
285
357
|
const target = threadId === void 0 ? this.#currentThreadId : threadId;
|
|
286
358
|
const changed = target !== this.#currentThreadId;
|
|
287
359
|
this.#currentThreadId = target ?? null;
|
|
360
|
+
this.#discoverySeedPromise = void 0;
|
|
361
|
+
this.#scopedHistorySeeds.clear();
|
|
288
362
|
this.rootStore.setState((s) => ({
|
|
289
363
|
...s,
|
|
290
364
|
threadId: this.#currentThreadId
|
|
@@ -341,9 +415,11 @@ var StreamController = class {
|
|
|
341
415
|
}));
|
|
342
416
|
let hydrationError;
|
|
343
417
|
let threadExists = false;
|
|
418
|
+
let threadActive = true;
|
|
344
419
|
try {
|
|
345
420
|
const state = await this.#options.client.threads.getState(this.#currentThreadId);
|
|
346
421
|
threadExists = state != null;
|
|
422
|
+
threadActive = isThreadStateActive(state);
|
|
347
423
|
if (state?.values != null) {
|
|
348
424
|
/**
|
|
349
425
|
* `threads.getState()` returns the legacy `ThreadState` shape
|
|
@@ -355,11 +431,43 @@ var StreamController = class {
|
|
|
355
431
|
*/
|
|
356
432
|
const checkpointId = state.checkpoint?.checkpoint_id;
|
|
357
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;
|
|
358
443
|
const syntheticCheckpoint = typeof checkpointId === "string" ? {
|
|
359
444
|
id: checkpointId,
|
|
360
|
-
...parentCheckpointId != null ? { parent_id: parentCheckpointId } : {}
|
|
445
|
+
...parentCheckpointId != null ? { parent_id: parentCheckpointId } : {},
|
|
446
|
+
...typeof seedStep === "number" ? { step: seedStep } : {}
|
|
361
447
|
} : void 0;
|
|
362
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);
|
|
363
471
|
}
|
|
364
472
|
/**
|
|
365
473
|
* Sync the visible interrupt list to the server's authoritative
|
|
@@ -413,27 +521,253 @@ var StreamController = class {
|
|
|
413
521
|
else this.#resolveHydration();
|
|
414
522
|
}
|
|
415
523
|
/**
|
|
416
|
-
*
|
|
417
|
-
*
|
|
418
|
-
*
|
|
419
|
-
*
|
|
420
|
-
* `
|
|
421
|
-
*
|
|
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.
|
|
422
535
|
*/
|
|
423
|
-
const thread = this.#ensureThread(this.#currentThreadId);
|
|
536
|
+
const thread = this.#ensureThread(this.#currentThreadId, !threadActive);
|
|
424
537
|
/**
|
|
425
|
-
* Start the wildcard lifecycle watcher up-front for existing
|
|
426
|
-
* threads. The root content pump runs at `depth: 1`, which
|
|
427
|
-
* root-namespace and one-deep events but not arbitrarily-
|
|
428
|
-
* subagent / subgraph lifecycle — the dedicated watcher
|
|
429
|
-
* 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.
|
|
430
543
|
*
|
|
431
|
-
*
|
|
432
|
-
*
|
|
433
|
-
*
|
|
434
|
-
*
|
|
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.
|
|
435
551
|
*/
|
|
436
|
-
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
|
+
}
|
|
437
771
|
}
|
|
438
772
|
/**
|
|
439
773
|
* Submit input to the active thread.
|
|
@@ -872,6 +1206,7 @@ var StreamController = class {
|
|
|
872
1206
|
this.#lifecycleLoading.reset();
|
|
873
1207
|
this.#subagents.reset();
|
|
874
1208
|
this.#subgraphs.reset();
|
|
1209
|
+
this.#scopedHistorySeeds.clear();
|
|
875
1210
|
this.#activeRunId = void 0;
|
|
876
1211
|
this.#localRunDepth = 0;
|
|
877
1212
|
this.#messageMetadata.reset();
|
|
@@ -1148,9 +1483,67 @@ var StreamController = class {
|
|
|
1148
1483
|
* @param raw - Raw `values` channel payload.
|
|
1149
1484
|
* @param checkpoint - Optional checkpoint envelope paired with the values event.
|
|
1150
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
|
+
}
|
|
1151
1543
|
#applyValues(raw, checkpoint) {
|
|
1152
1544
|
if (raw == null || typeof raw !== "object" || Array.isArray(raw)) return;
|
|
1153
1545
|
const state = raw;
|
|
1546
|
+
this.#sawValuesForRun = true;
|
|
1154
1547
|
/**
|
|
1155
1548
|
* Surface parent_checkpoint per-message when the values event
|
|
1156
1549
|
* carries the lightweight checkpoint envelope (populated by
|
|
@@ -1165,7 +1558,7 @@ var StreamController = class {
|
|
|
1165
1558
|
let nextValues;
|
|
1166
1559
|
let nextMessages;
|
|
1167
1560
|
if (Array.isArray(maybeMessages)) {
|
|
1168
|
-
const coerced =
|
|
1561
|
+
const coerced = require_message_coercion.ensureMessageInstances(maybeMessages);
|
|
1169
1562
|
nextValues = {
|
|
1170
1563
|
...state,
|
|
1171
1564
|
[this.#messagesKey]: coerced
|
|
@@ -1175,15 +1568,18 @@ var StreamController = class {
|
|
|
1175
1568
|
nextValues = state;
|
|
1176
1569
|
nextMessages = [];
|
|
1177
1570
|
}
|
|
1178
|
-
this.#rootMessages.applyValues(nextValues, nextMessages);
|
|
1179
|
-
if (nextMessages.length > 0)
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
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
|
+
}
|
|
1187
1583
|
}
|
|
1188
1584
|
/**
|
|
1189
1585
|
* Mirror root protocol interrupts into the root snapshot.
|
|
@@ -1314,6 +1710,23 @@ var StreamController = class {
|
|
|
1314
1710
|
}
|
|
1315
1711
|
};
|
|
1316
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
|
+
/**
|
|
1317
1730
|
* Extract and coerce the configured messages key from a values object.
|
|
1318
1731
|
*
|
|
1319
1732
|
* @param values - State values object to read from.
|
|
@@ -1322,7 +1735,13 @@ var StreamController = class {
|
|
|
1322
1735
|
function extractAndCoerceMessages(values, messagesKey) {
|
|
1323
1736
|
const raw = values[messagesKey];
|
|
1324
1737
|
if (!Array.isArray(raw)) return [];
|
|
1325
|
-
return
|
|
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);
|
|
1326
1745
|
}
|
|
1327
1746
|
//#endregion
|
|
1328
1747
|
exports.ROOT_PUMP_CHANNELS = ROOT_PUMP_CHANNELS;
|