UNPKG

@langchain/langgraph-sdk

Version:

Client library for interacting with the LangGraph API

776 lines (775 loc) • 25.2 kB
require("../../_virtual/_rolldown/runtime.cjs"); const require_multi_cursor_buffer = require("./multi-cursor-buffer.cjs"); let _langchain_core_messages = require("@langchain/core/messages"); //#region src/client/stream/messages.ts function isToolCallChunk(block) { return block?.type === "tool_call_chunk" || block?.type === "server_tool_call_chunk"; } function applyCoreContentDelta(target, delta) { if (target.type !== delta.type) return structuredClone(delta); switch (delta.type) { case "text": return { ...target, ...delta, text: `${"text" in target ? target.text : ""}${delta.text}` }; case "reasoning": return { ...target, ...delta, reasoning: `${"reasoning" in target ? target.reasoning : ""}${delta.reasoning}` }; case "tool_call_chunk": case "server_tool_call_chunk": { const merged = { ...target, ...delta }; if (delta.id == null && "id" in target && target.id != null) merged.id = target.id; if (delta.name == null && "name" in target && target.name != null) merged.name = target.name; merged.args = `${("args" in target ? target.args : "") ?? ""}${delta.args ?? ""}`; return merged; } default: return { ...target, ...delta }; } } function coreContentBlockFromDelta(delta, current) { switch (delta.type) { case "text-delta": return { type: "text", text: delta.text }; case "reasoning-delta": return { type: "reasoning", reasoning: delta.reasoning }; case "data-delta": { const merged = { ...current ?? {}, data: delta.data }; if (delta.encoding) merged.encoding = delta.encoding; return merged; } case "block-delta": return delta.fields; case "tool-call-delta": return isToolCallChunk(current) ? { type: current.type, args: delta.args } : void 0; default: return; } } function applyCoreEventDelta(current, event) { if (event.content) return current ? applyCoreContentDelta(current, event.content) : event.content; switch (event.delta.type) { case "text-delta": if (current?.type === "text") return { ...current, text: `${"text" in current ? current.text : ""}${event.delta.text}` }; return coreContentBlockFromDelta(event.delta, current); case "reasoning-delta": if (current?.type === "reasoning") return { ...current, reasoning: `${"reasoning" in current ? current.reasoning : ""}${event.delta.reasoning}` }; return coreContentBlockFromDelta(event.delta, current); case "data-delta": { const merged = { ...current ?? {} }; merged.data = `${merged.data ?? ""}${event.delta.data}`; if (event.delta.encoding) merged.encoding = event.delta.encoding; return merged; } case "block-delta": return { ...current ?? {}, ...event.delta.fields }; case "tool-call-delta": if (!isToolCallChunk(current)) return current; return { ...current, args: `${current.args ?? ""}${event.delta.args}` }; default: return current; } } function normalizeUsage(usage) { if (!usage) return void 0; return { ...usage, input_tokens: usage.input_tokens ?? 0, output_tokens: usage.output_tokens ?? 0, total_tokens: usage.total_tokens ?? 0 }; } function readRunId(data) { const record = data; return typeof record.run_id === "string" ? record.run_id : void 0; } function readUsage(data) { const usage = data.usage; return usage != null && typeof usage === "object" && !Array.isArray(usage) ? usage : void 0; } function readFinishMetadata(data) { const record = data; const isRecord = (value) => value != null && typeof value === "object" && !Array.isArray(value); const merged = { ...isRecord(record.responseMetadata) ? record.responseMetadata : {}, ...isRecord(record.metadata) ? record.metadata : {} }; return Object.keys(merged).length > 0 ? merged : void 0; } /** * Symbol keys for assembler → StreamingMessage communication. * Module-private: invisible to external consumers, accessible to * {@link StreamingMessageAssembler} within this file. */ const PUSH_TEXT = Symbol("pushText"); const PUSH_REASONING = Symbol("pushReasoning"); const PUSH_EVENT = Symbol("pushEvent"); const UPDATE_CONTEXT = Symbol("updateContext"); const FINISH = Symbol("finish"); const ERROR = Symbol("error"); /** * Live streaming view of a single message lifecycle, matching the * in-process `ChatModelStream` dual-interface pattern. * * - `text` / `reasoning`: iterate for streaming deltas, or await for * the full concatenated string after the message completes. * - `usage`: promise that resolves with token usage on message-finish. * - `blocks`: the assembled content blocks (updated as deltas arrive). * * Created by {@link StreamingMessageAssembler} and yielded by * the `session.messages` lazy getter. */ var StreamingMessage = class { id; namespace; node; metadata; assembled; #events = new require_multi_cursor_buffer.MultiCursorBuffer(); #textChunks = []; #reasoningChunks = []; #textWaiters = []; #reasoningWaiters = []; #textDone = false; #reasoningDone = false; #resolveText; #resolveReasoning; #textPromise; #reasoningPromise; constructor(assembled) { this.id = assembled.id; this.assembled = assembled; this.namespace = assembled.namespace; this.node = assembled.node; this.metadata = assembled.metadata; this.#textPromise = new Promise((r) => { this.#resolveText = r; }); this.#reasoningPromise = new Promise((r) => { this.#resolveReasoning = r; }); } get text() { const chunks = this.#textChunks; const waiters = this.#textWaiters; const getDone = () => this.#textDone; let cursor = 0; return { [Symbol.asyncIterator]() { return { async next() { while (true) { if (cursor < chunks.length) return { done: false, value: chunks[cursor++] }; if (getDone()) return { done: true, value: void 0 }; await new Promise((resolve) => { waiters.push(resolve); }); } } }; }, then: this.#textPromise.then.bind(this.#textPromise), full: { async *[Symbol.asyncIterator]() { let accumulated = ""; for await (const chunk of { [Symbol.asyncIterator]: () => ({ next: async () => { while (true) { if (cursor < chunks.length) return { done: false, value: chunks[cursor++] }; if (getDone()) return { done: true, value: void 0 }; await new Promise((resolve) => { waiters.push(resolve); }); } } }) }) { accumulated += chunk; yield accumulated; } } } }; } get reasoning() { const chunks = this.#reasoningChunks; const waiters = this.#reasoningWaiters; const getDone = () => this.#reasoningDone; let cursor = 0; return { [Symbol.asyncIterator]() { return { async next() { while (true) { if (cursor < chunks.length) return { done: false, value: chunks[cursor++] }; if (getDone()) return { done: true, value: void 0 }; await new Promise((resolve) => { waiters.push(resolve); }); } } }; }, then: this.#reasoningPromise.then.bind(this.#reasoningPromise), full: { async *[Symbol.asyncIterator]() { let accumulated = ""; for await (const chunk of { [Symbol.asyncIterator]: () => ({ next: async () => { while (true) { if (cursor < chunks.length) return { done: false, value: chunks[cursor++] }; if (getDone()) return { done: true, value: void 0 }; await new Promise((resolve) => { waiters.push(resolve); }); } } }) }) { accumulated += chunk; yield accumulated; } } } }; } get usage() { const promise = (async () => { let usage; for await (const snapshot of this.#usageIterator()) usage = snapshot; return usage; })(); return { [Symbol.asyncIterator]: () => this.#usageIterator(), then: promise.then.bind(promise) }; } get toolCalls() { const events = this.#events; const iterator = async function* () { for await (const event of events) if (event.event === "content-block-finish" && event.content.type === "tool_call") yield event.content; }; return { [Symbol.asyncIterator]: iterator, then: async (onfulfilled, onrejected) => { try { const calls = []; for await (const call of iterator()) calls.push(call); return onfulfilled ? onfulfilled(calls) : calls; } catch (err) { if (onrejected) return onrejected(err); throw err; } }, full: { async *[Symbol.asyncIterator]() { const calls = []; for await (const call of iterator()) { calls.push(call); yield [...calls]; } } } }; } get output() { return { then: (onf, onr) => this.#assembleMessage().then(onf, onr) }; } get blocks() { return this.assembled.blocks; } [Symbol.asyncIterator]() { return this.#events[Symbol.asyncIterator](); } then(onfulfilled, onrejected) { return this.#assembleMessage().then(onfulfilled, onrejected); } async *#usageIterator() { for await (const event of this.#events) if (event.event === "message-start" && event.usage) yield normalizeUsage(event.usage); else if (event.event === "message-finish" && event.usage) yield normalizeUsage(event.usage); } async #assembleMessage() { const contentBlocks = []; let id = this.id; let usage = normalizeUsage(this.assembled.usage); let metadata = { ...this.assembled.finishMetadata ?? {} }; let finishReason = this.assembled.finishReason; for await (const event of this.#events) switch (event.event) { case "message-start": id = event.id ?? id; if (event.usage) usage = normalizeUsage(event.usage); break; case "content-block-start": contentBlocks[event.index] = event.content; break; case "content-block-delta": { const current = contentBlocks[event.index]; contentBlocks[event.index] = applyCoreEventDelta(current, event); break; } case "content-block-finish": contentBlocks[event.index] = event.content; break; case "message-finish": { finishReason = event.reason; if (event.usage) usage = normalizeUsage(event.usage); const finishMetadata = readFinishMetadata(event); if (finishMetadata != null) metadata = { ...metadata, ...finishMetadata }; break; } default: break; } return new _langchain_core_messages.AIMessage({ id, content: contentBlocks.filter((block) => block != null), usage_metadata: usage, additional_kwargs: { namespace: this.namespace, ...this.node != null ? { node: this.node } : {}, ...this.assembled.runId != null ? { run_id: this.assembled.runId } : {}, ...this.metadata != null ? { metadata: this.metadata } : {}, ...this.assembled.usage != null ? { usage: this.assembled.usage } : {} }, response_metadata: { ...finishReason != null ? { finish_reason: finishReason } : {}, ...metadata, output_version: "v1" } }); } [PUSH_EVENT](event) { this.#events.push(event); } [UPDATE_CONTEXT](event) { this.node = event.params.node ?? this.node; } [PUSH_TEXT](delta) { this.#textChunks.push(delta); const pending = this.#textWaiters.splice(0, this.#textWaiters.length); for (const waiter of pending) waiter(); } [PUSH_REASONING](delta) { this.#reasoningChunks.push(delta); const pending = this.#reasoningWaiters.splice(0, this.#reasoningWaiters.length); for (const waiter of pending) waiter(); } [FINISH]() { this.#textDone = true; this.#reasoningDone = true; this.#resolveText(this.#textChunks.join("")); this.#resolveReasoning(this.#reasoningChunks.join("")); const textPending = this.#textWaiters.splice(0, this.#textWaiters.length); for (const waiter of textPending) waiter(); const reasoningPending = this.#reasoningWaiters.splice(0, this.#reasoningWaiters.length); for (const waiter of reasoningPending) waiter(); this.#events.close(); } [ERROR]() { this[FINISH](); } }; function toStreamingMessageHandle(message) { return new Proxy(message, { get(target, prop) { if (prop === "then") return void 0; const value = Reflect.get(target, prop, target); return typeof value === "function" ? value.bind(target) : value; }, has(target, prop) { if (prop === "then") return false; return prop in target; } }); } function cloneBlock(block) { return structuredClone(block); } function blockFromDelta(delta, current) { return coreContentBlockFromDelta(delta, current); } function applyContentDelta(target, delta) { if (target.type !== delta.type) return cloneBlock(delta); switch (delta.type) { case "text": return { ...target, ...delta, text: `${"text" in target ? target.text : ""}${delta.text}` }; case "reasoning": return { ...target, ...delta, reasoning: `${"reasoning" in target ? target.reasoning : ""}${delta.reasoning}` }; case "tool_call_chunk": case "server_tool_call_chunk": { const merged = { ...target, ...delta }; if (delta.id == null && "id" in target && target.id != null) merged.id = target.id; if (delta.name == null && "name" in target && target.name != null) merged.name = target.name; merged.args = `${("args" in target ? target.args : "") ?? ""}${delta.args ?? ""}`; return merged; } default: return { ...target, ...delta }; } } function messageKeyFor(event) { const { namespace, node, data } = event.params; const namespaceKey = namespace.join("/"); const runId = readRunId(data); const messageId = data.event === "message-start" ? data.id ?? "" : ""; return `${namespaceKey}::${node ?? ""}::${runId ?? messageId}`; } function toChatModelStreamEvent(event) { return event.params.data; } /** * Incrementally assembles `messages` events into complete message objects. */ var MessageAssembler = class { activeMessages = /* @__PURE__ */ new Map(); activeByNamespaceNode = /* @__PURE__ */ new Map(); blockIndexByProtocolIndexAndType = /* @__PURE__ */ new Map(); /** * Applies a single message event and returns the resulting assembly update. * * Returns `undefined` when the event changes nothing, such as a delta * variant this version doesn't recognise. Those are ignored so that a * newer producer can't break the stream; `content-block-finish` still * carries the complete block. * * @param event - Incoming `messages` event to fold into the assembler state. */ consume(event) { const data = event.params.data; const namespaceNodeKey = `${event.params.namespace.join("/")}::${event.params.node ?? ""}`; const runId = readRunId(data); if (data.event === "message-start") { const key = messageKeyFor(event); this.activeByNamespaceNode.set(namespaceNodeKey, key); const message = { id: data.id, namespace: [...event.params.namespace], node: event.params.node, runId, usage: readUsage(data), metadata: data.metadata, blocks: [] }; this.activeMessages.set(key, message); return { kind: "message-start", key, message, event }; } const runKey = runId != null ? `${namespaceNodeKey}::${runId}` : void 0; const fallbackKey = this.activeByNamespaceNode.get(namespaceNodeKey); const fallbackMessage = fallbackKey != null ? this.activeMessages.get(fallbackKey) : void 0; const activeKey = (runKey != null && this.activeMessages.has(runKey) ? runKey : void 0) ?? (runId == null || fallbackMessage?.runId == null ? fallbackKey : void 0); if (!activeKey) { const syntheticKey = runKey ?? `${namespaceNodeKey}::`; this.activeByNamespaceNode.set(namespaceNodeKey, syntheticKey); const synthetic = { id: data.id, namespace: [...event.params.namespace], node: event.params.node, runId, blocks: [] }; this.activeMessages.set(syntheticKey, synthetic); return this.consume(event); } const message = this.activeMessages.get(activeKey); if (!message) throw new Error(`No active message state found for key ${activeKey}`); if (data.event === "usage") { message.usage = data.usage; return { kind: "usage", key: activeKey, message, event }; } switch (data.event) { case "content-block-start": message.blocks[data.index] = cloneBlock(data.content); this.blockIndexByProtocolIndexAndType.set(blockIndexKey(activeKey, data.index, data.content.type), data.index); return { kind: "content-block-start", key: activeKey, message, index: data.index, block: data.content, event }; case "content-block-delta": { const deltaEvent = data; if (deltaEvent.content == null && deltaEvent.delta == null) throw new Error("Received content-block-delta without content"); const deltaBlock = deltaEvent.content ?? blockFromDelta(deltaEvent.delta, message.blocks[data.index]); if (deltaBlock == null) return void 0; const targetIndex = this.resolveBlockIndex(activeKey, message.blocks, data.index, deltaBlock.type); const current = message.blocks[targetIndex]; message.blocks[targetIndex] = deltaEvent.content != null ? current == null ? cloneBlock(deltaEvent.content) : applyContentDelta(current, deltaEvent.content) : applyCoreEventDelta(current, data); return { kind: "content-block-delta", key: activeKey, message, index: targetIndex, block: deltaBlock, event }; } case "content-block-finish": { const targetIndex = this.resolveFinishBlockIndex(activeKey, data.index, data.content.type); message.blocks[targetIndex] = cloneBlock(data.content); return { kind: "content-block-finish", key: activeKey, message, index: targetIndex, block: data.content, event }; } case "message-finish": message.usage = readUsage(data) ?? message.usage; message.finishMetadata = readFinishMetadata(data); message.finishReason = data.reason; this.clearActiveMessage(activeKey, namespaceNodeKey); this.clearBlockIndexAliases(activeKey); return { kind: "message-finish", key: activeKey, message: structuredClone(message), event }; case "error": message.error = { message: data.message, code: data.code }; this.clearActiveMessage(activeKey, namespaceNodeKey); this.clearBlockIndexAliases(activeKey); return { kind: "message-error", key: activeKey, message: structuredClone(message), event }; } } /** * Drop every message that started but never got `message-finish` or * `error`, e.g. because the run was cancelled mid-stream. * * @param namespace - Only abandon messages at or below this namespace. * @returns The abandoned messages. */ abandonActive(namespace) { const abandoned = []; for (const [key, message] of this.activeMessages) { if (namespace != null && !hasNamespacePrefix(message.namespace, namespace)) continue; abandoned.push(message); this.activeMessages.delete(key); this.clearBlockIndexAliases(key); for (const [nsNode, activeKey] of this.activeByNamespaceNode) if (activeKey === key) this.activeByNamespaceNode.delete(nsNode); } return abandoned; } clearActiveMessage(activeKey, namespaceNodeKey) { this.activeMessages.delete(activeKey); if (this.activeByNamespaceNode.get(namespaceNodeKey) === activeKey) this.activeByNamespaceNode.delete(namespaceNodeKey); } resolveBlockIndex(activeKey, blocks, protocolIndex, blockType) { const current = blocks[protocolIndex]; if (current == null || current.type === blockType || areCompatibleBlockTypes(current.type, blockType)) { this.blockIndexByProtocolIndexAndType.set(blockIndexKey(activeKey, protocolIndex, blockType), protocolIndex); return protocolIndex; } const key = blockIndexKey(activeKey, protocolIndex, blockType); const existing = this.blockIndexByProtocolIndexAndType.get(key); if (existing != null) return existing; const nextIndex = blocks.length; this.blockIndexByProtocolIndexAndType.set(key, nextIndex); return nextIndex; } resolveFinishBlockIndex(activeKey, protocolIndex, blockType) { const key = blockIndexKey(activeKey, protocolIndex, blockType); const existing = this.blockIndexByProtocolIndexAndType.get(key); if (existing != null) return existing; this.blockIndexByProtocolIndexAndType.set(key, protocolIndex); return protocolIndex; } clearBlockIndexAliases(activeKey) { const prefix = `${activeKey}::`; for (const key of this.blockIndexByProtocolIndexAndType.keys()) if (key.startsWith(prefix)) this.blockIndexByProtocolIndexAndType.delete(key); } }; function hasNamespacePrefix(namespace, prefix) { return prefix.every((segment, i) => namespace[i] === segment); } function blockIndexKey(activeKey, protocolIndex, blockType) { return `${activeKey}::${protocolIndex}::${blockType}`; } function areCompatibleBlockTypes(currentType, nextType) { const toolCallTypes = new Set([ "tool_call", "tool_call_chunk", "tool_use", "input_json_delta" ]); const serverToolCallTypes = new Set(["server_tool_call", "server_tool_call_chunk"]); return toolCallTypes.has(currentType) && toolCallTypes.has(nextType) || serverToolCallTypes.has(currentType) && serverToolCallTypes.has(nextType); } /** * Assembles `messages` events into {@link StreamingMessage} instances * with live text/reasoning delta streams, matching the in-process * `ChatModelStream` dual-interface pattern. */ var StreamingMessageAssembler = class { #assembler = new MessageAssembler(); #activeStreaming = /* @__PURE__ */ new Map(); /** * Folds a single event and returns a new {@link StreamingMessage} * when a `message-start` is seen, or `undefined` for continuation * events (deltas, finish, error). */ consume(event) { const update = this.#assembler.consume(event); if (update == null) return void 0; switch (update.kind) { case "message-start": { const streaming = new StreamingMessage(update.message); streaming[UPDATE_CONTEXT](update.event); streaming[PUSH_EVENT](toChatModelStreamEvent(update.event)); this.#activeStreaming.set(update.key, streaming); return streaming; } case "content-block-start": { const streaming = this.#activeStreaming.get(update.key); if (streaming) { streaming[UPDATE_CONTEXT](update.event); streaming[PUSH_EVENT](toChatModelStreamEvent(update.event)); } if (streaming && update.block.type === "text" && "text" in update.block && update.block.text) streaming[PUSH_TEXT](update.block.text); if (streaming && update.block.type === "reasoning" && "reasoning" in update.block && update.block.reasoning) streaming[PUSH_REASONING](update.block.reasoning); return; } case "content-block-delta": { const streaming = this.#activeStreaming.get(update.key); if (!streaming) return void 0; streaming[UPDATE_CONTEXT](update.event); streaming[PUSH_EVENT](toChatModelStreamEvent(update.event)); if (update.block.type === "text" && "text" in update.block) streaming[PUSH_TEXT](update.block.text); if (update.block.type === "reasoning" && "reasoning" in update.block) streaming[PUSH_REASONING](update.block.reasoning); return; } case "content-block-finish": { const streaming = this.#activeStreaming.get(update.key); if (streaming) { streaming[UPDATE_CONTEXT](update.event); streaming[PUSH_EVENT](toChatModelStreamEvent(update.event)); } return; } case "usage": { const streaming = this.#activeStreaming.get(update.key); if (streaming) { streaming[UPDATE_CONTEXT](update.event); streaming[PUSH_EVENT](toChatModelStreamEvent(update.event)); } return; } case "message-finish": { const streaming = this.#activeStreaming.get(update.key); if (streaming) { streaming[UPDATE_CONTEXT](update.event); streaming[PUSH_EVENT](toChatModelStreamEvent(update.event)); streaming[FINISH](); this.#activeStreaming.delete(update.key); } return; } case "message-error": { const streaming = this.#activeStreaming.get(update.key); if (streaming) { streaming[UPDATE_CONTEXT](update.event); streaming[PUSH_EVENT](toChatModelStreamEvent(update.event)); streaming[ERROR](); this.#activeStreaming.delete(update.key); } return; } } } /** * Settle every message that never finished, marking it with an * `aborted` error so awaiting consumers don't hang. * * @param namespace - Only abandon messages at or below this namespace. * @returns The abandoned messages. */ abandonActive(namespace) { this.#assembler.abandonActive(namespace); const abandoned = []; for (const [key, streaming] of this.#activeStreaming) { if (namespace != null && !hasNamespacePrefix(streaming.namespace, namespace)) continue; streaming.assembled.error = { message: "Message stream ended before message-finish", code: "aborted" }; streaming[ERROR](); this.#activeStreaming.delete(key); abandoned.push(streaming); } return abandoned; } }; //#endregion exports.MessageAssembler = MessageAssembler; exports.StreamingMessage = StreamingMessage; exports.StreamingMessageAssembler = StreamingMessageAssembler; exports.toStreamingMessageHandle = toStreamingMessageHandle; //# sourceMappingURL=messages.cjs.map