@langchain/langgraph-sdk 1.9.16 → 1.9.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) 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/messages.cjs +24 -14
  70. package/dist/stream/projections/messages.cjs.map +1 -1
  71. package/dist/stream/projections/messages.js +21 -11
  72. package/dist/stream/projections/messages.js.map +1 -1
  73. package/dist/stream/projections/tool-calls.cjs +22 -10
  74. package/dist/stream/projections/tool-calls.cjs.map +1 -1
  75. package/dist/stream/projections/tool-calls.js +22 -10
  76. package/dist/stream/projections/tool-calls.js.map +1 -1
  77. package/dist/stream/projections/values.cjs +2 -2
  78. package/dist/stream/projections/values.cjs.map +1 -1
  79. package/dist/stream/projections/values.js +1 -1
  80. package/dist/stream/projections/values.js.map +1 -1
  81. package/dist/stream/root-message-projection.cjs +130 -3
  82. package/dist/stream/root-message-projection.cjs.map +1 -1
  83. package/dist/stream/root-message-projection.js +130 -3
  84. package/dist/stream/root-message-projection.js.map +1 -1
  85. package/dist/stream/submit-coordinator.cjs +28 -6
  86. package/dist/stream/submit-coordinator.cjs.map +1 -1
  87. package/dist/stream/submit-coordinator.d.cts.map +1 -1
  88. package/dist/stream/submit-coordinator.d.ts +0 -1
  89. package/dist/stream/submit-coordinator.d.ts.map +1 -1
  90. package/dist/stream/submit-coordinator.js +28 -6
  91. package/dist/stream/submit-coordinator.js.map +1 -1
  92. package/dist/stream/tool-calls.cjs +32 -0
  93. package/dist/stream/tool-calls.cjs.map +1 -1
  94. package/dist/stream/tool-calls.js +32 -1
  95. package/dist/stream/tool-calls.js.map +1 -1
  96. package/dist/stream/types.d.cts +43 -0
  97. package/dist/stream/types.d.cts.map +1 -1
  98. package/dist/stream/types.d.ts +43 -0
  99. package/dist/stream/types.d.ts.map +1 -1
  100. package/dist/ui/index.d.cts +1 -1
  101. package/dist/ui/index.d.ts +1 -1
  102. package/dist/ui/messages.cjs +4 -50
  103. package/dist/ui/messages.cjs.map +1 -1
  104. package/dist/ui/messages.d.cts.map +1 -1
  105. package/dist/ui/messages.d.ts.map +1 -1
  106. package/dist/ui/messages.js +3 -48
  107. package/dist/ui/messages.js.map +1 -1
  108. package/package.json +1 -1
@@ -82,12 +82,21 @@ var MessageMetadataTracker = class {
82
82
  */
83
83
  #pendingCheckpointByNamespace = /* @__PURE__ */ new Map();
84
84
  /**
85
+ * Ids of messages currently in the `"pending"` optimistic state.
86
+ * Maintained alongside the metadata map so the controller can cheaply
87
+ * (a) flip ids to `"sent"` when the server echoes them and (b) flip
88
+ * any leftover ids to `"sent"` / `"failed"` at run terminal, without
89
+ * scanning the whole metadata map.
90
+ */
91
+ #pendingOptimisticIds = /* @__PURE__ */ new Set();
92
+ /**
85
93
  * Drop all buffered checkpoints and reset the metadata map to the
86
94
  * shared empty instance. Called on thread rebind / dispose so a new
87
95
  * thread's metadata can't bleed into the old one.
88
96
  */
89
97
  reset() {
90
98
  this.#pendingCheckpointByNamespace.clear();
99
+ this.#pendingOptimisticIds.clear();
91
100
  this.store.setState(() => EMPTY_METADATA_MAP);
92
101
  }
93
102
  /**
@@ -109,6 +118,7 @@ var MessageMetadataTracker = class {
109
118
  if (data == null || typeof data.id !== "string") return;
110
119
  const envelope = { id: data.id };
111
120
  if (typeof data.parent_id === "string") envelope.parent_id = data.parent_id;
121
+ if (typeof data.step === "number") envelope.step = data.step;
112
122
  this.#pendingCheckpointByNamespace.set(namespaceKey(namespace), envelope);
113
123
  }
114
124
  /**
@@ -158,6 +168,88 @@ var MessageMetadataTracker = class {
158
168
  }
159
169
  if (changed) this.store.setState(() => next);
160
170
  }
171
+ /**
172
+ * Mark a set of message ids as optimistically `"pending"`.
173
+ *
174
+ * Called from {@link StreamController}'s optimistic submit path right
175
+ * after the messages are appended to the projection, so a UI can
176
+ * render a "sending…" affordance via
177
+ * `useMessageMetadata(stream, id).optimisticStatus`.
178
+ *
179
+ * @param ids - Message ids that were just applied optimistically.
180
+ */
181
+ markPending(ids) {
182
+ let changed = false;
183
+ const next = new Map(this.store.getSnapshot());
184
+ for (const id of ids) {
185
+ if (typeof id !== "string" || id.length === 0) continue;
186
+ this.#pendingOptimisticIds.add(id);
187
+ const prev = next.get(id);
188
+ if (prev?.optimisticStatus === "pending") continue;
189
+ next.set(id, {
190
+ parentCheckpointId: prev?.parentCheckpointId,
191
+ ...prev,
192
+ optimisticStatus: "pending"
193
+ });
194
+ changed = true;
195
+ }
196
+ if (changed) this.store.setState(() => next);
197
+ }
198
+ /**
199
+ * Transition the given ids out of `"pending"`.
200
+ *
201
+ * Only ids currently tracked as pending are affected, so passing a
202
+ * full server `values.messages` id list (to flip echoed messages to
203
+ * `"sent"`) never stamps a status onto ordinary server messages.
204
+ *
205
+ * @param ids - Candidate ids (e.g. all ids in a server snapshot,
206
+ * or the ids echoed by a single submit).
207
+ * @param status - Terminal optimistic status (`"sent"` / `"failed"`).
208
+ */
209
+ resolvePending(ids, status) {
210
+ let changed = false;
211
+ const next = new Map(this.store.getSnapshot());
212
+ for (const id of ids) {
213
+ if (!this.#pendingOptimisticIds.has(id)) continue;
214
+ this.#pendingOptimisticIds.delete(id);
215
+ const prev = next.get(id);
216
+ next.set(id, {
217
+ parentCheckpointId: prev?.parentCheckpointId,
218
+ ...prev,
219
+ optimisticStatus: status
220
+ });
221
+ changed = true;
222
+ }
223
+ if (changed) this.store.setState(() => next);
224
+ }
225
+ /**
226
+ * Snapshot of ids whose optimistic status is `"pending"` or
227
+ * `"failed"` — i.e. messages applied locally that the server has not
228
+ * echoed. Used by {@link StreamController.hydrate} to drop
229
+ * never-persisted optimistic messages so a reload converges to
230
+ * server truth.
231
+ */
232
+ unpersistedOptimisticIds() {
233
+ const ids = new Set(this.#pendingOptimisticIds);
234
+ for (const [id, meta] of this.store.getSnapshot()) if (meta.optimisticStatus === "failed") ids.add(id);
235
+ return ids;
236
+ }
237
+ /**
238
+ * Drop all metadata for the given ids. Called after never-persisted
239
+ * optimistic messages are removed from the projection on
240
+ * {@link StreamController.hydrate}, so their status doesn't linger.
241
+ *
242
+ * @param ids - Message ids to forget.
243
+ */
244
+ forget(ids) {
245
+ let changed = false;
246
+ const next = new Map(this.store.getSnapshot());
247
+ for (const id of ids) {
248
+ this.#pendingOptimisticIds.delete(id);
249
+ if (next.delete(id)) changed = true;
250
+ }
251
+ if (changed) this.store.setState(() => next);
252
+ }
161
253
  };
162
254
  //#endregion
163
255
  export { MessageMetadataTracker };
@@ -1 +1 @@
1
- {"version":3,"file":"message-metadata-tracker.js","names":["#pendingCheckpointByNamespace"],"sources":["../../src/stream/message-metadata-tracker.ts"],"sourcesContent":["/**\n * Per-message checkpoint metadata projection.\n *\n * # What this module is\n *\n * The protocol emits a `checkpoints` event immediately *before* its\n * companion `values` event for the same superstep:\n *\n * 1. `checkpoints` — `{ id, parent_id?, step?, source? }`\n * 2. `values` — `{ messages, ... }` (same namespace)\n *\n * Both events carry the same `seq` ordering but live on different\n * channels, so the controller can't atomically observe them. This\n * tracker bridges the gap by buffering each `checkpoints` envelope\n * keyed on its namespace, then consuming it when the matching values\n * payload arrives. Once paired, the consumer (typically the\n * controller) writes a {@link MessageMetadata} record under each\n * message id.\n *\n * # Why fork / edit flows need this\n *\n * Surfacing `parentCheckpointId` per-message lets UI flows like\n * \"edit a message and re-run\" call\n * `submit(input, { forkFrom: checkpointId })` without making the\n * caller juggle thread state. Each message remembers the checkpoint\n * it was first observed at, so a \"fork from this message\" UI can read\n * `useMessageMetadata(stream, msg.id)` directly.\n *\n * # Lifecycle\n *\n * - `bufferCheckpoint(ns, data)` — store the envelope until the\n * companion values event arrives.\n * - `consumeCheckpoint(ns)` — read-and-clear the envelope when\n * the values event lands. Returning `undefined` signals \"no\n * metadata to attach\" — older snapshots without a paired\n * checkpoint are still applied to the store, just without\n * `parentCheckpointId`.\n * - `recordMessages(msgs, meta)` — write metadata for the supplied\n * message ids if it differs from what's already stored.\n * - `reset()` — clear everything (called on\n * thread rebind / dispose).\n *\n * The buffer is read-and-cleared on consumption so a values event that\n * arrives without a fresh checkpoint envelope doesn't reuse stale\n * metadata from a previous superstep.\n */\nimport { StreamStore } from \"./store.js\";\nimport { namespaceKey } from \"./namespace.js\";\n\n/**\n * Metadata tracked per message id. Surfaced to applications via\n * `useMessageMetadata(stream, messageId)`.\n */\nexport interface MessageMetadata {\n /**\n * Checkpoint id the message's *parent* was at when this message was\n * observed. Drives fork / edit flows\n * (`submit(input, { forkFrom: checkpointId })`).\n *\n * `undefined` when the message was observed without a paired\n * checkpoint envelope (e.g. before checkpoints rolled out, or when\n * the caller stripped them upstream).\n */\n readonly parentCheckpointId: string | undefined;\n}\n\n/**\n * Read-only map exposed via {@link MessageMetadataTracker.store}.\n */\nexport type MessageMetadataMap = ReadonlyMap<string, MessageMetadata>;\n\n/**\n * Lightweight envelope mirroring the on-wire `checkpoints` event.\n *\n * The protocol payload may include additional fields (`step`,\n * `source`, etc.) — we only carry what the per-message metadata\n * actually needs.\n */\nexport interface CheckpointEnvelope {\n /** Checkpoint id this superstep wrote. */\n readonly id: string;\n /**\n * Parent checkpoint id, when present. Becomes\n * {@link MessageMetadata.parentCheckpointId} on the next values event.\n */\n readonly parent_id?: string;\n}\n\n/**\n * Frozen empty map used as the store's initial value. Keeping the\n * reference stable avoids spurious `setSnapshot` notifications on\n * `reset()` for consumers that haven't observed any metadata yet.\n */\nconst EMPTY_METADATA_MAP: MessageMetadataMap = new Map();\n\n/**\n * Tracks checkpoint-derived metadata for messages.\n *\n * Owns one {@link StreamStore} mapping `messageId → MessageMetadata`\n * plus a per-namespace buffer of pending checkpoint envelopes. The\n * controller wires it up via three call sites:\n *\n * 1. `controller.#onRootEvent(\"checkpoints\")`\n * → `bufferCheckpoint(namespace, data)`\n * 2. `controller.#onRootEvent(\"values\")`\n * → `consumeCheckpoint(namespace)` then\n * `recordMessages(values.messages, { parentCheckpointId })`\n * 3. `controller.#teardownThread`\n * → `reset()`\n *\n * @see useMessageMetadata - The framework hook that reads from\n * {@link MessageMetadataTracker.store}.\n */\nexport class MessageMetadataTracker {\n /** Observable map of messageId → metadata for framework consumers. */\n readonly store = new StreamStore<MessageMetadataMap>(EMPTY_METADATA_MAP);\n\n /**\n * Pending checkpoint envelopes awaiting their companion values\n * event. Keyed by `namespaceKey(namespace)` so a deeply-nested\n * checkpoint at one namespace doesn't collide with a root-level\n * checkpoint emitted in the same tick.\n */\n readonly #pendingCheckpointByNamespace = new Map<\n string,\n CheckpointEnvelope\n >();\n\n /**\n * Drop all buffered checkpoints and reset the metadata map to the\n * shared empty instance. Called on thread rebind / dispose so a new\n * thread's metadata can't bleed into the old one.\n */\n reset(): void {\n this.#pendingCheckpointByNamespace.clear();\n this.store.setState(() => EMPTY_METADATA_MAP);\n }\n\n /**\n * Buffer a `checkpoints` event for later pairing with its values\n * companion.\n *\n * Defensive against missing / malformed payloads:\n *\n * - `data == null` → no-op (some upstream nodes elide the\n * payload entirely; we keep the previous buffered envelope so\n * the next consume call still wins).\n * - `id` not a string → no-op.\n * - `parent_id` not a string → omitted from the envelope.\n *\n * @param namespace - Event namespace (used as the buffer key).\n * @param data - Raw checkpoints payload.\n */\n bufferCheckpoint(\n namespace: readonly string[],\n data: { id?: unknown; parent_id?: unknown } | null\n ): void {\n if (data == null || typeof data.id !== \"string\") return;\n const envelope: CheckpointEnvelope = { id: data.id };\n if (typeof data.parent_id === \"string\") {\n (envelope as { parent_id?: string }).parent_id = data.parent_id;\n }\n this.#pendingCheckpointByNamespace.set(namespaceKey(namespace), envelope);\n }\n\n /**\n * Read-and-clear the buffered checkpoint envelope for `namespace`.\n *\n * Always pairs with a single {@link bufferCheckpoint} call: a values\n * event without a matching buffered checkpoint returns `undefined`\n * (meaning \"no metadata to attach\"), and the next checkpoint event\n * starts fresh rather than reusing stale data.\n *\n * @param namespace - Event namespace to consume.\n * @returns The buffered envelope, or `undefined` when none was buffered.\n */\n consumeCheckpoint(\n namespace: readonly string[]\n ): CheckpointEnvelope | undefined {\n const key = namespaceKey(namespace);\n const checkpoint = this.#pendingCheckpointByNamespace.get(key);\n if (checkpoint != null) this.#pendingCheckpointByNamespace.delete(key);\n return checkpoint;\n }\n\n /**\n * Record metadata for a list of messages.\n *\n * Skips messages whose existing entry already matches `metadata`;\n * those without an `id` (or with a non-string id) are silently\n * ignored — there's nothing to key the metadata on. The store is\n * only updated when at least one entry actually changed, so\n * reapplying the same values snapshot is cheap.\n *\n * @param messages - Messages from the latest values payload.\n * @param metadata - Metadata to attach (currently just\n * `parentCheckpointId`).\n */\n recordMessages(\n messages: Array<{ id?: string }>,\n metadata: MessageMetadata\n ): void {\n const current = this.store.getSnapshot();\n let changed = false;\n const next = new Map(current);\n for (const msg of messages) {\n const id = msg?.id;\n if (typeof id !== \"string\" || id.length === 0) continue;\n const prev = next.get(id);\n if (\n prev != null &&\n prev.parentCheckpointId === metadata.parentCheckpointId\n ) {\n continue;\n }\n next.set(id, { ...prev, ...metadata });\n changed = true;\n }\n if (changed) this.store.setState(() => next);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6FA,MAAM,qCAAyC,IAAI,KAAK;;;;;;;;;;;;;;;;;;;AAoBxD,IAAa,yBAAb,MAAoC;;CAElC,QAAiB,IAAI,YAAgC,mBAAmB;;;;;;;CAQxE,gDAAyC,IAAI,KAG1C;;;;;;CAOH,QAAc;AACZ,QAAA,6BAAmC,OAAO;AAC1C,OAAK,MAAM,eAAe,mBAAmB;;;;;;;;;;;;;;;;;CAkB/C,iBACE,WACA,MACM;AACN,MAAI,QAAQ,QAAQ,OAAO,KAAK,OAAO,SAAU;EACjD,MAAM,WAA+B,EAAE,IAAI,KAAK,IAAI;AACpD,MAAI,OAAO,KAAK,cAAc,SAC3B,UAAoC,YAAY,KAAK;AAExD,QAAA,6BAAmC,IAAI,aAAa,UAAU,EAAE,SAAS;;;;;;;;;;;;;CAc3E,kBACE,WACgC;EAChC,MAAM,MAAM,aAAa,UAAU;EACnC,MAAM,aAAa,MAAA,6BAAmC,IAAI,IAAI;AAC9D,MAAI,cAAc,KAAM,OAAA,6BAAmC,OAAO,IAAI;AACtE,SAAO;;;;;;;;;;;;;;;CAgBT,eACE,UACA,UACM;EACN,MAAM,UAAU,KAAK,MAAM,aAAa;EACxC,IAAI,UAAU;EACd,MAAM,OAAO,IAAI,IAAI,QAAQ;AAC7B,OAAK,MAAM,OAAO,UAAU;GAC1B,MAAM,KAAK,KAAK;AAChB,OAAI,OAAO,OAAO,YAAY,GAAG,WAAW,EAAG;GAC/C,MAAM,OAAO,KAAK,IAAI,GAAG;AACzB,OACE,QAAQ,QACR,KAAK,uBAAuB,SAAS,mBAErC;AAEF,QAAK,IAAI,IAAI;IAAE,GAAG;IAAM,GAAG;IAAU,CAAC;AACtC,aAAU;;AAEZ,MAAI,QAAS,MAAK,MAAM,eAAe,KAAK"}
1
+ {"version":3,"file":"message-metadata-tracker.js","names":["#pendingCheckpointByNamespace","#pendingOptimisticIds"],"sources":["../../src/stream/message-metadata-tracker.ts"],"sourcesContent":["/**\n * Per-message checkpoint metadata projection.\n *\n * # What this module is\n *\n * The protocol emits a `checkpoints` event immediately *before* its\n * companion `values` event for the same superstep:\n *\n * 1. `checkpoints` — `{ id, parent_id?, step?, source? }`\n * 2. `values` — `{ messages, ... }` (same namespace)\n *\n * Both events carry the same `seq` ordering but live on different\n * channels, so the controller can't atomically observe them. This\n * tracker bridges the gap by buffering each `checkpoints` envelope\n * keyed on its namespace, then consuming it when the matching values\n * payload arrives. Once paired, the consumer (typically the\n * controller) writes a {@link MessageMetadata} record under each\n * message id.\n *\n * # Why fork / edit flows need this\n *\n * Surfacing `parentCheckpointId` per-message lets UI flows like\n * \"edit a message and re-run\" call\n * `submit(input, { forkFrom: checkpointId })` without making the\n * caller juggle thread state. Each message remembers the checkpoint\n * it was first observed at, so a \"fork from this message\" UI can read\n * `useMessageMetadata(stream, msg.id)` directly.\n *\n * # Lifecycle\n *\n * - `bufferCheckpoint(ns, data)` — store the envelope until the\n * companion values event arrives.\n * - `consumeCheckpoint(ns)` — read-and-clear the envelope when\n * the values event lands. Returning `undefined` signals \"no\n * metadata to attach\" — older snapshots without a paired\n * checkpoint are still applied to the store, just without\n * `parentCheckpointId`.\n * - `recordMessages(msgs, meta)` — write metadata for the supplied\n * message ids if it differs from what's already stored.\n * - `reset()` — clear everything (called on\n * thread rebind / dispose).\n *\n * The buffer is read-and-cleared on consumption so a values event that\n * arrives without a fresh checkpoint envelope doesn't reuse stale\n * metadata from a previous superstep.\n */\nimport { StreamStore } from \"./store.js\";\nimport { namespaceKey } from \"./namespace.js\";\n\n/**\n * Optimistic lifecycle status for a message that originated from a\n * local {@link StreamController.submit} before the server echoed it.\n *\n * - `\"pending\"` — applied optimistically; the run is in flight and\n * the server has not yet echoed this id in a `values` snapshot.\n * - `\"sent\"` — the server echoed the id (run progressed); the\n * message is now server-authoritative.\n * - `\"failed\"` — the run failed before the id was echoed. The\n * message is kept (so UIs can show it with a retry affordance) but\n * is dropped on the next {@link StreamController.hydrate} because\n * it was never persisted server-side.\n *\n * Server-originated messages (history, streamed assistant turns) never\n * carry a status — `undefined` means \"not optimistic\".\n */\nexport type OptimisticStatus = \"pending\" | \"sent\" | \"failed\";\n\n/**\n * Metadata tracked per message id. Surfaced to applications via\n * `useMessageMetadata(stream, messageId)`.\n */\nexport interface MessageMetadata {\n /**\n * Checkpoint id the message's *parent* was at when this message was\n * observed. Drives fork / edit flows\n * (`submit(input, { forkFrom: checkpointId })`).\n *\n * `undefined` when the message was observed without a paired\n * checkpoint envelope (e.g. before checkpoints rolled out, or when\n * the caller stripped them upstream).\n */\n readonly parentCheckpointId: string | undefined;\n\n /**\n * Optimistic lifecycle status, present only for messages applied\n * locally by an optimistic `submit()`. `undefined` for ordinary\n * server-originated messages. See {@link OptimisticStatus}.\n */\n readonly optimisticStatus?: OptimisticStatus;\n}\n\n/**\n * Read-only map exposed via {@link MessageMetadataTracker.store}.\n */\nexport type MessageMetadataMap = ReadonlyMap<string, MessageMetadata>;\n\n/**\n * Lightweight envelope mirroring the on-wire `checkpoints` event.\n *\n * The protocol payload may include additional fields (`step`,\n * `source`, etc.) — we only carry what the per-message metadata\n * actually needs.\n */\nexport interface CheckpointEnvelope {\n /** Checkpoint id this superstep wrote. */\n readonly id: string;\n /**\n * Parent checkpoint id, when present. Becomes\n * {@link MessageMetadata.parentCheckpointId} on the next values event.\n */\n readonly parent_id?: string;\n /**\n * Monotonic superstep counter for the checkpoint. Used by the root\n * message projection to distinguish a fresh/live `values` snapshot\n * from an older one replayed by the content pump on reconnect, so a\n * stale replay can't remove tail messages the authoritative\n * `getState()` seed already established.\n */\n readonly step?: number;\n}\n\n/**\n * Frozen empty map used as the store's initial value. Keeping the\n * reference stable avoids spurious `setSnapshot` notifications on\n * `reset()` for consumers that haven't observed any metadata yet.\n */\nconst EMPTY_METADATA_MAP: MessageMetadataMap = new Map();\n\n/**\n * Tracks checkpoint-derived metadata for messages.\n *\n * Owns one {@link StreamStore} mapping `messageId → MessageMetadata`\n * plus a per-namespace buffer of pending checkpoint envelopes. The\n * controller wires it up via three call sites:\n *\n * 1. `controller.#onRootEvent(\"checkpoints\")`\n * → `bufferCheckpoint(namespace, data)`\n * 2. `controller.#onRootEvent(\"values\")`\n * → `consumeCheckpoint(namespace)` then\n * `recordMessages(values.messages, { parentCheckpointId })`\n * 3. `controller.#teardownThread`\n * → `reset()`\n *\n * @see useMessageMetadata - The framework hook that reads from\n * {@link MessageMetadataTracker.store}.\n */\nexport class MessageMetadataTracker {\n /** Observable map of messageId → metadata for framework consumers. */\n readonly store = new StreamStore<MessageMetadataMap>(EMPTY_METADATA_MAP);\n\n /**\n * Pending checkpoint envelopes awaiting their companion values\n * event. Keyed by `namespaceKey(namespace)` so a deeply-nested\n * checkpoint at one namespace doesn't collide with a root-level\n * checkpoint emitted in the same tick.\n */\n readonly #pendingCheckpointByNamespace = new Map<\n string,\n CheckpointEnvelope\n >();\n\n /**\n * Ids of messages currently in the `\"pending\"` optimistic state.\n * Maintained alongside the metadata map so the controller can cheaply\n * (a) flip ids to `\"sent\"` when the server echoes them and (b) flip\n * any leftover ids to `\"sent\"` / `\"failed\"` at run terminal, without\n * scanning the whole metadata map.\n */\n readonly #pendingOptimisticIds = new Set<string>();\n\n /**\n * Drop all buffered checkpoints and reset the metadata map to the\n * shared empty instance. Called on thread rebind / dispose so a new\n * thread's metadata can't bleed into the old one.\n */\n reset(): void {\n this.#pendingCheckpointByNamespace.clear();\n this.#pendingOptimisticIds.clear();\n this.store.setState(() => EMPTY_METADATA_MAP);\n }\n\n /**\n * Buffer a `checkpoints` event for later pairing with its values\n * companion.\n *\n * Defensive against missing / malformed payloads:\n *\n * - `data == null` → no-op (some upstream nodes elide the\n * payload entirely; we keep the previous buffered envelope so\n * the next consume call still wins).\n * - `id` not a string → no-op.\n * - `parent_id` not a string → omitted from the envelope.\n *\n * @param namespace - Event namespace (used as the buffer key).\n * @param data - Raw checkpoints payload.\n */\n bufferCheckpoint(\n namespace: readonly string[],\n data: { id?: unknown; parent_id?: unknown; step?: unknown } | null\n ): void {\n if (data == null || typeof data.id !== \"string\") return;\n const envelope: CheckpointEnvelope = { id: data.id };\n if (typeof data.parent_id === \"string\") {\n (envelope as { parent_id?: string }).parent_id = data.parent_id;\n }\n if (typeof data.step === \"number\") {\n (envelope as { step?: number }).step = data.step;\n }\n this.#pendingCheckpointByNamespace.set(namespaceKey(namespace), envelope);\n }\n\n /**\n * Read-and-clear the buffered checkpoint envelope for `namespace`.\n *\n * Always pairs with a single {@link bufferCheckpoint} call: a values\n * event without a matching buffered checkpoint returns `undefined`\n * (meaning \"no metadata to attach\"), and the next checkpoint event\n * starts fresh rather than reusing stale data.\n *\n * @param namespace - Event namespace to consume.\n * @returns The buffered envelope, or `undefined` when none was buffered.\n */\n consumeCheckpoint(\n namespace: readonly string[]\n ): CheckpointEnvelope | undefined {\n const key = namespaceKey(namespace);\n const checkpoint = this.#pendingCheckpointByNamespace.get(key);\n if (checkpoint != null) this.#pendingCheckpointByNamespace.delete(key);\n return checkpoint;\n }\n\n /**\n * Record metadata for a list of messages.\n *\n * Skips messages whose existing entry already matches `metadata`;\n * those without an `id` (or with a non-string id) are silently\n * ignored — there's nothing to key the metadata on. The store is\n * only updated when at least one entry actually changed, so\n * reapplying the same values snapshot is cheap.\n *\n * @param messages - Messages from the latest values payload.\n * @param metadata - Metadata to attach (currently just\n * `parentCheckpointId`).\n */\n recordMessages(\n messages: Array<{ id?: string }>,\n metadata: MessageMetadata\n ): void {\n const current = this.store.getSnapshot();\n let changed = false;\n const next = new Map(current);\n for (const msg of messages) {\n const id = msg?.id;\n if (typeof id !== \"string\" || id.length === 0) continue;\n const prev = next.get(id);\n if (\n prev != null &&\n prev.parentCheckpointId === metadata.parentCheckpointId\n ) {\n continue;\n }\n next.set(id, { ...prev, ...metadata });\n changed = true;\n }\n if (changed) this.store.setState(() => next);\n }\n\n /**\n * Mark a set of message ids as optimistically `\"pending\"`.\n *\n * Called from {@link StreamController}'s optimistic submit path right\n * after the messages are appended to the projection, so a UI can\n * render a \"sending…\" affordance via\n * `useMessageMetadata(stream, id).optimisticStatus`.\n *\n * @param ids - Message ids that were just applied optimistically.\n */\n markPending(ids: Iterable<string>): void {\n let changed = false;\n const next = new Map(this.store.getSnapshot());\n for (const id of ids) {\n if (typeof id !== \"string\" || id.length === 0) continue;\n this.#pendingOptimisticIds.add(id);\n const prev = next.get(id);\n if (prev?.optimisticStatus === \"pending\") continue;\n next.set(id, {\n parentCheckpointId: prev?.parentCheckpointId,\n ...prev,\n optimisticStatus: \"pending\",\n });\n changed = true;\n }\n if (changed) this.store.setState(() => next);\n }\n\n /**\n * Transition the given ids out of `\"pending\"`.\n *\n * Only ids currently tracked as pending are affected, so passing a\n * full server `values.messages` id list (to flip echoed messages to\n * `\"sent\"`) never stamps a status onto ordinary server messages.\n *\n * @param ids - Candidate ids (e.g. all ids in a server snapshot,\n * or the ids echoed by a single submit).\n * @param status - Terminal optimistic status (`\"sent\"` / `\"failed\"`).\n */\n resolvePending(ids: Iterable<string>, status: OptimisticStatus): void {\n let changed = false;\n const next = new Map(this.store.getSnapshot());\n for (const id of ids) {\n if (!this.#pendingOptimisticIds.has(id)) continue;\n this.#pendingOptimisticIds.delete(id);\n const prev = next.get(id);\n next.set(id, {\n parentCheckpointId: prev?.parentCheckpointId,\n ...prev,\n optimisticStatus: status,\n });\n changed = true;\n }\n if (changed) this.store.setState(() => next);\n }\n\n /**\n * Snapshot of ids whose optimistic status is `\"pending\"` or\n * `\"failed\"` — i.e. messages applied locally that the server has not\n * echoed. Used by {@link StreamController.hydrate} to drop\n * never-persisted optimistic messages so a reload converges to\n * server truth.\n */\n unpersistedOptimisticIds(): Set<string> {\n const ids = new Set<string>(this.#pendingOptimisticIds);\n for (const [id, meta] of this.store.getSnapshot()) {\n if (meta.optimisticStatus === \"failed\") ids.add(id);\n }\n return ids;\n }\n\n /**\n * Drop all metadata for the given ids. Called after never-persisted\n * optimistic messages are removed from the projection on\n * {@link StreamController.hydrate}, so their status doesn't linger.\n *\n * @param ids - Message ids to forget.\n */\n forget(ids: Iterable<string>): void {\n let changed = false;\n const next = new Map(this.store.getSnapshot());\n for (const id of ids) {\n this.#pendingOptimisticIds.delete(id);\n if (next.delete(id)) changed = true;\n }\n if (changed) this.store.setState(() => next);\n }\n}\n"],"mappings":"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"}
@@ -8,7 +8,7 @@
8
8
  * values snapshot, and stream-only messages are preserved until they either
9
9
  * appear in values or are known to have been removed.
10
10
  */
11
- function reconcileMessagesFromValues({ valueMessages, currentMessages, currentIndexById, previousValueMessageIds, streamedMessageIds, preferValuesMessage }) {
11
+ function reconcileMessagesFromValues({ valueMessages, currentMessages, currentIndexById, previousValueMessageIds, streamedMessageIds, preferValuesMessage, addOnly }) {
12
12
  const valueMessageIds = /* @__PURE__ */ new Set();
13
13
  const merged = [];
14
14
  for (const valuesMessage of valueMessages) {
@@ -27,7 +27,7 @@ function reconcileMessagesFromValues({ valueMessages, currentMessages, currentIn
27
27
  const id = normalizedMessageId(existing);
28
28
  if (id == null) continue;
29
29
  if (valueMessageIds.has(id)) continue;
30
- if (previousValueMessageIds.has(id)) continue;
30
+ if (!addOnly && previousValueMessageIds.has(id)) continue;
31
31
  if (streamedMessageIds != null && !streamedMessageIds.has(id)) continue;
32
32
  merged.push(existing);
33
33
  }
@@ -1 +1 @@
1
- {"version":3,"file":"message-reconciliation.cjs","names":[],"sources":["../../src/stream/message-reconciliation.ts"],"sourcesContent":["import type { BaseMessage } from \"@langchain/core/messages\";\n\nexport interface ReconcileMessagesFromValuesOptions {\n /**\n * Messages from the authoritative `values.messages` snapshot.\n */\n readonly valueMessages: readonly BaseMessage[];\n /**\n * Current message projection, including stream-assembled in-flight messages.\n */\n readonly currentMessages: readonly BaseMessage[];\n /**\n * Index from message id to current message position.\n */\n readonly currentIndexById: ReadonlyMap<string, number>;\n /**\n * Ids observed in the most recent previous `values.messages` snapshot.\n * If one of these ids is missing from the next snapshot, it is treated as\n * an explicit server-side removal.\n */\n readonly previousValueMessageIds: ReadonlySet<string>;\n /**\n * Optional stream-id filter. When supplied, only these current ids are\n * eligible to override the values snapshot. When omitted, any id present in\n * `currentIndexById` is eligible, preserving the root controller's historic\n * behavior.\n */\n readonly streamedMessageIds?: ReadonlySet<string>;\n /**\n * Allows callers to keep a values message even when a streamed message with\n * the same id exists. Used by the root controller when the values message\n * carries finalized tool-call data missing from the streamed message.\n */\n readonly preferValuesMessage?: (\n valuesMessage: BaseMessage,\n streamedMessage: BaseMessage\n ) => boolean;\n}\n\nexport interface ReconciledMessages {\n readonly messages: readonly BaseMessage[];\n readonly valueMessageIds: Set<string>;\n}\n\n/**\n * Merge an authoritative `values.messages` snapshot with the current streamed\n * message projection.\n *\n * Values remain authoritative for ordering and removals. Streamed messages\n * remain authoritative for in-flight content until the server echoes them in a\n * values snapshot, and stream-only messages are preserved until they either\n * appear in values or are known to have been removed.\n */\nexport function reconcileMessagesFromValues({\n valueMessages,\n currentMessages,\n currentIndexById,\n previousValueMessageIds,\n streamedMessageIds,\n preferValuesMessage,\n}: ReconcileMessagesFromValuesOptions): ReconciledMessages {\n const valueMessageIds = new Set<string>();\n const merged: BaseMessage[] = [];\n\n for (const valuesMessage of valueMessages) {\n const id = normalizedMessageId(valuesMessage);\n if (id == null) {\n merged.push(valuesMessage);\n continue;\n }\n\n valueMessageIds.add(id);\n const streamIdx = currentIndexById.get(id);\n const canUseStreamed =\n streamIdx != null &&\n (streamedMessageIds == null || streamedMessageIds.has(id));\n const streamedMessage = canUseStreamed\n ? currentMessages[streamIdx]\n : undefined;\n\n if (\n streamedMessage != null &&\n preferValuesMessage?.(valuesMessage, streamedMessage) !== true\n ) {\n merged.push(streamedMessage);\n } else {\n merged.push(valuesMessage);\n }\n }\n\n for (const existing of currentMessages) {\n const id = normalizedMessageId(existing);\n if (id == null) continue;\n if (valueMessageIds.has(id)) continue;\n if (previousValueMessageIds.has(id)) continue;\n if (streamedMessageIds != null && !streamedMessageIds.has(id)) continue;\n merged.push(existing);\n }\n\n return {\n messages: messagesEqualList(currentMessages, merged)\n ? currentMessages\n : merged,\n valueMessageIds,\n };\n}\n\n/**\n * Build a position index for keyed messages.\n */\nexport function buildMessageIndex(\n messages: readonly BaseMessage[]\n): Map<string, number> {\n const index = new Map<string, number>();\n messages.forEach((message, idx) => {\n const id = normalizedMessageId(message);\n if (id != null) index.set(id, idx);\n });\n return index;\n}\n\n/**\n * Decide whether a values message carries tool-call data missing from the\n * streamed message.\n */\nexport function shouldPreferValuesMessageForToolCalls(\n valuesMessage: BaseMessage,\n streamedMessage: BaseMessage\n): boolean {\n const valuesToolCalls = getMessageToolCalls(valuesMessage);\n if (valuesToolCalls.length === 0) return false;\n\n const streamedToolCalls = getMessageToolCalls(streamedMessage);\n if (streamedToolCalls.length < valuesToolCalls.length) return true;\n\n const streamedIds = new Set(\n streamedToolCalls\n .map((toolCall) => toolCall.id)\n .filter((id): id is string => typeof id === \"string\" && id.length > 0)\n );\n if (\n valuesToolCalls.some((toolCall) => {\n return typeof toolCall.id === \"string\" && !streamedIds.has(toolCall.id);\n })\n ) {\n return true;\n }\n\n // Values snapshots carry the finalized tool-call args. Prefer them only when\n // they add meaningful data, so empty placeholder args do not replace an\n // otherwise useful streamed message.\n return valuesToolCalls.some((valuesToolCall) => {\n const streamedToolCall = streamedToolCalls.find(\n (candidate) =>\n typeof valuesToolCall.id === \"string\" &&\n candidate.id === valuesToolCall.id\n );\n return (\n streamedToolCall != null &&\n hasMeaningfulArgs(valuesToolCall.args) &&\n !jsonishEqual(valuesToolCall.args, streamedToolCall.args)\n );\n });\n}\n\nfunction hasMeaningfulArgs(args: unknown): boolean {\n if (args == null) return false;\n if (typeof args === \"string\") return args.length > 0;\n if (typeof args === \"object\") return Object.keys(args).length > 0;\n return true;\n}\n\nexport function 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\nexport function messagesEqual(\n previous: BaseMessage | undefined,\n next: BaseMessage | undefined\n): boolean {\n if (previous === next) return true;\n if (previous == null || next == null) return false;\n const previousRecord = previous as unknown as Record<string, unknown>;\n const nextRecord = next as unknown as Record<string, unknown>;\n const previousType =\n typeof previous.getType === \"function\"\n ? previous.getType()\n : previousRecord.type;\n const nextType =\n typeof next.getType === \"function\" ? next.getType() : nextRecord.type;\n\n return (\n previous.id === next.id &&\n previousType === nextType &&\n jsonishEqual(previous.content, next.content) &&\n previousRecord.tool_call_id === nextRecord.tool_call_id &&\n previousRecord.status === nextRecord.status &&\n jsonishEqual(\n previousRecord.additional_kwargs,\n nextRecord.additional_kwargs\n ) &&\n jsonishEqual(\n previousRecord.response_metadata,\n nextRecord.response_metadata\n ) &&\n jsonishEqual(previousRecord.tool_calls, nextRecord.tool_calls) &&\n jsonishEqual(\n previousRecord.tool_call_chunks,\n nextRecord.tool_call_chunks\n ) &&\n jsonishEqual(previousRecord.usage_metadata, nextRecord.usage_metadata)\n );\n}\n\nfunction normalizedMessageId(message: BaseMessage): string | undefined {\n return typeof message.id === \"string\" && message.id.length > 0\n ? message.id\n : undefined;\n}\n\nfunction getMessageToolCalls(\n message: BaseMessage\n): Array<{ id?: string; name?: string; args?: unknown }> {\n const raw = (message as unknown as { tool_calls?: unknown }).tool_calls;\n if (!Array.isArray(raw)) return [];\n return raw.filter(\n (toolCall): toolCall is { id?: string; name?: string; args?: unknown } =>\n toolCall != null && typeof toolCall === \"object\"\n );\n}\n\nfunction jsonishEqual(previous: unknown, next: unknown): boolean {\n return jsonishEqualAtDepth(previous, next, 0);\n}\n\nfunction jsonishEqualAtDepth(\n previous: unknown,\n next: unknown,\n depth: number\n): boolean {\n if (Object.is(previous, next)) return true;\n if (previous == null || next == null) return false;\n if (typeof previous !== \"object\" || typeof next !== \"object\") return false;\n if (depth >= 4) return false;\n\n if (Array.isArray(previous) || Array.isArray(next)) {\n if (!Array.isArray(previous) || !Array.isArray(next)) return false;\n if (previous.length !== next.length) return false;\n for (let i = 0; i < previous.length; i += 1) {\n if (!jsonishEqualAtDepth(previous[i], next[i], depth + 1)) return false;\n }\n return true;\n }\n\n const previousRecord = previous as Record<string, unknown>;\n const nextRecord = next as Record<string, unknown>;\n const previousKeys = Object.keys(previousRecord).filter(\n (key) => typeof previousRecord[key] !== \"function\"\n );\n const nextKeys = Object.keys(nextRecord).filter(\n (key) => typeof nextRecord[key] !== \"function\"\n );\n if (previousKeys.length !== nextKeys.length) return false;\n\n for (const key of previousKeys) {\n if (!Object.prototype.hasOwnProperty.call(nextRecord, key)) return false;\n if (!jsonishEqualAtDepth(previousRecord[key], nextRecord[key], depth + 1)) {\n return false;\n }\n }\n return true;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"message-reconciliation.cjs","names":[],"sources":["../../src/stream/message-reconciliation.ts"],"sourcesContent":["import type { BaseMessage } from \"@langchain/core/messages\";\n\nexport interface ReconcileMessagesFromValuesOptions {\n /**\n * Messages from the authoritative `values.messages` snapshot.\n */\n readonly valueMessages: readonly BaseMessage[];\n /**\n * Current message projection, including stream-assembled in-flight messages.\n */\n readonly currentMessages: readonly BaseMessage[];\n /**\n * Index from message id to current message position.\n */\n readonly currentIndexById: ReadonlyMap<string, number>;\n /**\n * Ids observed in the most recent previous `values.messages` snapshot.\n * If one of these ids is missing from the next snapshot, it is treated as\n * an explicit server-side removal.\n */\n readonly previousValueMessageIds: ReadonlySet<string>;\n /**\n * Optional stream-id filter. When supplied, only these current ids are\n * eligible to override the values snapshot. When omitted, any id present in\n * `currentIndexById` is eligible, preserving the root controller's historic\n * behavior.\n */\n readonly streamedMessageIds?: ReadonlySet<string>;\n /**\n * Allows callers to keep a values message even when a streamed message with\n * the same id exists. Used by the root controller when the values message\n * carries finalized tool-call data missing from the streamed message.\n */\n readonly preferValuesMessage?: (\n valuesMessage: BaseMessage,\n streamedMessage: BaseMessage\n ) => boolean;\n /**\n * When true, treat the snapshot as a non-authoritative (older / replayed)\n * view: never drop a current message just because it is absent from this\n * snapshot. Used on reconnect, where the content pump replays older\n * checkpoints after the authoritative `getState()` seed — an older\n * snapshot legitimately lacks later messages and must not remove them.\n */\n readonly addOnly?: boolean;\n}\n\nexport interface ReconciledMessages {\n readonly messages: readonly BaseMessage[];\n readonly valueMessageIds: Set<string>;\n}\n\n/**\n * Merge an authoritative `values.messages` snapshot with the current streamed\n * message projection.\n *\n * Values remain authoritative for ordering and removals. Streamed messages\n * remain authoritative for in-flight content until the server echoes them in a\n * values snapshot, and stream-only messages are preserved until they either\n * appear in values or are known to have been removed.\n */\nexport function reconcileMessagesFromValues({\n valueMessages,\n currentMessages,\n currentIndexById,\n previousValueMessageIds,\n streamedMessageIds,\n preferValuesMessage,\n addOnly,\n}: ReconcileMessagesFromValuesOptions): ReconciledMessages {\n const valueMessageIds = new Set<string>();\n const merged: BaseMessage[] = [];\n\n for (const valuesMessage of valueMessages) {\n const id = normalizedMessageId(valuesMessage);\n if (id == null) {\n merged.push(valuesMessage);\n continue;\n }\n\n valueMessageIds.add(id);\n const streamIdx = currentIndexById.get(id);\n const canUseStreamed =\n streamIdx != null &&\n (streamedMessageIds == null || streamedMessageIds.has(id));\n const streamedMessage = canUseStreamed\n ? currentMessages[streamIdx]\n : undefined;\n\n if (\n streamedMessage != null &&\n preferValuesMessage?.(valuesMessage, streamedMessage) !== true\n ) {\n merged.push(streamedMessage);\n } else {\n merged.push(valuesMessage);\n }\n }\n\n for (const existing of currentMessages) {\n const id = normalizedMessageId(existing);\n if (id == null) continue;\n if (valueMessageIds.has(id)) continue;\n // A previously-seen id missing from this snapshot is a server-side\n // removal — UNLESS this is an older/replayed snapshot (`addOnly`),\n // where the absence only means \"this earlier checkpoint predates the\n // message\", not \"the message was removed\".\n if (!addOnly && previousValueMessageIds.has(id)) continue;\n if (streamedMessageIds != null && !streamedMessageIds.has(id)) continue;\n merged.push(existing);\n }\n\n return {\n messages: messagesEqualList(currentMessages, merged)\n ? currentMessages\n : merged,\n valueMessageIds,\n };\n}\n\n/**\n * Build a position index for keyed messages.\n */\nexport function buildMessageIndex(\n messages: readonly BaseMessage[]\n): Map<string, number> {\n const index = new Map<string, number>();\n messages.forEach((message, idx) => {\n const id = normalizedMessageId(message);\n if (id != null) index.set(id, idx);\n });\n return index;\n}\n\n/**\n * Decide whether a values message carries tool-call data missing from the\n * streamed message.\n */\nexport function shouldPreferValuesMessageForToolCalls(\n valuesMessage: BaseMessage,\n streamedMessage: BaseMessage\n): boolean {\n const valuesToolCalls = getMessageToolCalls(valuesMessage);\n if (valuesToolCalls.length === 0) return false;\n\n const streamedToolCalls = getMessageToolCalls(streamedMessage);\n if (streamedToolCalls.length < valuesToolCalls.length) return true;\n\n const streamedIds = new Set(\n streamedToolCalls\n .map((toolCall) => toolCall.id)\n .filter((id): id is string => typeof id === \"string\" && id.length > 0)\n );\n if (\n valuesToolCalls.some((toolCall) => {\n return typeof toolCall.id === \"string\" && !streamedIds.has(toolCall.id);\n })\n ) {\n return true;\n }\n\n // Values snapshots carry the finalized tool-call args. Prefer them only when\n // they add meaningful data, so empty placeholder args do not replace an\n // otherwise useful streamed message.\n return valuesToolCalls.some((valuesToolCall) => {\n const streamedToolCall = streamedToolCalls.find(\n (candidate) =>\n typeof valuesToolCall.id === \"string\" &&\n candidate.id === valuesToolCall.id\n );\n return (\n streamedToolCall != null &&\n hasMeaningfulArgs(valuesToolCall.args) &&\n !jsonishEqual(valuesToolCall.args, streamedToolCall.args)\n );\n });\n}\n\nfunction hasMeaningfulArgs(args: unknown): boolean {\n if (args == null) return false;\n if (typeof args === \"string\") return args.length > 0;\n if (typeof args === \"object\") return Object.keys(args).length > 0;\n return true;\n}\n\nexport function 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\nexport function messagesEqual(\n previous: BaseMessage | undefined,\n next: BaseMessage | undefined\n): boolean {\n if (previous === next) return true;\n if (previous == null || next == null) return false;\n const previousRecord = previous as unknown as Record<string, unknown>;\n const nextRecord = next as unknown as Record<string, unknown>;\n const previousType =\n typeof previous.getType === \"function\"\n ? previous.getType()\n : previousRecord.type;\n const nextType =\n typeof next.getType === \"function\" ? next.getType() : nextRecord.type;\n\n return (\n previous.id === next.id &&\n previousType === nextType &&\n jsonishEqual(previous.content, next.content) &&\n previousRecord.tool_call_id === nextRecord.tool_call_id &&\n previousRecord.status === nextRecord.status &&\n jsonishEqual(\n previousRecord.additional_kwargs,\n nextRecord.additional_kwargs\n ) &&\n jsonishEqual(\n previousRecord.response_metadata,\n nextRecord.response_metadata\n ) &&\n jsonishEqual(previousRecord.tool_calls, nextRecord.tool_calls) &&\n jsonishEqual(\n previousRecord.tool_call_chunks,\n nextRecord.tool_call_chunks\n ) &&\n jsonishEqual(previousRecord.usage_metadata, nextRecord.usage_metadata)\n );\n}\n\nfunction normalizedMessageId(message: BaseMessage): string | undefined {\n return typeof message.id === \"string\" && message.id.length > 0\n ? message.id\n : undefined;\n}\n\nfunction getMessageToolCalls(\n message: BaseMessage\n): Array<{ id?: string; name?: string; args?: unknown }> {\n const raw = (message as unknown as { tool_calls?: unknown }).tool_calls;\n if (!Array.isArray(raw)) return [];\n return raw.filter(\n (toolCall): toolCall is { id?: string; name?: string; args?: unknown } =>\n toolCall != null && typeof toolCall === \"object\"\n );\n}\n\nfunction jsonishEqual(previous: unknown, next: unknown): boolean {\n return jsonishEqualAtDepth(previous, next, 0);\n}\n\nfunction jsonishEqualAtDepth(\n previous: unknown,\n next: unknown,\n depth: number\n): boolean {\n if (Object.is(previous, next)) return true;\n if (previous == null || next == null) return false;\n if (typeof previous !== \"object\" || typeof next !== \"object\") return false;\n if (depth >= 4) return false;\n\n if (Array.isArray(previous) || Array.isArray(next)) {\n if (!Array.isArray(previous) || !Array.isArray(next)) return false;\n if (previous.length !== next.length) return false;\n for (let i = 0; i < previous.length; i += 1) {\n if (!jsonishEqualAtDepth(previous[i], next[i], depth + 1)) return false;\n }\n return true;\n }\n\n const previousRecord = previous as Record<string, unknown>;\n const nextRecord = next as Record<string, unknown>;\n const previousKeys = Object.keys(previousRecord).filter(\n (key) => typeof previousRecord[key] !== \"function\"\n );\n const nextKeys = Object.keys(nextRecord).filter(\n (key) => typeof nextRecord[key] !== \"function\"\n );\n if (previousKeys.length !== nextKeys.length) return false;\n\n for (const key of previousKeys) {\n if (!Object.prototype.hasOwnProperty.call(nextRecord, key)) return false;\n if (!jsonishEqualAtDepth(previousRecord[key], nextRecord[key], depth + 1)) {\n return false;\n }\n }\n return true;\n}\n"],"mappings":"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"}
@@ -8,7 +8,7 @@
8
8
  * values snapshot, and stream-only messages are preserved until they either
9
9
  * appear in values or are known to have been removed.
10
10
  */
11
- function reconcileMessagesFromValues({ valueMessages, currentMessages, currentIndexById, previousValueMessageIds, streamedMessageIds, preferValuesMessage }) {
11
+ function reconcileMessagesFromValues({ valueMessages, currentMessages, currentIndexById, previousValueMessageIds, streamedMessageIds, preferValuesMessage, addOnly }) {
12
12
  const valueMessageIds = /* @__PURE__ */ new Set();
13
13
  const merged = [];
14
14
  for (const valuesMessage of valueMessages) {
@@ -27,7 +27,7 @@ function reconcileMessagesFromValues({ valueMessages, currentMessages, currentIn
27
27
  const id = normalizedMessageId(existing);
28
28
  if (id == null) continue;
29
29
  if (valueMessageIds.has(id)) continue;
30
- if (previousValueMessageIds.has(id)) continue;
30
+ if (!addOnly && previousValueMessageIds.has(id)) continue;
31
31
  if (streamedMessageIds != null && !streamedMessageIds.has(id)) continue;
32
32
  merged.push(existing);
33
33
  }
@@ -1 +1 @@
1
- {"version":3,"file":"message-reconciliation.js","names":[],"sources":["../../src/stream/message-reconciliation.ts"],"sourcesContent":["import type { BaseMessage } from \"@langchain/core/messages\";\n\nexport interface ReconcileMessagesFromValuesOptions {\n /**\n * Messages from the authoritative `values.messages` snapshot.\n */\n readonly valueMessages: readonly BaseMessage[];\n /**\n * Current message projection, including stream-assembled in-flight messages.\n */\n readonly currentMessages: readonly BaseMessage[];\n /**\n * Index from message id to current message position.\n */\n readonly currentIndexById: ReadonlyMap<string, number>;\n /**\n * Ids observed in the most recent previous `values.messages` snapshot.\n * If one of these ids is missing from the next snapshot, it is treated as\n * an explicit server-side removal.\n */\n readonly previousValueMessageIds: ReadonlySet<string>;\n /**\n * Optional stream-id filter. When supplied, only these current ids are\n * eligible to override the values snapshot. When omitted, any id present in\n * `currentIndexById` is eligible, preserving the root controller's historic\n * behavior.\n */\n readonly streamedMessageIds?: ReadonlySet<string>;\n /**\n * Allows callers to keep a values message even when a streamed message with\n * the same id exists. Used by the root controller when the values message\n * carries finalized tool-call data missing from the streamed message.\n */\n readonly preferValuesMessage?: (\n valuesMessage: BaseMessage,\n streamedMessage: BaseMessage\n ) => boolean;\n}\n\nexport interface ReconciledMessages {\n readonly messages: readonly BaseMessage[];\n readonly valueMessageIds: Set<string>;\n}\n\n/**\n * Merge an authoritative `values.messages` snapshot with the current streamed\n * message projection.\n *\n * Values remain authoritative for ordering and removals. Streamed messages\n * remain authoritative for in-flight content until the server echoes them in a\n * values snapshot, and stream-only messages are preserved until they either\n * appear in values or are known to have been removed.\n */\nexport function reconcileMessagesFromValues({\n valueMessages,\n currentMessages,\n currentIndexById,\n previousValueMessageIds,\n streamedMessageIds,\n preferValuesMessage,\n}: ReconcileMessagesFromValuesOptions): ReconciledMessages {\n const valueMessageIds = new Set<string>();\n const merged: BaseMessage[] = [];\n\n for (const valuesMessage of valueMessages) {\n const id = normalizedMessageId(valuesMessage);\n if (id == null) {\n merged.push(valuesMessage);\n continue;\n }\n\n valueMessageIds.add(id);\n const streamIdx = currentIndexById.get(id);\n const canUseStreamed =\n streamIdx != null &&\n (streamedMessageIds == null || streamedMessageIds.has(id));\n const streamedMessage = canUseStreamed\n ? currentMessages[streamIdx]\n : undefined;\n\n if (\n streamedMessage != null &&\n preferValuesMessage?.(valuesMessage, streamedMessage) !== true\n ) {\n merged.push(streamedMessage);\n } else {\n merged.push(valuesMessage);\n }\n }\n\n for (const existing of currentMessages) {\n const id = normalizedMessageId(existing);\n if (id == null) continue;\n if (valueMessageIds.has(id)) continue;\n if (previousValueMessageIds.has(id)) continue;\n if (streamedMessageIds != null && !streamedMessageIds.has(id)) continue;\n merged.push(existing);\n }\n\n return {\n messages: messagesEqualList(currentMessages, merged)\n ? currentMessages\n : merged,\n valueMessageIds,\n };\n}\n\n/**\n * Build a position index for keyed messages.\n */\nexport function buildMessageIndex(\n messages: readonly BaseMessage[]\n): Map<string, number> {\n const index = new Map<string, number>();\n messages.forEach((message, idx) => {\n const id = normalizedMessageId(message);\n if (id != null) index.set(id, idx);\n });\n return index;\n}\n\n/**\n * Decide whether a values message carries tool-call data missing from the\n * streamed message.\n */\nexport function shouldPreferValuesMessageForToolCalls(\n valuesMessage: BaseMessage,\n streamedMessage: BaseMessage\n): boolean {\n const valuesToolCalls = getMessageToolCalls(valuesMessage);\n if (valuesToolCalls.length === 0) return false;\n\n const streamedToolCalls = getMessageToolCalls(streamedMessage);\n if (streamedToolCalls.length < valuesToolCalls.length) return true;\n\n const streamedIds = new Set(\n streamedToolCalls\n .map((toolCall) => toolCall.id)\n .filter((id): id is string => typeof id === \"string\" && id.length > 0)\n );\n if (\n valuesToolCalls.some((toolCall) => {\n return typeof toolCall.id === \"string\" && !streamedIds.has(toolCall.id);\n })\n ) {\n return true;\n }\n\n // Values snapshots carry the finalized tool-call args. Prefer them only when\n // they add meaningful data, so empty placeholder args do not replace an\n // otherwise useful streamed message.\n return valuesToolCalls.some((valuesToolCall) => {\n const streamedToolCall = streamedToolCalls.find(\n (candidate) =>\n typeof valuesToolCall.id === \"string\" &&\n candidate.id === valuesToolCall.id\n );\n return (\n streamedToolCall != null &&\n hasMeaningfulArgs(valuesToolCall.args) &&\n !jsonishEqual(valuesToolCall.args, streamedToolCall.args)\n );\n });\n}\n\nfunction hasMeaningfulArgs(args: unknown): boolean {\n if (args == null) return false;\n if (typeof args === \"string\") return args.length > 0;\n if (typeof args === \"object\") return Object.keys(args).length > 0;\n return true;\n}\n\nexport function 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\nexport function messagesEqual(\n previous: BaseMessage | undefined,\n next: BaseMessage | undefined\n): boolean {\n if (previous === next) return true;\n if (previous == null || next == null) return false;\n const previousRecord = previous as unknown as Record<string, unknown>;\n const nextRecord = next as unknown as Record<string, unknown>;\n const previousType =\n typeof previous.getType === \"function\"\n ? previous.getType()\n : previousRecord.type;\n const nextType =\n typeof next.getType === \"function\" ? next.getType() : nextRecord.type;\n\n return (\n previous.id === next.id &&\n previousType === nextType &&\n jsonishEqual(previous.content, next.content) &&\n previousRecord.tool_call_id === nextRecord.tool_call_id &&\n previousRecord.status === nextRecord.status &&\n jsonishEqual(\n previousRecord.additional_kwargs,\n nextRecord.additional_kwargs\n ) &&\n jsonishEqual(\n previousRecord.response_metadata,\n nextRecord.response_metadata\n ) &&\n jsonishEqual(previousRecord.tool_calls, nextRecord.tool_calls) &&\n jsonishEqual(\n previousRecord.tool_call_chunks,\n nextRecord.tool_call_chunks\n ) &&\n jsonishEqual(previousRecord.usage_metadata, nextRecord.usage_metadata)\n );\n}\n\nfunction normalizedMessageId(message: BaseMessage): string | undefined {\n return typeof message.id === \"string\" && message.id.length > 0\n ? message.id\n : undefined;\n}\n\nfunction getMessageToolCalls(\n message: BaseMessage\n): Array<{ id?: string; name?: string; args?: unknown }> {\n const raw = (message as unknown as { tool_calls?: unknown }).tool_calls;\n if (!Array.isArray(raw)) return [];\n return raw.filter(\n (toolCall): toolCall is { id?: string; name?: string; args?: unknown } =>\n toolCall != null && typeof toolCall === \"object\"\n );\n}\n\nfunction jsonishEqual(previous: unknown, next: unknown): boolean {\n return jsonishEqualAtDepth(previous, next, 0);\n}\n\nfunction jsonishEqualAtDepth(\n previous: unknown,\n next: unknown,\n depth: number\n): boolean {\n if (Object.is(previous, next)) return true;\n if (previous == null || next == null) return false;\n if (typeof previous !== \"object\" || typeof next !== \"object\") return false;\n if (depth >= 4) return false;\n\n if (Array.isArray(previous) || Array.isArray(next)) {\n if (!Array.isArray(previous) || !Array.isArray(next)) return false;\n if (previous.length !== next.length) return false;\n for (let i = 0; i < previous.length; i += 1) {\n if (!jsonishEqualAtDepth(previous[i], next[i], depth + 1)) return false;\n }\n return true;\n }\n\n const previousRecord = previous as Record<string, unknown>;\n const nextRecord = next as Record<string, unknown>;\n const previousKeys = Object.keys(previousRecord).filter(\n (key) => typeof previousRecord[key] !== \"function\"\n );\n const nextKeys = Object.keys(nextRecord).filter(\n (key) => typeof nextRecord[key] !== \"function\"\n );\n if (previousKeys.length !== nextKeys.length) return false;\n\n for (const key of previousKeys) {\n if (!Object.prototype.hasOwnProperty.call(nextRecord, key)) return false;\n if (!jsonishEqualAtDepth(previousRecord[key], nextRecord[key], depth + 1)) {\n return false;\n }\n }\n return true;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"message-reconciliation.js","names":[],"sources":["../../src/stream/message-reconciliation.ts"],"sourcesContent":["import type { BaseMessage } from \"@langchain/core/messages\";\n\nexport interface ReconcileMessagesFromValuesOptions {\n /**\n * Messages from the authoritative `values.messages` snapshot.\n */\n readonly valueMessages: readonly BaseMessage[];\n /**\n * Current message projection, including stream-assembled in-flight messages.\n */\n readonly currentMessages: readonly BaseMessage[];\n /**\n * Index from message id to current message position.\n */\n readonly currentIndexById: ReadonlyMap<string, number>;\n /**\n * Ids observed in the most recent previous `values.messages` snapshot.\n * If one of these ids is missing from the next snapshot, it is treated as\n * an explicit server-side removal.\n */\n readonly previousValueMessageIds: ReadonlySet<string>;\n /**\n * Optional stream-id filter. When supplied, only these current ids are\n * eligible to override the values snapshot. When omitted, any id present in\n * `currentIndexById` is eligible, preserving the root controller's historic\n * behavior.\n */\n readonly streamedMessageIds?: ReadonlySet<string>;\n /**\n * Allows callers to keep a values message even when a streamed message with\n * the same id exists. Used by the root controller when the values message\n * carries finalized tool-call data missing from the streamed message.\n */\n readonly preferValuesMessage?: (\n valuesMessage: BaseMessage,\n streamedMessage: BaseMessage\n ) => boolean;\n /**\n * When true, treat the snapshot as a non-authoritative (older / replayed)\n * view: never drop a current message just because it is absent from this\n * snapshot. Used on reconnect, where the content pump replays older\n * checkpoints after the authoritative `getState()` seed — an older\n * snapshot legitimately lacks later messages and must not remove them.\n */\n readonly addOnly?: boolean;\n}\n\nexport interface ReconciledMessages {\n readonly messages: readonly BaseMessage[];\n readonly valueMessageIds: Set<string>;\n}\n\n/**\n * Merge an authoritative `values.messages` snapshot with the current streamed\n * message projection.\n *\n * Values remain authoritative for ordering and removals. Streamed messages\n * remain authoritative for in-flight content until the server echoes them in a\n * values snapshot, and stream-only messages are preserved until they either\n * appear in values or are known to have been removed.\n */\nexport function reconcileMessagesFromValues({\n valueMessages,\n currentMessages,\n currentIndexById,\n previousValueMessageIds,\n streamedMessageIds,\n preferValuesMessage,\n addOnly,\n}: ReconcileMessagesFromValuesOptions): ReconciledMessages {\n const valueMessageIds = new Set<string>();\n const merged: BaseMessage[] = [];\n\n for (const valuesMessage of valueMessages) {\n const id = normalizedMessageId(valuesMessage);\n if (id == null) {\n merged.push(valuesMessage);\n continue;\n }\n\n valueMessageIds.add(id);\n const streamIdx = currentIndexById.get(id);\n const canUseStreamed =\n streamIdx != null &&\n (streamedMessageIds == null || streamedMessageIds.has(id));\n const streamedMessage = canUseStreamed\n ? currentMessages[streamIdx]\n : undefined;\n\n if (\n streamedMessage != null &&\n preferValuesMessage?.(valuesMessage, streamedMessage) !== true\n ) {\n merged.push(streamedMessage);\n } else {\n merged.push(valuesMessage);\n }\n }\n\n for (const existing of currentMessages) {\n const id = normalizedMessageId(existing);\n if (id == null) continue;\n if (valueMessageIds.has(id)) continue;\n // A previously-seen id missing from this snapshot is a server-side\n // removal — UNLESS this is an older/replayed snapshot (`addOnly`),\n // where the absence only means \"this earlier checkpoint predates the\n // message\", not \"the message was removed\".\n if (!addOnly && previousValueMessageIds.has(id)) continue;\n if (streamedMessageIds != null && !streamedMessageIds.has(id)) continue;\n merged.push(existing);\n }\n\n return {\n messages: messagesEqualList(currentMessages, merged)\n ? currentMessages\n : merged,\n valueMessageIds,\n };\n}\n\n/**\n * Build a position index for keyed messages.\n */\nexport function buildMessageIndex(\n messages: readonly BaseMessage[]\n): Map<string, number> {\n const index = new Map<string, number>();\n messages.forEach((message, idx) => {\n const id = normalizedMessageId(message);\n if (id != null) index.set(id, idx);\n });\n return index;\n}\n\n/**\n * Decide whether a values message carries tool-call data missing from the\n * streamed message.\n */\nexport function shouldPreferValuesMessageForToolCalls(\n valuesMessage: BaseMessage,\n streamedMessage: BaseMessage\n): boolean {\n const valuesToolCalls = getMessageToolCalls(valuesMessage);\n if (valuesToolCalls.length === 0) return false;\n\n const streamedToolCalls = getMessageToolCalls(streamedMessage);\n if (streamedToolCalls.length < valuesToolCalls.length) return true;\n\n const streamedIds = new Set(\n streamedToolCalls\n .map((toolCall) => toolCall.id)\n .filter((id): id is string => typeof id === \"string\" && id.length > 0)\n );\n if (\n valuesToolCalls.some((toolCall) => {\n return typeof toolCall.id === \"string\" && !streamedIds.has(toolCall.id);\n })\n ) {\n return true;\n }\n\n // Values snapshots carry the finalized tool-call args. Prefer them only when\n // they add meaningful data, so empty placeholder args do not replace an\n // otherwise useful streamed message.\n return valuesToolCalls.some((valuesToolCall) => {\n const streamedToolCall = streamedToolCalls.find(\n (candidate) =>\n typeof valuesToolCall.id === \"string\" &&\n candidate.id === valuesToolCall.id\n );\n return (\n streamedToolCall != null &&\n hasMeaningfulArgs(valuesToolCall.args) &&\n !jsonishEqual(valuesToolCall.args, streamedToolCall.args)\n );\n });\n}\n\nfunction hasMeaningfulArgs(args: unknown): boolean {\n if (args == null) return false;\n if (typeof args === \"string\") return args.length > 0;\n if (typeof args === \"object\") return Object.keys(args).length > 0;\n return true;\n}\n\nexport function 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\nexport function messagesEqual(\n previous: BaseMessage | undefined,\n next: BaseMessage | undefined\n): boolean {\n if (previous === next) return true;\n if (previous == null || next == null) return false;\n const previousRecord = previous as unknown as Record<string, unknown>;\n const nextRecord = next as unknown as Record<string, unknown>;\n const previousType =\n typeof previous.getType === \"function\"\n ? previous.getType()\n : previousRecord.type;\n const nextType =\n typeof next.getType === \"function\" ? next.getType() : nextRecord.type;\n\n return (\n previous.id === next.id &&\n previousType === nextType &&\n jsonishEqual(previous.content, next.content) &&\n previousRecord.tool_call_id === nextRecord.tool_call_id &&\n previousRecord.status === nextRecord.status &&\n jsonishEqual(\n previousRecord.additional_kwargs,\n nextRecord.additional_kwargs\n ) &&\n jsonishEqual(\n previousRecord.response_metadata,\n nextRecord.response_metadata\n ) &&\n jsonishEqual(previousRecord.tool_calls, nextRecord.tool_calls) &&\n jsonishEqual(\n previousRecord.tool_call_chunks,\n nextRecord.tool_call_chunks\n ) &&\n jsonishEqual(previousRecord.usage_metadata, nextRecord.usage_metadata)\n );\n}\n\nfunction normalizedMessageId(message: BaseMessage): string | undefined {\n return typeof message.id === \"string\" && message.id.length > 0\n ? message.id\n : undefined;\n}\n\nfunction getMessageToolCalls(\n message: BaseMessage\n): Array<{ id?: string; name?: string; args?: unknown }> {\n const raw = (message as unknown as { tool_calls?: unknown }).tool_calls;\n if (!Array.isArray(raw)) return [];\n return raw.filter(\n (toolCall): toolCall is { id?: string; name?: string; args?: unknown } =>\n toolCall != null && typeof toolCall === \"object\"\n );\n}\n\nfunction jsonishEqual(previous: unknown, next: unknown): boolean {\n return jsonishEqualAtDepth(previous, next, 0);\n}\n\nfunction jsonishEqualAtDepth(\n previous: unknown,\n next: unknown,\n depth: number\n): boolean {\n if (Object.is(previous, next)) return true;\n if (previous == null || next == null) return false;\n if (typeof previous !== \"object\" || typeof next !== \"object\") return false;\n if (depth >= 4) return false;\n\n if (Array.isArray(previous) || Array.isArray(next)) {\n if (!Array.isArray(previous) || !Array.isArray(next)) return false;\n if (previous.length !== next.length) return false;\n for (let i = 0; i < previous.length; i += 1) {\n if (!jsonishEqualAtDepth(previous[i], next[i], depth + 1)) return false;\n }\n return true;\n }\n\n const previousRecord = previous as Record<string, unknown>;\n const nextRecord = next as Record<string, unknown>;\n const previousKeys = Object.keys(previousRecord).filter(\n (key) => typeof previousRecord[key] !== \"function\"\n );\n const nextKeys = Object.keys(nextRecord).filter(\n (key) => typeof nextRecord[key] !== \"function\"\n );\n if (previousKeys.length !== nextKeys.length) return false;\n\n for (const key of previousKeys) {\n if (!Object.prototype.hasOwnProperty.call(nextRecord, key)) return false;\n if (!jsonishEqualAtDepth(previousRecord[key], nextRecord[key], depth + 1)) {\n return false;\n }\n }\n return true;\n}\n"],"mappings":"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"}
@@ -0,0 +1,86 @@
1
+ const require_message_coercion = require("./message-coercion.cjs");
2
+ const require_messages = require("../ui/messages.cjs");
3
+ //#region src/stream/optimistic-input.ts
4
+ function isBaseMessageInstance(value) {
5
+ return value != null && typeof value.getType === "function";
6
+ }
7
+ function extractId(value) {
8
+ const id = value?.id;
9
+ return typeof id === "string" && id.length > 0 ? id : void 0;
10
+ }
11
+ /**
12
+ * Normalize a message-key value into an array of entries. Mirrors the
13
+ * server's `add_messages` coercion: a bare string or single message
14
+ * object is treated as a one-element list.
15
+ */
16
+ function toEntryArray(value) {
17
+ if (Array.isArray(value)) return value;
18
+ return [value];
19
+ }
20
+ /**
21
+ * Build a message dict carrying `id` from an arbitrary input entry,
22
+ * without mutating the original.
23
+ */
24
+ function toDispatchDict(entry, id) {
25
+ if (typeof entry === "string") return {
26
+ type: "human",
27
+ content: entry,
28
+ id
29
+ };
30
+ if (isBaseMessageInstance(entry)) return {
31
+ ...require_messages.toMessageDict(entry),
32
+ id
33
+ };
34
+ return {
35
+ ...entry,
36
+ id
37
+ };
38
+ }
39
+ /**
40
+ * Prepare a raw submit input for optimistic dispatch.
41
+ *
42
+ * @param raw - Raw input passed to `submit()`. Must be a
43
+ * non-null, non-array object (caller guards this).
44
+ * @param messagesKey - State key holding the message array.
45
+ * @param mintId - Factory for stable client message ids.
46
+ * @returns The dispatch payload, optimistic messages, echoed ids, and
47
+ * the non-message portion of the input.
48
+ */
49
+ function prepareOptimisticInput(raw, messagesKey, mintId) {
50
+ const dispatchInput = { ...raw };
51
+ const extraValues = {};
52
+ for (const key of Object.keys(raw)) if (key !== messagesKey) extraValues[key] = raw[key];
53
+ const echoedIds = [];
54
+ const messagesValue = raw[messagesKey];
55
+ if (messagesValue == null) return {
56
+ dispatchInput,
57
+ optimisticMessages: [],
58
+ echoedIds,
59
+ extraValues
60
+ };
61
+ const entries = toEntryArray(messagesValue);
62
+ const dispatchEntries = [];
63
+ const optimisticDicts = [];
64
+ for (const entry of entries) {
65
+ if (!(typeof entry === "string" || isBaseMessageInstance(entry) || entry != null && typeof entry === "object" && !Array.isArray(entry))) {
66
+ dispatchEntries.push(entry);
67
+ continue;
68
+ }
69
+ const id = extractId(entry) ?? mintId();
70
+ const dict = toDispatchDict(entry, id);
71
+ dispatchEntries.push(dict);
72
+ optimisticDicts.push(dict);
73
+ echoedIds.push(id);
74
+ }
75
+ dispatchInput[messagesKey] = dispatchEntries;
76
+ return {
77
+ dispatchInput,
78
+ optimisticMessages: require_message_coercion.ensureMessageInstances(optimisticDicts),
79
+ echoedIds,
80
+ extraValues
81
+ };
82
+ }
83
+ //#endregion
84
+ exports.prepareOptimisticInput = prepareOptimisticInput;
85
+
86
+ //# sourceMappingURL=optimistic-input.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"optimistic-input.cjs","names":["toMessageDict","ensureMessageInstances"],"sources":["../../src/stream/optimistic-input.ts"],"sourcesContent":["/**\n * Pure helpers for the optimistic `submit()` path.\n *\n * Splitting the input-shaping logic out of {@link StreamController}\n * keeps it unit-testable in isolation: given a raw submit input it\n * produces (a) the payload to dispatch to the server — with stable ids\n * minted for any id-less message so the server echo reconciles by id —\n * and (b) the coerced `BaseMessage` instances to append to the root\n * projection immediately.\n *\n * No mutation of the caller's input is performed: message entries are\n * rebuilt as fresh dicts before ids are injected, and the top-level\n * object is shallow-cloned.\n */\nimport type { BaseMessage } from \"@langchain/core/messages\";\nimport type { Message } from \"../types.messages.js\";\nimport { toMessageDict } from \"../ui/messages.js\";\nimport { ensureMessageInstances } from \"./message-coercion.js\";\n\n/**\n * Pre-submit snapshot of a single non-message `values` key, captured so\n * it can be rolled back if the run fails before the server echoes any\n * `values`.\n */\nexport interface OptimisticKeySnapshot {\n readonly key: string;\n /** Whether the key existed in `values` before the optimistic merge. */\n readonly hadKey: boolean;\n /** The pre-submit value (meaningful only when `hadKey` is true). */\n readonly prevValue: unknown;\n}\n\n/**\n * Opaque handle returned by the controller's optimistic apply step and\n * threaded back through the submit coordinator to the terminal\n * reconciliation step. Carries the echoed message ids (to transition\n * `pending` → `sent` / `failed`) and the non-message key snapshot (to\n * roll back on failure-before-echo).\n */\nexport interface OptimisticHandle {\n readonly echoedIds: string[];\n readonly restoreKeys: OptimisticKeySnapshot[];\n}\n\n/**\n * Result of preparing a raw submit input for optimistic dispatch.\n */\nexport interface PreparedOptimisticInput {\n /**\n * Input to actually send to the server. The messages key (when\n * present) is normalized to an array of message dicts, each carrying\n * a stable id; all other keys are copied verbatim.\n */\n readonly dispatchInput: Record<string, unknown>;\n /** Coerced message instances (with ids) to append to the projection. */\n readonly optimisticMessages: BaseMessage[];\n /** Ids of the messages echoed optimistically (minted or pre-existing). */\n readonly echoedIds: string[];\n /** Non-message input keys to shallow-merge into `values`. */\n readonly extraValues: Record<string, unknown>;\n}\n\nfunction isBaseMessageInstance(value: unknown): value is BaseMessage {\n return (\n value != null &&\n typeof (value as { getType?: unknown }).getType === \"function\"\n );\n}\n\nfunction extractId(value: unknown): string | undefined {\n const id = (value as { id?: unknown } | null)?.id;\n return typeof id === \"string\" && id.length > 0 ? id : undefined;\n}\n\n/**\n * Normalize a message-key value into an array of entries. Mirrors the\n * server's `add_messages` coercion: a bare string or single message\n * object is treated as a one-element list.\n */\nfunction toEntryArray(value: unknown): unknown[] {\n if (Array.isArray(value)) return value;\n return [value];\n}\n\n/**\n * Build a message dict carrying `id` from an arbitrary input entry,\n * without mutating the original.\n */\nfunction toDispatchDict(entry: unknown, id: string): Message {\n if (typeof entry === \"string\") {\n return { type: \"human\", content: entry, id } as unknown as Message;\n }\n if (isBaseMessageInstance(entry)) {\n return { ...toMessageDict(entry), id } as Message;\n }\n return { ...(entry as object), id } as Message;\n}\n\n/**\n * Prepare a raw submit input for optimistic dispatch.\n *\n * @param raw - Raw input passed to `submit()`. Must be a\n * non-null, non-array object (caller guards this).\n * @param messagesKey - State key holding the message array.\n * @param mintId - Factory for stable client message ids.\n * @returns The dispatch payload, optimistic messages, echoed ids, and\n * the non-message portion of the input.\n */\nexport function prepareOptimisticInput(\n raw: Record<string, unknown>,\n messagesKey: string,\n mintId: () => string\n): PreparedOptimisticInput {\n const dispatchInput: Record<string, unknown> = { ...raw };\n const extraValues: Record<string, unknown> = {};\n for (const key of Object.keys(raw)) {\n if (key !== messagesKey) extraValues[key] = raw[key];\n }\n\n const echoedIds: string[] = [];\n const messagesValue = raw[messagesKey];\n if (messagesValue == null) {\n return { dispatchInput, optimisticMessages: [], echoedIds, extraValues };\n }\n\n const entries = toEntryArray(messagesValue);\n const dispatchEntries: unknown[] = [];\n const optimisticDicts: Message[] = [];\n for (const entry of entries) {\n const echoable =\n typeof entry === \"string\" ||\n isBaseMessageInstance(entry) ||\n (entry != null && typeof entry === \"object\" && !Array.isArray(entry));\n if (!echoable) {\n // Non-message-shaped entry (number/bool/null): forward as-is,\n // nothing to echo.\n dispatchEntries.push(entry);\n continue;\n }\n const id = extractId(entry) ?? mintId();\n const dict = toDispatchDict(entry, id);\n dispatchEntries.push(dict);\n optimisticDicts.push(dict);\n echoedIds.push(id);\n }\n\n dispatchInput[messagesKey] = dispatchEntries;\n const optimisticMessages = ensureMessageInstances(\n optimisticDicts\n ) as BaseMessage[];\n return { dispatchInput, optimisticMessages, echoedIds, extraValues };\n}\n"],"mappings":";;;AA8DA,SAAS,sBAAsB,OAAsC;AACnE,QACE,SAAS,QACT,OAAQ,MAAgC,YAAY;;AAIxD,SAAS,UAAU,OAAoC;CACrD,MAAM,KAAM,OAAmC;AAC/C,QAAO,OAAO,OAAO,YAAY,GAAG,SAAS,IAAI,KAAK,KAAA;;;;;;;AAQxD,SAAS,aAAa,OAA2B;AAC/C,KAAI,MAAM,QAAQ,MAAM,CAAE,QAAO;AACjC,QAAO,CAAC,MAAM;;;;;;AAOhB,SAAS,eAAe,OAAgB,IAAqB;AAC3D,KAAI,OAAO,UAAU,SACnB,QAAO;EAAE,MAAM;EAAS,SAAS;EAAO;EAAI;AAE9C,KAAI,sBAAsB,MAAM,CAC9B,QAAO;EAAE,GAAGA,iBAAAA,cAAc,MAAM;EAAE;EAAI;AAExC,QAAO;EAAE,GAAI;EAAkB;EAAI;;;;;;;;;;;;AAarC,SAAgB,uBACd,KACA,aACA,QACyB;CACzB,MAAM,gBAAyC,EAAE,GAAG,KAAK;CACzD,MAAM,cAAuC,EAAE;AAC/C,MAAK,MAAM,OAAO,OAAO,KAAK,IAAI,CAChC,KAAI,QAAQ,YAAa,aAAY,OAAO,IAAI;CAGlD,MAAM,YAAsB,EAAE;CAC9B,MAAM,gBAAgB,IAAI;AAC1B,KAAI,iBAAiB,KACnB,QAAO;EAAE;EAAe,oBAAoB,EAAE;EAAE;EAAW;EAAa;CAG1E,MAAM,UAAU,aAAa,cAAc;CAC3C,MAAM,kBAA6B,EAAE;CACrC,MAAM,kBAA6B,EAAE;AACrC,MAAK,MAAM,SAAS,SAAS;AAK3B,MAAI,EAHF,OAAO,UAAU,YACjB,sBAAsB,MAAM,IAC3B,SAAS,QAAQ,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,MAAM,GACvD;AAGb,mBAAgB,KAAK,MAAM;AAC3B;;EAEF,MAAM,KAAK,UAAU,MAAM,IAAI,QAAQ;EACvC,MAAM,OAAO,eAAe,OAAO,GAAG;AACtC,kBAAgB,KAAK,KAAK;AAC1B,kBAAgB,KAAK,KAAK;AAC1B,YAAU,KAAK,GAAG;;AAGpB,eAAc,eAAe;AAI7B,QAAO;EAAE;EAAe,oBAHGC,yBAAAA,uBACzB,gBACD;EAC2C;EAAW;EAAa"}
@@ -0,0 +1 @@
1
+ import { BaseMessage } from "@langchain/core/messages";
@@ -0,0 +1,86 @@
1
+ import { ensureMessageInstances } from "./message-coercion.js";
2
+ import { toMessageDict } from "../ui/messages.js";
3
+ //#region src/stream/optimistic-input.ts
4
+ function isBaseMessageInstance(value) {
5
+ return value != null && typeof value.getType === "function";
6
+ }
7
+ function extractId(value) {
8
+ const id = value?.id;
9
+ return typeof id === "string" && id.length > 0 ? id : void 0;
10
+ }
11
+ /**
12
+ * Normalize a message-key value into an array of entries. Mirrors the
13
+ * server's `add_messages` coercion: a bare string or single message
14
+ * object is treated as a one-element list.
15
+ */
16
+ function toEntryArray(value) {
17
+ if (Array.isArray(value)) return value;
18
+ return [value];
19
+ }
20
+ /**
21
+ * Build a message dict carrying `id` from an arbitrary input entry,
22
+ * without mutating the original.
23
+ */
24
+ function toDispatchDict(entry, id) {
25
+ if (typeof entry === "string") return {
26
+ type: "human",
27
+ content: entry,
28
+ id
29
+ };
30
+ if (isBaseMessageInstance(entry)) return {
31
+ ...toMessageDict(entry),
32
+ id
33
+ };
34
+ return {
35
+ ...entry,
36
+ id
37
+ };
38
+ }
39
+ /**
40
+ * Prepare a raw submit input for optimistic dispatch.
41
+ *
42
+ * @param raw - Raw input passed to `submit()`. Must be a
43
+ * non-null, non-array object (caller guards this).
44
+ * @param messagesKey - State key holding the message array.
45
+ * @param mintId - Factory for stable client message ids.
46
+ * @returns The dispatch payload, optimistic messages, echoed ids, and
47
+ * the non-message portion of the input.
48
+ */
49
+ function prepareOptimisticInput(raw, messagesKey, mintId) {
50
+ const dispatchInput = { ...raw };
51
+ const extraValues = {};
52
+ for (const key of Object.keys(raw)) if (key !== messagesKey) extraValues[key] = raw[key];
53
+ const echoedIds = [];
54
+ const messagesValue = raw[messagesKey];
55
+ if (messagesValue == null) return {
56
+ dispatchInput,
57
+ optimisticMessages: [],
58
+ echoedIds,
59
+ extraValues
60
+ };
61
+ const entries = toEntryArray(messagesValue);
62
+ const dispatchEntries = [];
63
+ const optimisticDicts = [];
64
+ for (const entry of entries) {
65
+ if (!(typeof entry === "string" || isBaseMessageInstance(entry) || entry != null && typeof entry === "object" && !Array.isArray(entry))) {
66
+ dispatchEntries.push(entry);
67
+ continue;
68
+ }
69
+ const id = extractId(entry) ?? mintId();
70
+ const dict = toDispatchDict(entry, id);
71
+ dispatchEntries.push(dict);
72
+ optimisticDicts.push(dict);
73
+ echoedIds.push(id);
74
+ }
75
+ dispatchInput[messagesKey] = dispatchEntries;
76
+ return {
77
+ dispatchInput,
78
+ optimisticMessages: ensureMessageInstances(optimisticDicts),
79
+ echoedIds,
80
+ extraValues
81
+ };
82
+ }
83
+ //#endregion
84
+ export { prepareOptimisticInput };
85
+
86
+ //# sourceMappingURL=optimistic-input.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"optimistic-input.js","names":[],"sources":["../../src/stream/optimistic-input.ts"],"sourcesContent":["/**\n * Pure helpers for the optimistic `submit()` path.\n *\n * Splitting the input-shaping logic out of {@link StreamController}\n * keeps it unit-testable in isolation: given a raw submit input it\n * produces (a) the payload to dispatch to the server — with stable ids\n * minted for any id-less message so the server echo reconciles by id —\n * and (b) the coerced `BaseMessage` instances to append to the root\n * projection immediately.\n *\n * No mutation of the caller's input is performed: message entries are\n * rebuilt as fresh dicts before ids are injected, and the top-level\n * object is shallow-cloned.\n */\nimport type { BaseMessage } from \"@langchain/core/messages\";\nimport type { Message } from \"../types.messages.js\";\nimport { toMessageDict } from \"../ui/messages.js\";\nimport { ensureMessageInstances } from \"./message-coercion.js\";\n\n/**\n * Pre-submit snapshot of a single non-message `values` key, captured so\n * it can be rolled back if the run fails before the server echoes any\n * `values`.\n */\nexport interface OptimisticKeySnapshot {\n readonly key: string;\n /** Whether the key existed in `values` before the optimistic merge. */\n readonly hadKey: boolean;\n /** The pre-submit value (meaningful only when `hadKey` is true). */\n readonly prevValue: unknown;\n}\n\n/**\n * Opaque handle returned by the controller's optimistic apply step and\n * threaded back through the submit coordinator to the terminal\n * reconciliation step. Carries the echoed message ids (to transition\n * `pending` → `sent` / `failed`) and the non-message key snapshot (to\n * roll back on failure-before-echo).\n */\nexport interface OptimisticHandle {\n readonly echoedIds: string[];\n readonly restoreKeys: OptimisticKeySnapshot[];\n}\n\n/**\n * Result of preparing a raw submit input for optimistic dispatch.\n */\nexport interface PreparedOptimisticInput {\n /**\n * Input to actually send to the server. The messages key (when\n * present) is normalized to an array of message dicts, each carrying\n * a stable id; all other keys are copied verbatim.\n */\n readonly dispatchInput: Record<string, unknown>;\n /** Coerced message instances (with ids) to append to the projection. */\n readonly optimisticMessages: BaseMessage[];\n /** Ids of the messages echoed optimistically (minted or pre-existing). */\n readonly echoedIds: string[];\n /** Non-message input keys to shallow-merge into `values`. */\n readonly extraValues: Record<string, unknown>;\n}\n\nfunction isBaseMessageInstance(value: unknown): value is BaseMessage {\n return (\n value != null &&\n typeof (value as { getType?: unknown }).getType === \"function\"\n );\n}\n\nfunction extractId(value: unknown): string | undefined {\n const id = (value as { id?: unknown } | null)?.id;\n return typeof id === \"string\" && id.length > 0 ? id : undefined;\n}\n\n/**\n * Normalize a message-key value into an array of entries. Mirrors the\n * server's `add_messages` coercion: a bare string or single message\n * object is treated as a one-element list.\n */\nfunction toEntryArray(value: unknown): unknown[] {\n if (Array.isArray(value)) return value;\n return [value];\n}\n\n/**\n * Build a message dict carrying `id` from an arbitrary input entry,\n * without mutating the original.\n */\nfunction toDispatchDict(entry: unknown, id: string): Message {\n if (typeof entry === \"string\") {\n return { type: \"human\", content: entry, id } as unknown as Message;\n }\n if (isBaseMessageInstance(entry)) {\n return { ...toMessageDict(entry), id } as Message;\n }\n return { ...(entry as object), id } as Message;\n}\n\n/**\n * Prepare a raw submit input for optimistic dispatch.\n *\n * @param raw - Raw input passed to `submit()`. Must be a\n * non-null, non-array object (caller guards this).\n * @param messagesKey - State key holding the message array.\n * @param mintId - Factory for stable client message ids.\n * @returns The dispatch payload, optimistic messages, echoed ids, and\n * the non-message portion of the input.\n */\nexport function prepareOptimisticInput(\n raw: Record<string, unknown>,\n messagesKey: string,\n mintId: () => string\n): PreparedOptimisticInput {\n const dispatchInput: Record<string, unknown> = { ...raw };\n const extraValues: Record<string, unknown> = {};\n for (const key of Object.keys(raw)) {\n if (key !== messagesKey) extraValues[key] = raw[key];\n }\n\n const echoedIds: string[] = [];\n const messagesValue = raw[messagesKey];\n if (messagesValue == null) {\n return { dispatchInput, optimisticMessages: [], echoedIds, extraValues };\n }\n\n const entries = toEntryArray(messagesValue);\n const dispatchEntries: unknown[] = [];\n const optimisticDicts: Message[] = [];\n for (const entry of entries) {\n const echoable =\n typeof entry === \"string\" ||\n isBaseMessageInstance(entry) ||\n (entry != null && typeof entry === \"object\" && !Array.isArray(entry));\n if (!echoable) {\n // Non-message-shaped entry (number/bool/null): forward as-is,\n // nothing to echo.\n dispatchEntries.push(entry);\n continue;\n }\n const id = extractId(entry) ?? mintId();\n const dict = toDispatchDict(entry, id);\n dispatchEntries.push(dict);\n optimisticDicts.push(dict);\n echoedIds.push(id);\n }\n\n dispatchInput[messagesKey] = dispatchEntries;\n const optimisticMessages = ensureMessageInstances(\n optimisticDicts\n ) as BaseMessage[];\n return { dispatchInput, optimisticMessages, echoedIds, extraValues };\n}\n"],"mappings":";;;AA8DA,SAAS,sBAAsB,OAAsC;AACnE,QACE,SAAS,QACT,OAAQ,MAAgC,YAAY;;AAIxD,SAAS,UAAU,OAAoC;CACrD,MAAM,KAAM,OAAmC;AAC/C,QAAO,OAAO,OAAO,YAAY,GAAG,SAAS,IAAI,KAAK,KAAA;;;;;;;AAQxD,SAAS,aAAa,OAA2B;AAC/C,KAAI,MAAM,QAAQ,MAAM,CAAE,QAAO;AACjC,QAAO,CAAC,MAAM;;;;;;AAOhB,SAAS,eAAe,OAAgB,IAAqB;AAC3D,KAAI,OAAO,UAAU,SACnB,QAAO;EAAE,MAAM;EAAS,SAAS;EAAO;EAAI;AAE9C,KAAI,sBAAsB,MAAM,CAC9B,QAAO;EAAE,GAAG,cAAc,MAAM;EAAE;EAAI;AAExC,QAAO;EAAE,GAAI;EAAkB;EAAI;;;;;;;;;;;;AAarC,SAAgB,uBACd,KACA,aACA,QACyB;CACzB,MAAM,gBAAyC,EAAE,GAAG,KAAK;CACzD,MAAM,cAAuC,EAAE;AAC/C,MAAK,MAAM,OAAO,OAAO,KAAK,IAAI,CAChC,KAAI,QAAQ,YAAa,aAAY,OAAO,IAAI;CAGlD,MAAM,YAAsB,EAAE;CAC9B,MAAM,gBAAgB,IAAI;AAC1B,KAAI,iBAAiB,KACnB,QAAO;EAAE;EAAe,oBAAoB,EAAE;EAAE;EAAW;EAAa;CAG1E,MAAM,UAAU,aAAa,cAAc;CAC3C,MAAM,kBAA6B,EAAE;CACrC,MAAM,kBAA6B,EAAE;AACrC,MAAK,MAAM,SAAS,SAAS;AAK3B,MAAI,EAHF,OAAO,UAAU,YACjB,sBAAsB,MAAM,IAC3B,SAAS,QAAQ,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,MAAM,GACvD;AAGb,mBAAgB,KAAK,MAAM;AAC3B;;EAEF,MAAM,KAAK,UAAU,MAAM,IAAI,QAAQ;EACvC,MAAM,OAAO,eAAe,OAAO,GAAG;AACtC,kBAAgB,KAAK,KAAK;AAC1B,kBAAgB,KAAK,KAAK;AAC1B,YAAU,KAAK,GAAG;;AAGpB,eAAc,eAAe;AAI7B,QAAO;EAAE;EAAe,oBAHG,uBACzB,gBACD;EAC2C;EAAW;EAAa"}
@@ -1,5 +1,5 @@
1
- const require_messages = require("../../ui/messages.cjs");
2
- const require_messages$1 = require("../../client/stream/messages.cjs");
1
+ const require_message_coercion = require("../message-coercion.cjs");
2
+ const require_messages = require("../../client/stream/messages.cjs");
3
3
  const require_namespace = require("../namespace.cjs");
4
4
  const require_assembled_to_message = require("../assembled-to-message.cjs");
5
5
  const require_message_reconciliation = require("../message-reconciliation.cjs");
@@ -12,7 +12,7 @@ function messagesProjection(namespace) {
12
12
  namespace: ns,
13
13
  initial: [],
14
14
  open({ thread, store, rootBus }) {
15
- const assembler = new require_messages$1.MessageAssembler();
15
+ const assembler = new require_messages.MessageAssembler();
16
16
  const roleByKey = /* @__PURE__ */ new Map();
17
17
  const indexById = /* @__PURE__ */ new Map();
18
18
  const streamMessageIds = /* @__PURE__ */ new Set();
@@ -52,7 +52,7 @@ function messagesProjection(namespace) {
52
52
  const rawMessages = data.messages;
53
53
  if (!Array.isArray(rawMessages) || rawMessages.length === 0) return;
54
54
  const reconciliation = require_message_reconciliation.reconcileMessagesFromValues({
55
- valueMessages: require_messages.ensureMessageInstances(rawMessages),
55
+ valueMessages: require_message_coercion.ensureMessageInstances(rawMessages),
56
56
  currentMessages: pendingMessages,
57
57
  currentIndexById: indexById,
58
58
  previousValueMessageIds: valuesMessageIds,
@@ -94,15 +94,25 @@ function messagesProjection(namespace) {
94
94
  } else pendingMessages[existingIdx] = base;
95
95
  scheduleFlush();
96
96
  };
97
- const runtime = require_runtime.openProjectionSubscription({
98
- thread,
99
- channels: ["messages", "values"],
100
- namespace: ns,
101
- onEvent(event) {
102
- if (event.method === "messages") applyEvent(event);
103
- else if (event.method === "values") applyValuesEvent(event);
104
- }
105
- });
97
+ let runtime;
98
+ const openSubscription = () => {
99
+ runtime = require_runtime.openProjectionSubscription({
100
+ thread,
101
+ channels: ["messages", "values"],
102
+ namespace: ns,
103
+ onEvent(event) {
104
+ if (event.method === "messages") applyEvent(event);
105
+ else if (event.method === "values") applyValuesEvent(event);
106
+ }
107
+ });
108
+ };
109
+ (async () => {
110
+ if (!(await rootBus.trySeedFromHistory?.({
111
+ kind: "messages",
112
+ namespace: ns,
113
+ store
114
+ }) === true) && !disposed) openSubscription();
115
+ })();
106
116
  return { async dispose() {
107
117
  disposed = true;
108
118
  if (flushChannel != null) {
@@ -110,7 +120,7 @@ function messagesProjection(namespace) {
110
120
  flushChannel.port1.close();
111
121
  flushChannel.port2.close();
112
122
  }
113
- await runtime.dispose();
123
+ await runtime?.dispose();
114
124
  } };
115
125
  }
116
126
  };