UNPKG

@langchain/langgraph-sdk

Version:

Client library for interacting with the LangGraph API

164 lines (163 loc) • 6.33 kB
import { ensureMessageInstances } from "../message-coercion.js"; import { MessageAssembler } from "../../client/stream/messages.js"; import { isRootNamespace, namespaceKey } from "../namespace.js"; import { assembledMessageToBaseMessage } from "../assembled-to-message.js"; import { buildMessageIndex, reconcileMessagesFromValues, shouldPreferValuesMessage } from "../message-reconciliation.js"; import { openProjectionSubscription } from "./runtime.js"; //#region src/stream/projections/messages.ts function messagesProjection(namespace) { const ns = [...namespace]; return { key: `messages|${namespaceKey(ns)}`, namespace: ns, initial: [], open({ thread, store, rootBus }) { const assembler = new MessageAssembler(); const roleByKey = /* @__PURE__ */ new Map(); const indexById = /* @__PURE__ */ new Map(); const streamMessageIds = /* @__PURE__ */ new Set(); let valuesMessageIds = /* @__PURE__ */ new Set(); const uncommittedIds = /* @__PURE__ */ new Set(); const finishedIds = /* @__PURE__ */ new Set(); const droppedIds = /* @__PURE__ */ new Set(); const rootShortCircuit = isRootNamespace(ns) && rootBus.channels.includes("messages"); let disposed = false; const pendingMessages = []; let dirty = false; let flushScheduled = false; const flushChannel = typeof MessageChannel !== "undefined" ? new MessageChannel() : null; const flush = () => { flushScheduled = false; if (!dirty || disposed) return; dirty = false; store.setValue(pendingMessages.slice()); }; if (flushChannel != null) flushChannel.port1.onmessage = flush; const scheduleFlush = () => { dirty = true; if (flushScheduled) return; flushScheduled = true; if (flushChannel != null) flushChannel.port2.postMessage(null); else setTimeout(flush, 0); }; const applyValuesEvent = (event) => { const data = event.params.data; if (data == null || typeof data !== "object" || Array.isArray(data)) return; const rawMessages = data.messages; if (!Array.isArray(rawMessages) || rawMessages.length === 0) return; const reconciliation = reconcileMessagesFromValues({ valueMessages: ensureMessageInstances(rawMessages), currentMessages: pendingMessages, currentIndexById: indexById, previousValueMessageIds: valuesMessageIds, streamedMessageIds: streamMessageIds, preferValuesMessage: shouldPreferValuesMessage }); valuesMessageIds = reconciliation.valueMessageIds; for (const id of valuesMessageIds) { uncommittedIds.delete(id); finishedIds.delete(id); } const reconciledMessages = [...reconciliation.messages]; pendingMessages.length = 0; for (const message of reconciledMessages) pendingMessages.push(message); indexById.clear(); for (const [id, idx] of buildMessageIndex(pendingMessages)) indexById.set(id, idx); scheduleFlush(); }; const applyEvent = (event) => { const data = event.params.data; if (data.event === "message-start") { const startData = data; const role = startData.role ?? "ai"; const extendedRole = startData.role ?? role; const maybeToolCallId = startData.tool_call_id; if (startData.id != null) roleByKey.set(startData.id, { role: extendedRole, toolCallId: maybeToolCallId }); } const update = assembler.consume(event); if (update == null) return; const msg = update.message; const id = msg.id; if (id == null || droppedIds.has(id)) return; if (!valuesMessageIds.has(id)) uncommittedIds.add(id); if (update.kind === "message-finish") finishedIds.add(id); const captured = roleByKey.get(id) ?? { role: "ai" }; const base = assembledMessageToBaseMessage(msg, captured.role, { toolCallId: captured.toolCallId }); streamMessageIds.add(id); const existingIdx = indexById.get(id); if (existingIdx == null) { indexById.set(id, pendingMessages.length); pendingMessages.push(base); } else pendingMessages[existingIdx] = base; scheduleFlush(); }; const closeRun = (unfinishedOnly) => { assembler.abandonActive(); const dropped = /* @__PURE__ */ new Set(); for (const id of uncommittedIds) { if (unfinishedOnly && finishedIds.has(id)) continue; dropped.add(id); } if (dropped.size === 0) return; for (const id of dropped) { uncommittedIds.delete(id); finishedIds.delete(id); droppedIds.add(id); } const kept = pendingMessages.filter((m) => m.id == null || !dropped.has(m.id)); pendingMessages.length = 0; pendingMessages.push(...kept); indexById.clear(); for (const [id, idx] of buildMessageIndex(pendingMessages)) indexById.set(id, idx); scheduleFlush(); }; const unsubscribeLifecycle = rootBus.subscribe((event) => { if (event.method !== "lifecycle") return; if (!isRootNamespace(event.params.namespace)) return; const lifecycle = event.params.data?.event; if (lifecycle === "running") setTimeout(() => !disposed && closeRun(false), 0); else if (lifecycle === "completed" || lifecycle === "interrupted" || lifecycle === "failed") setTimeout(() => !disposed && closeRun(true), 0); }); let runtime; const openSubscription = () => { runtime = openProjectionSubscription({ thread, channels: ["messages", "values"], namespace: ns, onEvent(event) { if (event.method === "messages") applyEvent(event); else if (event.method === "values") applyValuesEvent(event); } }); }; if (rootShortCircuit) runtime = { dispose: rootBus.subscribe((event) => { if (!isRootNamespace(event.params.namespace)) return; if (event.method === "messages") applyEvent(event); else if (event.method === "values") applyValuesEvent(event); }) }; else (async () => { if (!(await rootBus.trySeedFromHistory?.({ kind: "messages", namespace: ns, store }) === true) && !disposed) openSubscription(); })(); return { async dispose() { disposed = true; unsubscribeLifecycle(); if (flushChannel != null) { flushChannel.port1.onmessage = null; flushChannel.port1.close(); flushChannel.port2.close(); } await runtime?.dispose(); } }; } }; } //#endregion export { messagesProjection }; //# sourceMappingURL=messages.js.map