@langchain/langgraph-sdk 1.9.16 → 1.9.18

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.
Files changed (114) hide show
  1. package/dist/client/base.cjs +70 -4
  2. package/dist/client/base.cjs.map +1 -1
  3. package/dist/client/base.d.cts +3 -0
  4. package/dist/client/base.d.cts.map +1 -1
  5. package/dist/client/base.d.ts +3 -0
  6. package/dist/client/base.d.ts.map +1 -1
  7. package/dist/client/base.js +70 -4
  8. package/dist/client/base.js.map +1 -1
  9. package/dist/client/threads/index.cjs +4 -2
  10. package/dist/client/threads/index.cjs.map +1 -1
  11. package/dist/client/threads/index.d.cts.map +1 -1
  12. package/dist/client/threads/index.d.ts.map +1 -1
  13. package/dist/client/threads/index.js +4 -2
  14. package/dist/client/threads/index.js.map +1 -1
  15. package/dist/stream/controller.cjs +451 -32
  16. package/dist/stream/controller.cjs.map +1 -1
  17. package/dist/stream/controller.d.cts +15 -0
  18. package/dist/stream/controller.d.cts.map +1 -1
  19. package/dist/stream/controller.d.ts +15 -0
  20. package/dist/stream/controller.d.ts.map +1 -1
  21. package/dist/stream/controller.js +472 -32
  22. package/dist/stream/controller.js.map +1 -1
  23. package/dist/stream/discovery/index.cjs +2 -0
  24. package/dist/stream/discovery/index.js +3 -0
  25. package/dist/stream/discovery/namespace-from-history.cjs +207 -0
  26. package/dist/stream/discovery/namespace-from-history.cjs.map +1 -0
  27. package/dist/stream/discovery/namespace-from-history.js +204 -0
  28. package/dist/stream/discovery/namespace-from-history.js.map +1 -0
  29. package/dist/stream/discovery/subagents.cjs +56 -1
  30. package/dist/stream/discovery/subagents.cjs.map +1 -1
  31. package/dist/stream/discovery/subagents.d.cts +31 -0
  32. package/dist/stream/discovery/subagents.d.cts.map +1 -1
  33. package/dist/stream/discovery/subagents.d.ts +31 -0
  34. package/dist/stream/discovery/subagents.d.ts.map +1 -1
  35. package/dist/stream/discovery/subagents.js +56 -1
  36. package/dist/stream/discovery/subagents.js.map +1 -1
  37. package/dist/stream/discovery/subgraphs.cjs +24 -0
  38. package/dist/stream/discovery/subgraphs.cjs.map +1 -1
  39. package/dist/stream/discovery/subgraphs.d.cts +13 -0
  40. package/dist/stream/discovery/subgraphs.d.cts.map +1 -1
  41. package/dist/stream/discovery/subgraphs.d.ts +13 -0
  42. package/dist/stream/discovery/subgraphs.d.ts.map +1 -1
  43. package/dist/stream/discovery/subgraphs.js +24 -0
  44. package/dist/stream/discovery/subgraphs.js.map +1 -1
  45. package/dist/stream/index.cjs +1 -0
  46. package/dist/stream/index.js +1 -0
  47. package/dist/stream/message-coercion.cjs +101 -0
  48. package/dist/stream/message-coercion.cjs.map +1 -0
  49. package/dist/stream/message-coercion.d.ts +1 -0
  50. package/dist/stream/message-coercion.js +98 -0
  51. package/dist/stream/message-coercion.js.map +1 -0
  52. package/dist/stream/message-metadata-tracker.cjs +92 -0
  53. package/dist/stream/message-metadata-tracker.cjs.map +1 -1
  54. package/dist/stream/message-metadata-tracker.d.cts +23 -0
  55. package/dist/stream/message-metadata-tracker.d.cts.map +1 -1
  56. package/dist/stream/message-metadata-tracker.d.ts +23 -0
  57. package/dist/stream/message-metadata-tracker.d.ts.map +1 -1
  58. package/dist/stream/message-metadata-tracker.js +92 -0
  59. package/dist/stream/message-metadata-tracker.js.map +1 -1
  60. package/dist/stream/message-reconciliation.cjs +2 -2
  61. package/dist/stream/message-reconciliation.cjs.map +1 -1
  62. package/dist/stream/message-reconciliation.js +2 -2
  63. package/dist/stream/message-reconciliation.js.map +1 -1
  64. package/dist/stream/optimistic-input.cjs +86 -0
  65. package/dist/stream/optimistic-input.cjs.map +1 -0
  66. package/dist/stream/optimistic-input.d.ts +1 -0
  67. package/dist/stream/optimistic-input.js +86 -0
  68. package/dist/stream/optimistic-input.js.map +1 -0
  69. package/dist/stream/projections/channel.cjs +1 -0
  70. package/dist/stream/projections/channel.cjs.map +1 -1
  71. package/dist/stream/projections/channel.d.cts.map +1 -1
  72. package/dist/stream/projections/channel.d.ts.map +1 -1
  73. package/dist/stream/projections/channel.js +1 -0
  74. package/dist/stream/projections/channel.js.map +1 -1
  75. package/dist/stream/projections/messages.cjs +24 -14
  76. package/dist/stream/projections/messages.cjs.map +1 -1
  77. package/dist/stream/projections/messages.js +21 -11
  78. package/dist/stream/projections/messages.js.map +1 -1
  79. package/dist/stream/projections/tool-calls.cjs +22 -10
  80. package/dist/stream/projections/tool-calls.cjs.map +1 -1
  81. package/dist/stream/projections/tool-calls.js +22 -10
  82. package/dist/stream/projections/tool-calls.js.map +1 -1
  83. package/dist/stream/projections/values.cjs +2 -2
  84. package/dist/stream/projections/values.cjs.map +1 -1
  85. package/dist/stream/projections/values.js +1 -1
  86. package/dist/stream/projections/values.js.map +1 -1
  87. package/dist/stream/root-message-projection.cjs +130 -3
  88. package/dist/stream/root-message-projection.cjs.map +1 -1
  89. package/dist/stream/root-message-projection.js +130 -3
  90. package/dist/stream/root-message-projection.js.map +1 -1
  91. package/dist/stream/submit-coordinator.cjs +28 -6
  92. package/dist/stream/submit-coordinator.cjs.map +1 -1
  93. package/dist/stream/submit-coordinator.d.cts.map +1 -1
  94. package/dist/stream/submit-coordinator.d.ts +0 -1
  95. package/dist/stream/submit-coordinator.d.ts.map +1 -1
  96. package/dist/stream/submit-coordinator.js +28 -6
  97. package/dist/stream/submit-coordinator.js.map +1 -1
  98. package/dist/stream/tool-calls.cjs +32 -0
  99. package/dist/stream/tool-calls.cjs.map +1 -1
  100. package/dist/stream/tool-calls.js +32 -1
  101. package/dist/stream/tool-calls.js.map +1 -1
  102. package/dist/stream/types.d.cts +43 -0
  103. package/dist/stream/types.d.cts.map +1 -1
  104. package/dist/stream/types.d.ts +43 -0
  105. package/dist/stream/types.d.ts.map +1 -1
  106. package/dist/ui/index.d.cts +1 -1
  107. package/dist/ui/index.d.ts +1 -1
  108. package/dist/ui/messages.cjs +4 -50
  109. package/dist/ui/messages.cjs.map +1 -1
  110. package/dist/ui/messages.d.cts.map +1 -1
  111. package/dist/ui/messages.d.ts.map +1 -1
  112. package/dist/ui/messages.js +3 -48
  113. package/dist/ui/messages.js.map +1 -1
  114. package/package.json +1 -1
@@ -69,6 +69,17 @@ var RootMessageProjection = class {
69
69
  #pendingValues = null;
70
70
  #flushScheduled = false;
71
71
  /**
72
+ * Highest checkpoint `step` whose `values` snapshot has been applied.
73
+ * Seeded by {@link StreamController.hydrate} from `getState()` and
74
+ * advanced by live `values` events. A snapshot arriving with a lower
75
+ * step is an older checkpoint replayed by the content pump on
76
+ * reconnect; it is reconciled in add-only mode so it cannot remove
77
+ * the seeded message tail (the final assistant turn). `undefined`
78
+ * until the first step-bearing snapshot, where the legacy
79
+ * remove-on-absence behavior is preserved.
80
+ */
81
+ #maxStep = void 0;
82
+ /**
72
83
  * @param params.messagesKey - Key inside `values` that holds the
73
84
  * message array.
74
85
  * @param params.store - Root snapshot store to mutate.
@@ -90,6 +101,7 @@ var RootMessageProjection = class {
90
101
  this.#pendingMessages = null;
91
102
  this.#pendingValues = null;
92
103
  this.#flushScheduled = false;
104
+ this.#maxStep = void 0;
93
105
  }
94
106
  /**
95
107
  * Record a `namespace → tool_call_id` mapping captured from a root
@@ -172,11 +184,17 @@ var RootMessageProjection = class {
172
184
  * @param nextValues - Full values snapshot from the `values` event.
173
185
  * @param nextMessages - The messages array extracted from
174
186
  * `values[messagesKey]` and coerced to `BaseMessage` instances.
187
+ * @param opts.step - Checkpoint superstep for this snapshot, when
188
+ * known. A snapshot whose step is below the highest applied step is
189
+ * treated as a stale reconnect replay and reconciled add-only.
175
190
  */
176
- applyValues(nextValues, nextMessages) {
191
+ applyValues(nextValues, nextMessages, opts) {
177
192
  const baselineSnapshot = this.#store.getSnapshot();
178
193
  const baselineMessages = this.#pendingMessages ?? baselineSnapshot.messages;
179
194
  const baselineValues = this.#pendingValues ?? baselineSnapshot.values;
195
+ const step = opts?.step;
196
+ const addOnly = step != null && this.#maxStep != null && step < this.#maxStep;
197
+ if (step != null && (this.#maxStep == null || step > this.#maxStep)) this.#maxStep = step;
180
198
  if (nextMessages.length === 0) {
181
199
  if (stateValuesShallowEqual(baselineValues, nextValues, this.#messagesKey)) return;
182
200
  this.#pendingValues = syncMessagesIntoValues(nextValues, this.#messagesKey, baselineMessages);
@@ -188,9 +206,10 @@ var RootMessageProjection = class {
188
206
  currentMessages: baselineMessages,
189
207
  currentIndexById: this.#indexById,
190
208
  previousValueMessageIds: this.#valuesMessageIds,
191
- preferValuesMessage: require_message_reconciliation.shouldPreferValuesMessageForToolCalls
209
+ preferValuesMessage: require_message_reconciliation.shouldPreferValuesMessageForToolCalls,
210
+ addOnly
192
211
  });
193
- this.#valuesMessageIds = reconciliation.valueMessageIds;
212
+ if (!addOnly) this.#valuesMessageIds = reconciliation.valueMessageIds;
194
213
  const messages = reconciliation.messages;
195
214
  const values = {
196
215
  ...nextValues,
@@ -204,6 +223,114 @@ var RootMessageProjection = class {
204
223
  this.#scheduleFlush();
205
224
  }
206
225
  /**
226
+ * Append messages applied optimistically by a local `submit()`,
227
+ * keyed by id so the eventual server echo reconciles cleanly.
228
+ *
229
+ * Unlike {@link applyValues}, the supplied messages are *not* treated
230
+ * as an authoritative ordered snapshot: they are appended to the end
231
+ * of the current projection (or replaced in place when the id already
232
+ * exists), preserving prior history ordering. When the server later
233
+ * emits a `values` snapshot containing the same ids,
234
+ * {@link applyValues} → {@link reconcileMessagesFromValues} takes over
235
+ * (server ordering wins, the echoed message replaces the optimistic
236
+ * one).
237
+ *
238
+ * Non-message input keys are shallow-merged into `values` via
239
+ * `extraValues`; they are dropped/overwritten automatically by the
240
+ * first server `values` event (which rebuilds `values` from the
241
+ * server snapshot), or rolled back via {@link restoreValueKeys} when
242
+ * the run fails before any echo.
243
+ *
244
+ * @param messages - Optimistic messages (already coerced to
245
+ * `BaseMessage` instances, each carrying a stable id).
246
+ * @param extraValues - Non-message input keys to shallow-merge into
247
+ * `values`.
248
+ */
249
+ appendOptimistic(messages, extraValues) {
250
+ let working = this.#pendingMessages ?? this.#store.getSnapshot().messages;
251
+ let mutated = false;
252
+ for (const message of messages) {
253
+ const id = message.id;
254
+ if (id == null) continue;
255
+ const existingIdx = this.#indexById.get(id);
256
+ if (existingIdx == null) {
257
+ if (!mutated) {
258
+ working = working.slice();
259
+ mutated = true;
260
+ }
261
+ this.#indexById.set(id, working.length);
262
+ working.push(message);
263
+ } else if (!require_message_reconciliation.messagesEqual(working[existingIdx], message)) {
264
+ if (!mutated) {
265
+ working = working.slice();
266
+ mutated = true;
267
+ }
268
+ working[existingIdx] = message;
269
+ }
270
+ }
271
+ const baselineValues = this.#pendingValues ?? this.#store.getSnapshot().values;
272
+ let values = baselineValues;
273
+ if (extraValues != null && Object.keys(extraValues).length > 0) values = {
274
+ ...baselineValues,
275
+ ...extraValues
276
+ };
277
+ values = syncMessagesIntoValues(values, this.#messagesKey, working);
278
+ if (!mutated && values === baselineValues) return;
279
+ this.#pendingMessages = working;
280
+ if (values !== baselineValues) this.#pendingValues = values;
281
+ this.#scheduleFlush();
282
+ }
283
+ /**
284
+ * Drop optimistic messages by id without disturbing the rest of the
285
+ * projection. Used by {@link StreamController.hydrate} to remove
286
+ * never-persisted optimistic messages (`pending` / `failed`) so a
287
+ * reload converges to server truth.
288
+ *
289
+ * @param ids - Message ids to remove.
290
+ */
291
+ dropOptimisticMessages(ids) {
292
+ if (ids.size === 0) return;
293
+ const baselineMessages = this.#pendingMessages ?? this.#store.getSnapshot().messages;
294
+ const next = baselineMessages.filter((m) => m.id == null || !ids.has(m.id));
295
+ if (next.length === baselineMessages.length) return;
296
+ this.#indexById.clear();
297
+ for (const [id, idx] of require_message_reconciliation.buildMessageIndex(next)) this.#indexById.set(id, idx);
298
+ const baselineValues = this.#pendingValues ?? this.#store.getSnapshot().values;
299
+ this.#pendingMessages = next;
300
+ this.#pendingValues = syncMessagesIntoValues(baselineValues, this.#messagesKey, next);
301
+ this.#scheduleFlush();
302
+ }
303
+ /**
304
+ * Restore (or delete) `values` keys that were optimistically merged
305
+ * by {@link appendOptimistic} but never echoed by the server — i.e.
306
+ * roll back non-message optimistic state when the run fails before
307
+ * any `values` event lands. Messages are left untouched (kept on
308
+ * failure per the optimistic contract).
309
+ *
310
+ * @param restore - Per-key pre-submit snapshot: when `hadKey` is
311
+ * false the key is deleted, otherwise it is reset to `prevValue`.
312
+ */
313
+ restoreValueKeys(restore) {
314
+ if (restore.length === 0) return;
315
+ const next = { ...this.#pendingValues ?? this.#store.getSnapshot().values };
316
+ let changed = false;
317
+ for (const { key, hadKey, prevValue } of restore) {
318
+ if (key === this.#messagesKey) continue;
319
+ if (hadKey) {
320
+ if (!Object.is(next[key], prevValue)) {
321
+ next[key] = prevValue;
322
+ changed = true;
323
+ }
324
+ } else if (Object.prototype.hasOwnProperty.call(next, key)) {
325
+ delete next[key];
326
+ changed = true;
327
+ }
328
+ }
329
+ if (!changed) return;
330
+ this.#pendingValues = next;
331
+ this.#scheduleFlush();
332
+ }
333
+ /**
207
334
  * Schedule a coalesced flush on the next macrotask. Idempotent
208
335
  * within a tick — multiple `handleMessage` / `applyValues` calls
209
336
  * before the flush fires collapse into one store write.
@@ -1 +1 @@
1
- {"version":3,"file":"root-message-projection.cjs","names":["#messagesKey","#store","MessageAssembler","#roles","#indexById","#toolCallIdByNamespace","#assembler","#valuesMessageIds","#pendingMessages","#pendingValues","#flushScheduled","namespaceKey","assembledMessageToBaseMessage","messagesEqual","#scheduleFlush","reconcileMessagesFromValues","shouldPreferValuesMessageForToolCalls","buildMessageIndex","#flushPending"],"sources":["../../src/stream/root-message-projection.ts"],"sourcesContent":["/**\n * Root-namespace message projection.\n *\n * # What this module is\n *\n * The {@link RootMessageProjection} is the piece of the\n * {@link StreamController} that owns \"what messages does the root\n * namespace currently contain?\". It assembles streamed message deltas\n * via {@link MessageAssembler}, reconciles them against authoritative\n * `values.messages` snapshots from the server, and writes the merged\n * list back into the controller's root snapshot store.\n *\n * # Two streams of truth\n *\n * Root messages arrive on two channels and need to merge cleanly:\n *\n * - **`messages` channel.** Token-level deltas that build messages\n * incrementally. The {@link MessageAssembler} keeps partial\n * messages by id and emits an updated `BaseMessage` per delta.\n * - **`values` channel.** Periodic full-state snapshots that include\n * the authoritative messages array. Used for ordering, removals,\n * and forks (where the streamed messages may pre-date the new\n * timeline).\n *\n * The reconciliation rules (delegated to\n * {@link reconcileMessagesFromValues}) preserve in-flight streamed\n * content while letting values dictate ordering and removals.\n *\n * # Tool-message namespace correlation\n *\n * Tool messages arrive on `messages-start` events with `role: \"tool\"`\n * but the start event doesn't always include a `tool_call_id`. We\n * recover it via three fallbacks:\n *\n * 1. The start event itself, when the server includes it.\n * 2. The legacy `<id>-tool-<call_id>` message id format.\n * 3. The most recent `tool-started` event recorded under the same\n * namespace via {@link recordToolCallNamespace}.\n *\n * Without this correlation, tool messages render with empty\n * `tool_call_id` and downstream UIs can't pair them with the\n * originating tool call.\n *\n * # Store-write batching\n *\n * Every {@link handleMessage} / {@link applyValues} call updates the\n * in-projection bookkeeping (assembler state, id index, role cache)\n * synchronously, then stages the new `messages` / `values` into a\n * pending buffer and schedules a `setTimeout(0)` flush. A single\n * coalesced `store.setState` runs at the next macrotask boundary.\n *\n * The motivation is the long-replay freeze: a thread with hundreds\n * of messages replays through the `messages` channel on refresh or\n * mid-run join. Those events drain through the controller's\n * `for await` pump as a long microtask chain. Per-event\n * `store.setState` notifies `useSyncExternalStore` per event, and\n * after enough notifications React's `nestedUpdateCount` guard trips\n * with \"Maximum update depth exceeded\", permanently freezing the UI\n * on the first few messages. Coalescing to one notification per\n * macrotask lets React's scheduler commit between flushes.\n *\n * # Lifecycle\n *\n * - `handleMessage(event)` — apply a `messages` event delta.\n * - `applyValues(values, msgs)` — merge a `values` snapshot.\n * - `recordToolCallNamespace(ns, id)` — capture `namespace → tool_call_id`\n * so subsequent tool message starts can recover the id.\n * - `reset()` — clear all state on thread rebind.\n */\nimport type {\n MessagesEvent,\n MessageRole,\n MessageStartData,\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 { StreamStore } from \"./store.js\";\nimport type { RootSnapshot } from \"./types.js\";\nimport { namespaceKey } from \"./namespace.js\";\nimport {\n buildMessageIndex,\n messagesEqual,\n reconcileMessagesFromValues,\n shouldPreferValuesMessageForToolCalls,\n} from \"./message-reconciliation.js\";\n\n/**\n * Root-namespace message projection. Owns the merge between the\n * `messages` (streamed deltas) and `values` (authoritative\n * snapshots) channels for the root namespace.\n *\n * @typeParam StateType - Root state shape; the messages array is read\n * from `values[messagesKey]`.\n * @typeParam InterruptType - Shape of root protocol interrupts (forwarded\n * into `RootSnapshot` updates).\n */\nexport class RootMessageProjection<\n StateType extends object,\n InterruptType = unknown,\n> {\n /**\n * Key inside `values` that holds the message array. Defaults to\n * `\"messages\"` in the controller; configurable for state graphs\n * that surface messages under a different slot.\n */\n readonly #messagesKey: string;\n\n /** Root snapshot store written to on every merge. */\n readonly #store: StreamStore<RootSnapshot<StateType, InterruptType>>;\n\n /**\n * Stateful chunk assembler for in-flight messages. Reset (via a\n * fresh instance) on every {@link reset} so a new thread starts\n * with no half-built messages from the previous one.\n */\n #assembler = new MessageAssembler();\n\n /**\n * `messageId → role/toolCallId` captured from `message-start` events.\n * The assembler's intermediate output drops these fields, so we cache\n * them at start-time and reapply when projecting to a `BaseMessage`.\n */\n readonly #roles = new Map<\n string,\n { role: ExtendedMessageRole; toolCallId?: string }\n >();\n\n /**\n * `messageId → position in #store.messages` for fast in-place\n * updates as deltas arrive. Rebuilt on every full reconciliation\n * driven by a `values` event.\n */\n readonly #indexById = new Map<string, number>();\n\n /**\n * Ids observed in the most recent `values.messages` snapshot.\n * Reconciliation uses this to detect server-side removals: a\n * previously-seen id missing from the next snapshot means it was\n * removed by the server (and should drop from the projection).\n */\n #valuesMessageIds = new Set<string>();\n\n /**\n * `namespaceKey → tool_call_id` captured from root `tool-started`\n * events. Used as a fallback when a tool-role `message-start` is\n * missing its `tool_call_id` field.\n */\n readonly #toolCallIdByNamespace = new Map<string, string>();\n\n /**\n * Coalescing buffer for store writes. {@link handleMessage} and\n * {@link applyValues} stage their computed `messages` / `values`\n * here instead of calling `store.setState` per event. A single\n * `setTimeout(0)` flush commits them in one `setState`, so a\n * burst of SSE events draining as a microtask chain becomes one\n * store notification at the next macrotask boundary.\n *\n * `null` means \"no staged write\" — once a flush settles, the\n * slots are cleared so the next call starts from the latest\n * committed store snapshot.\n */\n #pendingMessages: BaseMessage[] | null = null;\n #pendingValues: StateType | null = null;\n #flushScheduled = false;\n\n /**\n * @param params.messagesKey - Key inside `values` that holds the\n * message array.\n * @param params.store - Root snapshot store to mutate.\n */\n constructor(params: {\n messagesKey: string;\n store: StreamStore<RootSnapshot<StateType, InterruptType>>;\n }) {\n this.#messagesKey = params.messagesKey;\n this.#store = params.store;\n }\n\n /**\n * Drop all per-thread state. Called by the controller on thread\n * rebind / dispose so a swap doesn't surface stale messages.\n */\n reset(): void {\n this.#assembler = new MessageAssembler();\n this.#roles.clear();\n this.#indexById.clear();\n this.#valuesMessageIds = new Set();\n this.#toolCallIdByNamespace.clear();\n // Drop any unflushed pending writes — they were computed against\n // the previous thread's baseline and committing them after a\n // rebind would bleed stale messages into the new thread.\n this.#pendingMessages = null;\n this.#pendingValues = null;\n this.#flushScheduled = false;\n }\n\n /**\n * Record a `namespace → tool_call_id` mapping captured from a root\n * `tool-started` event.\n *\n * The companion tool-role `message-start` event may not carry a\n * `tool_call_id`, so we fall back to the most recent value recorded\n * here for the same namespace.\n *\n * @param namespace - Event namespace from the `tool-started` event.\n * @param toolCallId - Tool call id from the same event.\n */\n recordToolCallNamespace(\n namespace: readonly string[],\n toolCallId: string\n ): void {\n this.#toolCallIdByNamespace.set(namespaceKey(namespace), toolCallId);\n }\n\n /**\n * Apply a `messages` channel event to the projection.\n *\n * Captures role/tool metadata on `message-start`, feeds the chunk\n * to the assembler, projects the assembled output to a\n * {@link BaseMessage}, and either appends or in-place updates the\n * pending messages buffer based on whether the id was seen before.\n *\n * @param event - The `messages` channel event to consume.\n */\n handleMessage(event: MessagesEvent): void {\n const data = event.params.data;\n if (data.event === \"message-start\") {\n const startData = data as MessageStartData;\n const role = (startData.role ?? \"ai\") as MessageRole;\n const extendedRole =\n (startData as { role?: ExtendedMessageRole }).role ?? role;\n let toolCallId = (startData as { tool_call_id?: string }).tool_call_id;\n // Tool messages need a tool_call_id to render. Fall back through:\n // 1. legacy `<id>-tool-<call_id>` message id format\n // 2. namespace-recorded tool_call_id (from #recordToolCallNamespace)\n if (extendedRole === \"tool\" && toolCallId == null) {\n const messageId = startData.id;\n if (messageId != null) {\n const match = /-tool-(.+)$/.exec(messageId);\n if (match != null) toolCallId = match[1];\n }\n if (toolCallId == null) {\n toolCallId = this.#toolCallIdByNamespace.get(\n namespaceKey(event.params.namespace)\n );\n }\n }\n if (startData.id != null) {\n this.#roles.set(startData.id, {\n role: extendedRole,\n toolCallId,\n });\n }\n }\n\n const update = this.#assembler.consume(event);\n if (update == null) return;\n const id = update.message.id;\n if (id == null) return;\n const captured = this.#roles.get(id) ?? { role: \"ai\" as const };\n const base = assembledMessageToBaseMessage(update.message, captured.role, {\n toolCallId: captured.toolCallId,\n });\n\n // Compute against the pending baseline if we have one (so an\n // earlier handleMessage in the same tick is the input to this\n // one), else against the latest committed store snapshot.\n // `#indexById` is the synchronous source of truth for \"where is\n // each id in the current messages list\" — every code path below\n // keeps it in sync before returning.\n const baselineMessages =\n this.#pendingMessages ?? this.#store.getSnapshot().messages;\n const existingIdx = this.#indexById.get(id);\n let messages: BaseMessage[];\n if (existingIdx == null) {\n this.#indexById.set(id, baselineMessages.length);\n messages = [...baselineMessages, base];\n } else if (messagesEqual(baselineMessages[existingIdx], base)) {\n // Identical re-emission — skip the store write to keep\n // snapshot identity stable.\n return;\n } else {\n messages = baselineMessages.slice();\n messages[existingIdx] = base;\n }\n\n // Mirror the new messages list into `values[messagesKey]` so\n // direct `values` reads (used by some hooks and by the eventual\n // `values` reconciliation) stay in sync.\n const baselineValues =\n this.#pendingValues ?? this.#store.getSnapshot().values;\n const values = syncMessagesIntoValues(\n baselineValues,\n this.#messagesKey,\n messages\n );\n this.#pendingMessages = messages;\n if (values !== baselineValues) this.#pendingValues = values;\n this.#scheduleFlush();\n }\n\n /**\n * Reconcile a full `values` snapshot into the projection.\n *\n * Delegates the merge to {@link reconcileMessagesFromValues}:\n * values stays authoritative for ordering and removals, while\n * streamed in-flight messages keep their content until the server\n * echoes them back. Empty messages just refresh the values blob.\n *\n * Rebuilds {@link #indexById} after the merge so subsequent delta\n * applications target the new positions.\n *\n * @param nextValues - Full values snapshot from the `values` event.\n * @param nextMessages - The messages array extracted from\n * `values[messagesKey]` and coerced to `BaseMessage` instances.\n */\n applyValues(nextValues: StateType, nextMessages: BaseMessage[]): void {\n const baselineSnapshot = this.#store.getSnapshot();\n const baselineMessages = this.#pendingMessages ?? baselineSnapshot.messages;\n const baselineValues = this.#pendingValues ?? baselineSnapshot.values;\n\n if (nextMessages.length === 0) {\n if (\n stateValuesShallowEqual(baselineValues, nextValues, this.#messagesKey)\n ) {\n return;\n }\n // Mirror the current `messages` list back into the values slot\n // so the staged snapshot stays consistent with the (separately\n // tracked) messages array.\n this.#pendingValues = syncMessagesIntoValues(\n nextValues,\n this.#messagesKey,\n baselineMessages\n );\n this.#scheduleFlush();\n return;\n }\n\n const reconciliation = reconcileMessagesFromValues({\n valueMessages: nextMessages,\n currentMessages: baselineMessages,\n currentIndexById: this.#indexById,\n previousValueMessageIds: this.#valuesMessageIds,\n preferValuesMessage: shouldPreferValuesMessageForToolCalls,\n });\n this.#valuesMessageIds = reconciliation.valueMessageIds;\n const messages = reconciliation.messages as BaseMessage[];\n const values = {\n ...(nextValues as Record<string, unknown>),\n [this.#messagesKey]: messages,\n } as StateType;\n if (\n messages === baselineMessages &&\n stateValuesShallowEqual(baselineValues, values, this.#messagesKey)\n ) {\n return;\n }\n\n // Reconciliation may reorder, drop, or substitute messages, so\n // rebuild the id → index map to match the new array.\n this.#indexById.clear();\n for (const [id, idx] of buildMessageIndex(messages)) {\n this.#indexById.set(id, idx);\n }\n this.#pendingMessages = messages;\n this.#pendingValues = values;\n this.#scheduleFlush();\n }\n\n /**\n * Schedule a coalesced flush on the next macrotask. Idempotent\n * within a tick — multiple `handleMessage` / `applyValues` calls\n * before the flush fires collapse into one store write.\n *\n * `setTimeout(0)` is a macrotask: it runs after the current\n * microtask chain drains, so a burst of SSE events processed by\n * the controller's `for await` pump becomes one `store.setState`\n * (and therefore one `useSyncExternalStore` notification).\n */\n #scheduleFlush = (): void => {\n if (this.#flushScheduled) return;\n this.#flushScheduled = true;\n setTimeout(this.#flushPending, 0);\n };\n\n /**\n * Drain `#pendingMessages` / `#pendingValues` to the store in a\n * single `setState` call.\n */\n #flushPending = (): void => {\n this.#flushScheduled = false;\n const messages = this.#pendingMessages;\n const values = this.#pendingValues;\n this.#pendingMessages = null;\n this.#pendingValues = null;\n if (messages == null && values == null) return;\n this.#store.setState((s) => {\n // Other rootStore mutators (controller-driven `isLoading`,\n // `interrupts`, `toolCalls`, etc.) do not touch `s.messages`\n // / `s.values`, so a last-write-wins commit on those two\n // fields is safe.\n if (messages == null) {\n return values == null ? s : { ...s, values };\n }\n if (values == null) return { ...s, messages };\n return { ...s, messages, values };\n });\n };\n}\n\n/**\n * Mirror a freshly-updated message list into `values[messagesKey]`.\n *\n * Returns the same `values` reference when the list is already\n * equal-by-content so the caller can keep the existing snapshot\n * identity (and avoid spurious `setSnapshot` notifications).\n */\nfunction syncMessagesIntoValues<StateType extends object>(\n values: StateType,\n messagesKey: string,\n messages: BaseMessage[]\n): StateType {\n const record = values as Record<string, unknown>;\n const current = record[messagesKey];\n if (Array.isArray(current) && messagesEqualList(current, messages)) {\n return values;\n }\n return {\n ...record,\n [messagesKey]: messages,\n } as StateType;\n}\n\n/**\n * True when two `BaseMessage` arrays carry the same per-message\n * content (using {@link messagesEqual}).\n */\nfunction messagesEqualList(\n previous: readonly BaseMessage[],\n next: readonly BaseMessage[]\n): boolean {\n if (previous === next) return true;\n if (previous.length !== next.length) return false;\n for (let i = 0; i < previous.length; i += 1) {\n if (!messagesEqual(previous[i], next[i])) return false;\n }\n return true;\n}\n\n/**\n * Shallow-equal for `values` objects, *ignoring* the messages slot.\n *\n * The messages array is compared separately by the caller (via\n * {@link messagesEqualList}) because both arrays contain class\n * instances whose JSON representation is not stable across reads.\n */\nfunction stateValuesShallowEqual(\n previous: object,\n next: object,\n messagesKey: string\n): boolean {\n if (previous === next) return true;\n const previousRecord = previous as Record<string, unknown>;\n const nextRecord = next as Record<string, unknown>;\n const previousKeys = Object.keys(previousRecord);\n const nextKeys = Object.keys(nextRecord);\n if (previousKeys.length !== nextKeys.length) return false;\n for (const key of previousKeys) {\n if (!Object.prototype.hasOwnProperty.call(nextRecord, key)) return false;\n const previousValue = previousRecord[key];\n const nextValue = nextRecord[key];\n if (\n key === messagesKey &&\n Array.isArray(previousValue) &&\n Array.isArray(nextValue)\n ) {\n continue;\n }\n if (!Object.is(previousValue, nextValue)) return false;\n }\n return true;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"root-message-projection.cjs","names":["#messagesKey","#store","MessageAssembler","#roles","#indexById","#toolCallIdByNamespace","#assembler","#valuesMessageIds","#pendingMessages","#pendingValues","#flushScheduled","#maxStep","namespaceKey","assembledMessageToBaseMessage","messagesEqual","#scheduleFlush","reconcileMessagesFromValues","shouldPreferValuesMessageForToolCalls","buildMessageIndex","#flushPending"],"sources":["../../src/stream/root-message-projection.ts"],"sourcesContent":["/**\n * Root-namespace message projection.\n *\n * # What this module is\n *\n * The {@link RootMessageProjection} is the piece of the\n * {@link StreamController} that owns \"what messages does the root\n * namespace currently contain?\". It assembles streamed message deltas\n * via {@link MessageAssembler}, reconciles them against authoritative\n * `values.messages` snapshots from the server, and writes the merged\n * list back into the controller's root snapshot store.\n *\n * # Two streams of truth\n *\n * Root messages arrive on two channels and need to merge cleanly:\n *\n * - **`messages` channel.** Token-level deltas that build messages\n * incrementally. The {@link MessageAssembler} keeps partial\n * messages by id and emits an updated `BaseMessage` per delta.\n * - **`values` channel.** Periodic full-state snapshots that include\n * the authoritative messages array. Used for ordering, removals,\n * and forks (where the streamed messages may pre-date the new\n * timeline).\n *\n * The reconciliation rules (delegated to\n * {@link reconcileMessagesFromValues}) preserve in-flight streamed\n * content while letting values dictate ordering and removals.\n *\n * # Tool-message namespace correlation\n *\n * Tool messages arrive on `messages-start` events with `role: \"tool\"`\n * but the start event doesn't always include a `tool_call_id`. We\n * recover it via three fallbacks:\n *\n * 1. The start event itself, when the server includes it.\n * 2. The legacy `<id>-tool-<call_id>` message id format.\n * 3. The most recent `tool-started` event recorded under the same\n * namespace via {@link recordToolCallNamespace}.\n *\n * Without this correlation, tool messages render with empty\n * `tool_call_id` and downstream UIs can't pair them with the\n * originating tool call.\n *\n * # Store-write batching\n *\n * Every {@link handleMessage} / {@link applyValues} call updates the\n * in-projection bookkeeping (assembler state, id index, role cache)\n * synchronously, then stages the new `messages` / `values` into a\n * pending buffer and schedules a `setTimeout(0)` flush. A single\n * coalesced `store.setState` runs at the next macrotask boundary.\n *\n * The motivation is the long-replay freeze: a thread with hundreds\n * of messages replays through the `messages` channel on refresh or\n * mid-run join. Those events drain through the controller's\n * `for await` pump as a long microtask chain. Per-event\n * `store.setState` notifies `useSyncExternalStore` per event, and\n * after enough notifications React's `nestedUpdateCount` guard trips\n * with \"Maximum update depth exceeded\", permanently freezing the UI\n * on the first few messages. Coalescing to one notification per\n * macrotask lets React's scheduler commit between flushes.\n *\n * # Lifecycle\n *\n * - `handleMessage(event)` — apply a `messages` event delta.\n * - `applyValues(values, msgs)` — merge a `values` snapshot.\n * - `recordToolCallNamespace(ns, id)` — capture `namespace → tool_call_id`\n * so subsequent tool message starts can recover the id.\n * - `reset()` — clear all state on thread rebind.\n */\nimport type {\n MessagesEvent,\n MessageRole,\n MessageStartData,\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 { StreamStore } from \"./store.js\";\nimport type { RootSnapshot } from \"./types.js\";\nimport { namespaceKey } from \"./namespace.js\";\nimport {\n buildMessageIndex,\n messagesEqual,\n reconcileMessagesFromValues,\n shouldPreferValuesMessageForToolCalls,\n} from \"./message-reconciliation.js\";\n\n/**\n * Root-namespace message projection. Owns the merge between the\n * `messages` (streamed deltas) and `values` (authoritative\n * snapshots) channels for the root namespace.\n *\n * @typeParam StateType - Root state shape; the messages array is read\n * from `values[messagesKey]`.\n * @typeParam InterruptType - Shape of root protocol interrupts (forwarded\n * into `RootSnapshot` updates).\n */\nexport class RootMessageProjection<\n StateType extends object,\n InterruptType = unknown,\n> {\n /**\n * Key inside `values` that holds the message array. Defaults to\n * `\"messages\"` in the controller; configurable for state graphs\n * that surface messages under a different slot.\n */\n readonly #messagesKey: string;\n\n /** Root snapshot store written to on every merge. */\n readonly #store: StreamStore<RootSnapshot<StateType, InterruptType>>;\n\n /**\n * Stateful chunk assembler for in-flight messages. Reset (via a\n * fresh instance) on every {@link reset} so a new thread starts\n * with no half-built messages from the previous one.\n */\n #assembler = new MessageAssembler();\n\n /**\n * `messageId → role/toolCallId` captured from `message-start` events.\n * The assembler's intermediate output drops these fields, so we cache\n * them at start-time and reapply when projecting to a `BaseMessage`.\n */\n readonly #roles = new Map<\n string,\n { role: ExtendedMessageRole; toolCallId?: string }\n >();\n\n /**\n * `messageId → position in #store.messages` for fast in-place\n * updates as deltas arrive. Rebuilt on every full reconciliation\n * driven by a `values` event.\n */\n readonly #indexById = new Map<string, number>();\n\n /**\n * Ids observed in the most recent `values.messages` snapshot.\n * Reconciliation uses this to detect server-side removals: a\n * previously-seen id missing from the next snapshot means it was\n * removed by the server (and should drop from the projection).\n */\n #valuesMessageIds = new Set<string>();\n\n /**\n * `namespaceKey → tool_call_id` captured from root `tool-started`\n * events. Used as a fallback when a tool-role `message-start` is\n * missing its `tool_call_id` field.\n */\n readonly #toolCallIdByNamespace = new Map<string, string>();\n\n /**\n * Coalescing buffer for store writes. {@link handleMessage} and\n * {@link applyValues} stage their computed `messages` / `values`\n * here instead of calling `store.setState` per event. A single\n * `setTimeout(0)` flush commits them in one `setState`, so a\n * burst of SSE events draining as a microtask chain becomes one\n * store notification at the next macrotask boundary.\n *\n * `null` means \"no staged write\" — once a flush settles, the\n * slots are cleared so the next call starts from the latest\n * committed store snapshot.\n */\n #pendingMessages: BaseMessage[] | null = null;\n #pendingValues: StateType | null = null;\n #flushScheduled = false;\n\n /**\n * Highest checkpoint `step` whose `values` snapshot has been applied.\n * Seeded by {@link StreamController.hydrate} from `getState()` and\n * advanced by live `values` events. A snapshot arriving with a lower\n * step is an older checkpoint replayed by the content pump on\n * reconnect; it is reconciled in add-only mode so it cannot remove\n * the seeded message tail (the final assistant turn). `undefined`\n * until the first step-bearing snapshot, where the legacy\n * remove-on-absence behavior is preserved.\n */\n #maxStep: number | undefined = undefined;\n\n /**\n * @param params.messagesKey - Key inside `values` that holds the\n * message array.\n * @param params.store - Root snapshot store to mutate.\n */\n constructor(params: {\n messagesKey: string;\n store: StreamStore<RootSnapshot<StateType, InterruptType>>;\n }) {\n this.#messagesKey = params.messagesKey;\n this.#store = params.store;\n }\n\n /**\n * Drop all per-thread state. Called by the controller on thread\n * rebind / dispose so a swap doesn't surface stale messages.\n */\n reset(): void {\n this.#assembler = new MessageAssembler();\n this.#roles.clear();\n this.#indexById.clear();\n this.#valuesMessageIds = new Set();\n this.#toolCallIdByNamespace.clear();\n // Drop any unflushed pending writes — they were computed against\n // the previous thread's baseline and committing them after a\n // rebind would bleed stale messages into the new thread.\n this.#pendingMessages = null;\n this.#pendingValues = null;\n this.#flushScheduled = false;\n this.#maxStep = undefined;\n }\n\n /**\n * Record a `namespace → tool_call_id` mapping captured from a root\n * `tool-started` event.\n *\n * The companion tool-role `message-start` event may not carry a\n * `tool_call_id`, so we fall back to the most recent value recorded\n * here for the same namespace.\n *\n * @param namespace - Event namespace from the `tool-started` event.\n * @param toolCallId - Tool call id from the same event.\n */\n recordToolCallNamespace(\n namespace: readonly string[],\n toolCallId: string\n ): void {\n this.#toolCallIdByNamespace.set(namespaceKey(namespace), toolCallId);\n }\n\n /**\n * Apply a `messages` channel event to the projection.\n *\n * Captures role/tool metadata on `message-start`, feeds the chunk\n * to the assembler, projects the assembled output to a\n * {@link BaseMessage}, and either appends or in-place updates the\n * pending messages buffer based on whether the id was seen before.\n *\n * @param event - The `messages` channel event to consume.\n */\n handleMessage(event: MessagesEvent): void {\n const data = event.params.data;\n if (data.event === \"message-start\") {\n const startData = data as MessageStartData;\n const role = (startData.role ?? \"ai\") as MessageRole;\n const extendedRole =\n (startData as { role?: ExtendedMessageRole }).role ?? role;\n let toolCallId = (startData as { tool_call_id?: string }).tool_call_id;\n // Tool messages need a tool_call_id to render. Fall back through:\n // 1. legacy `<id>-tool-<call_id>` message id format\n // 2. namespace-recorded tool_call_id (from #recordToolCallNamespace)\n if (extendedRole === \"tool\" && toolCallId == null) {\n const messageId = startData.id;\n if (messageId != null) {\n const match = /-tool-(.+)$/.exec(messageId);\n if (match != null) toolCallId = match[1];\n }\n if (toolCallId == null) {\n toolCallId = this.#toolCallIdByNamespace.get(\n namespaceKey(event.params.namespace)\n );\n }\n }\n if (startData.id != null) {\n this.#roles.set(startData.id, {\n role: extendedRole,\n toolCallId,\n });\n }\n }\n\n const update = this.#assembler.consume(event);\n if (update == null) return;\n const id = update.message.id;\n if (id == null) return;\n const captured = this.#roles.get(id) ?? { role: \"ai\" as const };\n const base = assembledMessageToBaseMessage(update.message, captured.role, {\n toolCallId: captured.toolCallId,\n });\n\n // Compute against the pending baseline if we have one (so an\n // earlier handleMessage in the same tick is the input to this\n // one), else against the latest committed store snapshot.\n // `#indexById` is the synchronous source of truth for \"where is\n // each id in the current messages list\" — every code path below\n // keeps it in sync before returning.\n const baselineMessages =\n this.#pendingMessages ?? this.#store.getSnapshot().messages;\n const existingIdx = this.#indexById.get(id);\n let messages: BaseMessage[];\n if (existingIdx == null) {\n this.#indexById.set(id, baselineMessages.length);\n messages = [...baselineMessages, base];\n } else if (messagesEqual(baselineMessages[existingIdx], base)) {\n // Identical re-emission — skip the store write to keep\n // snapshot identity stable.\n return;\n } else {\n messages = baselineMessages.slice();\n messages[existingIdx] = base;\n }\n\n // Mirror the new messages list into `values[messagesKey]` so\n // direct `values` reads (used by some hooks and by the eventual\n // `values` reconciliation) stay in sync.\n const baselineValues =\n this.#pendingValues ?? this.#store.getSnapshot().values;\n const values = syncMessagesIntoValues(\n baselineValues,\n this.#messagesKey,\n messages\n );\n this.#pendingMessages = messages;\n if (values !== baselineValues) this.#pendingValues = values;\n this.#scheduleFlush();\n }\n\n /**\n * Reconcile a full `values` snapshot into the projection.\n *\n * Delegates the merge to {@link reconcileMessagesFromValues}:\n * values stays authoritative for ordering and removals, while\n * streamed in-flight messages keep their content until the server\n * echoes them back. Empty messages just refresh the values blob.\n *\n * Rebuilds {@link #indexById} after the merge so subsequent delta\n * applications target the new positions.\n *\n * @param nextValues - Full values snapshot from the `values` event.\n * @param nextMessages - The messages array extracted from\n * `values[messagesKey]` and coerced to `BaseMessage` instances.\n * @param opts.step - Checkpoint superstep for this snapshot, when\n * known. A snapshot whose step is below the highest applied step is\n * treated as a stale reconnect replay and reconciled add-only.\n */\n applyValues(\n nextValues: StateType,\n nextMessages: BaseMessage[],\n opts?: { step?: number }\n ): void {\n const baselineSnapshot = this.#store.getSnapshot();\n const baselineMessages = this.#pendingMessages ?? baselineSnapshot.messages;\n const baselineValues = this.#pendingValues ?? baselineSnapshot.values;\n\n const step = opts?.step;\n // Stale only when we have both a prior high-water step and a lower\n // incoming step. A missing step preserves the legacy semantics.\n const addOnly =\n step != null && this.#maxStep != null && step < this.#maxStep;\n if (step != null && (this.#maxStep == null || step > this.#maxStep)) {\n this.#maxStep = step;\n }\n\n if (nextMessages.length === 0) {\n if (\n stateValuesShallowEqual(baselineValues, nextValues, this.#messagesKey)\n ) {\n return;\n }\n // Mirror the current `messages` list back into the values slot\n // so the staged snapshot stays consistent with the (separately\n // tracked) messages array.\n this.#pendingValues = syncMessagesIntoValues(\n nextValues,\n this.#messagesKey,\n baselineMessages\n );\n this.#scheduleFlush();\n return;\n }\n\n const reconciliation = reconcileMessagesFromValues({\n valueMessages: nextMessages,\n currentMessages: baselineMessages,\n currentIndexById: this.#indexById,\n previousValueMessageIds: this.#valuesMessageIds,\n preferValuesMessage: shouldPreferValuesMessageForToolCalls,\n addOnly,\n });\n // A stale replay snapshot must not shrink the authoritative id set:\n // keep the (larger) seeded set so a genuinely-newer removal is still\n // detected once the timeline advances past the seed.\n if (!addOnly) this.#valuesMessageIds = reconciliation.valueMessageIds;\n const messages = reconciliation.messages as BaseMessage[];\n const values = {\n ...(nextValues as Record<string, unknown>),\n [this.#messagesKey]: messages,\n } as StateType;\n if (\n messages === baselineMessages &&\n stateValuesShallowEqual(baselineValues, values, this.#messagesKey)\n ) {\n return;\n }\n\n // Reconciliation may reorder, drop, or substitute messages, so\n // rebuild the id → index map to match the new array.\n this.#indexById.clear();\n for (const [id, idx] of buildMessageIndex(messages)) {\n this.#indexById.set(id, idx);\n }\n this.#pendingMessages = messages;\n this.#pendingValues = values;\n this.#scheduleFlush();\n }\n\n /**\n * Append messages applied optimistically by a local `submit()`,\n * keyed by id so the eventual server echo reconciles cleanly.\n *\n * Unlike {@link applyValues}, the supplied messages are *not* treated\n * as an authoritative ordered snapshot: they are appended to the end\n * of the current projection (or replaced in place when the id already\n * exists), preserving prior history ordering. When the server later\n * emits a `values` snapshot containing the same ids,\n * {@link applyValues} → {@link reconcileMessagesFromValues} takes over\n * (server ordering wins, the echoed message replaces the optimistic\n * one).\n *\n * Non-message input keys are shallow-merged into `values` via\n * `extraValues`; they are dropped/overwritten automatically by the\n * first server `values` event (which rebuilds `values` from the\n * server snapshot), or rolled back via {@link restoreValueKeys} when\n * the run fails before any echo.\n *\n * @param messages - Optimistic messages (already coerced to\n * `BaseMessage` instances, each carrying a stable id).\n * @param extraValues - Non-message input keys to shallow-merge into\n * `values`.\n */\n appendOptimistic(\n messages: BaseMessage[],\n extraValues?: Record<string, unknown>\n ): void {\n let working = this.#pendingMessages ?? this.#store.getSnapshot().messages;\n let mutated = false;\n for (const message of messages) {\n const id = message.id;\n if (id == null) continue;\n const existingIdx = this.#indexById.get(id);\n if (existingIdx == null) {\n if (!mutated) {\n working = working.slice();\n mutated = true;\n }\n this.#indexById.set(id, working.length);\n working.push(message);\n } else if (!messagesEqual(working[existingIdx], message)) {\n if (!mutated) {\n working = working.slice();\n mutated = true;\n }\n working[existingIdx] = message;\n }\n }\n\n const baselineValues =\n this.#pendingValues ?? this.#store.getSnapshot().values;\n let values = baselineValues;\n if (extraValues != null && Object.keys(extraValues).length > 0) {\n values = { ...(baselineValues as object), ...extraValues } as StateType;\n }\n values = syncMessagesIntoValues(values, this.#messagesKey, working);\n if (!mutated && values === baselineValues) return;\n this.#pendingMessages = working;\n if (values !== baselineValues) this.#pendingValues = values;\n this.#scheduleFlush();\n }\n\n /**\n * Drop optimistic messages by id without disturbing the rest of the\n * projection. Used by {@link StreamController.hydrate} to remove\n * never-persisted optimistic messages (`pending` / `failed`) so a\n * reload converges to server truth.\n *\n * @param ids - Message ids to remove.\n */\n dropOptimisticMessages(ids: ReadonlySet<string>): void {\n if (ids.size === 0) return;\n const baselineMessages =\n this.#pendingMessages ?? this.#store.getSnapshot().messages;\n const next = baselineMessages.filter((m) => m.id == null || !ids.has(m.id));\n if (next.length === baselineMessages.length) return;\n this.#indexById.clear();\n for (const [id, idx] of buildMessageIndex(next)) {\n this.#indexById.set(id, idx);\n }\n const baselineValues =\n this.#pendingValues ?? this.#store.getSnapshot().values;\n this.#pendingMessages = next;\n this.#pendingValues = syncMessagesIntoValues(\n baselineValues,\n this.#messagesKey,\n next\n );\n this.#scheduleFlush();\n }\n\n /**\n * Restore (or delete) `values` keys that were optimistically merged\n * by {@link appendOptimistic} but never echoed by the server — i.e.\n * roll back non-message optimistic state when the run fails before\n * any `values` event lands. Messages are left untouched (kept on\n * failure per the optimistic contract).\n *\n * @param restore - Per-key pre-submit snapshot: when `hadKey` is\n * false the key is deleted, otherwise it is reset to `prevValue`.\n */\n restoreValueKeys(\n restore: ReadonlyArray<{\n key: string;\n hadKey: boolean;\n prevValue: unknown;\n }>\n ): void {\n if (restore.length === 0) return;\n const baselineValues =\n this.#pendingValues ?? this.#store.getSnapshot().values;\n const next = { ...(baselineValues as Record<string, unknown>) };\n let changed = false;\n for (const { key, hadKey, prevValue } of restore) {\n if (key === this.#messagesKey) continue;\n if (hadKey) {\n if (!Object.is(next[key], prevValue)) {\n next[key] = prevValue;\n changed = true;\n }\n } else if (Object.prototype.hasOwnProperty.call(next, key)) {\n delete next[key];\n changed = true;\n }\n }\n if (!changed) return;\n this.#pendingValues = next as StateType;\n this.#scheduleFlush();\n }\n\n /**\n * Schedule a coalesced flush on the next macrotask. Idempotent\n * within a tick — multiple `handleMessage` / `applyValues` calls\n * before the flush fires collapse into one store write.\n *\n * `setTimeout(0)` is a macrotask: it runs after the current\n * microtask chain drains, so a burst of SSE events processed by\n * the controller's `for await` pump becomes one `store.setState`\n * (and therefore one `useSyncExternalStore` notification).\n */\n #scheduleFlush = (): void => {\n if (this.#flushScheduled) return;\n this.#flushScheduled = true;\n setTimeout(this.#flushPending, 0);\n };\n\n /**\n * Drain `#pendingMessages` / `#pendingValues` to the store in a\n * single `setState` call.\n */\n #flushPending = (): void => {\n this.#flushScheduled = false;\n const messages = this.#pendingMessages;\n const values = this.#pendingValues;\n this.#pendingMessages = null;\n this.#pendingValues = null;\n if (messages == null && values == null) return;\n this.#store.setState((s) => {\n // Other rootStore mutators (controller-driven `isLoading`,\n // `interrupts`, `toolCalls`, etc.) do not touch `s.messages`\n // / `s.values`, so a last-write-wins commit on those two\n // fields is safe.\n if (messages == null) {\n return values == null ? s : { ...s, values };\n }\n if (values == null) return { ...s, messages };\n return { ...s, messages, values };\n });\n };\n}\n\n/**\n * Mirror a freshly-updated message list into `values[messagesKey]`.\n *\n * Returns the same `values` reference when the list is already\n * equal-by-content so the caller can keep the existing snapshot\n * identity (and avoid spurious `setSnapshot` notifications).\n */\nfunction syncMessagesIntoValues<StateType extends object>(\n values: StateType,\n messagesKey: string,\n messages: BaseMessage[]\n): StateType {\n const record = values as Record<string, unknown>;\n const current = record[messagesKey];\n if (Array.isArray(current) && messagesEqualList(current, messages)) {\n return values;\n }\n return {\n ...record,\n [messagesKey]: messages,\n } as StateType;\n}\n\n/**\n * True when two `BaseMessage` arrays carry the same per-message\n * content (using {@link messagesEqual}).\n */\nfunction messagesEqualList(\n previous: readonly BaseMessage[],\n next: readonly BaseMessage[]\n): boolean {\n if (previous === next) return true;\n if (previous.length !== next.length) return false;\n for (let i = 0; i < previous.length; i += 1) {\n if (!messagesEqual(previous[i], next[i])) return false;\n }\n return true;\n}\n\n/**\n * Shallow-equal for `values` objects, *ignoring* the messages slot.\n *\n * The messages array is compared separately by the caller (via\n * {@link messagesEqualList}) because both arrays contain class\n * instances whose JSON representation is not stable across reads.\n */\nfunction stateValuesShallowEqual(\n previous: object,\n next: object,\n messagesKey: string\n): boolean {\n if (previous === next) return true;\n const previousRecord = previous as Record<string, unknown>;\n const nextRecord = next as Record<string, unknown>;\n const previousKeys = Object.keys(previousRecord);\n const nextKeys = Object.keys(nextRecord);\n if (previousKeys.length !== nextKeys.length) return false;\n for (const key of previousKeys) {\n if (!Object.prototype.hasOwnProperty.call(nextRecord, key)) return false;\n const previousValue = previousRecord[key];\n const nextValue = nextRecord[key];\n if (\n key === messagesKey &&\n Array.isArray(previousValue) &&\n Array.isArray(nextValue)\n ) {\n continue;\n }\n if (!Object.is(previousValue, nextValue)) return false;\n }\n return true;\n}\n"],"mappings":"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"}
@@ -69,6 +69,17 @@ var RootMessageProjection = class {
69
69
  #pendingValues = null;
70
70
  #flushScheduled = false;
71
71
  /**
72
+ * Highest checkpoint `step` whose `values` snapshot has been applied.
73
+ * Seeded by {@link StreamController.hydrate} from `getState()` and
74
+ * advanced by live `values` events. A snapshot arriving with a lower
75
+ * step is an older checkpoint replayed by the content pump on
76
+ * reconnect; it is reconciled in add-only mode so it cannot remove
77
+ * the seeded message tail (the final assistant turn). `undefined`
78
+ * until the first step-bearing snapshot, where the legacy
79
+ * remove-on-absence behavior is preserved.
80
+ */
81
+ #maxStep = void 0;
82
+ /**
72
83
  * @param params.messagesKey - Key inside `values` that holds the
73
84
  * message array.
74
85
  * @param params.store - Root snapshot store to mutate.
@@ -90,6 +101,7 @@ var RootMessageProjection = class {
90
101
  this.#pendingMessages = null;
91
102
  this.#pendingValues = null;
92
103
  this.#flushScheduled = false;
104
+ this.#maxStep = void 0;
93
105
  }
94
106
  /**
95
107
  * Record a `namespace → tool_call_id` mapping captured from a root
@@ -172,11 +184,17 @@ var RootMessageProjection = class {
172
184
  * @param nextValues - Full values snapshot from the `values` event.
173
185
  * @param nextMessages - The messages array extracted from
174
186
  * `values[messagesKey]` and coerced to `BaseMessage` instances.
187
+ * @param opts.step - Checkpoint superstep for this snapshot, when
188
+ * known. A snapshot whose step is below the highest applied step is
189
+ * treated as a stale reconnect replay and reconciled add-only.
175
190
  */
176
- applyValues(nextValues, nextMessages) {
191
+ applyValues(nextValues, nextMessages, opts) {
177
192
  const baselineSnapshot = this.#store.getSnapshot();
178
193
  const baselineMessages = this.#pendingMessages ?? baselineSnapshot.messages;
179
194
  const baselineValues = this.#pendingValues ?? baselineSnapshot.values;
195
+ const step = opts?.step;
196
+ const addOnly = step != null && this.#maxStep != null && step < this.#maxStep;
197
+ if (step != null && (this.#maxStep == null || step > this.#maxStep)) this.#maxStep = step;
180
198
  if (nextMessages.length === 0) {
181
199
  if (stateValuesShallowEqual(baselineValues, nextValues, this.#messagesKey)) return;
182
200
  this.#pendingValues = syncMessagesIntoValues(nextValues, this.#messagesKey, baselineMessages);
@@ -188,9 +206,10 @@ var RootMessageProjection = class {
188
206
  currentMessages: baselineMessages,
189
207
  currentIndexById: this.#indexById,
190
208
  previousValueMessageIds: this.#valuesMessageIds,
191
- preferValuesMessage: shouldPreferValuesMessageForToolCalls
209
+ preferValuesMessage: shouldPreferValuesMessageForToolCalls,
210
+ addOnly
192
211
  });
193
- this.#valuesMessageIds = reconciliation.valueMessageIds;
212
+ if (!addOnly) this.#valuesMessageIds = reconciliation.valueMessageIds;
194
213
  const messages = reconciliation.messages;
195
214
  const values = {
196
215
  ...nextValues,
@@ -204,6 +223,114 @@ var RootMessageProjection = class {
204
223
  this.#scheduleFlush();
205
224
  }
206
225
  /**
226
+ * Append messages applied optimistically by a local `submit()`,
227
+ * keyed by id so the eventual server echo reconciles cleanly.
228
+ *
229
+ * Unlike {@link applyValues}, the supplied messages are *not* treated
230
+ * as an authoritative ordered snapshot: they are appended to the end
231
+ * of the current projection (or replaced in place when the id already
232
+ * exists), preserving prior history ordering. When the server later
233
+ * emits a `values` snapshot containing the same ids,
234
+ * {@link applyValues} → {@link reconcileMessagesFromValues} takes over
235
+ * (server ordering wins, the echoed message replaces the optimistic
236
+ * one).
237
+ *
238
+ * Non-message input keys are shallow-merged into `values` via
239
+ * `extraValues`; they are dropped/overwritten automatically by the
240
+ * first server `values` event (which rebuilds `values` from the
241
+ * server snapshot), or rolled back via {@link restoreValueKeys} when
242
+ * the run fails before any echo.
243
+ *
244
+ * @param messages - Optimistic messages (already coerced to
245
+ * `BaseMessage` instances, each carrying a stable id).
246
+ * @param extraValues - Non-message input keys to shallow-merge into
247
+ * `values`.
248
+ */
249
+ appendOptimistic(messages, extraValues) {
250
+ let working = this.#pendingMessages ?? this.#store.getSnapshot().messages;
251
+ let mutated = false;
252
+ for (const message of messages) {
253
+ const id = message.id;
254
+ if (id == null) continue;
255
+ const existingIdx = this.#indexById.get(id);
256
+ if (existingIdx == null) {
257
+ if (!mutated) {
258
+ working = working.slice();
259
+ mutated = true;
260
+ }
261
+ this.#indexById.set(id, working.length);
262
+ working.push(message);
263
+ } else if (!messagesEqual(working[existingIdx], message)) {
264
+ if (!mutated) {
265
+ working = working.slice();
266
+ mutated = true;
267
+ }
268
+ working[existingIdx] = message;
269
+ }
270
+ }
271
+ const baselineValues = this.#pendingValues ?? this.#store.getSnapshot().values;
272
+ let values = baselineValues;
273
+ if (extraValues != null && Object.keys(extraValues).length > 0) values = {
274
+ ...baselineValues,
275
+ ...extraValues
276
+ };
277
+ values = syncMessagesIntoValues(values, this.#messagesKey, working);
278
+ if (!mutated && values === baselineValues) return;
279
+ this.#pendingMessages = working;
280
+ if (values !== baselineValues) this.#pendingValues = values;
281
+ this.#scheduleFlush();
282
+ }
283
+ /**
284
+ * Drop optimistic messages by id without disturbing the rest of the
285
+ * projection. Used by {@link StreamController.hydrate} to remove
286
+ * never-persisted optimistic messages (`pending` / `failed`) so a
287
+ * reload converges to server truth.
288
+ *
289
+ * @param ids - Message ids to remove.
290
+ */
291
+ dropOptimisticMessages(ids) {
292
+ if (ids.size === 0) return;
293
+ const baselineMessages = this.#pendingMessages ?? this.#store.getSnapshot().messages;
294
+ const next = baselineMessages.filter((m) => m.id == null || !ids.has(m.id));
295
+ if (next.length === baselineMessages.length) return;
296
+ this.#indexById.clear();
297
+ for (const [id, idx] of buildMessageIndex(next)) this.#indexById.set(id, idx);
298
+ const baselineValues = this.#pendingValues ?? this.#store.getSnapshot().values;
299
+ this.#pendingMessages = next;
300
+ this.#pendingValues = syncMessagesIntoValues(baselineValues, this.#messagesKey, next);
301
+ this.#scheduleFlush();
302
+ }
303
+ /**
304
+ * Restore (or delete) `values` keys that were optimistically merged
305
+ * by {@link appendOptimistic} but never echoed by the server — i.e.
306
+ * roll back non-message optimistic state when the run fails before
307
+ * any `values` event lands. Messages are left untouched (kept on
308
+ * failure per the optimistic contract).
309
+ *
310
+ * @param restore - Per-key pre-submit snapshot: when `hadKey` is
311
+ * false the key is deleted, otherwise it is reset to `prevValue`.
312
+ */
313
+ restoreValueKeys(restore) {
314
+ if (restore.length === 0) return;
315
+ const next = { ...this.#pendingValues ?? this.#store.getSnapshot().values };
316
+ let changed = false;
317
+ for (const { key, hadKey, prevValue } of restore) {
318
+ if (key === this.#messagesKey) continue;
319
+ if (hadKey) {
320
+ if (!Object.is(next[key], prevValue)) {
321
+ next[key] = prevValue;
322
+ changed = true;
323
+ }
324
+ } else if (Object.prototype.hasOwnProperty.call(next, key)) {
325
+ delete next[key];
326
+ changed = true;
327
+ }
328
+ }
329
+ if (!changed) return;
330
+ this.#pendingValues = next;
331
+ this.#scheduleFlush();
332
+ }
333
+ /**
207
334
  * Schedule a coalesced flush on the next macrotask. Idempotent
208
335
  * within a tick — multiple `handleMessage` / `applyValues` calls
209
336
  * before the flush fires collapse into one store write.
@@ -1 +1 @@
1
- {"version":3,"file":"root-message-projection.js","names":["#messagesKey","#store","#roles","#indexById","#toolCallIdByNamespace","#assembler","#valuesMessageIds","#pendingMessages","#pendingValues","#flushScheduled","#scheduleFlush","#flushPending"],"sources":["../../src/stream/root-message-projection.ts"],"sourcesContent":["/**\n * Root-namespace message projection.\n *\n * # What this module is\n *\n * The {@link RootMessageProjection} is the piece of the\n * {@link StreamController} that owns \"what messages does the root\n * namespace currently contain?\". It assembles streamed message deltas\n * via {@link MessageAssembler}, reconciles them against authoritative\n * `values.messages` snapshots from the server, and writes the merged\n * list back into the controller's root snapshot store.\n *\n * # Two streams of truth\n *\n * Root messages arrive on two channels and need to merge cleanly:\n *\n * - **`messages` channel.** Token-level deltas that build messages\n * incrementally. The {@link MessageAssembler} keeps partial\n * messages by id and emits an updated `BaseMessage` per delta.\n * - **`values` channel.** Periodic full-state snapshots that include\n * the authoritative messages array. Used for ordering, removals,\n * and forks (where the streamed messages may pre-date the new\n * timeline).\n *\n * The reconciliation rules (delegated to\n * {@link reconcileMessagesFromValues}) preserve in-flight streamed\n * content while letting values dictate ordering and removals.\n *\n * # Tool-message namespace correlation\n *\n * Tool messages arrive on `messages-start` events with `role: \"tool\"`\n * but the start event doesn't always include a `tool_call_id`. We\n * recover it via three fallbacks:\n *\n * 1. The start event itself, when the server includes it.\n * 2. The legacy `<id>-tool-<call_id>` message id format.\n * 3. The most recent `tool-started` event recorded under the same\n * namespace via {@link recordToolCallNamespace}.\n *\n * Without this correlation, tool messages render with empty\n * `tool_call_id` and downstream UIs can't pair them with the\n * originating tool call.\n *\n * # Store-write batching\n *\n * Every {@link handleMessage} / {@link applyValues} call updates the\n * in-projection bookkeeping (assembler state, id index, role cache)\n * synchronously, then stages the new `messages` / `values` into a\n * pending buffer and schedules a `setTimeout(0)` flush. A single\n * coalesced `store.setState` runs at the next macrotask boundary.\n *\n * The motivation is the long-replay freeze: a thread with hundreds\n * of messages replays through the `messages` channel on refresh or\n * mid-run join. Those events drain through the controller's\n * `for await` pump as a long microtask chain. Per-event\n * `store.setState` notifies `useSyncExternalStore` per event, and\n * after enough notifications React's `nestedUpdateCount` guard trips\n * with \"Maximum update depth exceeded\", permanently freezing the UI\n * on the first few messages. Coalescing to one notification per\n * macrotask lets React's scheduler commit between flushes.\n *\n * # Lifecycle\n *\n * - `handleMessage(event)` — apply a `messages` event delta.\n * - `applyValues(values, msgs)` — merge a `values` snapshot.\n * - `recordToolCallNamespace(ns, id)` — capture `namespace → tool_call_id`\n * so subsequent tool message starts can recover the id.\n * - `reset()` — clear all state on thread rebind.\n */\nimport type {\n MessagesEvent,\n MessageRole,\n MessageStartData,\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 { StreamStore } from \"./store.js\";\nimport type { RootSnapshot } from \"./types.js\";\nimport { namespaceKey } from \"./namespace.js\";\nimport {\n buildMessageIndex,\n messagesEqual,\n reconcileMessagesFromValues,\n shouldPreferValuesMessageForToolCalls,\n} from \"./message-reconciliation.js\";\n\n/**\n * Root-namespace message projection. Owns the merge between the\n * `messages` (streamed deltas) and `values` (authoritative\n * snapshots) channels for the root namespace.\n *\n * @typeParam StateType - Root state shape; the messages array is read\n * from `values[messagesKey]`.\n * @typeParam InterruptType - Shape of root protocol interrupts (forwarded\n * into `RootSnapshot` updates).\n */\nexport class RootMessageProjection<\n StateType extends object,\n InterruptType = unknown,\n> {\n /**\n * Key inside `values` that holds the message array. Defaults to\n * `\"messages\"` in the controller; configurable for state graphs\n * that surface messages under a different slot.\n */\n readonly #messagesKey: string;\n\n /** Root snapshot store written to on every merge. */\n readonly #store: StreamStore<RootSnapshot<StateType, InterruptType>>;\n\n /**\n * Stateful chunk assembler for in-flight messages. Reset (via a\n * fresh instance) on every {@link reset} so a new thread starts\n * with no half-built messages from the previous one.\n */\n #assembler = new MessageAssembler();\n\n /**\n * `messageId → role/toolCallId` captured from `message-start` events.\n * The assembler's intermediate output drops these fields, so we cache\n * them at start-time and reapply when projecting to a `BaseMessage`.\n */\n readonly #roles = new Map<\n string,\n { role: ExtendedMessageRole; toolCallId?: string }\n >();\n\n /**\n * `messageId → position in #store.messages` for fast in-place\n * updates as deltas arrive. Rebuilt on every full reconciliation\n * driven by a `values` event.\n */\n readonly #indexById = new Map<string, number>();\n\n /**\n * Ids observed in the most recent `values.messages` snapshot.\n * Reconciliation uses this to detect server-side removals: a\n * previously-seen id missing from the next snapshot means it was\n * removed by the server (and should drop from the projection).\n */\n #valuesMessageIds = new Set<string>();\n\n /**\n * `namespaceKey → tool_call_id` captured from root `tool-started`\n * events. Used as a fallback when a tool-role `message-start` is\n * missing its `tool_call_id` field.\n */\n readonly #toolCallIdByNamespace = new Map<string, string>();\n\n /**\n * Coalescing buffer for store writes. {@link handleMessage} and\n * {@link applyValues} stage their computed `messages` / `values`\n * here instead of calling `store.setState` per event. A single\n * `setTimeout(0)` flush commits them in one `setState`, so a\n * burst of SSE events draining as a microtask chain becomes one\n * store notification at the next macrotask boundary.\n *\n * `null` means \"no staged write\" — once a flush settles, the\n * slots are cleared so the next call starts from the latest\n * committed store snapshot.\n */\n #pendingMessages: BaseMessage[] | null = null;\n #pendingValues: StateType | null = null;\n #flushScheduled = false;\n\n /**\n * @param params.messagesKey - Key inside `values` that holds the\n * message array.\n * @param params.store - Root snapshot store to mutate.\n */\n constructor(params: {\n messagesKey: string;\n store: StreamStore<RootSnapshot<StateType, InterruptType>>;\n }) {\n this.#messagesKey = params.messagesKey;\n this.#store = params.store;\n }\n\n /**\n * Drop all per-thread state. Called by the controller on thread\n * rebind / dispose so a swap doesn't surface stale messages.\n */\n reset(): void {\n this.#assembler = new MessageAssembler();\n this.#roles.clear();\n this.#indexById.clear();\n this.#valuesMessageIds = new Set();\n this.#toolCallIdByNamespace.clear();\n // Drop any unflushed pending writes — they were computed against\n // the previous thread's baseline and committing them after a\n // rebind would bleed stale messages into the new thread.\n this.#pendingMessages = null;\n this.#pendingValues = null;\n this.#flushScheduled = false;\n }\n\n /**\n * Record a `namespace → tool_call_id` mapping captured from a root\n * `tool-started` event.\n *\n * The companion tool-role `message-start` event may not carry a\n * `tool_call_id`, so we fall back to the most recent value recorded\n * here for the same namespace.\n *\n * @param namespace - Event namespace from the `tool-started` event.\n * @param toolCallId - Tool call id from the same event.\n */\n recordToolCallNamespace(\n namespace: readonly string[],\n toolCallId: string\n ): void {\n this.#toolCallIdByNamespace.set(namespaceKey(namespace), toolCallId);\n }\n\n /**\n * Apply a `messages` channel event to the projection.\n *\n * Captures role/tool metadata on `message-start`, feeds the chunk\n * to the assembler, projects the assembled output to a\n * {@link BaseMessage}, and either appends or in-place updates the\n * pending messages buffer based on whether the id was seen before.\n *\n * @param event - The `messages` channel event to consume.\n */\n handleMessage(event: MessagesEvent): void {\n const data = event.params.data;\n if (data.event === \"message-start\") {\n const startData = data as MessageStartData;\n const role = (startData.role ?? \"ai\") as MessageRole;\n const extendedRole =\n (startData as { role?: ExtendedMessageRole }).role ?? role;\n let toolCallId = (startData as { tool_call_id?: string }).tool_call_id;\n // Tool messages need a tool_call_id to render. Fall back through:\n // 1. legacy `<id>-tool-<call_id>` message id format\n // 2. namespace-recorded tool_call_id (from #recordToolCallNamespace)\n if (extendedRole === \"tool\" && toolCallId == null) {\n const messageId = startData.id;\n if (messageId != null) {\n const match = /-tool-(.+)$/.exec(messageId);\n if (match != null) toolCallId = match[1];\n }\n if (toolCallId == null) {\n toolCallId = this.#toolCallIdByNamespace.get(\n namespaceKey(event.params.namespace)\n );\n }\n }\n if (startData.id != null) {\n this.#roles.set(startData.id, {\n role: extendedRole,\n toolCallId,\n });\n }\n }\n\n const update = this.#assembler.consume(event);\n if (update == null) return;\n const id = update.message.id;\n if (id == null) return;\n const captured = this.#roles.get(id) ?? { role: \"ai\" as const };\n const base = assembledMessageToBaseMessage(update.message, captured.role, {\n toolCallId: captured.toolCallId,\n });\n\n // Compute against the pending baseline if we have one (so an\n // earlier handleMessage in the same tick is the input to this\n // one), else against the latest committed store snapshot.\n // `#indexById` is the synchronous source of truth for \"where is\n // each id in the current messages list\" — every code path below\n // keeps it in sync before returning.\n const baselineMessages =\n this.#pendingMessages ?? this.#store.getSnapshot().messages;\n const existingIdx = this.#indexById.get(id);\n let messages: BaseMessage[];\n if (existingIdx == null) {\n this.#indexById.set(id, baselineMessages.length);\n messages = [...baselineMessages, base];\n } else if (messagesEqual(baselineMessages[existingIdx], base)) {\n // Identical re-emission — skip the store write to keep\n // snapshot identity stable.\n return;\n } else {\n messages = baselineMessages.slice();\n messages[existingIdx] = base;\n }\n\n // Mirror the new messages list into `values[messagesKey]` so\n // direct `values` reads (used by some hooks and by the eventual\n // `values` reconciliation) stay in sync.\n const baselineValues =\n this.#pendingValues ?? this.#store.getSnapshot().values;\n const values = syncMessagesIntoValues(\n baselineValues,\n this.#messagesKey,\n messages\n );\n this.#pendingMessages = messages;\n if (values !== baselineValues) this.#pendingValues = values;\n this.#scheduleFlush();\n }\n\n /**\n * Reconcile a full `values` snapshot into the projection.\n *\n * Delegates the merge to {@link reconcileMessagesFromValues}:\n * values stays authoritative for ordering and removals, while\n * streamed in-flight messages keep their content until the server\n * echoes them back. Empty messages just refresh the values blob.\n *\n * Rebuilds {@link #indexById} after the merge so subsequent delta\n * applications target the new positions.\n *\n * @param nextValues - Full values snapshot from the `values` event.\n * @param nextMessages - The messages array extracted from\n * `values[messagesKey]` and coerced to `BaseMessage` instances.\n */\n applyValues(nextValues: StateType, nextMessages: BaseMessage[]): void {\n const baselineSnapshot = this.#store.getSnapshot();\n const baselineMessages = this.#pendingMessages ?? baselineSnapshot.messages;\n const baselineValues = this.#pendingValues ?? baselineSnapshot.values;\n\n if (nextMessages.length === 0) {\n if (\n stateValuesShallowEqual(baselineValues, nextValues, this.#messagesKey)\n ) {\n return;\n }\n // Mirror the current `messages` list back into the values slot\n // so the staged snapshot stays consistent with the (separately\n // tracked) messages array.\n this.#pendingValues = syncMessagesIntoValues(\n nextValues,\n this.#messagesKey,\n baselineMessages\n );\n this.#scheduleFlush();\n return;\n }\n\n const reconciliation = reconcileMessagesFromValues({\n valueMessages: nextMessages,\n currentMessages: baselineMessages,\n currentIndexById: this.#indexById,\n previousValueMessageIds: this.#valuesMessageIds,\n preferValuesMessage: shouldPreferValuesMessageForToolCalls,\n });\n this.#valuesMessageIds = reconciliation.valueMessageIds;\n const messages = reconciliation.messages as BaseMessage[];\n const values = {\n ...(nextValues as Record<string, unknown>),\n [this.#messagesKey]: messages,\n } as StateType;\n if (\n messages === baselineMessages &&\n stateValuesShallowEqual(baselineValues, values, this.#messagesKey)\n ) {\n return;\n }\n\n // Reconciliation may reorder, drop, or substitute messages, so\n // rebuild the id → index map to match the new array.\n this.#indexById.clear();\n for (const [id, idx] of buildMessageIndex(messages)) {\n this.#indexById.set(id, idx);\n }\n this.#pendingMessages = messages;\n this.#pendingValues = values;\n this.#scheduleFlush();\n }\n\n /**\n * Schedule a coalesced flush on the next macrotask. Idempotent\n * within a tick — multiple `handleMessage` / `applyValues` calls\n * before the flush fires collapse into one store write.\n *\n * `setTimeout(0)` is a macrotask: it runs after the current\n * microtask chain drains, so a burst of SSE events processed by\n * the controller's `for await` pump becomes one `store.setState`\n * (and therefore one `useSyncExternalStore` notification).\n */\n #scheduleFlush = (): void => {\n if (this.#flushScheduled) return;\n this.#flushScheduled = true;\n setTimeout(this.#flushPending, 0);\n };\n\n /**\n * Drain `#pendingMessages` / `#pendingValues` to the store in a\n * single `setState` call.\n */\n #flushPending = (): void => {\n this.#flushScheduled = false;\n const messages = this.#pendingMessages;\n const values = this.#pendingValues;\n this.#pendingMessages = null;\n this.#pendingValues = null;\n if (messages == null && values == null) return;\n this.#store.setState((s) => {\n // Other rootStore mutators (controller-driven `isLoading`,\n // `interrupts`, `toolCalls`, etc.) do not touch `s.messages`\n // / `s.values`, so a last-write-wins commit on those two\n // fields is safe.\n if (messages == null) {\n return values == null ? s : { ...s, values };\n }\n if (values == null) return { ...s, messages };\n return { ...s, messages, values };\n });\n };\n}\n\n/**\n * Mirror a freshly-updated message list into `values[messagesKey]`.\n *\n * Returns the same `values` reference when the list is already\n * equal-by-content so the caller can keep the existing snapshot\n * identity (and avoid spurious `setSnapshot` notifications).\n */\nfunction syncMessagesIntoValues<StateType extends object>(\n values: StateType,\n messagesKey: string,\n messages: BaseMessage[]\n): StateType {\n const record = values as Record<string, unknown>;\n const current = record[messagesKey];\n if (Array.isArray(current) && messagesEqualList(current, messages)) {\n return values;\n }\n return {\n ...record,\n [messagesKey]: messages,\n } as StateType;\n}\n\n/**\n * True when two `BaseMessage` arrays carry the same per-message\n * content (using {@link messagesEqual}).\n */\nfunction messagesEqualList(\n previous: readonly BaseMessage[],\n next: readonly BaseMessage[]\n): boolean {\n if (previous === next) return true;\n if (previous.length !== next.length) return false;\n for (let i = 0; i < previous.length; i += 1) {\n if (!messagesEqual(previous[i], next[i])) return false;\n }\n return true;\n}\n\n/**\n * Shallow-equal for `values` objects, *ignoring* the messages slot.\n *\n * The messages array is compared separately by the caller (via\n * {@link messagesEqualList}) because both arrays contain class\n * instances whose JSON representation is not stable across reads.\n */\nfunction stateValuesShallowEqual(\n previous: object,\n next: object,\n messagesKey: string\n): boolean {\n if (previous === next) return true;\n const previousRecord = previous as Record<string, unknown>;\n const nextRecord = next as Record<string, unknown>;\n const previousKeys = Object.keys(previousRecord);\n const nextKeys = Object.keys(nextRecord);\n if (previousKeys.length !== nextKeys.length) return false;\n for (const key of previousKeys) {\n if (!Object.prototype.hasOwnProperty.call(nextRecord, key)) return false;\n const previousValue = previousRecord[key];\n const nextValue = nextRecord[key];\n if (\n key === messagesKey &&\n Array.isArray(previousValue) &&\n Array.isArray(nextValue)\n ) {\n continue;\n }\n if (!Object.is(previousValue, nextValue)) return false;\n }\n return true;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"root-message-projection.js","names":["#messagesKey","#store","#roles","#indexById","#toolCallIdByNamespace","#assembler","#valuesMessageIds","#pendingMessages","#pendingValues","#flushScheduled","#maxStep","#scheduleFlush","#flushPending"],"sources":["../../src/stream/root-message-projection.ts"],"sourcesContent":["/**\n * Root-namespace message projection.\n *\n * # What this module is\n *\n * The {@link RootMessageProjection} is the piece of the\n * {@link StreamController} that owns \"what messages does the root\n * namespace currently contain?\". It assembles streamed message deltas\n * via {@link MessageAssembler}, reconciles them against authoritative\n * `values.messages` snapshots from the server, and writes the merged\n * list back into the controller's root snapshot store.\n *\n * # Two streams of truth\n *\n * Root messages arrive on two channels and need to merge cleanly:\n *\n * - **`messages` channel.** Token-level deltas that build messages\n * incrementally. The {@link MessageAssembler} keeps partial\n * messages by id and emits an updated `BaseMessage` per delta.\n * - **`values` channel.** Periodic full-state snapshots that include\n * the authoritative messages array. Used for ordering, removals,\n * and forks (where the streamed messages may pre-date the new\n * timeline).\n *\n * The reconciliation rules (delegated to\n * {@link reconcileMessagesFromValues}) preserve in-flight streamed\n * content while letting values dictate ordering and removals.\n *\n * # Tool-message namespace correlation\n *\n * Tool messages arrive on `messages-start` events with `role: \"tool\"`\n * but the start event doesn't always include a `tool_call_id`. We\n * recover it via three fallbacks:\n *\n * 1. The start event itself, when the server includes it.\n * 2. The legacy `<id>-tool-<call_id>` message id format.\n * 3. The most recent `tool-started` event recorded under the same\n * namespace via {@link recordToolCallNamespace}.\n *\n * Without this correlation, tool messages render with empty\n * `tool_call_id` and downstream UIs can't pair them with the\n * originating tool call.\n *\n * # Store-write batching\n *\n * Every {@link handleMessage} / {@link applyValues} call updates the\n * in-projection bookkeeping (assembler state, id index, role cache)\n * synchronously, then stages the new `messages` / `values` into a\n * pending buffer and schedules a `setTimeout(0)` flush. A single\n * coalesced `store.setState` runs at the next macrotask boundary.\n *\n * The motivation is the long-replay freeze: a thread with hundreds\n * of messages replays through the `messages` channel on refresh or\n * mid-run join. Those events drain through the controller's\n * `for await` pump as a long microtask chain. Per-event\n * `store.setState` notifies `useSyncExternalStore` per event, and\n * after enough notifications React's `nestedUpdateCount` guard trips\n * with \"Maximum update depth exceeded\", permanently freezing the UI\n * on the first few messages. Coalescing to one notification per\n * macrotask lets React's scheduler commit between flushes.\n *\n * # Lifecycle\n *\n * - `handleMessage(event)` — apply a `messages` event delta.\n * - `applyValues(values, msgs)` — merge a `values` snapshot.\n * - `recordToolCallNamespace(ns, id)` — capture `namespace → tool_call_id`\n * so subsequent tool message starts can recover the id.\n * - `reset()` — clear all state on thread rebind.\n */\nimport type {\n MessagesEvent,\n MessageRole,\n MessageStartData,\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 { StreamStore } from \"./store.js\";\nimport type { RootSnapshot } from \"./types.js\";\nimport { namespaceKey } from \"./namespace.js\";\nimport {\n buildMessageIndex,\n messagesEqual,\n reconcileMessagesFromValues,\n shouldPreferValuesMessageForToolCalls,\n} from \"./message-reconciliation.js\";\n\n/**\n * Root-namespace message projection. Owns the merge between the\n * `messages` (streamed deltas) and `values` (authoritative\n * snapshots) channels for the root namespace.\n *\n * @typeParam StateType - Root state shape; the messages array is read\n * from `values[messagesKey]`.\n * @typeParam InterruptType - Shape of root protocol interrupts (forwarded\n * into `RootSnapshot` updates).\n */\nexport class RootMessageProjection<\n StateType extends object,\n InterruptType = unknown,\n> {\n /**\n * Key inside `values` that holds the message array. Defaults to\n * `\"messages\"` in the controller; configurable for state graphs\n * that surface messages under a different slot.\n */\n readonly #messagesKey: string;\n\n /** Root snapshot store written to on every merge. */\n readonly #store: StreamStore<RootSnapshot<StateType, InterruptType>>;\n\n /**\n * Stateful chunk assembler for in-flight messages. Reset (via a\n * fresh instance) on every {@link reset} so a new thread starts\n * with no half-built messages from the previous one.\n */\n #assembler = new MessageAssembler();\n\n /**\n * `messageId → role/toolCallId` captured from `message-start` events.\n * The assembler's intermediate output drops these fields, so we cache\n * them at start-time and reapply when projecting to a `BaseMessage`.\n */\n readonly #roles = new Map<\n string,\n { role: ExtendedMessageRole; toolCallId?: string }\n >();\n\n /**\n * `messageId → position in #store.messages` for fast in-place\n * updates as deltas arrive. Rebuilt on every full reconciliation\n * driven by a `values` event.\n */\n readonly #indexById = new Map<string, number>();\n\n /**\n * Ids observed in the most recent `values.messages` snapshot.\n * Reconciliation uses this to detect server-side removals: a\n * previously-seen id missing from the next snapshot means it was\n * removed by the server (and should drop from the projection).\n */\n #valuesMessageIds = new Set<string>();\n\n /**\n * `namespaceKey → tool_call_id` captured from root `tool-started`\n * events. Used as a fallback when a tool-role `message-start` is\n * missing its `tool_call_id` field.\n */\n readonly #toolCallIdByNamespace = new Map<string, string>();\n\n /**\n * Coalescing buffer for store writes. {@link handleMessage} and\n * {@link applyValues} stage their computed `messages` / `values`\n * here instead of calling `store.setState` per event. A single\n * `setTimeout(0)` flush commits them in one `setState`, so a\n * burst of SSE events draining as a microtask chain becomes one\n * store notification at the next macrotask boundary.\n *\n * `null` means \"no staged write\" — once a flush settles, the\n * slots are cleared so the next call starts from the latest\n * committed store snapshot.\n */\n #pendingMessages: BaseMessage[] | null = null;\n #pendingValues: StateType | null = null;\n #flushScheduled = false;\n\n /**\n * Highest checkpoint `step` whose `values` snapshot has been applied.\n * Seeded by {@link StreamController.hydrate} from `getState()` and\n * advanced by live `values` events. A snapshot arriving with a lower\n * step is an older checkpoint replayed by the content pump on\n * reconnect; it is reconciled in add-only mode so it cannot remove\n * the seeded message tail (the final assistant turn). `undefined`\n * until the first step-bearing snapshot, where the legacy\n * remove-on-absence behavior is preserved.\n */\n #maxStep: number | undefined = undefined;\n\n /**\n * @param params.messagesKey - Key inside `values` that holds the\n * message array.\n * @param params.store - Root snapshot store to mutate.\n */\n constructor(params: {\n messagesKey: string;\n store: StreamStore<RootSnapshot<StateType, InterruptType>>;\n }) {\n this.#messagesKey = params.messagesKey;\n this.#store = params.store;\n }\n\n /**\n * Drop all per-thread state. Called by the controller on thread\n * rebind / dispose so a swap doesn't surface stale messages.\n */\n reset(): void {\n this.#assembler = new MessageAssembler();\n this.#roles.clear();\n this.#indexById.clear();\n this.#valuesMessageIds = new Set();\n this.#toolCallIdByNamespace.clear();\n // Drop any unflushed pending writes — they were computed against\n // the previous thread's baseline and committing them after a\n // rebind would bleed stale messages into the new thread.\n this.#pendingMessages = null;\n this.#pendingValues = null;\n this.#flushScheduled = false;\n this.#maxStep = undefined;\n }\n\n /**\n * Record a `namespace → tool_call_id` mapping captured from a root\n * `tool-started` event.\n *\n * The companion tool-role `message-start` event may not carry a\n * `tool_call_id`, so we fall back to the most recent value recorded\n * here for the same namespace.\n *\n * @param namespace - Event namespace from the `tool-started` event.\n * @param toolCallId - Tool call id from the same event.\n */\n recordToolCallNamespace(\n namespace: readonly string[],\n toolCallId: string\n ): void {\n this.#toolCallIdByNamespace.set(namespaceKey(namespace), toolCallId);\n }\n\n /**\n * Apply a `messages` channel event to the projection.\n *\n * Captures role/tool metadata on `message-start`, feeds the chunk\n * to the assembler, projects the assembled output to a\n * {@link BaseMessage}, and either appends or in-place updates the\n * pending messages buffer based on whether the id was seen before.\n *\n * @param event - The `messages` channel event to consume.\n */\n handleMessage(event: MessagesEvent): void {\n const data = event.params.data;\n if (data.event === \"message-start\") {\n const startData = data as MessageStartData;\n const role = (startData.role ?? \"ai\") as MessageRole;\n const extendedRole =\n (startData as { role?: ExtendedMessageRole }).role ?? role;\n let toolCallId = (startData as { tool_call_id?: string }).tool_call_id;\n // Tool messages need a tool_call_id to render. Fall back through:\n // 1. legacy `<id>-tool-<call_id>` message id format\n // 2. namespace-recorded tool_call_id (from #recordToolCallNamespace)\n if (extendedRole === \"tool\" && toolCallId == null) {\n const messageId = startData.id;\n if (messageId != null) {\n const match = /-tool-(.+)$/.exec(messageId);\n if (match != null) toolCallId = match[1];\n }\n if (toolCallId == null) {\n toolCallId = this.#toolCallIdByNamespace.get(\n namespaceKey(event.params.namespace)\n );\n }\n }\n if (startData.id != null) {\n this.#roles.set(startData.id, {\n role: extendedRole,\n toolCallId,\n });\n }\n }\n\n const update = this.#assembler.consume(event);\n if (update == null) return;\n const id = update.message.id;\n if (id == null) return;\n const captured = this.#roles.get(id) ?? { role: \"ai\" as const };\n const base = assembledMessageToBaseMessage(update.message, captured.role, {\n toolCallId: captured.toolCallId,\n });\n\n // Compute against the pending baseline if we have one (so an\n // earlier handleMessage in the same tick is the input to this\n // one), else against the latest committed store snapshot.\n // `#indexById` is the synchronous source of truth for \"where is\n // each id in the current messages list\" — every code path below\n // keeps it in sync before returning.\n const baselineMessages =\n this.#pendingMessages ?? this.#store.getSnapshot().messages;\n const existingIdx = this.#indexById.get(id);\n let messages: BaseMessage[];\n if (existingIdx == null) {\n this.#indexById.set(id, baselineMessages.length);\n messages = [...baselineMessages, base];\n } else if (messagesEqual(baselineMessages[existingIdx], base)) {\n // Identical re-emission — skip the store write to keep\n // snapshot identity stable.\n return;\n } else {\n messages = baselineMessages.slice();\n messages[existingIdx] = base;\n }\n\n // Mirror the new messages list into `values[messagesKey]` so\n // direct `values` reads (used by some hooks and by the eventual\n // `values` reconciliation) stay in sync.\n const baselineValues =\n this.#pendingValues ?? this.#store.getSnapshot().values;\n const values = syncMessagesIntoValues(\n baselineValues,\n this.#messagesKey,\n messages\n );\n this.#pendingMessages = messages;\n if (values !== baselineValues) this.#pendingValues = values;\n this.#scheduleFlush();\n }\n\n /**\n * Reconcile a full `values` snapshot into the projection.\n *\n * Delegates the merge to {@link reconcileMessagesFromValues}:\n * values stays authoritative for ordering and removals, while\n * streamed in-flight messages keep their content until the server\n * echoes them back. Empty messages just refresh the values blob.\n *\n * Rebuilds {@link #indexById} after the merge so subsequent delta\n * applications target the new positions.\n *\n * @param nextValues - Full values snapshot from the `values` event.\n * @param nextMessages - The messages array extracted from\n * `values[messagesKey]` and coerced to `BaseMessage` instances.\n * @param opts.step - Checkpoint superstep for this snapshot, when\n * known. A snapshot whose step is below the highest applied step is\n * treated as a stale reconnect replay and reconciled add-only.\n */\n applyValues(\n nextValues: StateType,\n nextMessages: BaseMessage[],\n opts?: { step?: number }\n ): void {\n const baselineSnapshot = this.#store.getSnapshot();\n const baselineMessages = this.#pendingMessages ?? baselineSnapshot.messages;\n const baselineValues = this.#pendingValues ?? baselineSnapshot.values;\n\n const step = opts?.step;\n // Stale only when we have both a prior high-water step and a lower\n // incoming step. A missing step preserves the legacy semantics.\n const addOnly =\n step != null && this.#maxStep != null && step < this.#maxStep;\n if (step != null && (this.#maxStep == null || step > this.#maxStep)) {\n this.#maxStep = step;\n }\n\n if (nextMessages.length === 0) {\n if (\n stateValuesShallowEqual(baselineValues, nextValues, this.#messagesKey)\n ) {\n return;\n }\n // Mirror the current `messages` list back into the values slot\n // so the staged snapshot stays consistent with the (separately\n // tracked) messages array.\n this.#pendingValues = syncMessagesIntoValues(\n nextValues,\n this.#messagesKey,\n baselineMessages\n );\n this.#scheduleFlush();\n return;\n }\n\n const reconciliation = reconcileMessagesFromValues({\n valueMessages: nextMessages,\n currentMessages: baselineMessages,\n currentIndexById: this.#indexById,\n previousValueMessageIds: this.#valuesMessageIds,\n preferValuesMessage: shouldPreferValuesMessageForToolCalls,\n addOnly,\n });\n // A stale replay snapshot must not shrink the authoritative id set:\n // keep the (larger) seeded set so a genuinely-newer removal is still\n // detected once the timeline advances past the seed.\n if (!addOnly) this.#valuesMessageIds = reconciliation.valueMessageIds;\n const messages = reconciliation.messages as BaseMessage[];\n const values = {\n ...(nextValues as Record<string, unknown>),\n [this.#messagesKey]: messages,\n } as StateType;\n if (\n messages === baselineMessages &&\n stateValuesShallowEqual(baselineValues, values, this.#messagesKey)\n ) {\n return;\n }\n\n // Reconciliation may reorder, drop, or substitute messages, so\n // rebuild the id → index map to match the new array.\n this.#indexById.clear();\n for (const [id, idx] of buildMessageIndex(messages)) {\n this.#indexById.set(id, idx);\n }\n this.#pendingMessages = messages;\n this.#pendingValues = values;\n this.#scheduleFlush();\n }\n\n /**\n * Append messages applied optimistically by a local `submit()`,\n * keyed by id so the eventual server echo reconciles cleanly.\n *\n * Unlike {@link applyValues}, the supplied messages are *not* treated\n * as an authoritative ordered snapshot: they are appended to the end\n * of the current projection (or replaced in place when the id already\n * exists), preserving prior history ordering. When the server later\n * emits a `values` snapshot containing the same ids,\n * {@link applyValues} → {@link reconcileMessagesFromValues} takes over\n * (server ordering wins, the echoed message replaces the optimistic\n * one).\n *\n * Non-message input keys are shallow-merged into `values` via\n * `extraValues`; they are dropped/overwritten automatically by the\n * first server `values` event (which rebuilds `values` from the\n * server snapshot), or rolled back via {@link restoreValueKeys} when\n * the run fails before any echo.\n *\n * @param messages - Optimistic messages (already coerced to\n * `BaseMessage` instances, each carrying a stable id).\n * @param extraValues - Non-message input keys to shallow-merge into\n * `values`.\n */\n appendOptimistic(\n messages: BaseMessage[],\n extraValues?: Record<string, unknown>\n ): void {\n let working = this.#pendingMessages ?? this.#store.getSnapshot().messages;\n let mutated = false;\n for (const message of messages) {\n const id = message.id;\n if (id == null) continue;\n const existingIdx = this.#indexById.get(id);\n if (existingIdx == null) {\n if (!mutated) {\n working = working.slice();\n mutated = true;\n }\n this.#indexById.set(id, working.length);\n working.push(message);\n } else if (!messagesEqual(working[existingIdx], message)) {\n if (!mutated) {\n working = working.slice();\n mutated = true;\n }\n working[existingIdx] = message;\n }\n }\n\n const baselineValues =\n this.#pendingValues ?? this.#store.getSnapshot().values;\n let values = baselineValues;\n if (extraValues != null && Object.keys(extraValues).length > 0) {\n values = { ...(baselineValues as object), ...extraValues } as StateType;\n }\n values = syncMessagesIntoValues(values, this.#messagesKey, working);\n if (!mutated && values === baselineValues) return;\n this.#pendingMessages = working;\n if (values !== baselineValues) this.#pendingValues = values;\n this.#scheduleFlush();\n }\n\n /**\n * Drop optimistic messages by id without disturbing the rest of the\n * projection. Used by {@link StreamController.hydrate} to remove\n * never-persisted optimistic messages (`pending` / `failed`) so a\n * reload converges to server truth.\n *\n * @param ids - Message ids to remove.\n */\n dropOptimisticMessages(ids: ReadonlySet<string>): void {\n if (ids.size === 0) return;\n const baselineMessages =\n this.#pendingMessages ?? this.#store.getSnapshot().messages;\n const next = baselineMessages.filter((m) => m.id == null || !ids.has(m.id));\n if (next.length === baselineMessages.length) return;\n this.#indexById.clear();\n for (const [id, idx] of buildMessageIndex(next)) {\n this.#indexById.set(id, idx);\n }\n const baselineValues =\n this.#pendingValues ?? this.#store.getSnapshot().values;\n this.#pendingMessages = next;\n this.#pendingValues = syncMessagesIntoValues(\n baselineValues,\n this.#messagesKey,\n next\n );\n this.#scheduleFlush();\n }\n\n /**\n * Restore (or delete) `values` keys that were optimistically merged\n * by {@link appendOptimistic} but never echoed by the server — i.e.\n * roll back non-message optimistic state when the run fails before\n * any `values` event lands. Messages are left untouched (kept on\n * failure per the optimistic contract).\n *\n * @param restore - Per-key pre-submit snapshot: when `hadKey` is\n * false the key is deleted, otherwise it is reset to `prevValue`.\n */\n restoreValueKeys(\n restore: ReadonlyArray<{\n key: string;\n hadKey: boolean;\n prevValue: unknown;\n }>\n ): void {\n if (restore.length === 0) return;\n const baselineValues =\n this.#pendingValues ?? this.#store.getSnapshot().values;\n const next = { ...(baselineValues as Record<string, unknown>) };\n let changed = false;\n for (const { key, hadKey, prevValue } of restore) {\n if (key === this.#messagesKey) continue;\n if (hadKey) {\n if (!Object.is(next[key], prevValue)) {\n next[key] = prevValue;\n changed = true;\n }\n } else if (Object.prototype.hasOwnProperty.call(next, key)) {\n delete next[key];\n changed = true;\n }\n }\n if (!changed) return;\n this.#pendingValues = next as StateType;\n this.#scheduleFlush();\n }\n\n /**\n * Schedule a coalesced flush on the next macrotask. Idempotent\n * within a tick — multiple `handleMessage` / `applyValues` calls\n * before the flush fires collapse into one store write.\n *\n * `setTimeout(0)` is a macrotask: it runs after the current\n * microtask chain drains, so a burst of SSE events processed by\n * the controller's `for await` pump becomes one `store.setState`\n * (and therefore one `useSyncExternalStore` notification).\n */\n #scheduleFlush = (): void => {\n if (this.#flushScheduled) return;\n this.#flushScheduled = true;\n setTimeout(this.#flushPending, 0);\n };\n\n /**\n * Drain `#pendingMessages` / `#pendingValues` to the store in a\n * single `setState` call.\n */\n #flushPending = (): void => {\n this.#flushScheduled = false;\n const messages = this.#pendingMessages;\n const values = this.#pendingValues;\n this.#pendingMessages = null;\n this.#pendingValues = null;\n if (messages == null && values == null) return;\n this.#store.setState((s) => {\n // Other rootStore mutators (controller-driven `isLoading`,\n // `interrupts`, `toolCalls`, etc.) do not touch `s.messages`\n // / `s.values`, so a last-write-wins commit on those two\n // fields is safe.\n if (messages == null) {\n return values == null ? s : { ...s, values };\n }\n if (values == null) return { ...s, messages };\n return { ...s, messages, values };\n });\n };\n}\n\n/**\n * Mirror a freshly-updated message list into `values[messagesKey]`.\n *\n * Returns the same `values` reference when the list is already\n * equal-by-content so the caller can keep the existing snapshot\n * identity (and avoid spurious `setSnapshot` notifications).\n */\nfunction syncMessagesIntoValues<StateType extends object>(\n values: StateType,\n messagesKey: string,\n messages: BaseMessage[]\n): StateType {\n const record = values as Record<string, unknown>;\n const current = record[messagesKey];\n if (Array.isArray(current) && messagesEqualList(current, messages)) {\n return values;\n }\n return {\n ...record,\n [messagesKey]: messages,\n } as StateType;\n}\n\n/**\n * True when two `BaseMessage` arrays carry the same per-message\n * content (using {@link messagesEqual}).\n */\nfunction messagesEqualList(\n previous: readonly BaseMessage[],\n next: readonly BaseMessage[]\n): boolean {\n if (previous === next) return true;\n if (previous.length !== next.length) return false;\n for (let i = 0; i < previous.length; i += 1) {\n if (!messagesEqual(previous[i], next[i])) return false;\n }\n return true;\n}\n\n/**\n * Shallow-equal for `values` objects, *ignoring* the messages slot.\n *\n * The messages array is compared separately by the caller (via\n * {@link messagesEqualList}) because both arrays contain class\n * instances whose JSON representation is not stable across reads.\n */\nfunction stateValuesShallowEqual(\n previous: object,\n next: object,\n messagesKey: string\n): boolean {\n if (previous === next) return true;\n const previousRecord = previous as Record<string, unknown>;\n const nextRecord = next as Record<string, unknown>;\n const previousKeys = Object.keys(previousRecord);\n const nextKeys = Object.keys(nextRecord);\n if (previousKeys.length !== nextKeys.length) return false;\n for (const key of previousKeys) {\n if (!Object.prototype.hasOwnProperty.call(nextRecord, key)) return false;\n const previousValue = previousRecord[key];\n const nextValue = nextRecord[key];\n if (\n key === messagesKey &&\n Array.isArray(previousValue) &&\n Array.isArray(nextValue)\n ) {\n continue;\n }\n if (!Object.is(previousValue, nextValue)) return false;\n }\n return true;\n}\n"],"mappings":"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"}