@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
@@ -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 type { AppendMessage } from "@assistant-ui/core";
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
+ });