@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.
- package/README.md +2 -0
- package/dist/agUiExtras.js +1 -2
- package/dist/hooks.js +1 -2
- package/dist/runtime/AgUiThreadController.d.ts +11 -0
- package/dist/runtime/AgUiThreadController.d.ts.map +1 -1
- package/dist/runtime/AgUiThreadController.js +565 -89
- package/dist/runtime/AgUiThreadController.js.map +1 -1
- package/dist/runtime/adapter/conversions.d.ts +9 -1
- package/dist/runtime/adapter/conversions.d.ts.map +1 -1
- package/dist/runtime/adapter/conversions.js +194 -12
- package/dist/runtime/adapter/conversions.js.map +1 -1
- package/dist/runtime/adapter/replay-content.d.ts +9 -0
- package/dist/runtime/adapter/replay-content.d.ts.map +1 -0
- package/dist/runtime/adapter/replay-content.js +185 -0
- package/dist/runtime/adapter/replay-content.js.map +1 -0
- package/dist/runtime/adapter/run-aggregator.d.ts +1 -0
- package/dist/runtime/adapter/run-aggregator.d.ts.map +1 -1
- package/dist/runtime/adapter/run-aggregator.js +35 -14
- package/dist/runtime/adapter/run-aggregator.js.map +1 -1
- package/dist/runtime/adapter/subscriber.d.ts +0 -1
- package/dist/runtime/adapter/subscriber.d.ts.map +1 -1
- package/dist/runtime/adapter/subscriber.js +2 -10
- package/dist/runtime/adapter/subscriber.js.map +1 -1
- package/dist/runtime/adapter/tool-approval.d.ts +2 -0
- package/dist/runtime/adapter/tool-approval.d.ts.map +1 -1
- package/dist/runtime/adapter/tool-approval.js +23 -6
- package/dist/runtime/adapter/tool-approval.js.map +1 -1
- package/dist/runtime/agUiThreadState.d.ts +1 -1
- package/dist/runtime/agUiThreadState.d.ts.map +1 -1
- package/dist/runtime/agUiThreadState.js +3 -4
- package/dist/runtime/agUiThreadState.js.map +1 -1
- package/dist/runtime/event-parser.d.ts.map +1 -1
- package/dist/runtime/event-parser.js +2 -2
- package/dist/runtime/event-parser.js.map +1 -1
- package/dist/runtime/interrupt-internals.js +1 -2
- package/dist/runtime/logger.js +1 -2
- package/dist/runtime/mcp-tool-result.js +1 -2
- package/dist/runtime/types.d.ts +2 -0
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/runtime/types.js +3 -3
- package/dist/runtime/types.js.map +1 -1
- package/dist/useAgUiRuntime.js +1 -2
- package/package.json +6 -6
- package/src/runtime/AgUiThreadController.deferred-results.test.ts +548 -0
- package/src/runtime/AgUiThreadController.disconnect.test.ts +914 -0
- package/src/runtime/AgUiThreadController.interrupt-history.test.ts +392 -0
- package/src/runtime/AgUiThreadController.replay.test.ts +3939 -0
- package/src/runtime/AgUiThreadController.snapshot-head-move.test.ts +167 -0
- package/src/runtime/AgUiThreadController.subagent-snapshot.test.ts +854 -0
- package/src/runtime/AgUiThreadController.test.ts +119 -0
- package/src/runtime/AgUiThreadController.ts +1184 -136
- package/src/runtime/adapter/conversions.content.test.ts +242 -1
- package/src/runtime/adapter/conversions.subagent-messages.test.ts +550 -0
- package/src/runtime/adapter/conversions.ts +349 -28
- package/src/runtime/adapter/replay-content.test.ts +235 -0
- package/src/runtime/adapter/replay-content.ts +306 -0
- package/src/runtime/adapter/run-aggregator.metadata.test.ts +151 -0
- package/src/runtime/adapter/run-aggregator.ts +52 -9
- package/src/runtime/adapter/subscriber.test.ts +1 -1
- package/src/runtime/adapter/subscriber.ts +1 -6
- package/src/runtime/adapter/tool-approval.ts +41 -3
- package/src/runtime/agUiThreadState.test.ts +21 -2
- package/src/runtime/agUiThreadState.ts +12 -3
- package/src/runtime/event-parser.ts +3 -0
- package/src/runtime/types.ts +5 -0
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { describe, expect, expectTypeOf, it } from "vitest";
|
|
2
2
|
import type { Message as AgUiWireMessage } from "@ag-ui/client";
|
|
3
3
|
import { MessageSchema, UserMessageSchema } from "@ag-ui/core/schemas";
|
|
4
|
-
import
|
|
4
|
+
import {
|
|
5
|
+
SimpleImageAttachmentAdapter,
|
|
6
|
+
type AppendMessage,
|
|
7
|
+
} from "@assistant-ui/core";
|
|
5
8
|
import {
|
|
6
9
|
fromAgUiMessages,
|
|
7
10
|
toAgUiMessages,
|
|
@@ -79,6 +82,27 @@ describe("toAgUiMessages data URLs", () => {
|
|
|
79
82
|
});
|
|
80
83
|
|
|
81
84
|
describe("toAgUiMessages content metadata", () => {
|
|
85
|
+
it("preserves an uploaded image's filename through a snapshot round trip", async () => {
|
|
86
|
+
const adapter = new SimpleImageAttachmentAdapter();
|
|
87
|
+
const pending = await adapter.add({
|
|
88
|
+
file: new File([new Uint8Array([137, 80, 78, 71])], "cat.png", {
|
|
89
|
+
type: "image/png",
|
|
90
|
+
}),
|
|
91
|
+
});
|
|
92
|
+
const attachment = await adapter.send(pending);
|
|
93
|
+
const sent = toAgUiMessages([userMessage([], [attachment])]);
|
|
94
|
+
const restored = fromAgUiMessages(sent);
|
|
95
|
+
|
|
96
|
+
expect(restored[0]?.attachments?.[0]?.name).toBe("cat.png");
|
|
97
|
+
expect(sent[0]?.content).toEqual([
|
|
98
|
+
{
|
|
99
|
+
type: "image",
|
|
100
|
+
source: { type: "data", value: "iVBORw==", mimeType: "image/png" },
|
|
101
|
+
metadata: { filename: "cat.png" },
|
|
102
|
+
},
|
|
103
|
+
]);
|
|
104
|
+
});
|
|
105
|
+
|
|
82
106
|
it.each(["image", "document"] as const)(
|
|
83
107
|
"preserves a %s file source and its metadata through a snapshot round trip",
|
|
84
108
|
(type) => {
|
|
@@ -114,6 +138,135 @@ describe("toAgUiMessages content metadata", () => {
|
|
|
114
138
|
},
|
|
115
139
|
);
|
|
116
140
|
|
|
141
|
+
it("restores each of two file sources that share a handle", () => {
|
|
142
|
+
const original = {
|
|
143
|
+
id: "u-shared",
|
|
144
|
+
role: "user",
|
|
145
|
+
content: [
|
|
146
|
+
{
|
|
147
|
+
type: "document",
|
|
148
|
+
source: {
|
|
149
|
+
type: "file",
|
|
150
|
+
value: "shared",
|
|
151
|
+
mimeType: "application/pdf",
|
|
152
|
+
},
|
|
153
|
+
metadata: { filename: "a.pdf" },
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
type: "document",
|
|
157
|
+
source: {
|
|
158
|
+
type: "file",
|
|
159
|
+
value: "shared",
|
|
160
|
+
provider: "openai",
|
|
161
|
+
mimeType: "application/pdf",
|
|
162
|
+
},
|
|
163
|
+
metadata: { filename: "b.pdf" },
|
|
164
|
+
},
|
|
165
|
+
],
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
expect(toAgUiMessages(fromAgUiMessages([original]))).toEqual([original]);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it.each(["image", "audio", "video"])(
|
|
172
|
+
"exports a %s file source without a mime type once",
|
|
173
|
+
(type) => {
|
|
174
|
+
const original = {
|
|
175
|
+
id: "u-untyped",
|
|
176
|
+
role: "user",
|
|
177
|
+
content: [{ type, source: { type: "file", value: `${type}-handle` } }],
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
expect(toAgUiMessages(fromAgUiMessages([original]))).toEqual([original]);
|
|
181
|
+
},
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
it("restores a file source a message carries only in its import metadata", () => {
|
|
185
|
+
const original = {
|
|
186
|
+
id: "u-legacy",
|
|
187
|
+
role: "user",
|
|
188
|
+
content: [
|
|
189
|
+
{ type: "text", text: "label" },
|
|
190
|
+
{
|
|
191
|
+
type: "image",
|
|
192
|
+
source: { type: "url", value: "https://example.com/other.png" },
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
type: "document",
|
|
196
|
+
source: { type: "file", value: "doc-handle", provider: "openai" },
|
|
197
|
+
},
|
|
198
|
+
],
|
|
199
|
+
};
|
|
200
|
+
const [imported] = fromAgUiMessages([original]);
|
|
201
|
+
const withoutHandles = {
|
|
202
|
+
...imported!,
|
|
203
|
+
attachments: imported!.attachments?.filter((attachment) =>
|
|
204
|
+
attachment.content?.every(
|
|
205
|
+
(part) => part.type !== "file" || part.sourceType !== "id",
|
|
206
|
+
),
|
|
207
|
+
),
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
expect(withoutHandles.attachments).toHaveLength(1);
|
|
211
|
+
expect(toAgUiMessages([withoutHandles])).toEqual([original]);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
describe.each(["content", "attachment"])("%s file handles", (location) => {
|
|
215
|
+
it.each([
|
|
216
|
+
["document", "application/pdf", "file-pdf"],
|
|
217
|
+
["image", "image/png", "https://provider.example/files/image"],
|
|
218
|
+
["audio", "audio/wav", "file-audio"],
|
|
219
|
+
["video", "video/mp4", "file-video"],
|
|
220
|
+
])(
|
|
221
|
+
"preserves a %s handle instead of sending bytes",
|
|
222
|
+
(type, mimeType, data) => {
|
|
223
|
+
const part = {
|
|
224
|
+
type: "file",
|
|
225
|
+
data,
|
|
226
|
+
mimeType,
|
|
227
|
+
sourceType: "id",
|
|
228
|
+
filename: "upload.bin",
|
|
229
|
+
providerMetadata: { agui: { trace: "kept" } },
|
|
230
|
+
};
|
|
231
|
+
const message =
|
|
232
|
+
location === "content"
|
|
233
|
+
? userMessage([part])
|
|
234
|
+
: userMessage([], [{ name: "upload.bin", content: [part] }]);
|
|
235
|
+
const exported = toAgUiMessages([message]);
|
|
236
|
+
const expected = {
|
|
237
|
+
id: "u1",
|
|
238
|
+
role: "user",
|
|
239
|
+
content: [
|
|
240
|
+
{
|
|
241
|
+
type,
|
|
242
|
+
source: { type: "file", value: data, mimeType },
|
|
243
|
+
metadata: { trace: "kept", filename: "upload.bin" },
|
|
244
|
+
},
|
|
245
|
+
],
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
const imported = fromAgUiMessages(exported);
|
|
249
|
+
|
|
250
|
+
expect(MessageSchema.parse(exported[0])).toEqual(expected);
|
|
251
|
+
expect(imported[0]?.attachments).toEqual([
|
|
252
|
+
expect.objectContaining({
|
|
253
|
+
content: [
|
|
254
|
+
{
|
|
255
|
+
type: "file",
|
|
256
|
+
data,
|
|
257
|
+
mimeType,
|
|
258
|
+
sourceType: "id",
|
|
259
|
+
filename: "upload.bin",
|
|
260
|
+
providerMetadata: { agui: { trace: "kept" } },
|
|
261
|
+
},
|
|
262
|
+
],
|
|
263
|
+
}),
|
|
264
|
+
]);
|
|
265
|
+
expect(toAgUiMessages(imported)).toEqual([expected]);
|
|
266
|
+
},
|
|
267
|
+
);
|
|
268
|
+
});
|
|
269
|
+
|
|
117
270
|
it("emits AgUiMessage values assignable to AG-UI Message", () => {
|
|
118
271
|
expectTypeOf<AgUiMessage>().toExtend<AgUiWireMessage>();
|
|
119
272
|
});
|
|
@@ -405,3 +558,91 @@ describe("toAgUiMessages percent-encoded data URLs", () => {
|
|
|
405
558
|
expect(toAgUiMessages(rebuilt as never)).toEqual(sent);
|
|
406
559
|
});
|
|
407
560
|
});
|
|
561
|
+
|
|
562
|
+
describe("toAgUiMessages tool model content", () => {
|
|
563
|
+
const withModelContent = (modelContent: unknown): Message => ({
|
|
564
|
+
id: "a1",
|
|
565
|
+
role: "assistant",
|
|
566
|
+
content: [
|
|
567
|
+
{
|
|
568
|
+
type: "tool-call",
|
|
569
|
+
toolCallId: "call-1",
|
|
570
|
+
toolName: "download",
|
|
571
|
+
args: {},
|
|
572
|
+
argsText: "{}",
|
|
573
|
+
result: { private: "not model-visible" },
|
|
574
|
+
modelContent,
|
|
575
|
+
},
|
|
576
|
+
],
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
it.each([
|
|
580
|
+
["image", "image/png"],
|
|
581
|
+
["document", "application/pdf"],
|
|
582
|
+
["audio", "audio/wav"],
|
|
583
|
+
["video", "video/mp4"],
|
|
584
|
+
])(
|
|
585
|
+
"keeps %s model content through a snapshot round trip",
|
|
586
|
+
(type, mediaType) => {
|
|
587
|
+
const exported = toAgUiMessages([
|
|
588
|
+
withModelContent([
|
|
589
|
+
{ type: "text", text: "Before" },
|
|
590
|
+
{ type: "file", data: "YWJj", mediaType, filename: "result.bin" },
|
|
591
|
+
{ type: "text", text: "After" },
|
|
592
|
+
]),
|
|
593
|
+
]);
|
|
594
|
+
const expected = {
|
|
595
|
+
id: "call-1:tool",
|
|
596
|
+
role: "tool",
|
|
597
|
+
toolCallId: "call-1",
|
|
598
|
+
content: [
|
|
599
|
+
{ type: "text", text: "Before" },
|
|
600
|
+
{
|
|
601
|
+
type,
|
|
602
|
+
source: { type: "data", value: "YWJj", mimeType: mediaType },
|
|
603
|
+
metadata: { filename: "result.bin" },
|
|
604
|
+
},
|
|
605
|
+
{ type: "text", text: "After" },
|
|
606
|
+
],
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
expect(MessageSchema.parse(exported[1])).toEqual(expected);
|
|
610
|
+
expect(toAgUiMessages(fromAgUiMessages(exported))[1]).toEqual(expected);
|
|
611
|
+
},
|
|
612
|
+
);
|
|
613
|
+
|
|
614
|
+
it("sends a file-only model result instead of an empty string", () => {
|
|
615
|
+
const exported = toAgUiMessages([
|
|
616
|
+
withModelContent([
|
|
617
|
+
{
|
|
618
|
+
type: "file",
|
|
619
|
+
data: "https://example.com/report.pdf",
|
|
620
|
+
mediaType: "application/pdf",
|
|
621
|
+
},
|
|
622
|
+
]),
|
|
623
|
+
]);
|
|
624
|
+
|
|
625
|
+
expect(exported[1]?.content).toEqual([
|
|
626
|
+
{
|
|
627
|
+
type: "document",
|
|
628
|
+
source: {
|
|
629
|
+
type: "url",
|
|
630
|
+
value: "https://example.com/report.pdf",
|
|
631
|
+
mimeType: "application/pdf",
|
|
632
|
+
},
|
|
633
|
+
},
|
|
634
|
+
]);
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
it.each([
|
|
638
|
+
{ modelContent: [], content: "" },
|
|
639
|
+
{ modelContent: [{ type: "text", text: "public" }], content: "public" },
|
|
640
|
+
])(
|
|
641
|
+
"keeps text-only model content as $content",
|
|
642
|
+
({ modelContent, content }) => {
|
|
643
|
+
expect(toAgUiMessages([withModelContent(modelContent)])[1]?.content).toBe(
|
|
644
|
+
content,
|
|
645
|
+
);
|
|
646
|
+
},
|
|
647
|
+
);
|
|
648
|
+
});
|