@langchain/langgraph-sdk 1.12.4 → 1.12.5
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/handles/subagents.cjs +9 -2
- package/dist/client/stream/handles/subagents.cjs.map +1 -1
- package/dist/client/stream/handles/subagents.d.cts.map +1 -1
- package/dist/client/stream/handles/subagents.d.ts.map +1 -1
- package/dist/client/stream/handles/subagents.js +10 -3
- package/dist/client/stream/handles/subagents.js.map +1 -1
- package/dist/client/stream/handles/subgraphs.cjs +24 -2
- package/dist/client/stream/handles/subgraphs.cjs.map +1 -1
- package/dist/client/stream/handles/subgraphs.d.cts.map +1 -1
- package/dist/client/stream/handles/subgraphs.d.ts.map +1 -1
- package/dist/client/stream/handles/subgraphs.js +24 -3
- package/dist/client/stream/handles/subgraphs.js.map +1 -1
- package/dist/client/stream/index.cjs +17 -7
- package/dist/client/stream/index.cjs.map +1 -1
- package/dist/client/stream/index.d.cts.map +1 -1
- package/dist/client/stream/index.d.ts.map +1 -1
- package/dist/client/stream/index.js +17 -7
- package/dist/client/stream/index.js.map +1 -1
- package/dist/client/stream/messages.cjs +43 -0
- package/dist/client/stream/messages.cjs.map +1 -1
- package/dist/client/stream/messages.d.cts +8 -0
- package/dist/client/stream/messages.d.cts.map +1 -1
- package/dist/client/stream/messages.d.ts +8 -0
- package/dist/client/stream/messages.d.ts.map +1 -1
- package/dist/client/stream/messages.js +43 -0
- package/dist/client/stream/messages.js.map +1 -1
- package/dist/client/stream/transport/http.cjs +1 -12
- package/dist/client/stream/transport/http.cjs.map +1 -1
- package/dist/client/stream/transport/http.d.cts.map +1 -1
- package/dist/client/stream/transport/http.d.ts.map +1 -1
- package/dist/client/stream/transport/http.js +1 -12
- package/dist/client/stream/transport/http.js.map +1 -1
- package/dist/client/stream/transport/utils.cjs +12 -0
- package/dist/client/stream/transport/utils.cjs.map +1 -1
- package/dist/client/stream/transport/utils.js +12 -1
- package/dist/client/stream/transport/utils.js.map +1 -1
- package/dist/client/stream/transport/websocket.cjs +33 -3
- package/dist/client/stream/transport/websocket.cjs.map +1 -1
- package/dist/client/stream/transport/websocket.d.cts +4 -1
- package/dist/client/stream/transport/websocket.d.cts.map +1 -1
- package/dist/client/stream/transport/websocket.d.ts +4 -1
- package/dist/client/stream/transport/websocket.d.ts.map +1 -1
- package/dist/client/stream/transport/websocket.js +34 -4
- package/dist/client/stream/transport/websocket.js.map +1 -1
- package/dist/stream/controller.cjs +94 -3
- package/dist/stream/controller.cjs.map +1 -1
- package/dist/stream/controller.d.cts.map +1 -1
- package/dist/stream/controller.d.ts.map +1 -1
- package/dist/stream/controller.js +94 -3
- package/dist/stream/controller.js.map +1 -1
- package/dist/stream/lifecycle-loading-tracker.cjs +5 -0
- package/dist/stream/lifecycle-loading-tracker.cjs.map +1 -1
- package/dist/stream/lifecycle-loading-tracker.js +5 -0
- package/dist/stream/lifecycle-loading-tracker.js.map +1 -1
- package/dist/stream/projections/messages.cjs +45 -12
- package/dist/stream/projections/messages.cjs.map +1 -1
- package/dist/stream/projections/messages.js +45 -12
- package/dist/stream/projections/messages.js.map +1 -1
- package/dist/stream/root-message-projection.cjs +71 -8
- package/dist/stream/root-message-projection.cjs.map +1 -1
- package/dist/stream/root-message-projection.js +71 -8
- package/dist/stream/root-message-projection.js.map +1 -1
- package/dist/stream/submit-coordinator.cjs +7 -2
- package/dist/stream/submit-coordinator.cjs.map +1 -1
- package/dist/stream/submit-coordinator.js +7 -2
- package/dist/stream/submit-coordinator.js.map +1 -1
- package/package.json +1 -1
|
@@ -28,17 +28,21 @@ var LifecycleLoadingTracker = class {
|
|
|
28
28
|
#lastRunningLifecycleSeq = -1;
|
|
29
29
|
/** Notified once `isLoading` actually settles to `false` on a terminal event. */
|
|
30
30
|
#onSettled;
|
|
31
|
+
#ignore;
|
|
31
32
|
/**
|
|
32
33
|
* @param params.store - Store whose `isLoading` slot we drive.
|
|
33
34
|
* @param params.isDisposed - Disposal probe consulted from deferred callbacks.
|
|
34
35
|
* @param params.onSettled - Called once `isLoading` flips to `false`,
|
|
35
36
|
* including for a run this client only observed and never
|
|
36
37
|
* dispatched itself.
|
|
38
|
+
* @param params.ignore - Lifecycle events to skip, e.g. another run's
|
|
39
|
+
* replayed terminal while this client's own run is in flight.
|
|
37
40
|
*/
|
|
38
41
|
constructor(params) {
|
|
39
42
|
this.#store = params.store;
|
|
40
43
|
this.#isDisposed = params.isDisposed;
|
|
41
44
|
this.#onSettled = params.onSettled;
|
|
45
|
+
this.#ignore = params.ignore;
|
|
42
46
|
}
|
|
43
47
|
/**
|
|
44
48
|
* Bound listener suitable for `EventBus.subscribe`. Re-exposed as a
|
|
@@ -69,6 +73,7 @@ var LifecycleLoadingTracker = class {
|
|
|
69
73
|
handle(event) {
|
|
70
74
|
if (event.method !== "lifecycle") return;
|
|
71
75
|
if (!require_namespace.isRootNamespace(event.params.namespace)) return;
|
|
76
|
+
if (this.#ignore?.(event)) return;
|
|
72
77
|
const lifecycle = event.params.data;
|
|
73
78
|
const seq = typeof event.seq === "number" ? event.seq : void 0;
|
|
74
79
|
if (lifecycle?.event === "running") {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lifecycle-loading-tracker.cjs","names":["#store","#isDisposed","#onSettled","#lastTerminalLifecycleSeq","#lastRunningLifecycleSeq","isRootNamespace"],"sources":["../../src/stream/lifecycle-loading-tracker.ts"],"sourcesContent":["/**\n * Drives the {@link RootSnapshot.isLoading} flag from root lifecycle\n * events.\n *\n * # What it does\n *\n * The tracker watches a stream of protocol events and flips the\n * `isLoading` slot of a {@link StreamStore} based on root-namespace\n * `lifecycle` payloads:\n *\n * - `running` → `isLoading = true`\n * - `completed` / `failed` / `interrupted`\n * → `isLoading = false`\n *\n * Non-root, non-lifecycle, and unknown events are ignored.\n *\n * # Why it lives in its own class\n *\n * Lifecycle handling has two subtleties that we want to keep out of\n * the {@link StreamController}'s critical path:\n *\n * 1. **Stale `running` filtering.** SSE replays older events on\n * reconnect — including a `running` lifecycle that fired before\n * the run terminated. Without filtering, that replay would flip\n * `isLoading` back to `true` after a `completed` already brought\n * it down. We track the highest terminal `seq` we've seen and\n * drop any `running` whose `seq` is at or below it.\n * 2. **Deferred terminal flip.** The flip from `true → false` is\n * pushed to the next macrotask (`setTimeout(..., 0)`). This\n * gives synchronous consumers — most notably `for await`\n * iterators in framework bindings — one event-loop tick to\n * observe terminal-related state (e.g. the final assistant\n * message landing in `values`) before `isLoading` settles.\n * The deferred callback re-checks whether a newer `running`\n * arrived in the meantime (HITL interrupt → resume, or SSE\n * history replay of interrupted → running) and bails if so,\n * so a stale terminal reset cannot stomp an active run.\n *\n * # Why it's safe to register the listener as `controller.onEvent`\n *\n * The tracker subscribes to the controller's root event bus via the\n * exported {@link listener} arrow. Because the listener is bound at\n * construction time, removing it later (`bus.delete(tracker.listener)`)\n * works without `bind()` gymnastics in the controller.\n *\n * @typeParam T - The snapshot shape; must contain an `isLoading` flag.\n */\nimport type { Event, LifecycleEvent } from \"@langchain/protocol\";\nimport { StreamStore } from \"./store.js\";\nimport { isRootNamespace } from \"./namespace.js\";\n\n/**\n * Minimal contract the snapshot must satisfy. The tracker only\n * touches `isLoading`, leaving everything else for the controller.\n */\ntype LoadingSnapshot = { readonly isLoading: boolean };\n\n/**\n * Drives root-snapshot `isLoading` from root lifecycle events.\n */\nexport class LifecycleLoadingTracker<T extends LoadingSnapshot> {\n /** Snapshot store whose `isLoading` slot we manage. */\n readonly #store: StreamStore<T>;\n\n /**\n * Disposal probe. Consulted from the deferred `setTimeout` so a\n * controller torn down between scheduling and firing doesn't end\n * up writing to a defunct store.\n */\n readonly #isDisposed: () => boolean;\n\n /**\n * Highest sequence number of a terminal lifecycle we've observed.\n * `running` events at or below this seq are stale replays and\n * are dropped to avoid flipping the loading flag back on after the\n * run has already ended.\n */\n #lastTerminalLifecycleSeq = -1;\n\n /**\n * Highest sequence number of a `running` lifecycle we've observed.\n * Deferred terminal resets compare against this so an\n * `interrupted`/`completed`/`failed` whose macrotask fires after a\n * newer `running` does not incorrectly clear `isLoading`.\n */\n #lastRunningLifecycleSeq = -1;\n\n /** Notified once `isLoading` actually settles to `false` on a terminal event. */\n readonly #onSettled: (() => void) | undefined;\n\n /**\n * @param params.store - Store whose `isLoading` slot we drive.\n * @param params.isDisposed - Disposal probe consulted from deferred callbacks.\n * @param params.onSettled - Called once `isLoading` flips to `false`,\n * including for a run this client only observed and never\n * dispatched itself.\n */\n constructor(params: {\n store: StreamStore<T>;\n isDisposed: () => boolean;\n onSettled?: () => void;\n }) {\n this.#store = params.store;\n this.#isDisposed = params.isDisposed;\n this.#onSettled = params.onSettled;\n }\n\n /**\n * Bound listener suitable for `EventBus.subscribe`. Re-exposed as a\n * stable property so the controller can later remove the same\n * function reference from the bus on teardown.\n */\n readonly listener = (event: Event): void => {\n this.handle(event);\n };\n\n /**\n * Reset internal state when rebinding to a new thread.\n *\n * The seq guards are per-thread: a new thread's lifecycle events\n * are not stale relative to the old thread's.\n */\n reset(): void {\n this.#lastTerminalLifecycleSeq = -1;\n this.#lastRunningLifecycleSeq = -1;\n }\n\n /**\n * Process a single protocol event.\n *\n * Filters down to root-namespace lifecycle events, then mutates the\n * store's `isLoading` slot. All other events are ignored.\n *\n * @param event - Any protocol event from the controller's root bus.\n */\n handle(event: Event): void {\n if (event.method !== \"lifecycle\") return;\n if (!isRootNamespace(event.params.namespace)) return;\n const lifecycle = (event as LifecycleEvent).params.data as {\n event?: string;\n };\n const seq = typeof event.seq === \"number\" ? event.seq : undefined;\n if (lifecycle?.event === \"running\") {\n // Drop stale `running` replays that arrive *after* a terminal\n // for the same run. SSE re-streams history on reconnect; without\n // this filter the loading flag would oscillate.\n if (seq != null && seq <= this.#lastTerminalLifecycleSeq) {\n return;\n }\n if (seq != null) {\n this.#lastRunningLifecycleSeq = Math.max(\n this.#lastRunningLifecycleSeq,\n seq\n );\n }\n this.#store.setState((s) =>\n s.isLoading ? s : { ...s, isLoading: true }\n );\n return;\n }\n if (\n lifecycle?.event === \"completed\" ||\n lifecycle?.event === \"failed\" ||\n lifecycle?.event === \"interrupted\"\n ) {\n if (seq != null) {\n this.#lastTerminalLifecycleSeq = Math.max(\n this.#lastTerminalLifecycleSeq,\n seq\n );\n }\n // Flip `isLoading=false` on the next macrotask so synchronous\n // consumers iterating events get one tick to observe terminal\n // state (the final values snapshot etc.) before the loading\n // indicator drops. Bail if a newer `running` arrived since this\n // terminal was scheduled (answered HITL interrupt, back-to-back\n // runs, or history replay of interrupted → running).\n setTimeout(() => {\n if (this.#isDisposed()) return;\n if (seq != null && this.#lastRunningLifecycleSeq > seq) return;\n const wasLoading = this.#store.getSnapshot().isLoading;\n this.#store.setState((s) =>\n s.isLoading ? { ...s, isLoading: false } : s\n );\n if (wasLoading) this.#onSettled?.();\n }, 0);\n }\n }\n}\n"],"mappings":";;;;;AA4DA,IAAa,0BAAb,MAAgE;;CAE9D;;;;;;CAOA;;;;;;;CAQA,4BAA4B;;;;;;;CAQ5B,2BAA2B;;CAG3B
|
|
1
|
+
{"version":3,"file":"lifecycle-loading-tracker.cjs","names":["#store","#isDisposed","#onSettled","#ignore","#lastTerminalLifecycleSeq","#lastRunningLifecycleSeq","isRootNamespace"],"sources":["../../src/stream/lifecycle-loading-tracker.ts"],"sourcesContent":["/**\n * Drives the {@link RootSnapshot.isLoading} flag from root lifecycle\n * events.\n *\n * # What it does\n *\n * The tracker watches a stream of protocol events and flips the\n * `isLoading` slot of a {@link StreamStore} based on root-namespace\n * `lifecycle` payloads:\n *\n * - `running` → `isLoading = true`\n * - `completed` / `failed` / `interrupted`\n * → `isLoading = false`\n *\n * Non-root, non-lifecycle, and unknown events are ignored.\n *\n * # Why it lives in its own class\n *\n * Lifecycle handling has two subtleties that we want to keep out of\n * the {@link StreamController}'s critical path:\n *\n * 1. **Stale `running` filtering.** SSE replays older events on\n * reconnect — including a `running` lifecycle that fired before\n * the run terminated. Without filtering, that replay would flip\n * `isLoading` back to `true` after a `completed` already brought\n * it down. We track the highest terminal `seq` we've seen and\n * drop any `running` whose `seq` is at or below it.\n * 2. **Deferred terminal flip.** The flip from `true → false` is\n * pushed to the next macrotask (`setTimeout(..., 0)`). This\n * gives synchronous consumers — most notably `for await`\n * iterators in framework bindings — one event-loop tick to\n * observe terminal-related state (e.g. the final assistant\n * message landing in `values`) before `isLoading` settles.\n * The deferred callback re-checks whether a newer `running`\n * arrived in the meantime (HITL interrupt → resume, or SSE\n * history replay of interrupted → running) and bails if so,\n * so a stale terminal reset cannot stomp an active run.\n *\n * # Why it's safe to register the listener as `controller.onEvent`\n *\n * The tracker subscribes to the controller's root event bus via the\n * exported {@link listener} arrow. Because the listener is bound at\n * construction time, removing it later (`bus.delete(tracker.listener)`)\n * works without `bind()` gymnastics in the controller.\n *\n * @typeParam T - The snapshot shape; must contain an `isLoading` flag.\n */\nimport type { Event, LifecycleEvent } from \"@langchain/protocol\";\nimport { StreamStore } from \"./store.js\";\nimport { isRootNamespace } from \"./namespace.js\";\n\n/**\n * Minimal contract the snapshot must satisfy. The tracker only\n * touches `isLoading`, leaving everything else for the controller.\n */\ntype LoadingSnapshot = { readonly isLoading: boolean };\n\n/**\n * Drives root-snapshot `isLoading` from root lifecycle events.\n */\nexport class LifecycleLoadingTracker<T extends LoadingSnapshot> {\n /** Snapshot store whose `isLoading` slot we manage. */\n readonly #store: StreamStore<T>;\n\n /**\n * Disposal probe. Consulted from the deferred `setTimeout` so a\n * controller torn down between scheduling and firing doesn't end\n * up writing to a defunct store.\n */\n readonly #isDisposed: () => boolean;\n\n /**\n * Highest sequence number of a terminal lifecycle we've observed.\n * `running` events at or below this seq are stale replays and\n * are dropped to avoid flipping the loading flag back on after the\n * run has already ended.\n */\n #lastTerminalLifecycleSeq = -1;\n\n /**\n * Highest sequence number of a `running` lifecycle we've observed.\n * Deferred terminal resets compare against this so an\n * `interrupted`/`completed`/`failed` whose macrotask fires after a\n * newer `running` does not incorrectly clear `isLoading`.\n */\n #lastRunningLifecycleSeq = -1;\n\n /** Notified once `isLoading` actually settles to `false` on a terminal event. */\n readonly #onSettled: (() => void) | undefined;\n\n readonly #ignore: ((event: Event) => boolean) | undefined;\n\n /**\n * @param params.store - Store whose `isLoading` slot we drive.\n * @param params.isDisposed - Disposal probe consulted from deferred callbacks.\n * @param params.onSettled - Called once `isLoading` flips to `false`,\n * including for a run this client only observed and never\n * dispatched itself.\n * @param params.ignore - Lifecycle events to skip, e.g. another run's\n * replayed terminal while this client's own run is in flight.\n */\n constructor(params: {\n store: StreamStore<T>;\n isDisposed: () => boolean;\n onSettled?: () => void;\n ignore?: (event: Event) => boolean;\n }) {\n this.#store = params.store;\n this.#isDisposed = params.isDisposed;\n this.#onSettled = params.onSettled;\n this.#ignore = params.ignore;\n }\n\n /**\n * Bound listener suitable for `EventBus.subscribe`. Re-exposed as a\n * stable property so the controller can later remove the same\n * function reference from the bus on teardown.\n */\n readonly listener = (event: Event): void => {\n this.handle(event);\n };\n\n /**\n * Reset internal state when rebinding to a new thread.\n *\n * The seq guards are per-thread: a new thread's lifecycle events\n * are not stale relative to the old thread's.\n */\n reset(): void {\n this.#lastTerminalLifecycleSeq = -1;\n this.#lastRunningLifecycleSeq = -1;\n }\n\n /**\n * Process a single protocol event.\n *\n * Filters down to root-namespace lifecycle events, then mutates the\n * store's `isLoading` slot. All other events are ignored.\n *\n * @param event - Any protocol event from the controller's root bus.\n */\n handle(event: Event): void {\n if (event.method !== \"lifecycle\") return;\n if (!isRootNamespace(event.params.namespace)) return;\n if (this.#ignore?.(event)) return;\n const lifecycle = (event as LifecycleEvent).params.data as {\n event?: string;\n };\n const seq = typeof event.seq === \"number\" ? event.seq : undefined;\n if (lifecycle?.event === \"running\") {\n // Drop stale `running` replays that arrive *after* a terminal\n // for the same run. SSE re-streams history on reconnect; without\n // this filter the loading flag would oscillate.\n if (seq != null && seq <= this.#lastTerminalLifecycleSeq) {\n return;\n }\n if (seq != null) {\n this.#lastRunningLifecycleSeq = Math.max(\n this.#lastRunningLifecycleSeq,\n seq\n );\n }\n this.#store.setState((s) =>\n s.isLoading ? s : { ...s, isLoading: true }\n );\n return;\n }\n if (\n lifecycle?.event === \"completed\" ||\n lifecycle?.event === \"failed\" ||\n lifecycle?.event === \"interrupted\"\n ) {\n if (seq != null) {\n this.#lastTerminalLifecycleSeq = Math.max(\n this.#lastTerminalLifecycleSeq,\n seq\n );\n }\n // Flip `isLoading=false` on the next macrotask so synchronous\n // consumers iterating events get one tick to observe terminal\n // state (the final values snapshot etc.) before the loading\n // indicator drops. Bail if a newer `running` arrived since this\n // terminal was scheduled (answered HITL interrupt, back-to-back\n // runs, or history replay of interrupted → running).\n setTimeout(() => {\n if (this.#isDisposed()) return;\n if (seq != null && this.#lastRunningLifecycleSeq > seq) return;\n const wasLoading = this.#store.getSnapshot().isLoading;\n this.#store.setState((s) =>\n s.isLoading ? { ...s, isLoading: false } : s\n );\n if (wasLoading) this.#onSettled?.();\n }, 0);\n }\n }\n}\n"],"mappings":";;;;;AA4DA,IAAa,0BAAb,MAAgE;;CAE9D;;;;;;CAOA;;;;;;;CAQA,4BAA4B;;;;;;;CAQ5B,2BAA2B;;CAG3B;CAEA;;;;;;;;;;CAWA,YAAY,QAKT;AACD,QAAA,QAAc,OAAO;AACrB,QAAA,aAAmB,OAAO;AAC1B,QAAA,YAAkB,OAAO;AACzB,QAAA,SAAe,OAAO;;;;;;;CAQxB,YAAqB,UAAuB;AAC1C,OAAK,OAAO,MAAM;;;;;;;;CASpB,QAAc;AACZ,QAAA,2BAAiC;AACjC,QAAA,0BAAgC;;;;;;;;;;CAWlC,OAAO,OAAoB;AACzB,MAAI,MAAM,WAAW,YAAa;AAClC,MAAI,CAACM,kBAAAA,gBAAgB,MAAM,OAAO,UAAU,CAAE;AAC9C,MAAI,MAAA,SAAe,MAAM,CAAE;EAC3B,MAAM,YAAa,MAAyB,OAAO;EAGnD,MAAM,MAAM,OAAO,MAAM,QAAQ,WAAW,MAAM,MAAM,KAAA;AACxD,MAAI,WAAW,UAAU,WAAW;AAIlC,OAAI,OAAO,QAAQ,OAAO,MAAA,yBACxB;AAEF,OAAI,OAAO,KACT,OAAA,0BAAgC,KAAK,IACnC,MAAA,yBACA,IACD;AAEH,SAAA,MAAY,UAAU,MACpB,EAAE,YAAY,IAAI;IAAE,GAAG;IAAG,WAAW;IAAM,CAC5C;AACD;;AAEF,MACE,WAAW,UAAU,eACrB,WAAW,UAAU,YACrB,WAAW,UAAU,eACrB;AACA,OAAI,OAAO,KACT,OAAA,2BAAiC,KAAK,IACpC,MAAA,0BACA,IACD;AAQH,oBAAiB;AACf,QAAI,MAAA,YAAkB,CAAE;AACxB,QAAI,OAAO,QAAQ,MAAA,0BAAgC,IAAK;IACxD,MAAM,aAAa,MAAA,MAAY,aAAa,CAAC;AAC7C,UAAA,MAAY,UAAU,MACpB,EAAE,YAAY;KAAE,GAAG;KAAG,WAAW;KAAO,GAAG,EAC5C;AACD,QAAI,WAAY,OAAA,aAAmB;MAClC,EAAE"}
|
|
@@ -28,17 +28,21 @@ var LifecycleLoadingTracker = class {
|
|
|
28
28
|
#lastRunningLifecycleSeq = -1;
|
|
29
29
|
/** Notified once `isLoading` actually settles to `false` on a terminal event. */
|
|
30
30
|
#onSettled;
|
|
31
|
+
#ignore;
|
|
31
32
|
/**
|
|
32
33
|
* @param params.store - Store whose `isLoading` slot we drive.
|
|
33
34
|
* @param params.isDisposed - Disposal probe consulted from deferred callbacks.
|
|
34
35
|
* @param params.onSettled - Called once `isLoading` flips to `false`,
|
|
35
36
|
* including for a run this client only observed and never
|
|
36
37
|
* dispatched itself.
|
|
38
|
+
* @param params.ignore - Lifecycle events to skip, e.g. another run's
|
|
39
|
+
* replayed terminal while this client's own run is in flight.
|
|
37
40
|
*/
|
|
38
41
|
constructor(params) {
|
|
39
42
|
this.#store = params.store;
|
|
40
43
|
this.#isDisposed = params.isDisposed;
|
|
41
44
|
this.#onSettled = params.onSettled;
|
|
45
|
+
this.#ignore = params.ignore;
|
|
42
46
|
}
|
|
43
47
|
/**
|
|
44
48
|
* Bound listener suitable for `EventBus.subscribe`. Re-exposed as a
|
|
@@ -69,6 +73,7 @@ var LifecycleLoadingTracker = class {
|
|
|
69
73
|
handle(event) {
|
|
70
74
|
if (event.method !== "lifecycle") return;
|
|
71
75
|
if (!isRootNamespace(event.params.namespace)) return;
|
|
76
|
+
if (this.#ignore?.(event)) return;
|
|
72
77
|
const lifecycle = event.params.data;
|
|
73
78
|
const seq = typeof event.seq === "number" ? event.seq : void 0;
|
|
74
79
|
if (lifecycle?.event === "running") {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lifecycle-loading-tracker.js","names":["#store","#isDisposed","#onSettled","#lastTerminalLifecycleSeq","#lastRunningLifecycleSeq"],"sources":["../../src/stream/lifecycle-loading-tracker.ts"],"sourcesContent":["/**\n * Drives the {@link RootSnapshot.isLoading} flag from root lifecycle\n * events.\n *\n * # What it does\n *\n * The tracker watches a stream of protocol events and flips the\n * `isLoading` slot of a {@link StreamStore} based on root-namespace\n * `lifecycle` payloads:\n *\n * - `running` → `isLoading = true`\n * - `completed` / `failed` / `interrupted`\n * → `isLoading = false`\n *\n * Non-root, non-lifecycle, and unknown events are ignored.\n *\n * # Why it lives in its own class\n *\n * Lifecycle handling has two subtleties that we want to keep out of\n * the {@link StreamController}'s critical path:\n *\n * 1. **Stale `running` filtering.** SSE replays older events on\n * reconnect — including a `running` lifecycle that fired before\n * the run terminated. Without filtering, that replay would flip\n * `isLoading` back to `true` after a `completed` already brought\n * it down. We track the highest terminal `seq` we've seen and\n * drop any `running` whose `seq` is at or below it.\n * 2. **Deferred terminal flip.** The flip from `true → false` is\n * pushed to the next macrotask (`setTimeout(..., 0)`). This\n * gives synchronous consumers — most notably `for await`\n * iterators in framework bindings — one event-loop tick to\n * observe terminal-related state (e.g. the final assistant\n * message landing in `values`) before `isLoading` settles.\n * The deferred callback re-checks whether a newer `running`\n * arrived in the meantime (HITL interrupt → resume, or SSE\n * history replay of interrupted → running) and bails if so,\n * so a stale terminal reset cannot stomp an active run.\n *\n * # Why it's safe to register the listener as `controller.onEvent`\n *\n * The tracker subscribes to the controller's root event bus via the\n * exported {@link listener} arrow. Because the listener is bound at\n * construction time, removing it later (`bus.delete(tracker.listener)`)\n * works without `bind()` gymnastics in the controller.\n *\n * @typeParam T - The snapshot shape; must contain an `isLoading` flag.\n */\nimport type { Event, LifecycleEvent } from \"@langchain/protocol\";\nimport { StreamStore } from \"./store.js\";\nimport { isRootNamespace } from \"./namespace.js\";\n\n/**\n * Minimal contract the snapshot must satisfy. The tracker only\n * touches `isLoading`, leaving everything else for the controller.\n */\ntype LoadingSnapshot = { readonly isLoading: boolean };\n\n/**\n * Drives root-snapshot `isLoading` from root lifecycle events.\n */\nexport class LifecycleLoadingTracker<T extends LoadingSnapshot> {\n /** Snapshot store whose `isLoading` slot we manage. */\n readonly #store: StreamStore<T>;\n\n /**\n * Disposal probe. Consulted from the deferred `setTimeout` so a\n * controller torn down between scheduling and firing doesn't end\n * up writing to a defunct store.\n */\n readonly #isDisposed: () => boolean;\n\n /**\n * Highest sequence number of a terminal lifecycle we've observed.\n * `running` events at or below this seq are stale replays and\n * are dropped to avoid flipping the loading flag back on after the\n * run has already ended.\n */\n #lastTerminalLifecycleSeq = -1;\n\n /**\n * Highest sequence number of a `running` lifecycle we've observed.\n * Deferred terminal resets compare against this so an\n * `interrupted`/`completed`/`failed` whose macrotask fires after a\n * newer `running` does not incorrectly clear `isLoading`.\n */\n #lastRunningLifecycleSeq = -1;\n\n /** Notified once `isLoading` actually settles to `false` on a terminal event. */\n readonly #onSettled: (() => void) | undefined;\n\n /**\n * @param params.store - Store whose `isLoading` slot we drive.\n * @param params.isDisposed - Disposal probe consulted from deferred callbacks.\n * @param params.onSettled - Called once `isLoading` flips to `false`,\n * including for a run this client only observed and never\n * dispatched itself.\n */\n constructor(params: {\n store: StreamStore<T>;\n isDisposed: () => boolean;\n onSettled?: () => void;\n }) {\n this.#store = params.store;\n this.#isDisposed = params.isDisposed;\n this.#onSettled = params.onSettled;\n }\n\n /**\n * Bound listener suitable for `EventBus.subscribe`. Re-exposed as a\n * stable property so the controller can later remove the same\n * function reference from the bus on teardown.\n */\n readonly listener = (event: Event): void => {\n this.handle(event);\n };\n\n /**\n * Reset internal state when rebinding to a new thread.\n *\n * The seq guards are per-thread: a new thread's lifecycle events\n * are not stale relative to the old thread's.\n */\n reset(): void {\n this.#lastTerminalLifecycleSeq = -1;\n this.#lastRunningLifecycleSeq = -1;\n }\n\n /**\n * Process a single protocol event.\n *\n * Filters down to root-namespace lifecycle events, then mutates the\n * store's `isLoading` slot. All other events are ignored.\n *\n * @param event - Any protocol event from the controller's root bus.\n */\n handle(event: Event): void {\n if (event.method !== \"lifecycle\") return;\n if (!isRootNamespace(event.params.namespace)) return;\n const lifecycle = (event as LifecycleEvent).params.data as {\n event?: string;\n };\n const seq = typeof event.seq === \"number\" ? event.seq : undefined;\n if (lifecycle?.event === \"running\") {\n // Drop stale `running` replays that arrive *after* a terminal\n // for the same run. SSE re-streams history on reconnect; without\n // this filter the loading flag would oscillate.\n if (seq != null && seq <= this.#lastTerminalLifecycleSeq) {\n return;\n }\n if (seq != null) {\n this.#lastRunningLifecycleSeq = Math.max(\n this.#lastRunningLifecycleSeq,\n seq\n );\n }\n this.#store.setState((s) =>\n s.isLoading ? s : { ...s, isLoading: true }\n );\n return;\n }\n if (\n lifecycle?.event === \"completed\" ||\n lifecycle?.event === \"failed\" ||\n lifecycle?.event === \"interrupted\"\n ) {\n if (seq != null) {\n this.#lastTerminalLifecycleSeq = Math.max(\n this.#lastTerminalLifecycleSeq,\n seq\n );\n }\n // Flip `isLoading=false` on the next macrotask so synchronous\n // consumers iterating events get one tick to observe terminal\n // state (the final values snapshot etc.) before the loading\n // indicator drops. Bail if a newer `running` arrived since this\n // terminal was scheduled (answered HITL interrupt, back-to-back\n // runs, or history replay of interrupted → running).\n setTimeout(() => {\n if (this.#isDisposed()) return;\n if (seq != null && this.#lastRunningLifecycleSeq > seq) return;\n const wasLoading = this.#store.getSnapshot().isLoading;\n this.#store.setState((s) =>\n s.isLoading ? { ...s, isLoading: false } : s\n );\n if (wasLoading) this.#onSettled?.();\n }, 0);\n }\n }\n}\n"],"mappings":";;;;;AA4DA,IAAa,0BAAb,MAAgE;;CAE9D;;;;;;CAOA;;;;;;;CAQA,4BAA4B;;;;;;;CAQ5B,2BAA2B;;CAG3B
|
|
1
|
+
{"version":3,"file":"lifecycle-loading-tracker.js","names":["#store","#isDisposed","#onSettled","#ignore","#lastTerminalLifecycleSeq","#lastRunningLifecycleSeq"],"sources":["../../src/stream/lifecycle-loading-tracker.ts"],"sourcesContent":["/**\n * Drives the {@link RootSnapshot.isLoading} flag from root lifecycle\n * events.\n *\n * # What it does\n *\n * The tracker watches a stream of protocol events and flips the\n * `isLoading` slot of a {@link StreamStore} based on root-namespace\n * `lifecycle` payloads:\n *\n * - `running` → `isLoading = true`\n * - `completed` / `failed` / `interrupted`\n * → `isLoading = false`\n *\n * Non-root, non-lifecycle, and unknown events are ignored.\n *\n * # Why it lives in its own class\n *\n * Lifecycle handling has two subtleties that we want to keep out of\n * the {@link StreamController}'s critical path:\n *\n * 1. **Stale `running` filtering.** SSE replays older events on\n * reconnect — including a `running` lifecycle that fired before\n * the run terminated. Without filtering, that replay would flip\n * `isLoading` back to `true` after a `completed` already brought\n * it down. We track the highest terminal `seq` we've seen and\n * drop any `running` whose `seq` is at or below it.\n * 2. **Deferred terminal flip.** The flip from `true → false` is\n * pushed to the next macrotask (`setTimeout(..., 0)`). This\n * gives synchronous consumers — most notably `for await`\n * iterators in framework bindings — one event-loop tick to\n * observe terminal-related state (e.g. the final assistant\n * message landing in `values`) before `isLoading` settles.\n * The deferred callback re-checks whether a newer `running`\n * arrived in the meantime (HITL interrupt → resume, or SSE\n * history replay of interrupted → running) and bails if so,\n * so a stale terminal reset cannot stomp an active run.\n *\n * # Why it's safe to register the listener as `controller.onEvent`\n *\n * The tracker subscribes to the controller's root event bus via the\n * exported {@link listener} arrow. Because the listener is bound at\n * construction time, removing it later (`bus.delete(tracker.listener)`)\n * works without `bind()` gymnastics in the controller.\n *\n * @typeParam T - The snapshot shape; must contain an `isLoading` flag.\n */\nimport type { Event, LifecycleEvent } from \"@langchain/protocol\";\nimport { StreamStore } from \"./store.js\";\nimport { isRootNamespace } from \"./namespace.js\";\n\n/**\n * Minimal contract the snapshot must satisfy. The tracker only\n * touches `isLoading`, leaving everything else for the controller.\n */\ntype LoadingSnapshot = { readonly isLoading: boolean };\n\n/**\n * Drives root-snapshot `isLoading` from root lifecycle events.\n */\nexport class LifecycleLoadingTracker<T extends LoadingSnapshot> {\n /** Snapshot store whose `isLoading` slot we manage. */\n readonly #store: StreamStore<T>;\n\n /**\n * Disposal probe. Consulted from the deferred `setTimeout` so a\n * controller torn down between scheduling and firing doesn't end\n * up writing to a defunct store.\n */\n readonly #isDisposed: () => boolean;\n\n /**\n * Highest sequence number of a terminal lifecycle we've observed.\n * `running` events at or below this seq are stale replays and\n * are dropped to avoid flipping the loading flag back on after the\n * run has already ended.\n */\n #lastTerminalLifecycleSeq = -1;\n\n /**\n * Highest sequence number of a `running` lifecycle we've observed.\n * Deferred terminal resets compare against this so an\n * `interrupted`/`completed`/`failed` whose macrotask fires after a\n * newer `running` does not incorrectly clear `isLoading`.\n */\n #lastRunningLifecycleSeq = -1;\n\n /** Notified once `isLoading` actually settles to `false` on a terminal event. */\n readonly #onSettled: (() => void) | undefined;\n\n readonly #ignore: ((event: Event) => boolean) | undefined;\n\n /**\n * @param params.store - Store whose `isLoading` slot we drive.\n * @param params.isDisposed - Disposal probe consulted from deferred callbacks.\n * @param params.onSettled - Called once `isLoading` flips to `false`,\n * including for a run this client only observed and never\n * dispatched itself.\n * @param params.ignore - Lifecycle events to skip, e.g. another run's\n * replayed terminal while this client's own run is in flight.\n */\n constructor(params: {\n store: StreamStore<T>;\n isDisposed: () => boolean;\n onSettled?: () => void;\n ignore?: (event: Event) => boolean;\n }) {\n this.#store = params.store;\n this.#isDisposed = params.isDisposed;\n this.#onSettled = params.onSettled;\n this.#ignore = params.ignore;\n }\n\n /**\n * Bound listener suitable for `EventBus.subscribe`. Re-exposed as a\n * stable property so the controller can later remove the same\n * function reference from the bus on teardown.\n */\n readonly listener = (event: Event): void => {\n this.handle(event);\n };\n\n /**\n * Reset internal state when rebinding to a new thread.\n *\n * The seq guards are per-thread: a new thread's lifecycle events\n * are not stale relative to the old thread's.\n */\n reset(): void {\n this.#lastTerminalLifecycleSeq = -1;\n this.#lastRunningLifecycleSeq = -1;\n }\n\n /**\n * Process a single protocol event.\n *\n * Filters down to root-namespace lifecycle events, then mutates the\n * store's `isLoading` slot. All other events are ignored.\n *\n * @param event - Any protocol event from the controller's root bus.\n */\n handle(event: Event): void {\n if (event.method !== \"lifecycle\") return;\n if (!isRootNamespace(event.params.namespace)) return;\n if (this.#ignore?.(event)) return;\n const lifecycle = (event as LifecycleEvent).params.data as {\n event?: string;\n };\n const seq = typeof event.seq === \"number\" ? event.seq : undefined;\n if (lifecycle?.event === \"running\") {\n // Drop stale `running` replays that arrive *after* a terminal\n // for the same run. SSE re-streams history on reconnect; without\n // this filter the loading flag would oscillate.\n if (seq != null && seq <= this.#lastTerminalLifecycleSeq) {\n return;\n }\n if (seq != null) {\n this.#lastRunningLifecycleSeq = Math.max(\n this.#lastRunningLifecycleSeq,\n seq\n );\n }\n this.#store.setState((s) =>\n s.isLoading ? s : { ...s, isLoading: true }\n );\n return;\n }\n if (\n lifecycle?.event === \"completed\" ||\n lifecycle?.event === \"failed\" ||\n lifecycle?.event === \"interrupted\"\n ) {\n if (seq != null) {\n this.#lastTerminalLifecycleSeq = Math.max(\n this.#lastTerminalLifecycleSeq,\n seq\n );\n }\n // Flip `isLoading=false` on the next macrotask so synchronous\n // consumers iterating events get one tick to observe terminal\n // state (the final values snapshot etc.) before the loading\n // indicator drops. Bail if a newer `running` arrived since this\n // terminal was scheduled (answered HITL interrupt, back-to-back\n // runs, or history replay of interrupted → running).\n setTimeout(() => {\n if (this.#isDisposed()) return;\n if (seq != null && this.#lastRunningLifecycleSeq > seq) return;\n const wasLoading = this.#store.getSnapshot().isLoading;\n this.#store.setState((s) =>\n s.isLoading ? { ...s, isLoading: false } : s\n );\n if (wasLoading) this.#onSettled?.();\n }, 0);\n }\n }\n}\n"],"mappings":";;;;;AA4DA,IAAa,0BAAb,MAAgE;;CAE9D;;;;;;CAOA;;;;;;;CAQA,4BAA4B;;;;;;;CAQ5B,2BAA2B;;CAG3B;CAEA;;;;;;;;;;CAWA,YAAY,QAKT;AACD,QAAA,QAAc,OAAO;AACrB,QAAA,aAAmB,OAAO;AAC1B,QAAA,YAAkB,OAAO;AACzB,QAAA,SAAe,OAAO;;;;;;;CAQxB,YAAqB,UAAuB;AAC1C,OAAK,OAAO,MAAM;;;;;;;;CASpB,QAAc;AACZ,QAAA,2BAAiC;AACjC,QAAA,0BAAgC;;;;;;;;;;CAWlC,OAAO,OAAoB;AACzB,MAAI,MAAM,WAAW,YAAa;AAClC,MAAI,CAAC,gBAAgB,MAAM,OAAO,UAAU,CAAE;AAC9C,MAAI,MAAA,SAAe,MAAM,CAAE;EAC3B,MAAM,YAAa,MAAyB,OAAO;EAGnD,MAAM,MAAM,OAAO,MAAM,QAAQ,WAAW,MAAM,MAAM,KAAA;AACxD,MAAI,WAAW,UAAU,WAAW;AAIlC,OAAI,OAAO,QAAQ,OAAO,MAAA,yBACxB;AAEF,OAAI,OAAO,KACT,OAAA,0BAAgC,KAAK,IACnC,MAAA,yBACA,IACD;AAEH,SAAA,MAAY,UAAU,MACpB,EAAE,YAAY,IAAI;IAAE,GAAG;IAAG,WAAW;IAAM,CAC5C;AACD;;AAEF,MACE,WAAW,UAAU,eACrB,WAAW,UAAU,YACrB,WAAW,UAAU,eACrB;AACA,OAAI,OAAO,KACT,OAAA,2BAAiC,KAAK,IACpC,MAAA,0BACA,IACD;AAQH,oBAAiB;AACf,QAAI,MAAA,YAAkB,CAAE;AACxB,QAAI,OAAO,QAAQ,MAAA,0BAAgC,IAAK;IACxD,MAAM,aAAa,MAAA,MAAY,aAAa,CAAC;AAC7C,UAAA,MAAY,UAAU,MACpB,EAAE,YAAY;KAAE,GAAG;KAAG,WAAW;KAAO,GAAG,EAC5C;AACD,QAAI,WAAY,OAAA,aAAmB;MAClC,EAAE"}
|
|
@@ -17,16 +17,10 @@ function messagesProjection(namespace) {
|
|
|
17
17
|
const indexById = /* @__PURE__ */ new Map();
|
|
18
18
|
const streamMessageIds = /* @__PURE__ */ new Set();
|
|
19
19
|
let valuesMessageIds = /* @__PURE__ */ new Set();
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
applyEvent(event);
|
|
25
|
-
});
|
|
26
|
-
return { dispose() {
|
|
27
|
-
unsubscribe();
|
|
28
|
-
} };
|
|
29
|
-
}
|
|
20
|
+
const uncommittedIds = /* @__PURE__ */ new Set();
|
|
21
|
+
const finishedIds = /* @__PURE__ */ new Set();
|
|
22
|
+
const droppedIds = /* @__PURE__ */ new Set();
|
|
23
|
+
const rootShortCircuit = require_namespace.isRootNamespace(ns) && rootBus.channels.includes("messages");
|
|
30
24
|
let disposed = false;
|
|
31
25
|
const pendingMessages = [];
|
|
32
26
|
let dirty = false;
|
|
@@ -60,6 +54,10 @@ function messagesProjection(namespace) {
|
|
|
60
54
|
preferValuesMessage: require_message_reconciliation.shouldPreferValuesMessage
|
|
61
55
|
});
|
|
62
56
|
valuesMessageIds = reconciliation.valueMessageIds;
|
|
57
|
+
for (const id of valuesMessageIds) {
|
|
58
|
+
uncommittedIds.delete(id);
|
|
59
|
+
finishedIds.delete(id);
|
|
60
|
+
}
|
|
63
61
|
const reconciledMessages = [...reconciliation.messages];
|
|
64
62
|
pendingMessages.length = 0;
|
|
65
63
|
for (const message of reconciledMessages) pendingMessages.push(message);
|
|
@@ -83,7 +81,9 @@ function messagesProjection(namespace) {
|
|
|
83
81
|
if (update == null) return;
|
|
84
82
|
const msg = update.message;
|
|
85
83
|
const id = msg.id;
|
|
86
|
-
if (id == null) return;
|
|
84
|
+
if (id == null || droppedIds.has(id)) return;
|
|
85
|
+
if (!valuesMessageIds.has(id)) uncommittedIds.add(id);
|
|
86
|
+
if (update.kind === "message-finish") finishedIds.add(id);
|
|
87
87
|
const captured = roleByKey.get(id) ?? { role: "ai" };
|
|
88
88
|
const base = require_assembled_to_message.assembledMessageToBaseMessage(msg, captured.role, { toolCallId: captured.toolCallId });
|
|
89
89
|
streamMessageIds.add(id);
|
|
@@ -94,6 +94,33 @@ function messagesProjection(namespace) {
|
|
|
94
94
|
} else pendingMessages[existingIdx] = base;
|
|
95
95
|
scheduleFlush();
|
|
96
96
|
};
|
|
97
|
+
const closeRun = (unfinishedOnly) => {
|
|
98
|
+
assembler.abandonActive();
|
|
99
|
+
const dropped = /* @__PURE__ */ new Set();
|
|
100
|
+
for (const id of uncommittedIds) {
|
|
101
|
+
if (unfinishedOnly && finishedIds.has(id)) continue;
|
|
102
|
+
dropped.add(id);
|
|
103
|
+
}
|
|
104
|
+
if (dropped.size === 0) return;
|
|
105
|
+
for (const id of dropped) {
|
|
106
|
+
uncommittedIds.delete(id);
|
|
107
|
+
finishedIds.delete(id);
|
|
108
|
+
droppedIds.add(id);
|
|
109
|
+
}
|
|
110
|
+
const kept = pendingMessages.filter((m) => m.id == null || !dropped.has(m.id));
|
|
111
|
+
pendingMessages.length = 0;
|
|
112
|
+
pendingMessages.push(...kept);
|
|
113
|
+
indexById.clear();
|
|
114
|
+
for (const [id, idx] of require_message_reconciliation.buildMessageIndex(pendingMessages)) indexById.set(id, idx);
|
|
115
|
+
scheduleFlush();
|
|
116
|
+
};
|
|
117
|
+
const unsubscribeLifecycle = rootBus.subscribe((event) => {
|
|
118
|
+
if (event.method !== "lifecycle") return;
|
|
119
|
+
if (!require_namespace.isRootNamespace(event.params.namespace)) return;
|
|
120
|
+
const lifecycle = event.params.data?.event;
|
|
121
|
+
if (lifecycle === "running") setTimeout(() => !disposed && closeRun(false), 0);
|
|
122
|
+
else if (lifecycle === "completed" || lifecycle === "interrupted" || lifecycle === "failed") setTimeout(() => !disposed && closeRun(true), 0);
|
|
123
|
+
});
|
|
97
124
|
let runtime;
|
|
98
125
|
const openSubscription = () => {
|
|
99
126
|
runtime = require_runtime.openProjectionSubscription({
|
|
@@ -106,7 +133,12 @@ function messagesProjection(namespace) {
|
|
|
106
133
|
}
|
|
107
134
|
});
|
|
108
135
|
};
|
|
109
|
-
(
|
|
136
|
+
if (rootShortCircuit) runtime = { dispose: rootBus.subscribe((event) => {
|
|
137
|
+
if (!require_namespace.isRootNamespace(event.params.namespace)) return;
|
|
138
|
+
if (event.method === "messages") applyEvent(event);
|
|
139
|
+
else if (event.method === "values") applyValuesEvent(event);
|
|
140
|
+
}) };
|
|
141
|
+
else (async () => {
|
|
110
142
|
if (!(await rootBus.trySeedFromHistory?.({
|
|
111
143
|
kind: "messages",
|
|
112
144
|
namespace: ns,
|
|
@@ -115,6 +147,7 @@ function messagesProjection(namespace) {
|
|
|
115
147
|
})();
|
|
116
148
|
return { async dispose() {
|
|
117
149
|
disposed = true;
|
|
150
|
+
unsubscribeLifecycle();
|
|
118
151
|
if (flushChannel != null) {
|
|
119
152
|
flushChannel.port1.onmessage = null;
|
|
120
153
|
flushChannel.port1.close();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"messages.cjs","names":["namespaceKey","MessageAssembler","isRootNamespace","reconcileMessagesFromValues","ensureMessageInstances","shouldPreferValuesMessage","buildMessageIndex","assembledMessageToBaseMessage","openProjectionSubscription"],"sources":["../../../src/stream/projections/messages.ts"],"sourcesContent":["/**\n * Namespace-scoped `messages` projection.\n *\n * Opens `thread.subscribe({ channels: [\"messages\"], namespaces: [ns] })`\n * and folds each `messages` event through {@link MessageAssembler}.\n * Every update — start, block delta, block finish, message finish —\n * re-derives a `BaseMessage` class instance for the currently-active\n * message and updates its slot in the store.\n *\n * The projection emits `BaseMessage[]` (class instances from\n * `@langchain/core/messages`), never plain serialized objects.\n */\nimport type {\n MessagesEvent,\n MessageRole,\n MessageStartData,\n ValuesEvent,\n} from \"@langchain/protocol\";\nimport type { BaseMessage } from \"@langchain/core/messages\";\nimport { MessageAssembler } from \"../../client/stream/messages.js\";\nimport {\n assembledMessageToBaseMessage,\n type ExtendedMessageRole,\n} from \"../assembled-to-message.js\";\nimport type { Message } from \"../../types.messages.js\";\nimport type { ProjectionSpec, ProjectionRuntime } from \"../types.js\";\nimport { ensureMessageInstances } from \"../message-coercion.js\";\nimport { isRootNamespace, namespaceKey } from \"../namespace.js\";\nimport {\n buildMessageIndex,\n reconcileMessagesFromValues,\n shouldPreferValuesMessage,\n} from \"../message-reconciliation.js\";\nimport { openProjectionSubscription } from \"./runtime.js\";\n\nexport function messagesProjection(\n namespace: readonly string[]\n): ProjectionSpec<BaseMessage[]> {\n const ns = [...namespace];\n const key = `messages|${namespaceKey(ns)}`;\n\n return {\n key,\n namespace: ns,\n initial: [],\n open({ thread, store, rootBus }): ProjectionRuntime {\n const assembler = new MessageAssembler();\n // Per-messageId state needed for BaseMessage projection:\n // - `role` is only in the `message-start` event; we cache it\n // so subsequent delta events still produce a typed message.\n // - `toolCallId` is pulled from message-start extras when role\n // is `tool` (a convention we keep compatible with serialized\n // v1 tool messages).\n const roleByKey = new Map<\n string,\n { role: ExtendedMessageRole; toolCallId?: string }\n >();\n const indexById = new Map<string, number>();\n // Ids this projection has observed via the `messages` channel\n // (token-level deltas). Used by `applyValuesEvent` to prefer the\n // stream-assembled version over the values-coerced shape while a\n // turn is streaming, matching the root controller's policy.\n const streamMessageIds = new Set<string>();\n // Ids observed in the most recent `values.messages` snapshot.\n // Messages that were present in a prior snapshot but are absent\n // from this one are treated as explicit removals (server-side\n // `RemoveMessage` reducer deltas). Stream-only messages (seen on\n // the messages channel but never echoed in a values snapshot)\n // are preserved — their enclosing superstep may simply not have\n // committed yet.\n let valuesMessageIds = new Set<string>();\n\n // Root-scoped projections whose channels are already covered by\n // the controller's root pump attach to the shared fan-out\n // instead of opening a second server subscription. The root\n // pump runs at `{namespaces: [[]], depth: 1}`, which is exactly\n // the scope a root-namespace `messagesProjection` wants.\n const rootShortCircuit =\n isRootNamespace(ns) && rootBus.channels.includes(\"messages\");\n\n if (rootShortCircuit) {\n const unsubscribe = rootBus.subscribe((event) => {\n if (event.method !== \"messages\") return;\n if (!isRootNamespace(event.params.namespace)) return;\n applyEvent(event as MessagesEvent);\n });\n return {\n dispose() {\n unsubscribe();\n },\n };\n }\n\n let disposed = false;\n\n // Local mirror of the store contents. Every `applyEvent` /\n // `applyValuesEvent` mutates this synchronously; a coalesced\n // `scheduleFlush` copies it to `store` once per macrotask.\n //\n // Why the indirection? When a namespace-scoped projection\n // (e.g. a subagent modal opened after the run finished) first\n // subscribes, the server replays the entire history from\n // `seq=0`. Dozens of `messages`-channel events can land in a\n // single SSE parse — they drain through the `for await` loop\n // as a long microtask chain. Microtasks run before any\n // macrotask, so React's concurrent scheduler never gets a\n // chance to commit between updates. Calling `store.setValue`\n // per event in that burst overflows React's\n // `nestedUpdateCount` guard and throws \"Maximum update depth\n // exceeded\", permanently killing the projection and\n // leaving the store stuck at its first few messages.\n //\n // Batching via `MessageChannel` (macrotask) coalesces the\n // replay burst into one `setValue` call and lets React\n // commit between flushes for live token streaming too.\n const pendingMessages: BaseMessage[] = [];\n let dirty = false;\n let flushScheduled = false;\n const flushChannel =\n typeof MessageChannel !== \"undefined\" ? new MessageChannel() : null;\n\n const flush = (): void => {\n flushScheduled = false;\n if (!dirty || disposed) return;\n dirty = false;\n // `.slice()` breaks identity so React's `Object.is` bail-out\n // in `StreamStore.setValue` propagates the change.\n store.setValue(pendingMessages.slice());\n };\n if (flushChannel != null) {\n flushChannel.port1.onmessage = flush;\n }\n\n const scheduleFlush = (): void => {\n dirty = true;\n if (flushScheduled) return;\n flushScheduled = true;\n if (flushChannel != null) {\n flushChannel.port2.postMessage(null);\n } else {\n setTimeout(flush, 0);\n }\n };\n\n // Rebuild the store from `values.messages` snapshots.\n //\n // `values` events carry the full, committed state of the\n // thread's `messages` channel at a checkpoint — they fire\n // on node completion, AFTER every `messages`-channel delta\n // for that turn has been emitted. They are the authoritative\n // source of truth for ORDER and for non-streamed messages\n // (human turns, serialised tool results, subagent echoes, …).\n //\n // Why rebuild rather than merge-by-id?\n //\n // In practice the server may emit the same logical message\n // with DIFFERENT ids across successive `values` snapshots at\n // the same namespace — e.g. a subagent first surfaces its\n // seed prompt with a synthetic id like\n // `subagent:<tool_call_id>:human`, then a later superstep\n // echoes the same prompt back with a real UUID (or vice\n // versa). A naive \"match-or-append by id\" strategy treats\n // each fresh id as a new entry and the list grows\n // monotonically, showing the same content twice (or more)\n // in the UI.\n //\n // Policy (mirrors the root controller's `#applyValues`):\n //\n // 1. Walk `values.messages` in order. For each id, prefer\n // the stream-assembled entry if we have one for that id\n // (keeps in-progress token streaming visible); otherwise\n // take the values-coerced instance. Same-id values still\n // win when they carry data the stream lacks:\n // - tool messages arriving without `tool_call_id` on\n // the messages channel — the values snapshot always\n // carries it;\n // - AI messages whose finalized `tool_calls` didn't\n // fully land via the messages channel — the values\n // snapshot's AI message has them populated;\n // - metadata enrichment with unchanged content (e.g.\n // committed `additional_kwargs` on an echoed human).\n //\n // 2. Append any stream-only ids (seen on the messages\n // channel but never echoed in ANY values snapshot yet)\n // — their enclosing superstep hasn't committed yet, so\n // dropping them would flash the UI.\n //\n // 3. Ids that WERE in a prior values snapshot but are gone\n // from this one are treated as explicit removals\n // (`RemoveMessage` reducer deltas) and dropped.\n //\n // Unkeyed messages (no stable id) are passed through in\n // their values order because we can't dedupe them safely.\n const applyValuesEvent = (event: ValuesEvent): void => {\n const data = event.params.data;\n if (data == null || typeof data !== \"object\" || Array.isArray(data)) {\n return;\n }\n const state = data as Record<string, unknown>;\n const rawMessages = state.messages;\n if (!Array.isArray(rawMessages) || rawMessages.length === 0) return;\n\n const coerced = ensureMessageInstances(\n rawMessages as (Message | BaseMessage)[]\n );\n\n const reconciliation = reconcileMessagesFromValues({\n valueMessages: coerced,\n currentMessages: pendingMessages,\n currentIndexById: indexById,\n previousValueMessageIds: valuesMessageIds,\n streamedMessageIds: streamMessageIds,\n preferValuesMessage: shouldPreferValuesMessage,\n });\n valuesMessageIds = reconciliation.valueMessageIds;\n const reconciledMessages = [...reconciliation.messages];\n\n pendingMessages.length = 0;\n for (const message of reconciledMessages) pendingMessages.push(message);\n indexById.clear();\n for (const [id, idx] of buildMessageIndex(pendingMessages)) {\n indexById.set(id, idx);\n }\n scheduleFlush();\n };\n\n const applyEvent = (event: MessagesEvent): void => {\n const data = event.params.data;\n\n if (data.event === \"message-start\") {\n const startData = data as MessageStartData;\n const role = (startData.role ?? \"ai\") as MessageRole;\n // \"tool\" role is a v1 convention not represented in the\n // protocol enum but common in practice — keep it working\n // for graphs that emit it as an extensible field.\n const extendedRole =\n (startData as { role?: ExtendedMessageRole }).role ?? role;\n const maybeToolCallId = (startData as { tool_call_id?: string })\n .tool_call_id;\n if (startData.id != null) {\n roleByKey.set(startData.id, {\n role: extendedRole,\n toolCallId: maybeToolCallId,\n });\n }\n }\n\n const update = assembler.consume(event);\n if (update == null) return;\n const msg = update.message;\n const id = msg.id;\n if (id == null) return;\n const captured = roleByKey.get(id) ?? { role: \"ai\" as const };\n const base = assembledMessageToBaseMessage(msg, captured.role, {\n toolCallId: captured.toolCallId,\n });\n\n streamMessageIds.add(id);\n const existingIdx = indexById.get(id);\n if (existingIdx == null) {\n indexById.set(id, pendingMessages.length);\n pendingMessages.push(base);\n } else {\n pendingMessages[existingIdx] = base;\n }\n scheduleFlush();\n };\n\n let runtime: ProjectionRuntime | undefined;\n const openSubscription = () => {\n runtime = openProjectionSubscription({\n thread,\n // Subscribe to both `messages` (live token deltas that drive\n // the in-flight assistant bubble) and `values` (periodic full-\n // state snapshots). Consuming values lets late-mounted scoped\n // projections backfill history after the run has finished.\n channels: [\"messages\", \"values\"],\n namespace: ns,\n onEvent(event) {\n if (event.method === \"messages\") {\n applyEvent(event as MessagesEvent);\n } else if (event.method === \"values\") {\n applyValuesEvent(event as ValuesEvent);\n }\n },\n });\n };\n\n void (async () => {\n const seeded =\n (await rootBus.trySeedFromHistory?.({\n kind: \"messages\",\n namespace: ns,\n store,\n })) === true;\n if (!seeded && !disposed) openSubscription();\n })();\n\n return {\n async dispose() {\n disposed = true;\n if (flushChannel != null) {\n flushChannel.port1.onmessage = null;\n flushChannel.port1.close();\n flushChannel.port2.close();\n }\n await runtime?.dispose();\n },\n };\n },\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"messages.cjs","names":["namespaceKey","MessageAssembler","isRootNamespace","reconcileMessagesFromValues","ensureMessageInstances","shouldPreferValuesMessage","buildMessageIndex","assembledMessageToBaseMessage","openProjectionSubscription"],"sources":["../../../src/stream/projections/messages.ts"],"sourcesContent":["/**\n * Namespace-scoped `messages` projection.\n *\n * Opens `thread.subscribe({ channels: [\"messages\"], namespaces: [ns] })`\n * and folds each `messages` event through {@link MessageAssembler}.\n * Every update — start, block delta, block finish, message finish —\n * re-derives a `BaseMessage` class instance for the currently-active\n * message and updates its slot in the store.\n *\n * The projection emits `BaseMessage[]` (class instances from\n * `@langchain/core/messages`), never plain serialized objects.\n */\nimport type {\n MessagesEvent,\n MessageRole,\n MessageStartData,\n ValuesEvent,\n} from \"@langchain/protocol\";\nimport type { BaseMessage } from \"@langchain/core/messages\";\nimport { MessageAssembler } from \"../../client/stream/messages.js\";\nimport {\n assembledMessageToBaseMessage,\n type ExtendedMessageRole,\n} from \"../assembled-to-message.js\";\nimport type { Message } from \"../../types.messages.js\";\nimport type { ProjectionSpec, ProjectionRuntime } from \"../types.js\";\nimport { ensureMessageInstances } from \"../message-coercion.js\";\nimport { isRootNamespace, namespaceKey } from \"../namespace.js\";\nimport {\n buildMessageIndex,\n reconcileMessagesFromValues,\n shouldPreferValuesMessage,\n} from \"../message-reconciliation.js\";\nimport { openProjectionSubscription } from \"./runtime.js\";\n\nexport function messagesProjection(\n namespace: readonly string[]\n): ProjectionSpec<BaseMessage[]> {\n const ns = [...namespace];\n const key = `messages|${namespaceKey(ns)}`;\n\n return {\n key,\n namespace: ns,\n initial: [],\n open({ thread, store, rootBus }): ProjectionRuntime {\n const assembler = new MessageAssembler();\n // Per-messageId state needed for BaseMessage projection:\n // - `role` is only in the `message-start` event; we cache it\n // so subsequent delta events still produce a typed message.\n // - `toolCallId` is pulled from message-start extras when role\n // is `tool` (a convention we keep compatible with serialized\n // v1 tool messages).\n const roleByKey = new Map<\n string,\n { role: ExtendedMessageRole; toolCallId?: string }\n >();\n const indexById = new Map<string, number>();\n // Ids this projection has observed via the `messages` channel\n // (token-level deltas). Used by `applyValuesEvent` to prefer the\n // stream-assembled version over the values-coerced shape while a\n // turn is streaming, matching the root controller's policy.\n const streamMessageIds = new Set<string>();\n // Ids observed in the most recent `values.messages` snapshot.\n // Messages that were present in a prior snapshot but are absent\n // from this one are treated as explicit removals (server-side\n // `RemoveMessage` reducer deltas). Stream-only messages (seen on\n // the messages channel but never echoed in a values snapshot)\n // are preserved — their enclosing superstep may simply not have\n // committed yet.\n let valuesMessageIds = new Set<string>();\n // Same run-boundary rule as the root projection, driven by root\n // lifecycle events: a terminal drops streamed, uncommitted messages\n // that never finished; the next `running` drops the rest. Streamed\n // events for a dropped id are ignored from then on, so a replayed\n // `message-start` can't rebuild it.\n const uncommittedIds = new Set<string>();\n const finishedIds = new Set<string>();\n const droppedIds = new Set<string>();\n\n // Root-scoped projections whose channels are already covered by\n // the controller's root pump attach to the shared fan-out\n // instead of opening a second server subscription. The root\n // pump runs at `{namespaces: [[]], depth: 1}`, which is exactly\n // the scope a root-namespace `messagesProjection` wants.\n const rootShortCircuit =\n isRootNamespace(ns) && rootBus.channels.includes(\"messages\");\n\n let disposed = false;\n\n // Local mirror of the store contents. Every `applyEvent` /\n // `applyValuesEvent` mutates this synchronously; a coalesced\n // `scheduleFlush` copies it to `store` once per macrotask.\n //\n // Why the indirection? When a namespace-scoped projection\n // (e.g. a subagent modal opened after the run finished) first\n // subscribes, the server replays the entire history from\n // `seq=0`. Dozens of `messages`-channel events can land in a\n // single SSE parse — they drain through the `for await` loop\n // as a long microtask chain. Microtasks run before any\n // macrotask, so React's concurrent scheduler never gets a\n // chance to commit between updates. Calling `store.setValue`\n // per event in that burst overflows React's\n // `nestedUpdateCount` guard and throws \"Maximum update depth\n // exceeded\", permanently killing the projection and\n // leaving the store stuck at its first few messages.\n //\n // Batching via `MessageChannel` (macrotask) coalesces the\n // replay burst into one `setValue` call and lets React\n // commit between flushes for live token streaming too.\n const pendingMessages: BaseMessage[] = [];\n let dirty = false;\n let flushScheduled = false;\n const flushChannel =\n typeof MessageChannel !== \"undefined\" ? new MessageChannel() : null;\n\n const flush = (): void => {\n flushScheduled = false;\n if (!dirty || disposed) return;\n dirty = false;\n // `.slice()` breaks identity so React's `Object.is` bail-out\n // in `StreamStore.setValue` propagates the change.\n store.setValue(pendingMessages.slice());\n };\n if (flushChannel != null) {\n flushChannel.port1.onmessage = flush;\n }\n\n const scheduleFlush = (): void => {\n dirty = true;\n if (flushScheduled) return;\n flushScheduled = true;\n if (flushChannel != null) {\n flushChannel.port2.postMessage(null);\n } else {\n setTimeout(flush, 0);\n }\n };\n\n // Rebuild the store from `values.messages` snapshots.\n //\n // `values` events carry the full, committed state of the\n // thread's `messages` channel at a checkpoint — they fire\n // on node completion, AFTER every `messages`-channel delta\n // for that turn has been emitted. They are the authoritative\n // source of truth for ORDER and for non-streamed messages\n // (human turns, serialised tool results, subagent echoes, …).\n //\n // Why rebuild rather than merge-by-id?\n //\n // In practice the server may emit the same logical message\n // with DIFFERENT ids across successive `values` snapshots at\n // the same namespace — e.g. a subagent first surfaces its\n // seed prompt with a synthetic id like\n // `subagent:<tool_call_id>:human`, then a later superstep\n // echoes the same prompt back with a real UUID (or vice\n // versa). A naive \"match-or-append by id\" strategy treats\n // each fresh id as a new entry and the list grows\n // monotonically, showing the same content twice (or more)\n // in the UI.\n //\n // Policy (mirrors the root controller's `#applyValues`):\n //\n // 1. Walk `values.messages` in order. For each id, prefer\n // the stream-assembled entry if we have one for that id\n // (keeps in-progress token streaming visible); otherwise\n // take the values-coerced instance. Same-id values still\n // win when they carry data the stream lacks:\n // - tool messages arriving without `tool_call_id` on\n // the messages channel — the values snapshot always\n // carries it;\n // - AI messages whose finalized `tool_calls` didn't\n // fully land via the messages channel — the values\n // snapshot's AI message has them populated;\n // - metadata enrichment with unchanged content (e.g.\n // committed `additional_kwargs` on an echoed human).\n //\n // 2. Append any stream-only ids (seen on the messages\n // channel but never echoed in ANY values snapshot yet)\n // — their enclosing superstep hasn't committed yet, so\n // dropping them would flash the UI.\n //\n // 3. Ids that WERE in a prior values snapshot but are gone\n // from this one are treated as explicit removals\n // (`RemoveMessage` reducer deltas) and dropped.\n //\n // Unkeyed messages (no stable id) are passed through in\n // their values order because we can't dedupe them safely.\n const applyValuesEvent = (event: ValuesEvent): void => {\n const data = event.params.data;\n if (data == null || typeof data !== \"object\" || Array.isArray(data)) {\n return;\n }\n const state = data as Record<string, unknown>;\n const rawMessages = state.messages;\n if (!Array.isArray(rawMessages) || rawMessages.length === 0) return;\n\n const coerced = ensureMessageInstances(\n rawMessages as (Message | BaseMessage)[]\n );\n\n const reconciliation = reconcileMessagesFromValues({\n valueMessages: coerced,\n currentMessages: pendingMessages,\n currentIndexById: indexById,\n previousValueMessageIds: valuesMessageIds,\n streamedMessageIds: streamMessageIds,\n preferValuesMessage: shouldPreferValuesMessage,\n });\n valuesMessageIds = reconciliation.valueMessageIds;\n for (const id of valuesMessageIds) {\n uncommittedIds.delete(id);\n finishedIds.delete(id);\n }\n const reconciledMessages = [...reconciliation.messages];\n\n pendingMessages.length = 0;\n for (const message of reconciledMessages) pendingMessages.push(message);\n indexById.clear();\n for (const [id, idx] of buildMessageIndex(pendingMessages)) {\n indexById.set(id, idx);\n }\n scheduleFlush();\n };\n\n const applyEvent = (event: MessagesEvent): void => {\n const data = event.params.data;\n\n if (data.event === \"message-start\") {\n const startData = data as MessageStartData;\n const role = (startData.role ?? \"ai\") as MessageRole;\n // \"tool\" role is a v1 convention not represented in the\n // protocol enum but common in practice — keep it working\n // for graphs that emit it as an extensible field.\n const extendedRole =\n (startData as { role?: ExtendedMessageRole }).role ?? role;\n const maybeToolCallId = (startData as { tool_call_id?: string })\n .tool_call_id;\n if (startData.id != null) {\n roleByKey.set(startData.id, {\n role: extendedRole,\n toolCallId: maybeToolCallId,\n });\n }\n }\n\n const update = assembler.consume(event);\n if (update == null) return;\n const msg = update.message;\n const id = msg.id;\n if (id == null || droppedIds.has(id)) return;\n if (!valuesMessageIds.has(id)) uncommittedIds.add(id);\n if (update.kind === \"message-finish\") finishedIds.add(id);\n const captured = roleByKey.get(id) ?? { role: \"ai\" as const };\n const base = assembledMessageToBaseMessage(msg, captured.role, {\n toolCallId: captured.toolCallId,\n });\n\n streamMessageIds.add(id);\n const existingIdx = indexById.get(id);\n if (existingIdx == null) {\n indexById.set(id, pendingMessages.length);\n pendingMessages.push(base);\n } else {\n pendingMessages[existingIdx] = base;\n }\n scheduleFlush();\n };\n\n const closeRun = (unfinishedOnly: boolean): void => {\n assembler.abandonActive();\n const dropped = new Set<string>();\n for (const id of uncommittedIds) {\n if (unfinishedOnly && finishedIds.has(id)) continue;\n dropped.add(id);\n }\n if (dropped.size === 0) return;\n for (const id of dropped) {\n uncommittedIds.delete(id);\n finishedIds.delete(id);\n droppedIds.add(id);\n }\n const kept = pendingMessages.filter(\n (m) => m.id == null || !dropped.has(m.id)\n );\n pendingMessages.length = 0;\n pendingMessages.push(...kept);\n indexById.clear();\n for (const [id, idx] of buildMessageIndex(pendingMessages)) {\n indexById.set(id, idx);\n }\n scheduleFlush();\n };\n\n // Root lifecycle arrives on the root pump, not this subscription.\n // Defer a macrotask so events this subscription has already\n // received from before the terminal are applied first.\n const unsubscribeLifecycle = rootBus.subscribe((event) => {\n if (event.method !== \"lifecycle\") return;\n if (!isRootNamespace(event.params.namespace)) return;\n const lifecycle = (event.params.data as { event?: string })?.event;\n if (lifecycle === \"running\") {\n setTimeout(() => !disposed && closeRun(false), 0);\n } else if (\n lifecycle === \"completed\" ||\n lifecycle === \"interrupted\" ||\n lifecycle === \"failed\"\n ) {\n setTimeout(() => !disposed && closeRun(true), 0);\n }\n });\n\n let runtime: ProjectionRuntime | undefined;\n const openSubscription = () => {\n runtime = openProjectionSubscription({\n thread,\n // Subscribe to both `messages` (live token deltas that drive\n // the in-flight assistant bubble) and `values` (periodic full-\n // state snapshots). Consuming values lets late-mounted scoped\n // projections backfill history after the run has finished.\n channels: [\"messages\", \"values\"],\n namespace: ns,\n onEvent(event) {\n if (event.method === \"messages\") {\n applyEvent(event as MessagesEvent);\n } else if (event.method === \"values\") {\n applyValuesEvent(event as ValuesEvent);\n }\n },\n });\n };\n\n if (rootShortCircuit) {\n runtime = {\n dispose: rootBus.subscribe((event) => {\n if (!isRootNamespace(event.params.namespace)) return;\n if (event.method === \"messages\") {\n applyEvent(event as MessagesEvent);\n } else if (event.method === \"values\") {\n applyValuesEvent(event as ValuesEvent);\n }\n }),\n };\n } else {\n void (async () => {\n const seeded =\n (await rootBus.trySeedFromHistory?.({\n kind: \"messages\",\n namespace: ns,\n store,\n })) === true;\n if (!seeded && !disposed) openSubscription();\n })();\n }\n\n return {\n async dispose() {\n disposed = true;\n unsubscribeLifecycle();\n if (flushChannel != null) {\n flushChannel.port1.onmessage = null;\n flushChannel.port1.close();\n flushChannel.port2.close();\n }\n await runtime?.dispose();\n },\n };\n },\n };\n}\n"],"mappings":"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"}
|
|
@@ -17,16 +17,10 @@ function messagesProjection(namespace) {
|
|
|
17
17
|
const indexById = /* @__PURE__ */ new Map();
|
|
18
18
|
const streamMessageIds = /* @__PURE__ */ new Set();
|
|
19
19
|
let valuesMessageIds = /* @__PURE__ */ new Set();
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
applyEvent(event);
|
|
25
|
-
});
|
|
26
|
-
return { dispose() {
|
|
27
|
-
unsubscribe();
|
|
28
|
-
} };
|
|
29
|
-
}
|
|
20
|
+
const uncommittedIds = /* @__PURE__ */ new Set();
|
|
21
|
+
const finishedIds = /* @__PURE__ */ new Set();
|
|
22
|
+
const droppedIds = /* @__PURE__ */ new Set();
|
|
23
|
+
const rootShortCircuit = isRootNamespace(ns) && rootBus.channels.includes("messages");
|
|
30
24
|
let disposed = false;
|
|
31
25
|
const pendingMessages = [];
|
|
32
26
|
let dirty = false;
|
|
@@ -60,6 +54,10 @@ function messagesProjection(namespace) {
|
|
|
60
54
|
preferValuesMessage: shouldPreferValuesMessage
|
|
61
55
|
});
|
|
62
56
|
valuesMessageIds = reconciliation.valueMessageIds;
|
|
57
|
+
for (const id of valuesMessageIds) {
|
|
58
|
+
uncommittedIds.delete(id);
|
|
59
|
+
finishedIds.delete(id);
|
|
60
|
+
}
|
|
63
61
|
const reconciledMessages = [...reconciliation.messages];
|
|
64
62
|
pendingMessages.length = 0;
|
|
65
63
|
for (const message of reconciledMessages) pendingMessages.push(message);
|
|
@@ -83,7 +81,9 @@ function messagesProjection(namespace) {
|
|
|
83
81
|
if (update == null) return;
|
|
84
82
|
const msg = update.message;
|
|
85
83
|
const id = msg.id;
|
|
86
|
-
if (id == null) return;
|
|
84
|
+
if (id == null || droppedIds.has(id)) return;
|
|
85
|
+
if (!valuesMessageIds.has(id)) uncommittedIds.add(id);
|
|
86
|
+
if (update.kind === "message-finish") finishedIds.add(id);
|
|
87
87
|
const captured = roleByKey.get(id) ?? { role: "ai" };
|
|
88
88
|
const base = assembledMessageToBaseMessage(msg, captured.role, { toolCallId: captured.toolCallId });
|
|
89
89
|
streamMessageIds.add(id);
|
|
@@ -94,6 +94,33 @@ function messagesProjection(namespace) {
|
|
|
94
94
|
} else pendingMessages[existingIdx] = base;
|
|
95
95
|
scheduleFlush();
|
|
96
96
|
};
|
|
97
|
+
const closeRun = (unfinishedOnly) => {
|
|
98
|
+
assembler.abandonActive();
|
|
99
|
+
const dropped = /* @__PURE__ */ new Set();
|
|
100
|
+
for (const id of uncommittedIds) {
|
|
101
|
+
if (unfinishedOnly && finishedIds.has(id)) continue;
|
|
102
|
+
dropped.add(id);
|
|
103
|
+
}
|
|
104
|
+
if (dropped.size === 0) return;
|
|
105
|
+
for (const id of dropped) {
|
|
106
|
+
uncommittedIds.delete(id);
|
|
107
|
+
finishedIds.delete(id);
|
|
108
|
+
droppedIds.add(id);
|
|
109
|
+
}
|
|
110
|
+
const kept = pendingMessages.filter((m) => m.id == null || !dropped.has(m.id));
|
|
111
|
+
pendingMessages.length = 0;
|
|
112
|
+
pendingMessages.push(...kept);
|
|
113
|
+
indexById.clear();
|
|
114
|
+
for (const [id, idx] of buildMessageIndex(pendingMessages)) indexById.set(id, idx);
|
|
115
|
+
scheduleFlush();
|
|
116
|
+
};
|
|
117
|
+
const unsubscribeLifecycle = rootBus.subscribe((event) => {
|
|
118
|
+
if (event.method !== "lifecycle") return;
|
|
119
|
+
if (!isRootNamespace(event.params.namespace)) return;
|
|
120
|
+
const lifecycle = event.params.data?.event;
|
|
121
|
+
if (lifecycle === "running") setTimeout(() => !disposed && closeRun(false), 0);
|
|
122
|
+
else if (lifecycle === "completed" || lifecycle === "interrupted" || lifecycle === "failed") setTimeout(() => !disposed && closeRun(true), 0);
|
|
123
|
+
});
|
|
97
124
|
let runtime;
|
|
98
125
|
const openSubscription = () => {
|
|
99
126
|
runtime = openProjectionSubscription({
|
|
@@ -106,7 +133,12 @@ function messagesProjection(namespace) {
|
|
|
106
133
|
}
|
|
107
134
|
});
|
|
108
135
|
};
|
|
109
|
-
(
|
|
136
|
+
if (rootShortCircuit) runtime = { dispose: rootBus.subscribe((event) => {
|
|
137
|
+
if (!isRootNamespace(event.params.namespace)) return;
|
|
138
|
+
if (event.method === "messages") applyEvent(event);
|
|
139
|
+
else if (event.method === "values") applyValuesEvent(event);
|
|
140
|
+
}) };
|
|
141
|
+
else (async () => {
|
|
110
142
|
if (!(await rootBus.trySeedFromHistory?.({
|
|
111
143
|
kind: "messages",
|
|
112
144
|
namespace: ns,
|
|
@@ -115,6 +147,7 @@ function messagesProjection(namespace) {
|
|
|
115
147
|
})();
|
|
116
148
|
return { async dispose() {
|
|
117
149
|
disposed = true;
|
|
150
|
+
unsubscribeLifecycle();
|
|
118
151
|
if (flushChannel != null) {
|
|
119
152
|
flushChannel.port1.onmessage = null;
|
|
120
153
|
flushChannel.port1.close();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"messages.js","names":[],"sources":["../../../src/stream/projections/messages.ts"],"sourcesContent":["/**\n * Namespace-scoped `messages` projection.\n *\n * Opens `thread.subscribe({ channels: [\"messages\"], namespaces: [ns] })`\n * and folds each `messages` event through {@link MessageAssembler}.\n * Every update — start, block delta, block finish, message finish —\n * re-derives a `BaseMessage` class instance for the currently-active\n * message and updates its slot in the store.\n *\n * The projection emits `BaseMessage[]` (class instances from\n * `@langchain/core/messages`), never plain serialized objects.\n */\nimport type {\n MessagesEvent,\n MessageRole,\n MessageStartData,\n ValuesEvent,\n} from \"@langchain/protocol\";\nimport type { BaseMessage } from \"@langchain/core/messages\";\nimport { MessageAssembler } from \"../../client/stream/messages.js\";\nimport {\n assembledMessageToBaseMessage,\n type ExtendedMessageRole,\n} from \"../assembled-to-message.js\";\nimport type { Message } from \"../../types.messages.js\";\nimport type { ProjectionSpec, ProjectionRuntime } from \"../types.js\";\nimport { ensureMessageInstances } from \"../message-coercion.js\";\nimport { isRootNamespace, namespaceKey } from \"../namespace.js\";\nimport {\n buildMessageIndex,\n reconcileMessagesFromValues,\n shouldPreferValuesMessage,\n} from \"../message-reconciliation.js\";\nimport { openProjectionSubscription } from \"./runtime.js\";\n\nexport function messagesProjection(\n namespace: readonly string[]\n): ProjectionSpec<BaseMessage[]> {\n const ns = [...namespace];\n const key = `messages|${namespaceKey(ns)}`;\n\n return {\n key,\n namespace: ns,\n initial: [],\n open({ thread, store, rootBus }): ProjectionRuntime {\n const assembler = new MessageAssembler();\n // Per-messageId state needed for BaseMessage projection:\n // - `role` is only in the `message-start` event; we cache it\n // so subsequent delta events still produce a typed message.\n // - `toolCallId` is pulled from message-start extras when role\n // is `tool` (a convention we keep compatible with serialized\n // v1 tool messages).\n const roleByKey = new Map<\n string,\n { role: ExtendedMessageRole; toolCallId?: string }\n >();\n const indexById = new Map<string, number>();\n // Ids this projection has observed via the `messages` channel\n // (token-level deltas). Used by `applyValuesEvent` to prefer the\n // stream-assembled version over the values-coerced shape while a\n // turn is streaming, matching the root controller's policy.\n const streamMessageIds = new Set<string>();\n // Ids observed in the most recent `values.messages` snapshot.\n // Messages that were present in a prior snapshot but are absent\n // from this one are treated as explicit removals (server-side\n // `RemoveMessage` reducer deltas). Stream-only messages (seen on\n // the messages channel but never echoed in a values snapshot)\n // are preserved — their enclosing superstep may simply not have\n // committed yet.\n let valuesMessageIds = new Set<string>();\n\n // Root-scoped projections whose channels are already covered by\n // the controller's root pump attach to the shared fan-out\n // instead of opening a second server subscription. The root\n // pump runs at `{namespaces: [[]], depth: 1}`, which is exactly\n // the scope a root-namespace `messagesProjection` wants.\n const rootShortCircuit =\n isRootNamespace(ns) && rootBus.channels.includes(\"messages\");\n\n if (rootShortCircuit) {\n const unsubscribe = rootBus.subscribe((event) => {\n if (event.method !== \"messages\") return;\n if (!isRootNamespace(event.params.namespace)) return;\n applyEvent(event as MessagesEvent);\n });\n return {\n dispose() {\n unsubscribe();\n },\n };\n }\n\n let disposed = false;\n\n // Local mirror of the store contents. Every `applyEvent` /\n // `applyValuesEvent` mutates this synchronously; a coalesced\n // `scheduleFlush` copies it to `store` once per macrotask.\n //\n // Why the indirection? When a namespace-scoped projection\n // (e.g. a subagent modal opened after the run finished) first\n // subscribes, the server replays the entire history from\n // `seq=0`. Dozens of `messages`-channel events can land in a\n // single SSE parse — they drain through the `for await` loop\n // as a long microtask chain. Microtasks run before any\n // macrotask, so React's concurrent scheduler never gets a\n // chance to commit between updates. Calling `store.setValue`\n // per event in that burst overflows React's\n // `nestedUpdateCount` guard and throws \"Maximum update depth\n // exceeded\", permanently killing the projection and\n // leaving the store stuck at its first few messages.\n //\n // Batching via `MessageChannel` (macrotask) coalesces the\n // replay burst into one `setValue` call and lets React\n // commit between flushes for live token streaming too.\n const pendingMessages: BaseMessage[] = [];\n let dirty = false;\n let flushScheduled = false;\n const flushChannel =\n typeof MessageChannel !== \"undefined\" ? new MessageChannel() : null;\n\n const flush = (): void => {\n flushScheduled = false;\n if (!dirty || disposed) return;\n dirty = false;\n // `.slice()` breaks identity so React's `Object.is` bail-out\n // in `StreamStore.setValue` propagates the change.\n store.setValue(pendingMessages.slice());\n };\n if (flushChannel != null) {\n flushChannel.port1.onmessage = flush;\n }\n\n const scheduleFlush = (): void => {\n dirty = true;\n if (flushScheduled) return;\n flushScheduled = true;\n if (flushChannel != null) {\n flushChannel.port2.postMessage(null);\n } else {\n setTimeout(flush, 0);\n }\n };\n\n // Rebuild the store from `values.messages` snapshots.\n //\n // `values` events carry the full, committed state of the\n // thread's `messages` channel at a checkpoint — they fire\n // on node completion, AFTER every `messages`-channel delta\n // for that turn has been emitted. They are the authoritative\n // source of truth for ORDER and for non-streamed messages\n // (human turns, serialised tool results, subagent echoes, …).\n //\n // Why rebuild rather than merge-by-id?\n //\n // In practice the server may emit the same logical message\n // with DIFFERENT ids across successive `values` snapshots at\n // the same namespace — e.g. a subagent first surfaces its\n // seed prompt with a synthetic id like\n // `subagent:<tool_call_id>:human`, then a later superstep\n // echoes the same prompt back with a real UUID (or vice\n // versa). A naive \"match-or-append by id\" strategy treats\n // each fresh id as a new entry and the list grows\n // monotonically, showing the same content twice (or more)\n // in the UI.\n //\n // Policy (mirrors the root controller's `#applyValues`):\n //\n // 1. Walk `values.messages` in order. For each id, prefer\n // the stream-assembled entry if we have one for that id\n // (keeps in-progress token streaming visible); otherwise\n // take the values-coerced instance. Same-id values still\n // win when they carry data the stream lacks:\n // - tool messages arriving without `tool_call_id` on\n // the messages channel — the values snapshot always\n // carries it;\n // - AI messages whose finalized `tool_calls` didn't\n // fully land via the messages channel — the values\n // snapshot's AI message has them populated;\n // - metadata enrichment with unchanged content (e.g.\n // committed `additional_kwargs` on an echoed human).\n //\n // 2. Append any stream-only ids (seen on the messages\n // channel but never echoed in ANY values snapshot yet)\n // — their enclosing superstep hasn't committed yet, so\n // dropping them would flash the UI.\n //\n // 3. Ids that WERE in a prior values snapshot but are gone\n // from this one are treated as explicit removals\n // (`RemoveMessage` reducer deltas) and dropped.\n //\n // Unkeyed messages (no stable id) are passed through in\n // their values order because we can't dedupe them safely.\n const applyValuesEvent = (event: ValuesEvent): void => {\n const data = event.params.data;\n if (data == null || typeof data !== \"object\" || Array.isArray(data)) {\n return;\n }\n const state = data as Record<string, unknown>;\n const rawMessages = state.messages;\n if (!Array.isArray(rawMessages) || rawMessages.length === 0) return;\n\n const coerced = ensureMessageInstances(\n rawMessages as (Message | BaseMessage)[]\n );\n\n const reconciliation = reconcileMessagesFromValues({\n valueMessages: coerced,\n currentMessages: pendingMessages,\n currentIndexById: indexById,\n previousValueMessageIds: valuesMessageIds,\n streamedMessageIds: streamMessageIds,\n preferValuesMessage: shouldPreferValuesMessage,\n });\n valuesMessageIds = reconciliation.valueMessageIds;\n const reconciledMessages = [...reconciliation.messages];\n\n pendingMessages.length = 0;\n for (const message of reconciledMessages) pendingMessages.push(message);\n indexById.clear();\n for (const [id, idx] of buildMessageIndex(pendingMessages)) {\n indexById.set(id, idx);\n }\n scheduleFlush();\n };\n\n const applyEvent = (event: MessagesEvent): void => {\n const data = event.params.data;\n\n if (data.event === \"message-start\") {\n const startData = data as MessageStartData;\n const role = (startData.role ?? \"ai\") as MessageRole;\n // \"tool\" role is a v1 convention not represented in the\n // protocol enum but common in practice — keep it working\n // for graphs that emit it as an extensible field.\n const extendedRole =\n (startData as { role?: ExtendedMessageRole }).role ?? role;\n const maybeToolCallId = (startData as { tool_call_id?: string })\n .tool_call_id;\n if (startData.id != null) {\n roleByKey.set(startData.id, {\n role: extendedRole,\n toolCallId: maybeToolCallId,\n });\n }\n }\n\n const update = assembler.consume(event);\n if (update == null) return;\n const msg = update.message;\n const id = msg.id;\n if (id == null) return;\n const captured = roleByKey.get(id) ?? { role: \"ai\" as const };\n const base = assembledMessageToBaseMessage(msg, captured.role, {\n toolCallId: captured.toolCallId,\n });\n\n streamMessageIds.add(id);\n const existingIdx = indexById.get(id);\n if (existingIdx == null) {\n indexById.set(id, pendingMessages.length);\n pendingMessages.push(base);\n } else {\n pendingMessages[existingIdx] = base;\n }\n scheduleFlush();\n };\n\n let runtime: ProjectionRuntime | undefined;\n const openSubscription = () => {\n runtime = openProjectionSubscription({\n thread,\n // Subscribe to both `messages` (live token deltas that drive\n // the in-flight assistant bubble) and `values` (periodic full-\n // state snapshots). Consuming values lets late-mounted scoped\n // projections backfill history after the run has finished.\n channels: [\"messages\", \"values\"],\n namespace: ns,\n onEvent(event) {\n if (event.method === \"messages\") {\n applyEvent(event as MessagesEvent);\n } else if (event.method === \"values\") {\n applyValuesEvent(event as ValuesEvent);\n }\n },\n });\n };\n\n void (async () => {\n const seeded =\n (await rootBus.trySeedFromHistory?.({\n kind: \"messages\",\n namespace: ns,\n store,\n })) === true;\n if (!seeded && !disposed) openSubscription();\n })();\n\n return {\n async dispose() {\n disposed = true;\n if (flushChannel != null) {\n flushChannel.port1.onmessage = null;\n flushChannel.port1.close();\n flushChannel.port2.close();\n }\n await runtime?.dispose();\n },\n };\n },\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"messages.js","names":[],"sources":["../../../src/stream/projections/messages.ts"],"sourcesContent":["/**\n * Namespace-scoped `messages` projection.\n *\n * Opens `thread.subscribe({ channels: [\"messages\"], namespaces: [ns] })`\n * and folds each `messages` event through {@link MessageAssembler}.\n * Every update — start, block delta, block finish, message finish —\n * re-derives a `BaseMessage` class instance for the currently-active\n * message and updates its slot in the store.\n *\n * The projection emits `BaseMessage[]` (class instances from\n * `@langchain/core/messages`), never plain serialized objects.\n */\nimport type {\n MessagesEvent,\n MessageRole,\n MessageStartData,\n ValuesEvent,\n} from \"@langchain/protocol\";\nimport type { BaseMessage } from \"@langchain/core/messages\";\nimport { MessageAssembler } from \"../../client/stream/messages.js\";\nimport {\n assembledMessageToBaseMessage,\n type ExtendedMessageRole,\n} from \"../assembled-to-message.js\";\nimport type { Message } from \"../../types.messages.js\";\nimport type { ProjectionSpec, ProjectionRuntime } from \"../types.js\";\nimport { ensureMessageInstances } from \"../message-coercion.js\";\nimport { isRootNamespace, namespaceKey } from \"../namespace.js\";\nimport {\n buildMessageIndex,\n reconcileMessagesFromValues,\n shouldPreferValuesMessage,\n} from \"../message-reconciliation.js\";\nimport { openProjectionSubscription } from \"./runtime.js\";\n\nexport function messagesProjection(\n namespace: readonly string[]\n): ProjectionSpec<BaseMessage[]> {\n const ns = [...namespace];\n const key = `messages|${namespaceKey(ns)}`;\n\n return {\n key,\n namespace: ns,\n initial: [],\n open({ thread, store, rootBus }): ProjectionRuntime {\n const assembler = new MessageAssembler();\n // Per-messageId state needed for BaseMessage projection:\n // - `role` is only in the `message-start` event; we cache it\n // so subsequent delta events still produce a typed message.\n // - `toolCallId` is pulled from message-start extras when role\n // is `tool` (a convention we keep compatible with serialized\n // v1 tool messages).\n const roleByKey = new Map<\n string,\n { role: ExtendedMessageRole; toolCallId?: string }\n >();\n const indexById = new Map<string, number>();\n // Ids this projection has observed via the `messages` channel\n // (token-level deltas). Used by `applyValuesEvent` to prefer the\n // stream-assembled version over the values-coerced shape while a\n // turn is streaming, matching the root controller's policy.\n const streamMessageIds = new Set<string>();\n // Ids observed in the most recent `values.messages` snapshot.\n // Messages that were present in a prior snapshot but are absent\n // from this one are treated as explicit removals (server-side\n // `RemoveMessage` reducer deltas). Stream-only messages (seen on\n // the messages channel but never echoed in a values snapshot)\n // are preserved — their enclosing superstep may simply not have\n // committed yet.\n let valuesMessageIds = new Set<string>();\n // Same run-boundary rule as the root projection, driven by root\n // lifecycle events: a terminal drops streamed, uncommitted messages\n // that never finished; the next `running` drops the rest. Streamed\n // events for a dropped id are ignored from then on, so a replayed\n // `message-start` can't rebuild it.\n const uncommittedIds = new Set<string>();\n const finishedIds = new Set<string>();\n const droppedIds = new Set<string>();\n\n // Root-scoped projections whose channels are already covered by\n // the controller's root pump attach to the shared fan-out\n // instead of opening a second server subscription. The root\n // pump runs at `{namespaces: [[]], depth: 1}`, which is exactly\n // the scope a root-namespace `messagesProjection` wants.\n const rootShortCircuit =\n isRootNamespace(ns) && rootBus.channels.includes(\"messages\");\n\n let disposed = false;\n\n // Local mirror of the store contents. Every `applyEvent` /\n // `applyValuesEvent` mutates this synchronously; a coalesced\n // `scheduleFlush` copies it to `store` once per macrotask.\n //\n // Why the indirection? When a namespace-scoped projection\n // (e.g. a subagent modal opened after the run finished) first\n // subscribes, the server replays the entire history from\n // `seq=0`. Dozens of `messages`-channel events can land in a\n // single SSE parse — they drain through the `for await` loop\n // as a long microtask chain. Microtasks run before any\n // macrotask, so React's concurrent scheduler never gets a\n // chance to commit between updates. Calling `store.setValue`\n // per event in that burst overflows React's\n // `nestedUpdateCount` guard and throws \"Maximum update depth\n // exceeded\", permanently killing the projection and\n // leaving the store stuck at its first few messages.\n //\n // Batching via `MessageChannel` (macrotask) coalesces the\n // replay burst into one `setValue` call and lets React\n // commit between flushes for live token streaming too.\n const pendingMessages: BaseMessage[] = [];\n let dirty = false;\n let flushScheduled = false;\n const flushChannel =\n typeof MessageChannel !== \"undefined\" ? new MessageChannel() : null;\n\n const flush = (): void => {\n flushScheduled = false;\n if (!dirty || disposed) return;\n dirty = false;\n // `.slice()` breaks identity so React's `Object.is` bail-out\n // in `StreamStore.setValue` propagates the change.\n store.setValue(pendingMessages.slice());\n };\n if (flushChannel != null) {\n flushChannel.port1.onmessage = flush;\n }\n\n const scheduleFlush = (): void => {\n dirty = true;\n if (flushScheduled) return;\n flushScheduled = true;\n if (flushChannel != null) {\n flushChannel.port2.postMessage(null);\n } else {\n setTimeout(flush, 0);\n }\n };\n\n // Rebuild the store from `values.messages` snapshots.\n //\n // `values` events carry the full, committed state of the\n // thread's `messages` channel at a checkpoint — they fire\n // on node completion, AFTER every `messages`-channel delta\n // for that turn has been emitted. They are the authoritative\n // source of truth for ORDER and for non-streamed messages\n // (human turns, serialised tool results, subagent echoes, …).\n //\n // Why rebuild rather than merge-by-id?\n //\n // In practice the server may emit the same logical message\n // with DIFFERENT ids across successive `values` snapshots at\n // the same namespace — e.g. a subagent first surfaces its\n // seed prompt with a synthetic id like\n // `subagent:<tool_call_id>:human`, then a later superstep\n // echoes the same prompt back with a real UUID (or vice\n // versa). A naive \"match-or-append by id\" strategy treats\n // each fresh id as a new entry and the list grows\n // monotonically, showing the same content twice (or more)\n // in the UI.\n //\n // Policy (mirrors the root controller's `#applyValues`):\n //\n // 1. Walk `values.messages` in order. For each id, prefer\n // the stream-assembled entry if we have one for that id\n // (keeps in-progress token streaming visible); otherwise\n // take the values-coerced instance. Same-id values still\n // win when they carry data the stream lacks:\n // - tool messages arriving without `tool_call_id` on\n // the messages channel — the values snapshot always\n // carries it;\n // - AI messages whose finalized `tool_calls` didn't\n // fully land via the messages channel — the values\n // snapshot's AI message has them populated;\n // - metadata enrichment with unchanged content (e.g.\n // committed `additional_kwargs` on an echoed human).\n //\n // 2. Append any stream-only ids (seen on the messages\n // channel but never echoed in ANY values snapshot yet)\n // — their enclosing superstep hasn't committed yet, so\n // dropping them would flash the UI.\n //\n // 3. Ids that WERE in a prior values snapshot but are gone\n // from this one are treated as explicit removals\n // (`RemoveMessage` reducer deltas) and dropped.\n //\n // Unkeyed messages (no stable id) are passed through in\n // their values order because we can't dedupe them safely.\n const applyValuesEvent = (event: ValuesEvent): void => {\n const data = event.params.data;\n if (data == null || typeof data !== \"object\" || Array.isArray(data)) {\n return;\n }\n const state = data as Record<string, unknown>;\n const rawMessages = state.messages;\n if (!Array.isArray(rawMessages) || rawMessages.length === 0) return;\n\n const coerced = ensureMessageInstances(\n rawMessages as (Message | BaseMessage)[]\n );\n\n const reconciliation = reconcileMessagesFromValues({\n valueMessages: coerced,\n currentMessages: pendingMessages,\n currentIndexById: indexById,\n previousValueMessageIds: valuesMessageIds,\n streamedMessageIds: streamMessageIds,\n preferValuesMessage: shouldPreferValuesMessage,\n });\n valuesMessageIds = reconciliation.valueMessageIds;\n for (const id of valuesMessageIds) {\n uncommittedIds.delete(id);\n finishedIds.delete(id);\n }\n const reconciledMessages = [...reconciliation.messages];\n\n pendingMessages.length = 0;\n for (const message of reconciledMessages) pendingMessages.push(message);\n indexById.clear();\n for (const [id, idx] of buildMessageIndex(pendingMessages)) {\n indexById.set(id, idx);\n }\n scheduleFlush();\n };\n\n const applyEvent = (event: MessagesEvent): void => {\n const data = event.params.data;\n\n if (data.event === \"message-start\") {\n const startData = data as MessageStartData;\n const role = (startData.role ?? \"ai\") as MessageRole;\n // \"tool\" role is a v1 convention not represented in the\n // protocol enum but common in practice — keep it working\n // for graphs that emit it as an extensible field.\n const extendedRole =\n (startData as { role?: ExtendedMessageRole }).role ?? role;\n const maybeToolCallId = (startData as { tool_call_id?: string })\n .tool_call_id;\n if (startData.id != null) {\n roleByKey.set(startData.id, {\n role: extendedRole,\n toolCallId: maybeToolCallId,\n });\n }\n }\n\n const update = assembler.consume(event);\n if (update == null) return;\n const msg = update.message;\n const id = msg.id;\n if (id == null || droppedIds.has(id)) return;\n if (!valuesMessageIds.has(id)) uncommittedIds.add(id);\n if (update.kind === \"message-finish\") finishedIds.add(id);\n const captured = roleByKey.get(id) ?? { role: \"ai\" as const };\n const base = assembledMessageToBaseMessage(msg, captured.role, {\n toolCallId: captured.toolCallId,\n });\n\n streamMessageIds.add(id);\n const existingIdx = indexById.get(id);\n if (existingIdx == null) {\n indexById.set(id, pendingMessages.length);\n pendingMessages.push(base);\n } else {\n pendingMessages[existingIdx] = base;\n }\n scheduleFlush();\n };\n\n const closeRun = (unfinishedOnly: boolean): void => {\n assembler.abandonActive();\n const dropped = new Set<string>();\n for (const id of uncommittedIds) {\n if (unfinishedOnly && finishedIds.has(id)) continue;\n dropped.add(id);\n }\n if (dropped.size === 0) return;\n for (const id of dropped) {\n uncommittedIds.delete(id);\n finishedIds.delete(id);\n droppedIds.add(id);\n }\n const kept = pendingMessages.filter(\n (m) => m.id == null || !dropped.has(m.id)\n );\n pendingMessages.length = 0;\n pendingMessages.push(...kept);\n indexById.clear();\n for (const [id, idx] of buildMessageIndex(pendingMessages)) {\n indexById.set(id, idx);\n }\n scheduleFlush();\n };\n\n // Root lifecycle arrives on the root pump, not this subscription.\n // Defer a macrotask so events this subscription has already\n // received from before the terminal are applied first.\n const unsubscribeLifecycle = rootBus.subscribe((event) => {\n if (event.method !== \"lifecycle\") return;\n if (!isRootNamespace(event.params.namespace)) return;\n const lifecycle = (event.params.data as { event?: string })?.event;\n if (lifecycle === \"running\") {\n setTimeout(() => !disposed && closeRun(false), 0);\n } else if (\n lifecycle === \"completed\" ||\n lifecycle === \"interrupted\" ||\n lifecycle === \"failed\"\n ) {\n setTimeout(() => !disposed && closeRun(true), 0);\n }\n });\n\n let runtime: ProjectionRuntime | undefined;\n const openSubscription = () => {\n runtime = openProjectionSubscription({\n thread,\n // Subscribe to both `messages` (live token deltas that drive\n // the in-flight assistant bubble) and `values` (periodic full-\n // state snapshots). Consuming values lets late-mounted scoped\n // projections backfill history after the run has finished.\n channels: [\"messages\", \"values\"],\n namespace: ns,\n onEvent(event) {\n if (event.method === \"messages\") {\n applyEvent(event as MessagesEvent);\n } else if (event.method === \"values\") {\n applyValuesEvent(event as ValuesEvent);\n }\n },\n });\n };\n\n if (rootShortCircuit) {\n runtime = {\n dispose: rootBus.subscribe((event) => {\n if (!isRootNamespace(event.params.namespace)) return;\n if (event.method === \"messages\") {\n applyEvent(event as MessagesEvent);\n } else if (event.method === \"values\") {\n applyValuesEvent(event as ValuesEvent);\n }\n }),\n };\n } else {\n void (async () => {\n const seeded =\n (await rootBus.trySeedFromHistory?.({\n kind: \"messages\",\n namespace: ns,\n store,\n })) === true;\n if (!seeded && !disposed) openSubscription();\n })();\n }\n\n return {\n async dispose() {\n disposed = true;\n unsubscribeLifecycle();\n if (flushChannel != null) {\n flushChannel.port1.onmessage = null;\n flushChannel.port1.close();\n flushChannel.port2.close();\n }\n await runtime?.dispose();\n },\n };\n },\n };\n}\n"],"mappings":"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"}
|