{"version":3,"file":"messages.cjs","names":["namespaceKey","MessageAssembler","isRootNamespace","reconcileMessagesFromValues","ensureMessageInstances","shouldPreferValuesMessage","buildMessageIndex","assembledMessageToBaseMessage","openProjectionSubscription"],"sources":["../../../src/stream/projections/messages.ts"],"sourcesContent":["/**\n * Namespace-scoped `messages` projection.\n *\n * Opens `thread.subscribe({ channels: [\"messages\"], namespaces: [ns] })`\n * and folds each `messages` event through {@link MessageAssembler}.\n * Every update — start, block delta, block finish, message finish —\n * re-derives a `BaseMessage` class instance for the currently-active\n * message and updates its slot in the store.\n *\n * The projection emits `BaseMessage[]` (class instances from\n * `@langchain/core/messages`), never plain serialized objects.\n */\nimport type {\n  MessagesEvent,\n  MessageRole,\n  MessageStartData,\n  ValuesEvent,\n} from \"@langchain/protocol\";\nimport type { BaseMessage } from \"@langchain/core/messages\";\nimport { MessageAssembler } from \"../../client/stream/messages.js\";\nimport {\n  assembledMessageToBaseMessage,\n  type ExtendedMessageRole,\n} from \"../assembled-to-message.js\";\nimport type { Message } from \"../../types.messages.js\";\nimport type { ProjectionSpec, ProjectionRuntime } from \"../types.js\";\nimport { ensureMessageInstances } from \"../message-coercion.js\";\nimport { isRootNamespace, namespaceKey } from \"../namespace.js\";\nimport {\n  buildMessageIndex,\n  reconcileMessagesFromValues,\n  shouldPreferValuesMessage,\n} from \"../message-reconciliation.js\";\nimport { openProjectionSubscription } from \"./runtime.js\";\n\nexport function messagesProjection(\n  namespace: readonly string[]\n): ProjectionSpec<BaseMessage[]> {\n  const ns = [...namespace];\n  const key = `messages|${namespaceKey(ns)}`;\n\n  return {\n    key,\n    namespace: ns,\n    initial: [],\n    open({ thread, store, rootBus }): ProjectionRuntime {\n      const assembler = new MessageAssembler();\n      // Per-messageId state needed for BaseMessage projection:\n      //  - `role` is only in the `message-start` event; we cache it\n      //    so subsequent delta events still produce a typed message.\n      //  - `toolCallId` is pulled from message-start extras when role\n      //    is `tool` (a convention we keep compatible with serialized\n      //    v1 tool messages).\n      const roleByKey = new Map<\n        string,\n        { role: ExtendedMessageRole; toolCallId?: string }\n      >();\n      const indexById = new Map<string, number>();\n      // Ids this projection has observed via the `messages` channel\n      // (token-level deltas). Used by `applyValuesEvent` to prefer the\n      // stream-assembled version over the values-coerced shape while a\n      // turn is streaming, matching the root controller's policy.\n      const streamMessageIds = new Set<string>();\n      // Ids observed in the most recent `values.messages` snapshot.\n      // Messages that were present in a prior snapshot but are absent\n      // from this one are treated as explicit removals (server-side\n      // `RemoveMessage` reducer deltas). Stream-only messages (seen on\n      // the messages channel but never echoed in a values snapshot)\n      // are preserved — their enclosing superstep may simply not have\n      // committed yet.\n      let valuesMessageIds = new Set<string>();\n      // Same run-boundary rule as the root projection, driven by root\n      // lifecycle events: a terminal drops streamed, uncommitted messages\n      // that never finished; the next `running` drops the rest. Streamed\n      // events for a dropped id are ignored from then on, so a replayed\n      // `message-start` can't rebuild it.\n      const uncommittedIds = new Set<string>();\n      const finishedIds = new Set<string>();\n      const droppedIds = new Set<string>();\n\n      // Root-scoped projections whose channels are already covered by\n      // the controller's root pump attach to the shared fan-out\n      // instead of opening a second server subscription. The root\n      // pump runs at `{namespaces: [[]], depth: 1}`, which is exactly\n      // the scope a root-namespace `messagesProjection` wants.\n      const rootShortCircuit =\n        isRootNamespace(ns) && rootBus.channels.includes(\"messages\");\n\n      let disposed = false;\n\n      // Local mirror of the store contents. Every `applyEvent` /\n      // `applyValuesEvent` mutates this synchronously; a coalesced\n      // `scheduleFlush` copies it to `store` once per macrotask.\n      //\n      // Why the indirection? When a namespace-scoped projection\n      // (e.g. a subagent modal opened after the run finished) first\n      // subscribes, the server replays the entire history from\n      // `seq=0`. Dozens of `messages`-channel events can land in a\n      // single SSE parse — they drain through the `for await` loop\n      // as a long microtask chain. Microtasks run before any\n      // macrotask, so React's concurrent scheduler never gets a\n      // chance to commit between updates. Calling `store.setValue`\n      // per event in that burst overflows React's\n      // `nestedUpdateCount` guard and throws \"Maximum update depth\n      // exceeded\", permanently killing the projection and\n      // leaving the store stuck at its first few messages.\n      //\n      // Batching via `MessageChannel` (macrotask) coalesces the\n      // replay burst into one `setValue` call and lets React\n      // commit between flushes for live token streaming too.\n      const pendingMessages: BaseMessage[] = [];\n      let dirty = false;\n      let flushScheduled = false;\n      const flushChannel =\n        typeof MessageChannel !== \"undefined\" ? new MessageChannel() : null;\n\n      const flush = (): void => {\n        flushScheduled = false;\n        if (!dirty || disposed) return;\n        dirty = false;\n        // `.slice()` breaks identity so React's `Object.is` bail-out\n        // in `StreamStore.setValue` propagates the change.\n        store.setValue(pendingMessages.slice());\n      };\n      if (flushChannel != null) {\n        flushChannel.port1.onmessage = flush;\n      }\n\n      const scheduleFlush = (): void => {\n        dirty = true;\n        if (flushScheduled) return;\n        flushScheduled = true;\n        if (flushChannel != null) {\n          flushChannel.port2.postMessage(null);\n        } else {\n          setTimeout(flush, 0);\n        }\n      };\n\n      // Rebuild the store from `values.messages` snapshots.\n      //\n      // `values` events carry the full, committed state of the\n      // thread's `messages` channel at a checkpoint — they fire\n      // on node completion, AFTER every `messages`-channel delta\n      // for that turn has been emitted. They are the authoritative\n      // source of truth for ORDER and for non-streamed messages\n      // (human turns, serialised tool results, subagent echoes, …).\n      //\n      // Why rebuild rather than merge-by-id?\n      //\n      // In practice the server may emit the same logical message\n      // with DIFFERENT ids across successive `values` snapshots at\n      // the same namespace — e.g. a subagent first surfaces its\n      // seed prompt with a synthetic id like\n      // `subagent:<tool_call_id>:human`, then a later superstep\n      // echoes the same prompt back with a real UUID (or vice\n      // versa). A naive \"match-or-append by id\" strategy treats\n      // each fresh id as a new entry and the list grows\n      // monotonically, showing the same content twice (or more)\n      // in the UI.\n      //\n      // Policy (mirrors the root controller's `#applyValues`):\n      //\n      //  1. Walk `values.messages` in order. For each id, prefer\n      //     the stream-assembled entry if we have one for that id\n      //     (keeps in-progress token streaming visible); otherwise\n      //     take the values-coerced instance. Same-id values still\n      //     win when they carry data the stream lacks:\n      //       - tool messages arriving without `tool_call_id` on\n      //         the messages channel — the values snapshot always\n      //         carries it;\n      //       - AI messages whose finalized `tool_calls` didn't\n      //         fully land via the messages channel — the values\n      //         snapshot's AI message has them populated;\n      //       - metadata enrichment with unchanged content (e.g.\n      //         committed `additional_kwargs` on an echoed human).\n      //\n      //  2. Append any stream-only ids (seen on the messages\n      //     channel but never echoed in ANY values snapshot yet)\n      //     — their enclosing superstep hasn't committed yet, so\n      //     dropping them would flash the UI.\n      //\n      //  3. Ids that WERE in a prior values snapshot but are gone\n      //     from this one are treated as explicit removals\n      //     (`RemoveMessage` reducer deltas) and dropped.\n      //\n      // Unkeyed messages (no stable id) are passed through in\n      // their values order because we can't dedupe them safely.\n      const applyValuesEvent = (event: ValuesEvent): void => {\n        const data = event.params.data;\n        if (data == null || typeof data !== \"object\" || Array.isArray(data)) {\n          return;\n        }\n        const state = data as Record<string, unknown>;\n        const rawMessages = state.messages;\n        if (!Array.isArray(rawMessages) || rawMessages.length === 0) return;\n\n        const coerced = ensureMessageInstances(\n          rawMessages as (Message | BaseMessage)[]\n        );\n\n        const reconciliation = reconcileMessagesFromValues({\n          valueMessages: coerced,\n          currentMessages: pendingMessages,\n          currentIndexById: indexById,\n          previousValueMessageIds: valuesMessageIds,\n          streamedMessageIds: streamMessageIds,\n          preferValuesMessage: shouldPreferValuesMessage,\n        });\n        valuesMessageIds = reconciliation.valueMessageIds;\n        for (const id of valuesMessageIds) {\n          uncommittedIds.delete(id);\n          finishedIds.delete(id);\n        }\n        const reconciledMessages = [...reconciliation.messages];\n\n        pendingMessages.length = 0;\n        for (const message of reconciledMessages) pendingMessages.push(message);\n        indexById.clear();\n        for (const [id, idx] of buildMessageIndex(pendingMessages)) {\n          indexById.set(id, idx);\n        }\n        scheduleFlush();\n      };\n\n      const applyEvent = (event: MessagesEvent): void => {\n        const data = event.params.data;\n\n        if (data.event === \"message-start\") {\n          const startData = data as MessageStartData;\n          const role = (startData.role ?? \"ai\") as MessageRole;\n          // \"tool\" role is a v1 convention not represented in the\n          // protocol enum but common in practice — keep it working\n          // for graphs that emit it as an extensible field.\n          const extendedRole =\n            (startData as { role?: ExtendedMessageRole }).role ?? role;\n          const maybeToolCallId = (startData as { tool_call_id?: string })\n            .tool_call_id;\n          if (startData.id != null) {\n            roleByKey.set(startData.id, {\n              role: extendedRole,\n              toolCallId: maybeToolCallId,\n            });\n          }\n        }\n\n        const update = assembler.consume(event);\n        if (update == null) return;\n        const msg = update.message;\n        const id = msg.id;\n        if (id == null || droppedIds.has(id)) return;\n        if (!valuesMessageIds.has(id)) uncommittedIds.add(id);\n        if (update.kind === \"message-finish\") finishedIds.add(id);\n        const captured = roleByKey.get(id) ?? { role: \"ai\" as const };\n        const base = assembledMessageToBaseMessage(msg, captured.role, {\n          toolCallId: captured.toolCallId,\n        });\n\n        streamMessageIds.add(id);\n        const existingIdx = indexById.get(id);\n        if (existingIdx == null) {\n          indexById.set(id, pendingMessages.length);\n          pendingMessages.push(base);\n        } else {\n          pendingMessages[existingIdx] = base;\n        }\n        scheduleFlush();\n      };\n\n      const closeRun = (unfinishedOnly: boolean): void => {\n        assembler.abandonActive();\n        const dropped = new Set<string>();\n        for (const id of uncommittedIds) {\n          if (unfinishedOnly && finishedIds.has(id)) continue;\n          dropped.add(id);\n        }\n        if (dropped.size === 0) return;\n        for (const id of dropped) {\n          uncommittedIds.delete(id);\n          finishedIds.delete(id);\n          droppedIds.add(id);\n        }\n        const kept = pendingMessages.filter(\n          (m) => m.id == null || !dropped.has(m.id)\n        );\n        pendingMessages.length = 0;\n        pendingMessages.push(...kept);\n        indexById.clear();\n        for (const [id, idx] of buildMessageIndex(pendingMessages)) {\n          indexById.set(id, idx);\n        }\n        scheduleFlush();\n      };\n\n      // Root lifecycle arrives on the root pump, not this subscription.\n      // Defer a macrotask so events this subscription has already\n      // received from before the terminal are applied first.\n      const unsubscribeLifecycle = rootBus.subscribe((event) => {\n        if (event.method !== \"lifecycle\") return;\n        if (!isRootNamespace(event.params.namespace)) return;\n        const lifecycle = (event.params.data as { event?: string })?.event;\n        if (lifecycle === \"running\") {\n          setTimeout(() => !disposed && closeRun(false), 0);\n        } else if (\n          lifecycle === \"completed\" ||\n          lifecycle === \"interrupted\" ||\n          lifecycle === \"failed\"\n        ) {\n          setTimeout(() => !disposed && closeRun(true), 0);\n        }\n      });\n\n      let runtime: ProjectionRuntime | undefined;\n      const openSubscription = () => {\n        runtime = openProjectionSubscription({\n          thread,\n          // Subscribe to both `messages` (live token deltas that drive\n          // the in-flight assistant bubble) and `values` (periodic full-\n          // state snapshots). Consuming values lets late-mounted scoped\n          // projections backfill history after the run has finished.\n          channels: [\"messages\", \"values\"],\n          namespace: ns,\n          onEvent(event) {\n            if (event.method === \"messages\") {\n              applyEvent(event as MessagesEvent);\n            } else if (event.method === \"values\") {\n              applyValuesEvent(event as ValuesEvent);\n            }\n          },\n        });\n      };\n\n      if (rootShortCircuit) {\n        runtime = {\n          dispose: rootBus.subscribe((event) => {\n            if (!isRootNamespace(event.params.namespace)) return;\n            if (event.method === \"messages\") {\n              applyEvent(event as MessagesEvent);\n            } else if (event.method === \"values\") {\n              applyValuesEvent(event as ValuesEvent);\n            }\n          }),\n        };\n      } else {\n        void (async () => {\n          const seeded =\n            (await rootBus.trySeedFromHistory?.({\n              kind: \"messages\",\n              namespace: ns,\n              store,\n            })) === true;\n          if (!seeded && !disposed) openSubscription();\n        })();\n      }\n\n      return {\n        async dispose() {\n          disposed = true;\n          unsubscribeLifecycle();\n          if (flushChannel != null) {\n            flushChannel.port1.onmessage = null;\n            flushChannel.port1.close();\n            flushChannel.port2.close();\n          }\n          await runtime?.dispose();\n        },\n      };\n    },\n  };\n}\n"],"mappings":"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"}