@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,20 +1,47 @@
|
|
|
1
|
-
import { ensureMessageInstances } from "
|
|
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
|
-
*
|
|
395
|
-
*
|
|
396
|
-
*
|
|
397
|
-
*
|
|
398
|
-
* `
|
|
399
|
-
*
|
|
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
|
|
405
|
-
* root-namespace and one-deep events but not arbitrarily-
|
|
406
|
-
* subagent / subgraph lifecycle — the dedicated watcher
|
|
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
|
-
*
|
|
410
|
-
*
|
|
411
|
-
*
|
|
412
|
-
*
|
|
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)
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
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
|
|