@assistant-ui/react-ag-ui 0.0.71 → 0.1.0
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/README.md +1 -1
- package/dist/runtime/{AgUiThreadRuntimeCore.d.ts → AgUiThreadController.d.ts} +6 -12
- package/dist/runtime/AgUiThreadController.d.ts.map +1 -0
- package/dist/runtime/{AgUiThreadRuntimeCore.js → AgUiThreadController.js} +246 -159
- package/dist/runtime/AgUiThreadController.js.map +1 -0
- package/dist/runtime/adapter/conversions.d.ts +4 -4
- package/dist/runtime/adapter/conversions.d.ts.map +1 -1
- package/dist/runtime/adapter/conversions.js +111 -33
- package/dist/runtime/adapter/conversions.js.map +1 -1
- package/dist/runtime/adapter/run-aggregator.d.ts +3 -5
- package/dist/runtime/adapter/run-aggregator.d.ts.map +1 -1
- package/dist/runtime/adapter/run-aggregator.js +47 -61
- package/dist/runtime/adapter/run-aggregator.js.map +1 -1
- package/dist/runtime/adapter/subscriber.d.ts +0 -24
- package/dist/runtime/adapter/subscriber.d.ts.map +1 -1
- package/dist/runtime/adapter/subscriber.js +1 -22
- package/dist/runtime/adapter/subscriber.js.map +1 -1
- package/dist/runtime/agUiThreadState.d.ts +60 -0
- package/dist/runtime/agUiThreadState.d.ts.map +1 -0
- package/dist/runtime/agUiThreadState.js +95 -0
- package/dist/runtime/agUiThreadState.js.map +1 -0
- package/dist/runtime/event-parser.d.ts.map +1 -1
- package/dist/runtime/event-parser.js +45 -48
- package/dist/runtime/event-parser.js.map +1 -1
- package/dist/runtime/mcp-tool-result.d.ts +3 -1
- package/dist/runtime/mcp-tool-result.d.ts.map +1 -1
- package/dist/runtime/mcp-tool-result.js +6 -3
- package/dist/runtime/mcp-tool-result.js.map +1 -1
- package/dist/runtime/types.d.ts +11 -35
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/runtime/types.js.map +1 -1
- package/dist/useAgUiRuntime.js +2 -2
- package/dist/useAgUiRuntime.js.map +1 -1
- package/package.json +12 -11
- package/src/runtime/{AgUiThreadRuntimeCore.interrupt-cancel.test.ts → AgUiThreadController.interrupt-cancel.test.ts} +4 -4
- package/src/runtime/{AgUiThreadRuntimeCore.test.ts → AgUiThreadController.test.ts} +35 -6
- package/src/runtime/{AgUiThreadRuntimeCore.ts → AgUiThreadController.ts} +356 -171
- package/src/runtime/adapter/conversions.content.test.ts +37 -5
- package/src/runtime/adapter/conversions.tools.test.ts +2 -1
- package/src/runtime/adapter/conversions.ts +235 -58
- package/src/runtime/adapter/run-aggregator.ts +79 -114
- package/src/runtime/adapter/subscriber.ts +1 -44
- package/src/runtime/agUiThreadState.test.ts +205 -0
- package/src/runtime/agUiThreadState.ts +130 -0
- package/src/runtime/event-parser.ts +40 -58
- package/src/runtime/mcp-tool-result.ts +16 -2
- package/src/runtime/types.ts +16 -28
- package/src/useAgUiRuntime.fast-refresh.test.tsx +3 -3
- package/src/useAgUiRuntime.thread-switch.test.tsx +4 -4
- package/src/useAgUiRuntime.ts +3 -3
- package/dist/runtime/AgUiThreadRuntimeCore.d.ts.map +0 -1
- package/dist/runtime/AgUiThreadRuntimeCore.js.map +0 -1
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import { describe, expect, expectTypeOf, it } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
UserMessageSchema,
|
|
5
|
-
type Message as AgUiWireMessage,
|
|
6
|
-
} from "@ag-ui/client";
|
|
2
|
+
import type { Message as AgUiWireMessage } from "@ag-ui/client";
|
|
3
|
+
import { MessageSchema, UserMessageSchema } from "@ag-ui/core/schemas";
|
|
7
4
|
import type { AppendMessage } from "@assistant-ui/core";
|
|
8
5
|
import {
|
|
9
6
|
fromAgUiMessages,
|
|
@@ -82,6 +79,41 @@ describe("toAgUiMessages data URLs", () => {
|
|
|
82
79
|
});
|
|
83
80
|
|
|
84
81
|
describe("toAgUiMessages content metadata", () => {
|
|
82
|
+
it.each(["image", "document"] as const)(
|
|
83
|
+
"preserves a %s file source and its metadata through a snapshot round trip",
|
|
84
|
+
(type) => {
|
|
85
|
+
const part = {
|
|
86
|
+
type,
|
|
87
|
+
source: {
|
|
88
|
+
type: "file",
|
|
89
|
+
value: `${type}-handle`,
|
|
90
|
+
provider: "openai",
|
|
91
|
+
mimeType: type === "image" ? "image/png" : "application/pdf",
|
|
92
|
+
},
|
|
93
|
+
metadata: { filename: `${type}.bin`, file_id: `${type}-1` },
|
|
94
|
+
};
|
|
95
|
+
const original = {
|
|
96
|
+
id: "u-file",
|
|
97
|
+
role: "user",
|
|
98
|
+
content: [
|
|
99
|
+
{ type: "text", text: "label" },
|
|
100
|
+
{
|
|
101
|
+
type: "image",
|
|
102
|
+
source: { type: "url", value: "https://example.com/other.png" },
|
|
103
|
+
},
|
|
104
|
+
part,
|
|
105
|
+
],
|
|
106
|
+
metadata: { trace: "kept" },
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const imported = fromAgUiMessages([original]);
|
|
110
|
+
const exported = toAgUiMessages(imported);
|
|
111
|
+
|
|
112
|
+
expect(MessageSchema.parse(exported[0])).toEqual(original);
|
|
113
|
+
expect(exported).toEqual([original]);
|
|
114
|
+
},
|
|
115
|
+
);
|
|
116
|
+
|
|
85
117
|
it("emits AgUiMessage values assignable to AG-UI Message", () => {
|
|
86
118
|
expectTypeOf<AgUiMessage>().toExtend<AgUiWireMessage>();
|
|
87
119
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, expect, expectTypeOf, it } from "vitest";
|
|
2
|
-
import {
|
|
2
|
+
import type { RunAgentParameters } from "@ag-ui/client";
|
|
3
|
+
import { ToolSchema } from "@ag-ui/core/schemas";
|
|
3
4
|
import { toAgUiTools } from "./conversions";
|
|
4
5
|
|
|
5
6
|
describe("toAgUiTools", () => {
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
ContentPart,
|
|
5
|
+
PartSource,
|
|
6
|
+
RunAgentParameters,
|
|
7
|
+
} from "@ag-ui/client";
|
|
4
8
|
import { generateId } from "@assistant-ui/core";
|
|
5
9
|
import type {
|
|
6
10
|
ThreadMessageLike as CoreThreadMessageLike,
|
|
@@ -34,10 +38,9 @@ import { projectAgUiToolApprovals } from "./tool-approval";
|
|
|
34
38
|
import {
|
|
35
39
|
parseMcpToolCallResult,
|
|
36
40
|
readMcpAppResourceUri,
|
|
41
|
+
toolContentText,
|
|
37
42
|
} from "../mcp-tool-result";
|
|
38
43
|
|
|
39
|
-
export type { InputContent };
|
|
40
|
-
|
|
41
44
|
type AttachmentLike = {
|
|
42
45
|
name?: string | undefined;
|
|
43
46
|
contentType?: string | undefined;
|
|
@@ -67,7 +70,7 @@ export type AgUiMessage =
|
|
|
67
70
|
| {
|
|
68
71
|
id: string;
|
|
69
72
|
role: "user";
|
|
70
|
-
content: string |
|
|
73
|
+
content: string | ContentPart[];
|
|
71
74
|
name?: string;
|
|
72
75
|
metadata?: Record<string, unknown>;
|
|
73
76
|
}
|
|
@@ -96,7 +99,7 @@ export type AgUiMessage =
|
|
|
96
99
|
| {
|
|
97
100
|
id: string;
|
|
98
101
|
role: "tool";
|
|
99
|
-
content: string;
|
|
102
|
+
content: string | ContentPart[];
|
|
100
103
|
toolCallId: string;
|
|
101
104
|
error?: string;
|
|
102
105
|
metadata?: Record<string, unknown>;
|
|
@@ -137,14 +140,22 @@ function readLegacyAgUiNamespace(metadata: unknown): Record<string, unknown> {
|
|
|
137
140
|
: {};
|
|
138
141
|
}
|
|
139
142
|
|
|
140
|
-
function
|
|
143
|
+
function withoutPersistedStatusMetadata(
|
|
141
144
|
wire: Record<string, unknown>,
|
|
142
145
|
): Record<string, unknown> {
|
|
143
146
|
if (!isObject(wire.custom)) return wire;
|
|
144
147
|
const namespaced = wire.custom[AG_UI_METADATA_NAMESPACE];
|
|
145
|
-
if (
|
|
146
|
-
|
|
147
|
-
|
|
148
|
+
if (
|
|
149
|
+
!isObject(namespaced) ||
|
|
150
|
+
(!("interrupts" in namespaced) && !("pendingToolCallIds" in namespaced))
|
|
151
|
+
)
|
|
152
|
+
return wire;
|
|
153
|
+
|
|
154
|
+
const {
|
|
155
|
+
interrupts: _interrupts,
|
|
156
|
+
pendingToolCallIds: _pendingToolCallIds,
|
|
157
|
+
...remainingNamespace
|
|
158
|
+
} = namespaced;
|
|
148
159
|
const { [AG_UI_METADATA_NAMESPACE]: _namespace, ...remainingCustom } =
|
|
149
160
|
wire.custom;
|
|
150
161
|
if (Object.keys(remainingNamespace).length > 0) {
|
|
@@ -160,12 +171,17 @@ function toSnapshotCustom(
|
|
|
160
171
|
metadata: unknown,
|
|
161
172
|
state: Record<string, unknown> = {},
|
|
162
173
|
): Record<string, unknown> {
|
|
163
|
-
const wire =
|
|
174
|
+
const wire = withoutPersistedStatusMetadata(
|
|
175
|
+
toAgUiWireMetadata(metadata) ?? {},
|
|
176
|
+
);
|
|
164
177
|
const needsWireSlot =
|
|
165
178
|
isObject(metadata) &&
|
|
166
179
|
(Object.keys(metadata).length === 0 || AG_UI_METADATA_NAMESPACE in wire);
|
|
167
180
|
const namespaced = {
|
|
168
181
|
...readLegacyAgUiNamespace(metadata),
|
|
182
|
+
...(readPersistedPendingToolCallIds(metadata) !== undefined
|
|
183
|
+
? { pendingToolCallIds: readPersistedPendingToolCallIds(metadata) }
|
|
184
|
+
: {}),
|
|
169
185
|
...state,
|
|
170
186
|
...(needsWireSlot ? { messageMetadata: wire } : {}),
|
|
171
187
|
};
|
|
@@ -246,7 +262,7 @@ const readAgUiReasoningMeta = (providerMetadata: unknown) => {
|
|
|
246
262
|
// AG-UI carries per-item extras in `metadata`; anything else on the item is
|
|
247
263
|
// stripped by its schema. The part's own `filename` wins over a key of the
|
|
248
264
|
// same name in the bag.
|
|
249
|
-
const
|
|
265
|
+
const buildPartMetadata = (
|
|
250
266
|
part: Record<string, unknown>,
|
|
251
267
|
filename?: string | undefined,
|
|
252
268
|
) => {
|
|
@@ -260,7 +276,7 @@ const buildInputMetadata = (
|
|
|
260
276
|
// The inverse: `filename` lands on the part's own field, everything else the
|
|
261
277
|
// item carried goes back into the namespace it came from, so a snapshot echo
|
|
262
278
|
// resends what the host attached.
|
|
263
|
-
const
|
|
279
|
+
const readPartMetadata = (
|
|
264
280
|
metadata: unknown,
|
|
265
281
|
): {
|
|
266
282
|
filename?: string | undefined;
|
|
@@ -326,9 +342,7 @@ function extractText(content: unknown): string {
|
|
|
326
342
|
.join("\n");
|
|
327
343
|
}
|
|
328
344
|
|
|
329
|
-
type
|
|
330
|
-
| { type: "data"; value: string; mimeType: string }
|
|
331
|
-
| { type: "url"; value: string; mimeType?: string };
|
|
345
|
+
type EncodedPartSource = Extract<PartSource, { type: "data" | "url" }>;
|
|
332
346
|
|
|
333
347
|
type MediaInputType = "image" | "audio" | "video" | "document";
|
|
334
348
|
|
|
@@ -345,11 +359,11 @@ function mediaTypeForMime(mimeType: string | undefined): MediaInputType {
|
|
|
345
359
|
// `sourceType: "url"` on the part forces the url leg for non-http references,
|
|
346
360
|
// and a data URL that is not base64 takes it too, since a `data` source
|
|
347
361
|
// carries base64 bytes.
|
|
348
|
-
function
|
|
362
|
+
function buildPartSource(
|
|
349
363
|
value: string,
|
|
350
364
|
declaredMimeType: string | undefined,
|
|
351
365
|
sourceType?: string,
|
|
352
|
-
):
|
|
366
|
+
): EncodedPartSource {
|
|
353
367
|
const source = resolveFilePartSource({
|
|
354
368
|
data: value,
|
|
355
369
|
mimeType: declaredMimeType ?? "application/octet-stream",
|
|
@@ -368,10 +382,10 @@ function buildInputSource(
|
|
|
368
382
|
};
|
|
369
383
|
}
|
|
370
384
|
|
|
371
|
-
function
|
|
385
|
+
function toContentPart(
|
|
372
386
|
part: unknown,
|
|
373
387
|
fallbackMimeType: string | undefined,
|
|
374
|
-
):
|
|
388
|
+
): ContentPart | null {
|
|
375
389
|
if (!isObject(part)) return null;
|
|
376
390
|
const type = getString(part, "type");
|
|
377
391
|
|
|
@@ -384,8 +398,8 @@ function toInputContent(
|
|
|
384
398
|
if (type === "image") {
|
|
385
399
|
const image = getString(part, "image");
|
|
386
400
|
if (image === undefined) return null;
|
|
387
|
-
const metadata =
|
|
388
|
-
const source =
|
|
401
|
+
const metadata = buildPartMetadata(part, getString(part, "filename"));
|
|
402
|
+
const source = buildPartSource(image, fallbackMimeType);
|
|
389
403
|
return {
|
|
390
404
|
type: "image",
|
|
391
405
|
source:
|
|
@@ -404,12 +418,12 @@ function toInputContent(
|
|
|
404
418
|
if (data === undefined) return null;
|
|
405
419
|
const declaredMimeType = getString(part, "mimeType") || fallbackMimeType;
|
|
406
420
|
const filename = getString(part, "filename");
|
|
407
|
-
const source =
|
|
421
|
+
const source = buildPartSource(
|
|
408
422
|
data,
|
|
409
423
|
declaredMimeType,
|
|
410
424
|
getString(part, "sourceType"),
|
|
411
425
|
);
|
|
412
|
-
const metadata =
|
|
426
|
+
const metadata = buildPartMetadata(part, filename);
|
|
413
427
|
switch (mediaTypeForMime(source.mimeType)) {
|
|
414
428
|
case "image":
|
|
415
429
|
return { type: "image", source, ...(metadata && { metadata }) };
|
|
@@ -430,7 +444,7 @@ function toInputContent(
|
|
|
430
444
|
if (data === undefined || format === undefined) return null;
|
|
431
445
|
return {
|
|
432
446
|
type: "audio",
|
|
433
|
-
source:
|
|
447
|
+
source: buildPartSource(
|
|
434
448
|
parseDataUrl(data)?.data ?? data,
|
|
435
449
|
`audio/${format}`,
|
|
436
450
|
),
|
|
@@ -446,8 +460,7 @@ type SnapshotAttachment = NonNullable<
|
|
|
446
460
|
|
|
447
461
|
const mediaInputTypes = new Set(["image", "audio", "video", "document"]);
|
|
448
462
|
|
|
449
|
-
|
|
450
|
-
function inputSourceToString(
|
|
463
|
+
function partSourceToString(
|
|
451
464
|
value: unknown,
|
|
452
465
|
): { value: string; mimeType?: string; isUrl?: boolean } | null {
|
|
453
466
|
if (!isObject(value)) return null;
|
|
@@ -494,23 +507,73 @@ function upgradeBinaryInputPart(
|
|
|
494
507
|
};
|
|
495
508
|
}
|
|
496
509
|
|
|
510
|
+
function normalizeInputPart(
|
|
511
|
+
rawPart: Record<string, unknown>,
|
|
512
|
+
): Record<string, unknown> | null {
|
|
513
|
+
return getString(rawPart, "type") === "binary"
|
|
514
|
+
? upgradeBinaryInputPart(rawPart)
|
|
515
|
+
: rawPart;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
type ContentSlot<T> = { index: number } & T;
|
|
519
|
+
|
|
520
|
+
// Text imports as one string and encoded media as attachments, so export
|
|
521
|
+
// needs the wire layout to restore the original order: each file source part
|
|
522
|
+
// keeps its rank among media, and each text part its position among all kept
|
|
523
|
+
// parts unless the text is one non-empty leading part.
|
|
524
|
+
function toUserContentLayout(content: unknown): {
|
|
525
|
+
fileSourceParts: ContentSlot<{ part: Record<string, unknown> }>[];
|
|
526
|
+
textParts: ContentSlot<{ text: string }>[];
|
|
527
|
+
} {
|
|
528
|
+
const fileSourceParts: ContentSlot<{ part: Record<string, unknown> }>[] = [];
|
|
529
|
+
const textParts: ContentSlot<{ text: string }>[] = [];
|
|
530
|
+
if (!Array.isArray(content)) return { fileSourceParts, textParts };
|
|
531
|
+
|
|
532
|
+
let mediaIndex = 0;
|
|
533
|
+
for (const rawPart of content) {
|
|
534
|
+
if (!isObject(rawPart)) continue;
|
|
535
|
+
if (rawPart.type === "text" && typeof rawPart.text === "string") {
|
|
536
|
+
textParts.push({
|
|
537
|
+
index: textParts.length + mediaIndex,
|
|
538
|
+
text: rawPart.text,
|
|
539
|
+
});
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
const part = normalizeInputPart(rawPart);
|
|
543
|
+
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++;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const leadingText =
|
|
556
|
+
textParts.length === 0 ||
|
|
557
|
+
(textParts.length === 1 &&
|
|
558
|
+
textParts[0]!.index === 0 &&
|
|
559
|
+
textParts[0]!.text !== "");
|
|
560
|
+
return { fileSourceParts, textParts: leadingText ? [] : textParts };
|
|
561
|
+
}
|
|
562
|
+
|
|
497
563
|
function toSnapshotAttachments(content: unknown): SnapshotAttachment[] {
|
|
498
564
|
if (!Array.isArray(content)) return [];
|
|
499
565
|
|
|
500
566
|
const attachments: SnapshotAttachment[] = [];
|
|
501
567
|
for (const rawPart of content) {
|
|
502
568
|
if (!isObject(rawPart)) continue;
|
|
503
|
-
const part =
|
|
504
|
-
getString(rawPart, "type") === "binary"
|
|
505
|
-
? upgradeBinaryInputPart(rawPart)
|
|
506
|
-
: rawPart;
|
|
569
|
+
const part = normalizeInputPart(rawPart);
|
|
507
570
|
if (!part) continue;
|
|
508
571
|
const type = getString(part, "type");
|
|
509
572
|
if (type === undefined || !mediaInputTypes.has(type)) continue;
|
|
510
|
-
const source =
|
|
573
|
+
const source = partSourceToString(part.source);
|
|
511
574
|
if (!source) continue;
|
|
512
575
|
|
|
513
|
-
const { filename, providerMetadata } =
|
|
576
|
+
const { filename, providerMetadata } = readPartMetadata(part.metadata);
|
|
514
577
|
const id = attachments.length.toString();
|
|
515
578
|
|
|
516
579
|
if (type === "image") {
|
|
@@ -554,12 +617,26 @@ function toSnapshotAttachments(content: unknown): SnapshotAttachment[] {
|
|
|
554
617
|
return attachments;
|
|
555
618
|
}
|
|
556
619
|
|
|
557
|
-
function
|
|
620
|
+
function readContentSlots(
|
|
621
|
+
value: unknown,
|
|
622
|
+
): { index: number; entry: Record<string, unknown> }[] {
|
|
623
|
+
if (!Array.isArray(value)) return [];
|
|
624
|
+
return value.flatMap((entry) =>
|
|
625
|
+
isObject(entry) &&
|
|
626
|
+
typeof entry.index === "number" &&
|
|
627
|
+
Number.isInteger(entry.index) &&
|
|
628
|
+
entry.index >= 0
|
|
629
|
+
? [{ index: entry.index, entry }]
|
|
630
|
+
: [],
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function buildUserContent(message: ThreadMessageLike): string | ContentPart[] {
|
|
558
635
|
const contentParts = Array.isArray(message.content) ? message.content : [];
|
|
559
636
|
|
|
560
637
|
const attachments = message.attachments ?? [];
|
|
561
638
|
|
|
562
|
-
const converted:
|
|
639
|
+
const converted: ContentPart[] = [];
|
|
563
640
|
|
|
564
641
|
// Promote string-form content to a leading text part so it survives when
|
|
565
642
|
// non-text attachments are present (fromAgUiMessages emits string content).
|
|
@@ -568,7 +645,7 @@ function buildUserContent(message: ThreadMessageLike): string | InputContent[] {
|
|
|
568
645
|
}
|
|
569
646
|
|
|
570
647
|
for (const part of contentParts) {
|
|
571
|
-
const input =
|
|
648
|
+
const input = toContentPart(part, undefined);
|
|
572
649
|
if (input) converted.push(input);
|
|
573
650
|
}
|
|
574
651
|
for (const attachment of attachments) {
|
|
@@ -577,13 +654,53 @@ function buildUserContent(message: ThreadMessageLike): string | InputContent[] {
|
|
|
577
654
|
if (!Array.isArray(attachmentContent)) continue;
|
|
578
655
|
const fallbackMime = getString(attachment, "contentType");
|
|
579
656
|
for (const part of attachmentContent) {
|
|
580
|
-
const input =
|
|
657
|
+
const input = toContentPart(part, fallbackMime);
|
|
581
658
|
if (input) converted.push(input);
|
|
582
659
|
}
|
|
583
660
|
}
|
|
584
661
|
|
|
585
|
-
const
|
|
586
|
-
|
|
662
|
+
const namespaced = readNamespacedMetadata(message.metadata);
|
|
663
|
+
const textSlots = readContentSlots(namespaced.textParts).flatMap(
|
|
664
|
+
({ index, entry }) =>
|
|
665
|
+
typeof entry.text === "string"
|
|
666
|
+
? [{ index, part: { type: "text" as const, text: entry.text } }]
|
|
667
|
+
: [],
|
|
668
|
+
);
|
|
669
|
+
const mergedText = converted
|
|
670
|
+
.flatMap((part) => (part.type === "text" ? [part.text] : []))
|
|
671
|
+
.join("\n");
|
|
672
|
+
const restoresText =
|
|
673
|
+
textSlots.length > 0 &&
|
|
674
|
+
textSlots.map(({ part }) => part.text).join("\n") === mergedText;
|
|
675
|
+
const fileSourceSlots = readContentSlots(namespaced.fileSourceParts).flatMap(
|
|
676
|
+
({ index, entry: { part } }) =>
|
|
677
|
+
isObject(part) &&
|
|
678
|
+
mediaInputTypes.has(getString(part, "type") ?? "") &&
|
|
679
|
+
isObject(part.source) &&
|
|
680
|
+
part.source.type === "file" &&
|
|
681
|
+
typeof part.source.value === "string"
|
|
682
|
+
? [{ index, part: part as ContentPart }]
|
|
683
|
+
: [],
|
|
684
|
+
);
|
|
685
|
+
const ordered = restoresText
|
|
686
|
+
? converted.filter((part) => part.type !== "text")
|
|
687
|
+
: converted;
|
|
688
|
+
for (const { index, part } of fileSourceSlots.sort(
|
|
689
|
+
(a, b) => a.index - b.index,
|
|
690
|
+
)) {
|
|
691
|
+
const mediaPositions = ordered.flatMap((contentPart, position) =>
|
|
692
|
+
contentPart.type !== "text" ? [position] : [],
|
|
693
|
+
);
|
|
694
|
+
ordered.splice(mediaPositions[index] ?? ordered.length, 0, part);
|
|
695
|
+
}
|
|
696
|
+
if (restoresText) {
|
|
697
|
+
for (const { index, part } of textSlots.sort((a, b) => a.index - b.index)) {
|
|
698
|
+
ordered.splice(Math.min(index, ordered.length), 0, part);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
if (restoresText || ordered.some((part) => part.type !== "text"))
|
|
703
|
+
return ordered;
|
|
587
704
|
|
|
588
705
|
// All-text path: collapse to plain string. Join text parts collected from
|
|
589
706
|
// both content and attachments so attachment-sourced text is not dropped.
|
|
@@ -695,6 +812,17 @@ function readPersistedInterrupts(
|
|
|
695
812
|
return validateInterrupts(namespaced.interrupts);
|
|
696
813
|
}
|
|
697
814
|
|
|
815
|
+
function readPersistedPendingToolCallIds(
|
|
816
|
+
metadata: unknown,
|
|
817
|
+
): string[] | undefined {
|
|
818
|
+
const value = readNamespacedMetadata(metadata).pendingToolCallIds;
|
|
819
|
+
return Array.isArray(value) &&
|
|
820
|
+
value.length > 0 &&
|
|
821
|
+
value.every((id): id is string => typeof id === "string")
|
|
822
|
+
? value
|
|
823
|
+
: undefined;
|
|
824
|
+
}
|
|
825
|
+
|
|
698
826
|
function readOpaqueReasoning(metadata: unknown): AgUiOpaqueReasoning[] {
|
|
699
827
|
if (!isObject(metadata)) return [];
|
|
700
828
|
const custom = metadata.custom;
|
|
@@ -794,10 +922,15 @@ function toUserOrSystemSnapshotMessage(
|
|
|
794
922
|
rawMessage: Record<string, unknown>,
|
|
795
923
|
): CoreThreadMessageLike {
|
|
796
924
|
const messageName = getString(rawMessage, "name");
|
|
797
|
-
const
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
925
|
+
const { fileSourceParts, textParts } =
|
|
926
|
+
role === "user"
|
|
927
|
+
? toUserContentLayout(rawMessage.content)
|
|
928
|
+
: { fileSourceParts: [], textParts: [] };
|
|
929
|
+
const custom = toSnapshotCustom(rawMessage.metadata, {
|
|
930
|
+
...(role === "developer" ? { role: "developer" } : {}),
|
|
931
|
+
...(fileSourceParts.length > 0 ? { fileSourceParts } : {}),
|
|
932
|
+
...(textParts.length > 0 ? { textParts } : {}),
|
|
933
|
+
});
|
|
801
934
|
const attachments =
|
|
802
935
|
role === "user" ? toSnapshotAttachments(rawMessage.content) : [];
|
|
803
936
|
return {
|
|
@@ -1030,18 +1163,27 @@ export function fromAgUiMessages(
|
|
|
1030
1163
|
flushPendingReasoning();
|
|
1031
1164
|
const toolCallId = getToolCallId(rawMessage) ?? `tool-${generateId()}`;
|
|
1032
1165
|
const toolMessageId = getString(rawMessage, "id");
|
|
1033
|
-
const modelContent =
|
|
1034
|
-
|
|
1035
|
-
|
|
1166
|
+
const modelContent = Array.isArray(rawMessage.content)
|
|
1167
|
+
? toolContentText(rawMessage.content as ContentPart[])
|
|
1168
|
+
: extractText(rawMessage.content);
|
|
1169
|
+
const mcpResult = parseMcpToolCallResult(
|
|
1170
|
+
rawMessage,
|
|
1171
|
+
Array.isArray(rawMessage.content)
|
|
1172
|
+
? (rawMessage.content as ContentPart[])
|
|
1173
|
+
: modelContent,
|
|
1174
|
+
);
|
|
1175
|
+
const toolModelContent = mcpResult
|
|
1036
1176
|
? [{ type: "text" as const, text: modelContent }]
|
|
1037
1177
|
: undefined;
|
|
1038
1178
|
const result =
|
|
1039
1179
|
mcpResult ??
|
|
1040
|
-
(rawMessage.
|
|
1041
|
-
? rawMessage.
|
|
1042
|
-
:
|
|
1043
|
-
?
|
|
1044
|
-
: rawMessage.content
|
|
1180
|
+
(Array.isArray(rawMessage.content)
|
|
1181
|
+
? rawMessage.content
|
|
1182
|
+
: rawMessage.result !== undefined
|
|
1183
|
+
? rawMessage.result
|
|
1184
|
+
: typeof rawMessage.content === "string"
|
|
1185
|
+
? parseJSONText(rawMessage.content)
|
|
1186
|
+
: rawMessage.content);
|
|
1045
1187
|
const isError =
|
|
1046
1188
|
typeof rawMessage.error === "string" ||
|
|
1047
1189
|
rawMessage.isError === true ||
|
|
@@ -1078,10 +1220,14 @@ export function fromAgUiMessages(
|
|
|
1078
1220
|
if (!isObject(part) || part.type !== "tool-call") continue;
|
|
1079
1221
|
if (getString(part, "toolCallId") !== toolCallId) continue;
|
|
1080
1222
|
|
|
1223
|
+
const { modelContent: _modelContent, ...partWithoutModelContent } =
|
|
1224
|
+
part as ToolCallPart;
|
|
1081
1225
|
const updatedPart: ToolCallPart = {
|
|
1082
|
-
...(
|
|
1226
|
+
...(Array.isArray(rawMessage.content) && !mcpResult
|
|
1227
|
+
? partWithoutModelContent
|
|
1228
|
+
: (part as ToolCallPart)),
|
|
1083
1229
|
result,
|
|
1084
|
-
...(
|
|
1230
|
+
...(toolModelContent ? { modelContent: toolModelContent } : {}),
|
|
1085
1231
|
...(isError !== undefined ? { isError } : {}),
|
|
1086
1232
|
...(toolMessageId !== undefined
|
|
1087
1233
|
? { unstable_toolMessageId: toolMessageId }
|
|
@@ -1125,7 +1271,7 @@ export function fromAgUiMessages(
|
|
|
1125
1271
|
args: {},
|
|
1126
1272
|
argsText: "{}",
|
|
1127
1273
|
result,
|
|
1128
|
-
...(
|
|
1274
|
+
...(toolModelContent ? { modelContent: toolModelContent } : {}),
|
|
1129
1275
|
...(isError !== undefined ? { isError } : {}),
|
|
1130
1276
|
...(toolMessageId !== undefined
|
|
1131
1277
|
? { unstable_toolMessageId: toolMessageId }
|
|
@@ -1345,13 +1491,18 @@ export function fromAgUiMessages(
|
|
|
1345
1491
|
|
|
1346
1492
|
const hasInterrupt =
|
|
1347
1493
|
readPersistedInterrupts(message.metadata) !== undefined;
|
|
1494
|
+
const pendingToolCallIds = readPersistedPendingToolCallIds(
|
|
1495
|
+
message.metadata,
|
|
1496
|
+
);
|
|
1348
1497
|
const hasPendingToolCall =
|
|
1349
1498
|
Array.isArray(message.content) &&
|
|
1350
1499
|
message.content.some(
|
|
1351
1500
|
(part) =>
|
|
1352
1501
|
isObject(part) &&
|
|
1353
1502
|
part.type === "tool-call" &&
|
|
1354
|
-
part.result === undefined
|
|
1503
|
+
part.result === undefined &&
|
|
1504
|
+
(pendingToolCallIds === undefined ||
|
|
1505
|
+
pendingToolCallIds.includes(getString(part, "toolCallId") ?? "")),
|
|
1355
1506
|
);
|
|
1356
1507
|
|
|
1357
1508
|
if (hasInterrupt || hasPendingToolCall) {
|
|
@@ -1478,6 +1629,22 @@ function convertAssistantMessage(
|
|
|
1478
1629
|
}
|
|
1479
1630
|
}
|
|
1480
1631
|
|
|
1632
|
+
function isToolContentParts(value: unknown): value is ContentPart[] {
|
|
1633
|
+
return (
|
|
1634
|
+
Array.isArray(value) &&
|
|
1635
|
+
value.length > 0 &&
|
|
1636
|
+
value.every(
|
|
1637
|
+
(part) =>
|
|
1638
|
+
isObject(part) &&
|
|
1639
|
+
(part.type === "text"
|
|
1640
|
+
? typeof part.text === "string"
|
|
1641
|
+
: mediaInputTypes.has(getString(part, "type") ?? "") &&
|
|
1642
|
+
isObject(part.source) &&
|
|
1643
|
+
typeof part.source.value === "string"),
|
|
1644
|
+
)
|
|
1645
|
+
);
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1481
1648
|
function emitToolResult(
|
|
1482
1649
|
toolCallId: string,
|
|
1483
1650
|
part: ToolCallPart,
|
|
@@ -1488,9 +1655,11 @@ function emitToolResult(
|
|
|
1488
1655
|
const resultContent =
|
|
1489
1656
|
part.modelContent !== undefined
|
|
1490
1657
|
? extractText(part.modelContent)
|
|
1491
|
-
:
|
|
1658
|
+
: isToolContentParts(part.result)
|
|
1492
1659
|
? part.result
|
|
1493
|
-
:
|
|
1660
|
+
: typeof part.result === "string"
|
|
1661
|
+
? part.result
|
|
1662
|
+
: JSON.stringify(part.result);
|
|
1494
1663
|
const messageMetadata = readAgUiPartMessageMetadata(part.providerMetadata);
|
|
1495
1664
|
|
|
1496
1665
|
converted.push({
|
|
@@ -1498,7 +1667,13 @@ function emitToolResult(
|
|
|
1498
1667
|
role: "tool",
|
|
1499
1668
|
content: resultContent,
|
|
1500
1669
|
toolCallId,
|
|
1501
|
-
...(part.isError
|
|
1670
|
+
...(part.isError
|
|
1671
|
+
? {
|
|
1672
|
+
error: Array.isArray(resultContent)
|
|
1673
|
+
? toolContentText(resultContent)
|
|
1674
|
+
: resultContent,
|
|
1675
|
+
}
|
|
1676
|
+
: {}),
|
|
1502
1677
|
...(messageMetadata ? { metadata: messageMetadata } : {}),
|
|
1503
1678
|
});
|
|
1504
1679
|
}
|
|
@@ -1507,7 +1682,9 @@ function convertToolMessage(
|
|
|
1507
1682
|
message: NormalizedThreadMessageLike,
|
|
1508
1683
|
converted: AgUiMessage[],
|
|
1509
1684
|
): void {
|
|
1510
|
-
const content =
|
|
1685
|
+
const content = isToolContentParts(message.content)
|
|
1686
|
+
? message.content
|
|
1687
|
+
: extractText(message.content);
|
|
1511
1688
|
const toolCallId = message.toolCallId ?? generateId();
|
|
1512
1689
|
|
|
1513
1690
|
converted.push({
|