@ai-sdk/langchain 3.0.120 → 3.0.123

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/index.js CHANGED
@@ -1,1682 +1,1916 @@
1
- // src/adapter.ts
2
- import {
3
- SystemMessage
4
- } from "@langchain/core/messages";
5
- import {
6
- convertToModelMessages
7
- } from "ai";
8
-
9
- // src/utils.ts
10
- import {
11
- AIMessage,
12
- HumanMessage,
13
- ToolMessage,
14
- AIMessageChunk
15
- } from "@langchain/core/messages";
1
+ import { AIMessage, AIMessageChunk, HumanMessage, SystemMessage, ToolMessage } from "@langchain/core/messages";
2
+ import { convertToModelMessages } from "ai";
3
+ import { RemoteGraph } from "@langchain/langgraph/remote";
4
+ //#region src/utils.ts
5
+ /**
6
+ * Parses a LangGraph event tuple into [namespace, type, data].
7
+ * Handles both 2-element [type, data] and 3-element [namespace, type, data] formats.
8
+ *
9
+ * @param event - The raw LangGraph event array.
10
+ * @returns A tuple of [namespace, type, data].
11
+ */
16
12
  function parseLangGraphEvent(event) {
17
- return event.length === 3 ? [event[0], event[1], event[2]] : [void 0, event[0], event[1]];
13
+ return event.length === 3 ? [
14
+ event[0],
15
+ event[1],
16
+ event[2]
17
+ ] : [
18
+ void 0,
19
+ event[0],
20
+ event[1]
21
+ ];
18
22
  }
19
23
  function getLangGraphProviderMetadata(namespace) {
20
- return namespace === void 0 ? void 0 : {
21
- langchain: {
22
- namespace
23
- }
24
- };
24
+ return namespace === void 0 ? void 0 : { langchain: { namespace } };
25
25
  }
26
26
  function addLangGraphNamespace(chunk, namespace) {
27
- if (namespace === void 0) {
28
- return chunk;
29
- }
30
- switch (chunk.type) {
31
- case "text-start":
32
- case "text-delta":
33
- case "text-end":
34
- case "reasoning-start":
35
- case "reasoning-delta":
36
- case "reasoning-end":
37
- case "custom":
38
- case "tool-input-start":
39
- case "tool-input-available":
40
- case "tool-input-error":
41
- case "tool-approval-response":
42
- case "tool-output-available":
43
- case "tool-output-error":
44
- case "source-url":
45
- case "source-document":
46
- case "file":
47
- case "reasoning-file": {
48
- return {
49
- ...chunk,
50
- providerMetadata: {
51
- ...chunk.providerMetadata,
52
- langchain: {
53
- ...chunk.providerMetadata?.langchain,
54
- namespace
55
- }
56
- }
57
- };
58
- }
59
- default:
60
- return chunk;
61
- }
27
+ if (namespace === void 0) return chunk;
28
+ switch (chunk.type) {
29
+ case "text-start":
30
+ case "text-delta":
31
+ case "text-end":
32
+ case "reasoning-start":
33
+ case "reasoning-delta":
34
+ case "reasoning-end":
35
+ case "custom":
36
+ case "tool-input-start":
37
+ case "tool-input-available":
38
+ case "tool-input-error":
39
+ case "tool-approval-response":
40
+ case "tool-output-available":
41
+ case "tool-output-error":
42
+ case "source-url":
43
+ case "source-document":
44
+ case "file":
45
+ case "reasoning-file": return {
46
+ ...chunk,
47
+ providerMetadata: {
48
+ ...chunk.providerMetadata,
49
+ langchain: {
50
+ ...chunk.providerMetadata?.langchain,
51
+ namespace
52
+ }
53
+ }
54
+ };
55
+ default: return chunk;
56
+ }
62
57
  }
63
58
  function createLangGraphNamespaceController(controller, state, eventNamespace) {
64
- return {
65
- get desiredSize() {
66
- return controller.desiredSize;
67
- },
68
- close: () => controller.close(),
69
- error: (reason) => controller.error(reason),
70
- enqueue: (chunk) => {
71
- if (chunk === void 0) {
72
- return;
73
- }
74
- let messageNamespace;
75
- switch (chunk.type) {
76
- case "text-start":
77
- case "text-delta":
78
- case "text-end":
79
- case "reasoning-start":
80
- case "reasoning-delta":
81
- case "reasoning-end":
82
- messageNamespace = state.messageNamespaces.get(chunk.id);
83
- break;
84
- }
85
- controller.enqueue(
86
- addLangGraphNamespace(chunk, messageNamespace ?? eventNamespace)
87
- );
88
- }
89
- };
59
+ return {
60
+ get desiredSize() {
61
+ return controller.desiredSize;
62
+ },
63
+ close: () => controller.close(),
64
+ error: (reason) => controller.error(reason),
65
+ enqueue: (chunk) => {
66
+ if (chunk === void 0) return;
67
+ let messageNamespace;
68
+ switch (chunk.type) {
69
+ case "text-start":
70
+ case "text-delta":
71
+ case "text-end":
72
+ case "reasoning-start":
73
+ case "reasoning-delta":
74
+ case "reasoning-end": messageNamespace = state.messageNamespaces.get(chunk.id);
75
+ }
76
+ controller.enqueue(addLangGraphNamespace(chunk, messageNamespace ?? eventNamespace));
77
+ }
78
+ };
90
79
  }
80
+ /**
81
+ * Converts a ToolResultPart to a LangChain ToolMessage
82
+ * @param block - The ToolResultPart to convert.
83
+ * @returns The converted ToolMessage.
84
+ */
91
85
  function convertToolResultPart(block) {
92
- const content = (() => {
93
- if (block.output.type === "text" || block.output.type === "error-text") {
94
- return block.output.value;
95
- }
96
- if (block.output.type === "json" || block.output.type === "error-json") {
97
- return JSON.stringify(block.output.value);
98
- }
99
- if (block.output.type === "content") {
100
- return block.output.value.map((outputBlock) => {
101
- if (outputBlock.type === "text") {
102
- return outputBlock.text;
103
- }
104
- return "";
105
- }).join("");
106
- }
107
- return "";
108
- })();
109
- return new ToolMessage({
110
- tool_call_id: block.toolCallId,
111
- content
112
- });
86
+ const content = (() => {
87
+ if (block.output.type === "text" || block.output.type === "error-text") return block.output.value;
88
+ if (block.output.type === "json" || block.output.type === "error-json") return JSON.stringify(block.output.value);
89
+ if (block.output.type === "content") return block.output.value.map((outputBlock) => {
90
+ if (outputBlock.type === "text") return outputBlock.text;
91
+ return "";
92
+ }).join("");
93
+ return "";
94
+ })();
95
+ return new ToolMessage({
96
+ tool_call_id: block.toolCallId,
97
+ content
98
+ });
113
99
  }
100
+ /**
101
+ * Converts AssistantContent to LangChain AIMessage
102
+ * @param content - The AssistantContent to convert.
103
+ * @returns The converted AIMessage.
104
+ */
114
105
  function convertAssistantContent(content) {
115
- if (typeof content === "string") {
116
- return new AIMessage({ content });
117
- }
118
- const textParts = [];
119
- const toolCalls = [];
120
- for (const part of content) {
121
- if (part.type === "text") {
122
- textParts.push(part.text);
123
- } else if (part.type === "tool-call") {
124
- toolCalls.push({
125
- id: part.toolCallId,
126
- name: part.toolName,
127
- args: part.input
128
- });
129
- }
130
- }
131
- return new AIMessage({
132
- content: textParts.join(""),
133
- tool_calls: toolCalls.length > 0 ? toolCalls : void 0
134
- });
106
+ if (typeof content === "string") return new AIMessage({ content });
107
+ const textParts = [];
108
+ const toolCalls = [];
109
+ for (const part of content) if (part.type === "text") textParts.push(part.text);
110
+ else if (part.type === "tool-call") toolCalls.push({
111
+ id: part.toolCallId,
112
+ name: part.toolName,
113
+ args: part.input
114
+ });
115
+ return new AIMessage({
116
+ content: textParts.join(""),
117
+ tool_calls: toolCalls.length > 0 ? toolCalls : void 0
118
+ });
135
119
  }
120
+ /**
121
+ * Helper to generate a default filename from mediaType
122
+ */
136
123
  function getDefaultFilename(mediaType, prefix = "file") {
137
- const fileExtension = mediaType.split("/")[1] || "bin";
138
- return `${prefix}.${fileExtension}`;
124
+ return `${prefix}.${mediaType.split("/")[1] || "bin"}`;
139
125
  }
140
126
  function convertImageToContentBlock(data, mediaType = "image/png") {
141
- if (data instanceof URL) {
142
- if (data.protocol !== "data:") {
143
- return { type: "image", url: data.toString() };
144
- }
145
- data = data.toString();
146
- }
147
- if (typeof data === "string") {
148
- if (data.startsWith("http://") || data.startsWith("https://")) {
149
- return { type: "image", url: data };
150
- }
151
- if (data.startsWith("data:")) {
152
- const matches = data.match(/^data:([^;]+);base64,(.+)$/);
153
- if (matches) {
154
- return {
155
- type: "image",
156
- data: matches[2],
157
- mimeType: matches[1]
158
- };
159
- }
160
- return { type: "image", url: data };
161
- }
162
- return { type: "image", data, mimeType: mediaType };
163
- }
164
- const bytes = data instanceof ArrayBuffer ? new Uint8Array(data) : data;
165
- return {
166
- type: "image",
167
- data: btoa(String.fromCharCode(...bytes)),
168
- mimeType: mediaType
169
- };
127
+ if (data instanceof URL) {
128
+ if (data.protocol !== "data:") return {
129
+ type: "image",
130
+ url: data.toString()
131
+ };
132
+ data = data.toString();
133
+ }
134
+ if (typeof data === "string") {
135
+ if (data.startsWith("http://") || data.startsWith("https://")) return {
136
+ type: "image",
137
+ url: data
138
+ };
139
+ if (data.startsWith("data:")) {
140
+ const matches = data.match(/^data:([^;]+);base64,(.+)$/);
141
+ if (matches) return {
142
+ type: "image",
143
+ data: matches[2],
144
+ mimeType: matches[1]
145
+ };
146
+ return {
147
+ type: "image",
148
+ url: data
149
+ };
150
+ }
151
+ return {
152
+ type: "image",
153
+ data,
154
+ mimeType: mediaType
155
+ };
156
+ }
157
+ const bytes = data instanceof ArrayBuffer ? new Uint8Array(data) : data;
158
+ return {
159
+ type: "image",
160
+ data: btoa(String.fromCharCode(...bytes)),
161
+ mimeType: mediaType
162
+ };
170
163
  }
164
+ /**
165
+ * Converts UserContent to LangChain HumanMessage
166
+ * @param content - The UserContent to convert.
167
+ * @returns The converted HumanMessage.
168
+ */
171
169
  function convertUserContent(content) {
172
- if (typeof content === "string") {
173
- return new HumanMessage({ content });
174
- }
175
- const contentBlocks = [];
176
- for (const part of content) {
177
- if (part.type === "text") {
178
- contentBlocks.push({ type: "text", text: part.text });
179
- } else if (part.type === "image") {
180
- const imagePart = part;
181
- if (imagePart.image instanceof URL || typeof imagePart.image === "string" || imagePart.image instanceof Uint8Array || imagePart.image instanceof ArrayBuffer) {
182
- contentBlocks.push(
183
- convertImageToContentBlock(imagePart.image, imagePart.mediaType)
184
- );
185
- }
186
- } else if (part.type === "file") {
187
- const rawFilePart = part;
188
- const normalizedData = (() => {
189
- const data = rawFilePart.data;
190
- if (typeof data === "object" && data !== null && !(data instanceof URL) && !(data instanceof Uint8Array) && !(data instanceof ArrayBuffer) && "type" in data) {
191
- switch (data.type) {
192
- case "data":
193
- return data.data;
194
- case "url":
195
- return data.url;
196
- case "text":
197
- return data.text;
198
- default:
199
- return "";
200
- }
201
- }
202
- return data;
203
- })();
204
- const filePart = {
205
- type: "file",
206
- data: normalizedData,
207
- mediaType: rawFilePart.mediaType,
208
- filename: rawFilePart.filename
209
- };
210
- const isImage = filePart.mediaType?.startsWith("image/");
211
- if (isImage) {
212
- contentBlocks.push(
213
- convertImageToContentBlock(filePart.data, filePart.mediaType)
214
- );
215
- } else {
216
- const filename = filePart.filename || getDefaultFilename(filePart.mediaType, "file");
217
- if (filePart.data instanceof URL) {
218
- contentBlocks.push({
219
- type: "file",
220
- url: filePart.data.toString(),
221
- mimeType: filePart.mediaType,
222
- filename
223
- });
224
- } else if (typeof filePart.data === "string") {
225
- if (filePart.data.startsWith("http://") || filePart.data.startsWith("https://")) {
226
- contentBlocks.push({
227
- type: "file",
228
- url: filePart.data,
229
- mimeType: filePart.mediaType,
230
- filename
231
- });
232
- } else if (filePart.data.startsWith("data:")) {
233
- const matches = filePart.data.match(/^data:([^;]+);base64,(.+)$/);
234
- if (matches) {
235
- contentBlocks.push({
236
- type: "file",
237
- data: matches[2],
238
- mimeType: matches[1],
239
- filename
240
- });
241
- } else {
242
- contentBlocks.push({
243
- type: "file",
244
- url: filePart.data,
245
- mimeType: filePart.mediaType,
246
- filename
247
- });
248
- }
249
- } else {
250
- contentBlocks.push({
251
- type: "file",
252
- data: filePart.data,
253
- mimeType: filePart.mediaType,
254
- filename
255
- });
256
- }
257
- } else if (filePart.data instanceof Uint8Array || filePart.data instanceof ArrayBuffer) {
258
- const bytes = filePart.data instanceof ArrayBuffer ? new Uint8Array(filePart.data) : filePart.data;
259
- const base64 = btoa(String.fromCharCode(...bytes));
260
- contentBlocks.push({
261
- type: "file",
262
- data: base64,
263
- mimeType: filePart.mediaType,
264
- filename
265
- });
266
- }
267
- }
268
- }
269
- }
270
- if (contentBlocks.every((block) => block.type === "text")) {
271
- return new HumanMessage({
272
- content: contentBlocks.map((block) => block.text).join("")
273
- });
274
- }
275
- return new HumanMessage({ contentBlocks });
170
+ if (typeof content === "string") return new HumanMessage({ content });
171
+ const contentBlocks = [];
172
+ for (const part of content) if (part.type === "text") contentBlocks.push({
173
+ type: "text",
174
+ text: part.text
175
+ });
176
+ else if (part.type === "image") {
177
+ const imagePart = part;
178
+ if (imagePart.image instanceof URL || typeof imagePart.image === "string" || imagePart.image instanceof Uint8Array || imagePart.image instanceof ArrayBuffer) contentBlocks.push(convertImageToContentBlock(imagePart.image, imagePart.mediaType));
179
+ } else if (part.type === "file") {
180
+ const rawFilePart = part;
181
+ const filePart = {
182
+ type: "file",
183
+ data: (() => {
184
+ const data = rawFilePart.data;
185
+ if (typeof data === "object" && data !== null && !(data instanceof URL) && !(data instanceof Uint8Array) && !(data instanceof ArrayBuffer) && "type" in data) switch (data.type) {
186
+ case "data": return data.data;
187
+ case "url": return data.url;
188
+ case "text": return data.text;
189
+ default: return "";
190
+ }
191
+ return data;
192
+ })(),
193
+ mediaType: rawFilePart.mediaType,
194
+ filename: rawFilePart.filename
195
+ };
196
+ if (filePart.mediaType?.startsWith("image/")) contentBlocks.push(convertImageToContentBlock(filePart.data, filePart.mediaType));
197
+ else {
198
+ const filename = filePart.filename || getDefaultFilename(filePart.mediaType, "file");
199
+ if (filePart.data instanceof URL) contentBlocks.push({
200
+ type: "file",
201
+ url: filePart.data.toString(),
202
+ mimeType: filePart.mediaType,
203
+ filename
204
+ });
205
+ else if (typeof filePart.data === "string") {
206
+ if (filePart.data.startsWith("http://") || filePart.data.startsWith("https://")) contentBlocks.push({
207
+ type: "file",
208
+ url: filePart.data,
209
+ mimeType: filePart.mediaType,
210
+ filename
211
+ });
212
+ else if (filePart.data.startsWith("data:")) {
213
+ const matches = filePart.data.match(/^data:([^;]+);base64,(.+)$/);
214
+ if (matches) contentBlocks.push({
215
+ type: "file",
216
+ data: matches[2],
217
+ mimeType: matches[1],
218
+ filename
219
+ });
220
+ else contentBlocks.push({
221
+ type: "file",
222
+ url: filePart.data,
223
+ mimeType: filePart.mediaType,
224
+ filename
225
+ });
226
+ } else contentBlocks.push({
227
+ type: "file",
228
+ data: filePart.data,
229
+ mimeType: filePart.mediaType,
230
+ filename
231
+ });
232
+ } else if (filePart.data instanceof Uint8Array || filePart.data instanceof ArrayBuffer) {
233
+ const bytes = filePart.data instanceof ArrayBuffer ? new Uint8Array(filePart.data) : filePart.data;
234
+ const base64 = btoa(String.fromCharCode(...bytes));
235
+ contentBlocks.push({
236
+ type: "file",
237
+ data: base64,
238
+ mimeType: filePart.mediaType,
239
+ filename
240
+ });
241
+ }
242
+ }
243
+ }
244
+ /**
245
+ * If we only have text parts, join them as a simple string for efficiency
246
+ */
247
+ if (contentBlocks.every((block) => block.type === "text")) return new HumanMessage({ content: contentBlocks.map((block) => block.text).join("") });
248
+ return new HumanMessage({ contentBlocks });
276
249
  }
250
+ /**
251
+ * Helper to check if a content item is a ToolResultPart
252
+ * @param item - The item to check.
253
+ * @returns True if the item is a ToolResultPart, false otherwise.
254
+ */
277
255
  function isToolResultPart(item) {
278
- return item != null && typeof item === "object" && "type" in item && item.type === "tool-result";
256
+ return item != null && typeof item === "object" && "type" in item && item.type === "tool-result";
279
257
  }
258
+ /**
259
+ * Processes a model stream chunk and emits UI message chunks.
260
+ * @param chunk - The AIMessageChunk to process.
261
+ * @param state - The state of the model stream.
262
+ * @param controller - The controller to use to emit the UI message chunks.
263
+ */
280
264
  function processModelChunk(chunk, state, controller) {
281
- if (!state.emittedImages) {
282
- state.emittedImages = /* @__PURE__ */ new Set();
283
- }
284
- if (!state.emittedSourceIds) {
285
- state.emittedSourceIds = /* @__PURE__ */ new Set();
286
- }
287
- if (chunk.id) {
288
- state.messageId = chunk.id;
289
- }
290
- const chunkObj = chunk;
291
- const additionalKwargs = chunkObj.additional_kwargs;
292
- const imageOutputs = extractImageOutputs(additionalKwargs);
293
- for (const imageOutput of imageOutputs) {
294
- if (imageOutput.result && !state.emittedImages.has(imageOutput.id)) {
295
- state.emittedImages.add(imageOutput.id);
296
- const mediaType = `image/${imageOutput.output_format || "png"}`;
297
- controller.enqueue({
298
- type: "file",
299
- mediaType,
300
- url: `data:${mediaType};base64,${imageOutput.result}`
301
- });
302
- state.started = true;
303
- }
304
- }
305
- const reasoning = extractReasoningFromContentBlocks(chunk) || extractReasoningFromValuesMessage(chunk);
306
- if (reasoning) {
307
- if (!state.reasoningStarted) {
308
- state.reasoningMessageId = state.messageId;
309
- controller.enqueue({ type: "reasoning-start", id: state.messageId });
310
- state.reasoningStarted = true;
311
- state.started = true;
312
- }
313
- controller.enqueue({
314
- type: "reasoning-delta",
315
- delta: reasoning,
316
- id: state.reasoningMessageId ?? state.messageId
317
- });
318
- }
319
- const text = typeof chunk.content === "string" ? chunk.content : Array.isArray(chunk.content) ? chunk.content.filter(
320
- (c) => typeof c === "object" && c !== null && "type" in c && c.type === "text"
321
- ).map((c) => c.text).join("") : "";
322
- if (text) {
323
- if (state.reasoningStarted && !state.textStarted) {
324
- controller.enqueue({
325
- type: "reasoning-end",
326
- id: state.reasoningMessageId ?? state.messageId
327
- });
328
- state.reasoningStarted = false;
329
- }
330
- if (!state.textStarted) {
331
- state.textMessageId = state.messageId;
332
- controller.enqueue({ type: "text-start", id: state.messageId });
333
- state.textStarted = true;
334
- state.started = true;
335
- }
336
- controller.enqueue({
337
- type: "text-delta",
338
- delta: text,
339
- id: state.textMessageId ?? state.messageId
340
- });
341
- }
342
- const citations = extractCitationsFromContentBlocks(chunk);
343
- if (citations.length > 0) {
344
- emitSourceChunks(
345
- citations,
346
- state.messageId,
347
- state.emittedSourceIds,
348
- controller
349
- );
350
- }
265
+ /**
266
+ * Initialize emittedImages set if not present
267
+ */
268
+ if (!state.emittedImages) state.emittedImages = /* @__PURE__ */ new Set();
269
+ if (!state.emittedSourceIds) state.emittedSourceIds = /* @__PURE__ */ new Set();
270
+ /**
271
+ * Get the message ID from the chunk if available
272
+ */
273
+ if (chunk.id) state.messageId = chunk.id;
274
+ const additionalKwargs = chunk.additional_kwargs;
275
+ const imageOutputs = extractImageOutputs(additionalKwargs);
276
+ for (const imageOutput of imageOutputs)
277
+ /**
278
+ * Only emit if we have image data and haven't emitted this image yet
279
+ */
280
+ if (imageOutput.result && !state.emittedImages.has(imageOutput.id)) {
281
+ state.emittedImages.add(imageOutput.id);
282
+ /**
283
+ * Emit as a file part using proper AI SDK multimodal format
284
+ */
285
+ const mediaType = `image/${imageOutput.output_format || "png"}`;
286
+ controller.enqueue({
287
+ type: "file",
288
+ mediaType,
289
+ url: `data:${mediaType};base64,${imageOutput.result}`
290
+ });
291
+ state.started = true;
292
+ }
293
+ /**
294
+ * Handle reasoning content from contentBlocks or response_metadata.output
295
+ * For direct model streams, we check both sources since there's no values event
296
+ * that would cause duplication (unlike LangGraph streams)
297
+ */
298
+ const reasoning = extractReasoningFromContentBlocks(chunk) || extractReasoningFromValuesMessage(chunk);
299
+ if (reasoning) {
300
+ if (!state.reasoningStarted) {
301
+ state.reasoningMessageId = state.messageId;
302
+ controller.enqueue({
303
+ type: "reasoning-start",
304
+ id: state.messageId
305
+ });
306
+ state.reasoningStarted = true;
307
+ state.started = true;
308
+ }
309
+ controller.enqueue({
310
+ type: "reasoning-delta",
311
+ delta: reasoning,
312
+ id: state.reasoningMessageId ?? state.messageId
313
+ });
314
+ }
315
+ /**
316
+ * Extract text content from AIMessageChunk
317
+ */
318
+ const text = typeof chunk.content === "string" ? chunk.content : Array.isArray(chunk.content) ? chunk.content.filter((c) => typeof c === "object" && c !== null && "type" in c && c.type === "text").map((c) => c.text).join("") : "";
319
+ if (text) {
320
+ /**
321
+ * If reasoning was streamed before text, close reasoning first
322
+ */
323
+ if (state.reasoningStarted && !state.textStarted) {
324
+ controller.enqueue({
325
+ type: "reasoning-end",
326
+ id: state.reasoningMessageId ?? state.messageId
327
+ });
328
+ state.reasoningStarted = false;
329
+ }
330
+ if (!state.textStarted) {
331
+ state.textMessageId = state.messageId;
332
+ controller.enqueue({
333
+ type: "text-start",
334
+ id: state.messageId
335
+ });
336
+ state.textStarted = true;
337
+ state.started = true;
338
+ }
339
+ controller.enqueue({
340
+ type: "text-delta",
341
+ delta: text,
342
+ id: state.textMessageId ?? state.messageId
343
+ });
344
+ }
345
+ const citations = extractCitationsFromContentBlocks(chunk);
346
+ if (citations.length > 0) emitSourceChunks(citations, state.messageId, state.emittedSourceIds, controller);
351
347
  }
348
+ /**
349
+ * Checks if a message is a plain object (not a LangChain class instance).
350
+ * LangChain class instances have a _getType method.
351
+ *
352
+ * @param msg - The message to check.
353
+ * @returns True if the message is a plain object, false otherwise.
354
+ */
352
355
  function isPlainMessageObject(msg) {
353
- if (msg == null || typeof msg !== "object") return false;
354
- return typeof msg._getType !== "function";
356
+ if (msg == null || typeof msg !== "object") return false;
357
+ /**
358
+ * LangChain class instances have _getType method
359
+ */
360
+ return typeof msg._getType !== "function";
355
361
  }
362
+ /**
363
+ * Extracts the actual message ID from a message.
364
+ * Handles both class instances (msg.id) and serialized LangChain messages (msg.kwargs.id).
365
+ *
366
+ * @param msg - The message to extract the ID from.
367
+ * @returns The message ID string, or undefined if not found.
368
+ */
356
369
  function getMessageId(msg) {
357
- if (msg == null || typeof msg !== "object") return void 0;
358
- const msgObj = msg;
359
- if (typeof msgObj.id === "string") {
360
- return msgObj.id;
361
- }
362
- if (msgObj.type === "constructor" && msgObj.kwargs && typeof msgObj.kwargs === "object") {
363
- const kwargs = msgObj.kwargs;
364
- if (typeof kwargs.id === "string") {
365
- return kwargs.id;
366
- }
367
- }
370
+ if (msg == null || typeof msg !== "object") return void 0;
371
+ const msgObj = msg;
372
+ /**
373
+ * For class instances, id is directly on the object
374
+ */
375
+ if (typeof msgObj.id === "string") return msgObj.id;
376
+ /**
377
+ * For serialized LangChain messages, id is in kwargs
378
+ */
379
+ if (msgObj.type === "constructor" && msgObj.kwargs && typeof msgObj.kwargs === "object") {
380
+ const kwargs = msgObj.kwargs;
381
+ if (typeof kwargs.id === "string") return kwargs.id;
382
+ }
368
383
  }
384
+ /**
385
+ * Checks if a message is an AIMessageChunk class instance, including instances
386
+ * created by a different LangChain module build.
387
+ */
369
388
  function isAIMessageChunkInstance(msg) {
370
- if (AIMessageChunk.isInstance(msg)) return true;
371
- if (msg == null || typeof msg !== "object") return false;
372
- const message = msg;
373
- return typeof message._getType === "function" && typeof message.concat === "function" && message._getType() === "ai";
389
+ if (AIMessageChunk.isInstance(msg)) return true;
390
+ if (msg == null || typeof msg !== "object") return false;
391
+ const message = msg;
392
+ return typeof message._getType === "function" && typeof message.concat === "function" && message._getType() === "ai";
374
393
  }
394
+ /**
395
+ * Checks if a message is an AI message chunk (works for both class instances and plain objects).
396
+ * For class instances, only AIMessageChunk is matched (not AIMessage).
397
+ * For plain objects from RemoteGraph API, matches type === 'ai' (TypeScript langchain-core)
398
+ * or type === 'AIMessageChunk' (Python langchain-core).
399
+ * For serialized LangChain messages, matches type === 'constructor' with AIMessageChunk in id path.
400
+ *
401
+ * @param msg - The message to check.
402
+ * @returns True if the message is an AI message chunk, false otherwise.
403
+ */
375
404
  function isAIMessageChunk(msg) {
376
- if (isAIMessageChunkInstance(msg)) return true;
377
- if (isPlainMessageObject(msg)) {
378
- const messageRecord = msg;
379
- if ("type" in messageRecord && (messageRecord.type === "ai" || messageRecord.type === "AIMessageChunk")) {
380
- return true;
381
- }
382
- if (messageRecord.type === "constructor" && Array.isArray(messageRecord.id) && (messageRecord.id.includes("AIMessageChunk") || messageRecord.id.includes("AIMessage"))) {
383
- return true;
384
- }
385
- }
386
- return false;
405
+ /**
406
+ * Actual AIMessageChunk class instance
407
+ */
408
+ if (isAIMessageChunkInstance(msg)) return true;
409
+ /**
410
+ * Plain object from RemoteGraph API (not a LangChain class instance)
411
+ */
412
+ if (isPlainMessageObject(msg)) {
413
+ const messageRecord = msg;
414
+ /**
415
+ * Direct type from RemoteGraph format. TypeScript langchain-core emits
416
+ * type === 'ai'; Python langchain-core emits type === 'AIMessageChunk'.
417
+ */
418
+ if ("type" in messageRecord && (messageRecord.type === "ai" || messageRecord.type === "AIMessageChunk")) return true;
419
+ /**
420
+ * Serialized LangChain message format: { lc: 1, type: "constructor", id: ["...", "AIMessageChunk"], kwargs: {...} }
421
+ */
422
+ if (messageRecord.type === "constructor" && Array.isArray(messageRecord.id) && (messageRecord.id.includes("AIMessageChunk") || messageRecord.id.includes("AIMessage"))) return true;
423
+ }
424
+ return false;
387
425
  }
426
+ /**
427
+ * Checks if a message is a Tool message (works for both class instances and plain objects).
428
+ *
429
+ * @param msg - The message to check.
430
+ * @returns True if the message is a Tool message, false otherwise.
431
+ */
388
432
  function isToolMessageType(msg) {
389
- if (ToolMessage.isInstance(msg)) return true;
390
- if (isPlainMessageObject(msg)) {
391
- const messageRecord = msg;
392
- if ("type" in messageRecord && messageRecord.type === "tool") return true;
393
- if (messageRecord.type === "constructor" && Array.isArray(messageRecord.id) && messageRecord.id.includes("ToolMessage")) {
394
- return true;
395
- }
396
- }
397
- return false;
433
+ if (ToolMessage.isInstance(msg)) return true;
434
+ /**
435
+ * Plain object from RemoteGraph API (not a LangChain class instance)
436
+ */
437
+ if (isPlainMessageObject(msg)) {
438
+ const messageRecord = msg;
439
+ /**
440
+ * Direct type === 'tool' (RemoteGraph format)
441
+ */
442
+ if ("type" in messageRecord && messageRecord.type === "tool") return true;
443
+ /**
444
+ * Serialized LangChain message format
445
+ */
446
+ if (messageRecord.type === "constructor" && Array.isArray(messageRecord.id) && messageRecord.id.includes("ToolMessage")) return true;
447
+ }
448
+ return false;
398
449
  }
450
+ /**
451
+ * Gets text content from a message (works for both class instances and plain objects).
452
+ *
453
+ * @param msg - The message to get the text from.
454
+ * @returns The text content of the message.
455
+ */
399
456
  function getMessageText(msg) {
400
- if (AIMessageChunk.isInstance(msg)) {
401
- return msg.text ?? "";
402
- }
403
- if (msg == null || typeof msg !== "object") return "";
404
- const msgObj = msg;
405
- const dataSource = msgObj.type === "constructor" && msgObj.kwargs && typeof msgObj.kwargs === "object" ? msgObj.kwargs : msgObj;
406
- if ("content" in dataSource) {
407
- const content = dataSource.content;
408
- if (typeof content === "string") {
409
- return content;
410
- }
411
- if (Array.isArray(content)) {
412
- return content.filter(
413
- (block) => block != null && typeof block === "object" && block.type === "text" && typeof block.text === "string"
414
- ).map((block) => block.text).join("");
415
- }
416
- return "";
417
- }
418
- return "";
457
+ if (AIMessageChunk.isInstance(msg)) return msg.text ?? "";
458
+ if (msg == null || typeof msg !== "object") return "";
459
+ const msgObj = msg;
460
+ const dataSource = msgObj.type === "constructor" && msgObj.kwargs && typeof msgObj.kwargs === "object" ? msgObj.kwargs : msgObj;
461
+ if ("content" in dataSource) {
462
+ const content = dataSource.content;
463
+ /**
464
+ * Handle string content
465
+ */
466
+ if (typeof content === "string") return content;
467
+ /**
468
+ * Handle array of content blocks (e.g., [{ type: 'text', text: 'The', index: 0 }])
469
+ */
470
+ if (Array.isArray(content)) return content.filter((block) => block != null && typeof block === "object" && block.type === "text" && typeof block.text === "string").map((block) => block.text).join("");
471
+ return "";
472
+ }
473
+ return "";
419
474
  }
475
+ /**
476
+ * Checks if an object is a reasoning content block
477
+ *
478
+ * @param obj - The object to check.
479
+ * @returns True if the object is a reasoning content block, false otherwise.
480
+ */
420
481
  function isReasoningContentBlock(obj) {
421
- return obj != null && typeof obj === "object" && "type" in obj && obj.type === "reasoning" && "reasoning" in obj && typeof obj.reasoning === "string";
482
+ return obj != null && typeof obj === "object" && "type" in obj && obj.type === "reasoning" && "reasoning" in obj && typeof obj.reasoning === "string";
422
483
  }
484
+ /**
485
+ * Checks if an object is a thinking content block (Anthropic-style)
486
+ *
487
+ * @param obj - The object to check.
488
+ * @returns True if the object is a thinking content block, false otherwise.
489
+ */
423
490
  function isThinkingContentBlock(obj) {
424
- return obj != null && typeof obj === "object" && "type" in obj && obj.type === "thinking" && "thinking" in obj && typeof obj.thinking === "string";
491
+ return obj != null && typeof obj === "object" && "type" in obj && obj.type === "thinking" && "thinking" in obj && typeof obj.thinking === "string";
425
492
  }
493
+ /**
494
+ * Checks if an object is a GPT-5 reasoning output block
495
+ */
426
496
  function isGPT5ReasoningOutput(obj) {
427
- return obj != null && typeof obj === "object" && "type" in obj && obj.type === "reasoning" && "summary" in obj && Array.isArray(obj.summary);
497
+ return obj != null && typeof obj === "object" && "type" in obj && obj.type === "reasoning" && "summary" in obj && Array.isArray(obj.summary);
428
498
  }
499
+ /**
500
+ * Extracts the reasoning block ID from a message (GPT-5 format).
501
+ * This ID is consistent across streaming and values events.
502
+ * Handles both class instances and serialized LangChain message objects.
503
+ *
504
+ * @param msg - The message to extract the reasoning ID from.
505
+ * @returns The reasoning block ID if found, undefined otherwise.
506
+ */
429
507
  function extractReasoningId(msg) {
430
- if (msg == null || typeof msg !== "object") return void 0;
431
- const msgObj = msg;
432
- const kwargs = msgObj.kwargs && typeof msgObj.kwargs === "object" ? msgObj.kwargs : msgObj;
433
- const additionalKwargs = kwargs.additional_kwargs;
434
- if (additionalKwargs?.reasoning?.id) {
435
- return additionalKwargs.reasoning.id;
436
- }
437
- const responseMetadata = kwargs.response_metadata;
438
- if (responseMetadata && Array.isArray(responseMetadata.output)) {
439
- for (const item of responseMetadata.output) {
440
- if (isGPT5ReasoningOutput(item)) {
441
- return item.id;
442
- }
443
- }
444
- }
508
+ if (msg == null || typeof msg !== "object") return void 0;
509
+ const msgObj = msg;
510
+ const kwargs = msgObj.kwargs && typeof msgObj.kwargs === "object" ? msgObj.kwargs : msgObj;
511
+ const additionalKwargs = kwargs.additional_kwargs;
512
+ if (additionalKwargs?.reasoning?.id) return additionalKwargs.reasoning.id;
513
+ const responseMetadata = kwargs.response_metadata;
514
+ if (responseMetadata && Array.isArray(responseMetadata.output)) {
515
+ for (const item of responseMetadata.output) if (isGPT5ReasoningOutput(item)) return item.id;
516
+ }
445
517
  }
518
+ /**
519
+ * Extracts reasoning content from contentBlocks or additional_kwargs.reasoning.summary
520
+ *
521
+ * IMPORTANT: This function is designed for STREAMING chunks where content is delta-based.
522
+ * It does NOT extract from response_metadata.output because that contains accumulated
523
+ * content (not deltas) and would cause duplication during streaming.
524
+ *
525
+ * For non-streaming/values events, use extractReasoningFromValuesMessage instead.
526
+ *
527
+ * Handles both class instances and serialized LangChain message objects.
528
+ *
529
+ * @param msg - The message to extract reasoning from.
530
+ * @returns The reasoning text if found, undefined otherwise.
531
+ */
446
532
  function extractReasoningFromContentBlocks(msg) {
447
- if (msg == null || typeof msg !== "object") return void 0;
448
- const msgObj = msg;
449
- const kwargs = msgObj.kwargs && typeof msgObj.kwargs === "object" ? msgObj.kwargs : msgObj;
450
- const contentBlocks = kwargs.contentBlocks;
451
- if (Array.isArray(contentBlocks)) {
452
- const reasoningParts = [];
453
- for (const block of contentBlocks) {
454
- if (isReasoningContentBlock(block)) {
455
- reasoningParts.push(block.reasoning);
456
- } else if (isThinkingContentBlock(block)) {
457
- reasoningParts.push(block.thinking);
458
- }
459
- }
460
- if (reasoningParts.length > 0) {
461
- return reasoningParts.join("");
462
- }
463
- }
464
- const additionalKwargs = kwargs.additional_kwargs;
465
- if (additionalKwargs?.reasoning && Array.isArray(additionalKwargs.reasoning.summary)) {
466
- const reasoningParts = [];
467
- for (const summaryItem of additionalKwargs.reasoning.summary) {
468
- if (typeof summaryItem === "object" && summaryItem !== null && "text" in summaryItem && typeof summaryItem.text === "string") {
469
- reasoningParts.push(summaryItem.text);
470
- }
471
- }
472
- if (reasoningParts.length > 0) {
473
- return reasoningParts.join("");
474
- }
475
- }
533
+ if (msg == null || typeof msg !== "object") return void 0;
534
+ const msgObj = msg;
535
+ const kwargs = msgObj.kwargs && typeof msgObj.kwargs === "object" ? msgObj.kwargs : msgObj;
536
+ const contentBlocks = kwargs.contentBlocks;
537
+ if (Array.isArray(contentBlocks)) {
538
+ const reasoningParts = [];
539
+ for (const block of contentBlocks) if (isReasoningContentBlock(block)) reasoningParts.push(block.reasoning);
540
+ else if (isThinkingContentBlock(block)) reasoningParts.push(block.thinking);
541
+ if (reasoningParts.length > 0) return reasoningParts.join("");
542
+ }
543
+ const additionalKwargs = kwargs.additional_kwargs;
544
+ if (additionalKwargs?.reasoning && Array.isArray(additionalKwargs.reasoning.summary)) {
545
+ const reasoningParts = [];
546
+ for (const summaryItem of additionalKwargs.reasoning.summary) if (typeof summaryItem === "object" && summaryItem !== null && "text" in summaryItem && typeof summaryItem.text === "string") reasoningParts.push(summaryItem.text);
547
+ if (reasoningParts.length > 0) return reasoningParts.join("");
548
+ }
476
549
  }
550
+ /**
551
+ * Extracts reasoning content from a values event message.
552
+ * This checks response_metadata.output which contains the full accumulated reasoning.
553
+ *
554
+ * @param msg - The message to extract reasoning from.
555
+ * @returns The reasoning text if found, undefined otherwise.
556
+ */
477
557
  function extractReasoningFromValuesMessage(msg) {
478
- if (msg == null || typeof msg !== "object") return void 0;
479
- const msgObj = msg;
480
- const kwargs = msgObj.kwargs && typeof msgObj.kwargs === "object" ? msgObj.kwargs : msgObj;
481
- const responseMetadata = kwargs.response_metadata;
482
- if (responseMetadata && Array.isArray(responseMetadata.output)) {
483
- const reasoningParts = [];
484
- for (const item of responseMetadata.output) {
485
- if (isGPT5ReasoningOutput(item)) {
486
- for (const summaryItem of item.summary) {
487
- if (typeof summaryItem === "object" && summaryItem !== null) {
488
- const text = summaryItem.text;
489
- if (typeof text === "string" && text) {
490
- reasoningParts.push(text);
491
- }
492
- }
493
- }
494
- }
495
- }
496
- if (reasoningParts.length > 0) {
497
- return reasoningParts.join("");
498
- }
499
- }
500
- const additionalKwargs = kwargs.additional_kwargs;
501
- if (additionalKwargs?.reasoning && Array.isArray(additionalKwargs.reasoning.summary)) {
502
- const reasoningParts = [];
503
- for (const summaryItem of additionalKwargs.reasoning.summary) {
504
- if (typeof summaryItem === "object" && summaryItem !== null && "text" in summaryItem && typeof summaryItem.text === "string") {
505
- reasoningParts.push(summaryItem.text);
506
- }
507
- }
508
- if (reasoningParts.length > 0) {
509
- return reasoningParts.join("");
510
- }
511
- }
558
+ if (msg == null || typeof msg !== "object") return void 0;
559
+ const msgObj = msg;
560
+ const kwargs = msgObj.kwargs && typeof msgObj.kwargs === "object" ? msgObj.kwargs : msgObj;
561
+ const responseMetadata = kwargs.response_metadata;
562
+ if (responseMetadata && Array.isArray(responseMetadata.output)) {
563
+ const reasoningParts = [];
564
+ for (const item of responseMetadata.output) if (isGPT5ReasoningOutput(item)) {
565
+ for (const summaryItem of item.summary) if (typeof summaryItem === "object" && summaryItem !== null) {
566
+ const text = summaryItem.text;
567
+ if (typeof text === "string" && text) reasoningParts.push(text);
568
+ }
569
+ }
570
+ if (reasoningParts.length > 0) return reasoningParts.join("");
571
+ }
572
+ const additionalKwargs = kwargs.additional_kwargs;
573
+ if (additionalKwargs?.reasoning && Array.isArray(additionalKwargs.reasoning.summary)) {
574
+ const reasoningParts = [];
575
+ for (const summaryItem of additionalKwargs.reasoning.summary) if (typeof summaryItem === "object" && summaryItem !== null && "text" in summaryItem && typeof summaryItem.text === "string") reasoningParts.push(summaryItem.text);
576
+ if (reasoningParts.length > 0) return reasoningParts.join("");
577
+ }
512
578
  }
513
579
  function isCitationContentBlock(obj) {
514
- return obj != null && typeof obj === "object" && "type" in obj && obj.type === "citation";
580
+ return obj != null && typeof obj === "object" && "type" in obj && obj.type === "citation";
515
581
  }
582
+ /**
583
+ * LangChain Core standardizes citations as `{ type: 'citation', ... }` entries in
584
+ * the `annotations` array of a text content block. The text extractors elsewhere
585
+ * in this file only read the `text` field, so without this the citations would be
586
+ * dropped instead of surfaced as AI SDK source parts.
587
+ *
588
+ * Handles both class instances and serialized LangChain message objects.
589
+ */
516
590
  function extractCitationsFromContentBlocks(msg) {
517
- if (msg == null || typeof msg !== "object") return [];
518
- const msgObj = msg;
519
- const kwargs = msgObj.kwargs && typeof msgObj.kwargs === "object" ? msgObj.kwargs : msgObj;
520
- const contentBlocks = kwargs.contentBlocks;
521
- const content = kwargs.content;
522
- const blockSources = Array.isArray(contentBlocks) ? contentBlocks : Array.isArray(content) ? content : [];
523
- const citations = [];
524
- for (const block of blockSources) {
525
- if (block == null || typeof block !== "object") continue;
526
- const annotations = block.annotations;
527
- if (!Array.isArray(annotations)) continue;
528
- for (const annotation of annotations) {
529
- if (!isCitationContentBlock(annotation)) continue;
530
- citations.push({
531
- url: annotation.url,
532
- title: annotation.title,
533
- source: annotation.source,
534
- citedText: annotation.citedText,
535
- startIndex: annotation.startIndex,
536
- endIndex: annotation.endIndex
537
- });
538
- }
539
- }
540
- return citations;
591
+ if (msg == null || typeof msg !== "object") return [];
592
+ const msgObj = msg;
593
+ const kwargs = msgObj.kwargs && typeof msgObj.kwargs === "object" ? msgObj.kwargs : msgObj;
594
+ const contentBlocks = kwargs.contentBlocks;
595
+ const content = kwargs.content;
596
+ const blockSources = Array.isArray(contentBlocks) ? contentBlocks : Array.isArray(content) ? content : [];
597
+ const citations = [];
598
+ for (const block of blockSources) {
599
+ if (block == null || typeof block !== "object") continue;
600
+ const annotations = block.annotations;
601
+ if (!Array.isArray(annotations)) continue;
602
+ for (const annotation of annotations) {
603
+ if (!isCitationContentBlock(annotation)) continue;
604
+ citations.push({
605
+ url: annotation.url,
606
+ title: annotation.title,
607
+ source: annotation.source,
608
+ citedText: annotation.citedText,
609
+ startIndex: annotation.startIndex,
610
+ endIndex: annotation.endIndex
611
+ });
612
+ }
613
+ }
614
+ return citations;
541
615
  }
542
616
  function buildSourceProviderMetadata(citation) {
543
- const langchain = {};
544
- if (typeof citation.citedText === "string") {
545
- langchain.citedText = citation.citedText;
546
- }
547
- if (typeof citation.startIndex === "number") {
548
- langchain.startIndex = citation.startIndex;
549
- }
550
- if (typeof citation.endIndex === "number") {
551
- langchain.endIndex = citation.endIndex;
552
- }
553
- if (typeof citation.source === "string") {
554
- langchain.source = citation.source;
555
- }
556
- if (Object.keys(langchain).length === 0) {
557
- return void 0;
558
- }
559
- return { langchain };
617
+ const langchain = {};
618
+ if (typeof citation.citedText === "string") langchain.citedText = citation.citedText;
619
+ if (typeof citation.startIndex === "number") langchain.startIndex = citation.startIndex;
620
+ if (typeof citation.endIndex === "number") langchain.endIndex = citation.endIndex;
621
+ if (typeof citation.source === "string") langchain.source = citation.source;
622
+ if (Object.keys(langchain).length === 0) return;
623
+ return { langchain };
560
624
  }
625
+ /**
626
+ * Emits AI SDK source chunks (`source-url` / `source-document`) for the given
627
+ * citations.
628
+ *
629
+ * Citations with a `url` become `source-url` parts keyed by that url; the rest
630
+ * become `source-document` parts. Citation metadata that the source parts cannot
631
+ * represent natively (`citedText`, `startIndex`, `endIndex`, `source`) is preserved
632
+ * under `providerMetadata.langchain`.
633
+ *
634
+ * `emittedSourceIds` dedupes across the LangGraph messages and values events, which
635
+ * can each surface the same citation. URL-less citations are keyed by their content
636
+ * rather than position, so a differing citation subset/order between those two events
637
+ * does not cause an id collision.
638
+ */
561
639
  function emitSourceChunks(citations, messageId, emittedSourceIds, controller) {
562
- for (const citation of citations) {
563
- if (citation.url) {
564
- if (emittedSourceIds.has(citation.url)) continue;
565
- emittedSourceIds.add(citation.url);
566
- const providerMetadata2 = buildSourceProviderMetadata(citation);
567
- controller.enqueue({
568
- type: "source-url",
569
- sourceId: citation.url,
570
- url: citation.url,
571
- ...citation.title ? { title: citation.title } : {},
572
- ...providerMetadata2 ? { providerMetadata: providerMetadata2 } : {}
573
- });
574
- continue;
575
- }
576
- const title = citation.title ?? citation.source;
577
- if (!title) continue;
578
- const sourceId = `${messageId}:${title}:${citation.citedText ?? ""}:${citation.startIndex ?? ""}:${citation.endIndex ?? ""}`;
579
- if (emittedSourceIds.has(sourceId)) continue;
580
- emittedSourceIds.add(sourceId);
581
- const providerMetadata = buildSourceProviderMetadata(citation);
582
- controller.enqueue({
583
- type: "source-document",
584
- sourceId,
585
- mediaType: "text/plain",
586
- title,
587
- ...providerMetadata ? { providerMetadata } : {}
588
- });
589
- }
640
+ for (const citation of citations) {
641
+ if (citation.url) {
642
+ if (emittedSourceIds.has(citation.url)) continue;
643
+ emittedSourceIds.add(citation.url);
644
+ const providerMetadata = buildSourceProviderMetadata(citation);
645
+ controller.enqueue({
646
+ type: "source-url",
647
+ sourceId: citation.url,
648
+ url: citation.url,
649
+ ...citation.title ? { title: citation.title } : {},
650
+ ...providerMetadata ? { providerMetadata } : {}
651
+ });
652
+ continue;
653
+ }
654
+ const title = citation.title ?? citation.source;
655
+ if (!title) continue;
656
+ const sourceId = `${messageId}:${title}:${citation.citedText ?? ""}:${citation.startIndex ?? ""}:${citation.endIndex ?? ""}`;
657
+ if (emittedSourceIds.has(sourceId)) continue;
658
+ emittedSourceIds.add(sourceId);
659
+ const providerMetadata = buildSourceProviderMetadata(citation);
660
+ controller.enqueue({
661
+ type: "source-document",
662
+ sourceId,
663
+ mediaType: "text/plain",
664
+ title,
665
+ ...providerMetadata ? { providerMetadata } : {}
666
+ });
667
+ }
590
668
  }
669
+ /**
670
+ * Checks if an object is an image generation output
671
+ *
672
+ * @param obj - The object to check.
673
+ * @returns True if the object is an image generation output, false otherwise.
674
+ */
591
675
  function isImageGenerationOutput(obj) {
592
- return obj != null && typeof obj === "object" && "type" in obj && obj.type === "image_generation_call";
676
+ return obj != null && typeof obj === "object" && "type" in obj && obj.type === "image_generation_call";
593
677
  }
678
+ /**
679
+ * Extracts image generation outputs from `additional_kwargs`
680
+ *
681
+ * @param additionalKwargs - The additional kwargs to extract the image generation outputs from.
682
+ * @returns The image generation outputs.
683
+ */
594
684
  function extractImageOutputs(additionalKwargs) {
595
- if (!additionalKwargs) return [];
596
- const toolOutputs = additionalKwargs.tool_outputs;
597
- if (!Array.isArray(toolOutputs)) return [];
598
- return toolOutputs.filter(isImageGenerationOutput);
685
+ if (!additionalKwargs) return [];
686
+ const toolOutputs = additionalKwargs.tool_outputs;
687
+ if (!Array.isArray(toolOutputs)) return [];
688
+ return toolOutputs.filter(isImageGenerationOutput);
599
689
  }
600
690
  function formatToolError(error) {
601
- if (error instanceof Error) return error.message;
602
- if (typeof error === "string") return error;
603
- try {
604
- const serialized = JSON.stringify(error);
605
- return serialized ?? String(error);
606
- } catch {
607
- return String(error);
608
- }
691
+ if (error instanceof Error) return error.message;
692
+ if (typeof error === "string") return error;
693
+ try {
694
+ return JSON.stringify(error) ?? String(error);
695
+ } catch {
696
+ return String(error);
697
+ }
609
698
  }
699
+ /**
700
+ * Returns per-message bookkeeping, creating it on first use.
701
+ * Map keys keep remote-controlled message IDs like "__proto__" from touching Object.prototype.
702
+ */
610
703
  function getOrCreateMessageSeen(messageSeen, msgId) {
611
- let seen = messageSeen.get(msgId);
612
- if (!seen) {
613
- seen = {};
614
- messageSeen.set(msgId, seen);
615
- }
616
- return seen;
704
+ let seen = messageSeen.get(msgId);
705
+ if (!seen) {
706
+ seen = {};
707
+ messageSeen.set(msgId, seen);
708
+ }
709
+ return seen;
617
710
  }
711
+ /**
712
+ * Returns the per-index tool call metadata for a message, creating it on first use.
713
+ * Later streamed chunks use this to recover tool call IDs/names from earlier chunks.
714
+ */
618
715
  function getOrCreateToolCallInfoByIndex(toolCallInfoByIndex, msgId) {
619
- let toolCallInfo = toolCallInfoByIndex.get(msgId);
620
- if (!toolCallInfo) {
621
- toolCallInfo = /* @__PURE__ */ new Map();
622
- toolCallInfoByIndex.set(msgId, toolCallInfo);
623
- }
624
- return toolCallInfo;
716
+ let toolCallInfo = toolCallInfoByIndex.get(msgId);
717
+ if (!toolCallInfo) {
718
+ toolCallInfo = /* @__PURE__ */ new Map();
719
+ toolCallInfoByIndex.set(msgId, toolCallInfo);
720
+ }
721
+ return toolCallInfo;
625
722
  }
723
+ /**
724
+ * Normalizes legacy tuples without a namespace and explicit empty root
725
+ * namespaces to the same key.
726
+ */
626
727
  function getLangGraphNamespaceKey(namespace) {
627
- return JSON.stringify(namespace ?? []);
728
+ return JSON.stringify(namespace ?? []);
628
729
  }
629
730
  function getOrCreateNamespaceSet(setsByNamespace, namespace) {
630
- let values = setsByNamespace.get(namespace);
631
- if (!values) {
632
- values = /* @__PURE__ */ new Set();
633
- setsByNamespace.set(namespace, values);
634
- }
635
- return values;
731
+ let values = setsByNamespace.get(namespace);
732
+ if (!values) {
733
+ values = /* @__PURE__ */ new Set();
734
+ setsByNamespace.set(namespace, values);
735
+ }
736
+ return values;
636
737
  }
637
738
  function hasEmittedToolCallInCurrentStep(state, toolCallId, namespace) {
638
- return !state.currentStepsByNamespace.has(namespace) ? state.emittedToolCalls.has(toolCallId) : state.emittedToolCallsInCurrentStepByNamespace.get(namespace)?.has(toolCallId) === true;
739
+ return !state.currentStepsByNamespace.has(namespace) ? state.emittedToolCalls.has(toolCallId) : state.emittedToolCallsInCurrentStepByNamespace.get(namespace)?.has(toolCallId) === true;
639
740
  }
640
741
  function markToolCallEmitted(state, toolCallId, namespace) {
641
- state.emittedToolCalls.add(toolCallId);
642
- if (state.currentStepsByNamespace.has(namespace)) {
643
- getOrCreateNamespaceSet(
644
- state.emittedToolCallsInCurrentStepByNamespace,
645
- namespace
646
- ).add(toolCallId);
647
- }
742
+ state.emittedToolCalls.add(toolCallId);
743
+ if (state.currentStepsByNamespace.has(namespace)) getOrCreateNamespaceSet(state.emittedToolCallsInCurrentStepByNamespace, namespace).add(toolCallId);
648
744
  }
649
745
  function markToolCallStarted(state, toolCallId, namespace) {
650
- markToolCallEmitted(state, toolCallId, namespace);
651
- getOrCreateNamespaceSet(state.unfinishedToolCallsByNamespace, namespace).add(
652
- toolCallId
653
- );
746
+ markToolCallEmitted(state, toolCallId, namespace);
747
+ getOrCreateNamespaceSet(state.unfinishedToolCallsByNamespace, namespace).add(toolCallId);
654
748
  }
655
749
  function hasUnfinishedToolCall(state, toolCallId, namespace) {
656
- return state.unfinishedToolCallsByNamespace.get(namespace)?.has(toolCallId) === true;
750
+ return state.unfinishedToolCallsByNamespace.get(namespace)?.has(toolCallId) === true;
657
751
  }
658
752
  function hasEmittedToolInputInCurrentStep(state, toolCallId, namespace) {
659
- return !state.currentStepsByNamespace.has(namespace) ? state.emittedToolInputs.has(toolCallId) : state.emittedToolInputsInCurrentStepByNamespace.get(namespace)?.has(toolCallId) === true;
753
+ return !state.currentStepsByNamespace.has(namespace) ? state.emittedToolInputs.has(toolCallId) : state.emittedToolInputsInCurrentStepByNamespace.get(namespace)?.has(toolCallId) === true;
660
754
  }
661
755
  function markToolInputEmitted(state, toolCallId, namespace) {
662
- state.emittedToolInputs.add(toolCallId);
663
- if (state.currentStepsByNamespace.has(namespace)) {
664
- getOrCreateNamespaceSet(
665
- state.emittedToolInputsInCurrentStepByNamespace,
666
- namespace
667
- ).add(toolCallId);
668
- }
756
+ state.emittedToolInputs.add(toolCallId);
757
+ if (state.currentStepsByNamespace.has(namespace)) getOrCreateNamespaceSet(state.emittedToolInputsInCurrentStepByNamespace, namespace).add(toolCallId);
669
758
  }
670
759
  function hasEmittedToolOutputInCurrentStep(state, toolCallId, namespace) {
671
- return !state.currentStepsByNamespace.has(namespace) ? state.emittedToolOutputCallIds.has(toolCallId) : state.emittedToolOutputsInCurrentStepByNamespace.get(namespace)?.has(toolCallId) === true;
760
+ return !state.currentStepsByNamespace.has(namespace) ? state.emittedToolOutputCallIds.has(toolCallId) : state.emittedToolOutputsInCurrentStepByNamespace.get(namespace)?.has(toolCallId) === true;
672
761
  }
673
762
  function markToolOutputEmitted(state, toolCallId, namespace) {
674
- state.emittedToolOutputCallIds.add(toolCallId);
675
- state.unfinishedToolCallsByNamespace.get(namespace)?.delete(toolCallId);
676
- if (state.currentStepsByNamespace.has(namespace)) {
677
- getOrCreateNamespaceSet(
678
- state.emittedToolOutputsInCurrentStepByNamespace,
679
- namespace
680
- ).add(toolCallId);
681
- }
763
+ state.emittedToolOutputCallIds.add(toolCallId);
764
+ state.unfinishedToolCallsByNamespace.get(namespace)?.delete(toolCallId);
765
+ if (state.currentStepsByNamespace.has(namespace)) getOrCreateNamespaceSet(state.emittedToolOutputsInCurrentStepByNamespace, namespace).add(toolCallId);
682
766
  }
683
767
  function findMessageCurrentStepNamespace(state, messageId) {
684
- for (const [
685
- namespace,
686
- messageIds
687
- ] of state.messageIdsInCurrentStepByNamespace) {
688
- if (messageIds.has(messageId)) {
689
- return namespace;
690
- }
691
- }
768
+ for (const [namespace, messageIds] of state.messageIdsInCurrentStepByNamespace) if (messageIds.has(messageId)) return namespace;
692
769
  }
770
+ /**
771
+ * Ends and clears message state for an advancing namespace. Returns whether
772
+ * another namespace still has active text or reasoning that would be
773
+ * invalidated by a global finish-step chunk.
774
+ */
693
775
  function closeStepNamespaceMessages(state, stepNamespace, controller) {
694
- let hasConcurrentMessageParts = false;
695
- for (const [id, seen] of state.messageSeen) {
696
- const messageNamespace = getLangGraphNamespaceKey(
697
- state.messageNamespaces.get(id)
698
- );
699
- if (messageNamespace !== stepNamespace) {
700
- if (seen.text || seen.reasoning) {
701
- hasConcurrentMessageParts = true;
702
- }
703
- continue;
704
- }
705
- if (seen.text) {
706
- controller.enqueue({ type: "text-end", id });
707
- }
708
- if (seen.reasoning) {
709
- controller.enqueue({ type: "reasoning-end", id });
710
- }
711
- state.messageSeen.delete(id);
712
- state.messageNamespaces.delete(id);
713
- state.messageConcat.delete(id);
714
- state.messageReasoningIds.delete(id);
715
- }
716
- return hasConcurrentMessageParts;
776
+ let hasConcurrentMessageParts = false;
777
+ for (const [id, seen] of state.messageSeen) {
778
+ if (getLangGraphNamespaceKey(state.messageNamespaces.get(id)) !== stepNamespace) {
779
+ if (seen.text || seen.reasoning) hasConcurrentMessageParts = true;
780
+ continue;
781
+ }
782
+ if (seen.text) controller.enqueue({
783
+ type: "text-end",
784
+ id
785
+ });
786
+ if (seen.reasoning) controller.enqueue({
787
+ type: "reasoning-end",
788
+ id
789
+ });
790
+ state.messageSeen.delete(id);
791
+ state.messageNamespaces.delete(id);
792
+ state.messageConcat.delete(id);
793
+ state.messageReasoningIds.delete(id);
794
+ }
795
+ return hasConcurrentMessageParts;
717
796
  }
797
+ /**
798
+ * Processes a LangGraph event and emits UI message chunks.
799
+ *
800
+ * @param event - The event to process.
801
+ * @param state - The state of the LangGraph event.
802
+ * @param controller - The controller to use to emit the UI message chunks.
803
+ */
718
804
  function processLangGraphEvent(event, state, controller) {
719
- const {
720
- messageSeen,
721
- messageConcat,
722
- emittedToolCalls,
723
- emittedImages,
724
- emittedReasoningIds,
725
- messageReasoningIds,
726
- toolCallInfoByIndex,
727
- emittedToolCallsByKey
728
- } = state;
729
- const [rawNamespace, type, data] = parseLangGraphEvent(event);
730
- const namespace = Array.isArray(rawNamespace) && rawNamespace.every((segment) => typeof segment === "string") ? rawNamespace : void 0;
731
- const eventNamespace = getLangGraphNamespaceKey(namespace);
732
- controller = createLangGraphNamespaceController(controller, state, namespace);
733
- switch (type) {
734
- case "custom": {
735
- let customTypeName = "custom";
736
- let partId;
737
- if (data != null && typeof data === "object" && !Array.isArray(data)) {
738
- const dataObj = data;
739
- if (typeof dataObj.type === "string" && dataObj.type) {
740
- customTypeName = dataObj.type;
741
- }
742
- if (typeof dataObj.id === "string" && dataObj.id) {
743
- partId = dataObj.id;
744
- }
745
- }
746
- controller.enqueue({
747
- type: `data-${customTypeName}`,
748
- id: partId,
749
- transient: partId == null,
750
- data
751
- });
752
- break;
753
- }
754
- case "messages": {
755
- const [rawMsg, metadata] = data;
756
- const msg = rawMsg;
757
- const msgId = getMessageId(msg);
758
- if (!msgId) return;
759
- if (namespace !== void 0) {
760
- state.messageNamespaces.set(msgId, namespace);
761
- }
762
- let startedUIReducerStep = false;
763
- const langgraphStep = typeof metadata?.langgraph_step === "number" ? metadata.langgraph_step : null;
764
- if (langgraphStep !== null) {
765
- const currentStep = state.currentStepsByNamespace.get(eventNamespace) ?? null;
766
- if (currentStep === null) {
767
- if (state.currentStepsByNamespace.size === 0) {
768
- controller.enqueue({ type: "start-step" });
769
- startedUIReducerStep = true;
770
- }
771
- state.currentStepsByNamespace.set(eventNamespace, langgraphStep);
772
- state.messageIdsInCurrentStepByNamespace.set(
773
- eventNamespace,
774
- /* @__PURE__ */ new Set()
775
- );
776
- state.emittedToolCallsInCurrentStepByNamespace.set(
777
- eventNamespace,
778
- /* @__PURE__ */ new Set()
779
- );
780
- state.emittedToolInputsInCurrentStepByNamespace.set(
781
- eventNamespace,
782
- /* @__PURE__ */ new Set()
783
- );
784
- state.emittedToolOutputsInCurrentStepByNamespace.set(
785
- eventNamespace,
786
- /* @__PURE__ */ new Set()
787
- );
788
- } else if (langgraphStep !== currentStep) {
789
- const hasConcurrentMessageParts = closeStepNamespaceMessages(
790
- state,
791
- eventNamespace,
792
- controller
793
- );
794
- if (!hasConcurrentMessageParts) {
795
- controller.enqueue({ type: "finish-step" });
796
- }
797
- controller.enqueue({ type: "start-step" });
798
- startedUIReducerStep = true;
799
- state.currentStepsByNamespace.set(eventNamespace, langgraphStep);
800
- state.messageIdsInCurrentStepByNamespace.set(
801
- eventNamespace,
802
- /* @__PURE__ */ new Set()
803
- );
804
- state.emittedToolCallsInCurrentStepByNamespace.set(
805
- eventNamespace,
806
- /* @__PURE__ */ new Set()
807
- );
808
- state.emittedToolInputsInCurrentStepByNamespace.set(
809
- eventNamespace,
810
- /* @__PURE__ */ new Set()
811
- );
812
- state.emittedToolOutputsInCurrentStepByNamespace.set(
813
- eventNamespace,
814
- /* @__PURE__ */ new Set()
815
- );
816
- }
817
- }
818
- if (state.currentStepsByNamespace.has(eventNamespace)) {
819
- getOrCreateNamespaceSet(
820
- state.messageIdsInCurrentStepByNamespace,
821
- eventNamespace
822
- ).add(msgId);
823
- }
824
- if (isAIMessageChunkInstance(msg)) {
825
- const existingMessage = messageConcat.get(msgId);
826
- if (existingMessage) {
827
- messageConcat.set(
828
- msgId,
829
- existingMessage.concat(msg)
830
- );
831
- } else {
832
- messageConcat.set(msgId, msg);
833
- }
834
- }
835
- if (isAIMessageChunk(msg)) {
836
- const concatChunk = messageConcat.get(msgId);
837
- const msgObj = msg;
838
- const dataSource = msgObj.type === "constructor" && msgObj.kwargs && typeof msgObj.kwargs === "object" ? msgObj.kwargs : msgObj;
839
- const additionalKwargs = dataSource.additional_kwargs;
840
- const imageOutputs = extractImageOutputs(additionalKwargs);
841
- for (const imageOutput of imageOutputs) {
842
- if (imageOutput.result && !emittedImages.has(imageOutput.id)) {
843
- emittedImages.add(imageOutput.id);
844
- const mediaType = `image/${imageOutput.output_format || "png"}`;
845
- controller.enqueue({
846
- type: "file",
847
- mediaType,
848
- url: `data:${mediaType};base64,${imageOutput.result}`
849
- });
850
- }
851
- }
852
- const toolCallChunks = dataSource.tool_call_chunks;
853
- if (toolCallChunks?.length) {
854
- for (const toolCallChunk of toolCallChunks) {
855
- const toolCallIndex = toolCallChunk.index ?? 0;
856
- if (toolCallChunk.id) {
857
- getOrCreateToolCallInfoByIndex(toolCallInfoByIndex, msgId).set(
858
- toolCallIndex,
859
- {
860
- id: toolCallChunk.id,
861
- name: toolCallChunk.name || concatChunk?.tool_call_chunks?.[toolCallIndex]?.name || "unknown"
862
- }
863
- );
864
- }
865
- const storedToolCallInfo = toolCallInfoByIndex.get(msgId)?.get(toolCallIndex);
866
- const toolCallId = toolCallChunk.id || storedToolCallInfo?.id || concatChunk?.tool_call_chunks?.[toolCallIndex]?.id;
867
- if (!toolCallId) {
868
- continue;
869
- }
870
- const toolName = toolCallChunk.name || storedToolCallInfo?.name || concatChunk?.tool_call_chunks?.[toolCallIndex]?.name || "unknown";
871
- const seen = messageSeen.get(msgId);
872
- if (!seen?.tool?.has(toolCallId)) {
873
- const updatedSeen = getOrCreateMessageSeen(messageSeen, msgId);
874
- updatedSeen.tool ??= /* @__PURE__ */ new Set();
875
- updatedSeen.tool.add(toolCallId);
876
- if (!hasEmittedToolCallInCurrentStep(
877
- state,
878
- toolCallId,
879
- eventNamespace
880
- )) {
881
- markToolCallStarted(state, toolCallId, eventNamespace);
882
- controller.enqueue({
883
- type: "tool-input-start",
884
- toolCallId,
885
- toolName,
886
- dynamic: true
887
- });
888
- }
889
- }
890
- if (toolCallChunk.args) {
891
- controller.enqueue({
892
- type: "tool-input-delta",
893
- toolCallId,
894
- inputTextDelta: toolCallChunk.args
895
- });
896
- }
897
- }
898
- return;
899
- }
900
- const chunkReasoningId = extractReasoningId(msg);
901
- if (chunkReasoningId) {
902
- if (!messageReasoningIds.has(msgId)) {
903
- messageReasoningIds.set(msgId, chunkReasoningId);
904
- }
905
- emittedReasoningIds.add(chunkReasoningId);
906
- }
907
- const reasoning = extractReasoningFromContentBlocks(msg);
908
- if (reasoning) {
909
- const reasoningId = messageReasoningIds.get(msgId) ?? chunkReasoningId ?? msgId;
910
- const seen = messageSeen.get(msgId);
911
- if (!seen?.reasoning) {
912
- controller.enqueue({ type: "reasoning-start", id: msgId });
913
- getOrCreateMessageSeen(messageSeen, msgId).reasoning = true;
914
- }
915
- controller.enqueue({
916
- type: "reasoning-delta",
917
- delta: reasoning,
918
- id: msgId
919
- });
920
- emittedReasoningIds.add(reasoningId);
921
- }
922
- const text = getMessageText(msg);
923
- if (text) {
924
- const seen = messageSeen.get(msgId);
925
- if (seen?.reasoning && !seen.text) {
926
- controller.enqueue({ type: "reasoning-end", id: msgId });
927
- seen.reasoning = false;
928
- }
929
- if (!seen?.text) {
930
- controller.enqueue({ type: "text-start", id: msgId });
931
- getOrCreateMessageSeen(messageSeen, msgId).text = true;
932
- }
933
- controller.enqueue({
934
- type: "text-delta",
935
- delta: text,
936
- id: msgId
937
- });
938
- }
939
- const citations = extractCitationsFromContentBlocks(msg);
940
- if (citations.length > 0) {
941
- emitSourceChunks(
942
- citations,
943
- msgId,
944
- state.emittedSourceIds,
945
- controller
946
- );
947
- }
948
- } else if (isToolMessageType(msg)) {
949
- const msgObj = msg;
950
- const dataSource = msgObj.type === "constructor" && msgObj.kwargs && typeof msgObj.kwargs === "object" ? msgObj.kwargs : msgObj;
951
- const toolCallId = dataSource.tool_call_id;
952
- const status = dataSource.status;
953
- if (toolCallId) {
954
- const wasEmittedInCurrentStep = hasEmittedToolCallInCurrentStep(
955
- state,
956
- toolCallId,
957
- eventNamespace
958
- );
959
- const isDelayedOutputForPreviousLifecycle = !wasEmittedInCurrentStep && hasUnfinishedToolCall(state, toolCallId, eventNamespace);
960
- if (!wasEmittedInCurrentStep && !isDelayedOutputForPreviousLifecycle) {
961
- if (!startedUIReducerStep && state.currentStepsByNamespace.has(eventNamespace) && emittedToolCalls.has(toolCallId)) {
962
- controller.enqueue({ type: "start-step" });
963
- }
964
- markToolCallStarted(state, toolCallId, eventNamespace);
965
- controller.enqueue({
966
- type: "tool-input-start",
967
- toolCallId,
968
- toolName: typeof dataSource.name === "string" ? dataSource.name : "unknown",
969
- dynamic: true
970
- });
971
- }
972
- state.emittedToolOutputMessageIds.add(msgId);
973
- markToolOutputEmitted(state, toolCallId, eventNamespace);
974
- if (status === "error") {
975
- controller.enqueue({
976
- type: "tool-output-error",
977
- toolCallId,
978
- errorText: typeof dataSource.content === "string" ? dataSource.content : "Tool execution failed"
979
- });
980
- } else {
981
- controller.enqueue({
982
- type: "tool-output-available",
983
- toolCallId,
984
- output: dataSource.content
985
- });
986
- }
987
- }
988
- }
989
- return;
990
- }
991
- case "tools": {
992
- if (data == null || typeof data !== "object" || Array.isArray(data)) {
993
- return;
994
- }
995
- const payload = data;
996
- const toolCallId = typeof payload.toolCallId === "string" ? payload.toolCallId : void 0;
997
- const toolName = typeof payload.name === "string" ? payload.name : "unknown";
998
- if (!toolCallId) return;
999
- const ensureToolInputLifecycle = ({
1000
- allowPreviousStep
1001
- }) => {
1002
- if (!hasEmittedToolCallInCurrentStep(state, toolCallId, eventNamespace)) {
1003
- if (allowPreviousStep && hasUnfinishedToolCall(state, toolCallId, eventNamespace)) {
1004
- return;
1005
- }
1006
- markToolCallStarted(state, toolCallId, eventNamespace);
1007
- controller.enqueue({
1008
- type: "tool-input-start",
1009
- toolCallId,
1010
- toolName,
1011
- dynamic: true
1012
- });
1013
- }
1014
- if (!hasEmittedToolInputInCurrentStep(state, toolCallId, eventNamespace)) {
1015
- markToolInputEmitted(state, toolCallId, eventNamespace);
1016
- controller.enqueue({
1017
- type: "tool-input-available",
1018
- toolCallId,
1019
- toolName,
1020
- input: payload.input,
1021
- dynamic: true
1022
- });
1023
- }
1024
- };
1025
- switch (payload.event) {
1026
- case "on_tool_start": {
1027
- const toolCallKey = `${toolName}:${JSON.stringify(payload.input)}`;
1028
- emittedToolCallsByKey.set(toolCallKey, toolCallId);
1029
- ensureToolInputLifecycle({ allowPreviousStep: false });
1030
- break;
1031
- }
1032
- case "on_tool_event": {
1033
- ensureToolInputLifecycle({ allowPreviousStep: true });
1034
- controller.enqueue({
1035
- type: "tool-output-available",
1036
- toolCallId,
1037
- output: payload.data,
1038
- preliminary: true
1039
- });
1040
- break;
1041
- }
1042
- case "on_tool_end": {
1043
- ensureToolInputLifecycle({ allowPreviousStep: true });
1044
- markToolOutputEmitted(state, toolCallId, eventNamespace);
1045
- controller.enqueue({
1046
- type: "tool-output-available",
1047
- toolCallId,
1048
- output: payload.output
1049
- });
1050
- break;
1051
- }
1052
- case "on_tool_error": {
1053
- ensureToolInputLifecycle({ allowPreviousStep: true });
1054
- markToolOutputEmitted(state, toolCallId, eventNamespace);
1055
- controller.enqueue({
1056
- type: "tool-output-error",
1057
- toolCallId,
1058
- errorText: formatToolError(payload.error)
1059
- });
1060
- break;
1061
- }
1062
- }
1063
- return;
1064
- }
1065
- case "values": {
1066
- for (const [id, seen] of messageSeen) {
1067
- const messageNamespace = getLangGraphNamespaceKey(
1068
- state.messageNamespaces.get(id)
1069
- );
1070
- if (seen.text) controller.enqueue({ type: "text-end", id });
1071
- if (seen.tool) {
1072
- for (const toolCallId of seen.tool) {
1073
- const concatMsg = messageConcat.get(id);
1074
- const toolCall = concatMsg?.tool_calls?.find(
1075
- (call) => call.id === toolCallId
1076
- );
1077
- if (toolCall) {
1078
- markToolCallEmitted(state, toolCallId, messageNamespace);
1079
- const toolCallKey = `${toolCall.name}:${JSON.stringify(toolCall.args)}`;
1080
- emittedToolCallsByKey.set(toolCallKey, toolCallId);
1081
- if (!hasEmittedToolInputInCurrentStep(
1082
- state,
1083
- toolCallId,
1084
- messageNamespace
1085
- )) {
1086
- markToolInputEmitted(state, toolCallId, messageNamespace);
1087
- controller.enqueue({
1088
- type: "tool-input-available",
1089
- toolCallId,
1090
- toolName: toolCall.name,
1091
- input: toolCall.args,
1092
- dynamic: true
1093
- });
1094
- }
1095
- }
1096
- }
1097
- }
1098
- if (seen.reasoning) {
1099
- controller.enqueue({ type: "reasoning-end", id });
1100
- }
1101
- messageSeen.delete(id);
1102
- state.messageNamespaces.delete(id);
1103
- messageConcat.delete(id);
1104
- messageReasoningIds.delete(id);
1105
- }
1106
- if (data != null && typeof data === "object" && "messages" in data) {
1107
- const messages = data.messages;
1108
- if (Array.isArray(messages)) {
1109
- const completedToolCallIds = /* @__PURE__ */ new Set();
1110
- const trailingToolMessages = /* @__PURE__ */ new Map();
1111
- let trailingToolMessageStart = messages.length;
1112
- while (trailingToolMessageStart > 0 && isToolMessageType(messages[trailingToolMessageStart - 1])) {
1113
- trailingToolMessageStart--;
1114
- }
1115
- for (const [index, msg] of messages.entries()) {
1116
- if (!msg || typeof msg !== "object") continue;
1117
- if (isToolMessageType(msg)) {
1118
- const msgObj = msg;
1119
- const dataSource = msgObj.type === "constructor" && msgObj.kwargs && typeof msgObj.kwargs === "object" ? msgObj.kwargs : msgObj;
1120
- const toolCallId = dataSource.tool_call_id;
1121
- if (toolCallId) {
1122
- completedToolCallIds.add(toolCallId);
1123
- if (index >= trailingToolMessageStart) {
1124
- trailingToolMessages.set(toolCallId, {
1125
- data: dataSource,
1126
- outputId: getMessageId(msg) ?? `${toolCallId}:${index}`
1127
- });
1128
- }
1129
- }
1130
- }
1131
- }
1132
- for (const msg of messages) {
1133
- if (!msg || typeof msg !== "object") continue;
1134
- const msgId = getMessageId(msg);
1135
- if (!msgId) continue;
1136
- let toolCalls;
1137
- if (AIMessageChunk.isInstance(msg) || AIMessage.isInstance(msg)) {
1138
- toolCalls = msg.tool_calls;
1139
- } else if (isPlainMessageObject(msg)) {
1140
- const messageRecord = msg;
1141
- const isSerializedFormat = messageRecord.type === "constructor" && Array.isArray(messageRecord.id) && (messageRecord.id.includes("AIMessageChunk") || messageRecord.id.includes("AIMessage"));
1142
- const dataSource = isSerializedFormat ? messageRecord.kwargs : messageRecord;
1143
- if (messageRecord.type === "ai" || messageRecord.type === "AIMessageChunk" || isSerializedFormat) {
1144
- if (Array.isArray(dataSource?.tool_calls)) {
1145
- toolCalls = dataSource.tool_calls;
1146
- } else if (
1147
- /**
1148
- * Fall back to additional_kwargs.tool_calls (OpenAI format)
1149
- */
1150
- dataSource?.additional_kwargs && typeof dataSource.additional_kwargs === "object"
1151
- ) {
1152
- const additionalKwargs = dataSource.additional_kwargs;
1153
- if (Array.isArray(additionalKwargs.tool_calls)) {
1154
- toolCalls = additionalKwargs.tool_calls.map((tc, idx) => {
1155
- const functionData = tc.function;
1156
- let args;
1157
- try {
1158
- args = functionData?.arguments ? JSON.parse(functionData.arguments) : {};
1159
- } catch {
1160
- args = {};
1161
- }
1162
- return {
1163
- id: tc.id || `call_${idx}`,
1164
- name: functionData?.name || "unknown",
1165
- args
1166
- };
1167
- });
1168
- }
1169
- }
1170
- }
1171
- }
1172
- if (toolCalls && toolCalls.length > 0) {
1173
- for (const toolCall of toolCalls) {
1174
- const messageNamespace = findMessageCurrentStepNamespace(
1175
- state,
1176
- msgId
1177
- );
1178
- const lifecycleNamespace = messageNamespace ?? eventNamespace;
1179
- const wasObservedInCurrentStep = messageNamespace !== void 0;
1180
- const wasToolCallEmittedInCurrentStep = toolCall.id ? hasEmittedToolCallInCurrentStep(
1181
- state,
1182
- toolCall.id,
1183
- lifecycleNamespace
1184
- ) : false;
1185
- const trailingToolMessageInfo = toolCall.id ? trailingToolMessages.get(toolCall.id) : void 0;
1186
- const canRecoverCompletedToolCall = wasObservedInCurrentStep || wasToolCallEmittedInCurrentStep;
1187
- if (toolCall.id && !wasToolCallEmittedInCurrentStep && (wasObservedInCurrentStep || !emittedToolCalls.has(toolCall.id) && !completedToolCallIds.has(toolCall.id))) {
1188
- markToolCallStarted(state, toolCall.id, lifecycleNamespace);
1189
- const toolCallKey = `${toolCall.name}:${JSON.stringify(toolCall.args)}`;
1190
- emittedToolCallsByKey.set(toolCallKey, toolCall.id);
1191
- controller.enqueue({
1192
- type: "tool-input-start",
1193
- toolCallId: toolCall.id,
1194
- toolName: toolCall.name,
1195
- dynamic: true
1196
- });
1197
- markToolInputEmitted(state, toolCall.id, lifecycleNamespace);
1198
- controller.enqueue({
1199
- type: "tool-input-available",
1200
- toolCallId: toolCall.id,
1201
- toolName: toolCall.name,
1202
- input: toolCall.args,
1203
- dynamic: true
1204
- });
1205
- } else if (toolCall.id && emittedToolCalls.has(toolCall.id)) {
1206
- const toolCallKey = `${toolCall.name}:${JSON.stringify(toolCall.args)}`;
1207
- emittedToolCallsByKey.set(toolCallKey, toolCall.id);
1208
- }
1209
- if (toolCall.id && trailingToolMessageInfo != null && !state.emittedToolOutputMessageIds.has(
1210
- trailingToolMessageInfo.outputId
1211
- ) && !hasEmittedToolOutputInCurrentStep(
1212
- state,
1213
- toolCall.id,
1214
- lifecycleNamespace
1215
- ) && canRecoverCompletedToolCall) {
1216
- state.emittedToolOutputMessageIds.add(
1217
- trailingToolMessageInfo.outputId
1218
- );
1219
- markToolOutputEmitted(state, toolCall.id, lifecycleNamespace);
1220
- const trailingToolMessage = trailingToolMessageInfo.data;
1221
- if (trailingToolMessage.status === "error") {
1222
- controller.enqueue({
1223
- type: "tool-output-error",
1224
- toolCallId: toolCall.id,
1225
- errorText: typeof trailingToolMessage.content === "string" ? trailingToolMessage.content : "Tool execution failed"
1226
- });
1227
- } else {
1228
- controller.enqueue({
1229
- type: "tool-output-available",
1230
- toolCallId: toolCall.id,
1231
- output: trailingToolMessage.content
1232
- });
1233
- }
1234
- }
1235
- }
1236
- }
1237
- const reasoningId = extractReasoningId(msg);
1238
- const wasStreamedThisRequest = messageSeen.has(msgId);
1239
- const hasToolCalls = toolCalls && toolCalls.length > 0;
1240
- const shouldEmitReasoning = reasoningId && !emittedReasoningIds.has(reasoningId) && (wasStreamedThisRequest || !hasToolCalls);
1241
- if (shouldEmitReasoning) {
1242
- const reasoning = extractReasoningFromValuesMessage(msg);
1243
- if (reasoning) {
1244
- controller.enqueue({ type: "reasoning-start", id: msgId });
1245
- controller.enqueue({
1246
- type: "reasoning-delta",
1247
- delta: reasoning,
1248
- id: msgId
1249
- });
1250
- controller.enqueue({ type: "reasoning-end", id: msgId });
1251
- emittedReasoningIds.add(reasoningId);
1252
- }
1253
- }
1254
- const valuesCitations = extractCitationsFromContentBlocks(msg);
1255
- if (valuesCitations.length > 0) {
1256
- emitSourceChunks(
1257
- valuesCitations,
1258
- msgId,
1259
- state.emittedSourceIds,
1260
- controller
1261
- );
1262
- }
1263
- }
1264
- }
1265
- }
1266
- if (data != null && typeof data === "object") {
1267
- const interrupt = data.__interrupt__;
1268
- if (Array.isArray(interrupt) && interrupt.length > 0) {
1269
- for (const interruptItem of interrupt) {
1270
- const interruptValue = interruptItem?.value;
1271
- if (!interruptValue) continue;
1272
- const actionRequests = interruptValue.actionRequests || interruptValue.action_requests;
1273
- if (!Array.isArray(actionRequests)) continue;
1274
- for (const actionRequest of actionRequests) {
1275
- const toolName = actionRequest.name;
1276
- const input = actionRequest.args || actionRequest.arguments;
1277
- const toolCallKey = `${toolName}:${JSON.stringify(input)}`;
1278
- const toolCallId = emittedToolCallsByKey.get(toolCallKey) || actionRequest.id || `hitl-${toolName}-${Date.now()}`;
1279
- if (!emittedToolCalls.has(toolCallId)) {
1280
- markToolCallStarted(state, toolCallId, eventNamespace);
1281
- emittedToolCallsByKey.set(toolCallKey, toolCallId);
1282
- controller.enqueue({
1283
- type: "tool-input-start",
1284
- toolCallId,
1285
- toolName,
1286
- dynamic: true
1287
- });
1288
- markToolInputEmitted(state, toolCallId, eventNamespace);
1289
- controller.enqueue({
1290
- type: "tool-input-available",
1291
- toolCallId,
1292
- toolName,
1293
- input,
1294
- dynamic: true
1295
- });
1296
- }
1297
- controller.enqueue({
1298
- type: "tool-approval-request",
1299
- approvalId: toolCallId,
1300
- toolCallId
1301
- });
1302
- }
1303
- }
1304
- }
1305
- }
1306
- break;
1307
- }
1308
- }
805
+ const { messageSeen, messageConcat, emittedToolCalls, emittedImages, emittedReasoningIds, messageReasoningIds, toolCallInfoByIndex, emittedToolCallsByKey } = state;
806
+ const [rawNamespace, type, data] = parseLangGraphEvent(event);
807
+ const namespace = Array.isArray(rawNamespace) && rawNamespace.every((segment) => typeof segment === "string") ? rawNamespace : void 0;
808
+ const eventNamespace = getLangGraphNamespaceKey(namespace);
809
+ controller = createLangGraphNamespaceController(controller, state, namespace);
810
+ switch (type) {
811
+ case "custom": {
812
+ /**
813
+ * Extract custom event type from the data's 'type' field if present.
814
+ * This allows users to emit custom events like:
815
+ * writer({ type: 'progress', value: 50 }) -> { type: 'data-progress', data: {...} }
816
+ * writer({ type: 'status', message: '...' }) -> { type: 'data-status', data: {...} }
817
+ * writer({ key: 'value' }) -> { type: 'data-custom', data: {...} } (fallback)
818
+ *
819
+ * The 'id' field can be used to make parts persistent and updateable.
820
+ * Parts with an 'id' are NOT transient (added to message.parts).
821
+ * Parts without an 'id' are transient (only passed to onData callback).
822
+ */
823
+ let customTypeName = "custom";
824
+ let partId;
825
+ if (data != null && typeof data === "object" && !Array.isArray(data)) {
826
+ const dataObj = data;
827
+ if (typeof dataObj.type === "string" && dataObj.type) customTypeName = dataObj.type;
828
+ if (typeof dataObj.id === "string" && dataObj.id) partId = dataObj.id;
829
+ }
830
+ controller.enqueue({
831
+ type: `data-${customTypeName}`,
832
+ id: partId,
833
+ transient: partId == null,
834
+ data
835
+ });
836
+ break;
837
+ }
838
+ case "messages": {
839
+ const [rawMsg, metadata] = data;
840
+ const msg = rawMsg;
841
+ const msgId = getMessageId(msg);
842
+ if (!msgId) return;
843
+ if (namespace !== void 0) state.messageNamespaces.set(msgId, namespace);
844
+ /**
845
+ * Each namespace has an independent LangGraph step counter. Advancing a
846
+ * namespace starts a new reducer scope so provider-scoped tool call IDs
847
+ * can be reused in that namespace. A finish-step chunk clears every
848
+ * active UI text/reasoning part, so omit it when another namespace is
849
+ * still active.
850
+ */
851
+ let startedUIReducerStep = false;
852
+ const langgraphStep = typeof metadata?.langgraph_step === "number" ? metadata.langgraph_step : null;
853
+ if (langgraphStep !== null) {
854
+ const currentStep = state.currentStepsByNamespace.get(eventNamespace) ?? null;
855
+ if (currentStep === null) {
856
+ if (state.currentStepsByNamespace.size === 0) {
857
+ controller.enqueue({ type: "start-step" });
858
+ startedUIReducerStep = true;
859
+ }
860
+ state.currentStepsByNamespace.set(eventNamespace, langgraphStep);
861
+ state.messageIdsInCurrentStepByNamespace.set(eventNamespace, /* @__PURE__ */ new Set());
862
+ state.emittedToolCallsInCurrentStepByNamespace.set(eventNamespace, /* @__PURE__ */ new Set());
863
+ state.emittedToolInputsInCurrentStepByNamespace.set(eventNamespace, /* @__PURE__ */ new Set());
864
+ state.emittedToolOutputsInCurrentStepByNamespace.set(eventNamespace, /* @__PURE__ */ new Set());
865
+ } else if (langgraphStep !== currentStep) {
866
+ if (!closeStepNamespaceMessages(state, eventNamespace, controller)) controller.enqueue({ type: "finish-step" });
867
+ /**
868
+ * A concurrent namespace prevents finish-step because it would close
869
+ * that namespace's active text/reasoning parts. Still emit start-step
870
+ * so tool calls in the advancing namespace have a distinct reducer
871
+ * scope while the concurrent message lifecycle remains active.
872
+ */
873
+ controller.enqueue({ type: "start-step" });
874
+ startedUIReducerStep = true;
875
+ state.currentStepsByNamespace.set(eventNamespace, langgraphStep);
876
+ state.messageIdsInCurrentStepByNamespace.set(eventNamespace, /* @__PURE__ */ new Set());
877
+ state.emittedToolCallsInCurrentStepByNamespace.set(eventNamespace, /* @__PURE__ */ new Set());
878
+ state.emittedToolInputsInCurrentStepByNamespace.set(eventNamespace, /* @__PURE__ */ new Set());
879
+ state.emittedToolOutputsInCurrentStepByNamespace.set(eventNamespace, /* @__PURE__ */ new Set());
880
+ }
881
+ }
882
+ if (state.currentStepsByNamespace.has(eventNamespace)) getOrCreateNamespaceSet(state.messageIdsInCurrentStepByNamespace, eventNamespace).add(msgId);
883
+ /**
884
+ * Accumulate message chunks for later reference
885
+ * Note: Only works for actual class instances, not serialized messages
886
+ */
887
+ if (isAIMessageChunkInstance(msg)) {
888
+ const existingMessage = messageConcat.get(msgId);
889
+ if (existingMessage) messageConcat.set(msgId, existingMessage.concat(msg));
890
+ else messageConcat.set(msgId, msg);
891
+ }
892
+ if (isAIMessageChunk(msg)) {
893
+ const concatChunk = messageConcat.get(msgId);
894
+ /**
895
+ * Handle image generation outputs from additional_kwargs.tool_outputs
896
+ * Handle both direct properties and serialized messages (kwargs)
897
+ */
898
+ const msgObj = msg;
899
+ const dataSource = msgObj.type === "constructor" && msgObj.kwargs && typeof msgObj.kwargs === "object" ? msgObj.kwargs : msgObj;
900
+ const additionalKwargs = dataSource.additional_kwargs;
901
+ const imageOutputs = extractImageOutputs(additionalKwargs);
902
+ for (const imageOutput of imageOutputs)
903
+ /**
904
+ * Only emit if we have image data and haven't emitted this image yet
905
+ */
906
+ if (imageOutput.result && !emittedImages.has(imageOutput.id)) {
907
+ emittedImages.add(imageOutput.id);
908
+ /**
909
+ * Emit as a file part using proper AI SDK multimodal format
910
+ */
911
+ const mediaType = `image/${imageOutput.output_format || "png"}`;
912
+ controller.enqueue({
913
+ type: "file",
914
+ mediaType,
915
+ url: `data:${mediaType};base64,${imageOutput.result}`
916
+ });
917
+ }
918
+ /**
919
+ * Handle tool call chunks for streaming tool calls
920
+ * Access from dataSource to handle both direct and serialized messages
921
+ *
922
+ * Tool call chunks are streamed as follows:
923
+ * 1. First chunk: has name, id, but often empty args
924
+ * 2. Subsequent chunks: have args but NO id or name
925
+ *
926
+ * We store tool call info by index when we first see it, then look it up
927
+ * for subsequent chunks that don't include the id.
928
+ */
929
+ const toolCallChunks = dataSource.tool_call_chunks;
930
+ if (toolCallChunks?.length) {
931
+ for (const toolCallChunk of toolCallChunks) {
932
+ const toolCallIndex = toolCallChunk.index ?? 0;
933
+ /**
934
+ * If this chunk has an id, store it for future lookups by index
935
+ */
936
+ if (toolCallChunk.id) getOrCreateToolCallInfoByIndex(toolCallInfoByIndex, msgId).set(toolCallIndex, {
937
+ id: toolCallChunk.id,
938
+ name: toolCallChunk.name || concatChunk?.tool_call_chunks?.[toolCallIndex]?.name || "unknown"
939
+ });
940
+ /**
941
+ * Get the tool call ID from the chunk, stored info, or accumulated chunks
942
+ */
943
+ const storedToolCallInfo = toolCallInfoByIndex.get(msgId)?.get(toolCallIndex);
944
+ const toolCallId = toolCallChunk.id || storedToolCallInfo?.id || concatChunk?.tool_call_chunks?.[toolCallIndex]?.id;
945
+ /**
946
+ * Skip if we don't have a proper tool call ID - we'll handle it in values
947
+ */
948
+ if (!toolCallId) continue;
949
+ const toolName = toolCallChunk.name || storedToolCallInfo?.name || concatChunk?.tool_call_chunks?.[toolCallIndex]?.name || "unknown";
950
+ if (!messageSeen.get(msgId)?.tool?.has(toolCallId)) {
951
+ const updatedSeen = getOrCreateMessageSeen(messageSeen, msgId);
952
+ updatedSeen.tool ??= /* @__PURE__ */ new Set();
953
+ updatedSeen.tool.add(toolCallId);
954
+ if (!hasEmittedToolCallInCurrentStep(state, toolCallId, eventNamespace)) {
955
+ markToolCallStarted(state, toolCallId, eventNamespace);
956
+ controller.enqueue({
957
+ type: "tool-input-start",
958
+ toolCallId,
959
+ toolName,
960
+ dynamic: true
961
+ });
962
+ }
963
+ }
964
+ /**
965
+ * Only emit tool-input-delta when args is non-empty
966
+ */
967
+ if (toolCallChunk.args) controller.enqueue({
968
+ type: "tool-input-delta",
969
+ toolCallId,
970
+ inputTextDelta: toolCallChunk.args
971
+ });
972
+ }
973
+ return;
974
+ }
975
+ /**
976
+ * Handle reasoning content from contentBlocks
977
+ * Streaming chunks contain DELTA text (not accumulated), so emit directly.
978
+ * Use reasoning block ID for deduplication as it's consistent across streaming and values events.
979
+ *
980
+ * Important: Early chunks may have reasoning ID but no content, later chunks may
981
+ * have content but no reasoning ID. We capture the ID when first seen and reuse it.
982
+ * We also immediately add to emittedReasoningIds to prevent values events from
983
+ * emitting the same reasoning (values events can arrive between streaming chunks).
984
+ */
985
+ const chunkReasoningId = extractReasoningId(msg);
986
+ if (chunkReasoningId) {
987
+ if (!messageReasoningIds.has(msgId)) messageReasoningIds.set(msgId, chunkReasoningId);
988
+ emittedReasoningIds.add(chunkReasoningId);
989
+ }
990
+ const reasoning = extractReasoningFromContentBlocks(msg);
991
+ if (reasoning) {
992
+ const reasoningId = messageReasoningIds.get(msgId) ?? chunkReasoningId ?? msgId;
993
+ if (!messageSeen.get(msgId)?.reasoning) {
994
+ controller.enqueue({
995
+ type: "reasoning-start",
996
+ id: msgId
997
+ });
998
+ getOrCreateMessageSeen(messageSeen, msgId).reasoning = true;
999
+ }
1000
+ controller.enqueue({
1001
+ type: "reasoning-delta",
1002
+ delta: reasoning,
1003
+ id: msgId
1004
+ });
1005
+ emittedReasoningIds.add(reasoningId);
1006
+ }
1007
+ /**
1008
+ * Handle text content
1009
+ */
1010
+ const text = getMessageText(msg);
1011
+ if (text) {
1012
+ const seen = messageSeen.get(msgId);
1013
+ if (seen?.reasoning && !seen.text) {
1014
+ controller.enqueue({
1015
+ type: "reasoning-end",
1016
+ id: msgId
1017
+ });
1018
+ seen.reasoning = false;
1019
+ }
1020
+ if (!seen?.text) {
1021
+ controller.enqueue({
1022
+ type: "text-start",
1023
+ id: msgId
1024
+ });
1025
+ getOrCreateMessageSeen(messageSeen, msgId).text = true;
1026
+ }
1027
+ controller.enqueue({
1028
+ type: "text-delta",
1029
+ delta: text,
1030
+ id: msgId
1031
+ });
1032
+ }
1033
+ const citations = extractCitationsFromContentBlocks(msg);
1034
+ if (citations.length > 0) emitSourceChunks(citations, msgId, state.emittedSourceIds, controller);
1035
+ } else if (isToolMessageType(msg)) {
1036
+ const msgObj = msg;
1037
+ const dataSource = msgObj.type === "constructor" && msgObj.kwargs && typeof msgObj.kwargs === "object" ? msgObj.kwargs : msgObj;
1038
+ const toolCallId = dataSource.tool_call_id;
1039
+ const status = dataSource.status;
1040
+ if (toolCallId) {
1041
+ const wasEmittedInCurrentStep = hasEmittedToolCallInCurrentStep(state, toolCallId, eventNamespace);
1042
+ const isDelayedOutputForPreviousLifecycle = !wasEmittedInCurrentStep && hasUnfinishedToolCall(state, toolCallId, eventNamespace);
1043
+ if (!wasEmittedInCurrentStep && !isDelayedOutputForPreviousLifecycle) {
1044
+ /**
1045
+ * A newly observed namespace does not normally start another global
1046
+ * reducer step while another namespace is active. Reused provider
1047
+ * IDs need a distinct reducer scope, however, or the new output
1048
+ * overwrites the prior namespace's tool part.
1049
+ */
1050
+ if (!startedUIReducerStep && state.currentStepsByNamespace.has(eventNamespace) && emittedToolCalls.has(toolCallId)) controller.enqueue({ type: "start-step" });
1051
+ markToolCallStarted(state, toolCallId, eventNamespace);
1052
+ controller.enqueue({
1053
+ type: "tool-input-start",
1054
+ toolCallId,
1055
+ toolName: typeof dataSource.name === "string" ? dataSource.name : "unknown",
1056
+ dynamic: true
1057
+ });
1058
+ }
1059
+ state.emittedToolOutputMessageIds.add(msgId);
1060
+ markToolOutputEmitted(state, toolCallId, eventNamespace);
1061
+ if (status === "error") controller.enqueue({
1062
+ type: "tool-output-error",
1063
+ toolCallId,
1064
+ errorText: typeof dataSource.content === "string" ? dataSource.content : "Tool execution failed"
1065
+ });
1066
+ else controller.enqueue({
1067
+ type: "tool-output-available",
1068
+ toolCallId,
1069
+ output: dataSource.content
1070
+ });
1071
+ }
1072
+ }
1073
+ return;
1074
+ }
1075
+ case "tools": {
1076
+ if (data == null || typeof data !== "object" || Array.isArray(data)) return;
1077
+ const payload = data;
1078
+ const toolCallId = typeof payload.toolCallId === "string" ? payload.toolCallId : void 0;
1079
+ const toolName = typeof payload.name === "string" ? payload.name : "unknown";
1080
+ if (!toolCallId) return;
1081
+ const ensureToolInputLifecycle = ({ allowPreviousStep }) => {
1082
+ if (!hasEmittedToolCallInCurrentStep(state, toolCallId, eventNamespace)) {
1083
+ if (allowPreviousStep && hasUnfinishedToolCall(state, toolCallId, eventNamespace)) return;
1084
+ markToolCallStarted(state, toolCallId, eventNamespace);
1085
+ controller.enqueue({
1086
+ type: "tool-input-start",
1087
+ toolCallId,
1088
+ toolName,
1089
+ dynamic: true
1090
+ });
1091
+ }
1092
+ if (!hasEmittedToolInputInCurrentStep(state, toolCallId, eventNamespace)) {
1093
+ markToolInputEmitted(state, toolCallId, eventNamespace);
1094
+ controller.enqueue({
1095
+ type: "tool-input-available",
1096
+ toolCallId,
1097
+ toolName,
1098
+ input: payload.input,
1099
+ dynamic: true
1100
+ });
1101
+ }
1102
+ };
1103
+ switch (payload.event) {
1104
+ case "on_tool_start": {
1105
+ const toolCallKey = `${toolName}:${JSON.stringify(payload.input)}`;
1106
+ emittedToolCallsByKey.set(toolCallKey, toolCallId);
1107
+ ensureToolInputLifecycle({ allowPreviousStep: false });
1108
+ break;
1109
+ }
1110
+ case "on_tool_event":
1111
+ ensureToolInputLifecycle({ allowPreviousStep: true });
1112
+ controller.enqueue({
1113
+ type: "tool-output-available",
1114
+ toolCallId,
1115
+ output: payload.data,
1116
+ preliminary: true
1117
+ });
1118
+ break;
1119
+ case "on_tool_end":
1120
+ ensureToolInputLifecycle({ allowPreviousStep: true });
1121
+ markToolOutputEmitted(state, toolCallId, eventNamespace);
1122
+ controller.enqueue({
1123
+ type: "tool-output-available",
1124
+ toolCallId,
1125
+ output: payload.output
1126
+ });
1127
+ break;
1128
+ case "on_tool_error":
1129
+ ensureToolInputLifecycle({ allowPreviousStep: true });
1130
+ markToolOutputEmitted(state, toolCallId, eventNamespace);
1131
+ controller.enqueue({
1132
+ type: "tool-output-error",
1133
+ toolCallId,
1134
+ errorText: formatToolError(payload.error)
1135
+ });
1136
+ }
1137
+ return;
1138
+ }
1139
+ case "values":
1140
+ /**
1141
+ * Finalize all pending message chunks
1142
+ */
1143
+ for (const [id, seen] of messageSeen) {
1144
+ const messageNamespace = getLangGraphNamespaceKey(state.messageNamespaces.get(id));
1145
+ if (seen.text) controller.enqueue({
1146
+ type: "text-end",
1147
+ id
1148
+ });
1149
+ if (seen.tool) for (const toolCallId of seen.tool) {
1150
+ const toolCall = messageConcat.get(id)?.tool_calls?.find((call) => call.id === toolCallId);
1151
+ if (toolCall) {
1152
+ markToolCallEmitted(state, toolCallId, messageNamespace);
1153
+ const toolCallKey = `${toolCall.name}:${JSON.stringify(toolCall.args)}`;
1154
+ emittedToolCallsByKey.set(toolCallKey, toolCallId);
1155
+ if (!hasEmittedToolInputInCurrentStep(state, toolCallId, messageNamespace)) {
1156
+ markToolInputEmitted(state, toolCallId, messageNamespace);
1157
+ controller.enqueue({
1158
+ type: "tool-input-available",
1159
+ toolCallId,
1160
+ toolName: toolCall.name,
1161
+ input: toolCall.args,
1162
+ dynamic: true
1163
+ });
1164
+ }
1165
+ }
1166
+ }
1167
+ if (seen.reasoning) controller.enqueue({
1168
+ type: "reasoning-end",
1169
+ id
1170
+ });
1171
+ messageSeen.delete(id);
1172
+ state.messageNamespaces.delete(id);
1173
+ messageConcat.delete(id);
1174
+ messageReasoningIds.delete(id);
1175
+ }
1176
+ /**
1177
+ * Also check for tool calls in the final state that weren't streamed
1178
+ * This handles cases where tool calls appear directly in values without being in messages events
1179
+ */
1180
+ if (data != null && typeof data === "object" && "messages" in data) {
1181
+ const messages = data.messages;
1182
+ if (Array.isArray(messages)) {
1183
+ /**
1184
+ * First pass: Collect all tool call IDs that have been responded to by
1185
+ * ToolMessages. Calls followed by another non-tool message are historical,
1186
+ * while trailing ToolMessages can be the only evidence of a completed call
1187
+ * when the node is tagged with `nostream`.
1188
+ */
1189
+ const completedToolCallIds = /* @__PURE__ */ new Set();
1190
+ const trailingToolMessages = /* @__PURE__ */ new Map();
1191
+ let trailingToolMessageStart = messages.length;
1192
+ while (trailingToolMessageStart > 0 && isToolMessageType(messages[trailingToolMessageStart - 1])) trailingToolMessageStart--;
1193
+ for (const [index, msg] of messages.entries()) {
1194
+ if (!msg || typeof msg !== "object") continue;
1195
+ if (isToolMessageType(msg)) {
1196
+ const msgObj = msg;
1197
+ const dataSource = msgObj.type === "constructor" && msgObj.kwargs && typeof msgObj.kwargs === "object" ? msgObj.kwargs : msgObj;
1198
+ const toolCallId = dataSource.tool_call_id;
1199
+ if (toolCallId) {
1200
+ completedToolCallIds.add(toolCallId);
1201
+ if (index >= trailingToolMessageStart) trailingToolMessages.set(toolCallId, {
1202
+ data: dataSource,
1203
+ outputId: getMessageId(msg) ?? `${toolCallId}:${index}`
1204
+ });
1205
+ }
1206
+ }
1207
+ }
1208
+ /**
1209
+ * Second pass: Process messages and emit tool events for new tool calls,
1210
+ * including calls completed by trailing ToolMessages that were not streamed.
1211
+ */
1212
+ for (const msg of messages) {
1213
+ if (!msg || typeof msg !== "object") continue;
1214
+ const msgId = getMessageId(msg);
1215
+ if (!msgId) continue;
1216
+ /**
1217
+ * Check if this is an AI message with tool calls
1218
+ */
1219
+ let toolCalls;
1220
+ /**
1221
+ * For class instances
1222
+ */
1223
+ if (AIMessageChunk.isInstance(msg) || AIMessage.isInstance(msg)) toolCalls = msg.tool_calls;
1224
+ else if (isPlainMessageObject(msg)) {
1225
+ /**
1226
+ * For plain objects from RemoteGraph API or serialized LangChain messages
1227
+ */
1228
+ const messageRecord = msg;
1229
+ /**
1230
+ * Determine the data source (handle both direct and serialized formats)
1231
+ */
1232
+ const isSerializedFormat = messageRecord.type === "constructor" && Array.isArray(messageRecord.id) && (messageRecord.id.includes("AIMessageChunk") || messageRecord.id.includes("AIMessage"));
1233
+ const dataSource = isSerializedFormat ? messageRecord.kwargs : messageRecord;
1234
+ if (messageRecord.type === "ai" || messageRecord.type === "AIMessageChunk" || isSerializedFormat) {
1235
+ /**
1236
+ * Try tool_calls first (normalized format)
1237
+ */
1238
+ if (Array.isArray(dataSource?.tool_calls)) toolCalls = dataSource.tool_calls;
1239
+ else if (dataSource?.additional_kwargs && typeof dataSource.additional_kwargs === "object") {
1240
+ const additionalKwargs = dataSource.additional_kwargs;
1241
+ if (Array.isArray(additionalKwargs.tool_calls))
1242
+ /**
1243
+ * Convert OpenAI format to normalized format
1244
+ */
1245
+ toolCalls = additionalKwargs.tool_calls.map((tc, idx) => {
1246
+ const functionData = tc.function;
1247
+ let args;
1248
+ try {
1249
+ args = functionData?.arguments ? JSON.parse(functionData.arguments) : {};
1250
+ } catch {
1251
+ args = {};
1252
+ }
1253
+ return {
1254
+ id: tc.id || `call_${idx}`,
1255
+ name: functionData?.name || "unknown",
1256
+ args
1257
+ };
1258
+ });
1259
+ }
1260
+ }
1261
+ }
1262
+ if (toolCalls && toolCalls.length > 0) for (const toolCall of toolCalls) {
1263
+ const messageNamespace = findMessageCurrentStepNamespace(state, msgId);
1264
+ const lifecycleNamespace = messageNamespace ?? eventNamespace;
1265
+ const wasObservedInCurrentStep = messageNamespace !== void 0;
1266
+ const wasToolCallEmittedInCurrentStep = toolCall.id ? hasEmittedToolCallInCurrentStep(state, toolCall.id, lifecycleNamespace) : false;
1267
+ const trailingToolMessageInfo = toolCall.id ? trailingToolMessages.get(toolCall.id) : void 0;
1268
+ const canRecoverCompletedToolCall = wasObservedInCurrentStep || wasToolCallEmittedInCurrentStep;
1269
+ /**
1270
+ * Emit tool calls recovered from a message in the current step,
1271
+ * even when a prior step used the same provider-scoped ID.
1272
+ * Otherwise, preserve stream-wide suppression for historical calls
1273
+ * while recovering completed calls observed in the current step or
1274
+ * an earlier values snapshot before their trailing ToolMessage arrived.
1275
+ */
1276
+ if (toolCall.id && !wasToolCallEmittedInCurrentStep && (wasObservedInCurrentStep || !emittedToolCalls.has(toolCall.id) && !completedToolCallIds.has(toolCall.id))) {
1277
+ markToolCallStarted(state, toolCall.id, lifecycleNamespace);
1278
+ const toolCallKey = `${toolCall.name}:${JSON.stringify(toolCall.args)}`;
1279
+ emittedToolCallsByKey.set(toolCallKey, toolCall.id);
1280
+ /**
1281
+ * Emit tool-input-start first to ensure proper lifecycle.
1282
+ * Tool calls that weren't streamed (no tool_call_chunks) need
1283
+ * the start event before tool-input-available.
1284
+ */
1285
+ controller.enqueue({
1286
+ type: "tool-input-start",
1287
+ toolCallId: toolCall.id,
1288
+ toolName: toolCall.name,
1289
+ dynamic: true
1290
+ });
1291
+ markToolInputEmitted(state, toolCall.id, lifecycleNamespace);
1292
+ controller.enqueue({
1293
+ type: "tool-input-available",
1294
+ toolCallId: toolCall.id,
1295
+ toolName: toolCall.name,
1296
+ input: toolCall.args,
1297
+ dynamic: true
1298
+ });
1299
+ } else if (toolCall.id && emittedToolCalls.has(toolCall.id)) {
1300
+ const toolCallKey = `${toolCall.name}:${JSON.stringify(toolCall.args)}`;
1301
+ emittedToolCallsByKey.set(toolCallKey, toolCall.id);
1302
+ }
1303
+ if (toolCall.id && trailingToolMessageInfo != null && !state.emittedToolOutputMessageIds.has(trailingToolMessageInfo.outputId) && !hasEmittedToolOutputInCurrentStep(state, toolCall.id, lifecycleNamespace) && canRecoverCompletedToolCall) {
1304
+ state.emittedToolOutputMessageIds.add(trailingToolMessageInfo.outputId);
1305
+ markToolOutputEmitted(state, toolCall.id, lifecycleNamespace);
1306
+ const trailingToolMessage = trailingToolMessageInfo.data;
1307
+ if (trailingToolMessage.status === "error") controller.enqueue({
1308
+ type: "tool-output-error",
1309
+ toolCallId: toolCall.id,
1310
+ errorText: typeof trailingToolMessage.content === "string" ? trailingToolMessage.content : "Tool execution failed"
1311
+ });
1312
+ else controller.enqueue({
1313
+ type: "tool-output-available",
1314
+ toolCallId: toolCall.id,
1315
+ output: trailingToolMessage.content
1316
+ });
1317
+ }
1318
+ }
1319
+ /**
1320
+ * Check for reasoning content that wasn't streamed
1321
+ * Use reasoning block ID for deduplication as it's consistent across streaming and values.
1322
+ *
1323
+ * IMPORTANT: Handle two cases differently:
1324
+ * 1. Message has reasoning WITHOUT tool_calls → emit reasoning (pure reasoning case)
1325
+ * 2. Message has reasoning WITH tool_calls → only emit if streamed this request
1326
+ * (When resuming from HITL interrupt, historical messages have both reasoning
1327
+ * AND tool_calls. We skip those to avoid duplicate reasoning entries.)
1328
+ */
1329
+ const reasoningId = extractReasoningId(msg);
1330
+ const wasStreamedThisRequest = messageSeen.has(msgId);
1331
+ const hasToolCalls = toolCalls && toolCalls.length > 0;
1332
+ if (reasoningId && !emittedReasoningIds.has(reasoningId) && (wasStreamedThisRequest || !hasToolCalls)) {
1333
+ /**
1334
+ * Use extractReasoningFromValuesMessage which extracts from response_metadata.output
1335
+ * This is the full accumulated reasoning, not deltas
1336
+ */
1337
+ const reasoning = extractReasoningFromValuesMessage(msg);
1338
+ if (reasoning) {
1339
+ controller.enqueue({
1340
+ type: "reasoning-start",
1341
+ id: msgId
1342
+ });
1343
+ controller.enqueue({
1344
+ type: "reasoning-delta",
1345
+ delta: reasoning,
1346
+ id: msgId
1347
+ });
1348
+ controller.enqueue({
1349
+ type: "reasoning-end",
1350
+ id: msgId
1351
+ });
1352
+ emittedReasoningIds.add(reasoningId);
1353
+ }
1354
+ }
1355
+ const valuesCitations = extractCitationsFromContentBlocks(msg);
1356
+ if (valuesCitations.length > 0) emitSourceChunks(valuesCitations, msgId, state.emittedSourceIds, controller);
1357
+ }
1358
+ }
1359
+ }
1360
+ /**
1361
+ * Handle Human-in-the-Loop interrupts
1362
+ * When HITL middleware pauses execution, the interrupt data is in __interrupt__
1363
+ * Note: This is outside the 'messages' check because interrupt can come as a separate event
1364
+ */
1365
+ if (data != null && typeof data === "object") {
1366
+ const interrupt = data.__interrupt__;
1367
+ if (Array.isArray(interrupt) && interrupt.length > 0) for (const interruptItem of interrupt) {
1368
+ const interruptValue = interruptItem?.value;
1369
+ if (!interruptValue) continue;
1370
+ /**
1371
+ * Support both camelCase (JS SDK) and snake_case (Python SDK)
1372
+ */
1373
+ const actionRequests = interruptValue.actionRequests || interruptValue.action_requests;
1374
+ if (!Array.isArray(actionRequests)) continue;
1375
+ for (const actionRequest of actionRequests) {
1376
+ const toolName = actionRequest.name;
1377
+ /**
1378
+ * Support both 'args' (JS SDK) and 'arguments' (Python SDK)
1379
+ */
1380
+ const input = actionRequest.args || actionRequest.arguments;
1381
+ /**
1382
+ * Look up the original tool call ID using the name+args key
1383
+ * Fall back to action request ID or generate one if not found
1384
+ */
1385
+ const toolCallKey = `${toolName}:${JSON.stringify(input)}`;
1386
+ const toolCallId = emittedToolCallsByKey.get(toolCallKey) || actionRequest.id || `hitl-${toolName}-${Date.now()}`;
1387
+ /**
1388
+ * First emit tool-input-start then tool-input-available
1389
+ * so the UI knows what tool is being called with proper lifecycle
1390
+ */
1391
+ if (!emittedToolCalls.has(toolCallId)) {
1392
+ markToolCallStarted(state, toolCallId, eventNamespace);
1393
+ emittedToolCallsByKey.set(toolCallKey, toolCallId);
1394
+ controller.enqueue({
1395
+ type: "tool-input-start",
1396
+ toolCallId,
1397
+ toolName,
1398
+ dynamic: true
1399
+ });
1400
+ markToolInputEmitted(state, toolCallId, eventNamespace);
1401
+ controller.enqueue({
1402
+ type: "tool-input-available",
1403
+ toolCallId,
1404
+ toolName,
1405
+ input,
1406
+ dynamic: true
1407
+ });
1408
+ }
1409
+ /**
1410
+ * Then emit tool-approval-request to mark it as awaiting approval
1411
+ */
1412
+ controller.enqueue({
1413
+ type: "tool-approval-request",
1414
+ approvalId: toolCallId,
1415
+ toolCallId
1416
+ });
1417
+ }
1418
+ }
1419
+ }
1420
+ }
1309
1421
  }
1310
-
1311
- // src/adapter.ts
1422
+ //#endregion
1423
+ //#region src/adapter.ts
1424
+ /**
1425
+ * Converts AI SDK UIMessages to LangChain BaseMessage objects.
1426
+ *
1427
+ * This function transforms the AI SDK's message format into LangChain's message
1428
+ * format, enabling seamless integration between the two frameworks.
1429
+ *
1430
+ * @param messages - Array of AI SDK UIMessage objects to convert.
1431
+ * @returns Promise resolving to an array of LangChain BaseMessage objects.
1432
+ *
1433
+ * @example
1434
+ * ```ts
1435
+ * import { toBaseMessages } from '@ai-sdk/langchain';
1436
+ *
1437
+ * const langchainMessages = await toBaseMessages(uiMessages);
1438
+ *
1439
+ * // Use with LangChain
1440
+ * const response = await model.invoke(langchainMessages);
1441
+ * ```
1442
+ */
1312
1443
  async function toBaseMessages(messages) {
1313
- const modelMessages = await convertToModelMessages(messages);
1314
- return convertModelMessages(modelMessages);
1444
+ return convertModelMessages(await convertToModelMessages(messages));
1315
1445
  }
1446
+ /**
1447
+ * Converts ModelMessages to LangChain BaseMessage objects.
1448
+ *
1449
+ * @param modelMessages - Array of ModelMessage objects from convertToModelMessages.
1450
+ * @returns Array of LangChain BaseMessage objects.
1451
+ */
1316
1452
  function convertModelMessages(modelMessages) {
1317
- const result = [];
1318
- for (const message of modelMessages) {
1319
- switch (message.role) {
1320
- case "tool": {
1321
- for (const item of message.content) {
1322
- if (isToolResultPart(item)) {
1323
- result.push(convertToolResultPart(item));
1324
- }
1325
- }
1326
- break;
1327
- }
1328
- case "assistant": {
1329
- result.push(convertAssistantContent(message.content));
1330
- break;
1331
- }
1332
- case "system": {
1333
- result.push(new SystemMessage({ content: message.content }));
1334
- break;
1335
- }
1336
- case "user": {
1337
- result.push(convertUserContent(message.content));
1338
- break;
1339
- }
1340
- }
1341
- }
1342
- return result;
1453
+ const result = [];
1454
+ for (const message of modelMessages) switch (message.role) {
1455
+ case "tool":
1456
+ for (const item of message.content) if (isToolResultPart(item)) result.push(convertToolResultPart(item));
1457
+ break;
1458
+ case "assistant":
1459
+ result.push(convertAssistantContent(message.content));
1460
+ break;
1461
+ case "system":
1462
+ result.push(new SystemMessage({ content: message.content }));
1463
+ break;
1464
+ case "user": result.push(convertUserContent(message.content));
1465
+ }
1466
+ return result;
1343
1467
  }
1468
+ /**
1469
+ * Type guard to check if a value is a streamEvents event object.
1470
+ * streamEvents produces objects with `event` and `data` properties.
1471
+ *
1472
+ * @param value - The value to check.
1473
+ * @returns True if the value is a streamEvents event object.
1474
+ */
1344
1475
  function isStreamEventsEvent(value) {
1345
- if (value == null || typeof value !== "object") return false;
1346
- const obj = value;
1347
- if (!("event" in obj) || typeof obj.event !== "string") return false;
1348
- if (!("data" in obj)) return false;
1349
- return obj.data === null || typeof obj.data === "object";
1476
+ if (value == null || typeof value !== "object") return false;
1477
+ const obj = value;
1478
+ if (!("event" in obj) || typeof obj.event !== "string") return false;
1479
+ if (!("data" in obj)) return false;
1480
+ return obj.data === null || typeof obj.data === "object";
1350
1481
  }
1482
+ /**
1483
+ * Processes a streamEvents event and emits UI message chunks.
1484
+ *
1485
+ * @param event - The streamEvents event to process.
1486
+ * @param state - The state for tracking stream progress.
1487
+ * @param controller - The controller to emit UI message chunks.
1488
+ */
1351
1489
  function processStreamEventsEvent(event, state, controller) {
1352
- if (event.run_id && !state.started) {
1353
- state.messageId = event.run_id;
1354
- }
1355
- if (!event.data) return;
1356
- switch (event.event) {
1357
- case "on_chat_model_start": {
1358
- if (state.reasoningStarted) {
1359
- controller.enqueue({
1360
- type: "reasoning-end",
1361
- id: state.reasoningMessageId != null ? state.reasoningMessageId : state.messageId
1362
- });
1363
- state.reasoningStarted = false;
1364
- state.reasoningMessageId = null;
1365
- }
1366
- if (state.textStarted) {
1367
- controller.enqueue({
1368
- type: "text-end",
1369
- id: state.textMessageId != null ? state.textMessageId : state.messageId
1370
- });
1371
- state.textStarted = false;
1372
- state.textMessageId = null;
1373
- }
1374
- const runId = event.run_id || event.data.run_id;
1375
- if (runId) {
1376
- state.messageId = runId;
1377
- }
1378
- break;
1379
- }
1380
- case "on_chat_model_stream": {
1381
- const chunk = event.data.chunk;
1382
- if (chunk && typeof chunk === "object") {
1383
- const chunkId = chunk.id;
1384
- if (chunkId) {
1385
- state.messageId = chunkId;
1386
- }
1387
- const reasoning = extractReasoningFromContentBlocks(chunk);
1388
- if (reasoning) {
1389
- if (!state.reasoningStarted) {
1390
- state.reasoningMessageId = state.messageId;
1391
- controller.enqueue({
1392
- type: "reasoning-start",
1393
- id: state.messageId
1394
- });
1395
- state.reasoningStarted = true;
1396
- state.started = true;
1397
- }
1398
- controller.enqueue({
1399
- type: "reasoning-delta",
1400
- delta: reasoning,
1401
- id: state.reasoningMessageId ?? state.messageId
1402
- });
1403
- }
1404
- const content = chunk.content;
1405
- const text = typeof content === "string" ? content : Array.isArray(content) ? content.filter(
1406
- (c) => typeof c === "object" && c !== null && "type" in c && c.type === "text"
1407
- ).map((c) => c.text).join("") : "";
1408
- if (text) {
1409
- if (state.reasoningStarted && !state.textStarted) {
1410
- controller.enqueue({
1411
- type: "reasoning-end",
1412
- id: state.reasoningMessageId ?? state.messageId
1413
- });
1414
- state.reasoningStarted = false;
1415
- }
1416
- if (!state.textStarted) {
1417
- state.textMessageId = state.messageId;
1418
- controller.enqueue({ type: "text-start", id: state.messageId });
1419
- state.textStarted = true;
1420
- state.started = true;
1421
- }
1422
- controller.enqueue({
1423
- type: "text-delta",
1424
- delta: text,
1425
- id: state.textMessageId ?? state.messageId
1426
- });
1427
- }
1428
- const citations = extractCitationsFromContentBlocks(chunk);
1429
- if (citations.length > 0) {
1430
- emitSourceChunks(
1431
- citations,
1432
- state.messageId,
1433
- state.emittedSourceIds,
1434
- controller
1435
- );
1436
- }
1437
- }
1438
- break;
1439
- }
1440
- case "on_tool_start": {
1441
- const runId = event.run_id || event.data.run_id;
1442
- const name = event.name || event.data.name;
1443
- if (runId && name) {
1444
- controller.enqueue({
1445
- type: "tool-input-start",
1446
- toolCallId: runId,
1447
- toolName: name,
1448
- dynamic: true
1449
- });
1450
- }
1451
- break;
1452
- }
1453
- case "on_tool_end": {
1454
- const runId = event.run_id || event.data.run_id;
1455
- const output = event.data.output;
1456
- if (runId) {
1457
- controller.enqueue({
1458
- type: "tool-output-available",
1459
- toolCallId: runId,
1460
- output
1461
- });
1462
- }
1463
- break;
1464
- }
1465
- }
1490
+ /**
1491
+ * Capture run_id from event level if available (streamEvents v2 format)
1492
+ */
1493
+ if (event.run_id && !state.started) state.messageId = event.run_id;
1494
+ /**
1495
+ * Skip events with null/undefined data
1496
+ */
1497
+ if (!event.data) return;
1498
+ switch (event.event) {
1499
+ case "on_chat_model_start": {
1500
+ /**
1501
+ * End the previous model turn's reasoning stream before a new LLM invocation.
1502
+ * Without this, reasoning-delta chunks from the next turn could attach to the
1503
+ * wrong reasoning part in the UI message stream.
1504
+ */
1505
+ if (state.reasoningStarted) {
1506
+ controller.enqueue({
1507
+ type: "reasoning-end",
1508
+ id: state.reasoningMessageId != null ? state.reasoningMessageId : state.messageId
1509
+ });
1510
+ state.reasoningStarted = false;
1511
+ state.reasoningMessageId = null;
1512
+ }
1513
+ /**
1514
+ * End the previous model turn's text stream before a new LLM invocation.
1515
+ * The streamEvents adapter otherwise leaves `textStarted` true, so all later
1516
+ * text-delta chunks append to the first text part — tools still grow the parts
1517
+ * array, which breaks chronological order in the assistant message.
1518
+ */
1519
+ if (state.textStarted) {
1520
+ controller.enqueue({
1521
+ type: "text-end",
1522
+ id: state.textMessageId != null ? state.textMessageId : state.messageId
1523
+ });
1524
+ state.textStarted = false;
1525
+ state.textMessageId = null;
1526
+ }
1527
+ /**
1528
+ * Handle model start — capture message metadata if available.
1529
+ * `run_id` is on the event in streamEvents v2; fall back to `data` for compatibility.
1530
+ */
1531
+ const runId = event.run_id || event.data.run_id;
1532
+ if (runId) state.messageId = runId;
1533
+ break;
1534
+ }
1535
+ case "on_chat_model_stream": {
1536
+ /**
1537
+ * Handle streaming token chunks
1538
+ */
1539
+ const chunk = event.data.chunk;
1540
+ if (chunk && typeof chunk === "object") {
1541
+ /**
1542
+ * Get message ID from chunk if available
1543
+ */
1544
+ const chunkId = chunk.id;
1545
+ if (chunkId) state.messageId = chunkId;
1546
+ /**
1547
+ * Handle reasoning content from contentBlocks
1548
+ */
1549
+ const reasoning = extractReasoningFromContentBlocks(chunk);
1550
+ if (reasoning) {
1551
+ if (!state.reasoningStarted) {
1552
+ state.reasoningMessageId = state.messageId;
1553
+ controller.enqueue({
1554
+ type: "reasoning-start",
1555
+ id: state.messageId
1556
+ });
1557
+ state.reasoningStarted = true;
1558
+ state.started = true;
1559
+ }
1560
+ controller.enqueue({
1561
+ type: "reasoning-delta",
1562
+ delta: reasoning,
1563
+ id: state.reasoningMessageId ?? state.messageId
1564
+ });
1565
+ }
1566
+ /**
1567
+ * Extract text content from chunk
1568
+ */
1569
+ const content = chunk.content;
1570
+ const text = typeof content === "string" ? content : Array.isArray(content) ? content.filter((c) => typeof c === "object" && c !== null && "type" in c && c.type === "text").map((c) => c.text).join("") : "";
1571
+ if (text) {
1572
+ /**
1573
+ * If reasoning was streamed before text, close reasoning first
1574
+ */
1575
+ if (state.reasoningStarted && !state.textStarted) {
1576
+ controller.enqueue({
1577
+ type: "reasoning-end",
1578
+ id: state.reasoningMessageId ?? state.messageId
1579
+ });
1580
+ state.reasoningStarted = false;
1581
+ }
1582
+ if (!state.textStarted) {
1583
+ state.textMessageId = state.messageId;
1584
+ controller.enqueue({
1585
+ type: "text-start",
1586
+ id: state.messageId
1587
+ });
1588
+ state.textStarted = true;
1589
+ state.started = true;
1590
+ }
1591
+ controller.enqueue({
1592
+ type: "text-delta",
1593
+ delta: text,
1594
+ id: state.textMessageId ?? state.messageId
1595
+ });
1596
+ }
1597
+ const citations = extractCitationsFromContentBlocks(chunk);
1598
+ if (citations.length > 0) emitSourceChunks(citations, state.messageId, state.emittedSourceIds, controller);
1599
+ }
1600
+ break;
1601
+ }
1602
+ case "on_tool_start": {
1603
+ /**
1604
+ * Handle tool call start
1605
+ * run_id and name are at event level in v2, check data for backwards compatibility
1606
+ */
1607
+ const runId = event.run_id || event.data.run_id;
1608
+ const name = event.name || event.data.name;
1609
+ if (runId && name) controller.enqueue({
1610
+ type: "tool-input-start",
1611
+ toolCallId: runId,
1612
+ toolName: name,
1613
+ dynamic: true
1614
+ });
1615
+ break;
1616
+ }
1617
+ case "on_tool_end": {
1618
+ /**
1619
+ * Handle tool call end
1620
+ * run_id is at event level in v2, check data for backwards compatibility
1621
+ */
1622
+ const runId = event.run_id || event.data.run_id;
1623
+ const output = event.data.output;
1624
+ if (runId) controller.enqueue({
1625
+ type: "tool-output-available",
1626
+ toolCallId: runId,
1627
+ output
1628
+ });
1629
+ break;
1630
+ }
1631
+ }
1466
1632
  }
1633
+ /**
1634
+ * Checks if an error is an abort error.
1635
+ */
1467
1636
  function isAbortError(error) {
1468
- if (error instanceof Error) {
1469
- return error.name === "AbortError" || error instanceof DOMException && error.name === "AbortError";
1470
- }
1471
- return false;
1637
+ if (error instanceof Error) return error.name === "AbortError" || error instanceof DOMException && error.name === "AbortError";
1638
+ return false;
1472
1639
  }
1640
+ /**
1641
+ * Converts a LangChain stream to an AI SDK UIMessageStream.
1642
+ *
1643
+ * This function automatically detects the stream type and handles:
1644
+ * - Direct model streams (AsyncIterable from `model.stream()`)
1645
+ * - LangGraph streams (ReadableStream with `streamMode: ['values', 'messages']`)
1646
+ * - streamEvents streams (from `agent.streamEvents()` or `model.streamEvents()`)
1647
+ *
1648
+ * @param stream - A stream from LangChain model.stream(), graph.stream(), or streamEvents().
1649
+ * @param options - Optional lifecycle callbacks and outer chunk controls.
1650
+ * @returns A ReadableStream of UIMessageChunk objects.
1651
+ *
1652
+ * @example
1653
+ * ```ts
1654
+ * // With a direct model stream
1655
+ * const model = new ChatOpenAI({ model: 'gpt-4o-mini' });
1656
+ * const stream = await model.stream(messages);
1657
+ * return createUIMessageStreamResponse({
1658
+ * stream: toUIMessageStream(stream),
1659
+ * });
1660
+ *
1661
+ * // With a LangGraph stream
1662
+ * const graphStream = await graph.stream(
1663
+ * { messages },
1664
+ * { streamMode: ['values', 'messages'] }
1665
+ * );
1666
+ * return createUIMessageStreamResponse({
1667
+ * stream: toUIMessageStream(graphStream),
1668
+ * });
1669
+ *
1670
+ * // With streamEvents
1671
+ * const streamEvents = agent.streamEvents(
1672
+ * { messages },
1673
+ * { version: "v2" }
1674
+ * );
1675
+ * return createUIMessageStreamResponse({
1676
+ * stream: toUIMessageStream(streamEvents),
1677
+ * });
1678
+ *
1679
+ * // With callbacks for LangGraph state
1680
+ * const graphStream = await graph.stream(
1681
+ * { messages },
1682
+ * { streamMode: ['values', 'messages'] }
1683
+ * );
1684
+ * return createUIMessageStreamResponse({
1685
+ * stream: toUIMessageStream<MyStateType>(graphStream, {
1686
+ * onFinish: async (finalState) => {
1687
+ * if (finalState) {
1688
+ * await saveToDatabase(finalState);
1689
+ * }
1690
+ * },
1691
+ * onError: (error) => console.error('Stream failed:', error),
1692
+ * onAbort: () => console.log('Stream aborted'),
1693
+ * }),
1694
+ * });
1695
+ * ```
1696
+ */
1473
1697
  function toUIMessageStream(stream, options) {
1474
- const { sendStart = true, sendFinish = true } = options ?? {};
1475
- const textChunks = [];
1476
- let lastValuesData;
1477
- const modelState = {
1478
- started: false,
1479
- messageId: "langchain-msg-1",
1480
- reasoningStarted: false,
1481
- textStarted: false,
1482
- /** Track the ID used for text-start to ensure text-end uses the same ID */
1483
- textMessageId: null,
1484
- /** Track the ID used for reasoning-start to ensure reasoning-end uses the same ID */
1485
- reasoningMessageId: null,
1486
- emittedSourceIds: /* @__PURE__ */ new Set()
1487
- };
1488
- const langGraphState = {
1489
- messageSeen: /* @__PURE__ */ new Map(),
1490
- messageNamespaces: /* @__PURE__ */ new Map(),
1491
- messageConcat: /* @__PURE__ */ new Map(),
1492
- messageIdsInCurrentStepByNamespace: /* @__PURE__ */ new Map(),
1493
- emittedToolCalls: /* @__PURE__ */ new Set(),
1494
- emittedToolCallsInCurrentStepByNamespace: /* @__PURE__ */ new Map(),
1495
- emittedToolInputs: /* @__PURE__ */ new Set(),
1496
- emittedToolInputsInCurrentStepByNamespace: /* @__PURE__ */ new Map(),
1497
- emittedToolOutputMessageIds: /* @__PURE__ */ new Set(),
1498
- emittedToolOutputCallIds: /* @__PURE__ */ new Set(),
1499
- emittedToolOutputsInCurrentStepByNamespace: /* @__PURE__ */ new Map(),
1500
- unfinishedToolCallsByNamespace: /* @__PURE__ */ new Map(),
1501
- emittedImages: /* @__PURE__ */ new Set(),
1502
- emittedReasoningIds: /* @__PURE__ */ new Set(),
1503
- messageReasoningIds: /* @__PURE__ */ new Map(),
1504
- toolCallInfoByIndex: /* @__PURE__ */ new Map(),
1505
- currentStepsByNamespace: /* @__PURE__ */ new Map(),
1506
- emittedToolCallsByKey: /* @__PURE__ */ new Map(),
1507
- emittedSourceIds: /* @__PURE__ */ new Set()
1508
- };
1509
- let streamType = null;
1510
- const getAsyncIterator = () => {
1511
- if (Symbol.asyncIterator in stream) {
1512
- return stream[Symbol.asyncIterator]();
1513
- }
1514
- const reader = stream.getReader();
1515
- return {
1516
- async next() {
1517
- const { done, value } = await reader.read();
1518
- return { done, value };
1519
- }
1520
- };
1521
- };
1522
- const iterator = getAsyncIterator();
1523
- const createCallbackController = (originalController) => {
1524
- return {
1525
- get desiredSize() {
1526
- return originalController.desiredSize;
1527
- },
1528
- close: () => originalController.close(),
1529
- error: (e) => originalController.error(e),
1530
- enqueue: (chunk) => {
1531
- if (chunk.type === "text-delta" && chunk.delta) {
1532
- if (options?.onFinal) {
1533
- textChunks.push(chunk.delta);
1534
- }
1535
- options?.onToken?.(chunk.delta);
1536
- options?.onText?.(chunk.delta);
1537
- }
1538
- originalController.enqueue(chunk);
1539
- }
1540
- };
1541
- };
1542
- return new ReadableStream({
1543
- async start(controller) {
1544
- await options?.onStart?.();
1545
- const wrappedController = createCallbackController(controller);
1546
- if (sendStart) {
1547
- controller.enqueue({ type: "start" });
1548
- }
1549
- try {
1550
- while (true) {
1551
- const { done, value } = await iterator.next();
1552
- if (done) break;
1553
- if (streamType === null) {
1554
- if (Array.isArray(value)) {
1555
- streamType = "langgraph";
1556
- } else if (isStreamEventsEvent(value)) {
1557
- streamType = "streamEvents";
1558
- } else {
1559
- streamType = "model";
1560
- }
1561
- }
1562
- if (streamType === "model") {
1563
- processModelChunk(
1564
- value,
1565
- modelState,
1566
- wrappedController
1567
- );
1568
- } else if (streamType === "streamEvents") {
1569
- processStreamEventsEvent(
1570
- value,
1571
- modelState,
1572
- wrappedController
1573
- );
1574
- } else {
1575
- const eventArray = value;
1576
- const [, type, data] = parseLangGraphEvent(eventArray);
1577
- if (type === "values") {
1578
- lastValuesData = data;
1579
- }
1580
- processLangGraphEvent(
1581
- eventArray,
1582
- langGraphState,
1583
- wrappedController
1584
- );
1585
- }
1586
- }
1587
- if (streamType === "model" || streamType === "streamEvents") {
1588
- if (modelState.reasoningStarted) {
1589
- controller.enqueue({
1590
- type: "reasoning-end",
1591
- id: modelState.reasoningMessageId ?? modelState.messageId
1592
- });
1593
- }
1594
- if (modelState.textStarted) {
1595
- controller.enqueue({
1596
- type: "text-end",
1597
- id: modelState.textMessageId ?? modelState.messageId
1598
- });
1599
- }
1600
- if (sendFinish) {
1601
- controller.enqueue({ type: "finish" });
1602
- }
1603
- } else if (streamType === "langgraph") {
1604
- for (const [id, seen] of langGraphState.messageSeen) {
1605
- const providerMetadata = getLangGraphProviderMetadata(
1606
- langGraphState.messageNamespaces.get(id)
1607
- );
1608
- if (seen.text) {
1609
- controller.enqueue({
1610
- type: "text-end",
1611
- id,
1612
- ...providerMetadata !== void 0 && { providerMetadata }
1613
- });
1614
- }
1615
- if (seen.reasoning) {
1616
- controller.enqueue({
1617
- type: "reasoning-end",
1618
- id,
1619
- ...providerMetadata !== void 0 && { providerMetadata }
1620
- });
1621
- }
1622
- }
1623
- if (langGraphState.currentStepsByNamespace.size > 0) {
1624
- controller.enqueue({ type: "finish-step" });
1625
- }
1626
- if (sendFinish) {
1627
- controller.enqueue({ type: "finish" });
1628
- }
1629
- }
1630
- await options?.onFinal?.(textChunks.join(""));
1631
- await options?.onFinish?.(lastValuesData);
1632
- } catch (error) {
1633
- const errorObj = error instanceof Error ? error : new Error(String(error));
1634
- await options?.onFinal?.(textChunks.join(""));
1635
- if (isAbortError(error)) {
1636
- await options?.onAbort?.();
1637
- } else {
1638
- await options?.onError?.(errorObj);
1639
- }
1640
- controller.enqueue({
1641
- type: "error",
1642
- errorText: errorObj.message
1643
- });
1644
- } finally {
1645
- controller.close();
1646
- }
1647
- }
1648
- });
1698
+ const { sendStart = true, sendFinish = true } = options ?? {};
1699
+ /**
1700
+ * Track text chunks for onFinal callback
1701
+ */
1702
+ const textChunks = [];
1703
+ /** Last LangGraph values event data for onFinish callback */
1704
+ let lastValuesData;
1705
+ /**
1706
+ * State for model stream handling
1707
+ */
1708
+ const modelState = {
1709
+ started: false,
1710
+ messageId: "langchain-msg-1",
1711
+ reasoningStarted: false,
1712
+ textStarted: false,
1713
+ /** Track the ID used for text-start to ensure text-end uses the same ID */
1714
+ textMessageId: null,
1715
+ /** Track the ID used for reasoning-start to ensure reasoning-end uses the same ID */
1716
+ reasoningMessageId: null,
1717
+ emittedSourceIds: /* @__PURE__ */ new Set()
1718
+ };
1719
+ /**
1720
+ * State for LangGraph stream handling
1721
+ */
1722
+ const langGraphState = {
1723
+ messageSeen: /* @__PURE__ */ new Map(),
1724
+ messageNamespaces: /* @__PURE__ */ new Map(),
1725
+ messageConcat: /* @__PURE__ */ new Map(),
1726
+ messageIdsInCurrentStepByNamespace: /* @__PURE__ */ new Map(),
1727
+ emittedToolCalls: /* @__PURE__ */ new Set(),
1728
+ emittedToolCallsInCurrentStepByNamespace: /* @__PURE__ */ new Map(),
1729
+ emittedToolInputs: /* @__PURE__ */ new Set(),
1730
+ emittedToolInputsInCurrentStepByNamespace: /* @__PURE__ */ new Map(),
1731
+ emittedToolOutputMessageIds: /* @__PURE__ */ new Set(),
1732
+ emittedToolOutputCallIds: /* @__PURE__ */ new Set(),
1733
+ emittedToolOutputsInCurrentStepByNamespace: /* @__PURE__ */ new Map(),
1734
+ unfinishedToolCallsByNamespace: /* @__PURE__ */ new Map(),
1735
+ emittedImages: /* @__PURE__ */ new Set(),
1736
+ emittedReasoningIds: /* @__PURE__ */ new Set(),
1737
+ messageReasoningIds: /* @__PURE__ */ new Map(),
1738
+ toolCallInfoByIndex: /* @__PURE__ */ new Map(),
1739
+ currentStepsByNamespace: /* @__PURE__ */ new Map(),
1740
+ emittedToolCallsByKey: /* @__PURE__ */ new Map(),
1741
+ emittedSourceIds: /* @__PURE__ */ new Set()
1742
+ };
1743
+ /**
1744
+ * Track detected stream type: null = not yet detected
1745
+ */
1746
+ let streamType = null;
1747
+ /**
1748
+ * Get async iterator from the stream (works for both AsyncIterable and ReadableStream)
1749
+ */
1750
+ const getAsyncIterator = () => {
1751
+ if (Symbol.asyncIterator in stream) return stream[Symbol.asyncIterator]();
1752
+ /**
1753
+ * For ReadableStream without Symbol.asyncIterator
1754
+ */
1755
+ const reader = stream.getReader();
1756
+ return { async next() {
1757
+ const { done, value } = await reader.read();
1758
+ return {
1759
+ done,
1760
+ value
1761
+ };
1762
+ } };
1763
+ };
1764
+ const iterator = getAsyncIterator();
1765
+ /**
1766
+ * Create a wrapper around the controller to intercept text chunks for callbacks
1767
+ */
1768
+ const createCallbackController = (originalController) => {
1769
+ return {
1770
+ get desiredSize() {
1771
+ return originalController.desiredSize;
1772
+ },
1773
+ close: () => originalController.close(),
1774
+ error: (e) => originalController.error(e),
1775
+ enqueue: (chunk) => {
1776
+ /**
1777
+ * Intercept text-delta chunks for callbacks
1778
+ */
1779
+ if (chunk.type === "text-delta" && chunk.delta) {
1780
+ if (options?.onFinal) textChunks.push(chunk.delta);
1781
+ options?.onToken?.(chunk.delta);
1782
+ options?.onText?.(chunk.delta);
1783
+ }
1784
+ originalController.enqueue(chunk);
1785
+ }
1786
+ };
1787
+ };
1788
+ return new ReadableStream({ async start(controller) {
1789
+ await options?.onStart?.();
1790
+ const wrappedController = createCallbackController(controller);
1791
+ if (sendStart) controller.enqueue({ type: "start" });
1792
+ try {
1793
+ while (true) {
1794
+ const { done, value } = await iterator.next();
1795
+ if (done) break;
1796
+ /**
1797
+ * Detect stream type on first value
1798
+ */
1799
+ if (streamType === null) {
1800
+ if (Array.isArray(value)) streamType = "langgraph";
1801
+ else if (isStreamEventsEvent(value)) streamType = "streamEvents";
1802
+ else streamType = "model";
1803
+ }
1804
+ /**
1805
+ * Process based on detected type
1806
+ */
1807
+ if (streamType === "model") processModelChunk(value, modelState, wrappedController);
1808
+ else if (streamType === "streamEvents") processStreamEventsEvent(value, modelState, wrappedController);
1809
+ else {
1810
+ const eventArray = value;
1811
+ const [, type, data] = parseLangGraphEvent(eventArray);
1812
+ if (type === "values") lastValuesData = data;
1813
+ processLangGraphEvent(eventArray, langGraphState, wrappedController);
1814
+ }
1815
+ }
1816
+ /**
1817
+ * Finalize based on stream type
1818
+ */
1819
+ if (streamType === "model" || streamType === "streamEvents") {
1820
+ if (modelState.reasoningStarted) controller.enqueue({
1821
+ type: "reasoning-end",
1822
+ id: modelState.reasoningMessageId ?? modelState.messageId
1823
+ });
1824
+ if (modelState.textStarted)
1825
+ /**
1826
+ * Use the same ID that was used for text-start
1827
+ */
1828
+ controller.enqueue({
1829
+ type: "text-end",
1830
+ id: modelState.textMessageId ?? modelState.messageId
1831
+ });
1832
+ if (sendFinish) controller.enqueue({ type: "finish" });
1833
+ } else if (streamType === "langgraph") {
1834
+ /**
1835
+ * Close any open text/reasoning parts before finishing.
1836
+ * This handles streams without values events (e.g. streamMode: 'messages')
1837
+ * where the values handler never ran to emit *-end events.
1838
+ */
1839
+ for (const [id, seen] of langGraphState.messageSeen) {
1840
+ const providerMetadata = getLangGraphProviderMetadata(langGraphState.messageNamespaces.get(id));
1841
+ if (seen.text) controller.enqueue({
1842
+ type: "text-end",
1843
+ id,
1844
+ ...providerMetadata !== void 0 && { providerMetadata }
1845
+ });
1846
+ if (seen.reasoning) controller.enqueue({
1847
+ type: "reasoning-end",
1848
+ id,
1849
+ ...providerMetadata !== void 0 && { providerMetadata }
1850
+ });
1851
+ }
1852
+ /**
1853
+ * Emit finish-step if a step was started
1854
+ */
1855
+ if (langGraphState.currentStepsByNamespace.size > 0) controller.enqueue({ type: "finish-step" });
1856
+ if (sendFinish) controller.enqueue({ type: "finish" });
1857
+ }
1858
+ /**
1859
+ * Call onFinal callback with aggregated text
1860
+ */
1861
+ await options?.onFinal?.(textChunks.join(""));
1862
+ await options?.onFinish?.(lastValuesData);
1863
+ } catch (error) {
1864
+ const errorObj = error instanceof Error ? error : new Error(String(error));
1865
+ await options?.onFinal?.(textChunks.join(""));
1866
+ if (isAbortError(error)) await options?.onAbort?.();
1867
+ else await options?.onError?.(errorObj);
1868
+ controller.enqueue({
1869
+ type: "error",
1870
+ errorText: errorObj.message
1871
+ });
1872
+ } finally {
1873
+ controller.close();
1874
+ }
1875
+ } });
1649
1876
  }
1650
-
1651
- // src/transport.ts
1652
- import {
1653
- RemoteGraph
1654
- } from "@langchain/langgraph/remote";
1877
+ //#endregion
1878
+ //#region src/transport.ts
1879
+ /**
1880
+ * A ChatTransport implementation for LangSmith/LangGraph deployments.
1881
+ *
1882
+ * This transport enables seamless integration between the AI SDK's useChat hook
1883
+ * and LangSmith deployed LangGraph agents.
1884
+ *
1885
+ * @example
1886
+ * ```ts
1887
+ * import { LangSmithDeploymentTransport } from '@ai-sdk/langchain';
1888
+ *
1889
+ * // Use with useChat
1890
+ * const { messages, input, handleSubmit } = useChat({
1891
+ * transport: new LangSmithDeploymentTransport({
1892
+ * url: 'https://your-deployment.us.langgraph.app',
1893
+ * apiKey: 'my-api-key',
1894
+ * }),
1895
+ * });
1896
+ * ```
1897
+ */
1655
1898
  var LangSmithDeploymentTransport = class {
1656
- constructor(options) {
1657
- this.graph = new RemoteGraph({
1658
- ...options,
1659
- graphId: options.graphId ?? "agent"
1660
- });
1661
- }
1662
- async sendMessages(options) {
1663
- const baseMessages = await toBaseMessages(options.messages);
1664
- const stream = await this.graph.stream(
1665
- { messages: baseMessages },
1666
- { streamMode: ["values", "messages"] }
1667
- );
1668
- return toUIMessageStream(
1669
- stream
1670
- );
1671
- }
1672
- async reconnectToStream(_options) {
1673
- throw new Error("Method not implemented.");
1674
- }
1675
- };
1676
- export {
1677
- LangSmithDeploymentTransport,
1678
- convertModelMessages,
1679
- toBaseMessages,
1680
- toUIMessageStream
1899
+ constructor(options) {
1900
+ this.graph = new RemoteGraph({
1901
+ ...options,
1902
+ graphId: options.graphId ?? "agent"
1903
+ });
1904
+ }
1905
+ async sendMessages(options) {
1906
+ const baseMessages = await toBaseMessages(options.messages);
1907
+ return toUIMessageStream(await this.graph.stream({ messages: baseMessages }, { streamMode: ["values", "messages"] }));
1908
+ }
1909
+ async reconnectToStream(_options) {
1910
+ throw new Error("Method not implemented.");
1911
+ }
1681
1912
  };
1913
+ //#endregion
1914
+ export { LangSmithDeploymentTransport, convertModelMessages, toBaseMessages, toUIMessageStream };
1915
+
1682
1916
  //# sourceMappingURL=index.js.map