@langchain/langgraph-sdk 1.9.0 → 1.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/crons/index.cjs +8 -2
- package/dist/client/crons/index.cjs.map +1 -1
- package/dist/client/crons/index.d.cts +7 -1
- package/dist/client/crons/index.d.cts.map +1 -1
- package/dist/client/crons/index.d.ts +7 -1
- package/dist/client/crons/index.d.ts.map +1 -1
- package/dist/client/crons/index.js +8 -2
- package/dist/client/crons/index.js.map +1 -1
- package/dist/client/stream/messages.cjs +50 -10
- package/dist/client/stream/messages.cjs.map +1 -1
- package/dist/client/stream/messages.d.cts +4 -0
- package/dist/client/stream/messages.d.cts.map +1 -1
- package/dist/client/stream/messages.d.ts +4 -0
- package/dist/client/stream/messages.d.ts.map +1 -1
- package/dist/client/stream/messages.js +50 -10
- package/dist/client/stream/messages.js.map +1 -1
- package/dist/client/stream/transport/agent-server.cjs.map +1 -1
- package/dist/client/stream/transport/agent-server.d.cts +1 -1
- package/dist/client/stream/transport/agent-server.d.ts +1 -1
- package/dist/client/stream/transport/agent-server.js.map +1 -1
- package/dist/client/stream/transport/http.cjs +3 -3
- package/dist/client/stream/transport/http.cjs.map +1 -1
- package/dist/client/stream/transport/http.d.cts +1 -1
- package/dist/client/stream/transport/http.d.ts +1 -1
- package/dist/client/stream/transport/http.js +3 -3
- package/dist/client/stream/transport/http.js.map +1 -1
- package/dist/client/stream/transport/websocket.cjs +2 -2
- package/dist/client/stream/transport/websocket.cjs.map +1 -1
- package/dist/client/stream/transport/websocket.d.cts +1 -1
- package/dist/client/stream/transport/websocket.d.ts +1 -1
- package/dist/client/stream/transport/websocket.js +2 -2
- package/dist/client/stream/transport/websocket.js.map +1 -1
- package/dist/stream/assembled-to-message.cjs +10 -6
- package/dist/stream/assembled-to-message.cjs.map +1 -1
- package/dist/stream/assembled-to-message.d.cts.map +1 -1
- package/dist/stream/assembled-to-message.d.ts.map +1 -1
- package/dist/stream/assembled-to-message.js +10 -6
- package/dist/stream/assembled-to-message.js.map +1 -1
- package/dist/stream/message-reconciliation.cjs +11 -1
- package/dist/stream/message-reconciliation.cjs.map +1 -1
- package/dist/stream/message-reconciliation.js +11 -1
- package/dist/stream/message-reconciliation.js.map +1 -1
- package/package.json +3 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http.js","names":[],"sources":["../../../../src/client/stream/transport/http.ts"],"sourcesContent":["import { AsyncQueue } from \"./queue.js\";\nimport type {\n Message,\n SubscribeParams,\n Command,\n CommandResponse,\n ErrorResponse,\n} from \"@langchain/protocol\";\n\nimport type {\n HeaderValue,\n ProtocolRequestHook,\n ProtocolSseTransportOptions,\n} from \"./types.js\";\nimport type { TransportAdapter, EventStreamHandle } from \"../transport.js\";\nimport {\n toAbsoluteUrl,\n isRecord,\n mergeHeaders,\n toError,\n isProtocolResponse,\n} from \"./utils.js\";\nimport { BytesLineDecoder, SSEDecoder } from \"./decoder.js\";\nimport { IterableReadableStream } from \"./stream.js\";\n\n/**\n * Transport adapter that speaks the thread-centric protocol over HTTP\n * commands plus SSE event streams. Bound to a specific `threadId`\n * at construction. Each {@link openEventStream} call opens an independent\n * filtered SSE connection via `POST /v2/threads/:thread_id/stream`.\n */\nexport class ProtocolSseTransportAdapter implements TransportAdapter {\n readonly threadId: string;\n\n private readonly queue = new AsyncQueue<Message>();\n\n private readonly fetchImpl: typeof fetch;\n\n private readonly apiUrl: string;\n\n private readonly defaultHeaders: Record<string, HeaderValue>;\n\n private readonly onRequest?: ProtocolRequestHook;\n\n private readonly fetchFactory?: () => typeof fetch | Promise<typeof fetch>;\n\n private readonly commandsUrl: string;\n\n private readonly streamUrl: string;\n\n private readonly sessionAbortController = new AbortController();\n\n private readonly eventStreams = new Set<AbortController>();\n\n private closed = false;\n\n constructor(options: ProtocolSseTransportOptions) {\n this.fetchImpl = options.fetch ?? fetch;\n this.apiUrl = options.apiUrl;\n this.defaultHeaders = options.defaultHeaders ?? {};\n this.onRequest = options.onRequest;\n this.fetchFactory = options.fetchFactory;\n this.threadId = options.threadId;\n this.commandsUrl =\n options.paths?.commands ?? `/v2/threads/${this.threadId}/commands`;\n this.streamUrl =\n options.paths?.stream ?? `/v2/threads/${this.threadId}/stream`;\n }\n\n private async resolveFetch(): Promise<typeof fetch> {\n if (this.fetchFactory) {\n return await this.fetchFactory();\n }\n return this.fetchImpl;\n }\n\n /**\n * HTTP/SSE transports have no handshake — connections are made\n * per-command and per-subscription.\n */\n async open(): Promise<void> {\n // no-op\n }\n\n async send(\n command: Command\n ): Promise<CommandResponse | ErrorResponse | void> {\n const response = await this.request(this.commandsUrl, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify(command),\n signal: this.sessionAbortController.signal,\n });\n\n if (response.status === 202 || response.status === 204) {\n return undefined;\n }\n\n const payload = (await response.json()) as unknown;\n if (!isProtocolResponse(payload)) {\n throw new Error(\"Protocol command did not return a valid response.\");\n }\n return payload;\n }\n\n /**\n * WebSocket-style single event stream.\n * For the SSE transport this returns a dummy iterable; real event\n * delivery happens via {@link openEventStream}.\n */\n events(): AsyncIterable<Message> {\n const queue = this.queue;\n return {\n [Symbol.asyncIterator]: () => ({\n next: async () => await queue.shift(),\n return: async () => {\n queue.close();\n return { done: true, value: undefined };\n },\n }),\n };\n }\n\n openEventStream(params: SubscribeParams): EventStreamHandle {\n if (this.closed) {\n throw new Error(\"Protocol transport is closed.\");\n }\n\n const ac = new AbortController();\n this.eventStreams.add(ac);\n const streamQueue = new AsyncQueue<Message>();\n const streamUrl = this.streamUrl;\n\n let resolveReady!: () => void;\n let rejectReady!: (err: unknown) => void;\n const ready = new Promise<void>((resolve, reject) => {\n resolveReady = resolve;\n rejectReady = reject;\n });\n\n const since = (params as SubscribeParams & { since?: unknown }).since;\n\n const startStream = async () => {\n try {\n const response = await this.request(streamUrl, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n accept: \"text/event-stream\",\n },\n body: JSON.stringify({\n channels: params.channels,\n ...(params.namespaces ? { namespaces: params.namespaces } : {}),\n ...(params.depth != null ? { depth: params.depth } : {}),\n ...(typeof since === \"number\" ? { since } : {}),\n }),\n signal: ac.signal,\n });\n\n resolveReady();\n\n const readable =\n response.body ??\n new ReadableStream<Uint8Array>({\n start(controller) {\n controller.close();\n },\n });\n\n const stream = readable\n .pipeThrough(BytesLineDecoder())\n .pipeThrough(SSEDecoder());\n const iterable = IterableReadableStream.fromReadableStream(stream);\n\n for await (const event of iterable) {\n if (ac.signal.aborted || this.closed) {\n break;\n }\n if (isRecord(event.data)) {\n const msg = event.data as Message & {\n seq?: number;\n method?: string;\n };\n streamQueue.push(msg);\n }\n }\n streamQueue.close();\n } catch (error) {\n rejectReady(error);\n if (ac.signal.aborted || this.closed) {\n streamQueue.close();\n return;\n }\n streamQueue.close(error);\n }\n };\n\n void startStream();\n\n const cleanup = () => {\n this.eventStreams.delete(ac);\n ac.abort();\n streamQueue.close();\n };\n\n return {\n events: {\n [Symbol.asyncIterator]: () => ({\n next: async () => await streamQueue.shift(),\n return: async () => {\n cleanup();\n return { done: true, value: undefined };\n },\n }),\n },\n ready,\n close: cleanup,\n };\n }\n\n async close(): Promise<void> {\n if (this.closed) {\n return;\n }\n this.closed = true;\n this.sessionAbortController.abort();\n for (const ac of this.eventStreams) ac.abort();\n this.eventStreams.clear();\n this.queue.close();\n }\n\n private async request(path: string, init: RequestInit): Promise<Response> {\n const url = toAbsoluteUrl(this.apiUrl, path);\n let requestInit: RequestInit = {\n ...init,\n headers: mergeHeaders(this.defaultHeaders, init.headers),\n };\n\n if (this.onRequest) {\n requestInit = await this.onRequest(url, requestInit);\n }\n\n try {\n const fetchImpl = await this.resolveFetch();\n const response = await fetchImpl(url.toString(), requestInit);\n if (!response.ok) {\n let detail = \"\";\n try {\n const body = await response.text();\n const parsed = JSON.parse(body);\n if (typeof parsed === \"object\" && parsed != null) {\n detail =\n ((parsed as Record<string, unknown>).message as string) ??\n ((parsed as Record<string, unknown>).error as string) ??\n \"\";\n }\n if (!detail) detail = body;\n } catch {\n // body unreadable or not JSON — fall through\n }\n const message = detail\n ? `Protocol request failed: ${response.status} ${response.statusText} — ${detail}`\n : `Protocol request failed: ${response.status} ${response.statusText}`;\n throw new Error(message);\n }\n return response;\n } catch (error) {\n throw toError(error);\n }\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"http.js","names":[],"sources":["../../../../src/client/stream/transport/http.ts"],"sourcesContent":["import { AsyncQueue } from \"./queue.js\";\nimport type {\n Message,\n SubscribeParams,\n Command,\n CommandResponse,\n ErrorResponse,\n} from \"@langchain/protocol\";\n\nimport type {\n HeaderValue,\n ProtocolRequestHook,\n ProtocolSseTransportOptions,\n} from \"./types.js\";\nimport type { TransportAdapter, EventStreamHandle } from \"../transport.js\";\nimport {\n toAbsoluteUrl,\n isRecord,\n mergeHeaders,\n toError,\n isProtocolResponse,\n} from \"./utils.js\";\nimport { BytesLineDecoder, SSEDecoder } from \"./decoder.js\";\nimport { IterableReadableStream } from \"./stream.js\";\n\n/**\n * Transport adapter that speaks the thread-centric protocol over HTTP\n * commands plus SSE event streams. Bound to a specific `threadId`\n * at construction. Each {@link openEventStream} call opens an independent\n * filtered SSE connection via `POST /threads/:thread_id/stream/events`.\n */\nexport class ProtocolSseTransportAdapter implements TransportAdapter {\n readonly threadId: string;\n\n private readonly queue = new AsyncQueue<Message>();\n\n private readonly fetchImpl: typeof fetch;\n\n private readonly apiUrl: string;\n\n private readonly defaultHeaders: Record<string, HeaderValue>;\n\n private readonly onRequest?: ProtocolRequestHook;\n\n private readonly fetchFactory?: () => typeof fetch | Promise<typeof fetch>;\n\n private readonly commandsUrl: string;\n\n private readonly streamUrl: string;\n\n private readonly sessionAbortController = new AbortController();\n\n private readonly eventStreams = new Set<AbortController>();\n\n private closed = false;\n\n constructor(options: ProtocolSseTransportOptions) {\n this.fetchImpl = options.fetch ?? fetch;\n this.apiUrl = options.apiUrl;\n this.defaultHeaders = options.defaultHeaders ?? {};\n this.onRequest = options.onRequest;\n this.fetchFactory = options.fetchFactory;\n this.threadId = options.threadId;\n this.commandsUrl =\n options.paths?.commands ?? `/threads/${this.threadId}/commands`;\n this.streamUrl =\n options.paths?.stream ?? `/threads/${this.threadId}/stream/events`;\n }\n\n private async resolveFetch(): Promise<typeof fetch> {\n if (this.fetchFactory) {\n return await this.fetchFactory();\n }\n return this.fetchImpl;\n }\n\n /**\n * HTTP/SSE transports have no handshake — connections are made\n * per-command and per-subscription.\n */\n async open(): Promise<void> {\n // no-op\n }\n\n async send(\n command: Command\n ): Promise<CommandResponse | ErrorResponse | void> {\n const response = await this.request(this.commandsUrl, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify(command),\n signal: this.sessionAbortController.signal,\n });\n\n if (response.status === 202 || response.status === 204) {\n return undefined;\n }\n\n const payload = (await response.json()) as unknown;\n if (!isProtocolResponse(payload)) {\n throw new Error(\"Protocol command did not return a valid response.\");\n }\n return payload;\n }\n\n /**\n * WebSocket-style single event stream.\n * For the SSE transport this returns a dummy iterable; real event\n * delivery happens via {@link openEventStream}.\n */\n events(): AsyncIterable<Message> {\n const queue = this.queue;\n return {\n [Symbol.asyncIterator]: () => ({\n next: async () => await queue.shift(),\n return: async () => {\n queue.close();\n return { done: true, value: undefined };\n },\n }),\n };\n }\n\n openEventStream(params: SubscribeParams): EventStreamHandle {\n if (this.closed) {\n throw new Error(\"Protocol transport is closed.\");\n }\n\n const ac = new AbortController();\n this.eventStreams.add(ac);\n const streamQueue = new AsyncQueue<Message>();\n const streamUrl = this.streamUrl;\n\n let resolveReady!: () => void;\n let rejectReady!: (err: unknown) => void;\n const ready = new Promise<void>((resolve, reject) => {\n resolveReady = resolve;\n rejectReady = reject;\n });\n\n const since = (params as SubscribeParams & { since?: unknown }).since;\n\n const startStream = async () => {\n try {\n const response = await this.request(streamUrl, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n accept: \"text/event-stream\",\n },\n body: JSON.stringify({\n channels: params.channels,\n ...(params.namespaces ? { namespaces: params.namespaces } : {}),\n ...(params.depth != null ? { depth: params.depth } : {}),\n ...(typeof since === \"number\" ? { since } : {}),\n }),\n signal: ac.signal,\n });\n\n resolveReady();\n\n const readable =\n response.body ??\n new ReadableStream<Uint8Array>({\n start(controller) {\n controller.close();\n },\n });\n\n const stream = readable\n .pipeThrough(BytesLineDecoder())\n .pipeThrough(SSEDecoder());\n const iterable = IterableReadableStream.fromReadableStream(stream);\n\n for await (const event of iterable) {\n if (ac.signal.aborted || this.closed) {\n break;\n }\n if (isRecord(event.data)) {\n const msg = event.data as Message & {\n seq?: number;\n method?: string;\n };\n streamQueue.push(msg);\n }\n }\n streamQueue.close();\n } catch (error) {\n rejectReady(error);\n if (ac.signal.aborted || this.closed) {\n streamQueue.close();\n return;\n }\n streamQueue.close(error);\n }\n };\n\n void startStream();\n\n const cleanup = () => {\n this.eventStreams.delete(ac);\n ac.abort();\n streamQueue.close();\n };\n\n return {\n events: {\n [Symbol.asyncIterator]: () => ({\n next: async () => await streamQueue.shift(),\n return: async () => {\n cleanup();\n return { done: true, value: undefined };\n },\n }),\n },\n ready,\n close: cleanup,\n };\n }\n\n async close(): Promise<void> {\n if (this.closed) {\n return;\n }\n this.closed = true;\n this.sessionAbortController.abort();\n for (const ac of this.eventStreams) ac.abort();\n this.eventStreams.clear();\n this.queue.close();\n }\n\n private async request(path: string, init: RequestInit): Promise<Response> {\n const url = toAbsoluteUrl(this.apiUrl, path);\n let requestInit: RequestInit = {\n ...init,\n headers: mergeHeaders(this.defaultHeaders, init.headers),\n };\n\n if (this.onRequest) {\n requestInit = await this.onRequest(url, requestInit);\n }\n\n try {\n const fetchImpl = await this.resolveFetch();\n const response = await fetchImpl(url.toString(), requestInit);\n if (!response.ok) {\n let detail = \"\";\n try {\n const body = await response.text();\n const parsed = JSON.parse(body);\n if (typeof parsed === \"object\" && parsed != null) {\n detail =\n ((parsed as Record<string, unknown>).message as string) ??\n ((parsed as Record<string, unknown>).error as string) ??\n \"\";\n }\n if (!detail) detail = body;\n } catch {\n // body unreadable or not JSON — fall through\n }\n const message = detail\n ? `Protocol request failed: ${response.status} ${response.statusText} — ${detail}`\n : `Protocol request failed: ${response.status} ${response.statusText}`;\n throw new Error(message);\n }\n return response;\n } catch (error) {\n throw toError(error);\n }\n }\n}\n"],"mappings":"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"}
|
|
@@ -4,7 +4,7 @@ const require_utils = require("./utils.cjs");
|
|
|
4
4
|
/**
|
|
5
5
|
* Transport adapter that speaks the thread-centric protocol over a
|
|
6
6
|
* bidirectional WebSocket. Bound to a specific `threadId` — the socket
|
|
7
|
-
* connects to `ws://.../
|
|
7
|
+
* connects to `ws://.../threads/:thread_id/stream/events`.
|
|
8
8
|
*/
|
|
9
9
|
var ProtocolWebSocketTransportAdapter = class {
|
|
10
10
|
threadId;
|
|
@@ -24,7 +24,7 @@ var ProtocolWebSocketTransportAdapter = class {
|
|
|
24
24
|
this.defaultHeaders = options.defaultHeaders;
|
|
25
25
|
this.onRequest = options.onRequest;
|
|
26
26
|
this.webSocketFactory = options.webSocketFactory ?? ((url) => new WebSocket(url));
|
|
27
|
-
this.streamUrl = options.paths?.stream ?? `/
|
|
27
|
+
this.streamUrl = options.paths?.stream ?? `/threads/${this.threadId}/stream/events`;
|
|
28
28
|
}
|
|
29
29
|
async open() {
|
|
30
30
|
if (this.socket != null) return;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"websocket.cjs","names":["AsyncQueue","toWebSocketUrl","hasHeaders","toError","isRecord"],"sources":["../../../../src/client/stream/transport/websocket.ts"],"sourcesContent":["import { AsyncQueue } from \"./queue.js\";\nimport type {\n Message,\n Command,\n CommandResponse,\n ErrorResponse,\n} from \"@langchain/protocol\";\n\nimport { toWebSocketUrl, isRecord, hasHeaders, toError } from \"./utils.js\";\nimport type {\n HeaderValue,\n ProtocolRequestHook,\n PendingResponse,\n ProtocolWebSocketTransportOptions,\n} from \"./types.js\";\nimport type { TransportAdapter } from \"../transport.js\";\n\n/**\n * Transport adapter that speaks the thread-centric protocol over a\n * bidirectional WebSocket. Bound to a specific `threadId` — the socket\n * connects to `ws://.../
|
|
1
|
+
{"version":3,"file":"websocket.cjs","names":["AsyncQueue","toWebSocketUrl","hasHeaders","toError","isRecord"],"sources":["../../../../src/client/stream/transport/websocket.ts"],"sourcesContent":["import { AsyncQueue } from \"./queue.js\";\nimport type {\n Message,\n Command,\n CommandResponse,\n ErrorResponse,\n} from \"@langchain/protocol\";\n\nimport { toWebSocketUrl, isRecord, hasHeaders, toError } from \"./utils.js\";\nimport type {\n HeaderValue,\n ProtocolRequestHook,\n PendingResponse,\n ProtocolWebSocketTransportOptions,\n} from \"./types.js\";\nimport type { TransportAdapter } from \"../transport.js\";\n\n/**\n * Transport adapter that speaks the thread-centric protocol over a\n * bidirectional WebSocket. Bound to a specific `threadId` — the socket\n * connects to `ws://.../threads/:thread_id/stream/events`.\n */\nexport class ProtocolWebSocketTransportAdapter implements TransportAdapter {\n readonly threadId: string;\n\n private readonly queue = new AsyncQueue<Message>();\n\n private readonly apiUrl: string;\n\n private readonly defaultHeaders?: Record<string, HeaderValue>;\n\n private readonly onRequest?: ProtocolRequestHook;\n\n private readonly webSocketFactory: (url: string) => WebSocket;\n\n private readonly streamUrl: string;\n\n private readonly pending = new Map<number, PendingResponse>();\n\n private socket: WebSocket | null = null;\n\n private closed = false;\n\n private intentionalClose = false;\n\n constructor(options: ProtocolWebSocketTransportOptions) {\n this.apiUrl = options.apiUrl;\n this.threadId = options.threadId;\n this.defaultHeaders = options.defaultHeaders;\n this.onRequest = options.onRequest;\n this.webSocketFactory =\n options.webSocketFactory ?? ((url) => new WebSocket(url));\n this.streamUrl =\n options.paths?.stream ?? `/threads/${this.threadId}/stream/events`;\n }\n\n async open(): Promise<void> {\n if (this.socket != null) return;\n this.assertBrowserSafeTransportConfig();\n\n const wsUrl = toWebSocketUrl(\n new URL(\n this.streamUrl,\n this.apiUrl.endsWith(\"/\") ? this.apiUrl : `${this.apiUrl}/`\n ).toString()\n );\n const socket = this.webSocketFactory(wsUrl);\n this.socket = socket;\n this.closed = false;\n this.intentionalClose = false;\n\n socket.addEventListener(\"message\", this.handleMessage);\n socket.addEventListener(\"close\", this.handleClose);\n socket.addEventListener(\"error\", this.handleSocketError);\n\n await new Promise<void>((resolve, reject) => {\n const onOpen = () => {\n cleanup();\n resolve();\n };\n const onError = () => {\n cleanup();\n reject(new Error(\"Failed to open protocol WebSocket.\"));\n };\n const cleanup = () => {\n socket.removeEventListener(\"open\", onOpen);\n socket.removeEventListener(\"error\", onError);\n };\n socket.addEventListener(\"open\", onOpen, { once: true });\n socket.addEventListener(\"error\", onError, { once: true });\n });\n }\n\n async send(\n command: Command\n ): Promise<CommandResponse | ErrorResponse | void> {\n return await this.sendCommand(command);\n }\n\n events(): AsyncIterable<Message> {\n const queue = this.queue;\n return {\n [Symbol.asyncIterator]: () => ({\n next: async () => await queue.shift(),\n return: async () => {\n queue.close();\n return { done: true, value: undefined };\n },\n }),\n };\n }\n\n async close(): Promise<void> {\n if (this.closed) {\n return;\n }\n\n this.closed = true;\n this.intentionalClose = true;\n\n for (const { reject } of this.pending.values()) {\n reject(new Error(\"Protocol WebSocket connection closed.\"));\n }\n this.pending.clear();\n this.queue.close();\n\n const socket = this.socket;\n this.socket = null;\n if (!socket) {\n return;\n }\n\n await new Promise<void>((resolve) => {\n if (socket.readyState === WebSocket.CLOSED) {\n resolve();\n return;\n }\n\n const onClose = () => {\n socket.removeEventListener(\"close\", onClose);\n resolve();\n };\n\n socket.addEventListener(\"close\", onClose, { once: true });\n if (\n socket.readyState === WebSocket.OPEN ||\n socket.readyState === WebSocket.CONNECTING\n ) {\n socket.close();\n } else {\n resolve();\n }\n });\n }\n\n private assertBrowserSafeTransportConfig(): void {\n if (hasHeaders(this.defaultHeaders) || this.onRequest != null) {\n throw new Error(\n \"Browser WebSocket protocol transport does not support defaultHeaders or onRequest hooks. Supply a custom protocolWebSocketFactory if you need custom WebSocket setup.\"\n );\n }\n }\n\n private async sendCommand(\n command: Command\n ): Promise<CommandResponse | ErrorResponse> {\n const socket = this.socket;\n if (socket == null || socket.readyState !== WebSocket.OPEN) {\n throw new Error(\"Protocol WebSocket is not open.\");\n }\n\n return await new Promise<CommandResponse | ErrorResponse>(\n (resolve, reject) => {\n this.pending.set(command.id, { resolve, reject });\n\n try {\n socket.send(JSON.stringify(command));\n } catch (error) {\n this.pending.delete(command.id);\n reject(toError(error));\n }\n }\n );\n }\n\n private readonly handleMessage = (event: MessageEvent): void => {\n let payload: unknown;\n try {\n payload = JSON.parse(String(event.data));\n } catch {\n return;\n }\n\n if (\n isRecord(payload) &&\n typeof payload.id === \"number\" &&\n (payload.type === \"success\" || payload.type === \"error\")\n ) {\n const pending = this.pending.get(payload.id);\n if (pending) {\n this.pending.delete(payload.id);\n pending.resolve(payload as CommandResponse | ErrorResponse);\n }\n return;\n }\n\n if (isRecord(payload) && payload.type === \"event\") {\n this.queue.push(payload as Message);\n }\n };\n\n private readonly handleClose = (): void => {\n this.socket = null;\n\n if (this.intentionalClose || this.closed) {\n this.queue.close();\n return;\n }\n\n const error = new Error(\"Protocol WebSocket closed unexpectedly.\");\n for (const { reject } of this.pending.values()) {\n reject(error);\n }\n this.pending.clear();\n this.queue.close(error);\n };\n\n private readonly handleSocketError = (): void => {\n if (this.closed || this.intentionalClose) {\n return;\n }\n\n const error = new Error(\"Protocol WebSocket encountered an error.\");\n for (const { reject } of this.pending.values()) {\n reject(error);\n }\n this.pending.clear();\n this.queue.close(error);\n };\n}\n"],"mappings":"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"}
|
|
@@ -6,7 +6,7 @@ import { Command, CommandResponse, ErrorResponse, Message } from "@langchain/pro
|
|
|
6
6
|
/**
|
|
7
7
|
* Transport adapter that speaks the thread-centric protocol over a
|
|
8
8
|
* bidirectional WebSocket. Bound to a specific `threadId` — the socket
|
|
9
|
-
* connects to `ws://.../
|
|
9
|
+
* connects to `ws://.../threads/:thread_id/stream/events`.
|
|
10
10
|
*/
|
|
11
11
|
declare class ProtocolWebSocketTransportAdapter implements TransportAdapter {
|
|
12
12
|
readonly threadId: string;
|
|
@@ -6,7 +6,7 @@ import { Command, CommandResponse, ErrorResponse, Message } from "@langchain/pro
|
|
|
6
6
|
/**
|
|
7
7
|
* Transport adapter that speaks the thread-centric protocol over a
|
|
8
8
|
* bidirectional WebSocket. Bound to a specific `threadId` — the socket
|
|
9
|
-
* connects to `ws://.../
|
|
9
|
+
* connects to `ws://.../threads/:thread_id/stream/events`.
|
|
10
10
|
*/
|
|
11
11
|
declare class ProtocolWebSocketTransportAdapter implements TransportAdapter {
|
|
12
12
|
readonly threadId: string;
|
|
@@ -4,7 +4,7 @@ import { hasHeaders, isRecord, toError, toWebSocketUrl } from "./utils.js";
|
|
|
4
4
|
/**
|
|
5
5
|
* Transport adapter that speaks the thread-centric protocol over a
|
|
6
6
|
* bidirectional WebSocket. Bound to a specific `threadId` — the socket
|
|
7
|
-
* connects to `ws://.../
|
|
7
|
+
* connects to `ws://.../threads/:thread_id/stream/events`.
|
|
8
8
|
*/
|
|
9
9
|
var ProtocolWebSocketTransportAdapter = class {
|
|
10
10
|
threadId;
|
|
@@ -24,7 +24,7 @@ var ProtocolWebSocketTransportAdapter = class {
|
|
|
24
24
|
this.defaultHeaders = options.defaultHeaders;
|
|
25
25
|
this.onRequest = options.onRequest;
|
|
26
26
|
this.webSocketFactory = options.webSocketFactory ?? ((url) => new WebSocket(url));
|
|
27
|
-
this.streamUrl = options.paths?.stream ?? `/
|
|
27
|
+
this.streamUrl = options.paths?.stream ?? `/threads/${this.threadId}/stream/events`;
|
|
28
28
|
}
|
|
29
29
|
async open() {
|
|
30
30
|
if (this.socket != null) return;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"websocket.js","names":[],"sources":["../../../../src/client/stream/transport/websocket.ts"],"sourcesContent":["import { AsyncQueue } from \"./queue.js\";\nimport type {\n Message,\n Command,\n CommandResponse,\n ErrorResponse,\n} from \"@langchain/protocol\";\n\nimport { toWebSocketUrl, isRecord, hasHeaders, toError } from \"./utils.js\";\nimport type {\n HeaderValue,\n ProtocolRequestHook,\n PendingResponse,\n ProtocolWebSocketTransportOptions,\n} from \"./types.js\";\nimport type { TransportAdapter } from \"../transport.js\";\n\n/**\n * Transport adapter that speaks the thread-centric protocol over a\n * bidirectional WebSocket. Bound to a specific `threadId` — the socket\n * connects to `ws://.../
|
|
1
|
+
{"version":3,"file":"websocket.js","names":[],"sources":["../../../../src/client/stream/transport/websocket.ts"],"sourcesContent":["import { AsyncQueue } from \"./queue.js\";\nimport type {\n Message,\n Command,\n CommandResponse,\n ErrorResponse,\n} from \"@langchain/protocol\";\n\nimport { toWebSocketUrl, isRecord, hasHeaders, toError } from \"./utils.js\";\nimport type {\n HeaderValue,\n ProtocolRequestHook,\n PendingResponse,\n ProtocolWebSocketTransportOptions,\n} from \"./types.js\";\nimport type { TransportAdapter } from \"../transport.js\";\n\n/**\n * Transport adapter that speaks the thread-centric protocol over a\n * bidirectional WebSocket. Bound to a specific `threadId` — the socket\n * connects to `ws://.../threads/:thread_id/stream/events`.\n */\nexport class ProtocolWebSocketTransportAdapter implements TransportAdapter {\n readonly threadId: string;\n\n private readonly queue = new AsyncQueue<Message>();\n\n private readonly apiUrl: string;\n\n private readonly defaultHeaders?: Record<string, HeaderValue>;\n\n private readonly onRequest?: ProtocolRequestHook;\n\n private readonly webSocketFactory: (url: string) => WebSocket;\n\n private readonly streamUrl: string;\n\n private readonly pending = new Map<number, PendingResponse>();\n\n private socket: WebSocket | null = null;\n\n private closed = false;\n\n private intentionalClose = false;\n\n constructor(options: ProtocolWebSocketTransportOptions) {\n this.apiUrl = options.apiUrl;\n this.threadId = options.threadId;\n this.defaultHeaders = options.defaultHeaders;\n this.onRequest = options.onRequest;\n this.webSocketFactory =\n options.webSocketFactory ?? ((url) => new WebSocket(url));\n this.streamUrl =\n options.paths?.stream ?? `/threads/${this.threadId}/stream/events`;\n }\n\n async open(): Promise<void> {\n if (this.socket != null) return;\n this.assertBrowserSafeTransportConfig();\n\n const wsUrl = toWebSocketUrl(\n new URL(\n this.streamUrl,\n this.apiUrl.endsWith(\"/\") ? this.apiUrl : `${this.apiUrl}/`\n ).toString()\n );\n const socket = this.webSocketFactory(wsUrl);\n this.socket = socket;\n this.closed = false;\n this.intentionalClose = false;\n\n socket.addEventListener(\"message\", this.handleMessage);\n socket.addEventListener(\"close\", this.handleClose);\n socket.addEventListener(\"error\", this.handleSocketError);\n\n await new Promise<void>((resolve, reject) => {\n const onOpen = () => {\n cleanup();\n resolve();\n };\n const onError = () => {\n cleanup();\n reject(new Error(\"Failed to open protocol WebSocket.\"));\n };\n const cleanup = () => {\n socket.removeEventListener(\"open\", onOpen);\n socket.removeEventListener(\"error\", onError);\n };\n socket.addEventListener(\"open\", onOpen, { once: true });\n socket.addEventListener(\"error\", onError, { once: true });\n });\n }\n\n async send(\n command: Command\n ): Promise<CommandResponse | ErrorResponse | void> {\n return await this.sendCommand(command);\n }\n\n events(): AsyncIterable<Message> {\n const queue = this.queue;\n return {\n [Symbol.asyncIterator]: () => ({\n next: async () => await queue.shift(),\n return: async () => {\n queue.close();\n return { done: true, value: undefined };\n },\n }),\n };\n }\n\n async close(): Promise<void> {\n if (this.closed) {\n return;\n }\n\n this.closed = true;\n this.intentionalClose = true;\n\n for (const { reject } of this.pending.values()) {\n reject(new Error(\"Protocol WebSocket connection closed.\"));\n }\n this.pending.clear();\n this.queue.close();\n\n const socket = this.socket;\n this.socket = null;\n if (!socket) {\n return;\n }\n\n await new Promise<void>((resolve) => {\n if (socket.readyState === WebSocket.CLOSED) {\n resolve();\n return;\n }\n\n const onClose = () => {\n socket.removeEventListener(\"close\", onClose);\n resolve();\n };\n\n socket.addEventListener(\"close\", onClose, { once: true });\n if (\n socket.readyState === WebSocket.OPEN ||\n socket.readyState === WebSocket.CONNECTING\n ) {\n socket.close();\n } else {\n resolve();\n }\n });\n }\n\n private assertBrowserSafeTransportConfig(): void {\n if (hasHeaders(this.defaultHeaders) || this.onRequest != null) {\n throw new Error(\n \"Browser WebSocket protocol transport does not support defaultHeaders or onRequest hooks. Supply a custom protocolWebSocketFactory if you need custom WebSocket setup.\"\n );\n }\n }\n\n private async sendCommand(\n command: Command\n ): Promise<CommandResponse | ErrorResponse> {\n const socket = this.socket;\n if (socket == null || socket.readyState !== WebSocket.OPEN) {\n throw new Error(\"Protocol WebSocket is not open.\");\n }\n\n return await new Promise<CommandResponse | ErrorResponse>(\n (resolve, reject) => {\n this.pending.set(command.id, { resolve, reject });\n\n try {\n socket.send(JSON.stringify(command));\n } catch (error) {\n this.pending.delete(command.id);\n reject(toError(error));\n }\n }\n );\n }\n\n private readonly handleMessage = (event: MessageEvent): void => {\n let payload: unknown;\n try {\n payload = JSON.parse(String(event.data));\n } catch {\n return;\n }\n\n if (\n isRecord(payload) &&\n typeof payload.id === \"number\" &&\n (payload.type === \"success\" || payload.type === \"error\")\n ) {\n const pending = this.pending.get(payload.id);\n if (pending) {\n this.pending.delete(payload.id);\n pending.resolve(payload as CommandResponse | ErrorResponse);\n }\n return;\n }\n\n if (isRecord(payload) && payload.type === \"event\") {\n this.queue.push(payload as Message);\n }\n };\n\n private readonly handleClose = (): void => {\n this.socket = null;\n\n if (this.intentionalClose || this.closed) {\n this.queue.close();\n return;\n }\n\n const error = new Error(\"Protocol WebSocket closed unexpectedly.\");\n for (const { reject } of this.pending.values()) {\n reject(error);\n }\n this.pending.clear();\n this.queue.close(error);\n };\n\n private readonly handleSocketError = (): void => {\n if (this.closed || this.intentionalClose) {\n return;\n }\n\n const error = new Error(\"Protocol WebSocket encountered an error.\");\n for (const { reject } of this.pending.values()) {\n reject(error);\n }\n this.pending.clear();\n this.queue.close(error);\n };\n}\n"],"mappings":"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"}
|
|
@@ -27,33 +27,34 @@ let _langchain_core_messages = require("@langchain/core/messages");
|
|
|
27
27
|
*/
|
|
28
28
|
function assembledToBaseMessage(input) {
|
|
29
29
|
const { id, role, blocks, toolCallId, usage } = input;
|
|
30
|
-
const
|
|
30
|
+
const textContent = extractContentString(blocks);
|
|
31
31
|
const toolCalls = extractToolCalls(blocks);
|
|
32
32
|
const toolCallChunks = extractToolCallChunks(blocks);
|
|
33
33
|
const additionalKwargs = usage != null ? { usage } : void 0;
|
|
34
34
|
switch (role) {
|
|
35
35
|
case "human": return new _langchain_core_messages.HumanMessage({
|
|
36
36
|
...id != null ? { id } : {},
|
|
37
|
-
content,
|
|
37
|
+
content: textContent,
|
|
38
38
|
...additionalKwargs != null ? { additional_kwargs: additionalKwargs } : {}
|
|
39
39
|
});
|
|
40
40
|
case "system": return new _langchain_core_messages.SystemMessage({
|
|
41
41
|
...id != null ? { id } : {},
|
|
42
|
-
content,
|
|
42
|
+
content: textContent,
|
|
43
43
|
...additionalKwargs != null ? { additional_kwargs: additionalKwargs } : {}
|
|
44
44
|
});
|
|
45
45
|
case "tool": return new _langchain_core_messages.ToolMessage({
|
|
46
46
|
...id != null ? { id } : {},
|
|
47
|
-
content,
|
|
47
|
+
content: textContent,
|
|
48
48
|
tool_call_id: toolCallId ?? ""
|
|
49
49
|
});
|
|
50
50
|
default: {
|
|
51
51
|
const payload = {
|
|
52
52
|
...id != null ? { id } : {},
|
|
53
|
-
content,
|
|
53
|
+
content: cloneContentBlocks(blocks),
|
|
54
54
|
...toolCalls.length > 0 ? { tool_calls: toolCalls } : {},
|
|
55
55
|
...toolCallChunks.length > 0 ? { tool_call_chunks: toolCallChunks } : {},
|
|
56
|
-
...additionalKwargs != null ? { additional_kwargs: additionalKwargs } : {}
|
|
56
|
+
...additionalKwargs != null ? { additional_kwargs: additionalKwargs } : {},
|
|
57
|
+
response_metadata: { output_version: "v1" }
|
|
57
58
|
};
|
|
58
59
|
return toolCallChunks.length > 0 ? new _langchain_core_messages.AIMessageChunk(payload) : new _langchain_core_messages.AIMessage(payload);
|
|
59
60
|
}
|
|
@@ -77,6 +78,9 @@ function extractContentString(blocks) {
|
|
|
77
78
|
for (const block of blocks) if (block.type === "text" && typeof block.text === "string") out += block.text;
|
|
78
79
|
return out;
|
|
79
80
|
}
|
|
81
|
+
function cloneContentBlocks(blocks) {
|
|
82
|
+
return blocks.map((block) => structuredClone(block));
|
|
83
|
+
}
|
|
80
84
|
function extractToolCalls(blocks) {
|
|
81
85
|
const out = [];
|
|
82
86
|
for (const block of blocks) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"assembled-to-message.cjs","names":["HumanMessage","SystemMessage","ToolMessage","AIMessageChunk","AIMessage"],"sources":["../../src/stream/assembled-to-message.ts"],"sourcesContent":["/**\n * Convert the protocol-native {@link AssembledMessage} (a namespace +\n * content-block bag) into a class instance from\n * `@langchain/core/messages`.\n *\n * The v2 messages channel carries `role` on the `message-start`\n * event, but `MessageAssembler` drops it by the time it produces an\n * `AssembledMessage`. The selector-side caller therefore captures the\n * role separately (via the assembler's per-update hook, see\n * `projections/messages.ts`) and passes it in.\n *\n * The conversion is intentionally lossless in the limit: when the\n * message finishes we emit the same BaseMessage shape that a\n * `values.messages` entry would round-trip to via\n * {@link tryCoerceMessageLikeToMessage}. Mid-stream, partial blocks\n * (e.g. tool_call_chunk) are folded into the same shape so the UI\n * can render an incrementally-completing message.\n */\nimport {\n AIMessage,\n AIMessageChunk,\n HumanMessage,\n SystemMessage,\n ToolMessage,\n type BaseMessage,\n} from \"@langchain/core/messages\";\nimport type { ContentBlock, MessageRole, UsageInfo } from \"@langchain/protocol\";\nimport type { AssembledMessage } from \"../client/stream/messages.js\";\n\nexport type ExtendedMessageRole = MessageRole | \"tool\";\n\nexport interface AssembledToMessageInput {\n /** Stable message id (from `MessageStartData.id`). */\n id?: string;\n /** Author role captured from the `message-start` event. */\n role: ExtendedMessageRole;\n /** Content blocks assembled so far. */\n blocks: ContentBlock[];\n /** Tool-call id a `role: \"tool\"` message is responding to, if any. */\n toolCallId?: string;\n /** Final-token usage (populated on `message-finish`). */\n usage?: UsageInfo;\n}\n\n/**\n * Produce a `BaseMessage` class instance from an in-progress or\n * finished assembled message. Safe to call repeatedly across deltas —\n * each call returns a new instance whose content reflects the\n * currently-observed blocks.\n */\nexport function assembledToBaseMessage(\n input: AssembledToMessageInput\n): BaseMessage {\n const { id, role, blocks, toolCallId, usage } = input;\n const
|
|
1
|
+
{"version":3,"file":"assembled-to-message.cjs","names":["HumanMessage","SystemMessage","ToolMessage","AIMessageChunk","AIMessage"],"sources":["../../src/stream/assembled-to-message.ts"],"sourcesContent":["/**\n * Convert the protocol-native {@link AssembledMessage} (a namespace +\n * content-block bag) into a class instance from\n * `@langchain/core/messages`.\n *\n * The v2 messages channel carries `role` on the `message-start`\n * event, but `MessageAssembler` drops it by the time it produces an\n * `AssembledMessage`. The selector-side caller therefore captures the\n * role separately (via the assembler's per-update hook, see\n * `projections/messages.ts`) and passes it in.\n *\n * The conversion is intentionally lossless in the limit: when the\n * message finishes we emit the same BaseMessage shape that a\n * `values.messages` entry would round-trip to via\n * {@link tryCoerceMessageLikeToMessage}. Mid-stream, partial blocks\n * (e.g. tool_call_chunk) are folded into the same shape so the UI\n * can render an incrementally-completing message.\n */\nimport {\n AIMessage,\n AIMessageChunk,\n HumanMessage,\n SystemMessage,\n ToolMessage,\n type BaseMessage,\n} from \"@langchain/core/messages\";\nimport type { ContentBlock, MessageRole, UsageInfo } from \"@langchain/protocol\";\nimport type { AssembledMessage } from \"../client/stream/messages.js\";\n\nexport type ExtendedMessageRole = MessageRole | \"tool\";\n\nexport interface AssembledToMessageInput {\n /** Stable message id (from `MessageStartData.id`). */\n id?: string;\n /** Author role captured from the `message-start` event. */\n role: ExtendedMessageRole;\n /** Content blocks assembled so far. */\n blocks: ContentBlock[];\n /** Tool-call id a `role: \"tool\"` message is responding to, if any. */\n toolCallId?: string;\n /** Final-token usage (populated on `message-finish`). */\n usage?: UsageInfo;\n}\n\n/**\n * Produce a `BaseMessage` class instance from an in-progress or\n * finished assembled message. Safe to call repeatedly across deltas —\n * each call returns a new instance whose content reflects the\n * currently-observed blocks.\n */\nexport function assembledToBaseMessage(\n input: AssembledToMessageInput\n): BaseMessage {\n const { id, role, blocks, toolCallId, usage } = input;\n const textContent = extractContentString(blocks);\n const toolCalls = extractToolCalls(blocks);\n const toolCallChunks = extractToolCallChunks(blocks);\n const additionalKwargs =\n usage != null ? ({ usage } as Record<string, unknown>) : undefined;\n\n switch (role) {\n case \"human\":\n return new HumanMessage({\n ...(id != null ? { id } : {}),\n content: textContent,\n ...(additionalKwargs != null\n ? { additional_kwargs: additionalKwargs }\n : {}),\n });\n case \"system\":\n return new SystemMessage({\n ...(id != null ? { id } : {}),\n content: textContent,\n ...(additionalKwargs != null\n ? { additional_kwargs: additionalKwargs }\n : {}),\n });\n case \"tool\":\n return new ToolMessage({\n ...(id != null ? { id } : {}),\n content: textContent,\n tool_call_id: toolCallId ?? \"\",\n });\n case \"ai\":\n default: {\n // Use `AIMessageChunk` whenever tool_call_chunks are present —\n // the concrete `AIMessage` class silently DROPS the\n // `tool_call_chunks` field, which would leave mid-stream tool\n // calls invisible to the UI (it sees an AI message with empty\n // content and no tool calls, rendering a blank bubble until\n // `content-block-finish` finally promotes the chunks to\n // finalized `tool_calls`). The chunk class is assignment-compatible\n // with `BaseMessage` and round-trips through the merge logic.\n const payload = {\n ...(id != null ? { id } : {}),\n content: cloneContentBlocks(blocks),\n ...(toolCalls.length > 0 ? { tool_calls: toolCalls } : {}),\n ...(toolCallChunks.length > 0\n ? { tool_call_chunks: toolCallChunks }\n : {}),\n ...(additionalKwargs != null\n ? { additional_kwargs: additionalKwargs }\n : {}),\n response_metadata: { output_version: \"v1\" as const },\n };\n return toolCallChunks.length > 0\n ? new AIMessageChunk(\n payload as ConstructorParameters<typeof AIMessageChunk>[0]\n )\n : new AIMessage(payload as ConstructorParameters<typeof AIMessage>[0]);\n }\n }\n}\n\n/**\n * Convenience: given the raw assembled message + the role captured\n * from `message-start`, produce a `BaseMessage` with the same id.\n */\nexport function assembledMessageToBaseMessage(\n assembled: AssembledMessage,\n role: ExtendedMessageRole,\n extras: { toolCallId?: string } = {}\n): BaseMessage {\n return assembledToBaseMessage({\n id: assembled.id,\n role,\n blocks: assembled.blocks,\n toolCallId: extras.toolCallId,\n usage: assembled.usage,\n });\n}\n\n// ---------- helpers ----------\n\nfunction extractContentString(blocks: ContentBlock[]): string {\n let out = \"\";\n for (const block of blocks) {\n if (\n block.type === \"text\" &&\n typeof (block as { text?: unknown }).text === \"string\"\n ) {\n out += (block as { text: string }).text;\n }\n }\n return out;\n}\n\nfunction cloneContentBlocks(blocks: ContentBlock[]): ContentBlock[] {\n return blocks.map((block) => structuredClone(block));\n}\n\ninterface LooseToolCall {\n id: string;\n name: string;\n args: Record<string, unknown>;\n type: \"tool_call\";\n}\n\nfunction extractToolCalls(blocks: ContentBlock[]): LooseToolCall[] {\n const out: LooseToolCall[] = [];\n for (const block of blocks) {\n if (block.type !== \"tool_call\" && block.type !== \"tool_use\") continue;\n const tc = block as ToolCallLikeBlock;\n out.push({\n id: tc.id ?? \"\",\n name: tc.name ?? \"\",\n args: normalizeToolCallArgs(tc.args ?? tc.input),\n type: \"tool_call\",\n });\n }\n return out;\n}\n\ninterface ToolCallLikeBlock {\n id?: string;\n name?: string;\n args?: unknown;\n input?: unknown;\n}\n\ninterface LooseToolCallChunk {\n id?: string;\n name?: string;\n args?: string;\n index?: number;\n type: \"tool_call_chunk\";\n}\n\nfunction extractToolCallChunks(blocks: ContentBlock[]): LooseToolCallChunk[] {\n const out: LooseToolCallChunk[] = [];\n for (const block of blocks) {\n if (block.type !== \"tool_call_chunk\") continue;\n const tc = block as {\n id?: string;\n name?: string;\n args?: string;\n index?: number;\n };\n out.push({\n id: tc.id,\n name: tc.name,\n args: tc.args,\n index: tc.index,\n type: \"tool_call_chunk\",\n });\n }\n return out;\n}\n\nfunction normalizeToolCallArgs(value: unknown): Record<string, unknown> {\n if (value != null && typeof value === \"object\" && !Array.isArray(value)) {\n return value as Record<string, unknown>;\n }\n if (typeof value === \"string\" && value.length > 0) {\n try {\n const parsed = JSON.parse(value);\n if (\n parsed != null &&\n typeof parsed === \"object\" &&\n !Array.isArray(parsed)\n ) {\n return parsed as Record<string, unknown>;\n }\n } catch {\n // Partial streaming input is represented via tool_call_chunks.\n }\n }\n return {};\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"assembled-to-message.d.cts","names":[],"sources":["../../src/stream/assembled-to-message.ts"],"mappings":";;;;;KA6BY,mBAAA,GAAsB,WAAA;AAAA,UAEjB,uBAAA;EAQf;EANA,EAAA;EAQQ;EANR,IAAA,EAAM,mBAAA;EAMW;EAJjB,MAAA,EAAQ,YAAA;EAa4B;EAXpC,UAAA;EAaY;EAXZ,KAAA,GAAQ,SAAA;AAAA;;;;
|
|
1
|
+
{"version":3,"file":"assembled-to-message.d.cts","names":[],"sources":["../../src/stream/assembled-to-message.ts"],"mappings":";;;;;KA6BY,mBAAA,GAAsB,WAAA;AAAA,UAEjB,uBAAA;EAQf;EANA,EAAA;EAQQ;EANR,IAAA,EAAM,mBAAA;EAMW;EAJjB,MAAA,EAAQ,YAAA;EAa4B;EAXpC,UAAA;EAaY;EAXZ,KAAA,GAAQ,SAAA;AAAA;;;;AA6EV;;;iBApEgB,sBAAA,CACd,KAAA,EAAO,uBAAA,GACN,WAAA;;;;;iBAkEa,6BAAA,CACd,SAAA,EAAW,gBAAA,EACX,IAAA,EAAM,mBAAA,EACN,MAAA;EAAU,UAAA;AAAA,IACT,WAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"assembled-to-message.d.ts","names":[],"sources":["../../src/stream/assembled-to-message.ts"],"mappings":";;;;;KA6BY,mBAAA,GAAsB,WAAA;AAAA,UAEjB,uBAAA;EAQf;EANA,EAAA;EAQQ;EANR,IAAA,EAAM,mBAAA;EAMW;EAJjB,MAAA,EAAQ,cAAA;EAa4B;EAXpC,UAAA;EAaY;EAXZ,KAAA,GAAQ,SAAA;AAAA;;;;
|
|
1
|
+
{"version":3,"file":"assembled-to-message.d.ts","names":[],"sources":["../../src/stream/assembled-to-message.ts"],"mappings":";;;;;KA6BY,mBAAA,GAAsB,WAAA;AAAA,UAEjB,uBAAA;EAQf;EANA,EAAA;EAQQ;EANR,IAAA,EAAM,mBAAA;EAMW;EAJjB,MAAA,EAAQ,cAAA;EAa4B;EAXpC,UAAA;EAaY;EAXZ,KAAA,GAAQ,SAAA;AAAA;;;;AA6EV;;;iBApEgB,sBAAA,CACd,KAAA,EAAO,uBAAA,GACN,WAAA;;;;;iBAkEa,6BAAA,CACd,SAAA,EAAW,gBAAA,EACX,IAAA,EAAM,mBAAA,EACN,MAAA;EAAU,UAAA;AAAA,IACT,WAAA"}
|
|
@@ -26,33 +26,34 @@ import { AIMessage, AIMessageChunk, HumanMessage, SystemMessage, ToolMessage } f
|
|
|
26
26
|
*/
|
|
27
27
|
function assembledToBaseMessage(input) {
|
|
28
28
|
const { id, role, blocks, toolCallId, usage } = input;
|
|
29
|
-
const
|
|
29
|
+
const textContent = extractContentString(blocks);
|
|
30
30
|
const toolCalls = extractToolCalls(blocks);
|
|
31
31
|
const toolCallChunks = extractToolCallChunks(blocks);
|
|
32
32
|
const additionalKwargs = usage != null ? { usage } : void 0;
|
|
33
33
|
switch (role) {
|
|
34
34
|
case "human": return new HumanMessage({
|
|
35
35
|
...id != null ? { id } : {},
|
|
36
|
-
content,
|
|
36
|
+
content: textContent,
|
|
37
37
|
...additionalKwargs != null ? { additional_kwargs: additionalKwargs } : {}
|
|
38
38
|
});
|
|
39
39
|
case "system": return new SystemMessage({
|
|
40
40
|
...id != null ? { id } : {},
|
|
41
|
-
content,
|
|
41
|
+
content: textContent,
|
|
42
42
|
...additionalKwargs != null ? { additional_kwargs: additionalKwargs } : {}
|
|
43
43
|
});
|
|
44
44
|
case "tool": return new ToolMessage({
|
|
45
45
|
...id != null ? { id } : {},
|
|
46
|
-
content,
|
|
46
|
+
content: textContent,
|
|
47
47
|
tool_call_id: toolCallId ?? ""
|
|
48
48
|
});
|
|
49
49
|
default: {
|
|
50
50
|
const payload = {
|
|
51
51
|
...id != null ? { id } : {},
|
|
52
|
-
content,
|
|
52
|
+
content: cloneContentBlocks(blocks),
|
|
53
53
|
...toolCalls.length > 0 ? { tool_calls: toolCalls } : {},
|
|
54
54
|
...toolCallChunks.length > 0 ? { tool_call_chunks: toolCallChunks } : {},
|
|
55
|
-
...additionalKwargs != null ? { additional_kwargs: additionalKwargs } : {}
|
|
55
|
+
...additionalKwargs != null ? { additional_kwargs: additionalKwargs } : {},
|
|
56
|
+
response_metadata: { output_version: "v1" }
|
|
56
57
|
};
|
|
57
58
|
return toolCallChunks.length > 0 ? new AIMessageChunk(payload) : new AIMessage(payload);
|
|
58
59
|
}
|
|
@@ -76,6 +77,9 @@ function extractContentString(blocks) {
|
|
|
76
77
|
for (const block of blocks) if (block.type === "text" && typeof block.text === "string") out += block.text;
|
|
77
78
|
return out;
|
|
78
79
|
}
|
|
80
|
+
function cloneContentBlocks(blocks) {
|
|
81
|
+
return blocks.map((block) => structuredClone(block));
|
|
82
|
+
}
|
|
79
83
|
function extractToolCalls(blocks) {
|
|
80
84
|
const out = [];
|
|
81
85
|
for (const block of blocks) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"assembled-to-message.js","names":[],"sources":["../../src/stream/assembled-to-message.ts"],"sourcesContent":["/**\n * Convert the protocol-native {@link AssembledMessage} (a namespace +\n * content-block bag) into a class instance from\n * `@langchain/core/messages`.\n *\n * The v2 messages channel carries `role` on the `message-start`\n * event, but `MessageAssembler` drops it by the time it produces an\n * `AssembledMessage`. The selector-side caller therefore captures the\n * role separately (via the assembler's per-update hook, see\n * `projections/messages.ts`) and passes it in.\n *\n * The conversion is intentionally lossless in the limit: when the\n * message finishes we emit the same BaseMessage shape that a\n * `values.messages` entry would round-trip to via\n * {@link tryCoerceMessageLikeToMessage}. Mid-stream, partial blocks\n * (e.g. tool_call_chunk) are folded into the same shape so the UI\n * can render an incrementally-completing message.\n */\nimport {\n AIMessage,\n AIMessageChunk,\n HumanMessage,\n SystemMessage,\n ToolMessage,\n type BaseMessage,\n} from \"@langchain/core/messages\";\nimport type { ContentBlock, MessageRole, UsageInfo } from \"@langchain/protocol\";\nimport type { AssembledMessage } from \"../client/stream/messages.js\";\n\nexport type ExtendedMessageRole = MessageRole | \"tool\";\n\nexport interface AssembledToMessageInput {\n /** Stable message id (from `MessageStartData.id`). */\n id?: string;\n /** Author role captured from the `message-start` event. */\n role: ExtendedMessageRole;\n /** Content blocks assembled so far. */\n blocks: ContentBlock[];\n /** Tool-call id a `role: \"tool\"` message is responding to, if any. */\n toolCallId?: string;\n /** Final-token usage (populated on `message-finish`). */\n usage?: UsageInfo;\n}\n\n/**\n * Produce a `BaseMessage` class instance from an in-progress or\n * finished assembled message. Safe to call repeatedly across deltas —\n * each call returns a new instance whose content reflects the\n * currently-observed blocks.\n */\nexport function assembledToBaseMessage(\n input: AssembledToMessageInput\n): BaseMessage {\n const { id, role, blocks, toolCallId, usage } = input;\n const
|
|
1
|
+
{"version":3,"file":"assembled-to-message.js","names":[],"sources":["../../src/stream/assembled-to-message.ts"],"sourcesContent":["/**\n * Convert the protocol-native {@link AssembledMessage} (a namespace +\n * content-block bag) into a class instance from\n * `@langchain/core/messages`.\n *\n * The v2 messages channel carries `role` on the `message-start`\n * event, but `MessageAssembler` drops it by the time it produces an\n * `AssembledMessage`. The selector-side caller therefore captures the\n * role separately (via the assembler's per-update hook, see\n * `projections/messages.ts`) and passes it in.\n *\n * The conversion is intentionally lossless in the limit: when the\n * message finishes we emit the same BaseMessage shape that a\n * `values.messages` entry would round-trip to via\n * {@link tryCoerceMessageLikeToMessage}. Mid-stream, partial blocks\n * (e.g. tool_call_chunk) are folded into the same shape so the UI\n * can render an incrementally-completing message.\n */\nimport {\n AIMessage,\n AIMessageChunk,\n HumanMessage,\n SystemMessage,\n ToolMessage,\n type BaseMessage,\n} from \"@langchain/core/messages\";\nimport type { ContentBlock, MessageRole, UsageInfo } from \"@langchain/protocol\";\nimport type { AssembledMessage } from \"../client/stream/messages.js\";\n\nexport type ExtendedMessageRole = MessageRole | \"tool\";\n\nexport interface AssembledToMessageInput {\n /** Stable message id (from `MessageStartData.id`). */\n id?: string;\n /** Author role captured from the `message-start` event. */\n role: ExtendedMessageRole;\n /** Content blocks assembled so far. */\n blocks: ContentBlock[];\n /** Tool-call id a `role: \"tool\"` message is responding to, if any. */\n toolCallId?: string;\n /** Final-token usage (populated on `message-finish`). */\n usage?: UsageInfo;\n}\n\n/**\n * Produce a `BaseMessage` class instance from an in-progress or\n * finished assembled message. Safe to call repeatedly across deltas —\n * each call returns a new instance whose content reflects the\n * currently-observed blocks.\n */\nexport function assembledToBaseMessage(\n input: AssembledToMessageInput\n): BaseMessage {\n const { id, role, blocks, toolCallId, usage } = input;\n const textContent = extractContentString(blocks);\n const toolCalls = extractToolCalls(blocks);\n const toolCallChunks = extractToolCallChunks(blocks);\n const additionalKwargs =\n usage != null ? ({ usage } as Record<string, unknown>) : undefined;\n\n switch (role) {\n case \"human\":\n return new HumanMessage({\n ...(id != null ? { id } : {}),\n content: textContent,\n ...(additionalKwargs != null\n ? { additional_kwargs: additionalKwargs }\n : {}),\n });\n case \"system\":\n return new SystemMessage({\n ...(id != null ? { id } : {}),\n content: textContent,\n ...(additionalKwargs != null\n ? { additional_kwargs: additionalKwargs }\n : {}),\n });\n case \"tool\":\n return new ToolMessage({\n ...(id != null ? { id } : {}),\n content: textContent,\n tool_call_id: toolCallId ?? \"\",\n });\n case \"ai\":\n default: {\n // Use `AIMessageChunk` whenever tool_call_chunks are present —\n // the concrete `AIMessage` class silently DROPS the\n // `tool_call_chunks` field, which would leave mid-stream tool\n // calls invisible to the UI (it sees an AI message with empty\n // content and no tool calls, rendering a blank bubble until\n // `content-block-finish` finally promotes the chunks to\n // finalized `tool_calls`). The chunk class is assignment-compatible\n // with `BaseMessage` and round-trips through the merge logic.\n const payload = {\n ...(id != null ? { id } : {}),\n content: cloneContentBlocks(blocks),\n ...(toolCalls.length > 0 ? { tool_calls: toolCalls } : {}),\n ...(toolCallChunks.length > 0\n ? { tool_call_chunks: toolCallChunks }\n : {}),\n ...(additionalKwargs != null\n ? { additional_kwargs: additionalKwargs }\n : {}),\n response_metadata: { output_version: \"v1\" as const },\n };\n return toolCallChunks.length > 0\n ? new AIMessageChunk(\n payload as ConstructorParameters<typeof AIMessageChunk>[0]\n )\n : new AIMessage(payload as ConstructorParameters<typeof AIMessage>[0]);\n }\n }\n}\n\n/**\n * Convenience: given the raw assembled message + the role captured\n * from `message-start`, produce a `BaseMessage` with the same id.\n */\nexport function assembledMessageToBaseMessage(\n assembled: AssembledMessage,\n role: ExtendedMessageRole,\n extras: { toolCallId?: string } = {}\n): BaseMessage {\n return assembledToBaseMessage({\n id: assembled.id,\n role,\n blocks: assembled.blocks,\n toolCallId: extras.toolCallId,\n usage: assembled.usage,\n });\n}\n\n// ---------- helpers ----------\n\nfunction extractContentString(blocks: ContentBlock[]): string {\n let out = \"\";\n for (const block of blocks) {\n if (\n block.type === \"text\" &&\n typeof (block as { text?: unknown }).text === \"string\"\n ) {\n out += (block as { text: string }).text;\n }\n }\n return out;\n}\n\nfunction cloneContentBlocks(blocks: ContentBlock[]): ContentBlock[] {\n return blocks.map((block) => structuredClone(block));\n}\n\ninterface LooseToolCall {\n id: string;\n name: string;\n args: Record<string, unknown>;\n type: \"tool_call\";\n}\n\nfunction extractToolCalls(blocks: ContentBlock[]): LooseToolCall[] {\n const out: LooseToolCall[] = [];\n for (const block of blocks) {\n if (block.type !== \"tool_call\" && block.type !== \"tool_use\") continue;\n const tc = block as ToolCallLikeBlock;\n out.push({\n id: tc.id ?? \"\",\n name: tc.name ?? \"\",\n args: normalizeToolCallArgs(tc.args ?? tc.input),\n type: \"tool_call\",\n });\n }\n return out;\n}\n\ninterface ToolCallLikeBlock {\n id?: string;\n name?: string;\n args?: unknown;\n input?: unknown;\n}\n\ninterface LooseToolCallChunk {\n id?: string;\n name?: string;\n args?: string;\n index?: number;\n type: \"tool_call_chunk\";\n}\n\nfunction extractToolCallChunks(blocks: ContentBlock[]): LooseToolCallChunk[] {\n const out: LooseToolCallChunk[] = [];\n for (const block of blocks) {\n if (block.type !== \"tool_call_chunk\") continue;\n const tc = block as {\n id?: string;\n name?: string;\n args?: string;\n index?: number;\n };\n out.push({\n id: tc.id,\n name: tc.name,\n args: tc.args,\n index: tc.index,\n type: \"tool_call_chunk\",\n });\n }\n return out;\n}\n\nfunction normalizeToolCallArgs(value: unknown): Record<string, unknown> {\n if (value != null && typeof value === \"object\" && !Array.isArray(value)) {\n return value as Record<string, unknown>;\n }\n if (typeof value === \"string\" && value.length > 0) {\n try {\n const parsed = JSON.parse(value);\n if (\n parsed != null &&\n typeof parsed === \"object\" &&\n !Array.isArray(parsed)\n ) {\n return parsed as Record<string, unknown>;\n }\n } catch {\n // Partial streaming input is represented via tool_call_chunks.\n }\n }\n return {};\n}\n"],"mappings":"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"}
|
|
@@ -57,10 +57,20 @@ function shouldPreferValuesMessageForToolCalls(valuesMessage, streamedMessage) {
|
|
|
57
57
|
const streamedToolCalls = getMessageToolCalls(streamedMessage);
|
|
58
58
|
if (streamedToolCalls.length < valuesToolCalls.length) return true;
|
|
59
59
|
const streamedIds = new Set(streamedToolCalls.map((toolCall) => toolCall.id).filter((id) => typeof id === "string" && id.length > 0));
|
|
60
|
-
|
|
60
|
+
if (valuesToolCalls.some((toolCall) => {
|
|
61
61
|
return typeof toolCall.id === "string" && !streamedIds.has(toolCall.id);
|
|
62
|
+
})) return true;
|
|
63
|
+
return valuesToolCalls.some((valuesToolCall) => {
|
|
64
|
+
const streamedToolCall = streamedToolCalls.find((candidate) => typeof valuesToolCall.id === "string" && candidate.id === valuesToolCall.id);
|
|
65
|
+
return streamedToolCall != null && hasMeaningfulArgs(valuesToolCall.args) && !jsonishEqual(valuesToolCall.args, streamedToolCall.args);
|
|
62
66
|
});
|
|
63
67
|
}
|
|
68
|
+
function hasMeaningfulArgs(args) {
|
|
69
|
+
if (args == null) return false;
|
|
70
|
+
if (typeof args === "string") return args.length > 0;
|
|
71
|
+
if (typeof args === "object") return Object.keys(args).length > 0;
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
64
74
|
function messagesEqualList(previous, next) {
|
|
65
75
|
if (previous === next) return true;
|
|
66
76
|
if (previous.length !== next.length) return false;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"message-reconciliation.cjs","names":[],"sources":["../../src/stream/message-reconciliation.ts"],"sourcesContent":["import type { BaseMessage } from \"@langchain/core/messages\";\n\nexport interface ReconcileMessagesFromValuesOptions {\n /**\n * Messages from the authoritative `values.messages` snapshot.\n */\n readonly valueMessages: readonly BaseMessage[];\n /**\n * Current message projection, including stream-assembled in-flight messages.\n */\n readonly currentMessages: readonly BaseMessage[];\n /**\n * Index from message id to current message position.\n */\n readonly currentIndexById: ReadonlyMap<string, number>;\n /**\n * Ids observed in the most recent previous `values.messages` snapshot.\n * If one of these ids is missing from the next snapshot, it is treated as\n * an explicit server-side removal.\n */\n readonly previousValueMessageIds: ReadonlySet<string>;\n /**\n * Optional stream-id filter. When supplied, only these current ids are\n * eligible to override the values snapshot. When omitted, any id present in\n * `currentIndexById` is eligible, preserving the root controller's historic\n * behavior.\n */\n readonly streamedMessageIds?: ReadonlySet<string>;\n /**\n * Allows callers to keep a values message even when a streamed message with\n * the same id exists. Used by the root controller when the values message\n * carries finalized tool-call data missing from the streamed message.\n */\n readonly preferValuesMessage?: (\n valuesMessage: BaseMessage,\n streamedMessage: BaseMessage\n ) => boolean;\n}\n\nexport interface ReconciledMessages {\n readonly messages: readonly BaseMessage[];\n readonly valueMessageIds: Set<string>;\n}\n\n/**\n * Merge an authoritative `values.messages` snapshot with the current streamed\n * message projection.\n *\n * Values remain authoritative for ordering and removals. Streamed messages\n * remain authoritative for in-flight content until the server echoes them in a\n * values snapshot, and stream-only messages are preserved until they either\n * appear in values or are known to have been removed.\n */\nexport function reconcileMessagesFromValues({\n valueMessages,\n currentMessages,\n currentIndexById,\n previousValueMessageIds,\n streamedMessageIds,\n preferValuesMessage,\n}: ReconcileMessagesFromValuesOptions): ReconciledMessages {\n const valueMessageIds = new Set<string>();\n const merged: BaseMessage[] = [];\n\n for (const valuesMessage of valueMessages) {\n const id = normalizedMessageId(valuesMessage);\n if (id == null) {\n merged.push(valuesMessage);\n continue;\n }\n\n valueMessageIds.add(id);\n const streamIdx = currentIndexById.get(id);\n const canUseStreamed =\n streamIdx != null &&\n (streamedMessageIds == null || streamedMessageIds.has(id));\n const streamedMessage = canUseStreamed\n ? currentMessages[streamIdx]\n : undefined;\n\n if (\n streamedMessage != null &&\n preferValuesMessage?.(valuesMessage, streamedMessage) !== true\n ) {\n merged.push(streamedMessage);\n } else {\n merged.push(valuesMessage);\n }\n }\n\n for (const existing of currentMessages) {\n const id = normalizedMessageId(existing);\n if (id == null) continue;\n if (valueMessageIds.has(id)) continue;\n if (previousValueMessageIds.has(id)) continue;\n if (streamedMessageIds != null && !streamedMessageIds.has(id)) continue;\n merged.push(existing);\n }\n\n return {\n messages: messagesEqualList(currentMessages, merged)\n ? currentMessages\n : merged,\n valueMessageIds,\n };\n}\n\n/**\n * Build a position index for keyed messages.\n */\nexport function buildMessageIndex(\n messages: readonly BaseMessage[]\n): Map<string, number> {\n const index = new Map<string, number>();\n messages.forEach((message, idx) => {\n const id = normalizedMessageId(message);\n if (id != null) index.set(id, idx);\n });\n return index;\n}\n\n/**\n * Decide whether a values message carries tool-call data missing from the\n * streamed message.\n */\nexport function shouldPreferValuesMessageForToolCalls(\n valuesMessage: BaseMessage,\n streamedMessage: BaseMessage\n): boolean {\n const valuesToolCalls = getMessageToolCalls(valuesMessage);\n if (valuesToolCalls.length === 0) return false;\n\n const streamedToolCalls = getMessageToolCalls(streamedMessage);\n if (streamedToolCalls.length < valuesToolCalls.length) return true;\n\n const streamedIds = new Set(\n streamedToolCalls\n .map((toolCall) => toolCall.id)\n .filter((id): id is string => typeof id === \"string\" && id.length > 0)\n );\n return valuesToolCalls.some((toolCall) => {\n return typeof toolCall.id === \"string\" && !streamedIds.has(toolCall.id);\n });\n}\n\nexport function messagesEqualList(\n previous: readonly BaseMessage[],\n next: readonly BaseMessage[]\n): boolean {\n if (previous === next) return true;\n if (previous.length !== next.length) return false;\n for (let i = 0; i < previous.length; i += 1) {\n if (!messagesEqual(previous[i], next[i])) return false;\n }\n return true;\n}\n\nexport function messagesEqual(\n previous: BaseMessage | undefined,\n next: BaseMessage | undefined\n): boolean {\n if (previous === next) return true;\n if (previous == null || next == null) return false;\n const previousRecord = previous as unknown as Record<string, unknown>;\n const nextRecord = next as unknown as Record<string, unknown>;\n const previousType =\n typeof previous.getType === \"function\"\n ? previous.getType()\n : previousRecord.type;\n const nextType =\n typeof next.getType === \"function\" ? next.getType() : nextRecord.type;\n\n return (\n previous.id === next.id &&\n previousType === nextType &&\n jsonishEqual(previous.content, next.content) &&\n previousRecord.tool_call_id === nextRecord.tool_call_id &&\n previousRecord.status === nextRecord.status &&\n jsonishEqual(\n previousRecord.additional_kwargs,\n nextRecord.additional_kwargs\n ) &&\n jsonishEqual(\n previousRecord.response_metadata,\n nextRecord.response_metadata\n ) &&\n jsonishEqual(previousRecord.tool_calls, nextRecord.tool_calls) &&\n jsonishEqual(\n previousRecord.tool_call_chunks,\n nextRecord.tool_call_chunks\n ) &&\n jsonishEqual(previousRecord.usage_metadata, nextRecord.usage_metadata)\n );\n}\n\nfunction normalizedMessageId(message: BaseMessage): string | undefined {\n return typeof message.id === \"string\" && message.id.length > 0\n ? message.id\n : undefined;\n}\n\nfunction getMessageToolCalls(\n message: BaseMessage\n): Array<{ id?: string; name?: string }> {\n const raw = (message as unknown as { tool_calls?: unknown }).tool_calls;\n if (!Array.isArray(raw)) return [];\n return raw.filter(\n (toolCall): toolCall is { id?: string; name?: string } =>\n toolCall != null && typeof toolCall === \"object\"\n );\n}\n\nfunction jsonishEqual(previous: unknown, next: unknown): boolean {\n return jsonishEqualAtDepth(previous, next, 0);\n}\n\nfunction jsonishEqualAtDepth(\n previous: unknown,\n next: unknown,\n depth: number\n): boolean {\n if (Object.is(previous, next)) return true;\n if (previous == null || next == null) return false;\n if (typeof previous !== \"object\" || typeof next !== \"object\") return false;\n if (depth >= 4) return false;\n\n if (Array.isArray(previous) || Array.isArray(next)) {\n if (!Array.isArray(previous) || !Array.isArray(next)) return false;\n if (previous.length !== next.length) return false;\n for (let i = 0; i < previous.length; i += 1) {\n if (!jsonishEqualAtDepth(previous[i], next[i], depth + 1)) return false;\n }\n return true;\n }\n\n const previousRecord = previous as Record<string, unknown>;\n const nextRecord = next as Record<string, unknown>;\n const previousKeys = Object.keys(previousRecord).filter(\n (key) => typeof previousRecord[key] !== \"function\"\n );\n const nextKeys = Object.keys(nextRecord).filter(\n (key) => typeof nextRecord[key] !== \"function\"\n );\n if (previousKeys.length !== nextKeys.length) return false;\n\n for (const key of previousKeys) {\n if (!Object.prototype.hasOwnProperty.call(nextRecord, key)) return false;\n if (!jsonishEqualAtDepth(previousRecord[key], nextRecord[key], depth + 1)) {\n return false;\n }\n }\n return true;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"message-reconciliation.cjs","names":[],"sources":["../../src/stream/message-reconciliation.ts"],"sourcesContent":["import type { BaseMessage } from \"@langchain/core/messages\";\n\nexport interface ReconcileMessagesFromValuesOptions {\n /**\n * Messages from the authoritative `values.messages` snapshot.\n */\n readonly valueMessages: readonly BaseMessage[];\n /**\n * Current message projection, including stream-assembled in-flight messages.\n */\n readonly currentMessages: readonly BaseMessage[];\n /**\n * Index from message id to current message position.\n */\n readonly currentIndexById: ReadonlyMap<string, number>;\n /**\n * Ids observed in the most recent previous `values.messages` snapshot.\n * If one of these ids is missing from the next snapshot, it is treated as\n * an explicit server-side removal.\n */\n readonly previousValueMessageIds: ReadonlySet<string>;\n /**\n * Optional stream-id filter. When supplied, only these current ids are\n * eligible to override the values snapshot. When omitted, any id present in\n * `currentIndexById` is eligible, preserving the root controller's historic\n * behavior.\n */\n readonly streamedMessageIds?: ReadonlySet<string>;\n /**\n * Allows callers to keep a values message even when a streamed message with\n * the same id exists. Used by the root controller when the values message\n * carries finalized tool-call data missing from the streamed message.\n */\n readonly preferValuesMessage?: (\n valuesMessage: BaseMessage,\n streamedMessage: BaseMessage\n ) => boolean;\n}\n\nexport interface ReconciledMessages {\n readonly messages: readonly BaseMessage[];\n readonly valueMessageIds: Set<string>;\n}\n\n/**\n * Merge an authoritative `values.messages` snapshot with the current streamed\n * message projection.\n *\n * Values remain authoritative for ordering and removals. Streamed messages\n * remain authoritative for in-flight content until the server echoes them in a\n * values snapshot, and stream-only messages are preserved until they either\n * appear in values or are known to have been removed.\n */\nexport function reconcileMessagesFromValues({\n valueMessages,\n currentMessages,\n currentIndexById,\n previousValueMessageIds,\n streamedMessageIds,\n preferValuesMessage,\n}: ReconcileMessagesFromValuesOptions): ReconciledMessages {\n const valueMessageIds = new Set<string>();\n const merged: BaseMessage[] = [];\n\n for (const valuesMessage of valueMessages) {\n const id = normalizedMessageId(valuesMessage);\n if (id == null) {\n merged.push(valuesMessage);\n continue;\n }\n\n valueMessageIds.add(id);\n const streamIdx = currentIndexById.get(id);\n const canUseStreamed =\n streamIdx != null &&\n (streamedMessageIds == null || streamedMessageIds.has(id));\n const streamedMessage = canUseStreamed\n ? currentMessages[streamIdx]\n : undefined;\n\n if (\n streamedMessage != null &&\n preferValuesMessage?.(valuesMessage, streamedMessage) !== true\n ) {\n merged.push(streamedMessage);\n } else {\n merged.push(valuesMessage);\n }\n }\n\n for (const existing of currentMessages) {\n const id = normalizedMessageId(existing);\n if (id == null) continue;\n if (valueMessageIds.has(id)) continue;\n if (previousValueMessageIds.has(id)) continue;\n if (streamedMessageIds != null && !streamedMessageIds.has(id)) continue;\n merged.push(existing);\n }\n\n return {\n messages: messagesEqualList(currentMessages, merged)\n ? currentMessages\n : merged,\n valueMessageIds,\n };\n}\n\n/**\n * Build a position index for keyed messages.\n */\nexport function buildMessageIndex(\n messages: readonly BaseMessage[]\n): Map<string, number> {\n const index = new Map<string, number>();\n messages.forEach((message, idx) => {\n const id = normalizedMessageId(message);\n if (id != null) index.set(id, idx);\n });\n return index;\n}\n\n/**\n * Decide whether a values message carries tool-call data missing from the\n * streamed message.\n */\nexport function shouldPreferValuesMessageForToolCalls(\n valuesMessage: BaseMessage,\n streamedMessage: BaseMessage\n): boolean {\n const valuesToolCalls = getMessageToolCalls(valuesMessage);\n if (valuesToolCalls.length === 0) return false;\n\n const streamedToolCalls = getMessageToolCalls(streamedMessage);\n if (streamedToolCalls.length < valuesToolCalls.length) return true;\n\n const streamedIds = new Set(\n streamedToolCalls\n .map((toolCall) => toolCall.id)\n .filter((id): id is string => typeof id === \"string\" && id.length > 0)\n );\n if (\n valuesToolCalls.some((toolCall) => {\n return typeof toolCall.id === \"string\" && !streamedIds.has(toolCall.id);\n })\n ) {\n return true;\n }\n\n // Values snapshots carry the finalized tool-call args. Prefer them only when\n // they add meaningful data, so empty placeholder args do not replace an\n // otherwise useful streamed message.\n return valuesToolCalls.some((valuesToolCall) => {\n const streamedToolCall = streamedToolCalls.find(\n (candidate) =>\n typeof valuesToolCall.id === \"string\" &&\n candidate.id === valuesToolCall.id\n );\n return (\n streamedToolCall != null &&\n hasMeaningfulArgs(valuesToolCall.args) &&\n !jsonishEqual(valuesToolCall.args, streamedToolCall.args)\n );\n });\n}\n\nfunction hasMeaningfulArgs(args: unknown): boolean {\n if (args == null) return false;\n if (typeof args === \"string\") return args.length > 0;\n if (typeof args === \"object\") return Object.keys(args).length > 0;\n return true;\n}\n\nexport function messagesEqualList(\n previous: readonly BaseMessage[],\n next: readonly BaseMessage[]\n): boolean {\n if (previous === next) return true;\n if (previous.length !== next.length) return false;\n for (let i = 0; i < previous.length; i += 1) {\n if (!messagesEqual(previous[i], next[i])) return false;\n }\n return true;\n}\n\nexport function messagesEqual(\n previous: BaseMessage | undefined,\n next: BaseMessage | undefined\n): boolean {\n if (previous === next) return true;\n if (previous == null || next == null) return false;\n const previousRecord = previous as unknown as Record<string, unknown>;\n const nextRecord = next as unknown as Record<string, unknown>;\n const previousType =\n typeof previous.getType === \"function\"\n ? previous.getType()\n : previousRecord.type;\n const nextType =\n typeof next.getType === \"function\" ? next.getType() : nextRecord.type;\n\n return (\n previous.id === next.id &&\n previousType === nextType &&\n jsonishEqual(previous.content, next.content) &&\n previousRecord.tool_call_id === nextRecord.tool_call_id &&\n previousRecord.status === nextRecord.status &&\n jsonishEqual(\n previousRecord.additional_kwargs,\n nextRecord.additional_kwargs\n ) &&\n jsonishEqual(\n previousRecord.response_metadata,\n nextRecord.response_metadata\n ) &&\n jsonishEqual(previousRecord.tool_calls, nextRecord.tool_calls) &&\n jsonishEqual(\n previousRecord.tool_call_chunks,\n nextRecord.tool_call_chunks\n ) &&\n jsonishEqual(previousRecord.usage_metadata, nextRecord.usage_metadata)\n );\n}\n\nfunction normalizedMessageId(message: BaseMessage): string | undefined {\n return typeof message.id === \"string\" && message.id.length > 0\n ? message.id\n : undefined;\n}\n\nfunction getMessageToolCalls(\n message: BaseMessage\n): Array<{ id?: string; name?: string; args?: unknown }> {\n const raw = (message as unknown as { tool_calls?: unknown }).tool_calls;\n if (!Array.isArray(raw)) return [];\n return raw.filter(\n (toolCall): toolCall is { id?: string; name?: string; args?: unknown } =>\n toolCall != null && typeof toolCall === \"object\"\n );\n}\n\nfunction jsonishEqual(previous: unknown, next: unknown): boolean {\n return jsonishEqualAtDepth(previous, next, 0);\n}\n\nfunction jsonishEqualAtDepth(\n previous: unknown,\n next: unknown,\n depth: number\n): boolean {\n if (Object.is(previous, next)) return true;\n if (previous == null || next == null) return false;\n if (typeof previous !== \"object\" || typeof next !== \"object\") return false;\n if (depth >= 4) return false;\n\n if (Array.isArray(previous) || Array.isArray(next)) {\n if (!Array.isArray(previous) || !Array.isArray(next)) return false;\n if (previous.length !== next.length) return false;\n for (let i = 0; i < previous.length; i += 1) {\n if (!jsonishEqualAtDepth(previous[i], next[i], depth + 1)) return false;\n }\n return true;\n }\n\n const previousRecord = previous as Record<string, unknown>;\n const nextRecord = next as Record<string, unknown>;\n const previousKeys = Object.keys(previousRecord).filter(\n (key) => typeof previousRecord[key] !== \"function\"\n );\n const nextKeys = Object.keys(nextRecord).filter(\n (key) => typeof nextRecord[key] !== \"function\"\n );\n if (previousKeys.length !== nextKeys.length) return false;\n\n for (const key of previousKeys) {\n if (!Object.prototype.hasOwnProperty.call(nextRecord, key)) return false;\n if (!jsonishEqualAtDepth(previousRecord[key], nextRecord[key], depth + 1)) {\n return false;\n }\n }\n return true;\n}\n"],"mappings":"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"}
|
|
@@ -57,10 +57,20 @@ function shouldPreferValuesMessageForToolCalls(valuesMessage, streamedMessage) {
|
|
|
57
57
|
const streamedToolCalls = getMessageToolCalls(streamedMessage);
|
|
58
58
|
if (streamedToolCalls.length < valuesToolCalls.length) return true;
|
|
59
59
|
const streamedIds = new Set(streamedToolCalls.map((toolCall) => toolCall.id).filter((id) => typeof id === "string" && id.length > 0));
|
|
60
|
-
|
|
60
|
+
if (valuesToolCalls.some((toolCall) => {
|
|
61
61
|
return typeof toolCall.id === "string" && !streamedIds.has(toolCall.id);
|
|
62
|
+
})) return true;
|
|
63
|
+
return valuesToolCalls.some((valuesToolCall) => {
|
|
64
|
+
const streamedToolCall = streamedToolCalls.find((candidate) => typeof valuesToolCall.id === "string" && candidate.id === valuesToolCall.id);
|
|
65
|
+
return streamedToolCall != null && hasMeaningfulArgs(valuesToolCall.args) && !jsonishEqual(valuesToolCall.args, streamedToolCall.args);
|
|
62
66
|
});
|
|
63
67
|
}
|
|
68
|
+
function hasMeaningfulArgs(args) {
|
|
69
|
+
if (args == null) return false;
|
|
70
|
+
if (typeof args === "string") return args.length > 0;
|
|
71
|
+
if (typeof args === "object") return Object.keys(args).length > 0;
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
64
74
|
function messagesEqualList(previous, next) {
|
|
65
75
|
if (previous === next) return true;
|
|
66
76
|
if (previous.length !== next.length) return false;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"message-reconciliation.js","names":[],"sources":["../../src/stream/message-reconciliation.ts"],"sourcesContent":["import type { BaseMessage } from \"@langchain/core/messages\";\n\nexport interface ReconcileMessagesFromValuesOptions {\n /**\n * Messages from the authoritative `values.messages` snapshot.\n */\n readonly valueMessages: readonly BaseMessage[];\n /**\n * Current message projection, including stream-assembled in-flight messages.\n */\n readonly currentMessages: readonly BaseMessage[];\n /**\n * Index from message id to current message position.\n */\n readonly currentIndexById: ReadonlyMap<string, number>;\n /**\n * Ids observed in the most recent previous `values.messages` snapshot.\n * If one of these ids is missing from the next snapshot, it is treated as\n * an explicit server-side removal.\n */\n readonly previousValueMessageIds: ReadonlySet<string>;\n /**\n * Optional stream-id filter. When supplied, only these current ids are\n * eligible to override the values snapshot. When omitted, any id present in\n * `currentIndexById` is eligible, preserving the root controller's historic\n * behavior.\n */\n readonly streamedMessageIds?: ReadonlySet<string>;\n /**\n * Allows callers to keep a values message even when a streamed message with\n * the same id exists. Used by the root controller when the values message\n * carries finalized tool-call data missing from the streamed message.\n */\n readonly preferValuesMessage?: (\n valuesMessage: BaseMessage,\n streamedMessage: BaseMessage\n ) => boolean;\n}\n\nexport interface ReconciledMessages {\n readonly messages: readonly BaseMessage[];\n readonly valueMessageIds: Set<string>;\n}\n\n/**\n * Merge an authoritative `values.messages` snapshot with the current streamed\n * message projection.\n *\n * Values remain authoritative for ordering and removals. Streamed messages\n * remain authoritative for in-flight content until the server echoes them in a\n * values snapshot, and stream-only messages are preserved until they either\n * appear in values or are known to have been removed.\n */\nexport function reconcileMessagesFromValues({\n valueMessages,\n currentMessages,\n currentIndexById,\n previousValueMessageIds,\n streamedMessageIds,\n preferValuesMessage,\n}: ReconcileMessagesFromValuesOptions): ReconciledMessages {\n const valueMessageIds = new Set<string>();\n const merged: BaseMessage[] = [];\n\n for (const valuesMessage of valueMessages) {\n const id = normalizedMessageId(valuesMessage);\n if (id == null) {\n merged.push(valuesMessage);\n continue;\n }\n\n valueMessageIds.add(id);\n const streamIdx = currentIndexById.get(id);\n const canUseStreamed =\n streamIdx != null &&\n (streamedMessageIds == null || streamedMessageIds.has(id));\n const streamedMessage = canUseStreamed\n ? currentMessages[streamIdx]\n : undefined;\n\n if (\n streamedMessage != null &&\n preferValuesMessage?.(valuesMessage, streamedMessage) !== true\n ) {\n merged.push(streamedMessage);\n } else {\n merged.push(valuesMessage);\n }\n }\n\n for (const existing of currentMessages) {\n const id = normalizedMessageId(existing);\n if (id == null) continue;\n if (valueMessageIds.has(id)) continue;\n if (previousValueMessageIds.has(id)) continue;\n if (streamedMessageIds != null && !streamedMessageIds.has(id)) continue;\n merged.push(existing);\n }\n\n return {\n messages: messagesEqualList(currentMessages, merged)\n ? currentMessages\n : merged,\n valueMessageIds,\n };\n}\n\n/**\n * Build a position index for keyed messages.\n */\nexport function buildMessageIndex(\n messages: readonly BaseMessage[]\n): Map<string, number> {\n const index = new Map<string, number>();\n messages.forEach((message, idx) => {\n const id = normalizedMessageId(message);\n if (id != null) index.set(id, idx);\n });\n return index;\n}\n\n/**\n * Decide whether a values message carries tool-call data missing from the\n * streamed message.\n */\nexport function shouldPreferValuesMessageForToolCalls(\n valuesMessage: BaseMessage,\n streamedMessage: BaseMessage\n): boolean {\n const valuesToolCalls = getMessageToolCalls(valuesMessage);\n if (valuesToolCalls.length === 0) return false;\n\n const streamedToolCalls = getMessageToolCalls(streamedMessage);\n if (streamedToolCalls.length < valuesToolCalls.length) return true;\n\n const streamedIds = new Set(\n streamedToolCalls\n .map((toolCall) => toolCall.id)\n .filter((id): id is string => typeof id === \"string\" && id.length > 0)\n );\n return valuesToolCalls.some((toolCall) => {\n return typeof toolCall.id === \"string\" && !streamedIds.has(toolCall.id);\n });\n}\n\nexport function messagesEqualList(\n previous: readonly BaseMessage[],\n next: readonly BaseMessage[]\n): boolean {\n if (previous === next) return true;\n if (previous.length !== next.length) return false;\n for (let i = 0; i < previous.length; i += 1) {\n if (!messagesEqual(previous[i], next[i])) return false;\n }\n return true;\n}\n\nexport function messagesEqual(\n previous: BaseMessage | undefined,\n next: BaseMessage | undefined\n): boolean {\n if (previous === next) return true;\n if (previous == null || next == null) return false;\n const previousRecord = previous as unknown as Record<string, unknown>;\n const nextRecord = next as unknown as Record<string, unknown>;\n const previousType =\n typeof previous.getType === \"function\"\n ? previous.getType()\n : previousRecord.type;\n const nextType =\n typeof next.getType === \"function\" ? next.getType() : nextRecord.type;\n\n return (\n previous.id === next.id &&\n previousType === nextType &&\n jsonishEqual(previous.content, next.content) &&\n previousRecord.tool_call_id === nextRecord.tool_call_id &&\n previousRecord.status === nextRecord.status &&\n jsonishEqual(\n previousRecord.additional_kwargs,\n nextRecord.additional_kwargs\n ) &&\n jsonishEqual(\n previousRecord.response_metadata,\n nextRecord.response_metadata\n ) &&\n jsonishEqual(previousRecord.tool_calls, nextRecord.tool_calls) &&\n jsonishEqual(\n previousRecord.tool_call_chunks,\n nextRecord.tool_call_chunks\n ) &&\n jsonishEqual(previousRecord.usage_metadata, nextRecord.usage_metadata)\n );\n}\n\nfunction normalizedMessageId(message: BaseMessage): string | undefined {\n return typeof message.id === \"string\" && message.id.length > 0\n ? message.id\n : undefined;\n}\n\nfunction getMessageToolCalls(\n message: BaseMessage\n): Array<{ id?: string; name?: string }> {\n const raw = (message as unknown as { tool_calls?: unknown }).tool_calls;\n if (!Array.isArray(raw)) return [];\n return raw.filter(\n (toolCall): toolCall is { id?: string; name?: string } =>\n toolCall != null && typeof toolCall === \"object\"\n );\n}\n\nfunction jsonishEqual(previous: unknown, next: unknown): boolean {\n return jsonishEqualAtDepth(previous, next, 0);\n}\n\nfunction jsonishEqualAtDepth(\n previous: unknown,\n next: unknown,\n depth: number\n): boolean {\n if (Object.is(previous, next)) return true;\n if (previous == null || next == null) return false;\n if (typeof previous !== \"object\" || typeof next !== \"object\") return false;\n if (depth >= 4) return false;\n\n if (Array.isArray(previous) || Array.isArray(next)) {\n if (!Array.isArray(previous) || !Array.isArray(next)) return false;\n if (previous.length !== next.length) return false;\n for (let i = 0; i < previous.length; i += 1) {\n if (!jsonishEqualAtDepth(previous[i], next[i], depth + 1)) return false;\n }\n return true;\n }\n\n const previousRecord = previous as Record<string, unknown>;\n const nextRecord = next as Record<string, unknown>;\n const previousKeys = Object.keys(previousRecord).filter(\n (key) => typeof previousRecord[key] !== \"function\"\n );\n const nextKeys = Object.keys(nextRecord).filter(\n (key) => typeof nextRecord[key] !== \"function\"\n );\n if (previousKeys.length !== nextKeys.length) return false;\n\n for (const key of previousKeys) {\n if (!Object.prototype.hasOwnProperty.call(nextRecord, key)) return false;\n if (!jsonishEqualAtDepth(previousRecord[key], nextRecord[key], depth + 1)) {\n return false;\n }\n }\n return true;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"message-reconciliation.js","names":[],"sources":["../../src/stream/message-reconciliation.ts"],"sourcesContent":["import type { BaseMessage } from \"@langchain/core/messages\";\n\nexport interface ReconcileMessagesFromValuesOptions {\n /**\n * Messages from the authoritative `values.messages` snapshot.\n */\n readonly valueMessages: readonly BaseMessage[];\n /**\n * Current message projection, including stream-assembled in-flight messages.\n */\n readonly currentMessages: readonly BaseMessage[];\n /**\n * Index from message id to current message position.\n */\n readonly currentIndexById: ReadonlyMap<string, number>;\n /**\n * Ids observed in the most recent previous `values.messages` snapshot.\n * If one of these ids is missing from the next snapshot, it is treated as\n * an explicit server-side removal.\n */\n readonly previousValueMessageIds: ReadonlySet<string>;\n /**\n * Optional stream-id filter. When supplied, only these current ids are\n * eligible to override the values snapshot. When omitted, any id present in\n * `currentIndexById` is eligible, preserving the root controller's historic\n * behavior.\n */\n readonly streamedMessageIds?: ReadonlySet<string>;\n /**\n * Allows callers to keep a values message even when a streamed message with\n * the same id exists. Used by the root controller when the values message\n * carries finalized tool-call data missing from the streamed message.\n */\n readonly preferValuesMessage?: (\n valuesMessage: BaseMessage,\n streamedMessage: BaseMessage\n ) => boolean;\n}\n\nexport interface ReconciledMessages {\n readonly messages: readonly BaseMessage[];\n readonly valueMessageIds: Set<string>;\n}\n\n/**\n * Merge an authoritative `values.messages` snapshot with the current streamed\n * message projection.\n *\n * Values remain authoritative for ordering and removals. Streamed messages\n * remain authoritative for in-flight content until the server echoes them in a\n * values snapshot, and stream-only messages are preserved until they either\n * appear in values or are known to have been removed.\n */\nexport function reconcileMessagesFromValues({\n valueMessages,\n currentMessages,\n currentIndexById,\n previousValueMessageIds,\n streamedMessageIds,\n preferValuesMessage,\n}: ReconcileMessagesFromValuesOptions): ReconciledMessages {\n const valueMessageIds = new Set<string>();\n const merged: BaseMessage[] = [];\n\n for (const valuesMessage of valueMessages) {\n const id = normalizedMessageId(valuesMessage);\n if (id == null) {\n merged.push(valuesMessage);\n continue;\n }\n\n valueMessageIds.add(id);\n const streamIdx = currentIndexById.get(id);\n const canUseStreamed =\n streamIdx != null &&\n (streamedMessageIds == null || streamedMessageIds.has(id));\n const streamedMessage = canUseStreamed\n ? currentMessages[streamIdx]\n : undefined;\n\n if (\n streamedMessage != null &&\n preferValuesMessage?.(valuesMessage, streamedMessage) !== true\n ) {\n merged.push(streamedMessage);\n } else {\n merged.push(valuesMessage);\n }\n }\n\n for (const existing of currentMessages) {\n const id = normalizedMessageId(existing);\n if (id == null) continue;\n if (valueMessageIds.has(id)) continue;\n if (previousValueMessageIds.has(id)) continue;\n if (streamedMessageIds != null && !streamedMessageIds.has(id)) continue;\n merged.push(existing);\n }\n\n return {\n messages: messagesEqualList(currentMessages, merged)\n ? currentMessages\n : merged,\n valueMessageIds,\n };\n}\n\n/**\n * Build a position index for keyed messages.\n */\nexport function buildMessageIndex(\n messages: readonly BaseMessage[]\n): Map<string, number> {\n const index = new Map<string, number>();\n messages.forEach((message, idx) => {\n const id = normalizedMessageId(message);\n if (id != null) index.set(id, idx);\n });\n return index;\n}\n\n/**\n * Decide whether a values message carries tool-call data missing from the\n * streamed message.\n */\nexport function shouldPreferValuesMessageForToolCalls(\n valuesMessage: BaseMessage,\n streamedMessage: BaseMessage\n): boolean {\n const valuesToolCalls = getMessageToolCalls(valuesMessage);\n if (valuesToolCalls.length === 0) return false;\n\n const streamedToolCalls = getMessageToolCalls(streamedMessage);\n if (streamedToolCalls.length < valuesToolCalls.length) return true;\n\n const streamedIds = new Set(\n streamedToolCalls\n .map((toolCall) => toolCall.id)\n .filter((id): id is string => typeof id === \"string\" && id.length > 0)\n );\n if (\n valuesToolCalls.some((toolCall) => {\n return typeof toolCall.id === \"string\" && !streamedIds.has(toolCall.id);\n })\n ) {\n return true;\n }\n\n // Values snapshots carry the finalized tool-call args. Prefer them only when\n // they add meaningful data, so empty placeholder args do not replace an\n // otherwise useful streamed message.\n return valuesToolCalls.some((valuesToolCall) => {\n const streamedToolCall = streamedToolCalls.find(\n (candidate) =>\n typeof valuesToolCall.id === \"string\" &&\n candidate.id === valuesToolCall.id\n );\n return (\n streamedToolCall != null &&\n hasMeaningfulArgs(valuesToolCall.args) &&\n !jsonishEqual(valuesToolCall.args, streamedToolCall.args)\n );\n });\n}\n\nfunction hasMeaningfulArgs(args: unknown): boolean {\n if (args == null) return false;\n if (typeof args === \"string\") return args.length > 0;\n if (typeof args === \"object\") return Object.keys(args).length > 0;\n return true;\n}\n\nexport function messagesEqualList(\n previous: readonly BaseMessage[],\n next: readonly BaseMessage[]\n): boolean {\n if (previous === next) return true;\n if (previous.length !== next.length) return false;\n for (let i = 0; i < previous.length; i += 1) {\n if (!messagesEqual(previous[i], next[i])) return false;\n }\n return true;\n}\n\nexport function messagesEqual(\n previous: BaseMessage | undefined,\n next: BaseMessage | undefined\n): boolean {\n if (previous === next) return true;\n if (previous == null || next == null) return false;\n const previousRecord = previous as unknown as Record<string, unknown>;\n const nextRecord = next as unknown as Record<string, unknown>;\n const previousType =\n typeof previous.getType === \"function\"\n ? previous.getType()\n : previousRecord.type;\n const nextType =\n typeof next.getType === \"function\" ? next.getType() : nextRecord.type;\n\n return (\n previous.id === next.id &&\n previousType === nextType &&\n jsonishEqual(previous.content, next.content) &&\n previousRecord.tool_call_id === nextRecord.tool_call_id &&\n previousRecord.status === nextRecord.status &&\n jsonishEqual(\n previousRecord.additional_kwargs,\n nextRecord.additional_kwargs\n ) &&\n jsonishEqual(\n previousRecord.response_metadata,\n nextRecord.response_metadata\n ) &&\n jsonishEqual(previousRecord.tool_calls, nextRecord.tool_calls) &&\n jsonishEqual(\n previousRecord.tool_call_chunks,\n nextRecord.tool_call_chunks\n ) &&\n jsonishEqual(previousRecord.usage_metadata, nextRecord.usage_metadata)\n );\n}\n\nfunction normalizedMessageId(message: BaseMessage): string | undefined {\n return typeof message.id === \"string\" && message.id.length > 0\n ? message.id\n : undefined;\n}\n\nfunction getMessageToolCalls(\n message: BaseMessage\n): Array<{ id?: string; name?: string; args?: unknown }> {\n const raw = (message as unknown as { tool_calls?: unknown }).tool_calls;\n if (!Array.isArray(raw)) return [];\n return raw.filter(\n (toolCall): toolCall is { id?: string; name?: string; args?: unknown } =>\n toolCall != null && typeof toolCall === \"object\"\n );\n}\n\nfunction jsonishEqual(previous: unknown, next: unknown): boolean {\n return jsonishEqualAtDepth(previous, next, 0);\n}\n\nfunction jsonishEqualAtDepth(\n previous: unknown,\n next: unknown,\n depth: number\n): boolean {\n if (Object.is(previous, next)) return true;\n if (previous == null || next == null) return false;\n if (typeof previous !== \"object\" || typeof next !== \"object\") return false;\n if (depth >= 4) return false;\n\n if (Array.isArray(previous) || Array.isArray(next)) {\n if (!Array.isArray(previous) || !Array.isArray(next)) return false;\n if (previous.length !== next.length) return false;\n for (let i = 0; i < previous.length; i += 1) {\n if (!jsonishEqualAtDepth(previous[i], next[i], depth + 1)) return false;\n }\n return true;\n }\n\n const previousRecord = previous as Record<string, unknown>;\n const nextRecord = next as Record<string, unknown>;\n const previousKeys = Object.keys(previousRecord).filter(\n (key) => typeof previousRecord[key] !== \"function\"\n );\n const nextKeys = Object.keys(nextRecord).filter(\n (key) => typeof nextRecord[key] !== \"function\"\n );\n if (previousKeys.length !== nextKeys.length) return false;\n\n for (const key of previousKeys) {\n if (!Object.prototype.hasOwnProperty.call(nextRecord, key)) return false;\n if (!jsonishEqualAtDepth(previousRecord[key], nextRecord[key], depth + 1)) {\n return false;\n }\n }\n return true;\n}\n"],"mappings":"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"}
|