@assistant-ui/react-google-adk 0.0.4 → 0.0.6
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/AdkEventAccumulator.d.ts.map +1 -1
- package/dist/AdkEventAccumulator.js +48 -4
- package/dist/AdkEventAccumulator.js.map +1 -1
- package/dist/hooks.d.ts +4 -1
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +18 -0
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/useAdkMessages.d.ts +1 -0
- package/dist/useAdkMessages.d.ts.map +1 -1
- package/dist/useAdkMessages.js +13 -0
- package/dist/useAdkMessages.js.map +1 -1
- package/dist/useAdkRuntime.d.ts.map +1 -1
- package/dist/useAdkRuntime.js +30 -14
- package/dist/useAdkRuntime.js.map +1 -1
- package/package.json +7 -7
- package/src/AdkEventAccumulator.test.ts +521 -2
- package/src/AdkEventAccumulator.ts +49 -4
- package/src/hooks.ts +28 -1
- package/src/index.ts +1 -0
- package/src/useAdkMessages.ts +17 -0
- package/src/useAdkRuntime.test.ts +138 -0
- package/src/useAdkRuntime.ts +37 -17
|
@@ -399,7 +399,7 @@ describe("AdkEventAccumulator - isFinalResponse logic", () => {
|
|
|
399
399
|
});
|
|
400
400
|
});
|
|
401
401
|
|
|
402
|
-
it("
|
|
402
|
+
it("HITL event leaves status undefined for auto-status", () => {
|
|
403
403
|
const acc = new AdkEventAccumulator();
|
|
404
404
|
const msgs = acc.processEvent(
|
|
405
405
|
makeEvent({
|
|
@@ -413,10 +413,283 @@ describe("AdkEventAccumulator - isFinalResponse logic", () => {
|
|
|
413
413
|
},
|
|
414
414
|
}),
|
|
415
415
|
);
|
|
416
|
+
expect((msgs[0] as AdkMessage & { type: "ai" }).status).toBeUndefined();
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
describe("AdkEventAccumulator - HITL requires-action", () => {
|
|
421
|
+
const makeHitlEvent = (
|
|
422
|
+
toolCallId: string,
|
|
423
|
+
overrides: Partial<AdkEvent> = {},
|
|
424
|
+
): AdkEvent =>
|
|
425
|
+
makeEvent({
|
|
426
|
+
author: "agent",
|
|
427
|
+
longRunningToolIds: [toolCallId],
|
|
428
|
+
content: {
|
|
429
|
+
parts: [
|
|
430
|
+
{
|
|
431
|
+
functionCall: {
|
|
432
|
+
id: toolCallId,
|
|
433
|
+
name: "adk_request_input",
|
|
434
|
+
args: { message: "What's the forecast period?" },
|
|
435
|
+
},
|
|
436
|
+
},
|
|
437
|
+
],
|
|
438
|
+
},
|
|
439
|
+
...overrides,
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
it("produces an AI message with tool call and no manual status", () => {
|
|
443
|
+
const acc = new AdkEventAccumulator();
|
|
444
|
+
const msgs = acc.processEvent(makeHitlEvent("tc-1"));
|
|
445
|
+
|
|
446
|
+
expect(msgs).toHaveLength(1);
|
|
447
|
+
const aiMsg = msgs[0] as AdkMessage & { type: "ai" };
|
|
448
|
+
expect(aiMsg.type).toBe("ai");
|
|
449
|
+
expect(aiMsg.tool_calls).toHaveLength(1);
|
|
450
|
+
expect(aiMsg.tool_calls![0]!.id).toBe("tc-1");
|
|
451
|
+
expect(aiMsg.tool_calls![0]!.name).toBe("adk_request_input");
|
|
452
|
+
expect(aiMsg.status).toBeUndefined();
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
it("matches tool call id to the longRunningToolIds entry", () => {
|
|
456
|
+
const acc = new AdkEventAccumulator();
|
|
457
|
+
const msgs = acc.processEvent(makeHitlEvent("tc-1"));
|
|
458
|
+
const aiMsg = msgs[0] as AdkMessage & { type: "ai" };
|
|
459
|
+
expect(aiMsg.tool_calls![0]!.id).toBe("tc-1");
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
it("stays pending across a subsequent bookkeeping event", () => {
|
|
463
|
+
const acc = new AdkEventAccumulator();
|
|
464
|
+
acc.processEvent(makeHitlEvent("tc-1", { author: "WorkflowA" }));
|
|
465
|
+
|
|
466
|
+
const msgs = acc.processEvent(
|
|
467
|
+
makeEvent({
|
|
468
|
+
id: "evt-bookkeeping",
|
|
469
|
+
author: "WorkflowA",
|
|
470
|
+
actions: { stateDelta: { waiting_for_user: true } },
|
|
471
|
+
}),
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
const aiMsg = msgs.find(
|
|
475
|
+
(m): m is AdkMessage & { type: "ai" } =>
|
|
476
|
+
m.type === "ai" && (m.tool_calls?.length ?? 0) > 0,
|
|
477
|
+
);
|
|
478
|
+
expect(aiMsg).toBeDefined();
|
|
479
|
+
expect(aiMsg!.tool_calls![0]!.id).toBe("tc-1");
|
|
480
|
+
expect(aiMsg!.status).toBeUndefined();
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
it("assigns manual complete status on non-HITL final event", () => {
|
|
484
|
+
const acc = new AdkEventAccumulator();
|
|
485
|
+
const msgs = acc.processEvent(
|
|
486
|
+
makeEvent({
|
|
487
|
+
author: "agent",
|
|
488
|
+
content: { role: "model", parts: [{ text: "Done." }] },
|
|
489
|
+
}),
|
|
490
|
+
);
|
|
491
|
+
|
|
416
492
|
expect(msgs[0]).toMatchObject({
|
|
417
493
|
status: { type: "complete", reason: "stop" },
|
|
418
494
|
});
|
|
419
495
|
});
|
|
496
|
+
|
|
497
|
+
it("assigns manual complete status on skipSummarization final event", () => {
|
|
498
|
+
const acc = new AdkEventAccumulator();
|
|
499
|
+
const msgs = acc.processEvent(
|
|
500
|
+
makeEvent({
|
|
501
|
+
author: "agent",
|
|
502
|
+
actions: { skipSummarization: true },
|
|
503
|
+
content: { role: "model", parts: [{ text: "skipped" }] },
|
|
504
|
+
}),
|
|
505
|
+
);
|
|
506
|
+
|
|
507
|
+
expect(msgs[0]).toMatchObject({
|
|
508
|
+
status: { type: "complete", reason: "stop" },
|
|
509
|
+
});
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
it("prioritizes skipSummarization over longRunningToolIds", () => {
|
|
513
|
+
const acc = new AdkEventAccumulator();
|
|
514
|
+
const msgs = acc.processEvent(
|
|
515
|
+
makeEvent({
|
|
516
|
+
author: "agent",
|
|
517
|
+
longRunningToolIds: ["lrt-both"],
|
|
518
|
+
actions: { skipSummarization: true },
|
|
519
|
+
content: {
|
|
520
|
+
role: "model",
|
|
521
|
+
parts: [
|
|
522
|
+
{ functionCall: { name: "slow_tool", id: "lrt-both", args: {} } },
|
|
523
|
+
],
|
|
524
|
+
},
|
|
525
|
+
}),
|
|
526
|
+
);
|
|
527
|
+
|
|
528
|
+
expect(msgs[0]).toMatchObject({
|
|
529
|
+
status: { type: "complete", reason: "stop" },
|
|
530
|
+
});
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
it("ignores empty longRunningToolIds array for HITL guard", () => {
|
|
534
|
+
const acc = new AdkEventAccumulator();
|
|
535
|
+
const msgs = acc.processEvent(
|
|
536
|
+
makeEvent({
|
|
537
|
+
author: "agent",
|
|
538
|
+
longRunningToolIds: [],
|
|
539
|
+
content: { role: "model", parts: [{ text: "Done." }] },
|
|
540
|
+
}),
|
|
541
|
+
);
|
|
542
|
+
|
|
543
|
+
expect(msgs[0]).toMatchObject({
|
|
544
|
+
status: { type: "complete", reason: "stop" },
|
|
545
|
+
});
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
it("preserves text and skips status when partial text precedes HITL event", () => {
|
|
549
|
+
const acc = new AdkEventAccumulator();
|
|
550
|
+
acc.processEvent(
|
|
551
|
+
makeEvent({
|
|
552
|
+
author: "ClarifyAgent",
|
|
553
|
+
partial: true,
|
|
554
|
+
content: {
|
|
555
|
+
role: "model",
|
|
556
|
+
parts: [{ text: "Let me ask about " }],
|
|
557
|
+
},
|
|
558
|
+
}),
|
|
559
|
+
);
|
|
560
|
+
|
|
561
|
+
const msgs = acc.processEvent(
|
|
562
|
+
makeHitlEvent("tc-1", { author: "ClarifyAgent" }),
|
|
563
|
+
);
|
|
564
|
+
|
|
565
|
+
const aiMsg = msgs.find(
|
|
566
|
+
(m): m is AdkMessage & { type: "ai" } =>
|
|
567
|
+
m.type === "ai" && (m.tool_calls?.length ?? 0) > 0,
|
|
568
|
+
);
|
|
569
|
+
expect(aiMsg).toBeDefined();
|
|
570
|
+
expect(aiMsg!.tool_calls).toHaveLength(1);
|
|
571
|
+
expect(
|
|
572
|
+
aiMsg!.content.some(
|
|
573
|
+
(c) => c.type === "text" && c.text.includes("Let me ask"),
|
|
574
|
+
),
|
|
575
|
+
).toBe(true);
|
|
576
|
+
expect(aiMsg!.status).toBeUndefined();
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
it("keeps status undefined for mixed text and functionCall content", () => {
|
|
580
|
+
const acc = new AdkEventAccumulator();
|
|
581
|
+
const msgs = acc.processEvent(
|
|
582
|
+
makeEvent({
|
|
583
|
+
author: "agent",
|
|
584
|
+
longRunningToolIds: ["mixed-tc-1"],
|
|
585
|
+
content: {
|
|
586
|
+
role: "model",
|
|
587
|
+
parts: [
|
|
588
|
+
{ text: "I need some clarification." },
|
|
589
|
+
{
|
|
590
|
+
functionCall: {
|
|
591
|
+
id: "mixed-tc-1",
|
|
592
|
+
name: "adk_request_input",
|
|
593
|
+
args: { message: "Which region?" },
|
|
594
|
+
},
|
|
595
|
+
},
|
|
596
|
+
],
|
|
597
|
+
},
|
|
598
|
+
}),
|
|
599
|
+
);
|
|
600
|
+
|
|
601
|
+
const aiMsg = msgs[0] as AdkMessage & { type: "ai" };
|
|
602
|
+
expect(aiMsg.content.some((c) => c.type === "text")).toBe(true);
|
|
603
|
+
expect(aiMsg.tool_calls).toHaveLength(1);
|
|
604
|
+
expect(aiMsg.tool_calls![0]!.id).toBe("mixed-tc-1");
|
|
605
|
+
expect(aiMsg.status).toBeUndefined();
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
it("handles multiple tool calls with partial longRunningToolIds overlap", () => {
|
|
609
|
+
const acc = new AdkEventAccumulator();
|
|
610
|
+
const msgs = acc.processEvent(
|
|
611
|
+
makeEvent({
|
|
612
|
+
author: "agent",
|
|
613
|
+
longRunningToolIds: ["tc-hitl"],
|
|
614
|
+
content: {
|
|
615
|
+
role: "model",
|
|
616
|
+
parts: [
|
|
617
|
+
{
|
|
618
|
+
functionCall: {
|
|
619
|
+
id: "tc-hitl",
|
|
620
|
+
name: "adk_request_input",
|
|
621
|
+
args: { message: "Confirm?" },
|
|
622
|
+
},
|
|
623
|
+
},
|
|
624
|
+
{
|
|
625
|
+
functionCall: {
|
|
626
|
+
id: "tc-regular",
|
|
627
|
+
name: "fetch_data",
|
|
628
|
+
args: { url: "https://example.com" },
|
|
629
|
+
},
|
|
630
|
+
},
|
|
631
|
+
],
|
|
632
|
+
},
|
|
633
|
+
}),
|
|
634
|
+
);
|
|
635
|
+
|
|
636
|
+
const aiMsg = msgs[0] as AdkMessage & { type: "ai" };
|
|
637
|
+
expect(aiMsg.tool_calls).toHaveLength(2);
|
|
638
|
+
expect(aiMsg.tool_calls!.map((tc) => tc.id).sort()).toEqual(
|
|
639
|
+
["tc-hitl", "tc-regular"].sort(),
|
|
640
|
+
);
|
|
641
|
+
expect(aiMsg.status).toBeUndefined();
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
it("does not override HITL guard with explicit finishReason", () => {
|
|
645
|
+
const acc = new AdkEventAccumulator();
|
|
646
|
+
const msgs = acc.processEvent(
|
|
647
|
+
makeHitlEvent("tc-fr", { finishReason: "STOP" }),
|
|
648
|
+
);
|
|
649
|
+
|
|
650
|
+
const aiMsg = msgs[0] as AdkMessage & { type: "ai" };
|
|
651
|
+
expect(aiMsg.status).toBeUndefined();
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
it("produces separate messages without status for sequential HITL events", () => {
|
|
655
|
+
const acc = new AdkEventAccumulator();
|
|
656
|
+
acc.processEvent(
|
|
657
|
+
makeHitlEvent("tc-seq-1", { id: "evt-1", author: "WorkflowA" }),
|
|
658
|
+
);
|
|
659
|
+
|
|
660
|
+
const msgs = acc.processEvent(
|
|
661
|
+
makeEvent({
|
|
662
|
+
id: "evt-2",
|
|
663
|
+
author: "WorkflowB",
|
|
664
|
+
longRunningToolIds: ["tc-seq-2"],
|
|
665
|
+
content: {
|
|
666
|
+
role: "model",
|
|
667
|
+
parts: [
|
|
668
|
+
{
|
|
669
|
+
functionCall: {
|
|
670
|
+
id: "tc-seq-2",
|
|
671
|
+
name: "adk_request_confirmation",
|
|
672
|
+
args: {
|
|
673
|
+
originalFunctionCall: { name: "delete_all", args: {} },
|
|
674
|
+
toolConfirmation: { hint: "Sure?", payload: {} },
|
|
675
|
+
},
|
|
676
|
+
},
|
|
677
|
+
},
|
|
678
|
+
],
|
|
679
|
+
},
|
|
680
|
+
}),
|
|
681
|
+
);
|
|
682
|
+
|
|
683
|
+
const hitlMsgs = msgs.filter(
|
|
684
|
+
(m): m is AdkMessage & { type: "ai" } =>
|
|
685
|
+
m.type === "ai" && (m.tool_calls?.length ?? 0) > 0,
|
|
686
|
+
);
|
|
687
|
+
expect(hitlMsgs).toHaveLength(2);
|
|
688
|
+
expect(hitlMsgs[0]!.status).toBeUndefined();
|
|
689
|
+
expect(hitlMsgs[1]!.status).toBeUndefined();
|
|
690
|
+
expect(hitlMsgs[0]!.tool_calls![0]!.id).toBe("tc-seq-1");
|
|
691
|
+
expect(hitlMsgs[1]!.tool_calls![0]!.id).toBe("tc-seq-2");
|
|
692
|
+
});
|
|
420
693
|
});
|
|
421
694
|
|
|
422
695
|
describe("AdkEventAccumulator - actions tracking", () => {
|
|
@@ -487,6 +760,44 @@ describe("AdkEventAccumulator - actions tracking", () => {
|
|
|
487
760
|
);
|
|
488
761
|
expect(acc.getLongRunningToolIds()).toEqual(["lrt-1", "lrt-2"]);
|
|
489
762
|
});
|
|
763
|
+
|
|
764
|
+
it("unions longRunningToolIds across multiple events", () => {
|
|
765
|
+
const acc = new AdkEventAccumulator();
|
|
766
|
+
acc.processEvent(
|
|
767
|
+
makeEvent({
|
|
768
|
+
longRunningToolIds: ["lrt-1"],
|
|
769
|
+
author: "agent",
|
|
770
|
+
content: { parts: [{ text: "x" }] },
|
|
771
|
+
}),
|
|
772
|
+
);
|
|
773
|
+
acc.processEvent(
|
|
774
|
+
makeEvent({
|
|
775
|
+
longRunningToolIds: ["lrt-2"],
|
|
776
|
+
author: "agent",
|
|
777
|
+
content: { parts: [{ text: "y" }] },
|
|
778
|
+
}),
|
|
779
|
+
);
|
|
780
|
+
expect(acc.getLongRunningToolIds()).toEqual(["lrt-1", "lrt-2"]);
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
it("deduplicates repeated longRunningToolIds", () => {
|
|
784
|
+
const acc = new AdkEventAccumulator();
|
|
785
|
+
acc.processEvent(
|
|
786
|
+
makeEvent({
|
|
787
|
+
longRunningToolIds: ["lrt-1"],
|
|
788
|
+
author: "agent",
|
|
789
|
+
content: { parts: [{ text: "x" }] },
|
|
790
|
+
}),
|
|
791
|
+
);
|
|
792
|
+
acc.processEvent(
|
|
793
|
+
makeEvent({
|
|
794
|
+
longRunningToolIds: ["lrt-1"],
|
|
795
|
+
author: "agent",
|
|
796
|
+
content: { parts: [{ text: "y" }] },
|
|
797
|
+
}),
|
|
798
|
+
);
|
|
799
|
+
expect(acc.getLongRunningToolIds()).toEqual(["lrt-1"]);
|
|
800
|
+
});
|
|
490
801
|
});
|
|
491
802
|
|
|
492
803
|
describe("AdkEventAccumulator - special function calls", () => {
|
|
@@ -576,7 +887,12 @@ describe("AdkEventAccumulator - author/agent tracking", () => {
|
|
|
576
887
|
|
|
577
888
|
it("does not track user as agent info", () => {
|
|
578
889
|
const acc = new AdkEventAccumulator();
|
|
579
|
-
acc.processEvent(
|
|
890
|
+
acc.processEvent(
|
|
891
|
+
makeEvent({
|
|
892
|
+
author: "user",
|
|
893
|
+
content: { role: "user", parts: [{ text: "hello" }] },
|
|
894
|
+
}),
|
|
895
|
+
);
|
|
580
896
|
expect(acc.getAgentInfo()).toEqual({});
|
|
581
897
|
});
|
|
582
898
|
|
|
@@ -667,3 +983,206 @@ describe("AdkEventAccumulator - initial messages", () => {
|
|
|
667
983
|
expect(msgs).toHaveLength(2);
|
|
668
984
|
});
|
|
669
985
|
});
|
|
986
|
+
|
|
987
|
+
describe("AdkEventAccumulator - user message handling", () => {
|
|
988
|
+
it("creates a human message for user-authored text events", () => {
|
|
989
|
+
const acc = new AdkEventAccumulator();
|
|
990
|
+
const msgs = acc.processEvent(
|
|
991
|
+
makeEvent({
|
|
992
|
+
author: "user",
|
|
993
|
+
content: { role: "user", parts: [{ text: "hello" }] },
|
|
994
|
+
}),
|
|
995
|
+
);
|
|
996
|
+
expect(msgs).toHaveLength(1);
|
|
997
|
+
expect(msgs[0]).toMatchObject({
|
|
998
|
+
type: "human",
|
|
999
|
+
content: "hello",
|
|
1000
|
+
});
|
|
1001
|
+
});
|
|
1002
|
+
|
|
1003
|
+
it("creates separate human and AI messages for a user/agent turn", () => {
|
|
1004
|
+
const acc = new AdkEventAccumulator();
|
|
1005
|
+
acc.processEvent(
|
|
1006
|
+
makeEvent({
|
|
1007
|
+
id: "u1",
|
|
1008
|
+
author: "user",
|
|
1009
|
+
content: { role: "user", parts: [{ text: "help" }] },
|
|
1010
|
+
}),
|
|
1011
|
+
);
|
|
1012
|
+
const msgs = acc.processEvent(makeTextEvent("How can I help?"));
|
|
1013
|
+
expect(msgs).toHaveLength(2);
|
|
1014
|
+
expect(msgs[0]).toMatchObject({ type: "human", content: "help" });
|
|
1015
|
+
expect(msgs[1]).toMatchObject({
|
|
1016
|
+
type: "ai",
|
|
1017
|
+
content: [{ type: "text", text: "How can I help?" }],
|
|
1018
|
+
});
|
|
1019
|
+
});
|
|
1020
|
+
|
|
1021
|
+
it("handles multiple user/agent turns with correct message types", () => {
|
|
1022
|
+
const acc = new AdkEventAccumulator();
|
|
1023
|
+
acc.processEvent(
|
|
1024
|
+
makeEvent({
|
|
1025
|
+
id: "u1",
|
|
1026
|
+
author: "user",
|
|
1027
|
+
content: { role: "user", parts: [{ text: "hello" }] },
|
|
1028
|
+
}),
|
|
1029
|
+
);
|
|
1030
|
+
acc.processEvent(makeTextEvent("Hi there!"));
|
|
1031
|
+
acc.processEvent(
|
|
1032
|
+
makeEvent({
|
|
1033
|
+
id: "u2",
|
|
1034
|
+
author: "user",
|
|
1035
|
+
content: { role: "user", parts: [{ text: "help" }] },
|
|
1036
|
+
}),
|
|
1037
|
+
);
|
|
1038
|
+
const msgs = acc.processEvent(makeTextEvent("How can I help?"));
|
|
1039
|
+
expect(msgs).toHaveLength(4);
|
|
1040
|
+
expect(msgs[0]).toMatchObject({ type: "human", content: "hello" });
|
|
1041
|
+
expect(msgs[1]).toMatchObject({ type: "ai" });
|
|
1042
|
+
expect(msgs[2]).toMatchObject({ type: "human", content: "help" });
|
|
1043
|
+
expect(msgs[3]).toMatchObject({ type: "ai" });
|
|
1044
|
+
});
|
|
1045
|
+
|
|
1046
|
+
it("creates human message with image content", () => {
|
|
1047
|
+
const acc = new AdkEventAccumulator();
|
|
1048
|
+
const msgs = acc.processEvent(
|
|
1049
|
+
makeEvent({
|
|
1050
|
+
author: "user",
|
|
1051
|
+
content: {
|
|
1052
|
+
role: "user",
|
|
1053
|
+
parts: [{ inlineData: { mimeType: "image/png", data: "abc123" } }],
|
|
1054
|
+
},
|
|
1055
|
+
}),
|
|
1056
|
+
);
|
|
1057
|
+
expect(msgs).toHaveLength(1);
|
|
1058
|
+
expect(msgs[0]).toMatchObject({
|
|
1059
|
+
type: "human",
|
|
1060
|
+
content: [{ type: "image", mimeType: "image/png", data: "abc123" }],
|
|
1061
|
+
});
|
|
1062
|
+
});
|
|
1063
|
+
|
|
1064
|
+
it("creates human message with mixed text and image content", () => {
|
|
1065
|
+
const acc = new AdkEventAccumulator();
|
|
1066
|
+
const msgs = acc.processEvent(
|
|
1067
|
+
makeEvent({
|
|
1068
|
+
author: "user",
|
|
1069
|
+
content: {
|
|
1070
|
+
role: "user",
|
|
1071
|
+
parts: [
|
|
1072
|
+
{ text: "Look at this" },
|
|
1073
|
+
{ inlineData: { mimeType: "image/png", data: "abc123" } },
|
|
1074
|
+
],
|
|
1075
|
+
},
|
|
1076
|
+
}),
|
|
1077
|
+
);
|
|
1078
|
+
expect(msgs).toHaveLength(1);
|
|
1079
|
+
expect(msgs[0]).toMatchObject({
|
|
1080
|
+
type: "human",
|
|
1081
|
+
content: [
|
|
1082
|
+
{ type: "text", text: "Look at this" },
|
|
1083
|
+
{ type: "image", mimeType: "image/png", data: "abc123" },
|
|
1084
|
+
],
|
|
1085
|
+
});
|
|
1086
|
+
});
|
|
1087
|
+
|
|
1088
|
+
it("creates human message with fileData content", () => {
|
|
1089
|
+
const acc = new AdkEventAccumulator();
|
|
1090
|
+
const msgs = acc.processEvent(
|
|
1091
|
+
makeEvent({
|
|
1092
|
+
author: "user",
|
|
1093
|
+
content: {
|
|
1094
|
+
role: "user",
|
|
1095
|
+
parts: [{ fileData: { fileUri: "gs://bucket/image.png" } }],
|
|
1096
|
+
},
|
|
1097
|
+
}),
|
|
1098
|
+
);
|
|
1099
|
+
expect(msgs).toHaveLength(1);
|
|
1100
|
+
expect(msgs[0]).toMatchObject({
|
|
1101
|
+
type: "human",
|
|
1102
|
+
content: [{ type: "image_url", url: "gs://bucket/image.png" }],
|
|
1103
|
+
});
|
|
1104
|
+
});
|
|
1105
|
+
|
|
1106
|
+
it("tool result events (no author, role:'user') still create tool messages", () => {
|
|
1107
|
+
// Regression: the user-author check must not hijack tool events.
|
|
1108
|
+
// messageToEvent for `type:'tool'` emits events without `author`,
|
|
1109
|
+
// with content.role:'user' and a functionResponse part.
|
|
1110
|
+
const acc = new AdkEventAccumulator();
|
|
1111
|
+
const msgs = acc.processEvent(
|
|
1112
|
+
makeEvent({
|
|
1113
|
+
content: {
|
|
1114
|
+
role: "user",
|
|
1115
|
+
parts: [
|
|
1116
|
+
{
|
|
1117
|
+
functionResponse: {
|
|
1118
|
+
name: "get_weather",
|
|
1119
|
+
id: "call_1",
|
|
1120
|
+
response: { temp: 72 },
|
|
1121
|
+
},
|
|
1122
|
+
},
|
|
1123
|
+
],
|
|
1124
|
+
},
|
|
1125
|
+
}),
|
|
1126
|
+
);
|
|
1127
|
+
expect(msgs).toHaveLength(1);
|
|
1128
|
+
expect(msgs[0]).toMatchObject({
|
|
1129
|
+
type: "tool",
|
|
1130
|
+
name: "get_weather",
|
|
1131
|
+
tool_call_id: "call_1",
|
|
1132
|
+
content: JSON.stringify({ temp: 72 }),
|
|
1133
|
+
});
|
|
1134
|
+
});
|
|
1135
|
+
|
|
1136
|
+
it("session replay produces correct message types", () => {
|
|
1137
|
+
// Simulate loading a session with alternating user/agent events
|
|
1138
|
+
const acc = new AdkEventAccumulator();
|
|
1139
|
+
const events: AdkEvent[] = [
|
|
1140
|
+
makeEvent({
|
|
1141
|
+
id: "e1",
|
|
1142
|
+
author: "user",
|
|
1143
|
+
content: { role: "user", parts: [{ text: "good morning" }] },
|
|
1144
|
+
}),
|
|
1145
|
+
// Workflow state-only events (no content)
|
|
1146
|
+
makeEvent({
|
|
1147
|
+
id: "e2",
|
|
1148
|
+
author: "WorkflowAgent",
|
|
1149
|
+
actions: { stateDelta: { intent: "greeting" } },
|
|
1150
|
+
}),
|
|
1151
|
+
// Final response with content
|
|
1152
|
+
makeEvent({
|
|
1153
|
+
id: "e3",
|
|
1154
|
+
author: "WorkflowAgent",
|
|
1155
|
+
content: { role: "model", parts: [{ text: "Hello! How can I help?" }] },
|
|
1156
|
+
}),
|
|
1157
|
+
// Second turn
|
|
1158
|
+
makeEvent({
|
|
1159
|
+
id: "e4",
|
|
1160
|
+
author: "user",
|
|
1161
|
+
content: { role: "user", parts: [{ text: "help" }] },
|
|
1162
|
+
}),
|
|
1163
|
+
makeEvent({
|
|
1164
|
+
id: "e5",
|
|
1165
|
+
author: "WorkflowAgent",
|
|
1166
|
+
content: {
|
|
1167
|
+
role: "model",
|
|
1168
|
+
parts: [{ text: "What do you need help with?" }],
|
|
1169
|
+
},
|
|
1170
|
+
}),
|
|
1171
|
+
];
|
|
1172
|
+
let msgs: AdkMessage[] = [];
|
|
1173
|
+
for (const event of events) {
|
|
1174
|
+
msgs = acc.processEvent(event);
|
|
1175
|
+
}
|
|
1176
|
+
expect(msgs).toHaveLength(4);
|
|
1177
|
+
expect(msgs[0]).toMatchObject({ type: "human", content: "good morning" });
|
|
1178
|
+
expect(msgs[1]).toMatchObject({
|
|
1179
|
+
type: "ai",
|
|
1180
|
+
content: [{ type: "text", text: "Hello! How can I help?" }],
|
|
1181
|
+
});
|
|
1182
|
+
expect(msgs[2]).toMatchObject({ type: "human", content: "help" });
|
|
1183
|
+
expect(msgs[3]).toMatchObject({
|
|
1184
|
+
type: "ai",
|
|
1185
|
+
content: [{ type: "text", text: "What do you need help with?" }],
|
|
1186
|
+
});
|
|
1187
|
+
});
|
|
1188
|
+
});
|
|
@@ -166,7 +166,7 @@ export class AdkEventAccumulator {
|
|
|
166
166
|
branch?: string | undefined;
|
|
167
167
|
} = {};
|
|
168
168
|
private lastTransferToAgent: string | undefined;
|
|
169
|
-
private pendingLongRunningToolIds
|
|
169
|
+
private pendingLongRunningToolIds = new Set<string>();
|
|
170
170
|
private toolConfirmations: AdkToolConfirmation[] = [];
|
|
171
171
|
private authRequests: AdkAuthRequest[] = [];
|
|
172
172
|
private escalated = false;
|
|
@@ -202,9 +202,13 @@ export class AdkEventAccumulator {
|
|
|
202
202
|
this.lastTransferToAgent = event.actions.transferToAgent;
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
-
// Track long-running tool IDs
|
|
205
|
+
// Track long-running tool IDs. Accumulate across events so that multiple
|
|
206
|
+
// HITL interrupts emitted in the same turn are all tracked — a
|
|
207
|
+
// single-event replacement would drop earlier ids.
|
|
206
208
|
if (event.longRunningToolIds?.length) {
|
|
207
|
-
|
|
209
|
+
for (const id of event.longRunningToolIds) {
|
|
210
|
+
this.pendingLongRunningToolIds.add(id);
|
|
211
|
+
}
|
|
208
212
|
}
|
|
209
213
|
|
|
210
214
|
// Track tool confirmations from actions
|
|
@@ -284,6 +288,41 @@ export class AdkEventAccumulator {
|
|
|
284
288
|
return this.getMessages();
|
|
285
289
|
}
|
|
286
290
|
|
|
291
|
+
// User-authored events → create human message, not AI.
|
|
292
|
+
// Without this, user events fall through to processPart →
|
|
293
|
+
// getOrCreateAiMessage, producing type:"ai" messages that
|
|
294
|
+
// convertAdkMessage maps to role:"assistant".
|
|
295
|
+
if (event.author === "user") {
|
|
296
|
+
this.finalizeCurrentMessage();
|
|
297
|
+
const humanParts: AdkMessageContentPart[] = [];
|
|
298
|
+
for (const part of parts) {
|
|
299
|
+
if (part.text != null && !part.thought) {
|
|
300
|
+
humanParts.push({ type: "text", text: part.text });
|
|
301
|
+
} else if (part.inlineData) {
|
|
302
|
+
humanParts.push({
|
|
303
|
+
type: "image",
|
|
304
|
+
mimeType: part.inlineData.mimeType,
|
|
305
|
+
data: part.inlineData.data,
|
|
306
|
+
});
|
|
307
|
+
} else if (part.fileData) {
|
|
308
|
+
humanParts.push({
|
|
309
|
+
type: "image_url",
|
|
310
|
+
url: part.fileData.fileUri,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
if (humanParts.length > 0) {
|
|
315
|
+
const id = event.id ?? uuidv4();
|
|
316
|
+
const first = humanParts[0];
|
|
317
|
+
const content: string | AdkMessageContentPart[] =
|
|
318
|
+
humanParts.length === 1 && first?.type === "text"
|
|
319
|
+
? first.text
|
|
320
|
+
: humanParts;
|
|
321
|
+
this.messagesMap.set(id, { id, type: "human", content });
|
|
322
|
+
}
|
|
323
|
+
return this.getMessages();
|
|
324
|
+
}
|
|
325
|
+
|
|
287
326
|
// If author changed, finalize previous message
|
|
288
327
|
if (this.currentMessageId && event.author && event.author !== "user") {
|
|
289
328
|
const current = this.messagesMap.get(this.currentMessageId);
|
|
@@ -302,7 +341,13 @@ export class AdkEventAccumulator {
|
|
|
302
341
|
// Check isFinalResponse (can be true even for partial events via skipSummarization/longRunningToolIds)
|
|
303
342
|
if (isFinalResponse(event) && this.currentMessageId) {
|
|
304
343
|
const msg = this.messagesMap.get(this.currentMessageId);
|
|
305
|
-
|
|
344
|
+
// Skip manual "complete" when longRunningToolIds is the sole reason for
|
|
345
|
+
// isFinalResponse — let auto-status apply requires-action for pending tool calls.
|
|
346
|
+
const isHitlOnly =
|
|
347
|
+
event.longRunningToolIds &&
|
|
348
|
+
event.longRunningToolIds.length > 0 &&
|
|
349
|
+
!event.actions?.skipSummarization;
|
|
350
|
+
if (msg && msg.type === "ai" && !msg.status && !isHitlOnly) {
|
|
306
351
|
const status = finishReasonToStatus(event.finishReason);
|
|
307
352
|
const updated: InProgressMessage = {
|
|
308
353
|
...msg,
|
package/src/hooks.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useAui, useAuiState } from "@assistant-ui/store";
|
|
4
4
|
import { v4 as uuidv4 } from "uuid";
|
|
5
|
+
import type { ReadonlyJSONValue } from "assistant-stream/utils";
|
|
5
6
|
import type {
|
|
6
7
|
AdkMessage,
|
|
7
8
|
AdkSendMessageConfig,
|
|
@@ -126,7 +127,11 @@ export const useAdkMessageMetadata = () => {
|
|
|
126
127
|
/** Returns a function to confirm or deny a pending tool confirmation. */
|
|
127
128
|
export const useAdkConfirmTool = () => {
|
|
128
129
|
const aui = useAui();
|
|
129
|
-
return (
|
|
130
|
+
return (
|
|
131
|
+
toolCallId: string,
|
|
132
|
+
confirmed: boolean,
|
|
133
|
+
payload?: ReadonlyJSONValue,
|
|
134
|
+
) => {
|
|
130
135
|
const extras = aui.thread().getState().extras;
|
|
131
136
|
const { send } = asAdkRuntimeExtras(extras);
|
|
132
137
|
return send(
|
|
@@ -170,6 +175,28 @@ export const useAdkSubmitAuth = () => {
|
|
|
170
175
|
};
|
|
171
176
|
};
|
|
172
177
|
|
|
178
|
+
/** Returns a function to submit the user's answer for a pending `adk_request_input` HITL interrupt. */
|
|
179
|
+
export const useAdkSubmitInput = () => {
|
|
180
|
+
const aui = useAui();
|
|
181
|
+
return (toolCallId: string, result: ReadonlyJSONValue) => {
|
|
182
|
+
const extras = aui.thread().getState().extras;
|
|
183
|
+
const { send } = asAdkRuntimeExtras(extras);
|
|
184
|
+
return send(
|
|
185
|
+
[
|
|
186
|
+
{
|
|
187
|
+
id: uuidv4(),
|
|
188
|
+
type: "tool",
|
|
189
|
+
tool_call_id: toolCallId,
|
|
190
|
+
name: "adk_request_input",
|
|
191
|
+
content: JSON.stringify({ result }),
|
|
192
|
+
status: "success",
|
|
193
|
+
},
|
|
194
|
+
],
|
|
195
|
+
{},
|
|
196
|
+
);
|
|
197
|
+
};
|
|
198
|
+
};
|
|
199
|
+
|
|
173
200
|
// ── State prefix helpers ──
|
|
174
201
|
|
|
175
202
|
const APP_PREFIX = "app:";
|