@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/CHANGELOG.md +30 -0
- package/dist/index.d.ts +61 -58
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1811 -1577
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
package/dist/index.js
CHANGED
|
@@ -1,1682 +1,1916 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
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
|
-
|
|
21
|
-
langchain: {
|
|
22
|
-
namespace
|
|
23
|
-
}
|
|
24
|
-
};
|
|
24
|
+
return namespace === void 0 ? void 0 : { langchain: { namespace } };
|
|
25
25
|
}
|
|
26
26
|
function addLangGraphNamespace(chunk, namespace) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
138
|
-
return `${prefix}.${fileExtension}`;
|
|
124
|
+
return `${prefix}.${mediaType.split("/")[1] || "bin"}`;
|
|
139
125
|
}
|
|
140
126
|
function convertImageToContentBlock(data, mediaType = "image/png") {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
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
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
-
|
|
354
|
-
|
|
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
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
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
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
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
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
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
|
-
|
|
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
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
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
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
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
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
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
|
-
|
|
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
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
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
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
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
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
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
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
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
|
-
|
|
728
|
+
return JSON.stringify(namespace ?? []);
|
|
628
729
|
}
|
|
629
730
|
function getOrCreateNamespaceSet(setsByNamespace, namespace) {
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
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
|
-
|
|
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
|
-
|
|
642
|
-
|
|
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
|
-
|
|
651
|
-
|
|
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
|
-
|
|
750
|
+
return state.unfinishedToolCallsByNamespace.get(namespace)?.has(toolCallId) === true;
|
|
657
751
|
}
|
|
658
752
|
function hasEmittedToolInputInCurrentStep(state, toolCallId, namespace) {
|
|
659
|
-
|
|
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
|
-
|
|
663
|
-
|
|
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
|
-
|
|
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
|
-
|
|
675
|
-
|
|
676
|
-
|
|
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
|
-
|
|
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
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
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
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
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
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
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
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
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
|
-
|
|
1469
|
-
|
|
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
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
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
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
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
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
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
|