@assistant-ui/react-ag-ui 0.0.69 → 0.0.70

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.
@@ -0,0 +1,400 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { fromAgUiMessages, toAgUiMessages } from "./conversions";
3
+
4
+ describe("AG-UI message metadata", () => {
5
+ it("round-trips message metadata through AG-UI snapshots", () => {
6
+ const interrupts = [{ id: "int-1", reason: "confirmation" }];
7
+ const imported = fromAgUiMessages([
8
+ {
9
+ id: "u-1",
10
+ role: "user",
11
+ content: "Hello",
12
+ metadata: { record: "r-1" },
13
+ },
14
+ {
15
+ id: "a-1",
16
+ role: "assistant",
17
+ content: "Waiting",
18
+ metadata: { stopped: true, agui: { interrupts } },
19
+ },
20
+ {
21
+ id: "d-1",
22
+ role: "developer",
23
+ content: "Hidden",
24
+ metadata: { source: "app" },
25
+ },
26
+ ] as any);
27
+
28
+ expect(imported.map((message) => message.metadata?.custom)).toEqual([
29
+ { record: "r-1" },
30
+ {
31
+ stopped: true,
32
+ agui: { messageMetadata: { stopped: true, agui: { interrupts } } },
33
+ },
34
+ { source: "app", agui: { role: "developer" } },
35
+ ]);
36
+ expect(toAgUiMessages(imported)).toEqual([
37
+ {
38
+ id: "u-1",
39
+ role: "user",
40
+ content: "Hello",
41
+ metadata: { record: "r-1" },
42
+ },
43
+ {
44
+ id: "a-1",
45
+ role: "assistant",
46
+ content: "Waiting",
47
+ metadata: { stopped: true, agui: { interrupts } },
48
+ },
49
+ {
50
+ id: "d-1",
51
+ role: "developer",
52
+ content: "Hidden",
53
+ metadata: { source: "app" },
54
+ },
55
+ ]);
56
+ });
57
+
58
+ it("round-trips tool and reasoning message metadata on their parts", () => {
59
+ const imported = fromAgUiMessages([
60
+ {
61
+ id: "a-1",
62
+ role: "assistant",
63
+ content: "",
64
+ tool_calls: [
65
+ {
66
+ id: "call-1",
67
+ type: "function",
68
+ function: { name: "search", arguments: "{}" },
69
+ },
70
+ ],
71
+ },
72
+ {
73
+ id: "tool-1",
74
+ role: "tool",
75
+ tool_call_id: "call-1",
76
+ content: "result",
77
+ metadata: { source: "tool" },
78
+ },
79
+ {
80
+ id: "reasoning-1",
81
+ role: "reasoning",
82
+ content: "Thinking",
83
+ metadata: { source: "reasoning" },
84
+ },
85
+ { id: "a-2", role: "assistant", content: "Done" },
86
+ ] as any);
87
+
88
+ expect(toAgUiMessages(imported)).toMatchObject([
89
+ { id: "a-1", role: "assistant" },
90
+ {
91
+ id: "tool-1",
92
+ role: "tool",
93
+ metadata: { source: "tool" },
94
+ },
95
+ {
96
+ id: "reasoning-1",
97
+ role: "reasoning",
98
+ metadata: { source: "reasoning" },
99
+ },
100
+ { id: "a-2", role: "assistant" },
101
+ ]);
102
+ });
103
+
104
+ it("round-trips metadata on encrypted reasoning records", () => {
105
+ const imported = fromAgUiMessages([
106
+ {
107
+ id: "reasoning-1",
108
+ role: "reasoning",
109
+ content: "",
110
+ encryptedValue: "signed",
111
+ metadata: { source: "reasoning" },
112
+ },
113
+ { id: "a-1", role: "assistant", content: "Done" },
114
+ ] as any);
115
+
116
+ expect(toAgUiMessages(imported)).toEqual([
117
+ {
118
+ id: "reasoning-1",
119
+ role: "reasoning",
120
+ content: "",
121
+ encryptedValue: "signed",
122
+ metadata: { source: "reasoning" },
123
+ },
124
+ { id: "a-1", role: "assistant", content: "Done" },
125
+ ]);
126
+ });
127
+
128
+ it("preserves a wire agui key", () => {
129
+ const source = [
130
+ {
131
+ id: "u-1",
132
+ role: "user",
133
+ content: "Hello",
134
+ metadata: { agui: { app: 1 } },
135
+ },
136
+ ];
137
+ expect(toAgUiMessages(fromAgUiMessages(source))).toEqual(source);
138
+ });
139
+
140
+ it("preserves nested custom.agui wire metadata", () => {
141
+ const source = [
142
+ {
143
+ id: "u-1",
144
+ role: "user",
145
+ content: "Hello",
146
+ metadata: { custom: { agui: { app: 1 }, keep: 2 }, other: 3 },
147
+ },
148
+ ];
149
+ expect(toAgUiMessages(fromAgUiMessages(source))).toEqual(source);
150
+ });
151
+
152
+ it("decodes interrupts without echoing them in nested wire metadata", () => {
153
+ const interrupts = [{ id: "int-1", reason: "confirmation" }];
154
+ const source = [
155
+ {
156
+ id: "a-1",
157
+ role: "assistant",
158
+ content: "Waiting",
159
+ metadata: { custom: { agui: { interrupts } } },
160
+ },
161
+ {
162
+ id: "a-2",
163
+ role: "assistant",
164
+ content: "Waiting",
165
+ metadata: { custom: { agui: { interrupts, app: 1 } } },
166
+ },
167
+ {
168
+ id: "a-3",
169
+ role: "assistant",
170
+ content: "Waiting",
171
+ metadata: { custom: { agui: { interrupts }, keep: 2 } },
172
+ },
173
+ ];
174
+ const imported = fromAgUiMessages(source);
175
+
176
+ expect(imported.map((message) => message.metadata?.custom)).toEqual([
177
+ { agui: { interrupts } },
178
+ { custom: { agui: { app: 1 } }, agui: { interrupts } },
179
+ { custom: { keep: 2 }, agui: { interrupts } },
180
+ ]);
181
+ expect(toAgUiMessages(imported)).toEqual([
182
+ { id: "a-1", role: "assistant", content: "Waiting" },
183
+ {
184
+ id: "a-2",
185
+ role: "assistant",
186
+ content: "Waiting",
187
+ metadata: { custom: { agui: { app: 1 } } },
188
+ },
189
+ {
190
+ id: "a-3",
191
+ role: "assistant",
192
+ content: "Waiting",
193
+ metadata: { custom: { keep: 2 } },
194
+ },
195
+ ]);
196
+ });
197
+
198
+ it("removes interrupts from wire metadata when the top-level agui key collides", () => {
199
+ const interrupts = [{ id: "int-1", reason: "confirmation" }];
200
+ const source = [
201
+ {
202
+ id: "a-1",
203
+ role: "assistant",
204
+ content: "Waiting",
205
+ metadata: { agui: { source: "app" }, custom: { agui: { interrupts } } },
206
+ },
207
+ {
208
+ id: "a-2",
209
+ role: "assistant",
210
+ content: "Waiting",
211
+ metadata: {
212
+ agui: { source: "app" },
213
+ custom: { agui: { interrupts, app: 1 }, keep: 2 },
214
+ },
215
+ },
216
+ ];
217
+ const imported = fromAgUiMessages(source);
218
+
219
+ expect(imported.map((message) => message.metadata?.custom)).toEqual([
220
+ {
221
+ agui: {
222
+ interrupts,
223
+ messageMetadata: { agui: { source: "app" } },
224
+ },
225
+ },
226
+ {
227
+ custom: { agui: { app: 1 }, keep: 2 },
228
+ agui: {
229
+ interrupts,
230
+ messageMetadata: {
231
+ agui: { source: "app" },
232
+ custom: { agui: { app: 1 }, keep: 2 },
233
+ },
234
+ },
235
+ },
236
+ ]);
237
+ expect(toAgUiMessages(imported)).toEqual([
238
+ {
239
+ id: "a-1",
240
+ role: "assistant",
241
+ content: "Waiting",
242
+ metadata: { agui: { source: "app" } },
243
+ },
244
+ {
245
+ id: "a-2",
246
+ role: "assistant",
247
+ content: "Waiting",
248
+ metadata: {
249
+ agui: { source: "app" },
250
+ custom: { agui: { app: 1 }, keep: 2 },
251
+ },
252
+ },
253
+ ]);
254
+ });
255
+
256
+ it("drops an empty assistant record with wire metadata", () => {
257
+ const source = [
258
+ {
259
+ id: "a-1",
260
+ role: "assistant",
261
+ content: "",
262
+ metadata: { stopped: true },
263
+ },
264
+ ];
265
+ const imported = fromAgUiMessages(source);
266
+ expect(imported[0]?.metadata?.custom).toEqual({ stopped: true });
267
+ expect(toAgUiMessages(imported)).toEqual([]);
268
+ });
269
+
270
+ it("exports only reasoning records with distinct ids from a metadata-bearing assistant shell", () => {
271
+ expect(
272
+ toAgUiMessages([
273
+ {
274
+ id: "a-1",
275
+ role: "assistant",
276
+ content: [
277
+ { type: "reasoning", text: "Thinking" },
278
+ { type: "reasoning", text: "More thinking" },
279
+ ],
280
+ metadata: { custom: { stopped: true } },
281
+ },
282
+ ]),
283
+ ).toEqual([
284
+ {
285
+ id: "a-1",
286
+ role: "reasoning",
287
+ content: "Thinking",
288
+ },
289
+ { id: "a-1:reasoning-1", role: "reasoning", content: "More thinking" },
290
+ ]);
291
+ });
292
+
293
+ it("preserves colliding metadata across every wire role", () => {
294
+ const metadata = {
295
+ agui: { custom: { agui: [1, { custom: null }] } },
296
+ custom: { agui: { app: 1 }, keep: 2 },
297
+ nested: { custom: { agui: { value: true } } },
298
+ };
299
+ const source = [
300
+ { id: "u-1", role: "user", content: "Hi", metadata },
301
+ { id: "s-1", role: "system", content: "Be brief", metadata },
302
+ { id: "d-1", role: "developer", content: "Internal", metadata },
303
+ {
304
+ id: "a-1",
305
+ role: "assistant",
306
+ content: "",
307
+ toolCalls: [
308
+ {
309
+ id: "c-1",
310
+ type: "function",
311
+ function: { name: "search", arguments: "{}" },
312
+ },
313
+ ],
314
+ metadata,
315
+ },
316
+ { id: "t-1", role: "tool", toolCallId: "c-1", content: "done", metadata },
317
+ { id: "r-1", role: "reasoning", content: "Thinking", metadata },
318
+ { id: "a-2", role: "assistant", content: "Done", metadata },
319
+ ];
320
+ expect(toAgUiMessages(fromAgUiMessages(source))).toEqual(source);
321
+ });
322
+
323
+ it("preserves an explicit empty metadata object", () => {
324
+ const source = [
325
+ { id: "u-1", role: "user", content: "Hi", metadata: {} },
326
+ { id: "a-1", role: "assistant", content: "Done", metadata: {} },
327
+ ];
328
+ expect(toAgUiMessages(fromAgUiMessages(source))).toEqual(source);
329
+ });
330
+
331
+ it("keeps non-owned nested wire keys while consuming interrupts", () => {
332
+ const source = [
333
+ {
334
+ id: "s-1",
335
+ role: "system",
336
+ content: "Be brief",
337
+ metadata: { custom: { agui: { role: "developer" } } },
338
+ },
339
+ {
340
+ id: "a-1",
341
+ role: "assistant",
342
+ content: "Done",
343
+ metadata: {
344
+ custom: {
345
+ agui: {
346
+ role: "developer",
347
+ interrupts: [{ id: "app-1", reason: "app" }],
348
+ opaqueReasoning: [{ id: "app-2", encryptedValue: "app" }],
349
+ },
350
+ },
351
+ },
352
+ },
353
+ ];
354
+ expect(toAgUiMessages(fromAgUiMessages(source))).toEqual([
355
+ source[0],
356
+ {
357
+ id: "a-1",
358
+ role: "assistant",
359
+ content: "Done",
360
+ metadata: {
361
+ custom: {
362
+ agui: {
363
+ role: "developer",
364
+ opaqueReasoning: [{ id: "app-2", encryptedValue: "app" }],
365
+ },
366
+ },
367
+ },
368
+ },
369
+ ]);
370
+ });
371
+
372
+ it("decodes adapter state in the legacy ThreadMessage custom.agui slot", () => {
373
+ expect(
374
+ toAgUiMessages([
375
+ {
376
+ id: "s-1",
377
+ role: "system",
378
+ content: "Internal",
379
+ metadata: { custom: { agui: { role: "developer" } } },
380
+ },
381
+ {
382
+ id: "a-1",
383
+ role: "assistant",
384
+ content: "Done",
385
+ metadata: {
386
+ custom: {
387
+ agui: {
388
+ opaqueReasoning: [{ id: "r-1", encryptedValue: "signed" }],
389
+ },
390
+ },
391
+ },
392
+ },
393
+ ]),
394
+ ).toEqual([
395
+ { id: "s-1", role: "developer", content: "Internal" },
396
+ { id: "r-1", role: "reasoning", content: "", encryptedValue: "signed" },
397
+ { id: "a-1", role: "assistant", content: "Done" },
398
+ ]);
399
+ });
400
+ });