@langchain/langgraph-sdk 1.9.28 → 1.9.30

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 (49) hide show
  1. package/dist/client/stream/types.d.cts +4 -4
  2. package/dist/client/stream/types.d.ts +4 -4
  3. package/dist/schema.d.cts +7 -0
  4. package/dist/schema.d.cts.map +1 -1
  5. package/dist/schema.d.ts +7 -0
  6. package/dist/schema.d.ts.map +1 -1
  7. package/dist/stream/controller.cjs +135 -71
  8. package/dist/stream/controller.cjs.map +1 -1
  9. package/dist/stream/controller.d.cts +12 -23
  10. package/dist/stream/controller.d.cts.map +1 -1
  11. package/dist/stream/controller.d.ts +12 -23
  12. package/dist/stream/controller.d.ts.map +1 -1
  13. package/dist/stream/controller.js +135 -71
  14. package/dist/stream/controller.js.map +1 -1
  15. package/dist/stream/lifecycle-loading-tracker.cjs +12 -2
  16. package/dist/stream/lifecycle-loading-tracker.cjs.map +1 -1
  17. package/dist/stream/lifecycle-loading-tracker.js +12 -2
  18. package/dist/stream/lifecycle-loading-tracker.js.map +1 -1
  19. package/dist/stream/message-reconciliation.cjs +80 -4
  20. package/dist/stream/message-reconciliation.cjs.map +1 -1
  21. package/dist/stream/message-reconciliation.js +80 -4
  22. package/dist/stream/message-reconciliation.js.map +1 -1
  23. package/dist/stream/projections/channel.cjs +4 -6
  24. package/dist/stream/projections/channel.cjs.map +1 -1
  25. package/dist/stream/projections/channel.d.cts.map +1 -1
  26. package/dist/stream/projections/channel.d.ts.map +1 -1
  27. package/dist/stream/projections/channel.js +4 -6
  28. package/dist/stream/projections/channel.js.map +1 -1
  29. package/dist/stream/projections/messages.cjs +1 -1
  30. package/dist/stream/projections/messages.cjs.map +1 -1
  31. package/dist/stream/projections/messages.js +2 -2
  32. package/dist/stream/projections/messages.js.map +1 -1
  33. package/dist/stream/root-message-projection.cjs +12 -6
  34. package/dist/stream/root-message-projection.cjs.map +1 -1
  35. package/dist/stream/root-message-projection.js +13 -7
  36. package/dist/stream/root-message-projection.js.map +1 -1
  37. package/dist/stream/submit-coordinator.cjs +6 -5
  38. package/dist/stream/submit-coordinator.cjs.map +1 -1
  39. package/dist/stream/submit-coordinator.js +6 -5
  40. package/dist/stream/submit-coordinator.js.map +1 -1
  41. package/dist/stream/types.d.cts +3 -3
  42. package/dist/stream/types.d.ts +3 -3
  43. package/dist/utils/async_caller.cjs +1 -1
  44. package/dist/utils/async_caller.cjs.map +1 -1
  45. package/dist/utils/async_caller.d.cts.map +1 -1
  46. package/dist/utils/async_caller.d.ts.map +1 -1
  47. package/dist/utils/async_caller.js +1 -1
  48. package/dist/utils/async_caller.js.map +1 -1
  49. package/package.json +7 -7
@@ -1,3 +1,4 @@
1
+ import { inferChannel } from "../../client/stream/subscription.js";
1
2
  import { isRootNamespace, namespaceKey } from "../namespace.js";
2
3
  import { openProjectionSubscription } from "./runtime.js";
3
4
  //#region src/stream/projections/channel.ts
@@ -15,13 +16,10 @@ function channelProjection(channels, namespace, options = {}) {
15
16
  open({ thread, store, rootBus }) {
16
17
  if (!replay && isRootNamespace(ns) && chs.every((c) => rootBus.channels.includes(c))) {
17
18
  const requestedSet = new Set(chs);
18
- const namedCustom = /* @__PURE__ */ new Set();
19
- for (const channel of chs) if (channel.startsWith("custom:")) namedCustom.add(channel.slice(7));
20
19
  const matches = (event) => {
21
- if (requestedSet.has(event.method)) return true;
22
- if (event.method !== "custom" || namedCustom.size === 0) return false;
23
- const data = event.params.data;
24
- return typeof data?.name === "string" && namedCustom.has(data.name);
20
+ const channel = inferChannel(event);
21
+ if (channel === void 0) return false;
22
+ return requestedSet.has(channel) || channel.startsWith("custom:") && requestedSet.has("custom");
25
23
  };
26
24
  const push = (event) => {
27
25
  if (!matches(event)) return;
@@ -1 +1 @@
1
- {"version":3,"file":"channel.js","names":[],"sources":["../../../src/stream/projections/channel.ts"],"sourcesContent":["/**\n * Raw channel escape hatch.\n *\n * Subscribes to an arbitrary list of channels at an arbitrary\n * namespace and retains a bounded buffer of events. The subscription\n * resumes across serial runs, so the buffer keeps accumulating events\n * for the lifetime of the thread (use `extensionProjection` instead when\n * you only need the most-recent payload of a single `custom:<name>`\n * channel). Consumers that need assembly semantics should use\n * `messagesProjection`, `toolCallsProjection`, etc. instead; this one is\n * for inspection, custom reducers, or niche use-cases.\n */\nimport type { Channel, Event } from \"@langchain/protocol\";\nimport type { ProjectionSpec, ProjectionRuntime } from \"../types.js\";\nimport { isRootNamespace, namespaceKey } from \"../namespace.js\";\nimport { openProjectionSubscription } from \"./runtime.js\";\n\n/** Max events retained per raw subscription. Older events are dropped. */\nconst DEFAULT_BUFFER = 4096;\n\nexport interface ChannelProjectionOptions {\n /**\n * Maximum number of events retained in the projection snapshot.\n * Defaults to 4096 so replay-backed discovery hooks can tolerate\n * bursty token/media streams without dropping early lifecycle events.\n */\n bufferSize?: number;\n /**\n * Whether to open a real subscription and receive replayed history.\n * Defaults to true. Set false for live-only root-bus inspection when\n * replay is unnecessary.\n */\n replay?: boolean;\n}\n\nexport function channelProjection(\n channels: readonly Channel[],\n namespace: readonly string[],\n options: ChannelProjectionOptions = {}\n): ProjectionSpec<Event[]> {\n const chs = [...channels].sort();\n const ns = [...namespace];\n const bufferSize = options.bufferSize ?? DEFAULT_BUFFER;\n const replay = options.replay ?? true;\n const key = `channel|${bufferSize}|${replay ? \"replay\" : \"live\"}|${chs.join(\",\")}|${namespaceKey(ns)}`;\n\n return {\n key,\n namespace: ns,\n initial: [],\n open({ thread, store, rootBus }): ProjectionRuntime {\n // If this projection is scoped to the root namespace AND every\n // requested channel is already covered by the controller's root\n // pump, attach to the shared fan-out instead of opening a\n // second server subscription. This is the common case for\n // lightweight event-trace / debug panels.\n const covered =\n !replay &&\n isRootNamespace(ns) &&\n chs.every((c) => rootBus.channels.includes(c));\n\n if (covered) {\n const requestedSet = new Set(chs as Channel[]);\n // Pre-compute `custom:<name>` sub-filters so incoming events\n // can be matched in O(1). The server delivers named custom\n // events as `{ method: \"custom\", params: { data: { name } } }`,\n // so matching purely on `event.method` would miss them — we\n // need to peek at `data.name` when the caller asked for a\n // specific `custom:<name>` channel.\n const namedCustom = new Set<string>();\n for (const channel of chs) {\n if (channel.startsWith(\"custom:\")) {\n namedCustom.add(channel.slice(\"custom:\".length));\n }\n }\n const matches = (event: Event): boolean => {\n if (requestedSet.has(event.method as Channel)) return true;\n if (event.method !== \"custom\" || namedCustom.size === 0) {\n return false;\n }\n const data = (event.params as Record<string, unknown>).data as\n | { name?: unknown }\n | undefined;\n return typeof data?.name === \"string\" && namedCustom.has(data.name);\n };\n const push = (event: Event): void => {\n if (!matches(event)) return;\n const current = store.getSnapshot();\n const next =\n current.length >= bufferSize\n ? [...current.slice(current.length - bufferSize + 1), event]\n : [...current, event];\n store.setValue(next);\n };\n const unsubscribe = rootBus.subscribe(push);\n return {\n dispose() {\n unsubscribe();\n },\n };\n }\n\n return openProjectionSubscription({\n thread,\n channels: chs as Channel[],\n namespace: ns,\n // Keep the buffer alive across serial runs. Some transports pause a\n // subscription on each run's terminal lifecycle event; without\n // resuming, the channel would go silent after the first run and a\n // second prompt on the same thread would emit no further events.\n resumeOnPause: true,\n onEvent(event) {\n const current = store.getSnapshot();\n const next =\n current.length >= bufferSize\n ? [...current.slice(current.length - bufferSize + 1), event]\n : [...current, event];\n store.setValue(next);\n },\n });\n },\n };\n}\n"],"mappings":";;;;AAkBA,MAAM,iBAAiB;AAiBvB,SAAgB,kBACd,UACA,WACA,UAAoC,EAAE,EACb;CACzB,MAAM,MAAM,CAAC,GAAG,SAAS,CAAC,MAAM;CAChC,MAAM,KAAK,CAAC,GAAG,UAAU;CACzB,MAAM,aAAa,QAAQ,cAAc;CACzC,MAAM,SAAS,QAAQ,UAAU;AAGjC,QAAO;EACL,KAHU,WAAW,WAAW,GAAG,SAAS,WAAW,OAAO,GAAG,IAAI,KAAK,IAAI,CAAC,GAAG,aAAa,GAAG;EAIlG,WAAW;EACX,SAAS,EAAE;EACX,KAAK,EAAE,QAAQ,OAAO,WAA8B;AAWlD,OAJE,CAAC,UACD,gBAAgB,GAAG,IACnB,IAAI,OAAO,MAAM,QAAQ,SAAS,SAAS,EAAE,CAAC,EAEnC;IACX,MAAM,eAAe,IAAI,IAAI,IAAiB;IAO9C,MAAM,8BAAc,IAAI,KAAa;AACrC,SAAK,MAAM,WAAW,IACpB,KAAI,QAAQ,WAAW,UAAU,CAC/B,aAAY,IAAI,QAAQ,MAAM,EAAiB,CAAC;IAGpD,MAAM,WAAW,UAA0B;AACzC,SAAI,aAAa,IAAI,MAAM,OAAkB,CAAE,QAAO;AACtD,SAAI,MAAM,WAAW,YAAY,YAAY,SAAS,EACpD,QAAO;KAET,MAAM,OAAQ,MAAM,OAAmC;AAGvD,YAAO,OAAO,MAAM,SAAS,YAAY,YAAY,IAAI,KAAK,KAAK;;IAErE,MAAM,QAAQ,UAAuB;AACnC,SAAI,CAAC,QAAQ,MAAM,CAAE;KACrB,MAAM,UAAU,MAAM,aAAa;KACnC,MAAM,OACJ,QAAQ,UAAU,aACd,CAAC,GAAG,QAAQ,MAAM,QAAQ,SAAS,aAAa,EAAE,EAAE,MAAM,GAC1D,CAAC,GAAG,SAAS,MAAM;AACzB,WAAM,SAAS,KAAK;;IAEtB,MAAM,cAAc,QAAQ,UAAU,KAAK;AAC3C,WAAO,EACL,UAAU;AACR,kBAAa;OAEhB;;AAGH,UAAO,2BAA2B;IAChC;IACA,UAAU;IACV,WAAW;IAKX,eAAe;IACf,QAAQ,OAAO;KACb,MAAM,UAAU,MAAM,aAAa;KACnC,MAAM,OACJ,QAAQ,UAAU,aACd,CAAC,GAAG,QAAQ,MAAM,QAAQ,SAAS,aAAa,EAAE,EAAE,MAAM,GAC1D,CAAC,GAAG,SAAS,MAAM;AACzB,WAAM,SAAS,KAAK;;IAEvB,CAAC;;EAEL"}
1
+ {"version":3,"file":"channel.js","names":[],"sources":["../../../src/stream/projections/channel.ts"],"sourcesContent":["/**\n * Raw channel escape hatch.\n *\n * Subscribes to an arbitrary list of channels at an arbitrary\n * namespace and retains a bounded buffer of events. The subscription\n * resumes across serial runs, so the buffer keeps accumulating events\n * for the lifetime of the thread (use `extensionProjection` instead when\n * you only need the most-recent payload of a single `custom:<name>`\n * channel). Consumers that need assembly semantics should use\n * `messagesProjection`, `toolCallsProjection`, etc. instead; this one is\n * for inspection, custom reducers, or niche use-cases.\n */\nimport type { Channel, Event } from \"@langchain/protocol\";\nimport { inferChannel } from \"../../client/stream/subscription.js\";\nimport type { ProjectionSpec, ProjectionRuntime } from \"../types.js\";\nimport { isRootNamespace, namespaceKey } from \"../namespace.js\";\nimport { openProjectionSubscription } from \"./runtime.js\";\n\n/** Max events retained per raw subscription. Older events are dropped. */\nconst DEFAULT_BUFFER = 4096;\n\nexport interface ChannelProjectionOptions {\n /**\n * Maximum number of events retained in the projection snapshot.\n * Defaults to 4096 so replay-backed discovery hooks can tolerate\n * bursty token/media streams without dropping early lifecycle events.\n */\n bufferSize?: number;\n /**\n * Whether to open a real subscription and receive replayed history.\n * Defaults to true. Set false for live-only root-bus inspection when\n * replay is unnecessary.\n */\n replay?: boolean;\n}\n\nexport function channelProjection(\n channels: readonly Channel[],\n namespace: readonly string[],\n options: ChannelProjectionOptions = {}\n): ProjectionSpec<Event[]> {\n const chs = [...channels].sort();\n const ns = [...namespace];\n const bufferSize = options.bufferSize ?? DEFAULT_BUFFER;\n const replay = options.replay ?? true;\n const key = `channel|${bufferSize}|${replay ? \"replay\" : \"live\"}|${chs.join(\",\")}|${namespaceKey(ns)}`;\n\n return {\n key,\n namespace: ns,\n initial: [],\n open({ thread, store, rootBus }): ProjectionRuntime {\n // If this projection is scoped to the root namespace AND every\n // requested channel is already covered by the controller's root\n // pump, attach to the shared fan-out instead of opening a\n // second server subscription. This is the common case for\n // lightweight event-trace / debug panels.\n const covered =\n !replay &&\n isRootNamespace(ns) &&\n chs.every((c) => rootBus.channels.includes(c));\n\n if (covered) {\n const requestedSet = new Set(chs as Channel[]);\n // Classify via `inferChannel` so namespaced methods (e.g.\n // `input.requested` → `input`) and named custom events\n // (`custom` + `data.name` → `custom:<name>`) match the same\n // way as the slow path's `matchesSubscription`. Comparing\n // `event.method` to channel names would silently drop `input`.\n const matches = (event: Event): boolean => {\n const channel = inferChannel(event);\n if (channel === undefined) return false;\n return (\n requestedSet.has(channel) ||\n (channel.startsWith(\"custom:\") && requestedSet.has(\"custom\"))\n );\n };\n const push = (event: Event): void => {\n if (!matches(event)) return;\n const current = store.getSnapshot();\n const next =\n current.length >= bufferSize\n ? [...current.slice(current.length - bufferSize + 1), event]\n : [...current, event];\n store.setValue(next);\n };\n const unsubscribe = rootBus.subscribe(push);\n return {\n dispose() {\n unsubscribe();\n },\n };\n }\n\n return openProjectionSubscription({\n thread,\n channels: chs as Channel[],\n namespace: ns,\n // Keep the buffer alive across serial runs. Some transports pause a\n // subscription on each run's terminal lifecycle event; without\n // resuming, the channel would go silent after the first run and a\n // second prompt on the same thread would emit no further events.\n resumeOnPause: true,\n onEvent(event) {\n const current = store.getSnapshot();\n const next =\n current.length >= bufferSize\n ? [...current.slice(current.length - bufferSize + 1), event]\n : [...current, event];\n store.setValue(next);\n },\n });\n },\n };\n}\n"],"mappings":";;;;;AAmBA,MAAM,iBAAiB;AAiBvB,SAAgB,kBACd,UACA,WACA,UAAoC,EAAE,EACb;CACzB,MAAM,MAAM,CAAC,GAAG,SAAS,CAAC,MAAM;CAChC,MAAM,KAAK,CAAC,GAAG,UAAU;CACzB,MAAM,aAAa,QAAQ,cAAc;CACzC,MAAM,SAAS,QAAQ,UAAU;AAGjC,QAAO;EACL,KAHU,WAAW,WAAW,GAAG,SAAS,WAAW,OAAO,GAAG,IAAI,KAAK,IAAI,CAAC,GAAG,aAAa,GAAG;EAIlG,WAAW;EACX,SAAS,EAAE;EACX,KAAK,EAAE,QAAQ,OAAO,WAA8B;AAWlD,OAJE,CAAC,UACD,gBAAgB,GAAG,IACnB,IAAI,OAAO,MAAM,QAAQ,SAAS,SAAS,EAAE,CAAC,EAEnC;IACX,MAAM,eAAe,IAAI,IAAI,IAAiB;IAM9C,MAAM,WAAW,UAA0B;KACzC,MAAM,UAAU,aAAa,MAAM;AACnC,SAAI,YAAY,KAAA,EAAW,QAAO;AAClC,YACE,aAAa,IAAI,QAAQ,IACxB,QAAQ,WAAW,UAAU,IAAI,aAAa,IAAI,SAAS;;IAGhE,MAAM,QAAQ,UAAuB;AACnC,SAAI,CAAC,QAAQ,MAAM,CAAE;KACrB,MAAM,UAAU,MAAM,aAAa;KACnC,MAAM,OACJ,QAAQ,UAAU,aACd,CAAC,GAAG,QAAQ,MAAM,QAAQ,SAAS,aAAa,EAAE,EAAE,MAAM,GAC1D,CAAC,GAAG,SAAS,MAAM;AACzB,WAAM,SAAS,KAAK;;IAEtB,MAAM,cAAc,QAAQ,UAAU,KAAK;AAC3C,WAAO,EACL,UAAU;AACR,kBAAa;OAEhB;;AAGH,UAAO,2BAA2B;IAChC;IACA,UAAU;IACV,WAAW;IAKX,eAAe;IACf,QAAQ,OAAO;KACb,MAAM,UAAU,MAAM,aAAa;KACnC,MAAM,OACJ,QAAQ,UAAU,aACd,CAAC,GAAG,QAAQ,MAAM,QAAQ,SAAS,aAAa,EAAE,EAAE,MAAM,GAC1D,CAAC,GAAG,SAAS,MAAM;AACzB,WAAM,SAAS,KAAK;;IAEvB,CAAC;;EAEL"}
@@ -57,7 +57,7 @@ function messagesProjection(namespace) {
57
57
  currentIndexById: indexById,
58
58
  previousValueMessageIds: valuesMessageIds,
59
59
  streamedMessageIds: streamMessageIds,
60
- preferValuesMessage: require_message_reconciliation.shouldPreferValuesMessageForToolCalls
60
+ preferValuesMessage: require_message_reconciliation.shouldPreferValuesMessage
61
61
  });
62
62
  valuesMessageIds = reconciliation.valueMessageIds;
63
63
  const reconciledMessages = [...reconciliation.messages];
@@ -1 +1 @@
1
- {"version":3,"file":"messages.cjs","names":["namespaceKey","MessageAssembler","isRootNamespace","reconcileMessagesFromValues","ensureMessageInstances","shouldPreferValuesMessageForToolCalls","buildMessageIndex","assembledMessageToBaseMessage","openProjectionSubscription"],"sources":["../../../src/stream/projections/messages.ts"],"sourcesContent":["/**\n * Namespace-scoped `messages` projection.\n *\n * Opens `thread.subscribe({ channels: [\"messages\"], namespaces: [ns] })`\n * and folds each `messages` event through {@link MessageAssembler}.\n * Every update — start, block delta, block finish, message finish —\n * re-derives a `BaseMessage` class instance for the currently-active\n * message and updates its slot in the store.\n *\n * The projection emits `BaseMessage[]` (class instances from\n * `@langchain/core/messages`), never plain serialized objects.\n */\nimport type {\n MessagesEvent,\n MessageRole,\n MessageStartData,\n ValuesEvent,\n} from \"@langchain/protocol\";\nimport type { BaseMessage } from \"@langchain/core/messages\";\nimport { MessageAssembler } from \"../../client/stream/messages.js\";\nimport {\n assembledMessageToBaseMessage,\n type ExtendedMessageRole,\n} from \"../assembled-to-message.js\";\nimport type { Message } from \"../../types.messages.js\";\nimport type { ProjectionSpec, ProjectionRuntime } from \"../types.js\";\nimport { ensureMessageInstances } from \"../message-coercion.js\";\nimport { isRootNamespace, namespaceKey } from \"../namespace.js\";\nimport {\n buildMessageIndex,\n reconcileMessagesFromValues,\n shouldPreferValuesMessageForToolCalls,\n} from \"../message-reconciliation.js\";\nimport { openProjectionSubscription } from \"./runtime.js\";\n\nexport function messagesProjection(\n namespace: readonly string[]\n): ProjectionSpec<BaseMessage[]> {\n const ns = [...namespace];\n const key = `messages|${namespaceKey(ns)}`;\n\n return {\n key,\n namespace: ns,\n initial: [],\n open({ thread, store, rootBus }): ProjectionRuntime {\n const assembler = new MessageAssembler();\n // Per-messageId state needed for BaseMessage projection:\n // - `role` is only in the `message-start` event; we cache it\n // so subsequent delta events still produce a typed message.\n // - `toolCallId` is pulled from message-start extras when role\n // is `tool` (a convention we keep compatible with serialized\n // v1 tool messages).\n const roleByKey = new Map<\n string,\n { role: ExtendedMessageRole; toolCallId?: string }\n >();\n const indexById = new Map<string, number>();\n // Ids this projection has observed via the `messages` channel\n // (token-level deltas). Used by `applyValuesEvent` to prefer the\n // stream-assembled version over the values-coerced shape while a\n // turn is streaming, matching the root controller's policy.\n const streamMessageIds = new Set<string>();\n // Ids observed in the most recent `values.messages` snapshot.\n // Messages that were present in a prior snapshot but are absent\n // from this one are treated as explicit removals (server-side\n // `RemoveMessage` reducer deltas). Stream-only messages (seen on\n // the messages channel but never echoed in a values snapshot)\n // are preserved — their enclosing superstep may simply not have\n // committed yet.\n let valuesMessageIds = new Set<string>();\n\n // Root-scoped projections whose channels are already covered by\n // the controller's root pump attach to the shared fan-out\n // instead of opening a second server subscription. The root\n // pump runs at `{namespaces: [[]], depth: 1}`, which is exactly\n // the scope a root-namespace `messagesProjection` wants.\n const rootShortCircuit =\n isRootNamespace(ns) && rootBus.channels.includes(\"messages\");\n\n if (rootShortCircuit) {\n const unsubscribe = rootBus.subscribe((event) => {\n if (event.method !== \"messages\") return;\n if (!isRootNamespace(event.params.namespace)) return;\n applyEvent(event as MessagesEvent);\n });\n return {\n dispose() {\n unsubscribe();\n },\n };\n }\n\n let disposed = false;\n\n // Local mirror of the store contents. Every `applyEvent` /\n // `applyValuesEvent` mutates this synchronously; a coalesced\n // `scheduleFlush` copies it to `store` once per macrotask.\n //\n // Why the indirection? When a namespace-scoped projection\n // (e.g. a subagent modal opened after the run finished) first\n // subscribes, the server replays the entire history from\n // `seq=0`. Dozens of `messages`-channel events can land in a\n // single SSE parse — they drain through the `for await` loop\n // as a long microtask chain. Microtasks run before any\n // macrotask, so React's concurrent scheduler never gets a\n // chance to commit between updates. Calling `store.setValue`\n // per event in that burst overflows React's\n // `nestedUpdateCount` guard and throws \"Maximum update depth\n // exceeded\", permanently killing the projection and\n // leaving the store stuck at its first few messages.\n //\n // Batching via `MessageChannel` (macrotask) coalesces the\n // replay burst into one `setValue` call and lets React\n // commit between flushes for live token streaming too.\n const pendingMessages: BaseMessage[] = [];\n let dirty = false;\n let flushScheduled = false;\n const flushChannel =\n typeof MessageChannel !== \"undefined\" ? new MessageChannel() : null;\n\n const flush = (): void => {\n flushScheduled = false;\n if (!dirty || disposed) return;\n dirty = false;\n // `.slice()` breaks identity so React's `Object.is` bail-out\n // in `StreamStore.setValue` propagates the change.\n store.setValue(pendingMessages.slice());\n };\n if (flushChannel != null) {\n flushChannel.port1.onmessage = flush;\n }\n\n const scheduleFlush = (): void => {\n dirty = true;\n if (flushScheduled) return;\n flushScheduled = true;\n if (flushChannel != null) {\n flushChannel.port2.postMessage(null);\n } else {\n setTimeout(flush, 0);\n }\n };\n\n // Rebuild the store from `values.messages` snapshots.\n //\n // `values` events carry the full, committed state of the\n // thread's `messages` channel at a checkpoint — they fire\n // on node completion, AFTER every `messages`-channel delta\n // for that turn has been emitted. They are the authoritative\n // source of truth for ORDER and for non-streamed messages\n // (human turns, serialised tool results, subagent echoes, …).\n //\n // Why rebuild rather than merge-by-id?\n //\n // In practice the server may emit the same logical message\n // with DIFFERENT ids across successive `values` snapshots at\n // the same namespace — e.g. a subagent first surfaces its\n // seed prompt with a synthetic id like\n // `subagent:<tool_call_id>:human`, then a later superstep\n // echoes the same prompt back with a real UUID (or vice\n // versa). A naive \"match-or-append by id\" strategy treats\n // each fresh id as a new entry and the list grows\n // monotonically, showing the same content twice (or more)\n // in the UI.\n //\n // Policy (mirrors the root controller's `#applyValues`):\n //\n // 1. Walk `values.messages` in order. For each id, prefer\n // the stream-assembled entry if we have one for that id\n // (keeps in-progress token streaming visible); otherwise\n // take the values-coerced instance. This self-heals the\n // two classes of glitch the old merge-by-id handler\n // targeted:\n // - tool messages arriving without `tool_call_id` on\n // the messages channel — the values snapshot always\n // carries it;\n // - AI messages whose finalized `tool_calls` didn't\n // fully land via the messages channel — the values\n // snapshot's AI message has them populated.\n //\n // 2. Append any stream-only ids (seen on the messages\n // channel but never echoed in ANY values snapshot yet)\n // — their enclosing superstep hasn't committed yet, so\n // dropping them would flash the UI.\n //\n // 3. Ids that WERE in a prior values snapshot but are gone\n // from this one are treated as explicit removals\n // (`RemoveMessage` reducer deltas) and dropped.\n //\n // Unkeyed messages (no stable id) are passed through in\n // their values order because we can't dedupe them safely.\n const applyValuesEvent = (event: ValuesEvent): void => {\n const data = event.params.data;\n if (data == null || typeof data !== \"object\" || Array.isArray(data)) {\n return;\n }\n const state = data as Record<string, unknown>;\n const rawMessages = state.messages;\n if (!Array.isArray(rawMessages) || rawMessages.length === 0) return;\n\n const coerced = ensureMessageInstances(\n rawMessages as (Message | BaseMessage)[]\n );\n\n const reconciliation = reconcileMessagesFromValues({\n valueMessages: coerced,\n currentMessages: pendingMessages,\n currentIndexById: indexById,\n previousValueMessageIds: valuesMessageIds,\n streamedMessageIds: streamMessageIds,\n preferValuesMessage: shouldPreferValuesMessageForToolCalls,\n });\n valuesMessageIds = reconciliation.valueMessageIds;\n const reconciledMessages = [...reconciliation.messages];\n\n pendingMessages.length = 0;\n for (const message of reconciledMessages) pendingMessages.push(message);\n indexById.clear();\n for (const [id, idx] of buildMessageIndex(pendingMessages)) {\n indexById.set(id, idx);\n }\n scheduleFlush();\n };\n\n const applyEvent = (event: MessagesEvent): void => {\n const data = event.params.data;\n\n if (data.event === \"message-start\") {\n const startData = data as MessageStartData;\n const role = (startData.role ?? \"ai\") as MessageRole;\n // \"tool\" role is a v1 convention not represented in the\n // protocol enum but common in practice — keep it working\n // for graphs that emit it as an extensible field.\n const extendedRole =\n (startData as { role?: ExtendedMessageRole }).role ?? role;\n const maybeToolCallId = (startData as { tool_call_id?: string })\n .tool_call_id;\n if (startData.id != null) {\n roleByKey.set(startData.id, {\n role: extendedRole,\n toolCallId: maybeToolCallId,\n });\n }\n }\n\n const update = assembler.consume(event);\n if (update == null) return;\n const msg = update.message;\n const id = msg.id;\n if (id == null) return;\n const captured = roleByKey.get(id) ?? { role: \"ai\" as const };\n const base = assembledMessageToBaseMessage(msg, captured.role, {\n toolCallId: captured.toolCallId,\n });\n\n streamMessageIds.add(id);\n const existingIdx = indexById.get(id);\n if (existingIdx == null) {\n indexById.set(id, pendingMessages.length);\n pendingMessages.push(base);\n } else {\n pendingMessages[existingIdx] = base;\n }\n scheduleFlush();\n };\n\n let runtime: ProjectionRuntime | undefined;\n const openSubscription = () => {\n runtime = openProjectionSubscription({\n thread,\n // Subscribe to both `messages` (live token deltas that drive\n // the in-flight assistant bubble) and `values` (periodic full-\n // state snapshots). Consuming values lets late-mounted scoped\n // projections backfill history after the run has finished.\n channels: [\"messages\", \"values\"],\n namespace: ns,\n onEvent(event) {\n if (event.method === \"messages\") {\n applyEvent(event as MessagesEvent);\n } else if (event.method === \"values\") {\n applyValuesEvent(event as ValuesEvent);\n }\n },\n });\n };\n\n void (async () => {\n const seeded =\n (await rootBus.trySeedFromHistory?.({\n kind: \"messages\",\n namespace: ns,\n store,\n })) === true;\n if (!seeded && !disposed) openSubscription();\n })();\n\n return {\n async dispose() {\n disposed = true;\n if (flushChannel != null) {\n flushChannel.port1.onmessage = null;\n flushChannel.port1.close();\n flushChannel.port2.close();\n }\n await runtime?.dispose();\n },\n };\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"messages.cjs","names":["namespaceKey","MessageAssembler","isRootNamespace","reconcileMessagesFromValues","ensureMessageInstances","shouldPreferValuesMessage","buildMessageIndex","assembledMessageToBaseMessage","openProjectionSubscription"],"sources":["../../../src/stream/projections/messages.ts"],"sourcesContent":["/**\n * Namespace-scoped `messages` projection.\n *\n * Opens `thread.subscribe({ channels: [\"messages\"], namespaces: [ns] })`\n * and folds each `messages` event through {@link MessageAssembler}.\n * Every update — start, block delta, block finish, message finish —\n * re-derives a `BaseMessage` class instance for the currently-active\n * message and updates its slot in the store.\n *\n * The projection emits `BaseMessage[]` (class instances from\n * `@langchain/core/messages`), never plain serialized objects.\n */\nimport type {\n MessagesEvent,\n MessageRole,\n MessageStartData,\n ValuesEvent,\n} from \"@langchain/protocol\";\nimport type { BaseMessage } from \"@langchain/core/messages\";\nimport { MessageAssembler } from \"../../client/stream/messages.js\";\nimport {\n assembledMessageToBaseMessage,\n type ExtendedMessageRole,\n} from \"../assembled-to-message.js\";\nimport type { Message } from \"../../types.messages.js\";\nimport type { ProjectionSpec, ProjectionRuntime } from \"../types.js\";\nimport { ensureMessageInstances } from \"../message-coercion.js\";\nimport { isRootNamespace, namespaceKey } from \"../namespace.js\";\nimport {\n buildMessageIndex,\n reconcileMessagesFromValues,\n shouldPreferValuesMessage,\n} from \"../message-reconciliation.js\";\nimport { openProjectionSubscription } from \"./runtime.js\";\n\nexport function messagesProjection(\n namespace: readonly string[]\n): ProjectionSpec<BaseMessage[]> {\n const ns = [...namespace];\n const key = `messages|${namespaceKey(ns)}`;\n\n return {\n key,\n namespace: ns,\n initial: [],\n open({ thread, store, rootBus }): ProjectionRuntime {\n const assembler = new MessageAssembler();\n // Per-messageId state needed for BaseMessage projection:\n // - `role` is only in the `message-start` event; we cache it\n // so subsequent delta events still produce a typed message.\n // - `toolCallId` is pulled from message-start extras when role\n // is `tool` (a convention we keep compatible with serialized\n // v1 tool messages).\n const roleByKey = new Map<\n string,\n { role: ExtendedMessageRole; toolCallId?: string }\n >();\n const indexById = new Map<string, number>();\n // Ids this projection has observed via the `messages` channel\n // (token-level deltas). Used by `applyValuesEvent` to prefer the\n // stream-assembled version over the values-coerced shape while a\n // turn is streaming, matching the root controller's policy.\n const streamMessageIds = new Set<string>();\n // Ids observed in the most recent `values.messages` snapshot.\n // Messages that were present in a prior snapshot but are absent\n // from this one are treated as explicit removals (server-side\n // `RemoveMessage` reducer deltas). Stream-only messages (seen on\n // the messages channel but never echoed in a values snapshot)\n // are preserved — their enclosing superstep may simply not have\n // committed yet.\n let valuesMessageIds = new Set<string>();\n\n // Root-scoped projections whose channels are already covered by\n // the controller's root pump attach to the shared fan-out\n // instead of opening a second server subscription. The root\n // pump runs at `{namespaces: [[]], depth: 1}`, which is exactly\n // the scope a root-namespace `messagesProjection` wants.\n const rootShortCircuit =\n isRootNamespace(ns) && rootBus.channels.includes(\"messages\");\n\n if (rootShortCircuit) {\n const unsubscribe = rootBus.subscribe((event) => {\n if (event.method !== \"messages\") return;\n if (!isRootNamespace(event.params.namespace)) return;\n applyEvent(event as MessagesEvent);\n });\n return {\n dispose() {\n unsubscribe();\n },\n };\n }\n\n let disposed = false;\n\n // Local mirror of the store contents. Every `applyEvent` /\n // `applyValuesEvent` mutates this synchronously; a coalesced\n // `scheduleFlush` copies it to `store` once per macrotask.\n //\n // Why the indirection? When a namespace-scoped projection\n // (e.g. a subagent modal opened after the run finished) first\n // subscribes, the server replays the entire history from\n // `seq=0`. Dozens of `messages`-channel events can land in a\n // single SSE parse — they drain through the `for await` loop\n // as a long microtask chain. Microtasks run before any\n // macrotask, so React's concurrent scheduler never gets a\n // chance to commit between updates. Calling `store.setValue`\n // per event in that burst overflows React's\n // `nestedUpdateCount` guard and throws \"Maximum update depth\n // exceeded\", permanently killing the projection and\n // leaving the store stuck at its first few messages.\n //\n // Batching via `MessageChannel` (macrotask) coalesces the\n // replay burst into one `setValue` call and lets React\n // commit between flushes for live token streaming too.\n const pendingMessages: BaseMessage[] = [];\n let dirty = false;\n let flushScheduled = false;\n const flushChannel =\n typeof MessageChannel !== \"undefined\" ? new MessageChannel() : null;\n\n const flush = (): void => {\n flushScheduled = false;\n if (!dirty || disposed) return;\n dirty = false;\n // `.slice()` breaks identity so React's `Object.is` bail-out\n // in `StreamStore.setValue` propagates the change.\n store.setValue(pendingMessages.slice());\n };\n if (flushChannel != null) {\n flushChannel.port1.onmessage = flush;\n }\n\n const scheduleFlush = (): void => {\n dirty = true;\n if (flushScheduled) return;\n flushScheduled = true;\n if (flushChannel != null) {\n flushChannel.port2.postMessage(null);\n } else {\n setTimeout(flush, 0);\n }\n };\n\n // Rebuild the store from `values.messages` snapshots.\n //\n // `values` events carry the full, committed state of the\n // thread's `messages` channel at a checkpoint — they fire\n // on node completion, AFTER every `messages`-channel delta\n // for that turn has been emitted. They are the authoritative\n // source of truth for ORDER and for non-streamed messages\n // (human turns, serialised tool results, subagent echoes, …).\n //\n // Why rebuild rather than merge-by-id?\n //\n // In practice the server may emit the same logical message\n // with DIFFERENT ids across successive `values` snapshots at\n // the same namespace — e.g. a subagent first surfaces its\n // seed prompt with a synthetic id like\n // `subagent:<tool_call_id>:human`, then a later superstep\n // echoes the same prompt back with a real UUID (or vice\n // versa). A naive \"match-or-append by id\" strategy treats\n // each fresh id as a new entry and the list grows\n // monotonically, showing the same content twice (or more)\n // in the UI.\n //\n // Policy (mirrors the root controller's `#applyValues`):\n //\n // 1. Walk `values.messages` in order. For each id, prefer\n // the stream-assembled entry if we have one for that id\n // (keeps in-progress token streaming visible); otherwise\n // take the values-coerced instance. Same-id values still\n // win when they carry data the stream lacks:\n // - tool messages arriving without `tool_call_id` on\n // the messages channel — the values snapshot always\n // carries it;\n // - AI messages whose finalized `tool_calls` didn't\n // fully land via the messages channel — the values\n // snapshot's AI message has them populated;\n // - metadata enrichment with unchanged content (e.g.\n // committed `additional_kwargs` on an echoed human).\n //\n // 2. Append any stream-only ids (seen on the messages\n // channel but never echoed in ANY values snapshot yet)\n // — their enclosing superstep hasn't committed yet, so\n // dropping them would flash the UI.\n //\n // 3. Ids that WERE in a prior values snapshot but are gone\n // from this one are treated as explicit removals\n // (`RemoveMessage` reducer deltas) and dropped.\n //\n // Unkeyed messages (no stable id) are passed through in\n // their values order because we can't dedupe them safely.\n const applyValuesEvent = (event: ValuesEvent): void => {\n const data = event.params.data;\n if (data == null || typeof data !== \"object\" || Array.isArray(data)) {\n return;\n }\n const state = data as Record<string, unknown>;\n const rawMessages = state.messages;\n if (!Array.isArray(rawMessages) || rawMessages.length === 0) return;\n\n const coerced = ensureMessageInstances(\n rawMessages as (Message | BaseMessage)[]\n );\n\n const reconciliation = reconcileMessagesFromValues({\n valueMessages: coerced,\n currentMessages: pendingMessages,\n currentIndexById: indexById,\n previousValueMessageIds: valuesMessageIds,\n streamedMessageIds: streamMessageIds,\n preferValuesMessage: shouldPreferValuesMessage,\n });\n valuesMessageIds = reconciliation.valueMessageIds;\n const reconciledMessages = [...reconciliation.messages];\n\n pendingMessages.length = 0;\n for (const message of reconciledMessages) pendingMessages.push(message);\n indexById.clear();\n for (const [id, idx] of buildMessageIndex(pendingMessages)) {\n indexById.set(id, idx);\n }\n scheduleFlush();\n };\n\n const applyEvent = (event: MessagesEvent): void => {\n const data = event.params.data;\n\n if (data.event === \"message-start\") {\n const startData = data as MessageStartData;\n const role = (startData.role ?? \"ai\") as MessageRole;\n // \"tool\" role is a v1 convention not represented in the\n // protocol enum but common in practice — keep it working\n // for graphs that emit it as an extensible field.\n const extendedRole =\n (startData as { role?: ExtendedMessageRole }).role ?? role;\n const maybeToolCallId = (startData as { tool_call_id?: string })\n .tool_call_id;\n if (startData.id != null) {\n roleByKey.set(startData.id, {\n role: extendedRole,\n toolCallId: maybeToolCallId,\n });\n }\n }\n\n const update = assembler.consume(event);\n if (update == null) return;\n const msg = update.message;\n const id = msg.id;\n if (id == null) return;\n const captured = roleByKey.get(id) ?? { role: \"ai\" as const };\n const base = assembledMessageToBaseMessage(msg, captured.role, {\n toolCallId: captured.toolCallId,\n });\n\n streamMessageIds.add(id);\n const existingIdx = indexById.get(id);\n if (existingIdx == null) {\n indexById.set(id, pendingMessages.length);\n pendingMessages.push(base);\n } else {\n pendingMessages[existingIdx] = base;\n }\n scheduleFlush();\n };\n\n let runtime: ProjectionRuntime | undefined;\n const openSubscription = () => {\n runtime = openProjectionSubscription({\n thread,\n // Subscribe to both `messages` (live token deltas that drive\n // the in-flight assistant bubble) and `values` (periodic full-\n // state snapshots). Consuming values lets late-mounted scoped\n // projections backfill history after the run has finished.\n channels: [\"messages\", \"values\"],\n namespace: ns,\n onEvent(event) {\n if (event.method === \"messages\") {\n applyEvent(event as MessagesEvent);\n } else if (event.method === \"values\") {\n applyValuesEvent(event as ValuesEvent);\n }\n },\n });\n };\n\n void (async () => {\n const seeded =\n (await rootBus.trySeedFromHistory?.({\n kind: \"messages\",\n namespace: ns,\n store,\n })) === true;\n if (!seeded && !disposed) openSubscription();\n })();\n\n return {\n async dispose() {\n disposed = true;\n if (flushChannel != null) {\n flushChannel.port1.onmessage = null;\n flushChannel.port1.close();\n flushChannel.port2.close();\n }\n await runtime?.dispose();\n },\n };\n },\n };\n}\n"],"mappings":"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"}
@@ -2,7 +2,7 @@ import { ensureMessageInstances } from "../message-coercion.js";
2
2
  import { MessageAssembler } from "../../client/stream/messages.js";
3
3
  import { isRootNamespace, namespaceKey } from "../namespace.js";
4
4
  import { assembledMessageToBaseMessage } from "../assembled-to-message.js";
5
- import { buildMessageIndex, reconcileMessagesFromValues, shouldPreferValuesMessageForToolCalls } from "../message-reconciliation.js";
5
+ import { buildMessageIndex, reconcileMessagesFromValues, shouldPreferValuesMessage } from "../message-reconciliation.js";
6
6
  import { openProjectionSubscription } from "./runtime.js";
7
7
  //#region src/stream/projections/messages.ts
8
8
  function messagesProjection(namespace) {
@@ -57,7 +57,7 @@ function messagesProjection(namespace) {
57
57
  currentIndexById: indexById,
58
58
  previousValueMessageIds: valuesMessageIds,
59
59
  streamedMessageIds: streamMessageIds,
60
- preferValuesMessage: shouldPreferValuesMessageForToolCalls
60
+ preferValuesMessage: shouldPreferValuesMessage
61
61
  });
62
62
  valuesMessageIds = reconciliation.valueMessageIds;
63
63
  const reconciledMessages = [...reconciliation.messages];
@@ -1 +1 @@
1
- {"version":3,"file":"messages.js","names":[],"sources":["../../../src/stream/projections/messages.ts"],"sourcesContent":["/**\n * Namespace-scoped `messages` projection.\n *\n * Opens `thread.subscribe({ channels: [\"messages\"], namespaces: [ns] })`\n * and folds each `messages` event through {@link MessageAssembler}.\n * Every update — start, block delta, block finish, message finish —\n * re-derives a `BaseMessage` class instance for the currently-active\n * message and updates its slot in the store.\n *\n * The projection emits `BaseMessage[]` (class instances from\n * `@langchain/core/messages`), never plain serialized objects.\n */\nimport type {\n MessagesEvent,\n MessageRole,\n MessageStartData,\n ValuesEvent,\n} from \"@langchain/protocol\";\nimport type { BaseMessage } from \"@langchain/core/messages\";\nimport { MessageAssembler } from \"../../client/stream/messages.js\";\nimport {\n assembledMessageToBaseMessage,\n type ExtendedMessageRole,\n} from \"../assembled-to-message.js\";\nimport type { Message } from \"../../types.messages.js\";\nimport type { ProjectionSpec, ProjectionRuntime } from \"../types.js\";\nimport { ensureMessageInstances } from \"../message-coercion.js\";\nimport { isRootNamespace, namespaceKey } from \"../namespace.js\";\nimport {\n buildMessageIndex,\n reconcileMessagesFromValues,\n shouldPreferValuesMessageForToolCalls,\n} from \"../message-reconciliation.js\";\nimport { openProjectionSubscription } from \"./runtime.js\";\n\nexport function messagesProjection(\n namespace: readonly string[]\n): ProjectionSpec<BaseMessage[]> {\n const ns = [...namespace];\n const key = `messages|${namespaceKey(ns)}`;\n\n return {\n key,\n namespace: ns,\n initial: [],\n open({ thread, store, rootBus }): ProjectionRuntime {\n const assembler = new MessageAssembler();\n // Per-messageId state needed for BaseMessage projection:\n // - `role` is only in the `message-start` event; we cache it\n // so subsequent delta events still produce a typed message.\n // - `toolCallId` is pulled from message-start extras when role\n // is `tool` (a convention we keep compatible with serialized\n // v1 tool messages).\n const roleByKey = new Map<\n string,\n { role: ExtendedMessageRole; toolCallId?: string }\n >();\n const indexById = new Map<string, number>();\n // Ids this projection has observed via the `messages` channel\n // (token-level deltas). Used by `applyValuesEvent` to prefer the\n // stream-assembled version over the values-coerced shape while a\n // turn is streaming, matching the root controller's policy.\n const streamMessageIds = new Set<string>();\n // Ids observed in the most recent `values.messages` snapshot.\n // Messages that were present in a prior snapshot but are absent\n // from this one are treated as explicit removals (server-side\n // `RemoveMessage` reducer deltas). Stream-only messages (seen on\n // the messages channel but never echoed in a values snapshot)\n // are preserved — their enclosing superstep may simply not have\n // committed yet.\n let valuesMessageIds = new Set<string>();\n\n // Root-scoped projections whose channels are already covered by\n // the controller's root pump attach to the shared fan-out\n // instead of opening a second server subscription. The root\n // pump runs at `{namespaces: [[]], depth: 1}`, which is exactly\n // the scope a root-namespace `messagesProjection` wants.\n const rootShortCircuit =\n isRootNamespace(ns) && rootBus.channels.includes(\"messages\");\n\n if (rootShortCircuit) {\n const unsubscribe = rootBus.subscribe((event) => {\n if (event.method !== \"messages\") return;\n if (!isRootNamespace(event.params.namespace)) return;\n applyEvent(event as MessagesEvent);\n });\n return {\n dispose() {\n unsubscribe();\n },\n };\n }\n\n let disposed = false;\n\n // Local mirror of the store contents. Every `applyEvent` /\n // `applyValuesEvent` mutates this synchronously; a coalesced\n // `scheduleFlush` copies it to `store` once per macrotask.\n //\n // Why the indirection? When a namespace-scoped projection\n // (e.g. a subagent modal opened after the run finished) first\n // subscribes, the server replays the entire history from\n // `seq=0`. Dozens of `messages`-channel events can land in a\n // single SSE parse — they drain through the `for await` loop\n // as a long microtask chain. Microtasks run before any\n // macrotask, so React's concurrent scheduler never gets a\n // chance to commit between updates. Calling `store.setValue`\n // per event in that burst overflows React's\n // `nestedUpdateCount` guard and throws \"Maximum update depth\n // exceeded\", permanently killing the projection and\n // leaving the store stuck at its first few messages.\n //\n // Batching via `MessageChannel` (macrotask) coalesces the\n // replay burst into one `setValue` call and lets React\n // commit between flushes for live token streaming too.\n const pendingMessages: BaseMessage[] = [];\n let dirty = false;\n let flushScheduled = false;\n const flushChannel =\n typeof MessageChannel !== \"undefined\" ? new MessageChannel() : null;\n\n const flush = (): void => {\n flushScheduled = false;\n if (!dirty || disposed) return;\n dirty = false;\n // `.slice()` breaks identity so React's `Object.is` bail-out\n // in `StreamStore.setValue` propagates the change.\n store.setValue(pendingMessages.slice());\n };\n if (flushChannel != null) {\n flushChannel.port1.onmessage = flush;\n }\n\n const scheduleFlush = (): void => {\n dirty = true;\n if (flushScheduled) return;\n flushScheduled = true;\n if (flushChannel != null) {\n flushChannel.port2.postMessage(null);\n } else {\n setTimeout(flush, 0);\n }\n };\n\n // Rebuild the store from `values.messages` snapshots.\n //\n // `values` events carry the full, committed state of the\n // thread's `messages` channel at a checkpoint — they fire\n // on node completion, AFTER every `messages`-channel delta\n // for that turn has been emitted. They are the authoritative\n // source of truth for ORDER and for non-streamed messages\n // (human turns, serialised tool results, subagent echoes, …).\n //\n // Why rebuild rather than merge-by-id?\n //\n // In practice the server may emit the same logical message\n // with DIFFERENT ids across successive `values` snapshots at\n // the same namespace — e.g. a subagent first surfaces its\n // seed prompt with a synthetic id like\n // `subagent:<tool_call_id>:human`, then a later superstep\n // echoes the same prompt back with a real UUID (or vice\n // versa). A naive \"match-or-append by id\" strategy treats\n // each fresh id as a new entry and the list grows\n // monotonically, showing the same content twice (or more)\n // in the UI.\n //\n // Policy (mirrors the root controller's `#applyValues`):\n //\n // 1. Walk `values.messages` in order. For each id, prefer\n // the stream-assembled entry if we have one for that id\n // (keeps in-progress token streaming visible); otherwise\n // take the values-coerced instance. This self-heals the\n // two classes of glitch the old merge-by-id handler\n // targeted:\n // - tool messages arriving without `tool_call_id` on\n // the messages channel — the values snapshot always\n // carries it;\n // - AI messages whose finalized `tool_calls` didn't\n // fully land via the messages channel — the values\n // snapshot's AI message has them populated.\n //\n // 2. Append any stream-only ids (seen on the messages\n // channel but never echoed in ANY values snapshot yet)\n // — their enclosing superstep hasn't committed yet, so\n // dropping them would flash the UI.\n //\n // 3. Ids that WERE in a prior values snapshot but are gone\n // from this one are treated as explicit removals\n // (`RemoveMessage` reducer deltas) and dropped.\n //\n // Unkeyed messages (no stable id) are passed through in\n // their values order because we can't dedupe them safely.\n const applyValuesEvent = (event: ValuesEvent): void => {\n const data = event.params.data;\n if (data == null || typeof data !== \"object\" || Array.isArray(data)) {\n return;\n }\n const state = data as Record<string, unknown>;\n const rawMessages = state.messages;\n if (!Array.isArray(rawMessages) || rawMessages.length === 0) return;\n\n const coerced = ensureMessageInstances(\n rawMessages as (Message | BaseMessage)[]\n );\n\n const reconciliation = reconcileMessagesFromValues({\n valueMessages: coerced,\n currentMessages: pendingMessages,\n currentIndexById: indexById,\n previousValueMessageIds: valuesMessageIds,\n streamedMessageIds: streamMessageIds,\n preferValuesMessage: shouldPreferValuesMessageForToolCalls,\n });\n valuesMessageIds = reconciliation.valueMessageIds;\n const reconciledMessages = [...reconciliation.messages];\n\n pendingMessages.length = 0;\n for (const message of reconciledMessages) pendingMessages.push(message);\n indexById.clear();\n for (const [id, idx] of buildMessageIndex(pendingMessages)) {\n indexById.set(id, idx);\n }\n scheduleFlush();\n };\n\n const applyEvent = (event: MessagesEvent): void => {\n const data = event.params.data;\n\n if (data.event === \"message-start\") {\n const startData = data as MessageStartData;\n const role = (startData.role ?? \"ai\") as MessageRole;\n // \"tool\" role is a v1 convention not represented in the\n // protocol enum but common in practice — keep it working\n // for graphs that emit it as an extensible field.\n const extendedRole =\n (startData as { role?: ExtendedMessageRole }).role ?? role;\n const maybeToolCallId = (startData as { tool_call_id?: string })\n .tool_call_id;\n if (startData.id != null) {\n roleByKey.set(startData.id, {\n role: extendedRole,\n toolCallId: maybeToolCallId,\n });\n }\n }\n\n const update = assembler.consume(event);\n if (update == null) return;\n const msg = update.message;\n const id = msg.id;\n if (id == null) return;\n const captured = roleByKey.get(id) ?? { role: \"ai\" as const };\n const base = assembledMessageToBaseMessage(msg, captured.role, {\n toolCallId: captured.toolCallId,\n });\n\n streamMessageIds.add(id);\n const existingIdx = indexById.get(id);\n if (existingIdx == null) {\n indexById.set(id, pendingMessages.length);\n pendingMessages.push(base);\n } else {\n pendingMessages[existingIdx] = base;\n }\n scheduleFlush();\n };\n\n let runtime: ProjectionRuntime | undefined;\n const openSubscription = () => {\n runtime = openProjectionSubscription({\n thread,\n // Subscribe to both `messages` (live token deltas that drive\n // the in-flight assistant bubble) and `values` (periodic full-\n // state snapshots). Consuming values lets late-mounted scoped\n // projections backfill history after the run has finished.\n channels: [\"messages\", \"values\"],\n namespace: ns,\n onEvent(event) {\n if (event.method === \"messages\") {\n applyEvent(event as MessagesEvent);\n } else if (event.method === \"values\") {\n applyValuesEvent(event as ValuesEvent);\n }\n },\n });\n };\n\n void (async () => {\n const seeded =\n (await rootBus.trySeedFromHistory?.({\n kind: \"messages\",\n namespace: ns,\n store,\n })) === true;\n if (!seeded && !disposed) openSubscription();\n })();\n\n return {\n async dispose() {\n disposed = true;\n if (flushChannel != null) {\n flushChannel.port1.onmessage = null;\n flushChannel.port1.close();\n flushChannel.port2.close();\n }\n await runtime?.dispose();\n },\n };\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"messages.js","names":[],"sources":["../../../src/stream/projections/messages.ts"],"sourcesContent":["/**\n * Namespace-scoped `messages` projection.\n *\n * Opens `thread.subscribe({ channels: [\"messages\"], namespaces: [ns] })`\n * and folds each `messages` event through {@link MessageAssembler}.\n * Every update — start, block delta, block finish, message finish —\n * re-derives a `BaseMessage` class instance for the currently-active\n * message and updates its slot in the store.\n *\n * The projection emits `BaseMessage[]` (class instances from\n * `@langchain/core/messages`), never plain serialized objects.\n */\nimport type {\n MessagesEvent,\n MessageRole,\n MessageStartData,\n ValuesEvent,\n} from \"@langchain/protocol\";\nimport type { BaseMessage } from \"@langchain/core/messages\";\nimport { MessageAssembler } from \"../../client/stream/messages.js\";\nimport {\n assembledMessageToBaseMessage,\n type ExtendedMessageRole,\n} from \"../assembled-to-message.js\";\nimport type { Message } from \"../../types.messages.js\";\nimport type { ProjectionSpec, ProjectionRuntime } from \"../types.js\";\nimport { ensureMessageInstances } from \"../message-coercion.js\";\nimport { isRootNamespace, namespaceKey } from \"../namespace.js\";\nimport {\n buildMessageIndex,\n reconcileMessagesFromValues,\n shouldPreferValuesMessage,\n} from \"../message-reconciliation.js\";\nimport { openProjectionSubscription } from \"./runtime.js\";\n\nexport function messagesProjection(\n namespace: readonly string[]\n): ProjectionSpec<BaseMessage[]> {\n const ns = [...namespace];\n const key = `messages|${namespaceKey(ns)}`;\n\n return {\n key,\n namespace: ns,\n initial: [],\n open({ thread, store, rootBus }): ProjectionRuntime {\n const assembler = new MessageAssembler();\n // Per-messageId state needed for BaseMessage projection:\n // - `role` is only in the `message-start` event; we cache it\n // so subsequent delta events still produce a typed message.\n // - `toolCallId` is pulled from message-start extras when role\n // is `tool` (a convention we keep compatible with serialized\n // v1 tool messages).\n const roleByKey = new Map<\n string,\n { role: ExtendedMessageRole; toolCallId?: string }\n >();\n const indexById = new Map<string, number>();\n // Ids this projection has observed via the `messages` channel\n // (token-level deltas). Used by `applyValuesEvent` to prefer the\n // stream-assembled version over the values-coerced shape while a\n // turn is streaming, matching the root controller's policy.\n const streamMessageIds = new Set<string>();\n // Ids observed in the most recent `values.messages` snapshot.\n // Messages that were present in a prior snapshot but are absent\n // from this one are treated as explicit removals (server-side\n // `RemoveMessage` reducer deltas). Stream-only messages (seen on\n // the messages channel but never echoed in a values snapshot)\n // are preserved — their enclosing superstep may simply not have\n // committed yet.\n let valuesMessageIds = new Set<string>();\n\n // Root-scoped projections whose channels are already covered by\n // the controller's root pump attach to the shared fan-out\n // instead of opening a second server subscription. The root\n // pump runs at `{namespaces: [[]], depth: 1}`, which is exactly\n // the scope a root-namespace `messagesProjection` wants.\n const rootShortCircuit =\n isRootNamespace(ns) && rootBus.channels.includes(\"messages\");\n\n if (rootShortCircuit) {\n const unsubscribe = rootBus.subscribe((event) => {\n if (event.method !== \"messages\") return;\n if (!isRootNamespace(event.params.namespace)) return;\n applyEvent(event as MessagesEvent);\n });\n return {\n dispose() {\n unsubscribe();\n },\n };\n }\n\n let disposed = false;\n\n // Local mirror of the store contents. Every `applyEvent` /\n // `applyValuesEvent` mutates this synchronously; a coalesced\n // `scheduleFlush` copies it to `store` once per macrotask.\n //\n // Why the indirection? When a namespace-scoped projection\n // (e.g. a subagent modal opened after the run finished) first\n // subscribes, the server replays the entire history from\n // `seq=0`. Dozens of `messages`-channel events can land in a\n // single SSE parse — they drain through the `for await` loop\n // as a long microtask chain. Microtasks run before any\n // macrotask, so React's concurrent scheduler never gets a\n // chance to commit between updates. Calling `store.setValue`\n // per event in that burst overflows React's\n // `nestedUpdateCount` guard and throws \"Maximum update depth\n // exceeded\", permanently killing the projection and\n // leaving the store stuck at its first few messages.\n //\n // Batching via `MessageChannel` (macrotask) coalesces the\n // replay burst into one `setValue` call and lets React\n // commit between flushes for live token streaming too.\n const pendingMessages: BaseMessage[] = [];\n let dirty = false;\n let flushScheduled = false;\n const flushChannel =\n typeof MessageChannel !== \"undefined\" ? new MessageChannel() : null;\n\n const flush = (): void => {\n flushScheduled = false;\n if (!dirty || disposed) return;\n dirty = false;\n // `.slice()` breaks identity so React's `Object.is` bail-out\n // in `StreamStore.setValue` propagates the change.\n store.setValue(pendingMessages.slice());\n };\n if (flushChannel != null) {\n flushChannel.port1.onmessage = flush;\n }\n\n const scheduleFlush = (): void => {\n dirty = true;\n if (flushScheduled) return;\n flushScheduled = true;\n if (flushChannel != null) {\n flushChannel.port2.postMessage(null);\n } else {\n setTimeout(flush, 0);\n }\n };\n\n // Rebuild the store from `values.messages` snapshots.\n //\n // `values` events carry the full, committed state of the\n // thread's `messages` channel at a checkpoint — they fire\n // on node completion, AFTER every `messages`-channel delta\n // for that turn has been emitted. They are the authoritative\n // source of truth for ORDER and for non-streamed messages\n // (human turns, serialised tool results, subagent echoes, …).\n //\n // Why rebuild rather than merge-by-id?\n //\n // In practice the server may emit the same logical message\n // with DIFFERENT ids across successive `values` snapshots at\n // the same namespace — e.g. a subagent first surfaces its\n // seed prompt with a synthetic id like\n // `subagent:<tool_call_id>:human`, then a later superstep\n // echoes the same prompt back with a real UUID (or vice\n // versa). A naive \"match-or-append by id\" strategy treats\n // each fresh id as a new entry and the list grows\n // monotonically, showing the same content twice (or more)\n // in the UI.\n //\n // Policy (mirrors the root controller's `#applyValues`):\n //\n // 1. Walk `values.messages` in order. For each id, prefer\n // the stream-assembled entry if we have one for that id\n // (keeps in-progress token streaming visible); otherwise\n // take the values-coerced instance. Same-id values still\n // win when they carry data the stream lacks:\n // - tool messages arriving without `tool_call_id` on\n // the messages channel — the values snapshot always\n // carries it;\n // - AI messages whose finalized `tool_calls` didn't\n // fully land via the messages channel — the values\n // snapshot's AI message has them populated;\n // - metadata enrichment with unchanged content (e.g.\n // committed `additional_kwargs` on an echoed human).\n //\n // 2. Append any stream-only ids (seen on the messages\n // channel but never echoed in ANY values snapshot yet)\n // — their enclosing superstep hasn't committed yet, so\n // dropping them would flash the UI.\n //\n // 3. Ids that WERE in a prior values snapshot but are gone\n // from this one are treated as explicit removals\n // (`RemoveMessage` reducer deltas) and dropped.\n //\n // Unkeyed messages (no stable id) are passed through in\n // their values order because we can't dedupe them safely.\n const applyValuesEvent = (event: ValuesEvent): void => {\n const data = event.params.data;\n if (data == null || typeof data !== \"object\" || Array.isArray(data)) {\n return;\n }\n const state = data as Record<string, unknown>;\n const rawMessages = state.messages;\n if (!Array.isArray(rawMessages) || rawMessages.length === 0) return;\n\n const coerced = ensureMessageInstances(\n rawMessages as (Message | BaseMessage)[]\n );\n\n const reconciliation = reconcileMessagesFromValues({\n valueMessages: coerced,\n currentMessages: pendingMessages,\n currentIndexById: indexById,\n previousValueMessageIds: valuesMessageIds,\n streamedMessageIds: streamMessageIds,\n preferValuesMessage: shouldPreferValuesMessage,\n });\n valuesMessageIds = reconciliation.valueMessageIds;\n const reconciledMessages = [...reconciliation.messages];\n\n pendingMessages.length = 0;\n for (const message of reconciledMessages) pendingMessages.push(message);\n indexById.clear();\n for (const [id, idx] of buildMessageIndex(pendingMessages)) {\n indexById.set(id, idx);\n }\n scheduleFlush();\n };\n\n const applyEvent = (event: MessagesEvent): void => {\n const data = event.params.data;\n\n if (data.event === \"message-start\") {\n const startData = data as MessageStartData;\n const role = (startData.role ?? \"ai\") as MessageRole;\n // \"tool\" role is a v1 convention not represented in the\n // protocol enum but common in practice — keep it working\n // for graphs that emit it as an extensible field.\n const extendedRole =\n (startData as { role?: ExtendedMessageRole }).role ?? role;\n const maybeToolCallId = (startData as { tool_call_id?: string })\n .tool_call_id;\n if (startData.id != null) {\n roleByKey.set(startData.id, {\n role: extendedRole,\n toolCallId: maybeToolCallId,\n });\n }\n }\n\n const update = assembler.consume(event);\n if (update == null) return;\n const msg = update.message;\n const id = msg.id;\n if (id == null) return;\n const captured = roleByKey.get(id) ?? { role: \"ai\" as const };\n const base = assembledMessageToBaseMessage(msg, captured.role, {\n toolCallId: captured.toolCallId,\n });\n\n streamMessageIds.add(id);\n const existingIdx = indexById.get(id);\n if (existingIdx == null) {\n indexById.set(id, pendingMessages.length);\n pendingMessages.push(base);\n } else {\n pendingMessages[existingIdx] = base;\n }\n scheduleFlush();\n };\n\n let runtime: ProjectionRuntime | undefined;\n const openSubscription = () => {\n runtime = openProjectionSubscription({\n thread,\n // Subscribe to both `messages` (live token deltas that drive\n // the in-flight assistant bubble) and `values` (periodic full-\n // state snapshots). Consuming values lets late-mounted scoped\n // projections backfill history after the run has finished.\n channels: [\"messages\", \"values\"],\n namespace: ns,\n onEvent(event) {\n if (event.method === \"messages\") {\n applyEvent(event as MessagesEvent);\n } else if (event.method === \"values\") {\n applyValuesEvent(event as ValuesEvent);\n }\n },\n });\n };\n\n void (async () => {\n const seeded =\n (await rootBus.trySeedFromHistory?.({\n kind: \"messages\",\n namespace: ns,\n store,\n })) === true;\n if (!seeded && !disposed) openSubscription();\n })();\n\n return {\n async dispose() {\n disposed = true;\n if (flushChannel != null) {\n flushChannel.port1.onmessage = null;\n flushChannel.port1.close();\n flushChannel.port2.close();\n }\n await runtime?.dispose();\n },\n };\n },\n };\n}\n"],"mappings":"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"}
@@ -228,9 +228,10 @@ var RootMessageProjection = class {
228
228
  * Reconcile a full `values` snapshot into the projection.
229
229
  *
230
230
  * Delegates the merge to {@link reconcileMessagesFromValues}:
231
- * values stays authoritative for ordering and removals, while
232
- * streamed in-flight messages keep their content until the server
233
- * echoes them back. Empty messages just refresh the values blob.
231
+ * values stays authoritative for ordering, removals, and same-id
232
+ * metadata enrichment, while streamed in-flight messages keep
233
+ * content that has moved past the snapshot. Empty messages just
234
+ * refresh the values blob.
234
235
  *
235
236
  * Rebuilds {@link #indexById} after the merge so subsequent delta
236
237
  * applications target the new positions.
@@ -261,7 +262,7 @@ var RootMessageProjection = class {
261
262
  currentMessages: baselineMessages,
262
263
  currentIndexById: this.#indexById,
263
264
  previousValueMessageIds: this.#valuesMessageIds,
264
- preferValuesMessage: require_message_reconciliation.shouldPreferValuesMessageForToolCalls,
265
+ preferValuesMessage: require_message_reconciliation.shouldPreferValuesMessage,
265
266
  addOnly
266
267
  });
267
268
  if (!addOnly) this.#valuesMessageIds = reconciliation.valueMessageIds;
@@ -287,8 +288,13 @@ var RootMessageProjection = class {
287
288
  * exists), preserving prior history ordering. When the server later
288
289
  * emits a `values` snapshot containing the same ids,
289
290
  * {@link applyValues} → {@link reconcileMessagesFromValues} takes over
290
- * (server ordering wins, the echoed message replaces the optimistic
291
- * one).
291
+ * (server ordering wins; the echoed message replaces the optimistic
292
+ * one when content matches *and* values adds metadata the optimistic
293
+ * copy lacks, so server-authored `additional_kwargs` is visible
294
+ * without waiting for hydration — lagging/poorer snapshots do not
295
+ * strip richer current metadata).
296
+ * In-flight streamed content still wins when it has moved past the
297
+ * snapshot.
292
298
  *
293
299
  * Non-message input keys are shallow-merged into `values` via
294
300
  * `extraValues`; they are dropped/overwritten automatically by the
@@ -1 +1 @@
1
- {"version":3,"file":"root-message-projection.cjs","names":["#messagesKey","#store","MessageAssembler","#roles","#indexById","#toolCallIdByNamespace","#sealedMessageIds","#assembler","#valuesMessageIds","#pendingMessages","#pendingValues","#flushScheduled","#maxStep","#sealStep","namespaceKey","assembledMessageToBaseMessage","messagesEqual","#scheduleFlush","reconcileMessagesFromValues","shouldPreferValuesMessageForToolCalls","buildMessageIndex","#flushPending"],"sources":["../../src/stream/root-message-projection.ts"],"sourcesContent":["/**\n * Root-namespace message projection.\n *\n * # What this module is\n *\n * The {@link RootMessageProjection} is the piece of the\n * {@link StreamController} that owns \"what messages does the root\n * namespace currently contain?\". It assembles streamed message deltas\n * via {@link MessageAssembler}, reconciles them against authoritative\n * `values.messages` snapshots from the server, and writes the merged\n * list back into the controller's root snapshot store.\n *\n * # Two streams of truth\n *\n * Root messages arrive on two channels and need to merge cleanly:\n *\n * - **`messages` channel.** Token-level deltas that build messages\n * incrementally. The {@link MessageAssembler} keeps partial\n * messages by id and emits an updated `BaseMessage` per delta.\n * - **`values` channel.** Periodic full-state snapshots that include\n * the authoritative messages array. Used for ordering, removals,\n * and forks (where the streamed messages may pre-date the new\n * timeline).\n *\n * The reconciliation rules (delegated to\n * {@link reconcileMessagesFromValues}) preserve in-flight streamed\n * content while letting values dictate ordering and removals.\n *\n * # Tool-message namespace correlation\n *\n * Tool messages arrive on `messages-start` events with `role: \"tool\"`\n * but the start event doesn't always include a `tool_call_id`. We\n * recover it via three fallbacks:\n *\n * 1. The start event itself, when the server includes it.\n * 2. The legacy `<id>-tool-<call_id>` message id format.\n * 3. The most recent `tool-started` event recorded under the same\n * namespace via {@link recordToolCallNamespace}.\n *\n * Without this correlation, tool messages render with empty\n * `tool_call_id` and downstream UIs can't pair them with the\n * originating tool call.\n *\n * # Store-write batching\n *\n * Every {@link handleMessage} / {@link applyValues} call updates the\n * in-projection bookkeeping (assembler state, id index, role cache)\n * synchronously, then stages the new `messages` / `values` into a\n * pending buffer and schedules a `setTimeout(0)` flush. A single\n * coalesced `store.setState` runs at the next macrotask boundary.\n *\n * The motivation is the long-replay freeze: a thread with hundreds\n * of messages replays through the `messages` channel on refresh or\n * mid-run join. Those events drain through the controller's\n * `for await` pump as a long microtask chain. Per-event\n * `store.setState` notifies `useSyncExternalStore` per event, and\n * after enough notifications React's `nestedUpdateCount` guard trips\n * with \"Maximum update depth exceeded\", permanently freezing the UI\n * on the first few messages. Coalescing to one notification per\n * macrotask lets React's scheduler commit between flushes.\n *\n * # Lifecycle\n *\n * - `handleMessage(event)` — apply a `messages` event delta.\n * - `applyValues(values, msgs)` — merge a `values` snapshot.\n * - `recordToolCallNamespace(ns, id)` — capture `namespace → tool_call_id`\n * so subsequent tool message starts can recover the id.\n * - `reset()` — clear all state on thread rebind.\n */\nimport type {\n MessagesEvent,\n MessageRole,\n MessageStartData,\n} from \"@langchain/protocol\";\nimport type { BaseMessage } from \"@langchain/core/messages\";\nimport { MessageAssembler } from \"../client/stream/messages.js\";\nimport {\n assembledMessageToBaseMessage,\n type ExtendedMessageRole,\n} from \"./assembled-to-message.js\";\nimport type { StreamStore } from \"./store.js\";\nimport type { RootSnapshot } from \"./types.js\";\nimport { namespaceKey } from \"./namespace.js\";\nimport {\n buildMessageIndex,\n messagesEqual,\n reconcileMessagesFromValues,\n shouldPreferValuesMessageForToolCalls,\n} from \"./message-reconciliation.js\";\n\n/**\n * Root-namespace message projection. Owns the merge between the\n * `messages` (streamed deltas) and `values` (authoritative\n * snapshots) channels for the root namespace.\n *\n * @typeParam StateType - Root state shape; the messages array is read\n * from `values[messagesKey]`.\n * @typeParam InterruptType - Shape of root protocol interrupts (forwarded\n * into `RootSnapshot` updates).\n */\nexport class RootMessageProjection<\n StateType extends object,\n InterruptType = unknown,\n> {\n /**\n * Key inside `values` that holds the message array. Defaults to\n * `\"messages\"` in the controller; configurable for state graphs\n * that surface messages under a different slot.\n */\n readonly #messagesKey: string;\n\n /** Root snapshot store written to on every merge. */\n readonly #store: StreamStore<RootSnapshot<StateType, InterruptType>>;\n\n /**\n * Stateful chunk assembler for in-flight messages. Reset (via a\n * fresh instance) on every {@link reset} so a new thread starts\n * with no half-built messages from the previous one.\n */\n #assembler = new MessageAssembler();\n\n /**\n * `messageId → role/toolCallId` captured from `message-start` events.\n * The assembler's intermediate output drops these fields, so we cache\n * them at start-time and reapply when projecting to a `BaseMessage`.\n */\n readonly #roles = new Map<\n string,\n { role: ExtendedMessageRole; toolCallId?: string }\n >();\n\n /**\n * `messageId → position in #store.messages` for fast in-place\n * updates as deltas arrive. Rebuilt on every full reconciliation\n * driven by a `values` event.\n */\n readonly #indexById = new Map<string, number>();\n\n /**\n * Ids observed in the most recent `values.messages` snapshot.\n * Reconciliation uses this to detect server-side removals: a\n * previously-seen id missing from the next snapshot means it was\n * removed by the server (and should drop from the projection).\n */\n #valuesMessageIds = new Set<string>();\n\n /**\n * `namespaceKey → tool_call_id` captured from root `tool-started`\n * events. Used as a fallback when a tool-role `message-start` is\n * missing its `tool_call_id` field.\n */\n readonly #toolCallIdByNamespace = new Map<string, string>();\n\n /**\n * Coalescing buffer for store writes. {@link handleMessage} and\n * {@link applyValues} stage their computed `messages` / `values`\n * here instead of calling `store.setState` per event. A single\n * `setTimeout(0)` flush commits them in one `setState`, so a\n * burst of SSE events draining as a microtask chain becomes one\n * store notification at the next macrotask boundary.\n *\n * `null` means \"no staged write\" — once a flush settles, the\n * slots are cleared so the next call starts from the latest\n * committed store snapshot.\n */\n #pendingMessages: BaseMessage[] | null = null;\n #pendingValues: StateType | null = null;\n #flushScheduled = false;\n\n /**\n * Highest checkpoint `step` whose `values` snapshot has been applied.\n * Seeded by {@link StreamController.hydrate} from `getState()` and\n * advanced by live `values` events. A snapshot arriving with a lower\n * step is an older checkpoint replayed by the content pump on\n * reconnect; it is reconciled in add-only mode so it cannot remove\n * the seeded message tail (the final assistant turn). `undefined`\n * until the first step-bearing snapshot, where the legacy\n * remove-on-absence behavior is preserved.\n */\n #maxStep: number | undefined = undefined;\n\n /**\n * Message ids seeded as complete-and-final from an idle thread's\n * `getState()` snapshot. An idle thread defers its root SSE pump, and\n * the first `submit()` brings it up — at which point the transport\n * replays the finished run from `seq=0`. Unlike the `values` channel\n * (guarded by {@link #maxStep}), `messages`-channel deltas carry no\n * step, so that replay would otherwise rebuild each already-complete\n * message from an empty `message-start` and re-stream the whole turn\n * token-by-token, clobbering the seeded tail (a visible \"messages\n * replay\" on the first submit). Deltas for a sealed id are dropped in\n * {@link handleMessage}. The seal is lifted once a checkpoint advances\n * strictly past {@link #sealStep} (see {@link applyValues}) or on\n * thread rebind ({@link reset}). New ids from the next run are never\n * sealed, so they stream normally.\n */\n readonly #sealedMessageIds = new Set<string>();\n\n /**\n * High-water {@link #maxStep} captured when {@link sealMessageIds} ran,\n * i.e. the seed checkpoint's step (or `undefined` when `getState()`\n * carried no `metadata.step`). It is the boundary between the replayed\n * idle history (steps `<= #sealStep`, emitted by the deferred pump's\n * `seq=0` replay) and the new run (steps `> #sealStep`); only a\n * checkpoint strictly past it lifts the seal. Without this boundary the\n * replayed old-run checkpoints — which themselves carry increasing\n * steps — would advance {@link #maxStep} and lift the seal mid-replay,\n * reopening the clobber. When the seed step is unknown the boundary\n * stays `undefined` and the seal holds until {@link reset}; the\n * `values` channel (which ignores the seal) still reconciles any\n * genuine change to a sealed id, only its streamed deltas are dropped.\n */\n #sealStep: number | undefined = undefined;\n\n /**\n * @param params.messagesKey - Key inside `values` that holds the\n * message array.\n * @param params.store - Root snapshot store to mutate.\n */\n constructor(params: {\n messagesKey: string;\n store: StreamStore<RootSnapshot<StateType, InterruptType>>;\n }) {\n this.#messagesKey = params.messagesKey;\n this.#store = params.store;\n }\n\n /**\n * Drop all per-thread state. Called by the controller on thread\n * rebind / dispose so a swap doesn't surface stale messages.\n */\n reset(): void {\n this.#assembler = new MessageAssembler();\n this.#roles.clear();\n this.#indexById.clear();\n this.#valuesMessageIds = new Set();\n this.#toolCallIdByNamespace.clear();\n // Drop any unflushed pending writes — they were computed against\n // the previous thread's baseline and committing them after a\n // rebind would bleed stale messages into the new thread.\n this.#pendingMessages = null;\n this.#pendingValues = null;\n this.#flushScheduled = false;\n this.#maxStep = undefined;\n this.#sealedMessageIds.clear();\n this.#sealStep = undefined;\n }\n\n /**\n * Seal message ids so the streamed `messages` channel cannot downgrade\n * them to partial re-streams. Called by {@link StreamController.hydrate}\n * after seeding an idle thread, whose deferred pump replays the finished\n * run from `seq=0` on the first submit.\n *\n * Captures the current {@link #maxStep} as the lift boundary\n * ({@link #sealStep}). The seal is applied immediately after the seed's\n * `getState()` snapshot is reconciled, so `#maxStep` here is the seed\n * step (or `undefined` when `getState()` carried no `metadata.step`).\n * The seal is lifted once a checkpoint advances strictly past that\n * boundary (see {@link applyValues}) or on thread rebind\n * ({@link reset}).\n *\n * @param ids - Complete message ids from the idle `getState()` seed.\n */\n sealMessageIds(ids: Iterable<string>): void {\n for (const id of ids) this.#sealedMessageIds.add(id);\n if (this.#sealStep == null) this.#sealStep = this.#maxStep;\n }\n\n /**\n * Record a `namespace → tool_call_id` mapping captured from a root\n * `tool-started` event.\n *\n * The companion tool-role `message-start` event may not carry a\n * `tool_call_id`, so we fall back to the most recent value recorded\n * here for the same namespace.\n *\n * @param namespace - Event namespace from the `tool-started` event.\n * @param toolCallId - Tool call id from the same event.\n */\n recordToolCallNamespace(\n namespace: readonly string[],\n toolCallId: string\n ): void {\n this.#toolCallIdByNamespace.set(namespaceKey(namespace), toolCallId);\n }\n\n /**\n * Apply a `messages` channel event to the projection.\n *\n * Captures role/tool metadata on `message-start`, feeds the chunk\n * to the assembler, projects the assembled output to a\n * {@link BaseMessage}, and either appends or in-place updates the\n * pending messages buffer based on whether the id was seen before.\n *\n * @param event - The `messages` channel event to consume.\n */\n handleMessage(event: MessagesEvent): void {\n const data = event.params.data;\n if (data.event === \"message-start\") {\n const startData = data as MessageStartData;\n const role = (startData.role ?? \"ai\") as MessageRole;\n const extendedRole =\n (startData as { role?: ExtendedMessageRole }).role ?? role;\n let toolCallId = (startData as { tool_call_id?: string }).tool_call_id;\n // Tool messages need a tool_call_id to render. Fall back through:\n // 1. legacy `<id>-tool-<call_id>` message id format\n // 2. namespace-recorded tool_call_id (from #recordToolCallNamespace)\n if (extendedRole === \"tool\" && toolCallId == null) {\n const messageId = startData.id;\n if (messageId != null) {\n const match = /-tool-(.+)$/.exec(messageId);\n if (match != null) toolCallId = match[1];\n }\n if (toolCallId == null) {\n toolCallId = this.#toolCallIdByNamespace.get(\n namespaceKey(event.params.namespace)\n );\n }\n }\n if (startData.id != null) {\n this.#roles.set(startData.id, {\n role: extendedRole,\n toolCallId,\n });\n }\n }\n\n const update = this.#assembler.consume(event);\n if (update == null) return;\n const id = update.message.id;\n if (id == null) return;\n // A sealed id belongs to a message seeded complete from an idle\n // thread's `getState()`; the deferred pump's `seq=0` replay would\n // otherwise rebuild it from an empty start and re-stream the whole\n // turn. Drop the replayed delta — the authoritative seed already\n // holds the final content (see {@link #sealedMessageIds}).\n if (this.#sealedMessageIds.has(id)) return;\n const captured = this.#roles.get(id) ?? { role: \"ai\" as const };\n const base = assembledMessageToBaseMessage(update.message, captured.role, {\n toolCallId: captured.toolCallId,\n });\n\n // Compute against the pending baseline if we have one (so an\n // earlier handleMessage in the same tick is the input to this\n // one), else against the latest committed store snapshot.\n // `#indexById` is the synchronous source of truth for \"where is\n // each id in the current messages list\" — every code path below\n // keeps it in sync before returning.\n const baselineMessages =\n this.#pendingMessages ?? this.#store.getSnapshot().messages;\n const existingIdx = this.#indexById.get(id);\n let messages: BaseMessage[];\n if (existingIdx == null) {\n this.#indexById.set(id, baselineMessages.length);\n messages = [...baselineMessages, base];\n } else if (messagesEqual(baselineMessages[existingIdx], base)) {\n // Identical re-emission — skip the store write to keep\n // snapshot identity stable.\n return;\n } else {\n messages = baselineMessages.slice();\n messages[existingIdx] = base;\n }\n\n // Mirror the new messages list into `values[messagesKey]` so\n // direct `values` reads (used by some hooks and by the eventual\n // `values` reconciliation) stay in sync.\n const baselineValues =\n this.#pendingValues ?? this.#store.getSnapshot().values;\n const values = syncMessagesIntoValues(\n baselineValues,\n this.#messagesKey,\n messages\n );\n this.#pendingMessages = messages;\n if (values !== baselineValues) this.#pendingValues = values;\n this.#scheduleFlush();\n }\n\n /**\n * Reconcile a full `values` snapshot into the projection.\n *\n * Delegates the merge to {@link reconcileMessagesFromValues}:\n * values stays authoritative for ordering and removals, while\n * streamed in-flight messages keep their content until the server\n * echoes them back. Empty messages just refresh the values blob.\n *\n * Rebuilds {@link #indexById} after the merge so subsequent delta\n * applications target the new positions.\n *\n * @param nextValues - Full values snapshot from the `values` event.\n * @param nextMessages - The messages array extracted from\n * `values[messagesKey]` and coerced to `BaseMessage` instances.\n * @param opts.step - Checkpoint superstep for this snapshot, when\n * known. A snapshot whose step is below the highest applied step is\n * treated as a stale reconnect replay and reconciled add-only.\n */\n applyValues(\n nextValues: StateType,\n nextMessages: BaseMessage[],\n opts?: { step?: number }\n ): void {\n const baselineSnapshot = this.#store.getSnapshot();\n const baselineMessages = this.#pendingMessages ?? baselineSnapshot.messages;\n const baselineValues = this.#pendingValues ?? baselineSnapshot.values;\n\n const step = opts?.step;\n // Stale only when we have both a prior high-water step and a lower\n // incoming step. A missing step preserves the legacy semantics.\n const addOnly =\n step != null && this.#maxStep != null && step < this.#maxStep;\n if (step != null && (this.#maxStep == null || step > this.#maxStep)) {\n this.#maxStep = step;\n }\n // Lift the replay seal only when a checkpoint advances strictly past\n // the step captured when the ids were sealed (the seed step). That\n // boundary separates the replayed idle history (steps <= #sealStep,\n // emitted by the deferred pump's seq=0 replay) from the new run\n // (steps > #sealStep), so crossing it means seeded ids may now take\n // genuine streamed updates. Replayed old-run checkpoints advance\n // #maxStep but never reach past #sealStep, so they can't lift it. A\n // `null` boundary (the seed step was unknown) keeps the seal until\n // reset() — we can't tell replay from live, and the values channel\n // still reconciles a sealed id even while its streamed deltas drop.\n if (\n this.#sealedMessageIds.size > 0 &&\n step != null &&\n this.#sealStep != null &&\n step > this.#sealStep\n ) {\n this.#sealedMessageIds.clear();\n }\n\n if (nextMessages.length === 0) {\n if (\n stateValuesShallowEqual(baselineValues, nextValues, this.#messagesKey)\n ) {\n return;\n }\n // Mirror the current `messages` list back into the values slot\n // so the staged snapshot stays consistent with the (separately\n // tracked) messages array.\n this.#pendingValues = syncMessagesIntoValues(\n nextValues,\n this.#messagesKey,\n baselineMessages\n );\n this.#scheduleFlush();\n return;\n }\n\n const reconciliation = reconcileMessagesFromValues({\n valueMessages: nextMessages,\n currentMessages: baselineMessages,\n currentIndexById: this.#indexById,\n previousValueMessageIds: this.#valuesMessageIds,\n preferValuesMessage: shouldPreferValuesMessageForToolCalls,\n addOnly,\n });\n // A stale replay snapshot must not shrink the authoritative id set:\n // keep the (larger) seeded set so a genuinely-newer removal is still\n // detected once the timeline advances past the seed.\n if (!addOnly) this.#valuesMessageIds = reconciliation.valueMessageIds;\n const messages = reconciliation.messages as BaseMessage[];\n const values = {\n ...(nextValues as Record<string, unknown>),\n [this.#messagesKey]: messages,\n } as StateType;\n if (\n messages === baselineMessages &&\n stateValuesShallowEqual(baselineValues, values, this.#messagesKey)\n ) {\n return;\n }\n\n // Reconciliation may reorder, drop, or substitute messages, so\n // rebuild the id → index map to match the new array.\n this.#indexById.clear();\n for (const [id, idx] of buildMessageIndex(messages)) {\n this.#indexById.set(id, idx);\n }\n this.#pendingMessages = messages;\n this.#pendingValues = values;\n this.#scheduleFlush();\n }\n\n /**\n * Append messages applied optimistically by a local `submit()`,\n * keyed by id so the eventual server echo reconciles cleanly.\n *\n * Unlike {@link applyValues}, the supplied messages are *not* treated\n * as an authoritative ordered snapshot: they are appended to the end\n * of the current projection (or replaced in place when the id already\n * exists), preserving prior history ordering. When the server later\n * emits a `values` snapshot containing the same ids,\n * {@link applyValues} → {@link reconcileMessagesFromValues} takes over\n * (server ordering wins, the echoed message replaces the optimistic\n * one).\n *\n * Non-message input keys are shallow-merged into `values` via\n * `extraValues`; they are dropped/overwritten automatically by the\n * first server `values` event (which rebuilds `values` from the\n * server snapshot), or rolled back via {@link restoreValueKeys} when\n * the run fails before any echo.\n *\n * @param messages - Optimistic messages (already coerced to\n * `BaseMessage` instances, each carrying a stable id).\n * @param extraValues - Non-message input keys to shallow-merge into\n * `values`.\n * @param options - When `sync` is true the staged write is\n * committed to the store *synchronously* instead of being coalesced\n * onto the next macrotask. Used for discrete, user-initiated\n * optimistic writes (`submit()` / `respond()` / `respondAll()`):\n * committing in the same tick as the triggering event lets the\n * framework render the optimistic message in the *same* commit as\n * any local state the caller flipped alongside it (e.g. a HITL form\n * hiding its inputs), so the pushed card never blinks out for the\n * one-macrotask window before the flush lands. Streaming writes\n * (`handleMessage` / `applyValues`) keep the default macrotask\n * coalescing, which is what tames high-frequency SSE bursts.\n */\n appendOptimistic(\n messages: BaseMessage[],\n extraValues?: Record<string, unknown>,\n options?: { sync?: boolean }\n ): void {\n let working = this.#pendingMessages ?? this.#store.getSnapshot().messages;\n let mutated = false;\n for (const message of messages) {\n const id = message.id;\n if (id == null) continue;\n const existingIdx = this.#indexById.get(id);\n if (existingIdx == null) {\n if (!mutated) {\n working = working.slice();\n mutated = true;\n }\n this.#indexById.set(id, working.length);\n working.push(message);\n } else if (!messagesEqual(working[existingIdx], message)) {\n if (!mutated) {\n working = working.slice();\n mutated = true;\n }\n working[existingIdx] = message;\n }\n }\n\n const baselineValues =\n this.#pendingValues ?? this.#store.getSnapshot().values;\n let values = baselineValues;\n if (extraValues != null && Object.keys(extraValues).length > 0) {\n values = { ...(baselineValues as object), ...extraValues } as StateType;\n }\n values = syncMessagesIntoValues(values, this.#messagesKey, working);\n if (!mutated && values === baselineValues) return;\n this.#pendingMessages = working;\n if (values !== baselineValues) this.#pendingValues = values;\n if (options?.sync) {\n // Commit now so the optimistic message is visible in the same tick\n // as the user event that produced it (no one-macrotask blink). Any\n // flush already scheduled by a prior streaming write is absorbed\n // here; its pending timer fires later as a no-op.\n this.#flushPending();\n } else {\n this.#scheduleFlush();\n }\n }\n\n /**\n * Drop optimistic messages by id without disturbing the rest of the\n * projection. Used by {@link StreamController.hydrate} to remove\n * never-persisted optimistic messages (`pending` / `failed`) so a\n * reload converges to server truth.\n *\n * @param ids - Message ids to remove.\n */\n dropOptimisticMessages(ids: ReadonlySet<string>): void {\n if (ids.size === 0) return;\n const baselineMessages =\n this.#pendingMessages ?? this.#store.getSnapshot().messages;\n const next = baselineMessages.filter((m) => m.id == null || !ids.has(m.id));\n if (next.length === baselineMessages.length) return;\n this.#indexById.clear();\n for (const [id, idx] of buildMessageIndex(next)) {\n this.#indexById.set(id, idx);\n }\n const baselineValues =\n this.#pendingValues ?? this.#store.getSnapshot().values;\n this.#pendingMessages = next;\n this.#pendingValues = syncMessagesIntoValues(\n baselineValues,\n this.#messagesKey,\n next\n );\n this.#scheduleFlush();\n }\n\n /**\n * Restore (or delete) `values` keys that were optimistically merged\n * by {@link appendOptimistic} but never echoed by the server — i.e.\n * roll back non-message optimistic state when the run fails before\n * any `values` event lands. Messages are left untouched (kept on\n * failure per the optimistic contract).\n *\n * @param restore - Per-key pre-submit snapshot: when `hadKey` is\n * false the key is deleted, otherwise it is reset to `prevValue`.\n */\n restoreValueKeys(\n restore: ReadonlyArray<{\n key: string;\n hadKey: boolean;\n prevValue: unknown;\n }>\n ): void {\n if (restore.length === 0) return;\n const baselineValues =\n this.#pendingValues ?? this.#store.getSnapshot().values;\n const next = { ...(baselineValues as Record<string, unknown>) };\n let changed = false;\n for (const { key, hadKey, prevValue } of restore) {\n if (key === this.#messagesKey) continue;\n if (hadKey) {\n if (!Object.is(next[key], prevValue)) {\n next[key] = prevValue;\n changed = true;\n }\n } else if (Object.prototype.hasOwnProperty.call(next, key)) {\n delete next[key];\n changed = true;\n }\n }\n if (!changed) return;\n this.#pendingValues = next as StateType;\n this.#scheduleFlush();\n }\n\n /**\n * Schedule a coalesced flush on the next macrotask. Idempotent\n * within a tick — multiple `handleMessage` / `applyValues` calls\n * before the flush fires collapse into one store write.\n *\n * `setTimeout(0)` is a macrotask: it runs after the current\n * microtask chain drains, so a burst of SSE events processed by\n * the controller's `for await` pump becomes one `store.setState`\n * (and therefore one `useSyncExternalStore` notification).\n */\n #scheduleFlush = (): void => {\n if (this.#flushScheduled) return;\n this.#flushScheduled = true;\n setTimeout(this.#flushPending, 0);\n };\n\n /**\n * Drain `#pendingMessages` / `#pendingValues` to the store in a\n * single `setState` call.\n */\n #flushPending = (): void => {\n this.#flushScheduled = false;\n const messages = this.#pendingMessages;\n const values = this.#pendingValues;\n this.#pendingMessages = null;\n this.#pendingValues = null;\n if (messages == null && values == null) return;\n this.#store.setState((s) => {\n // Other rootStore mutators (controller-driven `isLoading`,\n // `interrupts`, `toolCalls`, etc.) do not touch `s.messages`\n // / `s.values`, so a last-write-wins commit on those two\n // fields is safe.\n if (messages == null) {\n return values == null ? s : { ...s, values };\n }\n if (values == null) return { ...s, messages };\n return { ...s, messages, values };\n });\n };\n}\n\n/**\n * Mirror a freshly-updated message list into `values[messagesKey]`.\n *\n * Returns the same `values` reference when the list is already\n * equal-by-content so the caller can keep the existing snapshot\n * identity (and avoid spurious `setSnapshot` notifications).\n */\nfunction syncMessagesIntoValues<StateType extends object>(\n values: StateType,\n messagesKey: string,\n messages: BaseMessage[]\n): StateType {\n const record = values as Record<string, unknown>;\n const current = record[messagesKey];\n if (Array.isArray(current) && messagesEqualList(current, messages)) {\n return values;\n }\n return {\n ...record,\n [messagesKey]: messages,\n } as StateType;\n}\n\n/**\n * True when two `BaseMessage` arrays carry the same per-message\n * content (using {@link messagesEqual}).\n */\nfunction messagesEqualList(\n previous: readonly BaseMessage[],\n next: readonly BaseMessage[]\n): boolean {\n if (previous === next) return true;\n if (previous.length !== next.length) return false;\n for (let i = 0; i < previous.length; i += 1) {\n if (!messagesEqual(previous[i], next[i])) return false;\n }\n return true;\n}\n\n/**\n * Shallow-equal for `values` objects, *ignoring* the messages slot.\n *\n * The messages array is compared separately by the caller (via\n * {@link messagesEqualList}) because both arrays contain class\n * instances whose JSON representation is not stable across reads.\n */\nfunction stateValuesShallowEqual(\n previous: object,\n next: object,\n messagesKey: string\n): boolean {\n if (previous === next) return true;\n const previousRecord = previous as Record<string, unknown>;\n const nextRecord = next as Record<string, unknown>;\n const previousKeys = Object.keys(previousRecord);\n const nextKeys = Object.keys(nextRecord);\n if (previousKeys.length !== nextKeys.length) return false;\n for (const key of previousKeys) {\n if (!Object.prototype.hasOwnProperty.call(nextRecord, key)) return false;\n const previousValue = previousRecord[key];\n const nextValue = nextRecord[key];\n if (\n key === messagesKey &&\n Array.isArray(previousValue) &&\n Array.isArray(nextValue)\n ) {\n continue;\n }\n if (!Object.is(previousValue, nextValue)) return false;\n }\n return true;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"root-message-projection.cjs","names":["#messagesKey","#store","MessageAssembler","#roles","#indexById","#toolCallIdByNamespace","#sealedMessageIds","#assembler","#valuesMessageIds","#pendingMessages","#pendingValues","#flushScheduled","#maxStep","#sealStep","namespaceKey","assembledMessageToBaseMessage","messagesEqual","#scheduleFlush","reconcileMessagesFromValues","shouldPreferValuesMessage","buildMessageIndex","#flushPending"],"sources":["../../src/stream/root-message-projection.ts"],"sourcesContent":["/**\n * Root-namespace message projection.\n *\n * # What this module is\n *\n * The {@link RootMessageProjection} is the piece of the\n * {@link StreamController} that owns \"what messages does the root\n * namespace currently contain?\". It assembles streamed message deltas\n * via {@link MessageAssembler}, reconciles them against authoritative\n * `values.messages` snapshots from the server, and writes the merged\n * list back into the controller's root snapshot store.\n *\n * # Two streams of truth\n *\n * Root messages arrive on two channels and need to merge cleanly:\n *\n * - **`messages` channel.** Token-level deltas that build messages\n * incrementally. The {@link MessageAssembler} keeps partial\n * messages by id and emits an updated `BaseMessage` per delta.\n * - **`values` channel.** Periodic full-state snapshots that include\n * the authoritative messages array. Used for ordering, removals,\n * and forks (where the streamed messages may pre-date the new\n * timeline).\n *\n * The reconciliation rules (delegated to\n * {@link reconcileMessagesFromValues}) preserve in-flight streamed\n * content while letting values dictate ordering, removals, and\n * same-id metadata enrichment (e.g. committed attachment kwargs).\n *\n * # Tool-message namespace correlation\n *\n * Tool messages arrive on `messages-start` events with `role: \"tool\"`\n * but the start event doesn't always include a `tool_call_id`. We\n * recover it via three fallbacks:\n *\n * 1. The start event itself, when the server includes it.\n * 2. The legacy `<id>-tool-<call_id>` message id format.\n * 3. The most recent `tool-started` event recorded under the same\n * namespace via {@link recordToolCallNamespace}.\n *\n * Without this correlation, tool messages render with empty\n * `tool_call_id` and downstream UIs can't pair them with the\n * originating tool call.\n *\n * # Store-write batching\n *\n * Every {@link handleMessage} / {@link applyValues} call updates the\n * in-projection bookkeeping (assembler state, id index, role cache)\n * synchronously, then stages the new `messages` / `values` into a\n * pending buffer and schedules a `setTimeout(0)` flush. A single\n * coalesced `store.setState` runs at the next macrotask boundary.\n *\n * The motivation is the long-replay freeze: a thread with hundreds\n * of messages replays through the `messages` channel on refresh or\n * mid-run join. Those events drain through the controller's\n * `for await` pump as a long microtask chain. Per-event\n * `store.setState` notifies `useSyncExternalStore` per event, and\n * after enough notifications React's `nestedUpdateCount` guard trips\n * with \"Maximum update depth exceeded\", permanently freezing the UI\n * on the first few messages. Coalescing to one notification per\n * macrotask lets React's scheduler commit between flushes.\n *\n * # Lifecycle\n *\n * - `handleMessage(event)` — apply a `messages` event delta.\n * - `applyValues(values, msgs)` — merge a `values` snapshot.\n * - `recordToolCallNamespace(ns, id)` — capture `namespace → tool_call_id`\n * so subsequent tool message starts can recover the id.\n * - `reset()` — clear all state on thread rebind.\n */\nimport type {\n MessagesEvent,\n MessageRole,\n MessageStartData,\n} from \"@langchain/protocol\";\nimport type { BaseMessage } from \"@langchain/core/messages\";\nimport { MessageAssembler } from \"../client/stream/messages.js\";\nimport {\n assembledMessageToBaseMessage,\n type ExtendedMessageRole,\n} from \"./assembled-to-message.js\";\nimport type { StreamStore } from \"./store.js\";\nimport type { RootSnapshot } from \"./types.js\";\nimport { namespaceKey } from \"./namespace.js\";\nimport {\n buildMessageIndex,\n messagesEqual,\n reconcileMessagesFromValues,\n shouldPreferValuesMessage,\n} from \"./message-reconciliation.js\";\n\n/**\n * Root-namespace message projection. Owns the merge between the\n * `messages` (streamed deltas) and `values` (authoritative\n * snapshots) channels for the root namespace.\n *\n * @typeParam StateType - Root state shape; the messages array is read\n * from `values[messagesKey]`.\n * @typeParam InterruptType - Shape of root protocol interrupts (forwarded\n * into `RootSnapshot` updates).\n */\nexport class RootMessageProjection<\n StateType extends object,\n InterruptType = unknown,\n> {\n /**\n * Key inside `values` that holds the message array. Defaults to\n * `\"messages\"` in the controller; configurable for state graphs\n * that surface messages under a different slot.\n */\n readonly #messagesKey: string;\n\n /** Root snapshot store written to on every merge. */\n readonly #store: StreamStore<RootSnapshot<StateType, InterruptType>>;\n\n /**\n * Stateful chunk assembler for in-flight messages. Reset (via a\n * fresh instance) on every {@link reset} so a new thread starts\n * with no half-built messages from the previous one.\n */\n #assembler = new MessageAssembler();\n\n /**\n * `messageId → role/toolCallId` captured from `message-start` events.\n * The assembler's intermediate output drops these fields, so we cache\n * them at start-time and reapply when projecting to a `BaseMessage`.\n */\n readonly #roles = new Map<\n string,\n { role: ExtendedMessageRole; toolCallId?: string }\n >();\n\n /**\n * `messageId → position in #store.messages` for fast in-place\n * updates as deltas arrive. Rebuilt on every full reconciliation\n * driven by a `values` event.\n */\n readonly #indexById = new Map<string, number>();\n\n /**\n * Ids observed in the most recent `values.messages` snapshot.\n * Reconciliation uses this to detect server-side removals: a\n * previously-seen id missing from the next snapshot means it was\n * removed by the server (and should drop from the projection).\n */\n #valuesMessageIds = new Set<string>();\n\n /**\n * `namespaceKey → tool_call_id` captured from root `tool-started`\n * events. Used as a fallback when a tool-role `message-start` is\n * missing its `tool_call_id` field.\n */\n readonly #toolCallIdByNamespace = new Map<string, string>();\n\n /**\n * Coalescing buffer for store writes. {@link handleMessage} and\n * {@link applyValues} stage their computed `messages` / `values`\n * here instead of calling `store.setState` per event. A single\n * `setTimeout(0)` flush commits them in one `setState`, so a\n * burst of SSE events draining as a microtask chain becomes one\n * store notification at the next macrotask boundary.\n *\n * `null` means \"no staged write\" — once a flush settles, the\n * slots are cleared so the next call starts from the latest\n * committed store snapshot.\n */\n #pendingMessages: BaseMessage[] | null = null;\n #pendingValues: StateType | null = null;\n #flushScheduled = false;\n\n /**\n * Highest checkpoint `step` whose `values` snapshot has been applied.\n * Seeded by {@link StreamController.hydrate} from `getState()` and\n * advanced by live `values` events. A snapshot arriving with a lower\n * step is an older checkpoint replayed by the content pump on\n * reconnect; it is reconciled in add-only mode so it cannot remove\n * the seeded message tail (the final assistant turn). `undefined`\n * until the first step-bearing snapshot, where the legacy\n * remove-on-absence behavior is preserved.\n */\n #maxStep: number | undefined = undefined;\n\n /**\n * Message ids seeded as complete-and-final from an idle thread's\n * `getState()` snapshot. An idle thread defers its root SSE pump, and\n * the first `submit()` brings it up — at which point the transport\n * replays the finished run from `seq=0`. Unlike the `values` channel\n * (guarded by {@link #maxStep}), `messages`-channel deltas carry no\n * step, so that replay would otherwise rebuild each already-complete\n * message from an empty `message-start` and re-stream the whole turn\n * token-by-token, clobbering the seeded tail (a visible \"messages\n * replay\" on the first submit). Deltas for a sealed id are dropped in\n * {@link handleMessage}. The seal is lifted once a checkpoint advances\n * strictly past {@link #sealStep} (see {@link applyValues}) or on\n * thread rebind ({@link reset}). New ids from the next run are never\n * sealed, so they stream normally.\n */\n readonly #sealedMessageIds = new Set<string>();\n\n /**\n * High-water {@link #maxStep} captured when {@link sealMessageIds} ran,\n * i.e. the seed checkpoint's step (or `undefined` when `getState()`\n * carried no `metadata.step`). It is the boundary between the replayed\n * idle history (steps `<= #sealStep`, emitted by the deferred pump's\n * `seq=0` replay) and the new run (steps `> #sealStep`); only a\n * checkpoint strictly past it lifts the seal. Without this boundary the\n * replayed old-run checkpoints — which themselves carry increasing\n * steps — would advance {@link #maxStep} and lift the seal mid-replay,\n * reopening the clobber. When the seed step is unknown the boundary\n * stays `undefined` and the seal holds until {@link reset}; the\n * `values` channel (which ignores the seal) still reconciles any\n * genuine change to a sealed id, only its streamed deltas are dropped.\n */\n #sealStep: number | undefined = undefined;\n\n /**\n * @param params.messagesKey - Key inside `values` that holds the\n * message array.\n * @param params.store - Root snapshot store to mutate.\n */\n constructor(params: {\n messagesKey: string;\n store: StreamStore<RootSnapshot<StateType, InterruptType>>;\n }) {\n this.#messagesKey = params.messagesKey;\n this.#store = params.store;\n }\n\n /**\n * Drop all per-thread state. Called by the controller on thread\n * rebind / dispose so a swap doesn't surface stale messages.\n */\n reset(): void {\n this.#assembler = new MessageAssembler();\n this.#roles.clear();\n this.#indexById.clear();\n this.#valuesMessageIds = new Set();\n this.#toolCallIdByNamespace.clear();\n // Drop any unflushed pending writes — they were computed against\n // the previous thread's baseline and committing them after a\n // rebind would bleed stale messages into the new thread.\n this.#pendingMessages = null;\n this.#pendingValues = null;\n this.#flushScheduled = false;\n this.#maxStep = undefined;\n this.#sealedMessageIds.clear();\n this.#sealStep = undefined;\n }\n\n /**\n * Seal message ids so the streamed `messages` channel cannot downgrade\n * them to partial re-streams. Called by {@link StreamController.hydrate}\n * after seeding an idle thread, whose deferred pump replays the finished\n * run from `seq=0` on the first submit.\n *\n * Captures the current {@link #maxStep} as the lift boundary\n * ({@link #sealStep}). The seal is applied immediately after the seed's\n * `getState()` snapshot is reconciled, so `#maxStep` here is the seed\n * step (or `undefined` when `getState()` carried no `metadata.step`).\n * The seal is lifted once a checkpoint advances strictly past that\n * boundary (see {@link applyValues}) or on thread rebind\n * ({@link reset}).\n *\n * @param ids - Complete message ids from the idle `getState()` seed.\n */\n sealMessageIds(ids: Iterable<string>): void {\n for (const id of ids) this.#sealedMessageIds.add(id);\n if (this.#sealStep == null) this.#sealStep = this.#maxStep;\n }\n\n /**\n * Record a `namespace → tool_call_id` mapping captured from a root\n * `tool-started` event.\n *\n * The companion tool-role `message-start` event may not carry a\n * `tool_call_id`, so we fall back to the most recent value recorded\n * here for the same namespace.\n *\n * @param namespace - Event namespace from the `tool-started` event.\n * @param toolCallId - Tool call id from the same event.\n */\n recordToolCallNamespace(\n namespace: readonly string[],\n toolCallId: string\n ): void {\n this.#toolCallIdByNamespace.set(namespaceKey(namespace), toolCallId);\n }\n\n /**\n * Apply a `messages` channel event to the projection.\n *\n * Captures role/tool metadata on `message-start`, feeds the chunk\n * to the assembler, projects the assembled output to a\n * {@link BaseMessage}, and either appends or in-place updates the\n * pending messages buffer based on whether the id was seen before.\n *\n * @param event - The `messages` channel event to consume.\n */\n handleMessage(event: MessagesEvent): void {\n const data = event.params.data;\n if (data.event === \"message-start\") {\n const startData = data as MessageStartData;\n const role = (startData.role ?? \"ai\") as MessageRole;\n const extendedRole =\n (startData as { role?: ExtendedMessageRole }).role ?? role;\n let toolCallId = (startData as { tool_call_id?: string }).tool_call_id;\n // Tool messages need a tool_call_id to render. Fall back through:\n // 1. legacy `<id>-tool-<call_id>` message id format\n // 2. namespace-recorded tool_call_id (from #recordToolCallNamespace)\n if (extendedRole === \"tool\" && toolCallId == null) {\n const messageId = startData.id;\n if (messageId != null) {\n const match = /-tool-(.+)$/.exec(messageId);\n if (match != null) toolCallId = match[1];\n }\n if (toolCallId == null) {\n toolCallId = this.#toolCallIdByNamespace.get(\n namespaceKey(event.params.namespace)\n );\n }\n }\n if (startData.id != null) {\n this.#roles.set(startData.id, {\n role: extendedRole,\n toolCallId,\n });\n }\n }\n\n const update = this.#assembler.consume(event);\n if (update == null) return;\n const id = update.message.id;\n if (id == null) return;\n // A sealed id belongs to a message seeded complete from an idle\n // thread's `getState()`; the deferred pump's `seq=0` replay would\n // otherwise rebuild it from an empty start and re-stream the whole\n // turn. Drop the replayed delta — the authoritative seed already\n // holds the final content (see {@link #sealedMessageIds}).\n if (this.#sealedMessageIds.has(id)) return;\n const captured = this.#roles.get(id) ?? { role: \"ai\" as const };\n const base = assembledMessageToBaseMessage(update.message, captured.role, {\n toolCallId: captured.toolCallId,\n });\n\n // Compute against the pending baseline if we have one (so an\n // earlier handleMessage in the same tick is the input to this\n // one), else against the latest committed store snapshot.\n // `#indexById` is the synchronous source of truth for \"where is\n // each id in the current messages list\" — every code path below\n // keeps it in sync before returning.\n const baselineMessages =\n this.#pendingMessages ?? this.#store.getSnapshot().messages;\n const existingIdx = this.#indexById.get(id);\n let messages: BaseMessage[];\n if (existingIdx == null) {\n this.#indexById.set(id, baselineMessages.length);\n messages = [...baselineMessages, base];\n } else if (messagesEqual(baselineMessages[existingIdx], base)) {\n // Identical re-emission — skip the store write to keep\n // snapshot identity stable.\n return;\n } else {\n messages = baselineMessages.slice();\n messages[existingIdx] = base;\n }\n\n // Mirror the new messages list into `values[messagesKey]` so\n // direct `values` reads (used by some hooks and by the eventual\n // `values` reconciliation) stay in sync.\n const baselineValues =\n this.#pendingValues ?? this.#store.getSnapshot().values;\n const values = syncMessagesIntoValues(\n baselineValues,\n this.#messagesKey,\n messages\n );\n this.#pendingMessages = messages;\n if (values !== baselineValues) this.#pendingValues = values;\n this.#scheduleFlush();\n }\n\n /**\n * Reconcile a full `values` snapshot into the projection.\n *\n * Delegates the merge to {@link reconcileMessagesFromValues}:\n * values stays authoritative for ordering, removals, and same-id\n * metadata enrichment, while streamed in-flight messages keep\n * content that has moved past the snapshot. Empty messages just\n * refresh the values blob.\n *\n * Rebuilds {@link #indexById} after the merge so subsequent delta\n * applications target the new positions.\n *\n * @param nextValues - Full values snapshot from the `values` event.\n * @param nextMessages - The messages array extracted from\n * `values[messagesKey]` and coerced to `BaseMessage` instances.\n * @param opts.step - Checkpoint superstep for this snapshot, when\n * known. A snapshot whose step is below the highest applied step is\n * treated as a stale reconnect replay and reconciled add-only.\n */\n applyValues(\n nextValues: StateType,\n nextMessages: BaseMessage[],\n opts?: { step?: number }\n ): void {\n const baselineSnapshot = this.#store.getSnapshot();\n const baselineMessages = this.#pendingMessages ?? baselineSnapshot.messages;\n const baselineValues = this.#pendingValues ?? baselineSnapshot.values;\n\n const step = opts?.step;\n // Stale only when we have both a prior high-water step and a lower\n // incoming step. A missing step preserves the legacy semantics.\n const addOnly =\n step != null && this.#maxStep != null && step < this.#maxStep;\n if (step != null && (this.#maxStep == null || step > this.#maxStep)) {\n this.#maxStep = step;\n }\n // Lift the replay seal only when a checkpoint advances strictly past\n // the step captured when the ids were sealed (the seed step). That\n // boundary separates the replayed idle history (steps <= #sealStep,\n // emitted by the deferred pump's seq=0 replay) from the new run\n // (steps > #sealStep), so crossing it means seeded ids may now take\n // genuine streamed updates. Replayed old-run checkpoints advance\n // #maxStep but never reach past #sealStep, so they can't lift it. A\n // `null` boundary (the seed step was unknown) keeps the seal until\n // reset() — we can't tell replay from live, and the values channel\n // still reconciles a sealed id even while its streamed deltas drop.\n if (\n this.#sealedMessageIds.size > 0 &&\n step != null &&\n this.#sealStep != null &&\n step > this.#sealStep\n ) {\n this.#sealedMessageIds.clear();\n }\n\n if (nextMessages.length === 0) {\n if (\n stateValuesShallowEqual(baselineValues, nextValues, this.#messagesKey)\n ) {\n return;\n }\n // Mirror the current `messages` list back into the values slot\n // so the staged snapshot stays consistent with the (separately\n // tracked) messages array.\n this.#pendingValues = syncMessagesIntoValues(\n nextValues,\n this.#messagesKey,\n baselineMessages\n );\n this.#scheduleFlush();\n return;\n }\n\n const reconciliation = reconcileMessagesFromValues({\n valueMessages: nextMessages,\n currentMessages: baselineMessages,\n currentIndexById: this.#indexById,\n previousValueMessageIds: this.#valuesMessageIds,\n preferValuesMessage: shouldPreferValuesMessage,\n addOnly,\n });\n // A stale replay snapshot must not shrink the authoritative id set:\n // keep the (larger) seeded set so a genuinely-newer removal is still\n // detected once the timeline advances past the seed.\n if (!addOnly) this.#valuesMessageIds = reconciliation.valueMessageIds;\n const messages = reconciliation.messages as BaseMessage[];\n const values = {\n ...(nextValues as Record<string, unknown>),\n [this.#messagesKey]: messages,\n } as StateType;\n if (\n messages === baselineMessages &&\n stateValuesShallowEqual(baselineValues, values, this.#messagesKey)\n ) {\n return;\n }\n\n // Reconciliation may reorder, drop, or substitute messages, so\n // rebuild the id → index map to match the new array.\n this.#indexById.clear();\n for (const [id, idx] of buildMessageIndex(messages)) {\n this.#indexById.set(id, idx);\n }\n this.#pendingMessages = messages;\n this.#pendingValues = values;\n this.#scheduleFlush();\n }\n\n /**\n * Append messages applied optimistically by a local `submit()`,\n * keyed by id so the eventual server echo reconciles cleanly.\n *\n * Unlike {@link applyValues}, the supplied messages are *not* treated\n * as an authoritative ordered snapshot: they are appended to the end\n * of the current projection (or replaced in place when the id already\n * exists), preserving prior history ordering. When the server later\n * emits a `values` snapshot containing the same ids,\n * {@link applyValues} → {@link reconcileMessagesFromValues} takes over\n * (server ordering wins; the echoed message replaces the optimistic\n * one when content matches *and* values adds metadata the optimistic\n * copy lacks, so server-authored `additional_kwargs` is visible\n * without waiting for hydration — lagging/poorer snapshots do not\n * strip richer current metadata).\n * In-flight streamed content still wins when it has moved past the\n * snapshot.\n *\n * Non-message input keys are shallow-merged into `values` via\n * `extraValues`; they are dropped/overwritten automatically by the\n * first server `values` event (which rebuilds `values` from the\n * server snapshot), or rolled back via {@link restoreValueKeys} when\n * the run fails before any echo.\n *\n * @param messages - Optimistic messages (already coerced to\n * `BaseMessage` instances, each carrying a stable id).\n * @param extraValues - Non-message input keys to shallow-merge into\n * `values`.\n * @param options - When `sync` is true the staged write is\n * committed to the store *synchronously* instead of being coalesced\n * onto the next macrotask. Used for discrete, user-initiated\n * optimistic writes (`submit()` / `respond()` / `respondAll()`):\n * committing in the same tick as the triggering event lets the\n * framework render the optimistic message in the *same* commit as\n * any local state the caller flipped alongside it (e.g. a HITL form\n * hiding its inputs), so the pushed card never blinks out for the\n * one-macrotask window before the flush lands. Streaming writes\n * (`handleMessage` / `applyValues`) keep the default macrotask\n * coalescing, which is what tames high-frequency SSE bursts.\n */\n appendOptimistic(\n messages: BaseMessage[],\n extraValues?: Record<string, unknown>,\n options?: { sync?: boolean }\n ): void {\n let working = this.#pendingMessages ?? this.#store.getSnapshot().messages;\n let mutated = false;\n for (const message of messages) {\n const id = message.id;\n if (id == null) continue;\n const existingIdx = this.#indexById.get(id);\n if (existingIdx == null) {\n if (!mutated) {\n working = working.slice();\n mutated = true;\n }\n this.#indexById.set(id, working.length);\n working.push(message);\n } else if (!messagesEqual(working[existingIdx], message)) {\n if (!mutated) {\n working = working.slice();\n mutated = true;\n }\n working[existingIdx] = message;\n }\n }\n\n const baselineValues =\n this.#pendingValues ?? this.#store.getSnapshot().values;\n let values = baselineValues;\n if (extraValues != null && Object.keys(extraValues).length > 0) {\n values = { ...(baselineValues as object), ...extraValues } as StateType;\n }\n values = syncMessagesIntoValues(values, this.#messagesKey, working);\n if (!mutated && values === baselineValues) return;\n this.#pendingMessages = working;\n if (values !== baselineValues) this.#pendingValues = values;\n if (options?.sync) {\n // Commit now so the optimistic message is visible in the same tick\n // as the user event that produced it (no one-macrotask blink). Any\n // flush already scheduled by a prior streaming write is absorbed\n // here; its pending timer fires later as a no-op.\n this.#flushPending();\n } else {\n this.#scheduleFlush();\n }\n }\n\n /**\n * Drop optimistic messages by id without disturbing the rest of the\n * projection. Used by {@link StreamController.hydrate} to remove\n * never-persisted optimistic messages (`pending` / `failed`) so a\n * reload converges to server truth.\n *\n * @param ids - Message ids to remove.\n */\n dropOptimisticMessages(ids: ReadonlySet<string>): void {\n if (ids.size === 0) return;\n const baselineMessages =\n this.#pendingMessages ?? this.#store.getSnapshot().messages;\n const next = baselineMessages.filter((m) => m.id == null || !ids.has(m.id));\n if (next.length === baselineMessages.length) return;\n this.#indexById.clear();\n for (const [id, idx] of buildMessageIndex(next)) {\n this.#indexById.set(id, idx);\n }\n const baselineValues =\n this.#pendingValues ?? this.#store.getSnapshot().values;\n this.#pendingMessages = next;\n this.#pendingValues = syncMessagesIntoValues(\n baselineValues,\n this.#messagesKey,\n next\n );\n this.#scheduleFlush();\n }\n\n /**\n * Restore (or delete) `values` keys that were optimistically merged\n * by {@link appendOptimistic} but never echoed by the server — i.e.\n * roll back non-message optimistic state when the run fails before\n * any `values` event lands. Messages are left untouched (kept on\n * failure per the optimistic contract).\n *\n * @param restore - Per-key pre-submit snapshot: when `hadKey` is\n * false the key is deleted, otherwise it is reset to `prevValue`.\n */\n restoreValueKeys(\n restore: ReadonlyArray<{\n key: string;\n hadKey: boolean;\n prevValue: unknown;\n }>\n ): void {\n if (restore.length === 0) return;\n const baselineValues =\n this.#pendingValues ?? this.#store.getSnapshot().values;\n const next = { ...(baselineValues as Record<string, unknown>) };\n let changed = false;\n for (const { key, hadKey, prevValue } of restore) {\n if (key === this.#messagesKey) continue;\n if (hadKey) {\n if (!Object.is(next[key], prevValue)) {\n next[key] = prevValue;\n changed = true;\n }\n } else if (Object.prototype.hasOwnProperty.call(next, key)) {\n delete next[key];\n changed = true;\n }\n }\n if (!changed) return;\n this.#pendingValues = next as StateType;\n this.#scheduleFlush();\n }\n\n /**\n * Schedule a coalesced flush on the next macrotask. Idempotent\n * within a tick — multiple `handleMessage` / `applyValues` calls\n * before the flush fires collapse into one store write.\n *\n * `setTimeout(0)` is a macrotask: it runs after the current\n * microtask chain drains, so a burst of SSE events processed by\n * the controller's `for await` pump becomes one `store.setState`\n * (and therefore one `useSyncExternalStore` notification).\n */\n #scheduleFlush = (): void => {\n if (this.#flushScheduled) return;\n this.#flushScheduled = true;\n setTimeout(this.#flushPending, 0);\n };\n\n /**\n * Drain `#pendingMessages` / `#pendingValues` to the store in a\n * single `setState` call.\n */\n #flushPending = (): void => {\n this.#flushScheduled = false;\n const messages = this.#pendingMessages;\n const values = this.#pendingValues;\n this.#pendingMessages = null;\n this.#pendingValues = null;\n if (messages == null && values == null) return;\n this.#store.setState((s) => {\n // Other rootStore mutators (controller-driven `isLoading`,\n // `interrupts`, `toolCalls`, etc.) do not touch `s.messages`\n // / `s.values`, so a last-write-wins commit on those two\n // fields is safe.\n if (messages == null) {\n return values == null ? s : { ...s, values };\n }\n if (values == null) return { ...s, messages };\n return { ...s, messages, values };\n });\n };\n}\n\n/**\n * Mirror a freshly-updated message list into `values[messagesKey]`.\n *\n * Returns the same `values` reference when the list is already\n * equal-by-content so the caller can keep the existing snapshot\n * identity (and avoid spurious `setSnapshot` notifications).\n */\nfunction syncMessagesIntoValues<StateType extends object>(\n values: StateType,\n messagesKey: string,\n messages: BaseMessage[]\n): StateType {\n const record = values as Record<string, unknown>;\n const current = record[messagesKey];\n if (Array.isArray(current) && messagesEqualList(current, messages)) {\n return values;\n }\n return {\n ...record,\n [messagesKey]: messages,\n } as StateType;\n}\n\n/**\n * True when two `BaseMessage` arrays carry the same per-message\n * content (using {@link messagesEqual}).\n */\nfunction messagesEqualList(\n previous: readonly BaseMessage[],\n next: readonly BaseMessage[]\n): boolean {\n if (previous === next) return true;\n if (previous.length !== next.length) return false;\n for (let i = 0; i < previous.length; i += 1) {\n if (!messagesEqual(previous[i], next[i])) return false;\n }\n return true;\n}\n\n/**\n * Shallow-equal for `values` objects, *ignoring* the messages slot.\n *\n * The messages array is compared separately by the caller (via\n * {@link messagesEqualList}) because both arrays contain class\n * instances whose JSON representation is not stable across reads.\n */\nfunction stateValuesShallowEqual(\n previous: object,\n next: object,\n messagesKey: string\n): boolean {\n if (previous === next) return true;\n const previousRecord = previous as Record<string, unknown>;\n const nextRecord = next as Record<string, unknown>;\n const previousKeys = Object.keys(previousRecord);\n const nextKeys = Object.keys(nextRecord);\n if (previousKeys.length !== nextKeys.length) return false;\n for (const key of previousKeys) {\n if (!Object.prototype.hasOwnProperty.call(nextRecord, key)) return false;\n const previousValue = previousRecord[key];\n const nextValue = nextRecord[key];\n if (\n key === messagesKey &&\n Array.isArray(previousValue) &&\n Array.isArray(nextValue)\n ) {\n continue;\n }\n if (!Object.is(previousValue, nextValue)) return false;\n }\n return true;\n}\n"],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  import { MessageAssembler } from "../client/stream/messages.js";
2
2
  import { namespaceKey } from "./namespace.js";
3
3
  import { assembledMessageToBaseMessage } from "./assembled-to-message.js";
4
- import { buildMessageIndex, messagesEqual, reconcileMessagesFromValues, shouldPreferValuesMessageForToolCalls } from "./message-reconciliation.js";
4
+ import { buildMessageIndex, messagesEqual, reconcileMessagesFromValues, shouldPreferValuesMessage } from "./message-reconciliation.js";
5
5
  //#region src/stream/root-message-projection.ts
6
6
  /**
7
7
  * Root-namespace message projection. Owns the merge between the
@@ -228,9 +228,10 @@ var RootMessageProjection = class {
228
228
  * Reconcile a full `values` snapshot into the projection.
229
229
  *
230
230
  * Delegates the merge to {@link reconcileMessagesFromValues}:
231
- * values stays authoritative for ordering and removals, while
232
- * streamed in-flight messages keep their content until the server
233
- * echoes them back. Empty messages just refresh the values blob.
231
+ * values stays authoritative for ordering, removals, and same-id
232
+ * metadata enrichment, while streamed in-flight messages keep
233
+ * content that has moved past the snapshot. Empty messages just
234
+ * refresh the values blob.
234
235
  *
235
236
  * Rebuilds {@link #indexById} after the merge so subsequent delta
236
237
  * applications target the new positions.
@@ -261,7 +262,7 @@ var RootMessageProjection = class {
261
262
  currentMessages: baselineMessages,
262
263
  currentIndexById: this.#indexById,
263
264
  previousValueMessageIds: this.#valuesMessageIds,
264
- preferValuesMessage: shouldPreferValuesMessageForToolCalls,
265
+ preferValuesMessage: shouldPreferValuesMessage,
265
266
  addOnly
266
267
  });
267
268
  if (!addOnly) this.#valuesMessageIds = reconciliation.valueMessageIds;
@@ -287,8 +288,13 @@ var RootMessageProjection = class {
287
288
  * exists), preserving prior history ordering. When the server later
288
289
  * emits a `values` snapshot containing the same ids,
289
290
  * {@link applyValues} → {@link reconcileMessagesFromValues} takes over
290
- * (server ordering wins, the echoed message replaces the optimistic
291
- * one).
291
+ * (server ordering wins; the echoed message replaces the optimistic
292
+ * one when content matches *and* values adds metadata the optimistic
293
+ * copy lacks, so server-authored `additional_kwargs` is visible
294
+ * without waiting for hydration — lagging/poorer snapshots do not
295
+ * strip richer current metadata).
296
+ * In-flight streamed content still wins when it has moved past the
297
+ * snapshot.
292
298
  *
293
299
  * Non-message input keys are shallow-merged into `values` via
294
300
  * `extraValues`; they are dropped/overwritten automatically by the