@assistant-ui/eve 0.0.6 → 0.0.8
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/dist/convertEveMessages.d.ts +32 -3
- package/dist/convertEveMessages.d.ts.map +1 -1
- package/dist/convertEveMessages.js +87 -5
- package/dist/convertEveMessages.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/useEveAgentRuntime.d.ts.map +1 -1
- package/dist/useEveAgentRuntime.js +46 -14
- package/dist/useEveAgentRuntime.js.map +1 -1
- package/package.json +7 -3
- package/src/convertEveMessages.test.ts +1134 -12
- package/src/convertEveMessages.ts +174 -9
- package/src/index.ts +5 -1
- package/src/useEveAgentRuntime.test.tsx +408 -0
- package/src/useEveAgentRuntime.ts +55 -23
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
import type { EveMessageData } from "eve/react";
|
|
3
|
+
import { defaultMessageReducer, type EveAgentReducerEvent } from "eve/client";
|
|
3
4
|
import {
|
|
4
5
|
convertEveMessages,
|
|
5
6
|
getEveMessageContent,
|
|
@@ -195,7 +196,7 @@ describe("convertEveMessages", () => {
|
|
|
195
196
|
expect(message?.metadata).not.toHaveProperty("isOptimistic");
|
|
196
197
|
});
|
|
197
198
|
|
|
198
|
-
it("falls back to an empty text part for user messages
|
|
199
|
+
it("falls back to an empty text part for user messages with only url-less file parts", () => {
|
|
199
200
|
const data = {
|
|
200
201
|
messages: [
|
|
201
202
|
{
|
|
@@ -209,9 +210,10 @@ describe("convertEveMessages", () => {
|
|
|
209
210
|
const [message] = convertEveMessages(data);
|
|
210
211
|
|
|
211
212
|
expect(message?.content).toEqual([{ type: "text", text: "" }]);
|
|
213
|
+
expect(message?.attachments).toEqual([]);
|
|
212
214
|
});
|
|
213
215
|
|
|
214
|
-
it("drops
|
|
216
|
+
it("drops url-less user file parts without triggering the fallback", () => {
|
|
215
217
|
const data = {
|
|
216
218
|
messages: [
|
|
217
219
|
{
|
|
@@ -230,6 +232,253 @@ describe("convertEveMessages", () => {
|
|
|
230
232
|
expect(message?.content).toEqual([{ type: "text", text: "Hello" }]);
|
|
231
233
|
});
|
|
232
234
|
|
|
235
|
+
it("converts a user file part into content and a file attachment", () => {
|
|
236
|
+
const data = {
|
|
237
|
+
messages: [
|
|
238
|
+
{
|
|
239
|
+
id: "u1",
|
|
240
|
+
role: "user",
|
|
241
|
+
parts: [
|
|
242
|
+
{ type: "text", text: "See the report" },
|
|
243
|
+
{
|
|
244
|
+
type: "file",
|
|
245
|
+
url: "https://example.com/report.pdf",
|
|
246
|
+
mediaType: "application/pdf",
|
|
247
|
+
filename: "report.pdf",
|
|
248
|
+
size: 1024,
|
|
249
|
+
},
|
|
250
|
+
],
|
|
251
|
+
},
|
|
252
|
+
],
|
|
253
|
+
} satisfies EveMessageData;
|
|
254
|
+
|
|
255
|
+
const [message] = convertEveMessages(data);
|
|
256
|
+
|
|
257
|
+
expect(message?.content).toEqual([
|
|
258
|
+
{ type: "text", text: "See the report" },
|
|
259
|
+
{
|
|
260
|
+
type: "file",
|
|
261
|
+
data: "https://example.com/report.pdf",
|
|
262
|
+
mimeType: "application/pdf",
|
|
263
|
+
filename: "report.pdf",
|
|
264
|
+
sourceType: "url",
|
|
265
|
+
},
|
|
266
|
+
]);
|
|
267
|
+
expect(message?.attachments).toEqual([
|
|
268
|
+
{
|
|
269
|
+
id: "0",
|
|
270
|
+
type: "file",
|
|
271
|
+
name: "report.pdf",
|
|
272
|
+
content: [
|
|
273
|
+
{
|
|
274
|
+
type: "file",
|
|
275
|
+
data: "https://example.com/report.pdf",
|
|
276
|
+
mimeType: "application/pdf",
|
|
277
|
+
filename: "report.pdf",
|
|
278
|
+
sourceType: "url",
|
|
279
|
+
},
|
|
280
|
+
],
|
|
281
|
+
contentType: "application/pdf",
|
|
282
|
+
status: { type: "complete" },
|
|
283
|
+
},
|
|
284
|
+
]);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it("converts a user image file part into an image attachment", () => {
|
|
288
|
+
const data = {
|
|
289
|
+
messages: [
|
|
290
|
+
{
|
|
291
|
+
id: "u1",
|
|
292
|
+
role: "user",
|
|
293
|
+
parts: [
|
|
294
|
+
{
|
|
295
|
+
type: "file",
|
|
296
|
+
url: "https://example.com/photo.png",
|
|
297
|
+
mediaType: "image/png",
|
|
298
|
+
filename: "photo.png",
|
|
299
|
+
},
|
|
300
|
+
],
|
|
301
|
+
},
|
|
302
|
+
],
|
|
303
|
+
} satisfies EveMessageData;
|
|
304
|
+
|
|
305
|
+
const [message] = convertEveMessages(data);
|
|
306
|
+
|
|
307
|
+
expect(message?.content).toEqual([
|
|
308
|
+
{
|
|
309
|
+
type: "file",
|
|
310
|
+
data: "https://example.com/photo.png",
|
|
311
|
+
mimeType: "image/png",
|
|
312
|
+
filename: "photo.png",
|
|
313
|
+
sourceType: "url",
|
|
314
|
+
},
|
|
315
|
+
]);
|
|
316
|
+
expect(message?.attachments).toEqual([
|
|
317
|
+
{
|
|
318
|
+
id: "0",
|
|
319
|
+
type: "image",
|
|
320
|
+
name: "photo.png",
|
|
321
|
+
content: [
|
|
322
|
+
{
|
|
323
|
+
type: "image",
|
|
324
|
+
image: "https://example.com/photo.png",
|
|
325
|
+
filename: "photo.png",
|
|
326
|
+
},
|
|
327
|
+
],
|
|
328
|
+
contentType: "image/png",
|
|
329
|
+
status: { type: "complete" },
|
|
330
|
+
},
|
|
331
|
+
]);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it("omits sourceType and falls back to a generic name for data url file parts", () => {
|
|
335
|
+
const data = {
|
|
336
|
+
messages: [
|
|
337
|
+
{
|
|
338
|
+
id: "u1",
|
|
339
|
+
role: "user",
|
|
340
|
+
parts: [
|
|
341
|
+
{
|
|
342
|
+
type: "file",
|
|
343
|
+
url: "data:application/pdf;base64,QUJD",
|
|
344
|
+
mediaType: "application/pdf",
|
|
345
|
+
},
|
|
346
|
+
],
|
|
347
|
+
},
|
|
348
|
+
],
|
|
349
|
+
} satisfies EveMessageData;
|
|
350
|
+
|
|
351
|
+
const [message] = convertEveMessages(data);
|
|
352
|
+
|
|
353
|
+
expect(message?.content).toEqual([
|
|
354
|
+
{
|
|
355
|
+
type: "file",
|
|
356
|
+
data: "data:application/pdf;base64,QUJD",
|
|
357
|
+
mimeType: "application/pdf",
|
|
358
|
+
},
|
|
359
|
+
]);
|
|
360
|
+
expect(message?.attachments).toEqual([
|
|
361
|
+
{
|
|
362
|
+
id: "0",
|
|
363
|
+
type: "file",
|
|
364
|
+
name: "file",
|
|
365
|
+
content: [
|
|
366
|
+
{
|
|
367
|
+
type: "file",
|
|
368
|
+
data: "data:application/pdf;base64,QUJD",
|
|
369
|
+
mimeType: "application/pdf",
|
|
370
|
+
},
|
|
371
|
+
],
|
|
372
|
+
contentType: "application/pdf",
|
|
373
|
+
status: { type: "complete" },
|
|
374
|
+
},
|
|
375
|
+
]);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it("assigns sequential attachment ids across multiple file parts", () => {
|
|
379
|
+
const data = {
|
|
380
|
+
messages: [
|
|
381
|
+
{
|
|
382
|
+
id: "u1",
|
|
383
|
+
role: "user",
|
|
384
|
+
parts: [
|
|
385
|
+
{
|
|
386
|
+
type: "file",
|
|
387
|
+
url: "https://example.com/a.pdf",
|
|
388
|
+
mediaType: "application/pdf",
|
|
389
|
+
},
|
|
390
|
+
{ type: "file", mediaType: "image/png" },
|
|
391
|
+
{
|
|
392
|
+
type: "file",
|
|
393
|
+
url: "https://example.com/b.png",
|
|
394
|
+
mediaType: "image/png",
|
|
395
|
+
},
|
|
396
|
+
],
|
|
397
|
+
},
|
|
398
|
+
],
|
|
399
|
+
} satisfies EveMessageData;
|
|
400
|
+
|
|
401
|
+
const [message] = convertEveMessages(data);
|
|
402
|
+
|
|
403
|
+
expect(message?.attachments?.map((a) => [a.id, a.type])).toEqual([
|
|
404
|
+
["0", "file"],
|
|
405
|
+
["1", "image"],
|
|
406
|
+
]);
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it("defaults a file part with a missing mediaType to unknown/unknown", () => {
|
|
410
|
+
const data = {
|
|
411
|
+
messages: [
|
|
412
|
+
{
|
|
413
|
+
id: "u1",
|
|
414
|
+
role: "user",
|
|
415
|
+
parts: [{ type: "file", url: "https://example.com/blob" }],
|
|
416
|
+
},
|
|
417
|
+
],
|
|
418
|
+
} as unknown as EveMessageData;
|
|
419
|
+
|
|
420
|
+
const [message] = convertEveMessages(data);
|
|
421
|
+
|
|
422
|
+
expect(message?.content).toEqual([
|
|
423
|
+
{
|
|
424
|
+
type: "file",
|
|
425
|
+
data: "https://example.com/blob",
|
|
426
|
+
mimeType: "unknown/unknown",
|
|
427
|
+
sourceType: "url",
|
|
428
|
+
},
|
|
429
|
+
]);
|
|
430
|
+
expect(message?.attachments).toEqual([
|
|
431
|
+
{
|
|
432
|
+
id: "0",
|
|
433
|
+
type: "file",
|
|
434
|
+
name: "file",
|
|
435
|
+
content: [
|
|
436
|
+
{
|
|
437
|
+
type: "file",
|
|
438
|
+
data: "https://example.com/blob",
|
|
439
|
+
mimeType: "unknown/unknown",
|
|
440
|
+
sourceType: "url",
|
|
441
|
+
},
|
|
442
|
+
],
|
|
443
|
+
contentType: "unknown/unknown",
|
|
444
|
+
status: { type: "complete" },
|
|
445
|
+
},
|
|
446
|
+
]);
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it("converts an assistant file part into a file content part", () => {
|
|
450
|
+
const data = {
|
|
451
|
+
messages: [
|
|
452
|
+
{
|
|
453
|
+
id: "a1",
|
|
454
|
+
role: "assistant",
|
|
455
|
+
parts: [
|
|
456
|
+
{ type: "text", text: "Here you go" },
|
|
457
|
+
{
|
|
458
|
+
type: "file",
|
|
459
|
+
url: "https://example.com/result.csv",
|
|
460
|
+
mediaType: "text/csv",
|
|
461
|
+
filename: "result.csv",
|
|
462
|
+
},
|
|
463
|
+
],
|
|
464
|
+
},
|
|
465
|
+
],
|
|
466
|
+
} satisfies EveMessageData;
|
|
467
|
+
|
|
468
|
+
const [message] = convertEveMessages(data);
|
|
469
|
+
|
|
470
|
+
expect(message?.content).toEqual([
|
|
471
|
+
{ type: "text", text: "Here you go" },
|
|
472
|
+
{
|
|
473
|
+
type: "file",
|
|
474
|
+
data: "https://example.com/result.csv",
|
|
475
|
+
mimeType: "text/csv",
|
|
476
|
+
filename: "result.csv",
|
|
477
|
+
sourceType: "url",
|
|
478
|
+
},
|
|
479
|
+
]);
|
|
480
|
+
});
|
|
481
|
+
|
|
233
482
|
it("drops non-convertible part types instead of throwing", () => {
|
|
234
483
|
const data = {
|
|
235
484
|
messages: [
|
|
@@ -238,6 +487,26 @@ describe("convertEveMessages", () => {
|
|
|
238
487
|
role: "assistant",
|
|
239
488
|
parts: [
|
|
240
489
|
{ type: "step-start" },
|
|
490
|
+
{ type: "future-part", payload: {} },
|
|
491
|
+
{ type: "file", mediaType: "application/pdf" },
|
|
492
|
+
{ type: "text", text: "Done" },
|
|
493
|
+
],
|
|
494
|
+
},
|
|
495
|
+
],
|
|
496
|
+
} as unknown as EveMessageData;
|
|
497
|
+
|
|
498
|
+
const [message] = convertEveMessages(data);
|
|
499
|
+
|
|
500
|
+
expect(message?.content).toEqual([{ type: "text", text: "Done" }]);
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
it("converts an authorization part into a data content part", () => {
|
|
504
|
+
const data = {
|
|
505
|
+
messages: [
|
|
506
|
+
{
|
|
507
|
+
id: "a1",
|
|
508
|
+
role: "assistant",
|
|
509
|
+
parts: [
|
|
241
510
|
{
|
|
242
511
|
type: "authorization",
|
|
243
512
|
state: "required",
|
|
@@ -246,9 +515,14 @@ describe("convertEveMessages", () => {
|
|
|
246
515
|
displayName: "GitHub",
|
|
247
516
|
stepIndex: 0,
|
|
248
517
|
turnId: "turn_1",
|
|
518
|
+
authorization: {
|
|
519
|
+
displayName: "GitHub",
|
|
520
|
+
instructions: "Enter the code on the device page",
|
|
521
|
+
url: "https://github.com/login/device",
|
|
522
|
+
userCode: "ABCD-1234",
|
|
523
|
+
expiresAt: "2026-01-01T00:00:00Z",
|
|
524
|
+
},
|
|
249
525
|
},
|
|
250
|
-
{ type: "file", mediaType: "application/pdf" },
|
|
251
|
-
{ type: "text", text: "Done" },
|
|
252
526
|
],
|
|
253
527
|
},
|
|
254
528
|
],
|
|
@@ -256,7 +530,709 @@ describe("convertEveMessages", () => {
|
|
|
256
530
|
|
|
257
531
|
const [message] = convertEveMessages(data);
|
|
258
532
|
|
|
259
|
-
expect(message?.content).toEqual([
|
|
533
|
+
expect(message?.content).toEqual([
|
|
534
|
+
{
|
|
535
|
+
type: "data",
|
|
536
|
+
name: "authorization",
|
|
537
|
+
data: {
|
|
538
|
+
state: "required",
|
|
539
|
+
name: "github",
|
|
540
|
+
displayName: "GitHub",
|
|
541
|
+
description: "Sign in to GitHub",
|
|
542
|
+
url: "https://github.com/login/device",
|
|
543
|
+
userCode: "ABCD-1234",
|
|
544
|
+
instructions: "Enter the code on the device page",
|
|
545
|
+
expiresAt: "2026-01-01T00:00:00Z",
|
|
546
|
+
},
|
|
547
|
+
},
|
|
548
|
+
]);
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
it("converts an authorization part with missing optional fields", () => {
|
|
552
|
+
const data = {
|
|
553
|
+
messages: [
|
|
554
|
+
{
|
|
555
|
+
id: "a1",
|
|
556
|
+
role: "assistant",
|
|
557
|
+
parts: [
|
|
558
|
+
{
|
|
559
|
+
type: "authorization",
|
|
560
|
+
state: "required",
|
|
561
|
+
name: "github",
|
|
562
|
+
},
|
|
563
|
+
],
|
|
564
|
+
},
|
|
565
|
+
],
|
|
566
|
+
} as unknown as EveMessageData;
|
|
567
|
+
|
|
568
|
+
const [message] = convertEveMessages(data);
|
|
569
|
+
|
|
570
|
+
expect(message?.content).toEqual([
|
|
571
|
+
{
|
|
572
|
+
type: "data",
|
|
573
|
+
name: "authorization",
|
|
574
|
+
data: { state: "required", name: "github" },
|
|
575
|
+
},
|
|
576
|
+
]);
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
it("drops an authorization url that is not an http(s) address", () => {
|
|
580
|
+
const data = {
|
|
581
|
+
messages: [
|
|
582
|
+
{
|
|
583
|
+
id: "a1",
|
|
584
|
+
role: "assistant",
|
|
585
|
+
parts: [
|
|
586
|
+
{
|
|
587
|
+
type: "authorization",
|
|
588
|
+
state: "required",
|
|
589
|
+
name: "github",
|
|
590
|
+
authorization: {
|
|
591
|
+
url: "javascript:alert(1)",
|
|
592
|
+
userCode: "ABCD-1234",
|
|
593
|
+
},
|
|
594
|
+
},
|
|
595
|
+
],
|
|
596
|
+
},
|
|
597
|
+
],
|
|
598
|
+
} as unknown as EveMessageData;
|
|
599
|
+
|
|
600
|
+
const [message] = convertEveMessages(data);
|
|
601
|
+
|
|
602
|
+
expect(message?.content).toEqual([
|
|
603
|
+
{
|
|
604
|
+
type: "data",
|
|
605
|
+
name: "authorization",
|
|
606
|
+
data: { state: "required", name: "github", userCode: "ABCD-1234" },
|
|
607
|
+
},
|
|
608
|
+
]);
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
it("carries the outcome of a completed authorization part", () => {
|
|
612
|
+
const data = {
|
|
613
|
+
messages: [
|
|
614
|
+
{
|
|
615
|
+
id: "a1",
|
|
616
|
+
role: "assistant",
|
|
617
|
+
parts: [
|
|
618
|
+
{
|
|
619
|
+
type: "authorization",
|
|
620
|
+
state: "completed",
|
|
621
|
+
name: "github",
|
|
622
|
+
description: "Sign in to GitHub",
|
|
623
|
+
displayName: "GitHub",
|
|
624
|
+
stepIndex: 0,
|
|
625
|
+
turnId: "turn_1",
|
|
626
|
+
outcome: "authorized",
|
|
627
|
+
},
|
|
628
|
+
],
|
|
629
|
+
},
|
|
630
|
+
],
|
|
631
|
+
} satisfies EveMessageData;
|
|
632
|
+
|
|
633
|
+
const [message] = convertEveMessages(data);
|
|
634
|
+
|
|
635
|
+
expect(message?.content).toEqual([
|
|
636
|
+
{
|
|
637
|
+
type: "data",
|
|
638
|
+
name: "authorization",
|
|
639
|
+
data: {
|
|
640
|
+
state: "completed",
|
|
641
|
+
name: "github",
|
|
642
|
+
displayName: "GitHub",
|
|
643
|
+
description: "Sign in to GitHub",
|
|
644
|
+
outcome: "authorized",
|
|
645
|
+
},
|
|
646
|
+
},
|
|
647
|
+
]);
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
describe("assistant message status mapping", () => {
|
|
651
|
+
const withStatus = (
|
|
652
|
+
status: "streaming" | "complete" | "failed",
|
|
653
|
+
): EveMessageData => ({
|
|
654
|
+
messages: [
|
|
655
|
+
{
|
|
656
|
+
id: "a1",
|
|
657
|
+
role: "assistant",
|
|
658
|
+
metadata: { status },
|
|
659
|
+
parts: [{ type: "text", text: "Hi" }],
|
|
660
|
+
},
|
|
661
|
+
],
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
it("maps a running last message to running", () => {
|
|
665
|
+
const [message] = convertEveMessages(withStatus("streaming"), {
|
|
666
|
+
isRunning: true,
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
expect(message?.status).toEqual({ type: "running" });
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
it("keeps the legacy running mapping for a streaming marker when liveness is omitted", () => {
|
|
673
|
+
const [message] = convertEveMessages(withStatus("streaming"));
|
|
674
|
+
|
|
675
|
+
expect(message?.status).toEqual({ type: "running" });
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
it("maps a stale streaming marker to cancelled when no longer running", () => {
|
|
679
|
+
const [message] = convertEveMessages(withStatus("streaming"), {
|
|
680
|
+
isRunning: false,
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
expect(message?.status).toEqual({
|
|
684
|
+
type: "incomplete",
|
|
685
|
+
reason: "cancelled",
|
|
686
|
+
});
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
it("maps a stale streaming marker to error when the session error is set", () => {
|
|
690
|
+
const [message] = convertEveMessages(withStatus("streaming"), {
|
|
691
|
+
isRunning: false,
|
|
692
|
+
error: new Error("boom"),
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
expect(message?.status).toEqual({
|
|
696
|
+
type: "incomplete",
|
|
697
|
+
reason: "error",
|
|
698
|
+
error: { code: "unknown", message: "boom" },
|
|
699
|
+
});
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
it("maps a stuck streaming message to cancelled while a newer turn runs", () => {
|
|
703
|
+
const data = {
|
|
704
|
+
messages: [
|
|
705
|
+
{
|
|
706
|
+
id: "a1",
|
|
707
|
+
role: "assistant",
|
|
708
|
+
metadata: { status: "streaming" },
|
|
709
|
+
parts: [{ type: "text", text: "Interrupted" }],
|
|
710
|
+
},
|
|
711
|
+
{
|
|
712
|
+
id: "u2",
|
|
713
|
+
role: "user",
|
|
714
|
+
parts: [{ type: "text", text: "Try again" }],
|
|
715
|
+
},
|
|
716
|
+
],
|
|
717
|
+
} satisfies EveMessageData;
|
|
718
|
+
|
|
719
|
+
const [assistant] = convertEveMessages(data, {
|
|
720
|
+
isRunning: true,
|
|
721
|
+
error: new Error("boom"),
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
expect(assistant?.status).toEqual({
|
|
725
|
+
type: "incomplete",
|
|
726
|
+
reason: "cancelled",
|
|
727
|
+
});
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
it("keeps a completed message complete even when the session error is set", () => {
|
|
731
|
+
const [message] = convertEveMessages(withStatus("complete"), {
|
|
732
|
+
isRunning: false,
|
|
733
|
+
error: new Error("boom"),
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
expect(message?.status).toEqual({ type: "complete", reason: "stop" });
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
it("maps an assistant failed marker to an error status", () => {
|
|
740
|
+
const [message] = convertEveMessages(withStatus("failed"), {
|
|
741
|
+
isRunning: false,
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
expect(message?.status).toEqual({ type: "incomplete", reason: "error" });
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
it("keeps requires-action for pending approvals when not running", () => {
|
|
748
|
+
const data = {
|
|
749
|
+
messages: [
|
|
750
|
+
{
|
|
751
|
+
id: "a1",
|
|
752
|
+
role: "assistant",
|
|
753
|
+
metadata: { status: "streaming" },
|
|
754
|
+
parts: [
|
|
755
|
+
{
|
|
756
|
+
type: "dynamic-tool",
|
|
757
|
+
state: "approval-requested",
|
|
758
|
+
toolCallId: "call_1",
|
|
759
|
+
toolName: "send_email",
|
|
760
|
+
input: {},
|
|
761
|
+
approval: { id: "req_1" },
|
|
762
|
+
},
|
|
763
|
+
],
|
|
764
|
+
},
|
|
765
|
+
],
|
|
766
|
+
} satisfies EveMessageData;
|
|
767
|
+
|
|
768
|
+
const [message] = convertEveMessages(data, { isRunning: false });
|
|
769
|
+
|
|
770
|
+
expect(message?.status).toEqual({
|
|
771
|
+
type: "requires-action",
|
|
772
|
+
reason: "tool-calls",
|
|
773
|
+
});
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
it("maps an auth-suspended message to requires-action instead of cancelled", () => {
|
|
777
|
+
const data = {
|
|
778
|
+
messages: [
|
|
779
|
+
{
|
|
780
|
+
id: "a1",
|
|
781
|
+
role: "assistant",
|
|
782
|
+
metadata: { status: "streaming" },
|
|
783
|
+
parts: [
|
|
784
|
+
{
|
|
785
|
+
type: "authorization",
|
|
786
|
+
state: "required",
|
|
787
|
+
name: "github",
|
|
788
|
+
description: "Sign in to GitHub",
|
|
789
|
+
displayName: "GitHub",
|
|
790
|
+
stepIndex: 0,
|
|
791
|
+
turnId: "turn_1",
|
|
792
|
+
},
|
|
793
|
+
],
|
|
794
|
+
},
|
|
795
|
+
],
|
|
796
|
+
} satisfies EveMessageData;
|
|
797
|
+
|
|
798
|
+
const [message] = convertEveMessages(data, { isRunning: false });
|
|
799
|
+
|
|
800
|
+
expect(message?.status).toEqual({
|
|
801
|
+
type: "requires-action",
|
|
802
|
+
reason: "interrupt",
|
|
803
|
+
});
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
it("reports tool-calls when an approval and an authorization are both pending", () => {
|
|
807
|
+
const data = {
|
|
808
|
+
messages: [
|
|
809
|
+
{
|
|
810
|
+
id: "a1",
|
|
811
|
+
role: "assistant",
|
|
812
|
+
metadata: { status: "streaming" },
|
|
813
|
+
parts: [
|
|
814
|
+
{
|
|
815
|
+
type: "dynamic-tool",
|
|
816
|
+
state: "approval-requested",
|
|
817
|
+
toolCallId: "call_1",
|
|
818
|
+
toolName: "send_email",
|
|
819
|
+
input: {},
|
|
820
|
+
approval: { id: "req_1" },
|
|
821
|
+
},
|
|
822
|
+
{
|
|
823
|
+
type: "authorization",
|
|
824
|
+
state: "required",
|
|
825
|
+
name: "github",
|
|
826
|
+
description: "Sign in to GitHub",
|
|
827
|
+
displayName: "GitHub",
|
|
828
|
+
stepIndex: 0,
|
|
829
|
+
turnId: "turn_1",
|
|
830
|
+
},
|
|
831
|
+
],
|
|
832
|
+
},
|
|
833
|
+
],
|
|
834
|
+
} satisfies EveMessageData;
|
|
835
|
+
|
|
836
|
+
const [message] = convertEveMessages(data, { isRunning: false });
|
|
837
|
+
|
|
838
|
+
expect(message?.status).toEqual({
|
|
839
|
+
type: "requires-action",
|
|
840
|
+
reason: "tool-calls",
|
|
841
|
+
});
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
it("settles a cancelled turn with a pending authorization", () => {
|
|
845
|
+
const reducer = defaultMessageReducer();
|
|
846
|
+
const events: readonly EveAgentReducerEvent[] = [
|
|
847
|
+
{
|
|
848
|
+
type: "authorization.required",
|
|
849
|
+
data: {
|
|
850
|
+
turnId: "turn_1",
|
|
851
|
+
stepIndex: 0,
|
|
852
|
+
sequence: 0,
|
|
853
|
+
name: "github",
|
|
854
|
+
description: "Sign in to GitHub",
|
|
855
|
+
},
|
|
856
|
+
},
|
|
857
|
+
{
|
|
858
|
+
type: "turn.cancelled",
|
|
859
|
+
data: { turnId: "turn_1", sequence: 1 },
|
|
860
|
+
},
|
|
861
|
+
];
|
|
862
|
+
const pendingData = events
|
|
863
|
+
.slice(0, 1)
|
|
864
|
+
.reduce(
|
|
865
|
+
(state, event) => reducer.reduce(state, event),
|
|
866
|
+
reducer.initial(),
|
|
867
|
+
);
|
|
868
|
+
const pendingAssistant = pendingData.messages.find(
|
|
869
|
+
(message) => message.role === "assistant",
|
|
870
|
+
);
|
|
871
|
+
expect(pendingAssistant?.metadata?.status).toBe("streaming");
|
|
872
|
+
expect(
|
|
873
|
+
convertEveMessages(pendingData, { isRunning: false }).at(-1)?.status,
|
|
874
|
+
).toEqual({
|
|
875
|
+
type: "requires-action",
|
|
876
|
+
reason: "interrupt",
|
|
877
|
+
});
|
|
878
|
+
|
|
879
|
+
const data = events
|
|
880
|
+
.slice(1)
|
|
881
|
+
.reduce((state, event) => reducer.reduce(state, event), pendingData);
|
|
882
|
+
|
|
883
|
+
const [message] = convertEveMessages(data, { isRunning: false });
|
|
884
|
+
|
|
885
|
+
expect(message?.status).toEqual({
|
|
886
|
+
type: "complete",
|
|
887
|
+
reason: "stop",
|
|
888
|
+
});
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
it("reports a failed turn with a pending authorization", () => {
|
|
892
|
+
const data = {
|
|
893
|
+
messages: [
|
|
894
|
+
{
|
|
895
|
+
id: "a1",
|
|
896
|
+
role: "assistant",
|
|
897
|
+
metadata: { status: "streaming" },
|
|
898
|
+
parts: [
|
|
899
|
+
{
|
|
900
|
+
type: "authorization",
|
|
901
|
+
state: "required",
|
|
902
|
+
name: "github",
|
|
903
|
+
description: "Sign in to GitHub",
|
|
904
|
+
displayName: "GitHub",
|
|
905
|
+
stepIndex: 0,
|
|
906
|
+
turnId: "turn_1",
|
|
907
|
+
},
|
|
908
|
+
],
|
|
909
|
+
},
|
|
910
|
+
],
|
|
911
|
+
} satisfies EveMessageData;
|
|
912
|
+
|
|
913
|
+
const error = new Error("authorization failed");
|
|
914
|
+
const [message] = convertEveMessages(data, {
|
|
915
|
+
isRunning: false,
|
|
916
|
+
error,
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
expect(message?.status).toEqual({
|
|
920
|
+
type: "incomplete",
|
|
921
|
+
reason: "error",
|
|
922
|
+
error: {
|
|
923
|
+
code: "unknown",
|
|
924
|
+
message: "authorization failed",
|
|
925
|
+
},
|
|
926
|
+
});
|
|
927
|
+
});
|
|
928
|
+
|
|
929
|
+
it("keeps an earlier authorization pending when a later turn fails", () => {
|
|
930
|
+
const data = {
|
|
931
|
+
messages: [
|
|
932
|
+
{
|
|
933
|
+
id: "a1",
|
|
934
|
+
role: "assistant",
|
|
935
|
+
metadata: { status: "streaming" },
|
|
936
|
+
parts: [
|
|
937
|
+
{
|
|
938
|
+
type: "authorization",
|
|
939
|
+
state: "required",
|
|
940
|
+
name: "github",
|
|
941
|
+
description: "Sign in to GitHub",
|
|
942
|
+
displayName: "GitHub",
|
|
943
|
+
stepIndex: 0,
|
|
944
|
+
turnId: "turn_1",
|
|
945
|
+
},
|
|
946
|
+
],
|
|
947
|
+
},
|
|
948
|
+
{
|
|
949
|
+
id: "a2",
|
|
950
|
+
role: "assistant",
|
|
951
|
+
metadata: { status: "streaming" },
|
|
952
|
+
parts: [{ type: "text", text: "Later turn" }],
|
|
953
|
+
},
|
|
954
|
+
],
|
|
955
|
+
} satisfies EveMessageData;
|
|
956
|
+
|
|
957
|
+
const [authorization] = convertEveMessages(data, {
|
|
958
|
+
isRunning: false,
|
|
959
|
+
error: new Error("later turn failed"),
|
|
960
|
+
});
|
|
961
|
+
|
|
962
|
+
expect(authorization?.status).toEqual({
|
|
963
|
+
type: "requires-action",
|
|
964
|
+
reason: "interrupt",
|
|
965
|
+
});
|
|
966
|
+
});
|
|
967
|
+
|
|
968
|
+
it("does not hold requires-action for a completed authorization", () => {
|
|
969
|
+
const data = {
|
|
970
|
+
messages: [
|
|
971
|
+
{
|
|
972
|
+
id: "a1",
|
|
973
|
+
role: "assistant",
|
|
974
|
+
metadata: { status: "complete" },
|
|
975
|
+
parts: [
|
|
976
|
+
{
|
|
977
|
+
type: "authorization",
|
|
978
|
+
state: "completed",
|
|
979
|
+
name: "github",
|
|
980
|
+
description: "Sign in to GitHub",
|
|
981
|
+
displayName: "GitHub",
|
|
982
|
+
stepIndex: 0,
|
|
983
|
+
turnId: "turn_1",
|
|
984
|
+
outcome: "authorized",
|
|
985
|
+
},
|
|
986
|
+
{ type: "text", text: "Done" },
|
|
987
|
+
],
|
|
988
|
+
},
|
|
989
|
+
],
|
|
990
|
+
} satisfies EveMessageData;
|
|
991
|
+
|
|
992
|
+
const [message] = convertEveMessages(data, { isRunning: false });
|
|
993
|
+
|
|
994
|
+
expect(message?.status).toEqual({ type: "complete", reason: "stop" });
|
|
995
|
+
});
|
|
996
|
+
|
|
997
|
+
describe("contract with eve's default reducer", () => {
|
|
998
|
+
const replay = (events: readonly EveAgentReducerEvent[]) => {
|
|
999
|
+
const reducer = defaultMessageReducer();
|
|
1000
|
+
return events.reduce(
|
|
1001
|
+
(state, event) => reducer.reduce(state, event),
|
|
1002
|
+
reducer.initial(),
|
|
1003
|
+
);
|
|
1004
|
+
};
|
|
1005
|
+
|
|
1006
|
+
const midStreamEvents: readonly EveAgentReducerEvent[] = [
|
|
1007
|
+
{
|
|
1008
|
+
type: "client.message.submitted",
|
|
1009
|
+
data: { submissionId: "sub_1", message: "hi", createdAt: 0 },
|
|
1010
|
+
},
|
|
1011
|
+
{
|
|
1012
|
+
type: "turn.started",
|
|
1013
|
+
data: { turnId: "turn_1", sequence: 0 },
|
|
1014
|
+
},
|
|
1015
|
+
{
|
|
1016
|
+
type: "step.started",
|
|
1017
|
+
data: { turnId: "turn_1", stepIndex: 0, sequence: 1 },
|
|
1018
|
+
},
|
|
1019
|
+
{
|
|
1020
|
+
type: "message.appended",
|
|
1021
|
+
data: {
|
|
1022
|
+
turnId: "turn_1",
|
|
1023
|
+
stepIndex: 0,
|
|
1024
|
+
sequence: 2,
|
|
1025
|
+
messageDelta: "Let me th",
|
|
1026
|
+
messageSoFar: "Let me th",
|
|
1027
|
+
},
|
|
1028
|
+
},
|
|
1029
|
+
];
|
|
1030
|
+
|
|
1031
|
+
it("carries the challenge Eve projects onto an authorization part", () => {
|
|
1032
|
+
const state = replay([
|
|
1033
|
+
...midStreamEvents,
|
|
1034
|
+
{
|
|
1035
|
+
type: "authorization.required",
|
|
1036
|
+
data: {
|
|
1037
|
+
turnId: "turn_1",
|
|
1038
|
+
stepIndex: 0,
|
|
1039
|
+
sequence: 3,
|
|
1040
|
+
name: "github",
|
|
1041
|
+
description: "Authorization required for github",
|
|
1042
|
+
authorization: {
|
|
1043
|
+
displayName: "GitHub",
|
|
1044
|
+
instructions: "Enter the code on the device page",
|
|
1045
|
+
url: "https://github.com/login/device",
|
|
1046
|
+
userCode: "ABCD-1234",
|
|
1047
|
+
expiresAt: "2026-01-01T00:00:00Z",
|
|
1048
|
+
},
|
|
1049
|
+
},
|
|
1050
|
+
},
|
|
1051
|
+
]);
|
|
1052
|
+
|
|
1053
|
+
const converted = convertEveMessages(state, { isRunning: false });
|
|
1054
|
+
|
|
1055
|
+
expect(converted.at(-1)?.content).toContainEqual({
|
|
1056
|
+
type: "data",
|
|
1057
|
+
name: "authorization",
|
|
1058
|
+
data: {
|
|
1059
|
+
state: "required",
|
|
1060
|
+
name: "github",
|
|
1061
|
+
displayName: "GitHub",
|
|
1062
|
+
description: "Authorization required for GitHub",
|
|
1063
|
+
url: "https://github.com/login/device",
|
|
1064
|
+
userCode: "ABCD-1234",
|
|
1065
|
+
instructions: "Enter the code on the device page",
|
|
1066
|
+
expiresAt: "2026-01-01T00:00:00Z",
|
|
1067
|
+
},
|
|
1068
|
+
});
|
|
1069
|
+
});
|
|
1070
|
+
|
|
1071
|
+
it("releases the hold when Eve settles the authorization part in place", () => {
|
|
1072
|
+
const state = replay([
|
|
1073
|
+
...midStreamEvents,
|
|
1074
|
+
{
|
|
1075
|
+
type: "authorization.required",
|
|
1076
|
+
data: {
|
|
1077
|
+
turnId: "turn_1",
|
|
1078
|
+
stepIndex: 0,
|
|
1079
|
+
sequence: 3,
|
|
1080
|
+
name: "github",
|
|
1081
|
+
description: "Authorization required for github",
|
|
1082
|
+
},
|
|
1083
|
+
},
|
|
1084
|
+
{
|
|
1085
|
+
type: "authorization.completed",
|
|
1086
|
+
data: {
|
|
1087
|
+
turnId: "turn_1",
|
|
1088
|
+
stepIndex: 0,
|
|
1089
|
+
sequence: 4,
|
|
1090
|
+
name: "github",
|
|
1091
|
+
outcome: "authorized",
|
|
1092
|
+
},
|
|
1093
|
+
},
|
|
1094
|
+
]);
|
|
1095
|
+
|
|
1096
|
+
const assistant = state.messages.find((m) => m.role === "assistant");
|
|
1097
|
+
const authorizationParts = assistant?.parts.filter(
|
|
1098
|
+
(part) => part.type === "authorization",
|
|
1099
|
+
);
|
|
1100
|
+
expect(authorizationParts).toHaveLength(1);
|
|
1101
|
+
expect(authorizationParts?.[0]).toMatchObject({ state: "completed" });
|
|
1102
|
+
|
|
1103
|
+
// Eve leaves the streaming marker set, so the released turn falls back
|
|
1104
|
+
// to the stale-marker mapping rather than to a terminal status.
|
|
1105
|
+
expect(
|
|
1106
|
+
convertEveMessages(state, { isRunning: false }).at(-1)?.status,
|
|
1107
|
+
).toEqual({ type: "incomplete", reason: "cancelled" });
|
|
1108
|
+
});
|
|
1109
|
+
|
|
1110
|
+
it("keeps the hold while another connector is still required", () => {
|
|
1111
|
+
const state = replay([
|
|
1112
|
+
...midStreamEvents,
|
|
1113
|
+
{
|
|
1114
|
+
type: "authorization.required",
|
|
1115
|
+
data: {
|
|
1116
|
+
turnId: "turn_1",
|
|
1117
|
+
stepIndex: 0,
|
|
1118
|
+
sequence: 3,
|
|
1119
|
+
name: "github",
|
|
1120
|
+
description: "Authorization required for github",
|
|
1121
|
+
},
|
|
1122
|
+
},
|
|
1123
|
+
{
|
|
1124
|
+
type: "authorization.required",
|
|
1125
|
+
data: {
|
|
1126
|
+
turnId: "turn_1",
|
|
1127
|
+
stepIndex: 0,
|
|
1128
|
+
sequence: 4,
|
|
1129
|
+
name: "slack",
|
|
1130
|
+
description: "Authorization required for slack",
|
|
1131
|
+
},
|
|
1132
|
+
},
|
|
1133
|
+
{
|
|
1134
|
+
type: "authorization.completed",
|
|
1135
|
+
data: {
|
|
1136
|
+
turnId: "turn_1",
|
|
1137
|
+
stepIndex: 0,
|
|
1138
|
+
sequence: 5,
|
|
1139
|
+
name: "github",
|
|
1140
|
+
outcome: "authorized",
|
|
1141
|
+
},
|
|
1142
|
+
},
|
|
1143
|
+
]);
|
|
1144
|
+
|
|
1145
|
+
expect(
|
|
1146
|
+
convertEveMessages(state, { isRunning: false }).at(-1)?.status,
|
|
1147
|
+
).toEqual({ type: "requires-action", reason: "interrupt" });
|
|
1148
|
+
});
|
|
1149
|
+
|
|
1150
|
+
it("a locally aborted turn keeps its streaming marker and converts to cancelled", () => {
|
|
1151
|
+
const state = replay(midStreamEvents);
|
|
1152
|
+
|
|
1153
|
+
const assistant = state.messages.find((m) => m.role === "assistant");
|
|
1154
|
+
expect(assistant?.metadata?.status).toBe("streaming");
|
|
1155
|
+
|
|
1156
|
+
const converted = convertEveMessages(state, { isRunning: false });
|
|
1157
|
+
expect(converted.at(-1)?.status).toEqual({
|
|
1158
|
+
type: "incomplete",
|
|
1159
|
+
reason: "cancelled",
|
|
1160
|
+
});
|
|
1161
|
+
});
|
|
1162
|
+
|
|
1163
|
+
it("a failed session keeps its streaming marker and converts to error", () => {
|
|
1164
|
+
const state = replay([
|
|
1165
|
+
...midStreamEvents,
|
|
1166
|
+
{
|
|
1167
|
+
type: "session.failed",
|
|
1168
|
+
data: { sessionId: "session_1", code: "internal", message: "boom" },
|
|
1169
|
+
},
|
|
1170
|
+
]);
|
|
1171
|
+
|
|
1172
|
+
const assistant = state.messages.find((m) => m.role === "assistant");
|
|
1173
|
+
expect(assistant?.metadata?.status).toBe("streaming");
|
|
1174
|
+
|
|
1175
|
+
const converted = convertEveMessages(state, {
|
|
1176
|
+
isRunning: false,
|
|
1177
|
+
error: new Error("boom"),
|
|
1178
|
+
});
|
|
1179
|
+
expect(converted.at(-1)?.status).toEqual({
|
|
1180
|
+
type: "incomplete",
|
|
1181
|
+
reason: "error",
|
|
1182
|
+
error: { code: "unknown", message: "boom" },
|
|
1183
|
+
});
|
|
1184
|
+
});
|
|
1185
|
+
|
|
1186
|
+
it("a failed turn converts to cancelled because the store surfaces no error for turn.failed", () => {
|
|
1187
|
+
const state = replay([
|
|
1188
|
+
...midStreamEvents,
|
|
1189
|
+
{
|
|
1190
|
+
type: "turn.failed",
|
|
1191
|
+
data: {
|
|
1192
|
+
turnId: "turn_1",
|
|
1193
|
+
sequence: 3,
|
|
1194
|
+
code: "internal",
|
|
1195
|
+
message: "boom",
|
|
1196
|
+
},
|
|
1197
|
+
},
|
|
1198
|
+
]);
|
|
1199
|
+
|
|
1200
|
+
const converted = convertEveMessages(state, { isRunning: false });
|
|
1201
|
+
expect(converted.at(-1)?.status).toEqual({
|
|
1202
|
+
type: "incomplete",
|
|
1203
|
+
reason: "cancelled",
|
|
1204
|
+
});
|
|
1205
|
+
});
|
|
1206
|
+
|
|
1207
|
+
it("a completed turn terminalizes the streaming marker and converts to complete", () => {
|
|
1208
|
+
const state = replay([
|
|
1209
|
+
...midStreamEvents,
|
|
1210
|
+
{
|
|
1211
|
+
type: "message.completed",
|
|
1212
|
+
data: {
|
|
1213
|
+
turnId: "turn_1",
|
|
1214
|
+
stepIndex: 0,
|
|
1215
|
+
sequence: 3,
|
|
1216
|
+
finishReason: "stop",
|
|
1217
|
+
message: "Let me think",
|
|
1218
|
+
},
|
|
1219
|
+
},
|
|
1220
|
+
{
|
|
1221
|
+
type: "turn.completed",
|
|
1222
|
+
data: { turnId: "turn_1", sequence: 4 },
|
|
1223
|
+
},
|
|
1224
|
+
]);
|
|
1225
|
+
|
|
1226
|
+
const assistant = state.messages.find((m) => m.role === "assistant");
|
|
1227
|
+
expect(assistant?.metadata?.status).toBe("complete");
|
|
1228
|
+
|
|
1229
|
+
const converted = convertEveMessages(state, { isRunning: false });
|
|
1230
|
+
expect(converted.at(-1)?.status).toEqual({
|
|
1231
|
+
type: "complete",
|
|
1232
|
+
reason: "stop",
|
|
1233
|
+
});
|
|
1234
|
+
});
|
|
1235
|
+
});
|
|
260
1236
|
});
|
|
261
1237
|
|
|
262
1238
|
it("uses the supplied message creation time", () => {
|
|
@@ -280,20 +1256,166 @@ describe("convertEveMessages", () => {
|
|
|
280
1256
|
});
|
|
281
1257
|
|
|
282
1258
|
describe("getEveMessageContent", () => {
|
|
1259
|
+
const baseAppendMessage = {
|
|
1260
|
+
role: "user",
|
|
1261
|
+
createdAt: new Date(),
|
|
1262
|
+
parentId: null,
|
|
1263
|
+
sourceId: null,
|
|
1264
|
+
runConfig: undefined,
|
|
1265
|
+
metadata: { custom: {} },
|
|
1266
|
+
attachments: [],
|
|
1267
|
+
} as const;
|
|
1268
|
+
|
|
283
1269
|
it("returns plain text for text-only messages", () => {
|
|
284
1270
|
const message = {
|
|
285
|
-
|
|
286
|
-
createdAt: new Date(),
|
|
287
|
-
parentId: null,
|
|
288
|
-
sourceId: null,
|
|
289
|
-
runConfig: undefined,
|
|
1271
|
+
...baseAppendMessage,
|
|
290
1272
|
content: [{ type: "text", text: "Hello" }],
|
|
291
|
-
metadata: { custom: {} },
|
|
292
|
-
attachments: [],
|
|
293
1273
|
} satisfies AppendMessage;
|
|
294
1274
|
|
|
295
1275
|
expect(getEveMessageContent(message)).toBe("Hello");
|
|
296
1276
|
});
|
|
1277
|
+
|
|
1278
|
+
it("converts an audio part into a file part with the format-derived media type", () => {
|
|
1279
|
+
const message = {
|
|
1280
|
+
...baseAppendMessage,
|
|
1281
|
+
content: [{ type: "audio", audio: { data: "QUJD", format: "mp3" } }],
|
|
1282
|
+
} as unknown as AppendMessage;
|
|
1283
|
+
|
|
1284
|
+
expect(getEveMessageContent(message)).toEqual([
|
|
1285
|
+
{
|
|
1286
|
+
type: "file",
|
|
1287
|
+
data: "data:audio/mp3;base64,QUJD",
|
|
1288
|
+
mediaType: "audio/mp3",
|
|
1289
|
+
},
|
|
1290
|
+
]);
|
|
1291
|
+
});
|
|
1292
|
+
|
|
1293
|
+
it("converts a wav audio part", () => {
|
|
1294
|
+
const message = {
|
|
1295
|
+
...baseAppendMessage,
|
|
1296
|
+
content: [{ type: "audio", audio: { data: "QUJD", format: "wav" } }],
|
|
1297
|
+
} as unknown as AppendMessage;
|
|
1298
|
+
|
|
1299
|
+
expect(getEveMessageContent(message)).toEqual([
|
|
1300
|
+
{
|
|
1301
|
+
type: "file",
|
|
1302
|
+
data: "data:audio/wav;base64,QUJD",
|
|
1303
|
+
mediaType: "audio/wav",
|
|
1304
|
+
},
|
|
1305
|
+
]);
|
|
1306
|
+
});
|
|
1307
|
+
|
|
1308
|
+
it("rebuilds the audio data URL envelope from the typed format", () => {
|
|
1309
|
+
const message = {
|
|
1310
|
+
...baseAppendMessage,
|
|
1311
|
+
content: [
|
|
1312
|
+
{
|
|
1313
|
+
type: "audio",
|
|
1314
|
+
audio: { data: "data:audio/mpeg;base64,QUJD", format: "mp3" },
|
|
1315
|
+
},
|
|
1316
|
+
],
|
|
1317
|
+
} as unknown as AppendMessage;
|
|
1318
|
+
|
|
1319
|
+
expect(getEveMessageContent(message)).toEqual([
|
|
1320
|
+
{
|
|
1321
|
+
type: "file",
|
|
1322
|
+
data: "data:audio/mp3;base64,QUJD",
|
|
1323
|
+
mediaType: "audio/mp3",
|
|
1324
|
+
},
|
|
1325
|
+
]);
|
|
1326
|
+
});
|
|
1327
|
+
|
|
1328
|
+
it("forwards an http audio source instead of wrapping it in a data URL", () => {
|
|
1329
|
+
const message = {
|
|
1330
|
+
...baseAppendMessage,
|
|
1331
|
+
content: [
|
|
1332
|
+
{
|
|
1333
|
+
type: "audio",
|
|
1334
|
+
audio: { data: "https://cdn.example.com/memo.mp3", format: "mp3" },
|
|
1335
|
+
},
|
|
1336
|
+
],
|
|
1337
|
+
} as unknown as AppendMessage;
|
|
1338
|
+
|
|
1339
|
+
expect(getEveMessageContent(message)).toEqual([
|
|
1340
|
+
{
|
|
1341
|
+
type: "file",
|
|
1342
|
+
data: "https://cdn.example.com/memo.mp3",
|
|
1343
|
+
mediaType: "audio/mp3",
|
|
1344
|
+
},
|
|
1345
|
+
]);
|
|
1346
|
+
});
|
|
1347
|
+
|
|
1348
|
+
it("round-trips a sent file attachment through the eve echo shape", () => {
|
|
1349
|
+
const message = {
|
|
1350
|
+
...baseAppendMessage,
|
|
1351
|
+
content: [],
|
|
1352
|
+
attachments: [
|
|
1353
|
+
{
|
|
1354
|
+
id: "1",
|
|
1355
|
+
type: "file",
|
|
1356
|
+
name: "report.pdf",
|
|
1357
|
+
content: [
|
|
1358
|
+
{
|
|
1359
|
+
type: "file",
|
|
1360
|
+
data: "https://example.com/report.pdf",
|
|
1361
|
+
mimeType: "application/pdf",
|
|
1362
|
+
filename: "report.pdf",
|
|
1363
|
+
},
|
|
1364
|
+
],
|
|
1365
|
+
status: { type: "complete" },
|
|
1366
|
+
},
|
|
1367
|
+
],
|
|
1368
|
+
} as unknown as AppendMessage;
|
|
1369
|
+
|
|
1370
|
+
expect(getEveMessageContent(message)).toEqual([
|
|
1371
|
+
{
|
|
1372
|
+
type: "file",
|
|
1373
|
+
data: "https://example.com/report.pdf",
|
|
1374
|
+
mediaType: "application/pdf",
|
|
1375
|
+
filename: "report.pdf",
|
|
1376
|
+
},
|
|
1377
|
+
]);
|
|
1378
|
+
|
|
1379
|
+
const [echoed] = convertEveMessages({
|
|
1380
|
+
messages: [
|
|
1381
|
+
{
|
|
1382
|
+
id: "t1:user",
|
|
1383
|
+
role: "user",
|
|
1384
|
+
metadata: { status: "complete", turnId: "t1" },
|
|
1385
|
+
parts: [
|
|
1386
|
+
{
|
|
1387
|
+
type: "file",
|
|
1388
|
+
url: "https://example.com/report.pdf",
|
|
1389
|
+
mediaType: "application/pdf",
|
|
1390
|
+
filename: "report.pdf",
|
|
1391
|
+
},
|
|
1392
|
+
],
|
|
1393
|
+
},
|
|
1394
|
+
],
|
|
1395
|
+
} satisfies EveMessageData);
|
|
1396
|
+
|
|
1397
|
+
expect(echoed?.content).toEqual([
|
|
1398
|
+
{
|
|
1399
|
+
type: "file",
|
|
1400
|
+
data: "https://example.com/report.pdf",
|
|
1401
|
+
mimeType: "application/pdf",
|
|
1402
|
+
filename: "report.pdf",
|
|
1403
|
+
sourceType: "url",
|
|
1404
|
+
},
|
|
1405
|
+
]);
|
|
1406
|
+
});
|
|
1407
|
+
|
|
1408
|
+
it("skips data parts while keeping surrounding text", () => {
|
|
1409
|
+
const message = {
|
|
1410
|
+
...baseAppendMessage,
|
|
1411
|
+
content: [
|
|
1412
|
+
{ type: "text", text: "hi" },
|
|
1413
|
+
{ type: "data", name: "chart", data: { x: 1 } },
|
|
1414
|
+
],
|
|
1415
|
+
} as unknown as AppendMessage;
|
|
1416
|
+
|
|
1417
|
+
expect(getEveMessageContent(message)).toBe("hi");
|
|
1418
|
+
});
|
|
297
1419
|
});
|
|
298
1420
|
|
|
299
1421
|
describe("toEveInputResponse", () => {
|