@cloudflare/ai-chat 0.0.3 → 0.0.5
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 +28 -0
- package/dist/ai-chat-v5-migration.d.ts +0 -1
- package/dist/ai-chat-v5-migration.js.map +1 -1
- package/dist/index.d.ts +24 -8
- package/dist/index.js +406 -352
- package/dist/index.js.map +1 -1
- package/dist/react.d.ts +27 -48
- package/dist/react.js +18 -18
- package/dist/react.js.map +1 -1
- package/dist/types.d.ts +19 -47
- package/dist/types.js +11 -11
- package/dist/types.js.map +1 -1
- package/package.json +6 -9
- package/src/index.ts +752 -625
- package/src/react-tests/setup.ts +3 -0
- package/src/react-tests/use-agent-chat.test.tsx +35 -23
- package/src/react-tests/vitest.config.ts +2 -1
- package/src/tests/chat-context.test.ts +4 -20
- package/src/tests/chat-persistence.test.ts +15 -32
- package/src/tests/client-tool-duplicate-message.test.ts +10 -13
- package/src/tests/client-tools-broadcast.test.ts +1 -19
- package/src/tests/cloudflare-test.d.ts +5 -0
- package/src/tests/non-sse-response.test.ts +186 -0
- package/src/tests/resumable-streaming.test.ts +92 -72
- package/src/tests/test-utils.ts +39 -0
- package/src/tests/worker.ts +31 -9
package/src/index.ts
CHANGED
|
@@ -235,6 +235,14 @@ export class AIChatAgent<
|
|
|
235
235
|
*/
|
|
236
236
|
private _lastCleanupTime = 0;
|
|
237
237
|
|
|
238
|
+
/**
|
|
239
|
+
* Set of connection IDs that are pending stream resume.
|
|
240
|
+
* These connections have received CF_AGENT_STREAM_RESUMING but haven't sent ACK yet.
|
|
241
|
+
* They should be excluded from live stream broadcasts until they ACK.
|
|
242
|
+
* @internal
|
|
243
|
+
*/
|
|
244
|
+
private _pendingResumeConnections: Set<string> = new Set();
|
|
245
|
+
|
|
238
246
|
/** Array of chat messages for the current conversation */
|
|
239
247
|
messages: ChatMessage[];
|
|
240
248
|
|
|
@@ -286,6 +294,20 @@ export class AIChatAgent<
|
|
|
286
294
|
return _onConnect(connection, ctx);
|
|
287
295
|
};
|
|
288
296
|
|
|
297
|
+
// Wrap onClose to clean up pending resume connections
|
|
298
|
+
const _onClose = this.onClose.bind(this);
|
|
299
|
+
this.onClose = async (
|
|
300
|
+
connection: Connection,
|
|
301
|
+
code: number,
|
|
302
|
+
reason: string,
|
|
303
|
+
wasClean: boolean
|
|
304
|
+
) => {
|
|
305
|
+
// Clean up pending resume state for this connection
|
|
306
|
+
this._pendingResumeConnections.delete(connection.id);
|
|
307
|
+
// Call consumer's onClose
|
|
308
|
+
return _onClose(connection, code, reason, wasClean);
|
|
309
|
+
};
|
|
310
|
+
|
|
289
311
|
// Wrap onMessage
|
|
290
312
|
const _onMessage = this.onMessage.bind(this);
|
|
291
313
|
this.onMessage = async (connection: Connection, message: WSMessage) => {
|
|
@@ -395,6 +417,7 @@ export class AIChatAgent<
|
|
|
395
417
|
this._activeStreamId = null;
|
|
396
418
|
this._activeRequestId = null;
|
|
397
419
|
this._streamChunkIndex = 0;
|
|
420
|
+
this._pendingResumeConnections.clear();
|
|
398
421
|
this.messages = [];
|
|
399
422
|
this._broadcastChatMessage(
|
|
400
423
|
{ type: MessageType.CF_AGENT_CHAT_CLEAR },
|
|
@@ -418,6 +441,8 @@ export class AIChatAgent<
|
|
|
418
441
|
|
|
419
442
|
// Handle stream resume acknowledgment
|
|
420
443
|
if (data.type === MessageType.CF_AGENT_STREAM_RESUME_ACK) {
|
|
444
|
+
this._pendingResumeConnections.delete(connection.id);
|
|
445
|
+
|
|
421
446
|
if (
|
|
422
447
|
this._activeStreamId &&
|
|
423
448
|
this._activeRequestId &&
|
|
@@ -574,6 +599,10 @@ export class AIChatAgent<
|
|
|
574
599
|
return;
|
|
575
600
|
}
|
|
576
601
|
|
|
602
|
+
// Add connection to pending set - they'll be excluded from live broadcasts
|
|
603
|
+
// until they send ACK to receive the full stream replay
|
|
604
|
+
this._pendingResumeConnections.add(connection.id);
|
|
605
|
+
|
|
577
606
|
// Notify client - they will send ACK when ready
|
|
578
607
|
connection.send(
|
|
579
608
|
JSON.stringify({
|
|
@@ -726,6 +755,9 @@ export class AIChatAgent<
|
|
|
726
755
|
this._activeRequestId = null;
|
|
727
756
|
this._streamChunkIndex = 0;
|
|
728
757
|
|
|
758
|
+
// Clear pending resume connections - no active stream to resume
|
|
759
|
+
this._pendingResumeConnections.clear();
|
|
760
|
+
|
|
729
761
|
// Periodically clean up old streams (not on every completion)
|
|
730
762
|
this._maybeCleanupOldStreams();
|
|
731
763
|
}
|
|
@@ -756,7 +788,41 @@ export class AIChatAgent<
|
|
|
756
788
|
}
|
|
757
789
|
|
|
758
790
|
private _broadcastChatMessage(message: OutgoingMessage, exclude?: string[]) {
|
|
759
|
-
|
|
791
|
+
// Combine explicit exclusions with connections pending stream resume.
|
|
792
|
+
// Pending connections should not receive live stream chunks until they ACK,
|
|
793
|
+
// at which point they'll receive the full replay via _sendStreamChunks.
|
|
794
|
+
const allExclusions = [
|
|
795
|
+
...(exclude || []),
|
|
796
|
+
...this._pendingResumeConnections
|
|
797
|
+
];
|
|
798
|
+
this.broadcast(JSON.stringify(message), allExclusions);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/**
|
|
802
|
+
* Broadcasts a text event for non-SSE responses.
|
|
803
|
+
* This ensures plain text responses follow the AI SDK v5 stream protocol.
|
|
804
|
+
*
|
|
805
|
+
* @param streamId - The stream identifier for chunk storage
|
|
806
|
+
* @param event - The text event payload (text-start, text-delta with delta, or text-end)
|
|
807
|
+
* @param continuation - Whether this is a continuation of a previous stream
|
|
808
|
+
*/
|
|
809
|
+
private _broadcastTextEvent(
|
|
810
|
+
streamId: string,
|
|
811
|
+
event:
|
|
812
|
+
| { type: "text-start"; id: string }
|
|
813
|
+
| { type: "text-delta"; id: string; delta: string }
|
|
814
|
+
| { type: "text-end"; id: string },
|
|
815
|
+
continuation: boolean
|
|
816
|
+
) {
|
|
817
|
+
const body = JSON.stringify(event);
|
|
818
|
+
this._storeStreamChunk(streamId, body);
|
|
819
|
+
this._broadcastChatMessage({
|
|
820
|
+
body,
|
|
821
|
+
done: false,
|
|
822
|
+
id: event.id,
|
|
823
|
+
type: MessageType.CF_AGENT_USE_CHAT_RESPONSE,
|
|
824
|
+
...(continuation && { continuation: true })
|
|
825
|
+
});
|
|
760
826
|
}
|
|
761
827
|
|
|
762
828
|
private _loadMessagesFromDb(): ChatMessage[] {
|
|
@@ -1241,17 +1307,37 @@ export class AIChatAgent<
|
|
|
1241
1307
|
return true;
|
|
1242
1308
|
}
|
|
1243
1309
|
|
|
1244
|
-
private async
|
|
1310
|
+
private async _streamSSEReply(
|
|
1245
1311
|
id: string,
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1312
|
+
streamId: string,
|
|
1313
|
+
reader: ReadableStreamDefaultReader<Uint8Array>,
|
|
1314
|
+
message: ChatMessage,
|
|
1315
|
+
streamCompleted: { value: boolean },
|
|
1316
|
+
continuation = false
|
|
1249
1317
|
) {
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1318
|
+
let activeTextParts: Record<string, TextUIPart> = {};
|
|
1319
|
+
let activeReasoningParts: Record<string, ReasoningUIPart> = {};
|
|
1320
|
+
const partialToolCalls: Record<
|
|
1321
|
+
string,
|
|
1322
|
+
{ text: string; index: number; toolName: string; dynamic?: boolean }
|
|
1323
|
+
> = {};
|
|
1324
|
+
|
|
1325
|
+
/* Lazy loading ai sdk, because putting it in module scope is
|
|
1326
|
+
* causing issues with startup time.
|
|
1327
|
+
* The only place it's used is in _reply, which only matters after
|
|
1328
|
+
* a chat message is received.
|
|
1329
|
+
* So it's safe to delay loading it until a chat message is received.
|
|
1330
|
+
*/
|
|
1331
|
+
const { getToolName, isToolUIPart, parsePartialJson } = await import("ai");
|
|
1332
|
+
|
|
1333
|
+
streamCompleted.value = false;
|
|
1334
|
+
while (true) {
|
|
1335
|
+
const { done, value } = await reader.read();
|
|
1336
|
+
if (done) {
|
|
1337
|
+
// Mark the stream as completed
|
|
1338
|
+
this._completeStream(streamId);
|
|
1339
|
+
streamCompleted.value = true;
|
|
1340
|
+
// Send final completion signal
|
|
1255
1341
|
this._broadcastChatMessage({
|
|
1256
1342
|
body: "",
|
|
1257
1343
|
done: true,
|
|
@@ -1259,659 +1345,700 @@ export class AIChatAgent<
|
|
|
1259
1345
|
type: MessageType.CF_AGENT_USE_CHAT_RESPONSE,
|
|
1260
1346
|
...(continuation && { continuation: true })
|
|
1261
1347
|
});
|
|
1262
|
-
|
|
1348
|
+
break;
|
|
1263
1349
|
}
|
|
1264
1350
|
|
|
1265
|
-
|
|
1266
|
-
|
|
1351
|
+
const chunk = decoder.decode(value);
|
|
1352
|
+
|
|
1353
|
+
// After streaming is complete, persist the complete assistant's response
|
|
1354
|
+
|
|
1355
|
+
// Parse AI SDK v5 SSE format and extract text deltas
|
|
1356
|
+
const lines = chunk.split("\n");
|
|
1357
|
+
for (const line of lines) {
|
|
1358
|
+
if (line.startsWith("data: ") && line !== "data: [DONE]") {
|
|
1359
|
+
try {
|
|
1360
|
+
const data: UIMessageChunk = JSON.parse(line.slice(6)); // Remove 'data: ' prefix
|
|
1361
|
+
switch (data.type) {
|
|
1362
|
+
case "text-start": {
|
|
1363
|
+
const textPart: TextUIPart = {
|
|
1364
|
+
type: "text",
|
|
1365
|
+
text: "",
|
|
1366
|
+
providerMetadata: data.providerMetadata,
|
|
1367
|
+
state: "streaming"
|
|
1368
|
+
};
|
|
1369
|
+
activeTextParts[data.id] = textPart;
|
|
1370
|
+
message.parts.push(textPart);
|
|
1371
|
+
break;
|
|
1372
|
+
}
|
|
1267
1373
|
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
await import("ai");
|
|
1374
|
+
case "text-delta": {
|
|
1375
|
+
const textPart = activeTextParts[data.id];
|
|
1376
|
+
textPart.text += data.delta;
|
|
1377
|
+
textPart.providerMetadata =
|
|
1378
|
+
data.providerMetadata ?? textPart.providerMetadata;
|
|
1379
|
+
break;
|
|
1380
|
+
}
|
|
1276
1381
|
|
|
1277
|
-
|
|
1382
|
+
case "text-end": {
|
|
1383
|
+
const textPart = activeTextParts[data.id];
|
|
1384
|
+
textPart.state = "done";
|
|
1385
|
+
textPart.providerMetadata =
|
|
1386
|
+
data.providerMetadata ?? textPart.providerMetadata;
|
|
1387
|
+
delete activeTextParts[data.id];
|
|
1388
|
+
break;
|
|
1389
|
+
}
|
|
1278
1390
|
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
this._streamCompletionResolve = resolve;
|
|
1291
|
-
});
|
|
1292
|
-
let activeTextParts: Record<string, TextUIPart> = {};
|
|
1293
|
-
let activeReasoningParts: Record<string, ReasoningUIPart> = {};
|
|
1294
|
-
const partialToolCalls: Record<
|
|
1295
|
-
string,
|
|
1296
|
-
{ text: string; index: number; toolName: string; dynamic?: boolean }
|
|
1297
|
-
> = {};
|
|
1298
|
-
|
|
1299
|
-
function updateDynamicToolPart(
|
|
1300
|
-
options: {
|
|
1301
|
-
toolName: string;
|
|
1302
|
-
toolCallId: string;
|
|
1303
|
-
providerExecuted?: boolean;
|
|
1304
|
-
} & (
|
|
1305
|
-
| {
|
|
1306
|
-
state: "input-streaming";
|
|
1307
|
-
input: unknown;
|
|
1308
|
-
}
|
|
1309
|
-
| {
|
|
1310
|
-
state: "input-available";
|
|
1311
|
-
input: unknown;
|
|
1312
|
-
providerMetadata?: ProviderMetadata;
|
|
1313
|
-
}
|
|
1314
|
-
| {
|
|
1315
|
-
state: "output-available";
|
|
1316
|
-
input: unknown;
|
|
1317
|
-
output: unknown;
|
|
1318
|
-
preliminary: boolean | undefined;
|
|
1319
|
-
}
|
|
1320
|
-
| {
|
|
1321
|
-
state: "output-error";
|
|
1322
|
-
input: unknown;
|
|
1323
|
-
errorText: string;
|
|
1324
|
-
providerMetadata?: ProviderMetadata;
|
|
1325
|
-
}
|
|
1326
|
-
)
|
|
1327
|
-
) {
|
|
1328
|
-
const part = message.parts.find(
|
|
1329
|
-
(part) =>
|
|
1330
|
-
part.type === "dynamic-tool" &&
|
|
1331
|
-
part.toolCallId === options.toolCallId
|
|
1332
|
-
) as DynamicToolUIPart | undefined;
|
|
1333
|
-
|
|
1334
|
-
const anyOptions = options as Record<string, unknown>;
|
|
1335
|
-
const anyPart = part as Record<string, unknown>;
|
|
1336
|
-
|
|
1337
|
-
if (part != null) {
|
|
1338
|
-
part.state = options.state;
|
|
1339
|
-
anyPart.toolName = options.toolName;
|
|
1340
|
-
anyPart.input = anyOptions.input;
|
|
1341
|
-
anyPart.output = anyOptions.output;
|
|
1342
|
-
anyPart.errorText = anyOptions.errorText;
|
|
1343
|
-
anyPart.rawInput = anyOptions.rawInput ?? anyPart.rawInput;
|
|
1344
|
-
anyPart.preliminary = anyOptions.preliminary;
|
|
1391
|
+
case "reasoning-start": {
|
|
1392
|
+
const reasoningPart: ReasoningUIPart = {
|
|
1393
|
+
type: "reasoning",
|
|
1394
|
+
text: "",
|
|
1395
|
+
providerMetadata: data.providerMetadata,
|
|
1396
|
+
state: "streaming"
|
|
1397
|
+
};
|
|
1398
|
+
activeReasoningParts[data.id] = reasoningPart;
|
|
1399
|
+
message.parts.push(reasoningPart);
|
|
1400
|
+
break;
|
|
1401
|
+
}
|
|
1345
1402
|
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
} else {
|
|
1354
|
-
message.parts.push({
|
|
1355
|
-
type: "dynamic-tool",
|
|
1356
|
-
toolName: options.toolName,
|
|
1357
|
-
toolCallId: options.toolCallId,
|
|
1358
|
-
state: options.state,
|
|
1359
|
-
input: anyOptions.input,
|
|
1360
|
-
output: anyOptions.output,
|
|
1361
|
-
errorText: anyOptions.errorText,
|
|
1362
|
-
preliminary: anyOptions.preliminary,
|
|
1363
|
-
...(anyOptions.providerMetadata != null
|
|
1364
|
-
? { callProviderMetadata: anyOptions.providerMetadata }
|
|
1365
|
-
: {})
|
|
1366
|
-
} as DynamicToolUIPart);
|
|
1367
|
-
}
|
|
1368
|
-
}
|
|
1403
|
+
case "reasoning-delta": {
|
|
1404
|
+
const reasoningPart = activeReasoningParts[data.id];
|
|
1405
|
+
reasoningPart.text += data.delta;
|
|
1406
|
+
reasoningPart.providerMetadata =
|
|
1407
|
+
data.providerMetadata ?? reasoningPart.providerMetadata;
|
|
1408
|
+
break;
|
|
1409
|
+
}
|
|
1369
1410
|
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
| {
|
|
1377
|
-
state: "input-streaming";
|
|
1378
|
-
input: unknown;
|
|
1379
|
-
providerExecuted?: boolean;
|
|
1380
|
-
}
|
|
1381
|
-
| {
|
|
1382
|
-
state: "input-available";
|
|
1383
|
-
input: unknown;
|
|
1384
|
-
providerExecuted?: boolean;
|
|
1385
|
-
providerMetadata?: ProviderMetadata;
|
|
1386
|
-
}
|
|
1387
|
-
| {
|
|
1388
|
-
state: "output-available";
|
|
1389
|
-
input: unknown;
|
|
1390
|
-
output: unknown;
|
|
1391
|
-
providerExecuted?: boolean;
|
|
1392
|
-
preliminary?: boolean;
|
|
1393
|
-
}
|
|
1394
|
-
| {
|
|
1395
|
-
state: "output-error";
|
|
1396
|
-
input: unknown;
|
|
1397
|
-
rawInput?: unknown;
|
|
1398
|
-
errorText: string;
|
|
1399
|
-
providerExecuted?: boolean;
|
|
1400
|
-
providerMetadata?: ProviderMetadata;
|
|
1401
|
-
}
|
|
1402
|
-
)
|
|
1403
|
-
) {
|
|
1404
|
-
const part = message.parts.find(
|
|
1405
|
-
(part) =>
|
|
1406
|
-
isToolUIPart(part) &&
|
|
1407
|
-
(part as ToolUIPart).toolCallId === options.toolCallId
|
|
1408
|
-
) as ToolUIPart | undefined;
|
|
1409
|
-
|
|
1410
|
-
const anyOptions = options as Record<string, unknown>;
|
|
1411
|
-
const anyPart = part as Record<string, unknown>;
|
|
1412
|
-
|
|
1413
|
-
if (part != null) {
|
|
1414
|
-
part.state = options.state;
|
|
1415
|
-
anyPart.input = anyOptions.input;
|
|
1416
|
-
anyPart.output = anyOptions.output;
|
|
1417
|
-
anyPart.errorText = anyOptions.errorText;
|
|
1418
|
-
anyPart.rawInput = anyOptions.rawInput;
|
|
1419
|
-
anyPart.preliminary = anyOptions.preliminary;
|
|
1420
|
-
|
|
1421
|
-
// once providerExecuted is set, it stays for streaming
|
|
1422
|
-
anyPart.providerExecuted =
|
|
1423
|
-
anyOptions.providerExecuted ?? part.providerExecuted;
|
|
1411
|
+
case "reasoning-end": {
|
|
1412
|
+
const reasoningPart = activeReasoningParts[data.id];
|
|
1413
|
+
reasoningPart.providerMetadata =
|
|
1414
|
+
data.providerMetadata ?? reasoningPart.providerMetadata;
|
|
1415
|
+
reasoningPart.state = "done";
|
|
1416
|
+
delete activeReasoningParts[data.id];
|
|
1424
1417
|
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
part.state === "input-available"
|
|
1428
|
-
) {
|
|
1429
|
-
part.callProviderMetadata =
|
|
1430
|
-
anyOptions.providerMetadata as ProviderMetadata;
|
|
1431
|
-
}
|
|
1432
|
-
} else {
|
|
1433
|
-
message.parts.push({
|
|
1434
|
-
type: `tool-${options.toolName}`,
|
|
1435
|
-
toolCallId: options.toolCallId,
|
|
1436
|
-
state: options.state,
|
|
1437
|
-
input: anyOptions.input,
|
|
1438
|
-
output: anyOptions.output,
|
|
1439
|
-
rawInput: anyOptions.rawInput,
|
|
1440
|
-
errorText: anyOptions.errorText,
|
|
1441
|
-
providerExecuted: anyOptions.providerExecuted,
|
|
1442
|
-
preliminary: anyOptions.preliminary,
|
|
1443
|
-
...(anyOptions.providerMetadata != null
|
|
1444
|
-
? { callProviderMetadata: anyOptions.providerMetadata }
|
|
1445
|
-
: {})
|
|
1446
|
-
} as ToolUIPart);
|
|
1447
|
-
}
|
|
1448
|
-
}
|
|
1418
|
+
break;
|
|
1419
|
+
}
|
|
1449
1420
|
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1421
|
+
case "file": {
|
|
1422
|
+
message.parts.push({
|
|
1423
|
+
type: "file",
|
|
1424
|
+
mediaType: data.mediaType,
|
|
1425
|
+
url: data.url
|
|
1426
|
+
});
|
|
1456
1427
|
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
}
|
|
1428
|
+
break;
|
|
1429
|
+
}
|
|
1460
1430
|
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
// Send final completion signal
|
|
1470
|
-
this._broadcastChatMessage({
|
|
1471
|
-
body: "",
|
|
1472
|
-
done: true,
|
|
1473
|
-
id,
|
|
1474
|
-
type: MessageType.CF_AGENT_USE_CHAT_RESPONSE,
|
|
1475
|
-
...(continuation && { continuation: true })
|
|
1476
|
-
});
|
|
1477
|
-
break;
|
|
1478
|
-
}
|
|
1431
|
+
case "source-url": {
|
|
1432
|
+
message.parts.push({
|
|
1433
|
+
type: "source-url",
|
|
1434
|
+
sourceId: data.sourceId,
|
|
1435
|
+
url: data.url,
|
|
1436
|
+
title: data.title,
|
|
1437
|
+
providerMetadata: data.providerMetadata
|
|
1438
|
+
});
|
|
1479
1439
|
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
// Determine response format based on content-type
|
|
1483
|
-
const contentType = response.headers.get("content-type") || "";
|
|
1484
|
-
const isSSE = contentType.includes("text/event-stream");
|
|
1485
|
-
|
|
1486
|
-
// After streaming is complete, persist the complete assistant's response
|
|
1487
|
-
if (isSSE) {
|
|
1488
|
-
// Parse AI SDK v5 SSE format and extract text deltas
|
|
1489
|
-
const lines = chunk.split("\n");
|
|
1490
|
-
for (const line of lines) {
|
|
1491
|
-
if (line.startsWith("data: ") && line !== "data: [DONE]") {
|
|
1492
|
-
try {
|
|
1493
|
-
const data: UIMessageChunk = JSON.parse(line.slice(6)); // Remove 'data: ' prefix
|
|
1494
|
-
switch (data.type) {
|
|
1495
|
-
case "text-start": {
|
|
1496
|
-
const textPart: TextUIPart = {
|
|
1497
|
-
type: "text",
|
|
1498
|
-
text: "",
|
|
1499
|
-
providerMetadata: data.providerMetadata,
|
|
1500
|
-
state: "streaming"
|
|
1501
|
-
};
|
|
1502
|
-
activeTextParts[data.id] = textPart;
|
|
1503
|
-
message.parts.push(textPart);
|
|
1504
|
-
break;
|
|
1505
|
-
}
|
|
1506
|
-
|
|
1507
|
-
case "text-delta": {
|
|
1508
|
-
const textPart = activeTextParts[data.id];
|
|
1509
|
-
textPart.text += data.delta;
|
|
1510
|
-
textPart.providerMetadata =
|
|
1511
|
-
data.providerMetadata ?? textPart.providerMetadata;
|
|
1512
|
-
break;
|
|
1513
|
-
}
|
|
1514
|
-
|
|
1515
|
-
case "text-end": {
|
|
1516
|
-
const textPart = activeTextParts[data.id];
|
|
1517
|
-
textPart.state = "done";
|
|
1518
|
-
textPart.providerMetadata =
|
|
1519
|
-
data.providerMetadata ?? textPart.providerMetadata;
|
|
1520
|
-
delete activeTextParts[data.id];
|
|
1521
|
-
break;
|
|
1522
|
-
}
|
|
1523
|
-
|
|
1524
|
-
case "reasoning-start": {
|
|
1525
|
-
const reasoningPart: ReasoningUIPart = {
|
|
1526
|
-
type: "reasoning",
|
|
1527
|
-
text: "",
|
|
1528
|
-
providerMetadata: data.providerMetadata,
|
|
1529
|
-
state: "streaming"
|
|
1530
|
-
};
|
|
1531
|
-
activeReasoningParts[data.id] = reasoningPart;
|
|
1532
|
-
message.parts.push(reasoningPart);
|
|
1533
|
-
break;
|
|
1534
|
-
}
|
|
1535
|
-
|
|
1536
|
-
case "reasoning-delta": {
|
|
1537
|
-
const reasoningPart = activeReasoningParts[data.id];
|
|
1538
|
-
reasoningPart.text += data.delta;
|
|
1539
|
-
reasoningPart.providerMetadata =
|
|
1540
|
-
data.providerMetadata ?? reasoningPart.providerMetadata;
|
|
1541
|
-
break;
|
|
1542
|
-
}
|
|
1543
|
-
|
|
1544
|
-
case "reasoning-end": {
|
|
1545
|
-
const reasoningPart = activeReasoningParts[data.id];
|
|
1546
|
-
reasoningPart.providerMetadata =
|
|
1547
|
-
data.providerMetadata ?? reasoningPart.providerMetadata;
|
|
1548
|
-
reasoningPart.state = "done";
|
|
1549
|
-
delete activeReasoningParts[data.id];
|
|
1550
|
-
|
|
1551
|
-
break;
|
|
1552
|
-
}
|
|
1553
|
-
|
|
1554
|
-
case "file": {
|
|
1555
|
-
message.parts.push({
|
|
1556
|
-
type: "file",
|
|
1557
|
-
mediaType: data.mediaType,
|
|
1558
|
-
url: data.url
|
|
1559
|
-
});
|
|
1560
|
-
|
|
1561
|
-
break;
|
|
1562
|
-
}
|
|
1563
|
-
|
|
1564
|
-
case "source-url": {
|
|
1565
|
-
message.parts.push({
|
|
1566
|
-
type: "source-url",
|
|
1567
|
-
sourceId: data.sourceId,
|
|
1568
|
-
url: data.url,
|
|
1569
|
-
title: data.title,
|
|
1570
|
-
providerMetadata: data.providerMetadata
|
|
1571
|
-
});
|
|
1572
|
-
|
|
1573
|
-
break;
|
|
1574
|
-
}
|
|
1575
|
-
|
|
1576
|
-
case "source-document": {
|
|
1577
|
-
message.parts.push({
|
|
1578
|
-
type: "source-document",
|
|
1579
|
-
sourceId: data.sourceId,
|
|
1580
|
-
mediaType: data.mediaType,
|
|
1581
|
-
title: data.title,
|
|
1582
|
-
filename: data.filename,
|
|
1583
|
-
providerMetadata: data.providerMetadata
|
|
1584
|
-
});
|
|
1585
|
-
|
|
1586
|
-
break;
|
|
1587
|
-
}
|
|
1588
|
-
|
|
1589
|
-
case "tool-input-start": {
|
|
1590
|
-
const toolInvocations =
|
|
1591
|
-
message.parts.filter(isToolUIPart);
|
|
1592
|
-
|
|
1593
|
-
// add the partial tool call to the map
|
|
1594
|
-
partialToolCalls[data.toolCallId] = {
|
|
1595
|
-
text: "",
|
|
1596
|
-
toolName: data.toolName,
|
|
1597
|
-
index: toolInvocations.length,
|
|
1598
|
-
dynamic: data.dynamic
|
|
1599
|
-
};
|
|
1600
|
-
|
|
1601
|
-
if (data.dynamic) {
|
|
1602
|
-
updateDynamicToolPart({
|
|
1603
|
-
toolCallId: data.toolCallId,
|
|
1604
|
-
toolName: data.toolName,
|
|
1605
|
-
state: "input-streaming",
|
|
1606
|
-
input: undefined
|
|
1607
|
-
});
|
|
1608
|
-
} else {
|
|
1609
|
-
updateToolPart({
|
|
1610
|
-
toolCallId: data.toolCallId,
|
|
1611
|
-
toolName: data.toolName,
|
|
1612
|
-
state: "input-streaming",
|
|
1613
|
-
input: undefined
|
|
1614
|
-
});
|
|
1615
|
-
}
|
|
1440
|
+
break;
|
|
1441
|
+
}
|
|
1616
1442
|
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
partialToolCall.text
|
|
1627
|
-
);
|
|
1628
|
-
const partialArgs = (
|
|
1629
|
-
partialArgsResult as { value: Record<string, unknown> }
|
|
1630
|
-
).value;
|
|
1631
|
-
|
|
1632
|
-
if (partialToolCall.dynamic) {
|
|
1633
|
-
updateDynamicToolPart({
|
|
1634
|
-
toolCallId: data.toolCallId,
|
|
1635
|
-
toolName: partialToolCall.toolName,
|
|
1636
|
-
state: "input-streaming",
|
|
1637
|
-
input: partialArgs
|
|
1638
|
-
});
|
|
1639
|
-
} else {
|
|
1640
|
-
updateToolPart({
|
|
1641
|
-
toolCallId: data.toolCallId,
|
|
1642
|
-
toolName: partialToolCall.toolName,
|
|
1643
|
-
state: "input-streaming",
|
|
1644
|
-
input: partialArgs
|
|
1645
|
-
});
|
|
1646
|
-
}
|
|
1443
|
+
case "source-document": {
|
|
1444
|
+
message.parts.push({
|
|
1445
|
+
type: "source-document",
|
|
1446
|
+
sourceId: data.sourceId,
|
|
1447
|
+
mediaType: data.mediaType,
|
|
1448
|
+
title: data.title,
|
|
1449
|
+
filename: data.filename,
|
|
1450
|
+
providerMetadata: data.providerMetadata
|
|
1451
|
+
});
|
|
1647
1452
|
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
case "tool-input-available": {
|
|
1652
|
-
if (data.dynamic) {
|
|
1653
|
-
updateDynamicToolPart({
|
|
1654
|
-
toolCallId: data.toolCallId,
|
|
1655
|
-
toolName: data.toolName,
|
|
1656
|
-
state: "input-available",
|
|
1657
|
-
input: data.input,
|
|
1658
|
-
providerMetadata: data.providerMetadata
|
|
1659
|
-
});
|
|
1660
|
-
} else {
|
|
1661
|
-
updateToolPart({
|
|
1662
|
-
toolCallId: data.toolCallId,
|
|
1663
|
-
toolName: data.toolName,
|
|
1664
|
-
state: "input-available",
|
|
1665
|
-
input: data.input,
|
|
1666
|
-
providerExecuted: data.providerExecuted,
|
|
1667
|
-
providerMetadata: data.providerMetadata
|
|
1668
|
-
});
|
|
1669
|
-
}
|
|
1453
|
+
break;
|
|
1454
|
+
}
|
|
1670
1455
|
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
// }
|
|
1682
|
-
break;
|
|
1683
|
-
}
|
|
1684
|
-
|
|
1685
|
-
case "tool-input-error": {
|
|
1686
|
-
if (data.dynamic) {
|
|
1687
|
-
updateDynamicToolPart({
|
|
1688
|
-
toolCallId: data.toolCallId,
|
|
1689
|
-
toolName: data.toolName,
|
|
1690
|
-
state: "output-error",
|
|
1691
|
-
input: data.input,
|
|
1692
|
-
errorText: data.errorText,
|
|
1693
|
-
providerMetadata: data.providerMetadata
|
|
1694
|
-
});
|
|
1695
|
-
} else {
|
|
1696
|
-
updateToolPart({
|
|
1697
|
-
toolCallId: data.toolCallId,
|
|
1698
|
-
toolName: data.toolName,
|
|
1699
|
-
state: "output-error",
|
|
1700
|
-
input: undefined,
|
|
1701
|
-
rawInput: data.input,
|
|
1702
|
-
errorText: data.errorText,
|
|
1703
|
-
providerExecuted: data.providerExecuted,
|
|
1704
|
-
providerMetadata: data.providerMetadata
|
|
1705
|
-
});
|
|
1706
|
-
}
|
|
1456
|
+
case "tool-input-start": {
|
|
1457
|
+
const toolInvocations = message.parts.filter(isToolUIPart);
|
|
1458
|
+
|
|
1459
|
+
// add the partial tool call to the map
|
|
1460
|
+
partialToolCalls[data.toolCallId] = {
|
|
1461
|
+
text: "",
|
|
1462
|
+
toolName: data.toolName,
|
|
1463
|
+
index: toolInvocations.length,
|
|
1464
|
+
dynamic: data.dynamic
|
|
1465
|
+
};
|
|
1707
1466
|
|
|
1708
|
-
|
|
1709
|
-
|
|
1467
|
+
if (data.dynamic) {
|
|
1468
|
+
this.updateDynamicToolPart(message, {
|
|
1469
|
+
toolCallId: data.toolCallId,
|
|
1470
|
+
toolName: data.toolName,
|
|
1471
|
+
state: "input-streaming",
|
|
1472
|
+
input: undefined
|
|
1473
|
+
});
|
|
1474
|
+
} else {
|
|
1475
|
+
await this.updateToolPart(message, {
|
|
1476
|
+
toolCallId: data.toolCallId,
|
|
1477
|
+
toolName: data.toolName,
|
|
1478
|
+
state: "input-streaming",
|
|
1479
|
+
input: undefined
|
|
1480
|
+
});
|
|
1481
|
+
}
|
|
1710
1482
|
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
const toolInvocations = message.parts.filter(
|
|
1714
|
-
(part) => part.type === "dynamic-tool"
|
|
1715
|
-
) as DynamicToolUIPart[];
|
|
1483
|
+
break;
|
|
1484
|
+
}
|
|
1716
1485
|
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
invocation.toolCallId === data.toolCallId
|
|
1720
|
-
);
|
|
1486
|
+
case "tool-input-delta": {
|
|
1487
|
+
const partialToolCall = partialToolCalls[data.toolCallId];
|
|
1721
1488
|
|
|
1722
|
-
|
|
1723
|
-
throw new Error("Tool invocation not found");
|
|
1724
|
-
|
|
1725
|
-
updateDynamicToolPart({
|
|
1726
|
-
toolCallId: data.toolCallId,
|
|
1727
|
-
toolName: toolInvocation.toolName,
|
|
1728
|
-
state: "output-available",
|
|
1729
|
-
input: toolInvocation.input,
|
|
1730
|
-
output: data.output,
|
|
1731
|
-
preliminary: data.preliminary
|
|
1732
|
-
});
|
|
1733
|
-
} else {
|
|
1734
|
-
const toolInvocations = message.parts.filter(
|
|
1735
|
-
isToolUIPart
|
|
1736
|
-
) as ToolUIPart[];
|
|
1737
|
-
|
|
1738
|
-
const toolInvocation = toolInvocations.find(
|
|
1739
|
-
(invocation) =>
|
|
1740
|
-
invocation.toolCallId === data.toolCallId
|
|
1741
|
-
);
|
|
1489
|
+
partialToolCall.text += data.inputTextDelta;
|
|
1742
1490
|
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1491
|
+
const partialArgsResult = await parsePartialJson(
|
|
1492
|
+
partialToolCall.text
|
|
1493
|
+
);
|
|
1494
|
+
const partialArgs = (
|
|
1495
|
+
partialArgsResult as { value: Record<string, unknown> }
|
|
1496
|
+
).value;
|
|
1497
|
+
|
|
1498
|
+
if (partialToolCall.dynamic) {
|
|
1499
|
+
this.updateDynamicToolPart(message, {
|
|
1500
|
+
toolCallId: data.toolCallId,
|
|
1501
|
+
toolName: partialToolCall.toolName,
|
|
1502
|
+
state: "input-streaming",
|
|
1503
|
+
input: partialArgs
|
|
1504
|
+
});
|
|
1505
|
+
} else {
|
|
1506
|
+
await this.updateToolPart(message, {
|
|
1507
|
+
toolCallId: data.toolCallId,
|
|
1508
|
+
toolName: partialToolCall.toolName,
|
|
1509
|
+
state: "input-streaming",
|
|
1510
|
+
input: partialArgs
|
|
1511
|
+
});
|
|
1512
|
+
}
|
|
1756
1513
|
|
|
1757
|
-
|
|
1758
|
-
|
|
1514
|
+
break;
|
|
1515
|
+
}
|
|
1759
1516
|
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1517
|
+
case "tool-input-available": {
|
|
1518
|
+
if (data.dynamic) {
|
|
1519
|
+
this.updateDynamicToolPart(message, {
|
|
1520
|
+
toolCallId: data.toolCallId,
|
|
1521
|
+
toolName: data.toolName,
|
|
1522
|
+
state: "input-available",
|
|
1523
|
+
input: data.input,
|
|
1524
|
+
providerMetadata: data.providerMetadata
|
|
1525
|
+
});
|
|
1526
|
+
} else {
|
|
1527
|
+
await this.updateToolPart(message, {
|
|
1528
|
+
toolCallId: data.toolCallId,
|
|
1529
|
+
toolName: data.toolName,
|
|
1530
|
+
state: "input-available",
|
|
1531
|
+
input: data.input,
|
|
1532
|
+
providerExecuted: data.providerExecuted,
|
|
1533
|
+
providerMetadata: data.providerMetadata
|
|
1534
|
+
});
|
|
1535
|
+
}
|
|
1765
1536
|
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1537
|
+
// TODO: Do we want to expose onToolCall?
|
|
1538
|
+
|
|
1539
|
+
// invoke the onToolCall callback if it exists. This is blocking.
|
|
1540
|
+
// In the future we should make this non-blocking, which
|
|
1541
|
+
// requires additional state management for error handling etc.
|
|
1542
|
+
// Skip calling onToolCall for provider-executed tools since they are already executed
|
|
1543
|
+
// if (onToolCall && !data.providerExecuted) {
|
|
1544
|
+
// await onToolCall({
|
|
1545
|
+
// toolCall: data
|
|
1546
|
+
// });
|
|
1547
|
+
// }
|
|
1548
|
+
break;
|
|
1549
|
+
}
|
|
1770
1550
|
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1551
|
+
case "tool-input-error": {
|
|
1552
|
+
if (data.dynamic) {
|
|
1553
|
+
this.updateDynamicToolPart(message, {
|
|
1554
|
+
toolCallId: data.toolCallId,
|
|
1555
|
+
toolName: data.toolName,
|
|
1556
|
+
state: "output-error",
|
|
1557
|
+
input: data.input,
|
|
1558
|
+
errorText: data.errorText,
|
|
1559
|
+
providerMetadata: data.providerMetadata
|
|
1560
|
+
});
|
|
1561
|
+
} else {
|
|
1562
|
+
await this.updateToolPart(message, {
|
|
1563
|
+
toolCallId: data.toolCallId,
|
|
1564
|
+
toolName: data.toolName,
|
|
1565
|
+
state: "output-error",
|
|
1566
|
+
input: undefined,
|
|
1567
|
+
rawInput: data.input,
|
|
1568
|
+
errorText: data.errorText,
|
|
1569
|
+
providerExecuted: data.providerExecuted,
|
|
1570
|
+
providerMetadata: data.providerMetadata
|
|
1571
|
+
});
|
|
1572
|
+
}
|
|
1790
1573
|
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
updateToolPart({
|
|
1794
|
-
toolCallId: data.toolCallId,
|
|
1795
|
-
toolName: getToolName(toolInvocation),
|
|
1796
|
-
state: "output-error",
|
|
1797
|
-
input: toolInvocation.input,
|
|
1798
|
-
rawInput:
|
|
1799
|
-
"rawInput" in toolInvocation
|
|
1800
|
-
? toolInvocation.rawInput
|
|
1801
|
-
: undefined,
|
|
1802
|
-
errorText: data.errorText
|
|
1803
|
-
});
|
|
1804
|
-
}
|
|
1574
|
+
break;
|
|
1575
|
+
}
|
|
1805
1576
|
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
message.parts.push({ type: "step-start" });
|
|
1812
|
-
break;
|
|
1813
|
-
}
|
|
1814
|
-
|
|
1815
|
-
case "finish-step": {
|
|
1816
|
-
// reset the current text and reasoning parts
|
|
1817
|
-
activeTextParts = {};
|
|
1818
|
-
activeReasoningParts = {};
|
|
1819
|
-
break;
|
|
1820
|
-
}
|
|
1821
|
-
|
|
1822
|
-
case "start": {
|
|
1823
|
-
if (data.messageId != null) {
|
|
1824
|
-
message.id = data.messageId;
|
|
1825
|
-
}
|
|
1577
|
+
case "tool-output-available": {
|
|
1578
|
+
if (data.dynamic) {
|
|
1579
|
+
const toolInvocations = message.parts.filter(
|
|
1580
|
+
(part) => part.type === "dynamic-tool"
|
|
1581
|
+
) as DynamicToolUIPart[];
|
|
1826
1582
|
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
}
|
|
1831
|
-
|
|
1832
|
-
case "finish": {
|
|
1833
|
-
await updateMessageMetadata(data.messageMetadata);
|
|
1834
|
-
break;
|
|
1835
|
-
}
|
|
1836
|
-
|
|
1837
|
-
case "message-metadata": {
|
|
1838
|
-
await updateMessageMetadata(data.messageMetadata);
|
|
1839
|
-
break;
|
|
1840
|
-
}
|
|
1841
|
-
|
|
1842
|
-
case "error": {
|
|
1843
|
-
this._broadcastChatMessage({
|
|
1844
|
-
error: true,
|
|
1845
|
-
body: data.errorText ?? JSON.stringify(data),
|
|
1846
|
-
done: false,
|
|
1847
|
-
id,
|
|
1848
|
-
type: MessageType.CF_AGENT_USE_CHAT_RESPONSE
|
|
1849
|
-
});
|
|
1850
|
-
|
|
1851
|
-
break;
|
|
1852
|
-
}
|
|
1853
|
-
// Do we want to handle data parts?
|
|
1854
|
-
}
|
|
1583
|
+
const toolInvocation = toolInvocations.find(
|
|
1584
|
+
(invocation) => invocation.toolCallId === data.toolCallId
|
|
1585
|
+
);
|
|
1855
1586
|
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
// not a UIMessageStreamPart (which expects "messageMetadata" instead).
|
|
1859
|
-
// See: https://github.com/cloudflare/agents/issues/677
|
|
1860
|
-
let eventToSend: unknown = data;
|
|
1861
|
-
if (data.type === "finish" && "finishReason" in data) {
|
|
1862
|
-
const { finishReason, ...rest } = data as {
|
|
1863
|
-
finishReason: string;
|
|
1864
|
-
[key: string]: unknown;
|
|
1865
|
-
};
|
|
1866
|
-
eventToSend = {
|
|
1867
|
-
...rest,
|
|
1868
|
-
type: "finish",
|
|
1869
|
-
messageMetadata: { finishReason }
|
|
1870
|
-
};
|
|
1871
|
-
}
|
|
1587
|
+
if (!toolInvocation)
|
|
1588
|
+
throw new Error("Tool invocation not found");
|
|
1872
1589
|
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1590
|
+
this.updateDynamicToolPart(message, {
|
|
1591
|
+
toolCallId: data.toolCallId,
|
|
1592
|
+
toolName: toolInvocation.toolName,
|
|
1593
|
+
state: "output-available",
|
|
1594
|
+
input: toolInvocation.input,
|
|
1595
|
+
output: data.output,
|
|
1596
|
+
preliminary: data.preliminary
|
|
1597
|
+
});
|
|
1598
|
+
} else {
|
|
1599
|
+
const toolInvocations = message.parts.filter(
|
|
1600
|
+
isToolUIPart
|
|
1601
|
+
) as ToolUIPart[];
|
|
1602
|
+
|
|
1603
|
+
const toolInvocation = toolInvocations.find(
|
|
1604
|
+
(invocation) => invocation.toolCallId === data.toolCallId
|
|
1605
|
+
);
|
|
1606
|
+
|
|
1607
|
+
if (!toolInvocation)
|
|
1608
|
+
throw new Error("Tool invocation not found");
|
|
1609
|
+
|
|
1610
|
+
await this.updateToolPart(message, {
|
|
1611
|
+
toolCallId: data.toolCallId,
|
|
1612
|
+
toolName: getToolName(toolInvocation),
|
|
1613
|
+
state: "output-available",
|
|
1614
|
+
input: toolInvocation.input,
|
|
1615
|
+
output: data.output,
|
|
1616
|
+
providerExecuted: data.providerExecuted,
|
|
1617
|
+
preliminary: data.preliminary
|
|
1884
1618
|
});
|
|
1885
|
-
} catch (_error) {
|
|
1886
|
-
// Skip malformed JSON lines silently
|
|
1887
1619
|
}
|
|
1620
|
+
|
|
1621
|
+
break;
|
|
1888
1622
|
}
|
|
1623
|
+
|
|
1624
|
+
case "tool-output-error": {
|
|
1625
|
+
if (data.dynamic) {
|
|
1626
|
+
const toolInvocations = message.parts.filter(
|
|
1627
|
+
(part) => part.type === "dynamic-tool"
|
|
1628
|
+
) as DynamicToolUIPart[];
|
|
1629
|
+
|
|
1630
|
+
const toolInvocation = toolInvocations.find(
|
|
1631
|
+
(invocation) => invocation.toolCallId === data.toolCallId
|
|
1632
|
+
);
|
|
1633
|
+
|
|
1634
|
+
if (!toolInvocation)
|
|
1635
|
+
throw new Error("Tool invocation not found");
|
|
1636
|
+
|
|
1637
|
+
this.updateDynamicToolPart(message, {
|
|
1638
|
+
toolCallId: data.toolCallId,
|
|
1639
|
+
toolName: toolInvocation.toolName,
|
|
1640
|
+
state: "output-error",
|
|
1641
|
+
input: toolInvocation.input,
|
|
1642
|
+
errorText: data.errorText
|
|
1643
|
+
});
|
|
1644
|
+
} else {
|
|
1645
|
+
const toolInvocations = message.parts.filter(
|
|
1646
|
+
isToolUIPart
|
|
1647
|
+
) as ToolUIPart[];
|
|
1648
|
+
|
|
1649
|
+
const toolInvocation = toolInvocations.find(
|
|
1650
|
+
(invocation) => invocation.toolCallId === data.toolCallId
|
|
1651
|
+
);
|
|
1652
|
+
|
|
1653
|
+
if (!toolInvocation)
|
|
1654
|
+
throw new Error("Tool invocation not found");
|
|
1655
|
+
await this.updateToolPart(message, {
|
|
1656
|
+
toolCallId: data.toolCallId,
|
|
1657
|
+
toolName: getToolName(toolInvocation),
|
|
1658
|
+
state: "output-error",
|
|
1659
|
+
input: toolInvocation.input,
|
|
1660
|
+
rawInput:
|
|
1661
|
+
"rawInput" in toolInvocation
|
|
1662
|
+
? toolInvocation.rawInput
|
|
1663
|
+
: undefined,
|
|
1664
|
+
errorText: data.errorText
|
|
1665
|
+
});
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
break;
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
case "start-step": {
|
|
1672
|
+
// add a step boundary part to the message
|
|
1673
|
+
message.parts.push({ type: "step-start" });
|
|
1674
|
+
break;
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
case "finish-step": {
|
|
1678
|
+
// reset the current text and reasoning parts
|
|
1679
|
+
activeTextParts = {};
|
|
1680
|
+
activeReasoningParts = {};
|
|
1681
|
+
break;
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
case "start": {
|
|
1685
|
+
if (data.messageId != null) {
|
|
1686
|
+
message.id = data.messageId;
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
await this.updateMessageMetadata(message, data.messageMetadata);
|
|
1690
|
+
|
|
1691
|
+
break;
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
case "finish": {
|
|
1695
|
+
await this.updateMessageMetadata(message, data.messageMetadata);
|
|
1696
|
+
break;
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
case "message-metadata": {
|
|
1700
|
+
await this.updateMessageMetadata(message, data.messageMetadata);
|
|
1701
|
+
break;
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
case "error": {
|
|
1705
|
+
this._broadcastChatMessage({
|
|
1706
|
+
error: true,
|
|
1707
|
+
body: data.errorText ?? JSON.stringify(data),
|
|
1708
|
+
done: false,
|
|
1709
|
+
id,
|
|
1710
|
+
type: MessageType.CF_AGENT_USE_CHAT_RESPONSE
|
|
1711
|
+
});
|
|
1712
|
+
|
|
1713
|
+
break;
|
|
1714
|
+
}
|
|
1715
|
+
// Do we want to handle data parts?
|
|
1889
1716
|
}
|
|
1890
|
-
|
|
1891
|
-
//
|
|
1892
|
-
//
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
type: MessageType.CF_AGENT_USE_CHAT_RESPONSE,
|
|
1907
|
-
...(continuation && { continuation: true })
|
|
1908
|
-
});
|
|
1717
|
+
|
|
1718
|
+
// Convert internal AI SDK stream events to valid UIMessageStreamPart format.
|
|
1719
|
+
// The "finish" event with "finishReason" is an internal LanguageModelV3StreamPart,
|
|
1720
|
+
// not a UIMessageStreamPart (which expects "messageMetadata" instead).
|
|
1721
|
+
// See: https://github.com/cloudflare/agents/issues/677
|
|
1722
|
+
let eventToSend: unknown = data;
|
|
1723
|
+
if (data.type === "finish" && "finishReason" in data) {
|
|
1724
|
+
const { finishReason, ...rest } = data as {
|
|
1725
|
+
finishReason: string;
|
|
1726
|
+
[key: string]: unknown;
|
|
1727
|
+
};
|
|
1728
|
+
eventToSend = {
|
|
1729
|
+
...rest,
|
|
1730
|
+
type: "finish",
|
|
1731
|
+
messageMetadata: { finishReason }
|
|
1732
|
+
};
|
|
1909
1733
|
}
|
|
1734
|
+
|
|
1735
|
+
// Store chunk for replay on reconnection
|
|
1736
|
+
const chunkBody = JSON.stringify(eventToSend);
|
|
1737
|
+
this._storeStreamChunk(streamId, chunkBody);
|
|
1738
|
+
|
|
1739
|
+
// Forward the converted event to the client
|
|
1740
|
+
this._broadcastChatMessage({
|
|
1741
|
+
body: chunkBody,
|
|
1742
|
+
done: false,
|
|
1743
|
+
id,
|
|
1744
|
+
type: MessageType.CF_AGENT_USE_CHAT_RESPONSE,
|
|
1745
|
+
...(continuation && { continuation: true })
|
|
1746
|
+
});
|
|
1747
|
+
} catch (_error) {
|
|
1748
|
+
// Skip malformed JSON lines silently
|
|
1910
1749
|
}
|
|
1911
1750
|
}
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
// Handle plain text responses (e.g., from generateText)
|
|
1756
|
+
private async _sendPlaintextReply(
|
|
1757
|
+
id: string,
|
|
1758
|
+
streamId: string,
|
|
1759
|
+
reader: ReadableStreamDefaultReader<Uint8Array>,
|
|
1760
|
+
message: ChatMessage,
|
|
1761
|
+
streamCompleted: { value: boolean },
|
|
1762
|
+
continuation = false
|
|
1763
|
+
) {
|
|
1764
|
+
// if not AI SDK SSE format, we need to inject text-start and text-end events ourselves
|
|
1765
|
+
this._broadcastTextEvent(
|
|
1766
|
+
streamId,
|
|
1767
|
+
{ type: "text-start", id },
|
|
1768
|
+
continuation
|
|
1769
|
+
);
|
|
1770
|
+
|
|
1771
|
+
while (true) {
|
|
1772
|
+
const { done, value } = await reader.read();
|
|
1773
|
+
if (done) {
|
|
1774
|
+
this._broadcastTextEvent(
|
|
1775
|
+
streamId,
|
|
1776
|
+
{ type: "text-end", id },
|
|
1777
|
+
continuation
|
|
1778
|
+
);
|
|
1779
|
+
|
|
1780
|
+
// Mark the stream as completed
|
|
1781
|
+
this._completeStream(streamId);
|
|
1782
|
+
streamCompleted.value = true;
|
|
1783
|
+
// Send final completion signal
|
|
1784
|
+
this._broadcastChatMessage({
|
|
1785
|
+
body: "",
|
|
1786
|
+
done: true,
|
|
1787
|
+
id,
|
|
1788
|
+
type: MessageType.CF_AGENT_USE_CHAT_RESPONSE,
|
|
1789
|
+
...(continuation && { continuation: true })
|
|
1790
|
+
});
|
|
1791
|
+
break;
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
const chunk = decoder.decode(value);
|
|
1795
|
+
|
|
1796
|
+
// Treat the entire chunk as a text delta to preserve exact formatting
|
|
1797
|
+
if (chunk.length > 0) {
|
|
1798
|
+
message.parts.push({ type: "text", text: chunk });
|
|
1799
|
+
this._broadcastTextEvent(
|
|
1800
|
+
streamId,
|
|
1801
|
+
{ type: "text-delta", id, delta: chunk },
|
|
1802
|
+
continuation
|
|
1803
|
+
);
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
private updateDynamicToolPart(
|
|
1809
|
+
message: ChatMessage,
|
|
1810
|
+
options: {
|
|
1811
|
+
toolName: string;
|
|
1812
|
+
toolCallId: string;
|
|
1813
|
+
providerExecuted?: boolean;
|
|
1814
|
+
} & (
|
|
1815
|
+
| {
|
|
1816
|
+
state: "input-streaming";
|
|
1817
|
+
input: unknown;
|
|
1818
|
+
}
|
|
1819
|
+
| {
|
|
1820
|
+
state: "input-available";
|
|
1821
|
+
input: unknown;
|
|
1822
|
+
providerMetadata?: ProviderMetadata;
|
|
1823
|
+
}
|
|
1824
|
+
| {
|
|
1825
|
+
state: "output-available";
|
|
1826
|
+
input: unknown;
|
|
1827
|
+
output: unknown;
|
|
1828
|
+
preliminary: boolean | undefined;
|
|
1829
|
+
}
|
|
1830
|
+
| {
|
|
1831
|
+
state: "output-error";
|
|
1832
|
+
input: unknown;
|
|
1833
|
+
errorText: string;
|
|
1834
|
+
providerMetadata?: ProviderMetadata;
|
|
1835
|
+
}
|
|
1836
|
+
)
|
|
1837
|
+
) {
|
|
1838
|
+
const part = message.parts.find(
|
|
1839
|
+
(part) =>
|
|
1840
|
+
part.type === "dynamic-tool" && part.toolCallId === options.toolCallId
|
|
1841
|
+
) as DynamicToolUIPart | undefined;
|
|
1842
|
+
|
|
1843
|
+
const anyOptions = options as Record<string, unknown>;
|
|
1844
|
+
const anyPart = part as Record<string, unknown>;
|
|
1845
|
+
|
|
1846
|
+
if (part != null) {
|
|
1847
|
+
part.state = options.state;
|
|
1848
|
+
anyPart.toolName = options.toolName;
|
|
1849
|
+
anyPart.input = anyOptions.input;
|
|
1850
|
+
anyPart.output = anyOptions.output;
|
|
1851
|
+
anyPart.errorText = anyOptions.errorText;
|
|
1852
|
+
anyPart.rawInput = anyOptions.rawInput ?? anyPart.rawInput;
|
|
1853
|
+
anyPart.preliminary = anyOptions.preliminary;
|
|
1854
|
+
|
|
1855
|
+
if (
|
|
1856
|
+
anyOptions.providerMetadata != null &&
|
|
1857
|
+
part.state === "input-available"
|
|
1858
|
+
) {
|
|
1859
|
+
part.callProviderMetadata =
|
|
1860
|
+
anyOptions.providerMetadata as ProviderMetadata;
|
|
1861
|
+
}
|
|
1862
|
+
} else {
|
|
1863
|
+
message.parts.push({
|
|
1864
|
+
type: "dynamic-tool",
|
|
1865
|
+
toolName: options.toolName,
|
|
1866
|
+
toolCallId: options.toolCallId,
|
|
1867
|
+
state: options.state,
|
|
1868
|
+
input: anyOptions.input,
|
|
1869
|
+
output: anyOptions.output,
|
|
1870
|
+
errorText: anyOptions.errorText,
|
|
1871
|
+
preliminary: anyOptions.preliminary,
|
|
1872
|
+
...(anyOptions.providerMetadata != null
|
|
1873
|
+
? { callProviderMetadata: anyOptions.providerMetadata }
|
|
1874
|
+
: {})
|
|
1875
|
+
} as DynamicToolUIPart);
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
private async updateToolPart(
|
|
1880
|
+
message: ChatMessage,
|
|
1881
|
+
options: {
|
|
1882
|
+
toolName: string;
|
|
1883
|
+
toolCallId: string;
|
|
1884
|
+
providerExecuted?: boolean;
|
|
1885
|
+
} & (
|
|
1886
|
+
| {
|
|
1887
|
+
state: "input-streaming";
|
|
1888
|
+
input: unknown;
|
|
1889
|
+
providerExecuted?: boolean;
|
|
1890
|
+
}
|
|
1891
|
+
| {
|
|
1892
|
+
state: "input-available";
|
|
1893
|
+
input: unknown;
|
|
1894
|
+
providerExecuted?: boolean;
|
|
1895
|
+
providerMetadata?: ProviderMetadata;
|
|
1896
|
+
}
|
|
1897
|
+
| {
|
|
1898
|
+
state: "output-available";
|
|
1899
|
+
input: unknown;
|
|
1900
|
+
output: unknown;
|
|
1901
|
+
providerExecuted?: boolean;
|
|
1902
|
+
preliminary?: boolean;
|
|
1903
|
+
}
|
|
1904
|
+
| {
|
|
1905
|
+
state: "output-error";
|
|
1906
|
+
input: unknown;
|
|
1907
|
+
rawInput?: unknown;
|
|
1908
|
+
errorText: string;
|
|
1909
|
+
providerExecuted?: boolean;
|
|
1910
|
+
providerMetadata?: ProviderMetadata;
|
|
1911
|
+
}
|
|
1912
|
+
)
|
|
1913
|
+
) {
|
|
1914
|
+
const { isToolUIPart } = await import("ai");
|
|
1915
|
+
|
|
1916
|
+
const part = message.parts.find(
|
|
1917
|
+
(part) =>
|
|
1918
|
+
isToolUIPart(part) &&
|
|
1919
|
+
(part as ToolUIPart).toolCallId === options.toolCallId
|
|
1920
|
+
) as ToolUIPart | undefined;
|
|
1921
|
+
|
|
1922
|
+
const anyOptions = options as Record<string, unknown>;
|
|
1923
|
+
const anyPart = part as Record<string, unknown>;
|
|
1924
|
+
|
|
1925
|
+
if (part != null) {
|
|
1926
|
+
part.state = options.state;
|
|
1927
|
+
anyPart.input = anyOptions.input;
|
|
1928
|
+
anyPart.output = anyOptions.output;
|
|
1929
|
+
anyPart.errorText = anyOptions.errorText;
|
|
1930
|
+
anyPart.rawInput = anyOptions.rawInput;
|
|
1931
|
+
anyPart.preliminary = anyOptions.preliminary;
|
|
1932
|
+
|
|
1933
|
+
// once providerExecuted is set, it stays for streaming
|
|
1934
|
+
anyPart.providerExecuted =
|
|
1935
|
+
anyOptions.providerExecuted ?? part.providerExecuted;
|
|
1936
|
+
|
|
1937
|
+
if (
|
|
1938
|
+
anyOptions.providerMetadata != null &&
|
|
1939
|
+
part.state === "input-available"
|
|
1940
|
+
) {
|
|
1941
|
+
part.callProviderMetadata =
|
|
1942
|
+
anyOptions.providerMetadata as ProviderMetadata;
|
|
1943
|
+
}
|
|
1944
|
+
} else {
|
|
1945
|
+
message.parts.push({
|
|
1946
|
+
type: `tool-${options.toolName}`,
|
|
1947
|
+
toolCallId: options.toolCallId,
|
|
1948
|
+
state: options.state,
|
|
1949
|
+
input: anyOptions.input,
|
|
1950
|
+
output: anyOptions.output,
|
|
1951
|
+
rawInput: anyOptions.rawInput,
|
|
1952
|
+
errorText: anyOptions.errorText,
|
|
1953
|
+
providerExecuted: anyOptions.providerExecuted,
|
|
1954
|
+
preliminary: anyOptions.preliminary,
|
|
1955
|
+
...(anyOptions.providerMetadata != null
|
|
1956
|
+
? { callProviderMetadata: anyOptions.providerMetadata }
|
|
1957
|
+
: {})
|
|
1958
|
+
} as ToolUIPart);
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
private async updateMessageMetadata(message: ChatMessage, metadata: unknown) {
|
|
1963
|
+
if (metadata != null) {
|
|
1964
|
+
const mergedMetadata =
|
|
1965
|
+
message.metadata != null
|
|
1966
|
+
? { ...message.metadata, ...metadata } // TODO: do proper merging
|
|
1967
|
+
: metadata;
|
|
1968
|
+
|
|
1969
|
+
message.metadata = mergedMetadata;
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
private async _reply(
|
|
1974
|
+
id: string,
|
|
1975
|
+
response: Response,
|
|
1976
|
+
excludeBroadcastIds: string[] = [],
|
|
1977
|
+
options: { continuation?: boolean } = {}
|
|
1978
|
+
) {
|
|
1979
|
+
const { continuation = false } = options;
|
|
1980
|
+
|
|
1981
|
+
return this._tryCatchChat(async () => {
|
|
1982
|
+
if (!response.body) {
|
|
1983
|
+
// Send empty response if no body
|
|
1984
|
+
this._broadcastChatMessage({
|
|
1985
|
+
body: "",
|
|
1986
|
+
done: true,
|
|
1987
|
+
id,
|
|
1988
|
+
type: MessageType.CF_AGENT_USE_CHAT_RESPONSE,
|
|
1989
|
+
...(continuation && { continuation: true })
|
|
1990
|
+
});
|
|
1991
|
+
return;
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
// Start tracking this stream for resumability
|
|
1995
|
+
const streamId = this._startStream(id);
|
|
1996
|
+
|
|
1997
|
+
const reader = response.body.getReader();
|
|
1998
|
+
|
|
1999
|
+
// Parsing state adapted from:
|
|
2000
|
+
// https://github.com/vercel/ai/blob/main/packages/ai/src/ui-message-stream/ui-message-chunks.ts#L295
|
|
2001
|
+
const message: ChatMessage = {
|
|
2002
|
+
id: `assistant_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`, // default
|
|
2003
|
+
role: "assistant",
|
|
2004
|
+
parts: []
|
|
2005
|
+
};
|
|
2006
|
+
// Track the streaming message so tool results can be applied before persistence
|
|
2007
|
+
this._streamingMessage = message;
|
|
2008
|
+
// Set up completion promise for tool continuation to wait on
|
|
2009
|
+
this._streamCompletionPromise = new Promise((resolve) => {
|
|
2010
|
+
this._streamCompletionResolve = resolve;
|
|
2011
|
+
});
|
|
2012
|
+
|
|
2013
|
+
// Determine response format based on content-type
|
|
2014
|
+
const contentType = response.headers.get("content-type") || "";
|
|
2015
|
+
const isSSE = contentType.includes("text/event-stream"); // AI SDK v5 SSE format
|
|
2016
|
+
const streamCompleted = { value: false };
|
|
2017
|
+
|
|
2018
|
+
try {
|
|
2019
|
+
if (isSSE) {
|
|
2020
|
+
// AI SDK v5 SSE format
|
|
2021
|
+
await this._streamSSEReply(
|
|
2022
|
+
id,
|
|
2023
|
+
streamId,
|
|
2024
|
+
reader,
|
|
2025
|
+
message,
|
|
2026
|
+
streamCompleted,
|
|
2027
|
+
continuation
|
|
2028
|
+
);
|
|
2029
|
+
} else {
|
|
2030
|
+
await this._sendPlaintextReply(
|
|
2031
|
+
id,
|
|
2032
|
+
streamId,
|
|
2033
|
+
reader,
|
|
2034
|
+
message,
|
|
2035
|
+
streamCompleted,
|
|
2036
|
+
continuation
|
|
2037
|
+
);
|
|
2038
|
+
}
|
|
1912
2039
|
} catch (error) {
|
|
1913
2040
|
// Mark stream as error if not already completed
|
|
1914
|
-
if (!streamCompleted) {
|
|
2041
|
+
if (!streamCompleted.value) {
|
|
1915
2042
|
this._markStreamError(streamId);
|
|
1916
2043
|
// Notify clients of the error
|
|
1917
2044
|
this._broadcastChatMessage({
|