@assistant-ui/react-google-adk 0.0.30 → 0.0.31
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 +2 -3
- package/dist/AdkClient.d.ts.map +1 -1
- package/dist/AdkEventAccumulator.d.ts +3 -5
- 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 +7 -4
- package/dist/AdkSessionAdapter.d.ts.map +1 -1
- package/dist/AdkSessionAdapter.js +7 -4
- package/dist/AdkSessionAdapter.js.map +1 -1
- package/dist/adkExtras.d.ts +1 -2
- package/dist/adkExtras.d.ts.map +1 -1
- package/dist/adkToolApproval.d.ts +6 -7
- 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 +1 -2
- package/dist/contentToParts.d.ts.map +1 -1
- package/dist/convertAdkMessages.d.ts +2 -3
- package/dist/convertAdkMessages.d.ts.map +1 -1
- package/dist/convertToAdkMessages.d.ts +5 -6
- package/dist/convertToAdkMessages.d.ts.map +1 -1
- package/dist/hooks.d.ts +15 -16
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +1 -1
- package/dist/normalizeAdkPart.d.ts +4 -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 +1 -2
- package/dist/parseAdkEvent.d.ts.map +1 -1
- package/dist/sdkIdentity.d.ts +1 -2
- package/dist/sdkIdentity.d.ts.map +1 -1
- package/dist/sdkIdentity.js +1 -1
- package/dist/server/adkEventStream.d.ts +2 -3
- package/dist/server/adkEventStream.d.ts.map +1 -1
- package/dist/server/createAdkApiRoute.d.ts +2 -3
- package/dist/server/createAdkApiRoute.d.ts.map +1 -1
- package/dist/server/parseAdkRequest.d.ts +2 -3
- package/dist/server/parseAdkRequest.d.ts.map +1 -1
- package/dist/structuredEvents.d.ts +1 -2
- 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 -3
- package/dist/toAdkFunctionResponse.d.ts.map +1 -1
- package/dist/trimTrailingSlashes.d.ts +1 -2
- package/dist/trimTrailingSlashes.d.ts.map +1 -1
- package/dist/types.d.ts +22 -23
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/useAdkMessages.d.ts +2 -3
- 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 +2 -3
- package/dist/useAdkRuntime.d.ts.map +1 -1
- package/dist/useAdkRuntime.js +1 -1
- package/package.json +10 -9
- package/src/AdkEventAccumulator.test.ts +312 -11
- package/src/AdkEventAccumulator.ts +68 -142
- package/src/AdkSessionAdapter.test.ts +267 -1
- package/src/AdkSessionAdapter.ts +24 -5
- package/src/adkToolApproval.test.ts +0 -4
- package/src/adkToolApproval.ts +1 -3
- package/src/normalizeAdkPart.ts +35 -0
- 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
|
|
|
@@ -375,6 +376,117 @@ describe("createAdkSessionAdapter - load", () => {
|
|
|
375
376
|
]);
|
|
376
377
|
});
|
|
377
378
|
|
|
379
|
+
it("loads valid events when history contains malformed media", async () => {
|
|
380
|
+
mockFetch.mockResolvedValueOnce(
|
|
381
|
+
new Response(
|
|
382
|
+
JSON.stringify({
|
|
383
|
+
id: "s1",
|
|
384
|
+
events: [
|
|
385
|
+
{
|
|
386
|
+
id: "user-1",
|
|
387
|
+
author: "user",
|
|
388
|
+
content: { parts: [{ text: "before" }] },
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
id: "bad-media",
|
|
392
|
+
author: "agent",
|
|
393
|
+
content: {
|
|
394
|
+
parts: [
|
|
395
|
+
{ inlineData: { data: "aGVsbG8=" } },
|
|
396
|
+
{ fileData: { mimeType: "image/png" } },
|
|
397
|
+
],
|
|
398
|
+
},
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
id: "agent-1",
|
|
402
|
+
author: "agent",
|
|
403
|
+
content: { parts: [{ text: "after" }] },
|
|
404
|
+
},
|
|
405
|
+
],
|
|
406
|
+
}),
|
|
407
|
+
{ status: 200 },
|
|
408
|
+
),
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
const { load } = createAdkSessionAdapter(baseOptions);
|
|
412
|
+
const result = await load("s1");
|
|
413
|
+
|
|
414
|
+
expect(result.messages).toMatchObject([
|
|
415
|
+
{ type: "human", content: "before" },
|
|
416
|
+
{ type: "ai", content: [{ type: "text", text: "after" }] },
|
|
417
|
+
]);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
it("loads valid events when history contains request calls without args", async () => {
|
|
421
|
+
mockFetch.mockResolvedValueOnce(
|
|
422
|
+
new Response(
|
|
423
|
+
JSON.stringify({
|
|
424
|
+
id: "s1",
|
|
425
|
+
events: [
|
|
426
|
+
{
|
|
427
|
+
id: "user-1",
|
|
428
|
+
author: "user",
|
|
429
|
+
content: { parts: [{ text: "before" }] },
|
|
430
|
+
},
|
|
431
|
+
{
|
|
432
|
+
id: "bad-requests",
|
|
433
|
+
author: "agent",
|
|
434
|
+
content: {
|
|
435
|
+
parts: [
|
|
436
|
+
{
|
|
437
|
+
functionCall: {
|
|
438
|
+
name: "adk_request_confirmation",
|
|
439
|
+
id: "rc-1",
|
|
440
|
+
},
|
|
441
|
+
},
|
|
442
|
+
{
|
|
443
|
+
functionCall: {
|
|
444
|
+
name: "adk_request_credential",
|
|
445
|
+
id: "rc-2",
|
|
446
|
+
},
|
|
447
|
+
},
|
|
448
|
+
],
|
|
449
|
+
},
|
|
450
|
+
},
|
|
451
|
+
{
|
|
452
|
+
id: "agent-1",
|
|
453
|
+
author: "agent",
|
|
454
|
+
content: { parts: [{ text: "after" }] },
|
|
455
|
+
},
|
|
456
|
+
],
|
|
457
|
+
}),
|
|
458
|
+
{ status: 200 },
|
|
459
|
+
),
|
|
460
|
+
);
|
|
461
|
+
|
|
462
|
+
const { load } = createAdkSessionAdapter(baseOptions);
|
|
463
|
+
const result = await load("s1");
|
|
464
|
+
|
|
465
|
+
expect(result.messages).toMatchObject([
|
|
466
|
+
{ type: "human", content: "before" },
|
|
467
|
+
{ type: "ai" },
|
|
468
|
+
{ type: "ai", content: [{ type: "text", text: "after" }] },
|
|
469
|
+
]);
|
|
470
|
+
expect(
|
|
471
|
+
(result.messages[1] as AdkMessage & { type: "ai" }).tool_calls,
|
|
472
|
+
).toEqual([
|
|
473
|
+
{
|
|
474
|
+
id: "rc-1",
|
|
475
|
+
name: "adk_request_confirmation",
|
|
476
|
+
args: {},
|
|
477
|
+
argsText: "{}",
|
|
478
|
+
},
|
|
479
|
+
{
|
|
480
|
+
id: "rc-2",
|
|
481
|
+
name: "adk_request_credential",
|
|
482
|
+
args: {},
|
|
483
|
+
argsText: "{}",
|
|
484
|
+
},
|
|
485
|
+
]);
|
|
486
|
+
expect(result.toolConfirmations).toMatchObject([{ toolCallId: "rc-1" }]);
|
|
487
|
+
expect(result.authRequests).toMatchObject([{ toolCallId: "rc-2" }]);
|
|
488
|
+
});
|
|
489
|
+
|
|
378
490
|
it("returns the per-turn state the events imply, not just the messages", async () => {
|
|
379
491
|
const session = {
|
|
380
492
|
id: "s1",
|
|
@@ -405,6 +517,85 @@ describe("createAdkSessionAdapter - load", () => {
|
|
|
405
517
|
expect(result.authRequests).toEqual([]);
|
|
406
518
|
});
|
|
407
519
|
|
|
520
|
+
it("reports only the requests the stored replies leave unanswered", async () => {
|
|
521
|
+
const session = {
|
|
522
|
+
id: "s1",
|
|
523
|
+
events: [
|
|
524
|
+
{
|
|
525
|
+
id: "e1",
|
|
526
|
+
author: "agent",
|
|
527
|
+
content: {
|
|
528
|
+
role: "model",
|
|
529
|
+
parts: [
|
|
530
|
+
{ functionCall: { name: "transfer", id: "gated-1", args: {} } },
|
|
531
|
+
{ functionCall: { name: "calendar", id: "gated-2", args: {} } },
|
|
532
|
+
],
|
|
533
|
+
},
|
|
534
|
+
},
|
|
535
|
+
{
|
|
536
|
+
id: "e2",
|
|
537
|
+
author: "agent",
|
|
538
|
+
longRunningToolIds: ["conf-1", "cred-1"],
|
|
539
|
+
actions: {
|
|
540
|
+
requestedToolConfirmations: { "gated-1": { hint: "Transfer?" } },
|
|
541
|
+
},
|
|
542
|
+
content: {
|
|
543
|
+
role: "user",
|
|
544
|
+
parts: [
|
|
545
|
+
{
|
|
546
|
+
functionCall: {
|
|
547
|
+
name: "adk_request_confirmation",
|
|
548
|
+
id: "conf-1",
|
|
549
|
+
args: {
|
|
550
|
+
originalFunctionCall: { id: "gated-1", name: "transfer" },
|
|
551
|
+
toolConfirmation: { hint: "Transfer?" },
|
|
552
|
+
},
|
|
553
|
+
},
|
|
554
|
+
},
|
|
555
|
+
{
|
|
556
|
+
functionCall: {
|
|
557
|
+
name: "adk_request_credential",
|
|
558
|
+
id: "cred-1",
|
|
559
|
+
args: {
|
|
560
|
+
functionCallId: "gated-2",
|
|
561
|
+
authConfig: { credentialKey: "k" },
|
|
562
|
+
},
|
|
563
|
+
},
|
|
564
|
+
},
|
|
565
|
+
],
|
|
566
|
+
},
|
|
567
|
+
},
|
|
568
|
+
{
|
|
569
|
+
id: "e3",
|
|
570
|
+
author: "user",
|
|
571
|
+
content: {
|
|
572
|
+
role: "user",
|
|
573
|
+
parts: [
|
|
574
|
+
{
|
|
575
|
+
functionResponse: {
|
|
576
|
+
name: "adk_request_confirmation",
|
|
577
|
+
id: "conf-1",
|
|
578
|
+
response: { confirmed: true },
|
|
579
|
+
},
|
|
580
|
+
},
|
|
581
|
+
],
|
|
582
|
+
},
|
|
583
|
+
},
|
|
584
|
+
],
|
|
585
|
+
};
|
|
586
|
+
mockFetch.mockResolvedValueOnce(
|
|
587
|
+
new Response(JSON.stringify(session), { status: 200 }),
|
|
588
|
+
);
|
|
589
|
+
|
|
590
|
+
const { load } = createAdkSessionAdapter(baseOptions);
|
|
591
|
+
const result = await load("s1");
|
|
592
|
+
|
|
593
|
+
expect(result.toolConfirmations).toEqual([]);
|
|
594
|
+
expect(result.authRequests).toEqual([
|
|
595
|
+
{ toolCallId: "cred-1", authConfig: { credentialKey: "k" } },
|
|
596
|
+
]);
|
|
597
|
+
});
|
|
598
|
+
|
|
408
599
|
it("passes an abort signal through to the request", async () => {
|
|
409
600
|
mockFetch.mockResolvedValueOnce(
|
|
410
601
|
new Response(JSON.stringify({ id: "s1", events: [] }), { status: 200 }),
|
|
@@ -727,6 +918,7 @@ describe("createAdkSessionAdapter - artifacts", () => {
|
|
|
727
918
|
"inline data",
|
|
728
919
|
{ inlineData: { mimeType: "image/png", data: "aGVsbG8=" } },
|
|
729
920
|
],
|
|
921
|
+
["file data", { fileData: { fileUri: "https://example.test/report.pdf" } }],
|
|
730
922
|
])("loads valid %s artifacts", async (_label, artifact) => {
|
|
731
923
|
mockFetch.mockResolvedValueOnce(
|
|
732
924
|
new Response(JSON.stringify(artifact), { status: 200 }),
|
|
@@ -737,6 +929,75 @@ describe("createAdkSessionAdapter - artifacts", () => {
|
|
|
737
929
|
await expect(artifacts.load("s1", "report.pdf")).resolves.toEqual(artifact);
|
|
738
930
|
});
|
|
739
931
|
|
|
932
|
+
it.each([
|
|
933
|
+
[
|
|
934
|
+
"inline data",
|
|
935
|
+
{ inline_data: { mime_type: "application/pdf", data: "aGVsbG8=" } },
|
|
936
|
+
{ inlineData: { mimeType: "application/pdf", data: "aGVsbG8=" } },
|
|
937
|
+
],
|
|
938
|
+
[
|
|
939
|
+
"file data",
|
|
940
|
+
{
|
|
941
|
+
file_data: {
|
|
942
|
+
mime_type: "application/pdf",
|
|
943
|
+
file_uri: "https://example.test/report.pdf",
|
|
944
|
+
},
|
|
945
|
+
},
|
|
946
|
+
{
|
|
947
|
+
fileData: {
|
|
948
|
+
mimeType: "application/pdf",
|
|
949
|
+
fileUri: "https://example.test/report.pdf",
|
|
950
|
+
},
|
|
951
|
+
},
|
|
952
|
+
],
|
|
953
|
+
])(
|
|
954
|
+
"normalizes snake_case %s artifact responses",
|
|
955
|
+
async (_label, value, expected) => {
|
|
956
|
+
mockFetch.mockResolvedValueOnce(
|
|
957
|
+
new Response(JSON.stringify(value), { status: 200 }),
|
|
958
|
+
);
|
|
959
|
+
|
|
960
|
+
const { artifacts } = createAdkSessionAdapter(baseOptions);
|
|
961
|
+
|
|
962
|
+
await expect(artifacts.load("s1", "report.pdf")).resolves.toMatchObject(
|
|
963
|
+
expected,
|
|
964
|
+
);
|
|
965
|
+
},
|
|
966
|
+
);
|
|
967
|
+
|
|
968
|
+
it("prefers camelCase artifact fields when both aliases are present", async () => {
|
|
969
|
+
mockFetch.mockResolvedValueOnce(
|
|
970
|
+
new Response(
|
|
971
|
+
JSON.stringify({
|
|
972
|
+
inlineData: {
|
|
973
|
+
mimeType: "image/png",
|
|
974
|
+
mime_type: "application/pdf",
|
|
975
|
+
data: "right",
|
|
976
|
+
},
|
|
977
|
+
inline_data: { mime_type: "text/plain", data: "wrong" },
|
|
978
|
+
fileData: {
|
|
979
|
+
mimeType: "application/pdf",
|
|
980
|
+
mime_type: "image/png",
|
|
981
|
+
fileUri: "https://example.test/right.pdf",
|
|
982
|
+
file_uri: "https://example.test/wrong.png",
|
|
983
|
+
},
|
|
984
|
+
file_data: { file_uri: "https://example.test/other.png" },
|
|
985
|
+
}),
|
|
986
|
+
{ status: 200 },
|
|
987
|
+
),
|
|
988
|
+
);
|
|
989
|
+
|
|
990
|
+
const { artifacts } = createAdkSessionAdapter(baseOptions);
|
|
991
|
+
|
|
992
|
+
await expect(artifacts.load("s1", "report.pdf")).resolves.toMatchObject({
|
|
993
|
+
inlineData: { mimeType: "image/png", data: "right" },
|
|
994
|
+
fileData: {
|
|
995
|
+
mimeType: "application/pdf",
|
|
996
|
+
fileUri: "https://example.test/right.pdf",
|
|
997
|
+
},
|
|
998
|
+
});
|
|
999
|
+
});
|
|
1000
|
+
|
|
740
1001
|
it("rejects an artifact without supported content", async () => {
|
|
741
1002
|
mockFetch.mockResolvedValueOnce(
|
|
742
1003
|
new Response(JSON.stringify({}), { status: 200 }),
|
|
@@ -745,7 +1006,7 @@ describe("createAdkSessionAdapter - artifacts", () => {
|
|
|
745
1006
|
const { artifacts } = createAdkSessionAdapter(baseOptions);
|
|
746
1007
|
|
|
747
1008
|
await expect(artifacts.load("s1", "report.pdf")).rejects.toThrow(
|
|
748
|
-
'Invalid ADK artifact load response: expected an object containing "text" or "
|
|
1009
|
+
'Invalid ADK artifact load response: expected an object containing "text", "inlineData", or "fileData".',
|
|
749
1010
|
);
|
|
750
1011
|
});
|
|
751
1012
|
|
|
@@ -760,6 +1021,11 @@ describe("createAdkSessionAdapter - artifacts", () => {
|
|
|
760
1021
|
{ inlineData: { mimeType: "image/png" } },
|
|
761
1022
|
'Invalid ADK artifact load response: "inlineData" must contain string "mimeType" and "data" fields.',
|
|
762
1023
|
],
|
|
1024
|
+
[
|
|
1025
|
+
"file data",
|
|
1026
|
+
{ fileData: { mimeType: "application/pdf" } },
|
|
1027
|
+
'Invalid ADK artifact load response: "fileData" must contain a string "fileUri" and an optional string "mimeType" field.',
|
|
1028
|
+
],
|
|
763
1029
|
])("rejects malformed %s artifact content", async (_label, value, error) => {
|
|
764
1030
|
mockFetch.mockResolvedValueOnce(
|
|
765
1031
|
new Response(JSON.stringify(value), { status: 200 }),
|
package/src/AdkSessionAdapter.ts
CHANGED
|
@@ -6,6 +6,7 @@ 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";
|
|
@@ -37,6 +38,7 @@ export type AdkSessionAdapterOptions = {
|
|
|
37
38
|
|
|
38
39
|
export type AdkArtifactData = {
|
|
39
40
|
inlineData?: { mimeType: string; data: string } | undefined;
|
|
41
|
+
fileData?: { fileUri: string; mimeType?: string | undefined } | undefined;
|
|
40
42
|
text?: string | undefined;
|
|
41
43
|
};
|
|
42
44
|
|
|
@@ -133,14 +135,19 @@ const parseAdkArtifactListResponse = (value: unknown): string[] => {
|
|
|
133
135
|
const parseAdkArtifactResponse = (value: unknown): AdkArtifactData => {
|
|
134
136
|
if (!isRecord(value)) {
|
|
135
137
|
throw new Error(
|
|
136
|
-
'Invalid ADK artifact load response: expected an object containing "text" or "
|
|
138
|
+
'Invalid ADK artifact load response: expected an object containing "text", "inlineData", or "fileData".',
|
|
137
139
|
);
|
|
138
140
|
}
|
|
139
141
|
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
+
const normalizedValue = normalizeAdkPart(value);
|
|
143
|
+
const { text, inlineData, fileData } = normalizedValue;
|
|
144
|
+
if (
|
|
145
|
+
text === undefined &&
|
|
146
|
+
inlineData === undefined &&
|
|
147
|
+
fileData === undefined
|
|
148
|
+
) {
|
|
142
149
|
throw new Error(
|
|
143
|
-
'Invalid ADK artifact load response: expected an object containing "text" or "
|
|
150
|
+
'Invalid ADK artifact load response: expected an object containing "text", "inlineData", or "fileData".',
|
|
144
151
|
);
|
|
145
152
|
}
|
|
146
153
|
|
|
@@ -161,7 +168,19 @@ const parseAdkArtifactResponse = (value: unknown): AdkArtifactData => {
|
|
|
161
168
|
);
|
|
162
169
|
}
|
|
163
170
|
|
|
164
|
-
|
|
171
|
+
if (
|
|
172
|
+
fileData !== undefined &&
|
|
173
|
+
(!isRecord(fileData) ||
|
|
174
|
+
typeof fileData.fileUri !== "string" ||
|
|
175
|
+
(fileData.mimeType !== undefined &&
|
|
176
|
+
typeof fileData.mimeType !== "string"))
|
|
177
|
+
) {
|
|
178
|
+
throw new Error(
|
|
179
|
+
'Invalid ADK artifact load response: "fileData" must contain a string "fileUri" and an optional string "mimeType" field.',
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return normalizedValue as AdkArtifactData;
|
|
165
184
|
};
|
|
166
185
|
|
|
167
186
|
const parseAdkArtifactVersionsResponse = (value: unknown): number[] => {
|
|
@@ -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;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { isRecord } from "@assistant-ui/core/internal";
|
|
2
|
+
|
|
3
|
+
export const normalizeAdkPart = (
|
|
4
|
+
part: Record<string, unknown>,
|
|
5
|
+
): Record<string, unknown> => {
|
|
6
|
+
const result: Record<string, unknown> = { ...part };
|
|
7
|
+
if ("function_call" in part && !("functionCall" in part))
|
|
8
|
+
result.functionCall = part.function_call;
|
|
9
|
+
if ("function_response" in part && !("functionResponse" in part))
|
|
10
|
+
result.functionResponse = part.function_response;
|
|
11
|
+
if ("inline_data" in part && !("inlineData" in part))
|
|
12
|
+
result.inlineData = part.inline_data;
|
|
13
|
+
if ("file_data" in part && !("fileData" in part))
|
|
14
|
+
result.fileData = part.file_data;
|
|
15
|
+
if (isRecord(result.inlineData)) {
|
|
16
|
+
const data = result.inlineData;
|
|
17
|
+
if ("mime_type" in data && !("mimeType" in data))
|
|
18
|
+
result.inlineData = { ...data, mimeType: data.mime_type };
|
|
19
|
+
}
|
|
20
|
+
if (isRecord(result.fileData)) {
|
|
21
|
+
const data = result.fileData;
|
|
22
|
+
result.fileData = {
|
|
23
|
+
...data,
|
|
24
|
+
...("mime_type" in data &&
|
|
25
|
+
!("mimeType" in data) && { mimeType: data.mime_type }),
|
|
26
|
+
...("file_uri" in data &&
|
|
27
|
+
!("fileUri" in data) && { fileUri: data.file_uri }),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
if ("executable_code" in part && !("executableCode" in part))
|
|
31
|
+
result.executableCode = part.executable_code;
|
|
32
|
+
if ("code_execution_result" in part && !("codeExecutionResult" in part))
|
|
33
|
+
result.codeExecutionResult = part.code_execution_result;
|
|
34
|
+
return result;
|
|
35
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { toAdkStructuredEvents } from "./structuredEvents";
|
|
3
|
+
import type { AdkEvent } from "./types";
|
|
4
|
+
|
|
5
|
+
const makeEvent = (overrides: Partial<AdkEvent> = {}): AdkEvent => ({
|
|
6
|
+
id: "evt-1",
|
|
7
|
+
...overrides,
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
describe("toAdkStructuredEvents", () => {
|
|
11
|
+
it("converts a function call with args", () => {
|
|
12
|
+
expect(
|
|
13
|
+
toAdkStructuredEvents(
|
|
14
|
+
makeEvent({
|
|
15
|
+
author: "agent",
|
|
16
|
+
content: {
|
|
17
|
+
role: "model",
|
|
18
|
+
parts: [
|
|
19
|
+
{
|
|
20
|
+
functionCall: { name: "search", id: "tc-1", args: { q: "x" } },
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
},
|
|
24
|
+
}),
|
|
25
|
+
),
|
|
26
|
+
).toEqual([
|
|
27
|
+
{
|
|
28
|
+
type: "tool_call",
|
|
29
|
+
call: { name: "search", id: "tc-1", args: { q: "x" } },
|
|
30
|
+
},
|
|
31
|
+
]);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("defaults a function call without args to empty args", () => {
|
|
35
|
+
expect(
|
|
36
|
+
toAdkStructuredEvents(
|
|
37
|
+
makeEvent({
|
|
38
|
+
author: "agent",
|
|
39
|
+
content: {
|
|
40
|
+
role: "model",
|
|
41
|
+
parts: [{ functionCall: { name: "search", id: "tc-1" } }],
|
|
42
|
+
},
|
|
43
|
+
}),
|
|
44
|
+
),
|
|
45
|
+
).toEqual([
|
|
46
|
+
{ type: "tool_call", call: { name: "search", id: "tc-1", args: {} } },
|
|
47
|
+
]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("keeps the other parts of an event carrying a call without args", () => {
|
|
51
|
+
expect(
|
|
52
|
+
toAdkStructuredEvents(
|
|
53
|
+
makeEvent({
|
|
54
|
+
author: "agent",
|
|
55
|
+
content: {
|
|
56
|
+
role: "model",
|
|
57
|
+
parts: [
|
|
58
|
+
{ functionCall: { name: "search" } },
|
|
59
|
+
{ text: "still here" },
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
}),
|
|
63
|
+
),
|
|
64
|
+
).toEqual([
|
|
65
|
+
{ type: "tool_call", call: { name: "search", args: {} } },
|
|
66
|
+
{ type: "content", content: "still here" },
|
|
67
|
+
]);
|
|
68
|
+
});
|
|
69
|
+
});
|
package/src/structuredEvents.ts
CHANGED
|
@@ -28,7 +28,10 @@ export function toAdkStructuredEvents(event: AdkEvent): AdkStructuredEvent[] {
|
|
|
28
28
|
if (part.functionCall) {
|
|
29
29
|
const call: AdkStructuredEvent & { type: "tool_call" } = {
|
|
30
30
|
type: "tool_call",
|
|
31
|
-
call: {
|
|
31
|
+
call: {
|
|
32
|
+
name: part.functionCall.name,
|
|
33
|
+
args: part.functionCall.args ?? {},
|
|
34
|
+
},
|
|
32
35
|
};
|
|
33
36
|
if (part.functionCall.id != null) call.call.id = part.functionCall.id;
|
|
34
37
|
result.push(call);
|
package/src/types.ts
CHANGED
|
@@ -17,7 +17,6 @@ import {
|
|
|
17
17
|
messageToEvent,
|
|
18
18
|
messagesToEvents,
|
|
19
19
|
useAdkMessages,
|
|
20
|
-
type UseAdkMessagesOptions,
|
|
21
20
|
} from "./useAdkMessages";
|
|
22
21
|
import { projectAdkToolApprovals } from "./adkToolApproval";
|
|
23
22
|
import { createAdkStream } from "./AdkClient";
|
|
@@ -113,7 +112,7 @@ describe("ADK runtime callbacks", () => {
|
|
|
113
112
|
};
|
|
114
113
|
const eventHandlers = {
|
|
115
114
|
[callbackName]: callback,
|
|
116
|
-
}
|
|
115
|
+
};
|
|
117
116
|
const { result } = renderHook(() =>
|
|
118
117
|
useAdkMessages({ stream, eventHandlers }),
|
|
119
118
|
);
|
|
@@ -466,6 +465,10 @@ describe("optimistic confirmation replies", () => {
|
|
|
466
465
|
projectAdkToolApprovals(result.current.messages).approvals.values(),
|
|
467
466
|
),
|
|
468
467
|
]).toEqual([{ id: "conf-a" }, { id: "conf-b" }]);
|
|
468
|
+
expect(result.current.toolConfirmations.map((c) => c.toolCallId)).toEqual([
|
|
469
|
+
"conf-a",
|
|
470
|
+
"conf-b",
|
|
471
|
+
]);
|
|
469
472
|
});
|
|
470
473
|
|
|
471
474
|
it("keeps both gates pending when an ai message sits between the replies", async () => {
|
|
@@ -522,6 +525,102 @@ describe("optimistic confirmation replies", () => {
|
|
|
522
525
|
projectAdkToolApprovals(result.current.messages).approvals.values(),
|
|
523
526
|
),
|
|
524
527
|
]).toEqual([{ id: "conf-a", approved: true }, { id: "conf-b" }]);
|
|
528
|
+
expect(result.current.toolConfirmations.map((c) => c.toolCallId)).toEqual([
|
|
529
|
+
"conf-b",
|
|
530
|
+
]);
|
|
531
|
+
});
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
describe("pending requests across sends", () => {
|
|
535
|
+
it("keeps an unanswered request listed across sends until its reply is sent", async () => {
|
|
536
|
+
let run = 0;
|
|
537
|
+
const stream: AdkStreamCallback = async function* () {
|
|
538
|
+
run += 1;
|
|
539
|
+
if (run === 1) {
|
|
540
|
+
yield {
|
|
541
|
+
id: "requests",
|
|
542
|
+
author: "agent",
|
|
543
|
+
longRunningToolIds: ["conf-1", "cred-1"],
|
|
544
|
+
content: {
|
|
545
|
+
role: "model",
|
|
546
|
+
parts: [
|
|
547
|
+
{
|
|
548
|
+
functionCall: {
|
|
549
|
+
id: "conf-1",
|
|
550
|
+
name: "adk_request_confirmation",
|
|
551
|
+
args: {
|
|
552
|
+
originalFunctionCall: { id: "gated-1", name: "transfer" },
|
|
553
|
+
toolConfirmation: { hint: "Transfer?" },
|
|
554
|
+
},
|
|
555
|
+
},
|
|
556
|
+
},
|
|
557
|
+
{
|
|
558
|
+
functionCall: {
|
|
559
|
+
id: "cred-1",
|
|
560
|
+
name: "adk_request_credential",
|
|
561
|
+
args: {
|
|
562
|
+
function_call_id: "gated-2",
|
|
563
|
+
auth_config: { credentialKey: "k" },
|
|
564
|
+
},
|
|
565
|
+
},
|
|
566
|
+
},
|
|
567
|
+
],
|
|
568
|
+
},
|
|
569
|
+
} satisfies AdkEvent;
|
|
570
|
+
} else if (run === 2) {
|
|
571
|
+
yield {
|
|
572
|
+
id: "answer",
|
|
573
|
+
author: "agent",
|
|
574
|
+
content: { role: "model", parts: [{ text: "still waiting" }] },
|
|
575
|
+
} satisfies AdkEvent;
|
|
576
|
+
}
|
|
577
|
+
};
|
|
578
|
+
const { result } = renderHook(() => useAdkMessages({ stream }));
|
|
579
|
+
const pending = () => ({
|
|
580
|
+
confirmations: result.current.toolConfirmations.map((c) => c.toolCallId),
|
|
581
|
+
authRequests: result.current.authRequests.map((r) => r.toolCallId),
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
await act(async () => {
|
|
585
|
+
await result.current.sendMessage(
|
|
586
|
+
[{ id: "user-1", type: "human", content: "start" }],
|
|
587
|
+
{},
|
|
588
|
+
);
|
|
589
|
+
});
|
|
590
|
+
expect(pending()).toEqual({
|
|
591
|
+
confirmations: ["conf-1"],
|
|
592
|
+
authRequests: ["cred-1"],
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
await act(async () => {
|
|
596
|
+
await result.current.sendMessage(
|
|
597
|
+
[{ id: "user-2", type: "human", content: "any news?" }],
|
|
598
|
+
{},
|
|
599
|
+
);
|
|
600
|
+
});
|
|
601
|
+
expect(pending()).toEqual({
|
|
602
|
+
confirmations: ["conf-1"],
|
|
603
|
+
authRequests: ["cred-1"],
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
await act(async () => {
|
|
607
|
+
await result.current.sendMessage(
|
|
608
|
+
[
|
|
609
|
+
{
|
|
610
|
+
id: "reply",
|
|
611
|
+
type: "tool",
|
|
612
|
+
tool_call_id: "cred-1",
|
|
613
|
+
name: "adk_request_credential",
|
|
614
|
+
content: JSON.stringify({
|
|
615
|
+
exchangedAuthCredential: { authType: "apiKey" },
|
|
616
|
+
}),
|
|
617
|
+
status: "success",
|
|
618
|
+
},
|
|
619
|
+
],
|
|
620
|
+
{},
|
|
621
|
+
);
|
|
622
|
+
});
|
|
623
|
+
expect(pending()).toEqual({ confirmations: ["conf-1"], authRequests: [] });
|
|
525
624
|
});
|
|
526
625
|
});
|
|
527
626
|
|
|
@@ -680,7 +779,7 @@ describe("messageToEvent (contentToParts)", () => {
|
|
|
680
779
|
name: "search",
|
|
681
780
|
});
|
|
682
781
|
|
|
683
|
-
expect(event.content?.parts[0]?.functionResponse?.response).toEqual(
|
|
782
|
+
expect(event.content?.parts?.[0]?.functionResponse?.response).toEqual(
|
|
684
783
|
response,
|
|
685
784
|
);
|
|
686
785
|
},
|
package/src/useAdkMessages.ts
CHANGED
|
@@ -170,6 +170,8 @@ export const useAdkMessages = ({
|
|
|
170
170
|
}
|
|
171
171
|
setMessagesImmediate(accumulator.getMessages());
|
|
172
172
|
setLongRunningToolIds(accumulator.getLongRunningToolIds());
|
|
173
|
+
setToolConfirmations(accumulator.getToolConfirmations());
|
|
174
|
+
setAuthRequests(accumulator.getAuthRequests());
|
|
173
175
|
|
|
174
176
|
// Google ADK replaces active runs, while React LangGraph queues sends.
|
|
175
177
|
abortControllerRef.current?.abort();
|