@langchain/core
Version:
Core LangChain.js abstractions and schemas
495 lines (494 loc) • 14.2 kB
JavaScript
import { __exportAll } from "../_virtual/_rolldown/runtime.js";
import { AIMessage } from "../messages/ai.js";
//#region src/language_models/stream.ts
/**
* Typed stream classes for chat model streaming.
*
* @module
*/
var stream_exports = /* @__PURE__ */ __exportAll({
ChatModelStream: () => ChatModelStream,
ReasoningContentStream: () => ReasoningContentStream,
TextContentStream: () => TextContentStream,
ToolCallsStream: () => ToolCallsStream,
UsageMetadataStream: () => UsageMetadataStream
});
/**
* A buffer that caches emitted events for replay.
*
* Multiple consumers can independently iterate the same buffer —
* each gets its own cursor. Events are never consumed or removed.
*
* @internal
*/
var ReplayBuffer = class {
events = [];
finished = false;
waiters = [];
error = null;
push(event) {
this.events.push(event);
const toWake = this.waiters.splice(0);
for (const waiter of toWake) waiter();
}
finish() {
this.finished = true;
const toWake = this.waiters.splice(0);
for (const waiter of toWake) waiter();
}
setError(err) {
this.error = err;
this.finished = true;
const toWake = this.waiters.splice(0);
for (const waiter of toWake) waiter();
}
async *iterate() {
if (this.finished) {
if (this.error) throw this.error;
yield* this.events;
return;
}
let cursor = 0;
while (true) {
while (cursor < this.events.length) {
yield this.events[cursor];
cursor++;
}
if (this.finished) {
if (this.error) throw this.error;
return;
}
await new Promise((resolve) => {
if (cursor < this.events.length || this.finished) {
resolve();
return;
}
this.waiters.push(resolve);
});
}
}
};
/**
* Apply a typed delta to an accumulated content block.
*
* - `text-delta` → append text
* - `reasoning-delta` → append reasoning text
* - `data-delta` → append encoded data to `data`
* - `block-delta` → shallow merge fields
*
* @internal
*/
function applyDelta(block, delta) {
switch (delta.type) {
case "text-delta":
if (block.type === "text") return {
...block,
text: (block.text ?? "") + delta.text
};
return block;
case "reasoning-delta":
if (block.type === "thinking") return {
...block,
thinking: (block.thinking ?? "") + delta.reasoning
};
if (block.type === "reasoning") return {
...block,
reasoning: (block.reasoning ?? "") + delta.reasoning
};
return block;
case "data-delta": return {
...block,
data: (block.data ?? "") + delta.data
};
case "block-delta": return {
...block,
...delta.fields
};
default: throw new Error(`Unknown delta type: ${JSON.stringify(delta)}`);
}
}
/**
* Returns the typed delta carried by a content-block delta event.
*
* Stream protocol compliant language models store incremental updates in
* `event.delta`, e.g. `{ type: "text-delta", text: "hello" }`. Some models and
* adapters still emit the older content-shaped form on `event.content`, e.g.
* `{ type: "text", text: "hello" }`, which predates explicit delta event
* variants.
*
* Keep accepting that content-shaped form here so {@link ChatModelStream}
* remains a tolerant consumer while producers migrate to protocol compliant
* typed deltas.
*
* @internal
*/
function getEventDelta(event) {
if (event.event !== "content-block-delta") return void 0;
if ("delta" in event && event.delta) return event.delta;
const content = event.content;
if (content == null || typeof content !== "object") return void 0;
const block = content;
if (block.type === "text" && typeof block.text === "string") return {
type: "text-delta",
text: block.text
};
if (block.type === "reasoning" && typeof block.reasoning === "string") return {
type: "reasoning-delta",
reasoning: block.reasoning
};
if (block.type === "thinking" && typeof block.thinking === "string") return {
type: "reasoning-delta",
reasoning: block.thinking
};
if (typeof block.data === "string") return {
type: "data-delta",
data: block.data,
encoding: "base64"
};
if (typeof block.type === "string") return {
type: "block-delta",
fields: {
...block,
type: block.type
}
};
}
function getReasoningDelta(content) {
if (content == null || typeof content !== "object") return void 0;
const block = content;
if (block.type === "reasoning" && typeof block.reasoning === "string") return block.reasoning;
if (block.type === "thinking" && typeof block.thinking === "string") return block.thinking;
}
function isReasoningContent(content) {
if (content == null || typeof content !== "object") return false;
const type = content.type;
return type === "reasoning" || type === "thinking";
}
/**
* Normalize protocol-compatible partial usage into Core's concrete usage shape.
*
* Some stream sources emit usage snapshots without every aggregate token field.
* Keep the stream event input permissive, then normalize at read time so
* high-level Core consumers always receive a complete {@link UsageMetadata}.
*/
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 parseToolArgs(value) {
if (value != null && typeof value === "object" && !Array.isArray(value)) return value;
if (typeof value !== "string" || value.length === 0) return {};
try {
const parsed = JSON.parse(value);
return parsed != null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
} catch {
return {};
}
}
function standardizeToolBlock(block) {
const record = block;
if (block.type === "tool_call") return block;
if (block.type !== "tool_call_chunk" && block.type !== "tool_use" && block.type !== "input_json_delta") return block;
const name = typeof record.name === "string" ? record.name : void 0;
if (name == null) return block;
const args = record.args ?? record.input;
return {
...record,
type: "tool_call",
name,
args: parseToolArgs(args)
};
}
/**
* Typed stream for text content.
*
* - **Iterate**: yields incremental text deltas.
* - **Await**: resolves to the complete concatenated text.
* - **`.full`**: yields the running accumulated text after each delta.
*/
var TextContentStream = class {
/** @internal */
_buffer;
/** @internal */
constructor(buffer) {
this._buffer = buffer;
}
/** Yields the accumulated text so far after each delta. */
get full() {
const buffer = this._buffer;
return { async *[Symbol.asyncIterator]() {
let accumulated = "";
for await (const event of buffer.iterate()) {
const delta = getEventDelta(event);
if (delta?.type === "text-delta") {
accumulated += delta.text;
yield accumulated;
}
}
} };
}
/** Yields incremental text deltas. */
[Symbol.asyncIterator]() {
const buffer = this._buffer;
async function* gen() {
for await (const event of buffer.iterate()) {
const delta = getEventDelta(event);
if (delta?.type === "text-delta") yield delta.text;
}
}
return gen();
}
then(onfulfilled, onrejected) {
return (async () => {
let text = "";
for await (const delta of this) text += delta;
return text;
})().then(onfulfilled, onrejected);
}
};
/**
* Typed stream for tool calls.
*
* - **Iterate**: yields individual `ToolCall` objects as each completes.
* - **Await**: resolves to the full array.
* - **`.full`**: yields the accumulated array after each new tool call.
*/
var ToolCallsStream = class {
/** @internal */
_buffer;
/** @internal */
constructor(buffer) {
this._buffer = buffer;
}
get full() {
const buffer = this._buffer;
return { async *[Symbol.asyncIterator]() {
const calls = [];
for await (const event of buffer.iterate()) if (event.event === "content-block-finish" && event.content.type === "tool_call") {
calls.push(event.content);
yield [...calls];
}
} };
}
[Symbol.asyncIterator]() {
const buffer = this._buffer;
async function* gen() {
for await (const event of buffer.iterate()) if (event.event === "content-block-finish" && event.content.type === "tool_call") yield event.content;
}
return gen();
}
then(onfulfilled, onrejected) {
return (async () => {
const calls = [];
for await (const call of this) calls.push(call);
return calls;
})().then(onfulfilled, onrejected);
}
};
/**
* Typed stream for reasoning content (chain-of-thought).
* Same interface as {@link TextContentStream} but for reasoning blocks.
*/
var ReasoningContentStream = class {
/** @internal */
_buffer;
/** @internal */
constructor(buffer) {
this._buffer = buffer;
}
get full() {
const buffer = this._buffer;
return { async *[Symbol.asyncIterator]() {
let accumulated = "";
let seenReasoning = false;
for await (const event of buffer.iterate()) if (event.event === "content-block-start") {
if (!isReasoningContent(event.content)) {
if (seenReasoning) return;
continue;
}
seenReasoning = true;
const delta = getReasoningDelta(event.content);
if (delta == null || delta.length === 0) continue;
accumulated += delta;
yield accumulated;
} else if (event.event === "content-block-delta") {
const eventDelta = getEventDelta(event);
if (eventDelta?.type !== "reasoning-delta") continue;
seenReasoning = true;
const delta = eventDelta.reasoning;
if (delta == null || delta.length === 0) continue;
accumulated += delta;
yield accumulated;
} else if (event.event === "content-block-finish" && isReasoningContent(event.content)) return;
else if (event.event === "message-finish") return;
} };
}
[Symbol.asyncIterator]() {
const buffer = this._buffer;
async function* gen() {
let seenReasoning = false;
for await (const event of buffer.iterate()) if (event.event === "content-block-start") {
if (!isReasoningContent(event.content)) {
if (seenReasoning) return;
continue;
}
seenReasoning = true;
const delta = getReasoningDelta(event.content);
if (delta != null && delta.length > 0) yield delta;
} else if (event.event === "content-block-delta") {
const eventDelta = getEventDelta(event);
if (eventDelta?.type !== "reasoning-delta") continue;
seenReasoning = true;
const delta = eventDelta.reasoning;
if (delta != null && delta.length > 0) yield delta;
} else if (event.event === "content-block-finish" && isReasoningContent(event.content)) return;
else if (event.event === "message-finish") return;
}
return gen();
}
then(onfulfilled, onrejected) {
return (async () => {
let text = "";
for await (const delta of this) text += delta;
return text;
})().then(onfulfilled, onrejected);
}
};
/**
* Typed stream for usage metadata.
*/
var UsageMetadataStream = class {
/** @internal */
_buffer;
/** @internal */
constructor(buffer) {
this._buffer = buffer;
}
[Symbol.asyncIterator]() {
const buffer = this._buffer;
async function* gen() {
for await (const event of buffer.iterate()) if (event.event === "usage") {
const usage = normalizeUsage(event.usage);
if (usage) yield usage;
} else if (event.event === "message-start" && event.usage) {
const usage = normalizeUsage(event.usage);
if (usage) yield usage;
} else if (event.event === "message-finish" && event.usage) {
const usage = normalizeUsage(event.usage);
if (usage) yield usage;
}
}
return gen();
}
then(onfulfilled, onrejected) {
return (async () => {
let latest;
for await (const usage of this) latest = usage;
return latest;
})().then(onfulfilled, onrejected);
}
};
/**
* The main stream object returned by chat model streaming.
*
* Implements `AsyncIterable<ChatModelStreamEvent>` for raw event access
* and `PromiseLike<AIMessage>` for simple `await` usage.
*/
var ChatModelStream = class {
/** @internal */
_buffer;
/** @internal */
constructor(source) {
this._buffer = new ReplayBuffer();
this._consume(source);
}
/** @internal */
async _consume(source) {
try {
for await (const event of source) this._buffer.push(event);
this._buffer.finish();
} catch (err) {
this._buffer.setError(err instanceof Error ? err : new Error(String(err)));
}
}
[Symbol.asyncIterator]() {
return this._buffer.iterate();
}
get text() {
return new TextContentStream(this._buffer);
}
get toolCalls() {
return new ToolCallsStream(this._buffer);
}
get reasoning() {
return new ReasoningContentStream(this._buffer);
}
get usage() {
return new UsageMetadataStream(this._buffer);
}
get output() {
return this._assembleMessage();
}
then(onfulfilled, onrejected) {
return this._assembleMessage().then(onfulfilled, onrejected);
}
/** @internal */
async _assembleMessage() {
const contentBlocks = [];
let id;
let usage;
let metadata = {};
let finishReason;
for await (const event of this._buffer.iterate()) 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];
const delta = getEventDelta(event);
if (current) {
if (delta) contentBlocks[event.index] = applyDelta(current, delta);
}
break;
}
case "content-block-finish":
contentBlocks[event.index] = event.content;
break;
case "usage":
usage = normalizeUsage(event.usage);
break;
case "message-finish":
finishReason = event.reason;
if (event.usage) usage = normalizeUsage(event.usage);
if (event.responseMetadata) metadata = {
...metadata,
...event.responseMetadata
};
break;
default: break;
}
const filteredBlocks = contentBlocks.filter((b) => b != null).map(standardizeToolBlock);
return new AIMessage({
id,
content: filteredBlocks,
usage_metadata: usage,
response_metadata: {
...metadata,
...finishReason ? { finish_reason: finishReason } : {},
output_version: "v1"
}
});
}
};
//#endregion
export { ChatModelStream, ReasoningContentStream, TextContentStream, ToolCallsStream, UsageMetadataStream, stream_exports };
//# sourceMappingURL=stream.js.map