@assistant-ui/react-ag-ui 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.md +2 -0
  2. package/dist/agUiExtras.js +1 -2
  3. package/dist/hooks.js +1 -2
  4. package/dist/runtime/AgUiThreadController.d.ts +11 -0
  5. package/dist/runtime/AgUiThreadController.d.ts.map +1 -1
  6. package/dist/runtime/AgUiThreadController.js +565 -89
  7. package/dist/runtime/AgUiThreadController.js.map +1 -1
  8. package/dist/runtime/adapter/conversions.d.ts +9 -1
  9. package/dist/runtime/adapter/conversions.d.ts.map +1 -1
  10. package/dist/runtime/adapter/conversions.js +194 -12
  11. package/dist/runtime/adapter/conversions.js.map +1 -1
  12. package/dist/runtime/adapter/replay-content.d.ts +9 -0
  13. package/dist/runtime/adapter/replay-content.d.ts.map +1 -0
  14. package/dist/runtime/adapter/replay-content.js +185 -0
  15. package/dist/runtime/adapter/replay-content.js.map +1 -0
  16. package/dist/runtime/adapter/run-aggregator.d.ts +1 -0
  17. package/dist/runtime/adapter/run-aggregator.d.ts.map +1 -1
  18. package/dist/runtime/adapter/run-aggregator.js +35 -14
  19. package/dist/runtime/adapter/run-aggregator.js.map +1 -1
  20. package/dist/runtime/adapter/subscriber.d.ts +0 -1
  21. package/dist/runtime/adapter/subscriber.d.ts.map +1 -1
  22. package/dist/runtime/adapter/subscriber.js +2 -10
  23. package/dist/runtime/adapter/subscriber.js.map +1 -1
  24. package/dist/runtime/adapter/tool-approval.d.ts +2 -0
  25. package/dist/runtime/adapter/tool-approval.d.ts.map +1 -1
  26. package/dist/runtime/adapter/tool-approval.js +23 -6
  27. package/dist/runtime/adapter/tool-approval.js.map +1 -1
  28. package/dist/runtime/agUiThreadState.d.ts +1 -1
  29. package/dist/runtime/agUiThreadState.d.ts.map +1 -1
  30. package/dist/runtime/agUiThreadState.js +3 -4
  31. package/dist/runtime/agUiThreadState.js.map +1 -1
  32. package/dist/runtime/event-parser.d.ts.map +1 -1
  33. package/dist/runtime/event-parser.js +2 -2
  34. package/dist/runtime/event-parser.js.map +1 -1
  35. package/dist/runtime/interrupt-internals.js +1 -2
  36. package/dist/runtime/logger.js +1 -2
  37. package/dist/runtime/mcp-tool-result.js +1 -2
  38. package/dist/runtime/types.d.ts +2 -0
  39. package/dist/runtime/types.d.ts.map +1 -1
  40. package/dist/runtime/types.js +3 -3
  41. package/dist/runtime/types.js.map +1 -1
  42. package/dist/useAgUiRuntime.js +1 -2
  43. package/package.json +6 -6
  44. package/src/runtime/AgUiThreadController.deferred-results.test.ts +548 -0
  45. package/src/runtime/AgUiThreadController.disconnect.test.ts +914 -0
  46. package/src/runtime/AgUiThreadController.interrupt-history.test.ts +392 -0
  47. package/src/runtime/AgUiThreadController.replay.test.ts +3939 -0
  48. package/src/runtime/AgUiThreadController.snapshot-head-move.test.ts +167 -0
  49. package/src/runtime/AgUiThreadController.subagent-snapshot.test.ts +854 -0
  50. package/src/runtime/AgUiThreadController.test.ts +119 -0
  51. package/src/runtime/AgUiThreadController.ts +1184 -136
  52. package/src/runtime/adapter/conversions.content.test.ts +242 -1
  53. package/src/runtime/adapter/conversions.subagent-messages.test.ts +550 -0
  54. package/src/runtime/adapter/conversions.ts +349 -28
  55. package/src/runtime/adapter/replay-content.test.ts +235 -0
  56. package/src/runtime/adapter/replay-content.ts +306 -0
  57. package/src/runtime/adapter/run-aggregator.metadata.test.ts +151 -0
  58. package/src/runtime/adapter/run-aggregator.ts +52 -9
  59. package/src/runtime/adapter/subscriber.test.ts +1 -1
  60. package/src/runtime/adapter/subscriber.ts +1 -6
  61. package/src/runtime/adapter/tool-approval.ts +41 -3
  62. package/src/runtime/agUiThreadState.test.ts +21 -2
  63. package/src/runtime/agUiThreadState.ts +12 -3
  64. package/src/runtime/event-parser.ts +3 -0
  65. package/src/runtime/types.ts +5 -0
@@ -5,8 +5,10 @@ import type {
5
5
  PartSource,
6
6
  RunAgentParameters,
7
7
  } from "@ag-ui/client";
8
- import { generateId } from "@assistant-ui/core";
8
+ import { fromThreadMessageLike, generateId } from "@assistant-ui/core";
9
9
  import type {
10
+ MessageStatus,
11
+ ThreadMessage,
10
12
  ThreadMessageLike as CoreThreadMessageLike,
11
13
  PartProviderMetadata,
12
14
  ReasoningMessagePart,
@@ -26,6 +28,7 @@ import {
26
28
  MCP_APPS_ACTIVITY_TYPE,
27
29
  type AgUiCustomMetadata,
28
30
  type AgUiOpaqueReasoning,
31
+ MAX_SUBAGENT_DEPTH,
29
32
  } from "../types";
30
33
  import {
31
34
  applyA2uiOperations,
@@ -212,7 +215,7 @@ function withAgUiMessageMetadata(metadata: unknown): {
212
215
  return messageMetadata !== undefined ? { metadata: messageMetadata } : {};
213
216
  }
214
217
 
215
- function withAgUiPartMessageMetadata(
218
+ export function withAgUiPartMessageMetadata(
216
219
  providerMetadata: PartProviderMetadata | undefined,
217
220
  messageMetadata: Record<string, unknown> | undefined,
218
221
  ): PartProviderMetadata {
@@ -222,11 +225,17 @@ function withAgUiPartMessageMetadata(
222
225
  const namespaced = isObject(existing[AG_UI_METADATA_NAMESPACE])
223
226
  ? existing[AG_UI_METADATA_NAMESPACE]
224
227
  : {};
228
+ const previousMessageMetadata = isObject(namespaced.messageMetadata)
229
+ ? namespaced.messageMetadata
230
+ : {};
225
231
  return {
226
232
  ...existing,
227
233
  [AG_UI_METADATA_NAMESPACE]: {
228
234
  ...namespaced,
229
- messageMetadata: messageMetadata as ReadonlyJSONObject,
235
+ messageMetadata: {
236
+ ...previousMessageMetadata,
237
+ ...messageMetadata,
238
+ } as ReadonlyJSONObject,
230
239
  },
231
240
  };
232
241
  }
@@ -342,8 +351,6 @@ function extractText(content: unknown): string {
342
351
  .join("\n");
343
352
  }
344
353
 
345
- type EncodedPartSource = Extract<PartSource, { type: "data" | "url" }>;
346
-
347
354
  type MediaInputType = "image" | "audio" | "video" | "document";
348
355
 
349
356
  function mediaTypeForMime(mimeType: string | undefined): MediaInputType {
@@ -363,7 +370,14 @@ function buildPartSource(
363
370
  value: string,
364
371
  declaredMimeType: string | undefined,
365
372
  sourceType?: string,
366
- ): EncodedPartSource {
373
+ ): PartSource {
374
+ if (sourceType === "id") {
375
+ return {
376
+ type: "file",
377
+ value,
378
+ ...(declaredMimeType !== undefined && { mimeType: declaredMimeType }),
379
+ };
380
+ }
367
381
  const source = resolveFilePartSource({
368
382
  data: value,
369
383
  mimeType: declaredMimeType ?? "application/octet-stream",
@@ -460,9 +474,12 @@ type SnapshotAttachment = NonNullable<
460
474
 
461
475
  const mediaInputTypes = new Set(["image", "audio", "video", "document"]);
462
476
 
463
- function partSourceToString(
464
- value: unknown,
465
- ): { value: string; mimeType?: string; isUrl?: boolean } | null {
477
+ function partSourceToString(value: unknown): {
478
+ value: string;
479
+ mimeType?: string;
480
+ isUrl?: boolean;
481
+ isId?: boolean;
482
+ } | null {
466
483
  if (!isObject(value)) return null;
467
484
  const sourceValue = getString(value, "value");
468
485
  if (sourceValue === undefined) return null;
@@ -475,6 +492,13 @@ function partSourceToString(
475
492
  ...(mimeType !== undefined && { mimeType }),
476
493
  };
477
494
  }
495
+ if (type === "file") {
496
+ return {
497
+ value: sourceValue,
498
+ isId: true,
499
+ ...(mimeType !== undefined && { mimeType }),
500
+ };
501
+ }
478
502
  if (type === "data") {
479
503
  const resolvedMimeType = mimeType ?? "application/octet-stream";
480
504
  return {
@@ -541,15 +565,11 @@ function toUserContentLayout(content: unknown): {
541
565
  }
542
566
  const part = normalizeInputPart(rawPart);
543
567
  if (!part || !mediaInputTypes.has(getString(part, "type") ?? "")) continue;
544
- if (
545
- isObject(part.source) &&
546
- part.source.type === "file" &&
547
- typeof part.source.value === "string"
548
- ) {
549
- fileSourceParts.push({ index: mediaIndex++, part });
550
- } else if (partSourceToString(part.source)) {
551
- mediaIndex++;
568
+ if (!partSourceToString(part.source)) continue;
569
+ if (isObject(part.source) && part.source.type === "file") {
570
+ fileSourceParts.push({ index: mediaIndex, part });
552
571
  }
572
+ mediaIndex++;
553
573
  }
554
574
 
555
575
  const leadingText =
@@ -576,7 +596,7 @@ function toSnapshotAttachments(content: unknown): SnapshotAttachment[] {
576
596
  const { filename, providerMetadata } = readPartMetadata(part.metadata);
577
597
  const id = attachments.length.toString();
578
598
 
579
- if (type === "image") {
599
+ if (type === "image" && !source.isId) {
580
600
  attachments.push({
581
601
  id,
582
602
  type: "image",
@@ -608,6 +628,7 @@ function toSnapshotAttachments(content: unknown): SnapshotAttachment[] {
608
628
  data: source.value,
609
629
  mimeType,
610
630
  ...(source.isUrl && { sourceType: "url" as const }),
631
+ ...(source.isId && { sourceType: "id" as const }),
611
632
  ...(filename !== undefined && { filename }),
612
633
  ...(providerMetadata && { providerMetadata }),
613
634
  },
@@ -679,15 +700,28 @@ function buildUserContent(message: ThreadMessageLike): string | ContentPart[] {
679
700
  isObject(part.source) &&
680
701
  part.source.type === "file" &&
681
702
  typeof part.source.value === "string"
682
- ? [{ index, part: part as ContentPart }]
703
+ ? [{ index, part: part as ContentPart, value: part.source.value }]
683
704
  : [],
684
705
  );
685
706
  const ordered = restoresText
686
707
  ? converted.filter((part) => part.type !== "text")
687
708
  : converted;
688
- for (const { index, part } of fileSourceSlots.sort(
709
+ for (const { index, part, value } of fileSourceSlots.sort(
689
710
  (a, b) => a.index - b.index,
690
711
  )) {
712
+ // A snapshot also imports the handle as an attachment; the slot replaces
713
+ // it in place to restore fields the attachment cannot hold.
714
+ const attached = ordered.findIndex(
715
+ (contentPart) =>
716
+ contentPart.type !== "text" &&
717
+ contentPart.source.type === "file" &&
718
+ contentPart.source.value === value &&
719
+ !fileSourceSlots.some((slot) => slot.part === contentPart),
720
+ );
721
+ if (attached !== -1) {
722
+ ordered[attached] = part;
723
+ continue;
724
+ }
691
725
  const mediaPositions = ordered.flatMap((contentPart, position) =>
692
726
  contentPart.type !== "text" ? [position] : [],
693
727
  );
@@ -1102,9 +1136,286 @@ export type FromAgUiMessagesOptions = {
1102
1136
  showThinking?: boolean;
1103
1137
  };
1104
1138
 
1139
+ const readSubagentRunId = (record: Record<string, unknown>) => {
1140
+ const runId = getString(record, "subagentRunId");
1141
+ return runId?.trim() ? runId : undefined;
1142
+ };
1143
+
1144
+ const readToolCallIds = (record: Record<string, unknown>) =>
1145
+ extractAssistantToolCalls(record).flatMap((part) =>
1146
+ part.toolCallId ? [part.toolCallId] : [],
1147
+ );
1148
+
1105
1149
  export function fromAgUiMessages(
1106
1150
  messages: readonly unknown[],
1107
1151
  options?: FromAgUiMessagesOptions,
1152
+ ): CoreThreadMessageLike[] {
1153
+ return fromAgUiSnapshotMessages(messages, options);
1154
+ }
1155
+
1156
+ /**
1157
+ * Converts AG-UI messages, nesting each subagent run's messages under the tool
1158
+ * call `SUBAGENT_STARTED.parentToolCallId` announced for it. A message's
1159
+ * `subagentRunId` names its run; a run with no announced spawning call in
1160
+ * `messages` stays in the parent thread.
1161
+ */
1162
+ export function fromAgUiSnapshotMessages(
1163
+ messages: readonly unknown[],
1164
+ options?: FromAgUiMessagesOptions,
1165
+ subagentParentToolCallIds?: ReadonlyMap<string, string | undefined>,
1166
+ ): CoreThreadMessageLike[] {
1167
+ const records = messages.filter(isObject);
1168
+ const callOwners = new Map<string, string | undefined>();
1169
+ for (const record of records) {
1170
+ if (getString(record, "role") !== "assistant") continue;
1171
+ for (const toolCallId of readToolCallIds(record)) {
1172
+ callOwners.set(toolCallId, readSubagentRunId(record));
1173
+ }
1174
+ }
1175
+
1176
+ const spawningCalls = new Map<string, string>();
1177
+ for (const record of records) {
1178
+ const runId = readSubagentRunId(record);
1179
+ if (runId === undefined || spawningCalls.has(runId)) continue;
1180
+ const toolCallId = subagentParentToolCallIds?.get(runId);
1181
+ if (toolCallId !== undefined && callOwners.has(toolCallId))
1182
+ spawningCalls.set(runId, toolCallId);
1183
+ }
1184
+ if (spawningCalls.size === 0) return convertAgUiMessages(messages, options);
1185
+
1186
+ const reachesRoot = (runId: string) => {
1187
+ let current: string | undefined = runId;
1188
+ for (let depth = 0; current !== undefined; depth++) {
1189
+ const toolCallId = spawningCalls.get(current);
1190
+ if (toolCallId === undefined || depth > MAX_SUBAGENT_DEPTH) return false;
1191
+ current = callOwners.get(toolCallId);
1192
+ }
1193
+ return true;
1194
+ };
1195
+ const nestedRuns = new Set([...spawningCalls.keys()].filter(reachesRoot));
1196
+
1197
+ const groups = new Map<string | undefined, unknown[]>();
1198
+ for (const record of records) {
1199
+ const ownRunId = readSubagentRunId(record);
1200
+ const answered =
1201
+ getString(record, "role") === "tool" ? getToolCallId(record) : undefined;
1202
+ const runId =
1203
+ answered !== undefined && callOwners.has(answered)
1204
+ ? callOwners.get(answered)
1205
+ : ownRunId;
1206
+ const role = getString(record, "role");
1207
+ const group =
1208
+ role !== "user" &&
1209
+ role !== "system" &&
1210
+ role !== "developer" &&
1211
+ runId !== undefined &&
1212
+ nestedRuns.has(runId)
1213
+ ? runId
1214
+ : undefined;
1215
+ const members = groups.get(group) ?? [];
1216
+ members.push(record);
1217
+ groups.set(group, members);
1218
+ }
1219
+
1220
+ const runsBySpawningCall = new Map<string, string[]>();
1221
+ for (const runId of nestedRuns) {
1222
+ const toolCallId = spawningCalls.get(runId)!;
1223
+ runsBySpawningCall.set(toolCallId, [
1224
+ ...(runsBySpawningCall.get(toolCallId) ?? []),
1225
+ runId,
1226
+ ]);
1227
+ }
1228
+
1229
+ const convertGroup = (group: string | undefined): CoreThreadMessageLike[] =>
1230
+ convertAgUiMessages(groups.get(group) ?? [], options).map((message) => {
1231
+ if (message.role !== "assistant" || !Array.isArray(message.content))
1232
+ return message;
1233
+ const nested: ThreadMessage[] = [];
1234
+ const content = message.content.map((part) => {
1235
+ if (part.type !== "tool-call") return part;
1236
+ const runIds = runsBySpawningCall.get(part.toolCallId);
1237
+ if (!runIds) return part;
1238
+ const subagents = runIds.map(toSubagentMessage);
1239
+ nested.push(...subagents);
1240
+ return { ...part, messages: [...(part.messages ?? []), ...subagents] };
1241
+ });
1242
+ return liftSubagentState({ ...message, content }, nested);
1243
+ });
1244
+
1245
+ const toSubagentMessage = (runId: string): ThreadMessage => {
1246
+ const messages = convertGroup(runId).filter(
1247
+ (message) => message.role === "assistant",
1248
+ );
1249
+ const folded = messages
1250
+ .map((message) =>
1251
+ typeof message.content === "string"
1252
+ ? {
1253
+ ...message,
1254
+ content: message.content
1255
+ ? [{ type: "text" as const, text: message.content }]
1256
+ : [],
1257
+ }
1258
+ : message,
1259
+ )
1260
+ .reduce<CoreThreadMessageLike | undefined>(
1261
+ (merged, message) =>
1262
+ merged ? foldTurnRecords(merged, message) : message,
1263
+ undefined,
1264
+ );
1265
+ const content = Array.isArray(folded?.content) ? folded.content : [];
1266
+ const pendingToolCallIds = new Set(
1267
+ messages
1268
+ .filter(
1269
+ (message) =>
1270
+ message.status?.type === "requires-action" &&
1271
+ message.status.reason === "tool-calls",
1272
+ )
1273
+ .flatMap((message) => admittedUnresolvedToolCallIds(message, true)),
1274
+ );
1275
+ const metadata = pendingToolCallIds.size
1276
+ ? withPendingToolCallIds(folded?.metadata, pendingToolCallIds)
1277
+ : folded?.metadata;
1278
+ return fromThreadMessageLike(
1279
+ {
1280
+ id: runId,
1281
+ role: "assistant",
1282
+ content,
1283
+ ...(metadata ? { metadata } : {}),
1284
+ },
1285
+ runId,
1286
+ subagentStatus(folded?.metadata, messages),
1287
+ );
1288
+ };
1289
+
1290
+ return convertGroup(undefined);
1291
+ }
1292
+
1293
+ const hasUnresolvedToolCall = (content: readonly unknown[]) =>
1294
+ content.some(
1295
+ (part) =>
1296
+ isObject(part) && part.type === "tool-call" && part.result === undefined,
1297
+ );
1298
+
1299
+ function admittedUnresolvedToolCallIds(
1300
+ message: CoreThreadMessageLike,
1301
+ includeNested: boolean,
1302
+ ): string[] {
1303
+ if (message.role !== "assistant" || !Array.isArray(message.content))
1304
+ return [];
1305
+ const listed = readPersistedPendingToolCallIds(message.metadata);
1306
+ if (listed) return listed;
1307
+ return message.content.flatMap((part) => {
1308
+ if (part.type !== "tool-call") return [];
1309
+ const calls = includeNested
1310
+ ? [
1311
+ part,
1312
+ ...Array.from(
1313
+ walkToolCallTree(part.messages ?? []),
1314
+ ({ part }) => part,
1315
+ ),
1316
+ ]
1317
+ : [part];
1318
+ return calls.flatMap((call) =>
1319
+ call.result === undefined && call.toolCallId ? [call.toolCallId] : [],
1320
+ );
1321
+ });
1322
+ }
1323
+
1324
+ function withPendingToolCallIds(
1325
+ metadata: CoreThreadMessageLike["metadata"],
1326
+ ids: ReadonlySet<string>,
1327
+ ): CoreThreadMessageLike["metadata"] {
1328
+ return {
1329
+ ...(isObject(metadata) ? metadata : {}),
1330
+ custom: {
1331
+ ...readCustomMetadata(metadata),
1332
+ [AG_UI_METADATA_NAMESPACE]: {
1333
+ ...readNamespacedMetadata(metadata),
1334
+ pendingToolCallIds: [...ids],
1335
+ },
1336
+ },
1337
+ };
1338
+ }
1339
+
1340
+ function subagentStatus(
1341
+ metadata: unknown,
1342
+ messages: readonly CoreThreadMessageLike[],
1343
+ ): MessageStatus {
1344
+ const hasInterrupt = readPersistedInterrupts(metadata) !== undefined;
1345
+ const hasPendingToolCall = messages.some(
1346
+ (message) =>
1347
+ message.status?.type === "requires-action" &&
1348
+ message.status.reason === "tool-calls",
1349
+ );
1350
+ return hasInterrupt || hasPendingToolCall
1351
+ ? getAutoStatus(false, false, hasInterrupt, hasPendingToolCall, undefined)
1352
+ : { type: "complete", reason: "unknown" };
1353
+ }
1354
+
1355
+ /**
1356
+ * A nested run's open interrupts and unanswered calls surface on the message
1357
+ * that spawned it, as the live run does.
1358
+ */
1359
+ function liftSubagentState(
1360
+ message: CoreThreadMessageLike,
1361
+ nested: readonly ThreadMessage[],
1362
+ ): CoreThreadMessageLike {
1363
+ const waiting = nested.filter(
1364
+ (subagent) => subagent.status?.type === "requires-action",
1365
+ );
1366
+ if (waiting.length === 0) return message;
1367
+ const interrupts = [
1368
+ ...(readPersistedInterrupts(message.metadata) ?? []),
1369
+ ...waiting.flatMap(
1370
+ (subagent) => readPersistedInterrupts(subagent.metadata) ?? [],
1371
+ ),
1372
+ ];
1373
+ const custom = readCustomMetadata(message.metadata);
1374
+ const metadata = {
1375
+ ...(isObject(message.metadata) ? message.metadata : {}),
1376
+ custom: {
1377
+ ...custom,
1378
+ [AG_UI_METADATA_NAMESPACE]: {
1379
+ ...readNamespacedMetadata(message.metadata),
1380
+ ...(interrupts.length > 0 ? { interrupts } : {}),
1381
+ },
1382
+ },
1383
+ };
1384
+ const hasInterrupt = interrupts.length > 0;
1385
+ const hasPendingToolCall =
1386
+ waiting.some(
1387
+ (subagent) =>
1388
+ subagent.status?.type === "requires-action" &&
1389
+ subagent.status.reason === "tool-calls",
1390
+ ) ||
1391
+ (Array.isArray(message.content) && hasUnresolvedToolCall(message.content));
1392
+ const pendingToolCallIds = new Set([
1393
+ ...admittedUnresolvedToolCallIds(message, false),
1394
+ ...waiting.flatMap((subagent) =>
1395
+ subagent.status?.type === "requires-action" &&
1396
+ subagent.status.reason === "tool-calls"
1397
+ ? admittedUnresolvedToolCallIds(subagent, true)
1398
+ : [],
1399
+ ),
1400
+ ]);
1401
+ return {
1402
+ ...message,
1403
+ metadata: pendingToolCallIds.size
1404
+ ? withPendingToolCallIds(metadata, pendingToolCallIds)
1405
+ : metadata,
1406
+ status: getAutoStatus(
1407
+ false,
1408
+ false,
1409
+ hasInterrupt,
1410
+ hasPendingToolCall,
1411
+ undefined,
1412
+ ),
1413
+ };
1414
+ }
1415
+
1416
+ function convertAgUiMessages(
1417
+ messages: readonly unknown[],
1418
+ options?: FromAgUiMessagesOptions,
1108
1419
  ): CoreThreadMessageLike[] {
1109
1420
  const showThinking = options?.showThinking ?? true;
1110
1421
  const converted: CoreThreadMessageLike[] = [];
@@ -1652,14 +1963,24 @@ function emitToolResult(
1652
1963
  ): void {
1653
1964
  if (part.result === undefined) return;
1654
1965
 
1655
- const resultContent =
1656
- part.modelContent !== undefined
1657
- ? extractText(part.modelContent)
1658
- : isToolContentParts(part.result)
1659
- ? part.result
1660
- : typeof part.result === "string"
1661
- ? part.result
1662
- : JSON.stringify(part.result);
1966
+ let resultContent: string | ContentPart[];
1967
+ if (part.modelContent?.some((content) => content.type === "file")) {
1968
+ resultContent = part.modelContent.flatMap((content) => {
1969
+ const contentPart = toContentPart(
1970
+ content,
1971
+ content.type === "file" ? content.mediaType : undefined,
1972
+ );
1973
+ return contentPart ? [contentPart] : [];
1974
+ });
1975
+ } else if (part.modelContent !== undefined) {
1976
+ resultContent = extractText(part.modelContent);
1977
+ } else if (isToolContentParts(part.result)) {
1978
+ resultContent = part.result;
1979
+ } else if (typeof part.result === "string") {
1980
+ resultContent = part.result;
1981
+ } else {
1982
+ resultContent = JSON.stringify(part.result);
1983
+ }
1663
1984
  const messageMetadata = readAgUiPartMessageMetadata(part.providerMetadata);
1664
1985
 
1665
1986
  converted.push({