UNPKG

@langchain/core

Version:
495 lines (494 loc) 14.2 kB
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