@langchain/langgraph-sdk 1.9.27 → 1.9.29
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/stream/types.d.cts +4 -4
- package/dist/client/stream/types.d.ts +4 -4
- package/dist/client/threads/index.cjs +3 -0
- package/dist/client/threads/index.cjs.map +1 -1
- package/dist/client/threads/index.js +3 -0
- package/dist/client/threads/index.js.map +1 -1
- package/dist/schema.d.cts +7 -0
- package/dist/schema.d.cts.map +1 -1
- package/dist/schema.d.ts +7 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/stream/controller.cjs +87 -57
- package/dist/stream/controller.cjs.map +1 -1
- package/dist/stream/controller.d.cts +12 -23
- package/dist/stream/controller.d.cts.map +1 -1
- package/dist/stream/controller.d.ts +12 -23
- package/dist/stream/controller.d.ts.map +1 -1
- package/dist/stream/controller.js +87 -57
- package/dist/stream/controller.js.map +1 -1
- package/dist/stream/projections/channel.cjs +4 -6
- package/dist/stream/projections/channel.cjs.map +1 -1
- package/dist/stream/projections/channel.d.cts.map +1 -1
- package/dist/stream/projections/channel.d.ts.map +1 -1
- package/dist/stream/projections/channel.js +4 -6
- package/dist/stream/projections/channel.js.map +1 -1
- package/dist/stream/submit-coordinator.cjs +7 -5
- package/dist/stream/submit-coordinator.cjs.map +1 -1
- package/dist/stream/submit-coordinator.js +7 -5
- package/dist/stream/submit-coordinator.js.map +1 -1
- package/dist/stream/types.d.cts +3 -3
- package/dist/stream/types.d.ts +3 -3
- package/dist/utils/async_caller.cjs +1 -1
- package/dist/utils/async_caller.cjs.map +1 -1
- package/dist/utils/async_caller.d.cts.map +1 -1
- package/dist/utils/async_caller.d.ts.map +1 -1
- package/dist/utils/async_caller.js +1 -1
- package/dist/utils/async_caller.js.map +1 -1
- package/package.json +10 -10
|
@@ -120,14 +120,15 @@ declare class StreamController<StateType extends object = Record<string, unknown
|
|
|
120
120
|
* oldest and picks the first entry whose `interruptId` has not already
|
|
121
121
|
* been resolved by a prior `respond()` call. That entry may be at the
|
|
122
122
|
* root (`namespace: []`) or inside a subgraph (non-empty `namespace`).
|
|
123
|
-
*
|
|
124
|
-
* `
|
|
125
|
-
*
|
|
123
|
+
* {@link RootSnapshot.interrupts `rootStore.interrupts`} /
|
|
124
|
+
* framework `stream.interrupts` mirrors the same pending interrupts
|
|
125
|
+
* (including nested namespaces) for UI rendering.
|
|
126
126
|
*
|
|
127
127
|
* Omitting `interruptId` is fine when exactly one interrupt is pending.
|
|
128
128
|
* When several can be active (parallel subagents, fan-out, nested
|
|
129
|
-
* graphs), pass an explicit `interruptId`
|
|
130
|
-
*
|
|
129
|
+
* graphs), pass an explicit `interruptId` so you resume the interrupt
|
|
130
|
+
* the user acted on. `namespace` is resolved automatically from
|
|
131
|
+
* `thread.interrupts` / `stream.interrupts` when omitted.
|
|
131
132
|
*
|
|
132
133
|
* To resume several interrupts pending at the same checkpoint in one
|
|
133
134
|
* command, use {@link respondAll} — sequential single `respond()` calls
|
|
@@ -135,9 +136,8 @@ declare class StreamController<StateType extends object = Record<string, unknown
|
|
|
135
136
|
* others with no interrupted run to respond to.
|
|
136
137
|
*
|
|
137
138
|
* The server validates `namespace` against the pending interrupt. Root
|
|
138
|
-
* interrupts use `namespace: []
|
|
139
|
-
*
|
|
140
|
-
* `getThread()?.interrupts` — see the example below.
|
|
139
|
+
* interrupts use `namespace: []`. Subgraph interrupts carry the exact
|
|
140
|
+
* tuple on each {@link Interrupt} / {@link InterruptPayload} entry.
|
|
141
141
|
*
|
|
142
142
|
* @param response - Payload sent back to the interrupted namespace.
|
|
143
143
|
* @param options - Optional target (`interruptId` / `namespace`) and
|
|
@@ -158,27 +158,16 @@ declare class StreamController<StateType extends object = Record<string, unknown
|
|
|
158
158
|
* );
|
|
159
159
|
* ```
|
|
160
160
|
*
|
|
161
|
-
* @example Multiple
|
|
161
|
+
* @example Multiple / nested interrupts — target by id
|
|
162
162
|
* ```tsx
|
|
163
163
|
* for (const intr of stream.interrupts) {
|
|
164
164
|
* await stream.respond(decide(intr.value), { interruptId: intr.id! });
|
|
165
165
|
* }
|
|
166
166
|
* ```
|
|
167
167
|
*
|
|
168
|
-
* @
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
* for (const entry of thread?.interrupts ?? []) {
|
|
172
|
-
* await stream.respond(buildResponse(entry.payload), {
|
|
173
|
-
* interruptId: entry.interruptId,
|
|
174
|
-
* namespace: entry.namespace,
|
|
175
|
-
* });
|
|
176
|
-
* }
|
|
177
|
-
* ```
|
|
178
|
-
*
|
|
179
|
-
* Each {@link InterruptPayload} on `thread.interrupts` mirrors an
|
|
180
|
-
* `input.requested` event: `{ interruptId, payload, namespace }`.
|
|
181
|
-
* Nested interrupts may appear here but not on `stream.interrupts`.
|
|
168
|
+
* Each {@link InterruptPayload} on `thread.interrupts` and each
|
|
169
|
+
* {@link Interrupt} on `stream.interrupts` mirrors an
|
|
170
|
+
* `input.requested` event with its protocol `namespace`.
|
|
182
171
|
*/
|
|
183
172
|
respond(response: unknown, options?: StreamRespondOptions<ConfigurableType>): Promise<void>;
|
|
184
173
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"controller.d.cts","names":[],"sources":["../../src/stream/controller.ts"],"mappings":";;;;;;;;;;;;;;AA6KA;;cAAa,kBAAA,WAA6B,OAAA;;AAkC1C;;;;;;;;;;;;cAAa,gBAAA,4BACgB,MAAA,8EAEO,MAAA;EAAA;WAEzB,SAAA,EAAW,WAAA,CAAY,YAAA,CAAa,SAAA,EAAW,aAAA;EAAA,SAC/C,aAAA,EAAe,WAAA,CAAY,WAAA;EAAA,SAC3B,aAAA,EAAe,WAAA,CAAY,WAAA;EAAA,SAC3B,mBAAA,EAAqB,WAAA,CAAY,iBAAA;EAAA,SACjC,oBAAA,EAAsB,WAAA,CAAY,kBAAA;EAAA,SAClC,UAAA,EAAY,WAAA,CAAY,uBAAA,CAAwB,SAAA;EAAA,SAChD,QAAA,EAAU,eAAA;
|
|
1
|
+
{"version":3,"file":"controller.d.cts","names":[],"sources":["../../src/stream/controller.ts"],"mappings":";;;;;;;;;;;;;;AA6KA;;cAAa,kBAAA,WAA6B,OAAA;;AAkC1C;;;;;;;;;;;;cAAa,gBAAA,4BACgB,MAAA,8EAEO,MAAA;EAAA;WAEzB,SAAA,EAAW,WAAA,CAAY,YAAA,CAAa,SAAA,EAAW,aAAA;EAAA,SAC/C,aAAA,EAAe,WAAA,CAAY,WAAA;EAAA,SAC3B,aAAA,EAAe,WAAA,CAAY,WAAA;EAAA,SAC3B,mBAAA,EAAqB,WAAA,CAAY,iBAAA;EAAA,SACjC,oBAAA,EAAsB,WAAA,CAAY,kBAAA;EAAA,SAClC,UAAA,EAAY,WAAA,CAAY,uBAAA,CAAwB,SAAA;EAAA,SAChD,QAAA,EAAU,eAAA;EAgI0B;;;;;EAA7C,WAAA,CAAY,OAAA,EAAS,uBAAA,CAAwB,SAAA;EAsyBF;;;;;;;EAAA,IAvrBvC,gBAAA,CAAA,GAAoB,OAAA;EA41BS;;;;;;;;EAnyB3B,OAAA,CAAQ,QAAA,mBAA2B,OAAA;EAihCpB;;;;;;;;;;;;;;EAzpBf,wBAAA,CAAyB,UAAA,WAAqB,OAAA;EArqB5B;;;;;;;;EAy6BlB,MAAA,CACJ,KAAA,WACA,OAAA,GAAU,mBAAA,CAAoB,SAAA,EAAW,gBAAA,IACxC,OAAA;EAz6B4B;;;;;;EAm7BzB,IAAA,CAAK,OAAA,GAAU,iBAAA,GAAoB,OAAA;EAj7BtB;;;;EAq8Bb,UAAA,CAAA,GAAc,OAAA;EAttBhB;;;;;;;;;;;EAsxBE,YAAA,CAAa,EAAA,WAAa,OAAA;EA/FW;;;EAsGrC,UAAA,CAAA,GAAc,OAAA;EA3FC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAwJf,OAAA,CACJ,QAAA,WACA,OAAA,GAAU,oBAAA,CAAqB,gBAAA,IAC9B,OAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA+GG,UAAA,CACJ,aAAA,EAAe,MAAA,mBACf,OAAA,GAAU,uBAAA,CAAwB,gBAAA,IACjC,OAAA;;;;EA0DG,OAAA,CAAA,GAAW,OAAA;;;;;;;;;;;;;;;;EAyBjB,QAAA,CAAA;;;;;;EA4BA,SAAA,CAAA,GAAa,YAAA;;;;;;;;EAWb,eAAA,CACE,QAAA,GAAW,MAAA,EAAQ,YAAA;AAAA"}
|
|
@@ -120,14 +120,15 @@ declare class StreamController<StateType extends object = Record<string, unknown
|
|
|
120
120
|
* oldest and picks the first entry whose `interruptId` has not already
|
|
121
121
|
* been resolved by a prior `respond()` call. That entry may be at the
|
|
122
122
|
* root (`namespace: []`) or inside a subgraph (non-empty `namespace`).
|
|
123
|
-
*
|
|
124
|
-
* `
|
|
125
|
-
*
|
|
123
|
+
* {@link RootSnapshot.interrupts `rootStore.interrupts`} /
|
|
124
|
+
* framework `stream.interrupts` mirrors the same pending interrupts
|
|
125
|
+
* (including nested namespaces) for UI rendering.
|
|
126
126
|
*
|
|
127
127
|
* Omitting `interruptId` is fine when exactly one interrupt is pending.
|
|
128
128
|
* When several can be active (parallel subagents, fan-out, nested
|
|
129
|
-
* graphs), pass an explicit `interruptId`
|
|
130
|
-
*
|
|
129
|
+
* graphs), pass an explicit `interruptId` so you resume the interrupt
|
|
130
|
+
* the user acted on. `namespace` is resolved automatically from
|
|
131
|
+
* `thread.interrupts` / `stream.interrupts` when omitted.
|
|
131
132
|
*
|
|
132
133
|
* To resume several interrupts pending at the same checkpoint in one
|
|
133
134
|
* command, use {@link respondAll} — sequential single `respond()` calls
|
|
@@ -135,9 +136,8 @@ declare class StreamController<StateType extends object = Record<string, unknown
|
|
|
135
136
|
* others with no interrupted run to respond to.
|
|
136
137
|
*
|
|
137
138
|
* The server validates `namespace` against the pending interrupt. Root
|
|
138
|
-
* interrupts use `namespace: []
|
|
139
|
-
*
|
|
140
|
-
* `getThread()?.interrupts` — see the example below.
|
|
139
|
+
* interrupts use `namespace: []`. Subgraph interrupts carry the exact
|
|
140
|
+
* tuple on each {@link Interrupt} / {@link InterruptPayload} entry.
|
|
141
141
|
*
|
|
142
142
|
* @param response - Payload sent back to the interrupted namespace.
|
|
143
143
|
* @param options - Optional target (`interruptId` / `namespace`) and
|
|
@@ -158,27 +158,16 @@ declare class StreamController<StateType extends object = Record<string, unknown
|
|
|
158
158
|
* );
|
|
159
159
|
* ```
|
|
160
160
|
*
|
|
161
|
-
* @example Multiple
|
|
161
|
+
* @example Multiple / nested interrupts — target by id
|
|
162
162
|
* ```tsx
|
|
163
163
|
* for (const intr of stream.interrupts) {
|
|
164
164
|
* await stream.respond(decide(intr.value), { interruptId: intr.id! });
|
|
165
165
|
* }
|
|
166
166
|
* ```
|
|
167
167
|
*
|
|
168
|
-
* @
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
* for (const entry of thread?.interrupts ?? []) {
|
|
172
|
-
* await stream.respond(buildResponse(entry.payload), {
|
|
173
|
-
* interruptId: entry.interruptId,
|
|
174
|
-
* namespace: entry.namespace,
|
|
175
|
-
* });
|
|
176
|
-
* }
|
|
177
|
-
* ```
|
|
178
|
-
*
|
|
179
|
-
* Each {@link InterruptPayload} on `thread.interrupts` mirrors an
|
|
180
|
-
* `input.requested` event: `{ interruptId, payload, namespace }`.
|
|
181
|
-
* Nested interrupts may appear here but not on `stream.interrupts`.
|
|
168
|
+
* Each {@link InterruptPayload} on `thread.interrupts` and each
|
|
169
|
+
* {@link Interrupt} on `stream.interrupts` mirrors an
|
|
170
|
+
* `input.requested` event with its protocol `namespace`.
|
|
182
171
|
*/
|
|
183
172
|
respond(response: unknown, options?: StreamRespondOptions<ConfigurableType>): Promise<void>;
|
|
184
173
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"controller.d.ts","names":[],"sources":["../../src/stream/controller.ts"],"mappings":";;;;;;;;;;;;;;;AA6KA;cAAa,kBAAA,WAA6B,OAAA;;;AAkC1C;;;;;;;;;;;cAAa,gBAAA,4BACgB,MAAA,8EAEO,MAAA;EAAA;WAEzB,SAAA,EAAW,WAAA,CAAY,YAAA,CAAa,SAAA,EAAW,aAAA;EAAA,SAC/C,aAAA,EAAe,WAAA,CAAY,WAAA;EAAA,SAC3B,aAAA,EAAe,WAAA,CAAY,WAAA;EAAA,SAC3B,mBAAA,EAAqB,WAAA,CAAY,iBAAA;EAAA,SACjC,oBAAA,EAAsB,WAAA,CAAY,kBAAA;EAAA,SAClC,UAAA,EAAY,WAAA,CAAY,uBAAA,CAAwB,SAAA;EAAA,SAChD,QAAA,EAAU,eAAA;EAAA;;;;;
|
|
1
|
+
{"version":3,"file":"controller.d.ts","names":[],"sources":["../../src/stream/controller.ts"],"mappings":";;;;;;;;;;;;;;;AA6KA;cAAa,kBAAA,WAA6B,OAAA;;;AAkC1C;;;;;;;;;;;cAAa,gBAAA,4BACgB,MAAA,8EAEO,MAAA;EAAA;WAEzB,SAAA,EAAW,WAAA,CAAY,YAAA,CAAa,SAAA,EAAW,aAAA;EAAA,SAC/C,aAAA,EAAe,WAAA,CAAY,WAAA;EAAA,SAC3B,aAAA,EAAe,WAAA,CAAY,WAAA;EAAA,SAC3B,mBAAA,EAAqB,WAAA,CAAY,iBAAA;EAAA,SACjC,oBAAA,EAAsB,WAAA,CAAY,kBAAA;EAAA,SAClC,UAAA,EAAY,WAAA,CAAY,uBAAA,CAAwB,SAAA;EAAA,SAChD,QAAA,EAAU,eAAA;EAAA;;;;;EAgInB,WAAA,CAAY,OAAA,EAAS,uBAAA,CAAwB,SAAA;EAsyBb;;;;;;;EAAA,IAvrB5B,gBAAA,CAAA,GAAoB,OAAA;EA6xBJ;;;;;;;;EApuBd,OAAA,CAAQ,QAAA,mBAA2B,OAAA;EAqgC5B;;;;;;;;;;;;;;EA7oBP,wBAAA,CAAyB,UAAA,WAAqB,OAAA;EArqB3C;;;;;;;;EAy6BH,MAAA,CACJ,KAAA,WACA,OAAA,GAAU,mBAAA,CAAoB,SAAA,EAAW,gBAAA,IACxC,OAAA;EAz6BM;;;;;;EAm7BH,IAAA,CAAK,OAAA,GAAU,iBAAA,GAAoB,OAAA;EAj7BhC;;;;EAq8BH,UAAA,CAAA,GAAc,OAAA;EAr0BR;;;;;;;;;;;EAq4BN,YAAA,CAAa,EAAA,WAAa,OAAA;EA/FA;;;EAsG1B,UAAA,CAAA,GAAc,OAAA;EA3Fd;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAwJA,OAAA,CACJ,QAAA,WACA,OAAA,GAAU,oBAAA,CAAqB,gBAAA,IAC9B,OAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA+GG,UAAA,CACJ,aAAA,EAAe,MAAA,mBACf,OAAA,GAAU,uBAAA,CAAwB,gBAAA,IACjC,OAAA;;;;EA0DG,OAAA,CAAA,GAAW,OAAA;;;;;;;;;;;;;;;;EAyBjB,QAAA,CAAA;;;;;;EA4BA,SAAA,CAAA,GAAa,YAAA;;;;;;;;EAWb,eAAA,CACE,QAAA,GAAW,MAAA,EAAQ,YAAA;AAAA"}
|
|
@@ -169,13 +169,23 @@ var StreamController = class {
|
|
|
169
169
|
*/
|
|
170
170
|
#submitGeneration = 0;
|
|
171
171
|
/**
|
|
172
|
-
* Thread ids
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
172
|
+
* Thread ids this controller minted client-side on first `submit()`.
|
|
173
|
+
* `hydrate()` skips `threads.getState()` for these — we know there
|
|
174
|
+
* is nothing checkpointed yet. Cleared once dispatch commits the
|
|
175
|
+
* thread server-side.
|
|
176
176
|
*/
|
|
177
177
|
#selfCreatedThreadIds = /* @__PURE__ */ new Set();
|
|
178
178
|
/**
|
|
179
|
+
* Caller-supplied thread ids that 404'd on hydrate. Unlike
|
|
180
|
+
* {@link #selfCreatedThreadIds}, these are revalidated on every
|
|
181
|
+
* hydrate: another client may create the same id between visits.
|
|
182
|
+
* While marked missing, `submit()` still defers the root SSE pump
|
|
183
|
+
* until `run.start` / `commands` commits the row — joining
|
|
184
|
+
* `/stream/events` beforehand leaves a dead subscription on
|
|
185
|
+
* langgraph_api's in-mem runtime.
|
|
186
|
+
*/
|
|
187
|
+
#missingThreadIds = /* @__PURE__ */ new Set();
|
|
188
|
+
/**
|
|
179
189
|
* In-flight per-subagent namespace resolutions, keyed by tool-call
|
|
180
190
|
* id. De-dupes concurrent {@link resolveSubagentNamespace} calls so
|
|
181
191
|
* re-renders / multiple consumers of the same subagent don't issue
|
|
@@ -270,12 +280,14 @@ var StreamController = class {
|
|
|
270
280
|
rememberSelfCreatedThreadId: (threadId) => {
|
|
271
281
|
this.#selfCreatedThreadIds.add(threadId);
|
|
272
282
|
},
|
|
283
|
+
isSelfCreatedThreadId: (threadId) => this.#selfCreatedThreadIds.has(threadId) || this.#missingThreadIds.has(threadId),
|
|
273
284
|
hydrate: (threadId) => this.hydrate(threadId),
|
|
274
285
|
ensureThread: (threadId, deferRootPump) => this.#ensureThread(threadId, deferRootPump),
|
|
275
286
|
startDeferredRootPump: () => this.#startDeferredRootPump(),
|
|
276
287
|
abandonDeferredRootPump: () => this.#abandonDeferredRootPump(),
|
|
277
288
|
forgetSelfCreatedThreadId: (threadId) => {
|
|
278
289
|
this.#selfCreatedThreadIds.delete(threadId);
|
|
290
|
+
this.#missingThreadIds.delete(threadId);
|
|
279
291
|
},
|
|
280
292
|
waitForRootPumpReady: () => this.#rootPumpReady,
|
|
281
293
|
awaitNextTerminal: (signal) => this.#awaitNextTerminal(signal),
|
|
@@ -423,13 +435,17 @@ var StreamController = class {
|
|
|
423
435
|
...s,
|
|
424
436
|
isThreadLoading: true
|
|
425
437
|
}));
|
|
438
|
+
const hydratedThreadId = this.#currentThreadId;
|
|
426
439
|
let hydrationError;
|
|
427
440
|
let threadExists = false;
|
|
441
|
+
let threadMissing = false;
|
|
428
442
|
let threadActive = true;
|
|
429
443
|
try {
|
|
430
444
|
const state = await this.#fetchHydrationState();
|
|
445
|
+
if (this.#disposed || this.#currentThreadId !== hydratedThreadId) return;
|
|
431
446
|
threadExists = state != null;
|
|
432
447
|
threadActive = isThreadStateActive(state);
|
|
448
|
+
this.#missingThreadIds.delete(hydratedThreadId);
|
|
433
449
|
if (state?.values != null) {
|
|
434
450
|
/**
|
|
435
451
|
* `threads.getState()` returns the legacy `ThreadState` shape
|
|
@@ -514,6 +530,8 @@ var StreamController = class {
|
|
|
514
530
|
const activeIds = /* @__PURE__ */ new Set();
|
|
515
531
|
for (const task of state.tasks) {
|
|
516
532
|
if (!Array.isArray(task?.interrupts)) continue;
|
|
533
|
+
const checkpointNs = task?.checkpoint?.checkpoint_ns;
|
|
534
|
+
const namespace = typeof checkpointNs === "string" && checkpointNs.length > 0 ? checkpointNs.split("|").filter((segment) => segment.length > 0) : [...ROOT_NAMESPACE];
|
|
517
535
|
for (const interrupt of task.interrupts) {
|
|
518
536
|
const typed = interrupt;
|
|
519
537
|
const id = typed?.id;
|
|
@@ -521,7 +539,8 @@ var StreamController = class {
|
|
|
521
539
|
activeIds.add(id);
|
|
522
540
|
activeInterrupts.push(normalizeInterruptForClient({
|
|
523
541
|
id,
|
|
524
|
-
value: typed?.value
|
|
542
|
+
value: typed?.value,
|
|
543
|
+
namespace: Array.isArray(typed?.namespace) ? [...typed.namespace] : namespace
|
|
525
544
|
}));
|
|
526
545
|
}
|
|
527
546
|
}
|
|
@@ -539,7 +558,7 @@ var StreamController = class {
|
|
|
539
558
|
...s,
|
|
540
559
|
error
|
|
541
560
|
}));
|
|
542
|
-
}
|
|
561
|
+
} else threadMissing = true;
|
|
543
562
|
} finally {
|
|
544
563
|
this.rootStore.setState((s) => ({
|
|
545
564
|
...s,
|
|
@@ -549,6 +568,20 @@ var StreamController = class {
|
|
|
549
568
|
else this.#resolveHydration();
|
|
550
569
|
}
|
|
551
570
|
/**
|
|
571
|
+
* Missing threads must not open `/stream/events`. The in-mem
|
|
572
|
+
* runtime accepts the join against a not-yet-created id but the
|
|
573
|
+
* subscription never attaches to the live channel — the first run
|
|
574
|
+
* then delivers 0 bytes until idle reconnect. Park the pump until
|
|
575
|
+
* `submit()`'s `run.start` creates the row (see
|
|
576
|
+
* {@link #startDeferredRootPump}). Tracked in `#missingThreadIds`
|
|
577
|
+
* (not `#selfCreatedThreadIds`) so a later hydrate revalidates —
|
|
578
|
+
* another client may create the same id between visits.
|
|
579
|
+
*/
|
|
580
|
+
if (threadMissing) {
|
|
581
|
+
this.#missingThreadIds.add(hydratedThreadId);
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
552
585
|
* Open the shared subscription on mount so in-flight server-side
|
|
553
586
|
* runs are observed even when no local `submit()` is active — BUT
|
|
554
587
|
* only when the thread is actually active (see
|
|
@@ -561,7 +594,7 @@ var StreamController = class {
|
|
|
561
594
|
* The transport replays from `seq=0` on the deferred subscribe, so
|
|
562
595
|
* nothing is missed.
|
|
563
596
|
*/
|
|
564
|
-
const thread = this.#ensureThread(
|
|
597
|
+
const thread = this.#ensureThread(hydratedThreadId, !threadActive);
|
|
565
598
|
/**
|
|
566
599
|
* Start the wildcard lifecycle watcher up-front for existing,
|
|
567
600
|
* active threads. The root content pump runs at `depth: 1`, which
|
|
@@ -685,7 +718,7 @@ var StreamController = class {
|
|
|
685
718
|
*/
|
|
686
719
|
async #trySeedProjectionFromHistory(params) {
|
|
687
720
|
const threadId = this.#currentThreadId;
|
|
688
|
-
if (this.#disposed || threadId == null || params.namespace.length === 0 || !this.#rootPumpDeferred || this.#selfCreatedThreadIds.has(threadId)) return false;
|
|
721
|
+
if (this.#disposed || threadId == null || params.namespace.length === 0 || !this.#rootPumpDeferred || this.#selfCreatedThreadIds.has(threadId) || this.#missingThreadIds.has(threadId)) return false;
|
|
689
722
|
if (await this.#skipDefaultSubagentProjection(params.namespace, threadId)) return true;
|
|
690
723
|
if (this.#disposed || this.#currentThreadId !== threadId || !this.#rootPumpDeferred) return false;
|
|
691
724
|
const seed = await this.#getScopedHistorySeed(threadId, params.namespace);
|
|
@@ -893,14 +926,15 @@ var StreamController = class {
|
|
|
893
926
|
* oldest and picks the first entry whose `interruptId` has not already
|
|
894
927
|
* been resolved by a prior `respond()` call. That entry may be at the
|
|
895
928
|
* root (`namespace: []`) or inside a subgraph (non-empty `namespace`).
|
|
896
|
-
*
|
|
897
|
-
* `
|
|
898
|
-
*
|
|
929
|
+
* {@link RootSnapshot.interrupts `rootStore.interrupts`} /
|
|
930
|
+
* framework `stream.interrupts` mirrors the same pending interrupts
|
|
931
|
+
* (including nested namespaces) for UI rendering.
|
|
899
932
|
*
|
|
900
933
|
* Omitting `interruptId` is fine when exactly one interrupt is pending.
|
|
901
934
|
* When several can be active (parallel subagents, fan-out, nested
|
|
902
|
-
* graphs), pass an explicit `interruptId`
|
|
903
|
-
*
|
|
935
|
+
* graphs), pass an explicit `interruptId` so you resume the interrupt
|
|
936
|
+
* the user acted on. `namespace` is resolved automatically from
|
|
937
|
+
* `thread.interrupts` / `stream.interrupts` when omitted.
|
|
904
938
|
*
|
|
905
939
|
* To resume several interrupts pending at the same checkpoint in one
|
|
906
940
|
* command, use {@link respondAll} — sequential single `respond()` calls
|
|
@@ -908,9 +942,8 @@ var StreamController = class {
|
|
|
908
942
|
* others with no interrupted run to respond to.
|
|
909
943
|
*
|
|
910
944
|
* The server validates `namespace` against the pending interrupt. Root
|
|
911
|
-
* interrupts use `namespace: []
|
|
912
|
-
*
|
|
913
|
-
* `getThread()?.interrupts` — see the example below.
|
|
945
|
+
* interrupts use `namespace: []`. Subgraph interrupts carry the exact
|
|
946
|
+
* tuple on each {@link Interrupt} / {@link InterruptPayload} entry.
|
|
914
947
|
*
|
|
915
948
|
* @param response - Payload sent back to the interrupted namespace.
|
|
916
949
|
* @param options - Optional target (`interruptId` / `namespace`) and
|
|
@@ -931,33 +964,22 @@ var StreamController = class {
|
|
|
931
964
|
* );
|
|
932
965
|
* ```
|
|
933
966
|
*
|
|
934
|
-
* @example Multiple
|
|
967
|
+
* @example Multiple / nested interrupts — target by id
|
|
935
968
|
* ```tsx
|
|
936
969
|
* for (const intr of stream.interrupts) {
|
|
937
970
|
* await stream.respond(decide(intr.value), { interruptId: intr.id! });
|
|
938
971
|
* }
|
|
939
972
|
* ```
|
|
940
973
|
*
|
|
941
|
-
* @
|
|
942
|
-
*
|
|
943
|
-
*
|
|
944
|
-
* for (const entry of thread?.interrupts ?? []) {
|
|
945
|
-
* await stream.respond(buildResponse(entry.payload), {
|
|
946
|
-
* interruptId: entry.interruptId,
|
|
947
|
-
* namespace: entry.namespace,
|
|
948
|
-
* });
|
|
949
|
-
* }
|
|
950
|
-
* ```
|
|
951
|
-
*
|
|
952
|
-
* Each {@link InterruptPayload} on `thread.interrupts` mirrors an
|
|
953
|
-
* `input.requested` event: `{ interruptId, payload, namespace }`.
|
|
954
|
-
* Nested interrupts may appear here but not on `stream.interrupts`.
|
|
974
|
+
* Each {@link InterruptPayload} on `thread.interrupts` and each
|
|
975
|
+
* {@link Interrupt} on `stream.interrupts` mirrors an
|
|
976
|
+
* `input.requested` event with its protocol `namespace`.
|
|
955
977
|
*/
|
|
956
978
|
async respond(response, options) {
|
|
957
979
|
if (this.#disposed || this.#thread == null) throw new Error("No active thread to respond to.");
|
|
958
980
|
const resolved = options?.interruptId != null ? {
|
|
959
981
|
interruptId: options.interruptId,
|
|
960
|
-
namespace: options.namespace
|
|
982
|
+
namespace: this.#resolveNamespaceForInterruptId(options.interruptId, options.namespace)
|
|
961
983
|
} : this.#resolveInterruptForResume();
|
|
962
984
|
if (resolved == null) throw new Error("No pending interrupt to respond to.");
|
|
963
985
|
const thread = this.#thread;
|
|
@@ -1027,11 +1049,10 @@ var StreamController = class {
|
|
|
1027
1049
|
const entries = Object.entries(responsesById);
|
|
1028
1050
|
if (entries.length === 0) throw new Error("respondAll() requires at least one response.");
|
|
1029
1051
|
const thread = this.#thread;
|
|
1030
|
-
const pending = thread.interrupts;
|
|
1031
1052
|
const responses = entries.map(([interruptId, response]) => ({
|
|
1032
1053
|
interrupt_id: interruptId,
|
|
1033
1054
|
response: normalizeHitlResponseForServer(response),
|
|
1034
|
-
namespace:
|
|
1055
|
+
namespace: this.#resolveNamespaceForInterruptId(interruptId)
|
|
1035
1056
|
}));
|
|
1036
1057
|
const prepared = options?.update != null ? this.#beginOptimistic(options.update) : void 0;
|
|
1037
1058
|
const dispatchUpdate = this.#resolveDispatchUpdate(options?.update, prepared);
|
|
@@ -1198,16 +1219,11 @@ var StreamController = class {
|
|
|
1198
1219
|
* Without this, the controller would be wedged in a state where:
|
|
1199
1220
|
* - `#thread` is wired but no content pump is open
|
|
1200
1221
|
* - `#rootPumpDeferred` stays `true`
|
|
1201
|
-
* - `selfCreatedThreadIds` still holds the id
|
|
1202
1222
|
*
|
|
1203
|
-
* A retry submit
|
|
1204
|
-
* `
|
|
1205
|
-
*
|
|
1206
|
-
* `#thread
|
|
1207
|
-
* would have an id committed to the URL but no live subscription.
|
|
1208
|
-
*
|
|
1209
|
-
* Tearing down `#thread` so the next submit re-runs `#ensureThread`
|
|
1210
|
-
* from scratch is the simplest recovery — the failed dispatch
|
|
1223
|
+
* A retry submit must re-run `#ensureThread` from scratch (with
|
|
1224
|
+
* `deferRootPump` still true — the id stays in
|
|
1225
|
+
* `#selfCreatedThreadIds` until a successful dispatch). Tearing
|
|
1226
|
+
* down `#thread` is the simplest recovery; the failed dispatch
|
|
1211
1227
|
* means there was nothing to subscribe to anyway.
|
|
1212
1228
|
*/
|
|
1213
1229
|
#abandonDeferredRootPump() {
|
|
@@ -1410,16 +1426,14 @@ var StreamController = class {
|
|
|
1410
1426
|
this.#subgraphs.push(event);
|
|
1411
1427
|
this.#lifecycleLoading.handle(event);
|
|
1412
1428
|
/**
|
|
1413
|
-
*
|
|
1414
|
-
* subgraph) are not observable via the narrow content pump.
|
|
1415
|
-
* `
|
|
1416
|
-
*
|
|
1417
|
-
*
|
|
1418
|
-
*
|
|
1419
|
-
* UI state does not depend on the narrower content pump being the
|
|
1420
|
-
* first consumer to see the event.
|
|
1429
|
+
* `input.requested` events (including HITL inside a subagent /
|
|
1430
|
+
* subgraph) are not always observable via the narrow content pump.
|
|
1431
|
+
* Mirror every namespace into `rootStore.interrupts` here so UI
|
|
1432
|
+
* state does not depend on the content pump being the first
|
|
1433
|
+
* consumer. `ThreadStream.interrupts` is updated in parallel for
|
|
1434
|
+
* resume targeting.
|
|
1421
1435
|
*/
|
|
1422
|
-
this.#
|
|
1436
|
+
this.#recordInterrupt(event);
|
|
1423
1437
|
}
|
|
1424
1438
|
/**
|
|
1425
1439
|
* Process one root-pump event and update all root projections.
|
|
@@ -1512,7 +1526,7 @@ var StreamController = class {
|
|
|
1512
1526
|
return;
|
|
1513
1527
|
}
|
|
1514
1528
|
if (event.method === "input.requested") {
|
|
1515
|
-
this.#
|
|
1529
|
+
this.#recordInterrupt(event);
|
|
1516
1530
|
return;
|
|
1517
1531
|
}
|
|
1518
1532
|
if (event.method === "lifecycle") event.params.data;
|
|
@@ -1640,21 +1654,22 @@ var StreamController = class {
|
|
|
1640
1654
|
}
|
|
1641
1655
|
}
|
|
1642
1656
|
/**
|
|
1643
|
-
* Mirror
|
|
1657
|
+
* Mirror protocol interrupts (any namespace) into the root snapshot.
|
|
1644
1658
|
*
|
|
1645
1659
|
* This can be called from both the wildcard lifecycle/input watcher and the
|
|
1646
1660
|
* root content pump. Store-level dedup keeps the user-facing list stable.
|
|
1647
1661
|
*/
|
|
1648
|
-
#
|
|
1662
|
+
#recordInterrupt(event) {
|
|
1649
1663
|
if (event.method !== "input.requested") return;
|
|
1650
|
-
if (!isRootNamespace(event.params.namespace)) return;
|
|
1651
1664
|
const data = event.params.data;
|
|
1652
1665
|
const interruptId = data?.interrupt_id;
|
|
1653
1666
|
if (typeof interruptId !== "string" || this.#resolvedInterrupts.has(interruptId)) return;
|
|
1654
1667
|
if (this.#hydratedActiveInterruptIds != null && !this.#hydratedActiveInterruptIds.has(interruptId)) return;
|
|
1668
|
+
const namespace = Array.isArray(event.params.namespace) ? [...event.params.namespace] : [...ROOT_NAMESPACE];
|
|
1655
1669
|
const interrupt = normalizeInterruptForClient({
|
|
1656
1670
|
id: interruptId,
|
|
1657
|
-
value: data.payload
|
|
1671
|
+
value: data.payload,
|
|
1672
|
+
namespace
|
|
1658
1673
|
});
|
|
1659
1674
|
this.rootStore.setState((s) => {
|
|
1660
1675
|
if (s.interrupts.some((entry) => entry.id === interruptId)) return s;
|
|
@@ -1667,6 +1682,21 @@ var StreamController = class {
|
|
|
1667
1682
|
});
|
|
1668
1683
|
}
|
|
1669
1684
|
/**
|
|
1685
|
+
* Resolve the protocol namespace for a targeted resume.
|
|
1686
|
+
*
|
|
1687
|
+
* Prefer an explicit caller-supplied namespace, then the matching
|
|
1688
|
+
* {@link ThreadStream.interrupts} entry, then the UI mirror on
|
|
1689
|
+
* `rootStore.interrupts`, finally root (`[]`).
|
|
1690
|
+
*/
|
|
1691
|
+
#resolveNamespaceForInterruptId(interruptId, explicitNamespace) {
|
|
1692
|
+
if (explicitNamespace != null) return [...explicitNamespace];
|
|
1693
|
+
const fromThread = this.#thread?.interrupts.find((entry) => entry.interruptId === interruptId)?.namespace;
|
|
1694
|
+
if (fromThread != null) return [...fromThread];
|
|
1695
|
+
const fromRoot = this.rootStore.getSnapshot().interrupts.find((entry) => entry.id === interruptId)?.namespace;
|
|
1696
|
+
if (fromRoot != null) return [...fromRoot];
|
|
1697
|
+
return [...ROOT_NAMESPACE];
|
|
1698
|
+
}
|
|
1699
|
+
/**
|
|
1670
1700
|
* Mark an interrupt resolved for replay filtering and mirror the
|
|
1671
1701
|
* removal into the root snapshot the framework hooks read.
|
|
1672
1702
|
*/
|