@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.
- 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 +496 -46
- 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 +517 -46
- 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 +100 -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 +100 -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 +4 -4
|
@@ -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;
|
|
@@ -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
|
-
*
|
|
394
|
-
*
|
|
395
|
-
*
|
|
396
|
-
*
|
|
397
|
-
* `
|
|
398
|
-
*
|
|
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
|
|
404
|
-
* root-namespace and one-deep events but not arbitrarily-
|
|
405
|
-
* subagent / subgraph lifecycle — the dedicated watcher
|
|
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
|
-
*
|
|
409
|
-
*
|
|
410
|
-
*
|
|
411
|
-
*
|
|
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.#
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
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
|
|
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.#
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
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)
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
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")
|
|
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
|
|