@langchain/langgraph-sdk
Version:
Client library for interacting with the LangGraph API
776 lines (775 loc) • 25.2 kB
JavaScript
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