@assistant-ui/react-google-adk 0.0.30 → 0.0.32
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/AdkClient.d.ts +26 -29
- package/dist/AdkClient.d.ts.map +1 -1
- package/dist/AdkClient.js +5 -3
- package/dist/AdkClient.js.map +1 -1
- package/dist/AdkEventAccumulator.d.ts +41 -45
- package/dist/AdkEventAccumulator.d.ts.map +1 -1
- package/dist/AdkEventAccumulator.js +63 -99
- package/dist/AdkEventAccumulator.js.map +1 -1
- package/dist/AdkSessionAdapter.d.ts +41 -39
- package/dist/AdkSessionAdapter.d.ts.map +1 -1
- package/dist/AdkSessionAdapter.js +11 -7
- package/dist/AdkSessionAdapter.js.map +1 -1
- package/dist/adkAuthRequest.d.ts +17 -0
- package/dist/adkAuthRequest.d.ts.map +1 -0
- package/dist/adkAuthRequest.js +45 -0
- package/dist/adkAuthRequest.js.map +1 -0
- package/dist/adkExtras.d.ts +2 -5
- package/dist/adkExtras.d.ts.map +1 -1
- package/dist/adkToolApproval.d.ts +18 -21
- package/dist/adkToolApproval.d.ts.map +1 -1
- package/dist/adkToolApproval.js +1 -3
- package/dist/adkToolApproval.js.map +1 -1
- package/dist/contentToParts.d.ts +2 -5
- package/dist/contentToParts.d.ts.map +1 -1
- package/dist/convertAdkMessages.d.ts +5 -8
- package/dist/convertAdkMessages.d.ts.map +1 -1
- package/dist/convertAdkMessages.js +4 -3
- package/dist/convertAdkMessages.js.map +1 -1
- package/dist/convertToAdkMessages.d.ts +36 -39
- package/dist/convertToAdkMessages.d.ts.map +1 -1
- package/dist/hooks.d.ts +25 -23
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +12 -10
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +11 -10
- package/dist/index.d.ts.map +1 -0
- package/dist/normalizeAdkPart.d.ts +2 -0
- package/dist/normalizeAdkPart.d.ts.map +1 -0
- package/dist/normalizeAdkPart.js +31 -0
- package/dist/normalizeAdkPart.js.map +1 -0
- package/dist/parseAdkEvent.d.ts +2 -5
- package/dist/parseAdkEvent.d.ts.map +1 -1
- package/dist/raceWithAbortSignal.d.ts +2 -0
- package/dist/raceWithAbortSignal.d.ts.map +1 -0
- package/dist/raceWithAbortSignal.js +45 -0
- package/dist/raceWithAbortSignal.js.map +1 -0
- package/dist/sdkIdentity.d.ts +2 -5
- package/dist/sdkIdentity.d.ts.map +1 -1
- package/dist/sdkIdentity.js +1 -1
- package/dist/server/adkEventStream.d.ts +25 -27
- package/dist/server/adkEventStream.d.ts.map +1 -1
- package/dist/server/adkEventStream.js +23 -15
- package/dist/server/adkEventStream.js.map +1 -1
- package/dist/server/createAdkApiRoute.d.ts +23 -25
- package/dist/server/createAdkApiRoute.d.ts.map +1 -1
- package/dist/server/index.d.ts +4 -4
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/parseAdkRequest.d.ts +18 -20
- package/dist/server/parseAdkRequest.d.ts.map +1 -1
- package/dist/structuredEvents.d.ts +2 -5
- package/dist/structuredEvents.d.ts.map +1 -1
- package/dist/structuredEvents.js +1 -1
- package/dist/structuredEvents.js.map +1 -1
- package/dist/toAdkFunctionResponse.d.ts +2 -5
- package/dist/toAdkFunctionResponse.d.ts.map +1 -1
- package/dist/trimTrailingSlashes.d.ts +1 -4
- package/dist/trimTrailingSlashes.d.ts.map +1 -1
- package/dist/types.d.ts +253 -256
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/useAdkMessages.d.ts +26 -29
- package/dist/useAdkMessages.d.ts.map +1 -1
- package/dist/useAdkMessages.js +2 -0
- package/dist/useAdkMessages.js.map +1 -1
- package/dist/useAdkRuntime.d.ts +50 -52
- package/dist/useAdkRuntime.d.ts.map +1 -1
- package/dist/useAdkRuntime.js +1 -1
- package/package.json +10 -9
- package/src/AdkClient.test.ts +50 -0
- package/src/AdkClient.ts +10 -3
- package/src/AdkEventAccumulator.test.ts +312 -11
- package/src/AdkEventAccumulator.ts +68 -142
- package/src/AdkSessionAdapter.test.ts +284 -1
- package/src/AdkSessionAdapter.ts +32 -8
- package/src/adkAuthRequest.test.ts +101 -0
- package/src/adkAuthRequest.ts +50 -0
- package/src/adkToolApproval.test.ts +0 -4
- package/src/adkToolApproval.ts +1 -3
- package/src/convertAdkMessages.test.ts +41 -0
- package/src/convertAdkMessages.ts +19 -7
- package/src/hooks.render.test.tsx +95 -2
- package/src/hooks.ts +12 -13
- package/src/normalizeAdkPart.ts +35 -0
- package/src/raceWithAbortSignal.test.ts +73 -0
- package/src/raceWithAbortSignal.ts +48 -0
- package/src/server/adkEventStream.test.ts +147 -0
- package/src/server/adkEventStream.ts +27 -20
- package/src/structuredEvents.test.ts +69 -0
- package/src/structuredEvents.ts +4 -1
- package/src/types.ts +1 -1
- package/src/useAdkMessages.test.ts +102 -3
- package/src/useAdkMessages.ts +2 -0
- package/src/useAdkRuntime.replacement.test.tsx +11 -7
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
2
|
import { createAdkSessionAdapter } from "./AdkSessionAdapter";
|
|
3
3
|
import { projectAdkToolApprovals } from "./adkToolApproval";
|
|
4
|
+
import type { AdkMessage } from "./types";
|
|
4
5
|
|
|
5
6
|
// ── Helpers ──
|
|
6
7
|
|
|
@@ -21,6 +22,23 @@ const baseOptions = {
|
|
|
21
22
|
const expectedBaseUrl =
|
|
22
23
|
"http://localhost:8000/apps/my-app/users/user-1/sessions";
|
|
23
24
|
|
|
25
|
+
describe("createAdkSessionAdapter - load cancellation", () => {
|
|
26
|
+
it("aborts while dynamic headers are pending", async () => {
|
|
27
|
+
const controller = new AbortController();
|
|
28
|
+
const reason = new Error("cancelled");
|
|
29
|
+
const { load } = createAdkSessionAdapter({
|
|
30
|
+
...baseOptions,
|
|
31
|
+
headers: () => new Promise<Record<string, string>>(() => {}),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const result = load("session-1", { signal: controller.signal });
|
|
35
|
+
controller.abort(reason);
|
|
36
|
+
|
|
37
|
+
await expect(result).rejects.toBe(reason);
|
|
38
|
+
expect(mockFetch).not.toHaveBeenCalled();
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
24
42
|
// ── adapter.list() ──
|
|
25
43
|
|
|
26
44
|
describe("createAdkSessionAdapter - list", () => {
|
|
@@ -375,6 +393,117 @@ describe("createAdkSessionAdapter - load", () => {
|
|
|
375
393
|
]);
|
|
376
394
|
});
|
|
377
395
|
|
|
396
|
+
it("loads valid events when history contains malformed media", async () => {
|
|
397
|
+
mockFetch.mockResolvedValueOnce(
|
|
398
|
+
new Response(
|
|
399
|
+
JSON.stringify({
|
|
400
|
+
id: "s1",
|
|
401
|
+
events: [
|
|
402
|
+
{
|
|
403
|
+
id: "user-1",
|
|
404
|
+
author: "user",
|
|
405
|
+
content: { parts: [{ text: "before" }] },
|
|
406
|
+
},
|
|
407
|
+
{
|
|
408
|
+
id: "bad-media",
|
|
409
|
+
author: "agent",
|
|
410
|
+
content: {
|
|
411
|
+
parts: [
|
|
412
|
+
{ inlineData: { data: "aGVsbG8=" } },
|
|
413
|
+
{ fileData: { mimeType: "image/png" } },
|
|
414
|
+
],
|
|
415
|
+
},
|
|
416
|
+
},
|
|
417
|
+
{
|
|
418
|
+
id: "agent-1",
|
|
419
|
+
author: "agent",
|
|
420
|
+
content: { parts: [{ text: "after" }] },
|
|
421
|
+
},
|
|
422
|
+
],
|
|
423
|
+
}),
|
|
424
|
+
{ status: 200 },
|
|
425
|
+
),
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
const { load } = createAdkSessionAdapter(baseOptions);
|
|
429
|
+
const result = await load("s1");
|
|
430
|
+
|
|
431
|
+
expect(result.messages).toMatchObject([
|
|
432
|
+
{ type: "human", content: "before" },
|
|
433
|
+
{ type: "ai", content: [{ type: "text", text: "after" }] },
|
|
434
|
+
]);
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
it("loads valid events when history contains request calls without args", async () => {
|
|
438
|
+
mockFetch.mockResolvedValueOnce(
|
|
439
|
+
new Response(
|
|
440
|
+
JSON.stringify({
|
|
441
|
+
id: "s1",
|
|
442
|
+
events: [
|
|
443
|
+
{
|
|
444
|
+
id: "user-1",
|
|
445
|
+
author: "user",
|
|
446
|
+
content: { parts: [{ text: "before" }] },
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
id: "bad-requests",
|
|
450
|
+
author: "agent",
|
|
451
|
+
content: {
|
|
452
|
+
parts: [
|
|
453
|
+
{
|
|
454
|
+
functionCall: {
|
|
455
|
+
name: "adk_request_confirmation",
|
|
456
|
+
id: "rc-1",
|
|
457
|
+
},
|
|
458
|
+
},
|
|
459
|
+
{
|
|
460
|
+
functionCall: {
|
|
461
|
+
name: "adk_request_credential",
|
|
462
|
+
id: "rc-2",
|
|
463
|
+
},
|
|
464
|
+
},
|
|
465
|
+
],
|
|
466
|
+
},
|
|
467
|
+
},
|
|
468
|
+
{
|
|
469
|
+
id: "agent-1",
|
|
470
|
+
author: "agent",
|
|
471
|
+
content: { parts: [{ text: "after" }] },
|
|
472
|
+
},
|
|
473
|
+
],
|
|
474
|
+
}),
|
|
475
|
+
{ status: 200 },
|
|
476
|
+
),
|
|
477
|
+
);
|
|
478
|
+
|
|
479
|
+
const { load } = createAdkSessionAdapter(baseOptions);
|
|
480
|
+
const result = await load("s1");
|
|
481
|
+
|
|
482
|
+
expect(result.messages).toMatchObject([
|
|
483
|
+
{ type: "human", content: "before" },
|
|
484
|
+
{ type: "ai" },
|
|
485
|
+
{ type: "ai", content: [{ type: "text", text: "after" }] },
|
|
486
|
+
]);
|
|
487
|
+
expect(
|
|
488
|
+
(result.messages[1] as AdkMessage & { type: "ai" }).tool_calls,
|
|
489
|
+
).toEqual([
|
|
490
|
+
{
|
|
491
|
+
id: "rc-1",
|
|
492
|
+
name: "adk_request_confirmation",
|
|
493
|
+
args: {},
|
|
494
|
+
argsText: "{}",
|
|
495
|
+
},
|
|
496
|
+
{
|
|
497
|
+
id: "rc-2",
|
|
498
|
+
name: "adk_request_credential",
|
|
499
|
+
args: {},
|
|
500
|
+
argsText: "{}",
|
|
501
|
+
},
|
|
502
|
+
]);
|
|
503
|
+
expect(result.toolConfirmations).toMatchObject([{ toolCallId: "rc-1" }]);
|
|
504
|
+
expect(result.authRequests).toMatchObject([{ toolCallId: "rc-2" }]);
|
|
505
|
+
});
|
|
506
|
+
|
|
378
507
|
it("returns the per-turn state the events imply, not just the messages", async () => {
|
|
379
508
|
const session = {
|
|
380
509
|
id: "s1",
|
|
@@ -405,6 +534,85 @@ describe("createAdkSessionAdapter - load", () => {
|
|
|
405
534
|
expect(result.authRequests).toEqual([]);
|
|
406
535
|
});
|
|
407
536
|
|
|
537
|
+
it("reports only the requests the stored replies leave unanswered", async () => {
|
|
538
|
+
const session = {
|
|
539
|
+
id: "s1",
|
|
540
|
+
events: [
|
|
541
|
+
{
|
|
542
|
+
id: "e1",
|
|
543
|
+
author: "agent",
|
|
544
|
+
content: {
|
|
545
|
+
role: "model",
|
|
546
|
+
parts: [
|
|
547
|
+
{ functionCall: { name: "transfer", id: "gated-1", args: {} } },
|
|
548
|
+
{ functionCall: { name: "calendar", id: "gated-2", args: {} } },
|
|
549
|
+
],
|
|
550
|
+
},
|
|
551
|
+
},
|
|
552
|
+
{
|
|
553
|
+
id: "e2",
|
|
554
|
+
author: "agent",
|
|
555
|
+
longRunningToolIds: ["conf-1", "cred-1"],
|
|
556
|
+
actions: {
|
|
557
|
+
requestedToolConfirmations: { "gated-1": { hint: "Transfer?" } },
|
|
558
|
+
},
|
|
559
|
+
content: {
|
|
560
|
+
role: "user",
|
|
561
|
+
parts: [
|
|
562
|
+
{
|
|
563
|
+
functionCall: {
|
|
564
|
+
name: "adk_request_confirmation",
|
|
565
|
+
id: "conf-1",
|
|
566
|
+
args: {
|
|
567
|
+
originalFunctionCall: { id: "gated-1", name: "transfer" },
|
|
568
|
+
toolConfirmation: { hint: "Transfer?" },
|
|
569
|
+
},
|
|
570
|
+
},
|
|
571
|
+
},
|
|
572
|
+
{
|
|
573
|
+
functionCall: {
|
|
574
|
+
name: "adk_request_credential",
|
|
575
|
+
id: "cred-1",
|
|
576
|
+
args: {
|
|
577
|
+
functionCallId: "gated-2",
|
|
578
|
+
authConfig: { credentialKey: "k" },
|
|
579
|
+
},
|
|
580
|
+
},
|
|
581
|
+
},
|
|
582
|
+
],
|
|
583
|
+
},
|
|
584
|
+
},
|
|
585
|
+
{
|
|
586
|
+
id: "e3",
|
|
587
|
+
author: "user",
|
|
588
|
+
content: {
|
|
589
|
+
role: "user",
|
|
590
|
+
parts: [
|
|
591
|
+
{
|
|
592
|
+
functionResponse: {
|
|
593
|
+
name: "adk_request_confirmation",
|
|
594
|
+
id: "conf-1",
|
|
595
|
+
response: { confirmed: true },
|
|
596
|
+
},
|
|
597
|
+
},
|
|
598
|
+
],
|
|
599
|
+
},
|
|
600
|
+
},
|
|
601
|
+
],
|
|
602
|
+
};
|
|
603
|
+
mockFetch.mockResolvedValueOnce(
|
|
604
|
+
new Response(JSON.stringify(session), { status: 200 }),
|
|
605
|
+
);
|
|
606
|
+
|
|
607
|
+
const { load } = createAdkSessionAdapter(baseOptions);
|
|
608
|
+
const result = await load("s1");
|
|
609
|
+
|
|
610
|
+
expect(result.toolConfirmations).toEqual([]);
|
|
611
|
+
expect(result.authRequests).toEqual([
|
|
612
|
+
{ toolCallId: "cred-1", authConfig: { credentialKey: "k" } },
|
|
613
|
+
]);
|
|
614
|
+
});
|
|
615
|
+
|
|
408
616
|
it("passes an abort signal through to the request", async () => {
|
|
409
617
|
mockFetch.mockResolvedValueOnce(
|
|
410
618
|
new Response(JSON.stringify({ id: "s1", events: [] }), { status: 200 }),
|
|
@@ -727,6 +935,7 @@ describe("createAdkSessionAdapter - artifacts", () => {
|
|
|
727
935
|
"inline data",
|
|
728
936
|
{ inlineData: { mimeType: "image/png", data: "aGVsbG8=" } },
|
|
729
937
|
],
|
|
938
|
+
["file data", { fileData: { fileUri: "https://example.test/report.pdf" } }],
|
|
730
939
|
])("loads valid %s artifacts", async (_label, artifact) => {
|
|
731
940
|
mockFetch.mockResolvedValueOnce(
|
|
732
941
|
new Response(JSON.stringify(artifact), { status: 200 }),
|
|
@@ -737,6 +946,75 @@ describe("createAdkSessionAdapter - artifacts", () => {
|
|
|
737
946
|
await expect(artifacts.load("s1", "report.pdf")).resolves.toEqual(artifact);
|
|
738
947
|
});
|
|
739
948
|
|
|
949
|
+
it.each([
|
|
950
|
+
[
|
|
951
|
+
"inline data",
|
|
952
|
+
{ inline_data: { mime_type: "application/pdf", data: "aGVsbG8=" } },
|
|
953
|
+
{ inlineData: { mimeType: "application/pdf", data: "aGVsbG8=" } },
|
|
954
|
+
],
|
|
955
|
+
[
|
|
956
|
+
"file data",
|
|
957
|
+
{
|
|
958
|
+
file_data: {
|
|
959
|
+
mime_type: "application/pdf",
|
|
960
|
+
file_uri: "https://example.test/report.pdf",
|
|
961
|
+
},
|
|
962
|
+
},
|
|
963
|
+
{
|
|
964
|
+
fileData: {
|
|
965
|
+
mimeType: "application/pdf",
|
|
966
|
+
fileUri: "https://example.test/report.pdf",
|
|
967
|
+
},
|
|
968
|
+
},
|
|
969
|
+
],
|
|
970
|
+
])(
|
|
971
|
+
"normalizes snake_case %s artifact responses",
|
|
972
|
+
async (_label, value, expected) => {
|
|
973
|
+
mockFetch.mockResolvedValueOnce(
|
|
974
|
+
new Response(JSON.stringify(value), { status: 200 }),
|
|
975
|
+
);
|
|
976
|
+
|
|
977
|
+
const { artifacts } = createAdkSessionAdapter(baseOptions);
|
|
978
|
+
|
|
979
|
+
await expect(artifacts.load("s1", "report.pdf")).resolves.toMatchObject(
|
|
980
|
+
expected,
|
|
981
|
+
);
|
|
982
|
+
},
|
|
983
|
+
);
|
|
984
|
+
|
|
985
|
+
it("prefers camelCase artifact fields when both aliases are present", async () => {
|
|
986
|
+
mockFetch.mockResolvedValueOnce(
|
|
987
|
+
new Response(
|
|
988
|
+
JSON.stringify({
|
|
989
|
+
inlineData: {
|
|
990
|
+
mimeType: "image/png",
|
|
991
|
+
mime_type: "application/pdf",
|
|
992
|
+
data: "right",
|
|
993
|
+
},
|
|
994
|
+
inline_data: { mime_type: "text/plain", data: "wrong" },
|
|
995
|
+
fileData: {
|
|
996
|
+
mimeType: "application/pdf",
|
|
997
|
+
mime_type: "image/png",
|
|
998
|
+
fileUri: "https://example.test/right.pdf",
|
|
999
|
+
file_uri: "https://example.test/wrong.png",
|
|
1000
|
+
},
|
|
1001
|
+
file_data: { file_uri: "https://example.test/other.png" },
|
|
1002
|
+
}),
|
|
1003
|
+
{ status: 200 },
|
|
1004
|
+
),
|
|
1005
|
+
);
|
|
1006
|
+
|
|
1007
|
+
const { artifacts } = createAdkSessionAdapter(baseOptions);
|
|
1008
|
+
|
|
1009
|
+
await expect(artifacts.load("s1", "report.pdf")).resolves.toMatchObject({
|
|
1010
|
+
inlineData: { mimeType: "image/png", data: "right" },
|
|
1011
|
+
fileData: {
|
|
1012
|
+
mimeType: "application/pdf",
|
|
1013
|
+
fileUri: "https://example.test/right.pdf",
|
|
1014
|
+
},
|
|
1015
|
+
});
|
|
1016
|
+
});
|
|
1017
|
+
|
|
740
1018
|
it("rejects an artifact without supported content", async () => {
|
|
741
1019
|
mockFetch.mockResolvedValueOnce(
|
|
742
1020
|
new Response(JSON.stringify({}), { status: 200 }),
|
|
@@ -745,7 +1023,7 @@ describe("createAdkSessionAdapter - artifacts", () => {
|
|
|
745
1023
|
const { artifacts } = createAdkSessionAdapter(baseOptions);
|
|
746
1024
|
|
|
747
1025
|
await expect(artifacts.load("s1", "report.pdf")).rejects.toThrow(
|
|
748
|
-
'Invalid ADK artifact load response: expected an object containing "text" or "
|
|
1026
|
+
'Invalid ADK artifact load response: expected an object containing "text", "inlineData", or "fileData".',
|
|
749
1027
|
);
|
|
750
1028
|
});
|
|
751
1029
|
|
|
@@ -760,6 +1038,11 @@ describe("createAdkSessionAdapter - artifacts", () => {
|
|
|
760
1038
|
{ inlineData: { mimeType: "image/png" } },
|
|
761
1039
|
'Invalid ADK artifact load response: "inlineData" must contain string "mimeType" and "data" fields.',
|
|
762
1040
|
],
|
|
1041
|
+
[
|
|
1042
|
+
"file data",
|
|
1043
|
+
{ fileData: { mimeType: "application/pdf" } },
|
|
1044
|
+
'Invalid ADK artifact load response: "fileData" must contain a string "fileUri" and an optional string "mimeType" field.',
|
|
1045
|
+
],
|
|
763
1046
|
])("rejects malformed %s artifact content", async (_label, value, error) => {
|
|
764
1047
|
mockFetch.mockResolvedValueOnce(
|
|
765
1048
|
new Response(JSON.stringify(value), { status: 200 }),
|
package/src/AdkSessionAdapter.ts
CHANGED
|
@@ -6,9 +6,11 @@ import type {
|
|
|
6
6
|
RemoteThreadMetadata,
|
|
7
7
|
} from "@assistant-ui/core";
|
|
8
8
|
import { AdkEventAccumulator } from "./AdkEventAccumulator";
|
|
9
|
+
import { normalizeAdkPart } from "./normalizeAdkPart";
|
|
9
10
|
import { parseAdkEventValue } from "./parseAdkEvent";
|
|
10
11
|
import type { AdkMessage, AdkThreadSnapshot } from "./types";
|
|
11
12
|
import { trimTrailingSlashes } from "./trimTrailingSlashes";
|
|
13
|
+
import { raceWithAbortSignal } from "./raceWithAbortSignal";
|
|
12
14
|
|
|
13
15
|
export type AdkSessionAdapterOptions = {
|
|
14
16
|
/**
|
|
@@ -37,6 +39,7 @@ export type AdkSessionAdapterOptions = {
|
|
|
37
39
|
|
|
38
40
|
export type AdkArtifactData = {
|
|
39
41
|
inlineData?: { mimeType: string; data: string } | undefined;
|
|
42
|
+
fileData?: { fileUri: string; mimeType?: string | undefined } | undefined;
|
|
40
43
|
text?: string | undefined;
|
|
41
44
|
};
|
|
42
45
|
|
|
@@ -133,14 +136,19 @@ const parseAdkArtifactListResponse = (value: unknown): string[] => {
|
|
|
133
136
|
const parseAdkArtifactResponse = (value: unknown): AdkArtifactData => {
|
|
134
137
|
if (!isRecord(value)) {
|
|
135
138
|
throw new Error(
|
|
136
|
-
'Invalid ADK artifact load response: expected an object containing "text" or "
|
|
139
|
+
'Invalid ADK artifact load response: expected an object containing "text", "inlineData", or "fileData".',
|
|
137
140
|
);
|
|
138
141
|
}
|
|
139
142
|
|
|
140
|
-
const
|
|
141
|
-
|
|
143
|
+
const normalizedValue = normalizeAdkPart(value);
|
|
144
|
+
const { text, inlineData, fileData } = normalizedValue;
|
|
145
|
+
if (
|
|
146
|
+
text === undefined &&
|
|
147
|
+
inlineData === undefined &&
|
|
148
|
+
fileData === undefined
|
|
149
|
+
) {
|
|
142
150
|
throw new Error(
|
|
143
|
-
'Invalid ADK artifact load response: expected an object containing "text" or "
|
|
151
|
+
'Invalid ADK artifact load response: expected an object containing "text", "inlineData", or "fileData".',
|
|
144
152
|
);
|
|
145
153
|
}
|
|
146
154
|
|
|
@@ -161,7 +169,19 @@ const parseAdkArtifactResponse = (value: unknown): AdkArtifactData => {
|
|
|
161
169
|
);
|
|
162
170
|
}
|
|
163
171
|
|
|
164
|
-
|
|
172
|
+
if (
|
|
173
|
+
fileData !== undefined &&
|
|
174
|
+
(!isRecord(fileData) ||
|
|
175
|
+
typeof fileData.fileUri !== "string" ||
|
|
176
|
+
(fileData.mimeType !== undefined &&
|
|
177
|
+
typeof fileData.mimeType !== "string"))
|
|
178
|
+
) {
|
|
179
|
+
throw new Error(
|
|
180
|
+
'Invalid ADK artifact load response: "fileData" must contain a string "fileUri" and an optional string "mimeType" field.',
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return normalizedValue as AdkArtifactData;
|
|
165
185
|
};
|
|
166
186
|
|
|
167
187
|
const parseAdkArtifactVersionsResponse = (value: unknown): number[] => {
|
|
@@ -211,9 +231,13 @@ export function createAdkSessionAdapter(
|
|
|
211
231
|
const normalizedApiUrl = trimTrailingSlashes(apiUrl);
|
|
212
232
|
const baseUrl = `${normalizedApiUrl}/apps/${encodeURIComponent(appName)}/users/${encodeURIComponent(userId)}/sessions`;
|
|
213
233
|
|
|
214
|
-
const getHeaders = async (
|
|
234
|
+
const getHeaders = async (
|
|
235
|
+
signal?: AbortSignal,
|
|
236
|
+
): Promise<Record<string, string>> => {
|
|
215
237
|
if (!options.headers) return {};
|
|
216
|
-
if (typeof options.headers === "function")
|
|
238
|
+
if (typeof options.headers === "function") {
|
|
239
|
+
return await raceWithAbortSignal(signal, options.headers);
|
|
240
|
+
}
|
|
217
241
|
return options.headers;
|
|
218
242
|
};
|
|
219
243
|
|
|
@@ -308,7 +332,7 @@ export function createAdkSessionAdapter(
|
|
|
308
332
|
sessionId: string,
|
|
309
333
|
options?: { signal?: AbortSignal | undefined },
|
|
310
334
|
): Promise<AdkThreadSnapshot> => {
|
|
311
|
-
const headers = await getHeaders();
|
|
335
|
+
const headers = await getHeaders(options?.signal);
|
|
312
336
|
const res = await fetch(`${baseUrl}/${encodeURIComponent(sessionId)}`, {
|
|
313
337
|
headers,
|
|
314
338
|
...(options?.signal ? { signal: options.signal } : {}),
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { toAdkAuthReply } from "./adkAuthRequest";
|
|
3
|
+
import type { AdkAuthRequest } from "./types";
|
|
4
|
+
|
|
5
|
+
const CREDENTIAL_CALL = "adk-credential-1";
|
|
6
|
+
|
|
7
|
+
const apiKeyScheme = { type: "apiKey", in: "header", name: "x-api-key" };
|
|
8
|
+
|
|
9
|
+
const request = (authConfig: unknown): AdkAuthRequest => ({
|
|
10
|
+
toolCallId: CREDENTIAL_CALL,
|
|
11
|
+
authConfig,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const wire = (content: string) => JSON.parse(content);
|
|
15
|
+
|
|
16
|
+
describe("toAdkAuthReply", () => {
|
|
17
|
+
it("answers the credential call with the request's config carrying the credential", () => {
|
|
18
|
+
const reply = toAdkAuthReply(
|
|
19
|
+
CREDENTIAL_CALL,
|
|
20
|
+
{ authType: "apiKey", apiKey: "secret" },
|
|
21
|
+
[request({ authScheme: apiKeyScheme, credentialKey: "weather" })],
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
expect(reply).toMatchObject({
|
|
25
|
+
type: "tool",
|
|
26
|
+
tool_call_id: CREDENTIAL_CALL,
|
|
27
|
+
name: "adk_request_credential",
|
|
28
|
+
status: "success",
|
|
29
|
+
});
|
|
30
|
+
expect(wire(reply.content)).toEqual({
|
|
31
|
+
authScheme: apiKeyScheme,
|
|
32
|
+
credentialKey: "weather",
|
|
33
|
+
exchangedAuthCredential: { authType: "apiKey", apiKey: "secret" },
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("lays the OAuth2 answer over the exchanged credential the request issued", () => {
|
|
38
|
+
const oauth2Scheme = { type: "oauth2", flows: {} };
|
|
39
|
+
const reply = toAdkAuthReply(
|
|
40
|
+
CREDENTIAL_CALL,
|
|
41
|
+
{
|
|
42
|
+
authType: "oauth2",
|
|
43
|
+
oauth2: { authResponseUri: "https://app/cb?code=c&state=s" },
|
|
44
|
+
},
|
|
45
|
+
[
|
|
46
|
+
request({
|
|
47
|
+
authScheme: oauth2Scheme,
|
|
48
|
+
credentialKey: "calendar",
|
|
49
|
+
rawAuthCredential: { authType: "oauth2", oauth2: { clientId: "id" } },
|
|
50
|
+
exchangedAuthCredential: {
|
|
51
|
+
authType: "oauth2",
|
|
52
|
+
oauth2: {
|
|
53
|
+
clientId: "id",
|
|
54
|
+
authUri: "https://idp/authorize?state=s",
|
|
55
|
+
state: "s",
|
|
56
|
+
redirectUri: "https://app/cb",
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
}),
|
|
60
|
+
],
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
expect(wire(reply.content)).toEqual({
|
|
64
|
+
authScheme: oauth2Scheme,
|
|
65
|
+
credentialKey: "calendar",
|
|
66
|
+
rawAuthCredential: { authType: "oauth2", oauth2: { clientId: "id" } },
|
|
67
|
+
exchangedAuthCredential: {
|
|
68
|
+
authType: "oauth2",
|
|
69
|
+
oauth2: {
|
|
70
|
+
clientId: "id",
|
|
71
|
+
authUri: "https://idp/authorize?state=s",
|
|
72
|
+
state: "s",
|
|
73
|
+
redirectUri: "https://app/cb",
|
|
74
|
+
authResponseUri: "https://app/cb?code=c&state=s",
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("sends the credential alone when the request carried no config", () => {
|
|
81
|
+
const reply = toAdkAuthReply(
|
|
82
|
+
CREDENTIAL_CALL,
|
|
83
|
+
{ authType: "apiKey", apiKey: "secret" },
|
|
84
|
+
[request(undefined)],
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
expect(wire(reply.content)).toEqual({
|
|
88
|
+
exchangedAuthCredential: { authType: "apiKey", apiKey: "secret" },
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("throws for an id that is not a pending request", () => {
|
|
93
|
+
expect(() =>
|
|
94
|
+
toAdkAuthReply("adk-credential-2", { authType: "apiKey", apiKey: "x" }, [
|
|
95
|
+
request({ authScheme: apiKeyScheme, credentialKey: "weather" }),
|
|
96
|
+
]),
|
|
97
|
+
).toThrow(
|
|
98
|
+
'No pending ADK auth request for tool call id "adk-credential-2"',
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { generateId } from "@assistant-ui/core";
|
|
2
|
+
import { isRecord } from "@assistant-ui/core/internal";
|
|
3
|
+
import type { AdkAuthCredential, AdkAuthRequest, AdkMessage } from "./types";
|
|
4
|
+
|
|
5
|
+
export const ADK_REQUEST_CREDENTIAL = "adk_request_credential";
|
|
6
|
+
|
|
7
|
+
const recordOf = (value: unknown): Record<string, unknown> =>
|
|
8
|
+
isRecord(value) ? value : {};
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* ADK binds a credential reply to the `adk_request_credential` call it issued
|
|
12
|
+
* and takes only `exchangedAuthCredential` off the response, so the reply is
|
|
13
|
+
* the request's own auth config with the credential filled in, as ADK's
|
|
14
|
+
* reference client sends it: adk-python validates the response as a whole auth
|
|
15
|
+
* config, and it exchanges an OAuth2 code with the client id, redirect uri and
|
|
16
|
+
* state the request carried, so the caller's fields land over the request's
|
|
17
|
+
* exchanged credential rather than replacing it. An id that is not a pending
|
|
18
|
+
* request would send a reply adk-js ignores and adk-python answers by
|
|
19
|
+
* requesting again, so it throws instead.
|
|
20
|
+
*/
|
|
21
|
+
export const toAdkAuthReply = (
|
|
22
|
+
toolCallId: string,
|
|
23
|
+
credential: AdkAuthCredential,
|
|
24
|
+
requests: readonly AdkAuthRequest[],
|
|
25
|
+
): AdkMessage & { type: "tool" } => {
|
|
26
|
+
const request = requests.find((r) => r.toolCallId === toolCallId);
|
|
27
|
+
if (request === undefined)
|
|
28
|
+
throw new Error(
|
|
29
|
+
`No pending ADK auth request for tool call id "${toolCallId}"`,
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const authConfig = recordOf(request.authConfig);
|
|
33
|
+
const requested = recordOf(authConfig.exchangedAuthCredential);
|
|
34
|
+
const oauth2 = { ...recordOf(requested.oauth2), ...credential.oauth2 };
|
|
35
|
+
return {
|
|
36
|
+
id: generateId(),
|
|
37
|
+
type: "tool",
|
|
38
|
+
tool_call_id: toolCallId,
|
|
39
|
+
name: ADK_REQUEST_CREDENTIAL,
|
|
40
|
+
content: JSON.stringify({
|
|
41
|
+
...authConfig,
|
|
42
|
+
exchangedAuthCredential: {
|
|
43
|
+
...requested,
|
|
44
|
+
...credential,
|
|
45
|
+
...(Object.keys(oauth2).length > 0 && { oauth2 }),
|
|
46
|
+
},
|
|
47
|
+
}),
|
|
48
|
+
status: "success",
|
|
49
|
+
};
|
|
50
|
+
};
|
|
@@ -206,10 +206,6 @@ describe("projectAdkToolApprovals", () => {
|
|
|
206
206
|
]);
|
|
207
207
|
});
|
|
208
208
|
|
|
209
|
-
/**
|
|
210
|
-
* ADK Python spells the confirmation args in snake_case, which the event
|
|
211
|
-
* accumulator already reads both ways.
|
|
212
|
-
*/
|
|
213
209
|
it("gates the call named by a snake_case confirmation request", () => {
|
|
214
210
|
const { approvals } = projectAdkToolApprovals([
|
|
215
211
|
aiCall(GATED_CALL, "delete_file", { path: "/tmp/a" }),
|
package/src/adkToolApproval.ts
CHANGED
|
@@ -85,9 +85,7 @@ const sourceEventOf = (toolMessageId: string): string =>
|
|
|
85
85
|
toolMessageId.replace(/:\d+$/, "");
|
|
86
86
|
|
|
87
87
|
/**
|
|
88
|
-
* ADK builds the confirmation request around the call it gates, carrying that
|
|
89
|
-
* call verbatim in `originalFunctionCall` — spelled `original_function_call` by
|
|
90
|
-
* ADK Python, which the accumulator reads the same way.
|
|
88
|
+
* ADK builds the confirmation request around the call it gates, carrying that call verbatim in `originalFunctionCall`; `original_function_call` is accepted as well, matching the accumulator.
|
|
91
89
|
*/
|
|
92
90
|
const gatedCallIdOf = (args: unknown): string | undefined => {
|
|
93
91
|
if (typeof args !== "object" || args === null) return undefined;
|
|
@@ -13,6 +13,20 @@ describe("convertAdkMessage - human messages", () => {
|
|
|
13
13
|
});
|
|
14
14
|
});
|
|
15
15
|
|
|
16
|
+
it("returns empty content when content is neither a string nor an array", () => {
|
|
17
|
+
for (const content of [null, undefined, { text: "Hello" }]) {
|
|
18
|
+
const msg = {
|
|
19
|
+
id: "m1",
|
|
20
|
+
type: "human",
|
|
21
|
+
content,
|
|
22
|
+
} as unknown as AdkMessage;
|
|
23
|
+
expect(convertAdkMessage(msg, {})).toMatchObject({
|
|
24
|
+
role: "user",
|
|
25
|
+
content: [],
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
16
30
|
it("converts a human message with text content parts", () => {
|
|
17
31
|
const msg: AdkMessage = {
|
|
18
32
|
id: "m1",
|
|
@@ -128,6 +142,33 @@ describe("convertAdkMessage - human messages", () => {
|
|
|
128
142
|
});
|
|
129
143
|
|
|
130
144
|
describe("convertAdkMessage - ai messages", () => {
|
|
145
|
+
it("coerces missing text on text and reasoning parts to an empty string", () => {
|
|
146
|
+
const msg = {
|
|
147
|
+
id: "m1",
|
|
148
|
+
type: "ai",
|
|
149
|
+
content: [{ type: "text" }, { type: "reasoning" }],
|
|
150
|
+
} as AdkMessage;
|
|
151
|
+
expect(convertAdkMessage(msg, {})).toMatchObject({
|
|
152
|
+
role: "assistant",
|
|
153
|
+
content: [
|
|
154
|
+
{ type: "text", text: "" },
|
|
155
|
+
{ type: "reasoning", text: "" },
|
|
156
|
+
],
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("skips non-object entries in content arrays", () => {
|
|
161
|
+
const msg = {
|
|
162
|
+
id: "m1",
|
|
163
|
+
type: "ai",
|
|
164
|
+
content: [null, "Hello", { type: "text", text: "Hi" }],
|
|
165
|
+
} as unknown as AdkMessage;
|
|
166
|
+
expect(convertAdkMessage(msg, {})).toMatchObject({
|
|
167
|
+
role: "assistant",
|
|
168
|
+
content: [{ type: "text", text: "Hi" }],
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
131
172
|
it("converts an ai message with text content", () => {
|
|
132
173
|
const msg: AdkMessage = {
|
|
133
174
|
id: "m1",
|