@assistant-ui/react-google-adk 0.0.29 → 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/AdkClient.js +2 -1
- package/dist/AdkClient.js.map +1 -1
- package/dist/AdkEventAccumulator.d.ts +4 -6
- package/dist/AdkEventAccumulator.d.ts.map +1 -1
- package/dist/AdkEventAccumulator.js +80 -95
- 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 +15 -7
- package/dist/convertToAdkMessages.d.ts.map +1 -1
- package/dist/convertToAdkMessages.js +20 -6
- package/dist/convertToAdkMessages.js.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 +5 -0
- package/dist/sdkIdentity.d.ts.map +1 -0
- package/dist/sdkIdentity.js +9 -0
- package/dist/sdkIdentity.js.map +1 -0
- 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/server/parseAdkRequest.js +2 -1
- package/dist/server/parseAdkRequest.js.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 +5 -0
- package/dist/toAdkFunctionResponse.d.ts.map +1 -0
- package/dist/toAdkFunctionResponse.js +11 -0
- package/dist/toAdkFunctionResponse.js.map +1 -0
- 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 +16 -5
- 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 +3 -1
- package/dist/useAdkRuntime.js.map +1 -1
- package/package.json +10 -9
- package/src/AdkClient.test.ts +139 -2
- package/src/AdkClient.ts +2 -1
- package/src/AdkEventAccumulator.test.ts +588 -11
- package/src/AdkEventAccumulator.ts +85 -130
- package/src/AdkSessionAdapter.test.ts +355 -1
- package/src/AdkSessionAdapter.ts +24 -5
- package/src/adkToolApproval.test.ts +0 -4
- package/src/adkToolApproval.ts +1 -3
- package/src/convertToAdkMessages.test.ts +70 -0
- package/src/convertToAdkMessages.ts +19 -4
- package/src/normalizeAdkPart.ts +35 -0
- package/src/sdkIdentity.ts +9 -0
- package/src/server/parseAdkRequest.test.ts +63 -0
- package/src/server/parseAdkRequest.ts +2 -1
- package/src/structuredEvents.test.ts +69 -0
- package/src/structuredEvents.ts +4 -1
- package/src/toAdkFunctionResponse.test.ts +46 -0
- package/src/toAdkFunctionResponse.ts +18 -0
- package/src/types.ts +1 -1
- package/src/useAdkMessages.test.ts +249 -2
- package/src/useAdkMessages.ts +24 -6
- package/src/useAdkRuntime.replacement.test.tsx +11 -7
- package/src/useAdkRuntime.ts +2 -0
- package/src/useAdkRuntimeApproval.test.tsx +0 -1
|
@@ -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
|
|
|
@@ -294,6 +295,198 @@ describe("createAdkSessionAdapter - fetch", () => {
|
|
|
294
295
|
// ── load() ──
|
|
295
296
|
|
|
296
297
|
describe("createAdkSessionAdapter - load", () => {
|
|
298
|
+
it("restores tool failures from stored function responses", async () => {
|
|
299
|
+
mockFetch.mockResolvedValueOnce(
|
|
300
|
+
new Response(
|
|
301
|
+
JSON.stringify({
|
|
302
|
+
id: "s1",
|
|
303
|
+
events: [
|
|
304
|
+
{
|
|
305
|
+
id: "failed",
|
|
306
|
+
author: "user",
|
|
307
|
+
content: {
|
|
308
|
+
parts: [
|
|
309
|
+
{
|
|
310
|
+
functionResponse: {
|
|
311
|
+
id: "tc-1",
|
|
312
|
+
name: "search",
|
|
313
|
+
response: { error: "denied" },
|
|
314
|
+
},
|
|
315
|
+
},
|
|
316
|
+
],
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
],
|
|
320
|
+
}),
|
|
321
|
+
{ status: 200 },
|
|
322
|
+
),
|
|
323
|
+
);
|
|
324
|
+
const { load } = createAdkSessionAdapter(baseOptions);
|
|
325
|
+
const result = await load("s1");
|
|
326
|
+
expect(result.messages).toMatchObject([
|
|
327
|
+
{
|
|
328
|
+
type: "tool",
|
|
329
|
+
tool_call_id: "tc-1",
|
|
330
|
+
status: "error",
|
|
331
|
+
content: JSON.stringify({ error: "denied" }),
|
|
332
|
+
},
|
|
333
|
+
]);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it("restores snake_case image and file parts from session history", async () => {
|
|
337
|
+
mockFetch.mockResolvedValueOnce(
|
|
338
|
+
new Response(
|
|
339
|
+
JSON.stringify({
|
|
340
|
+
id: "s1",
|
|
341
|
+
events: [
|
|
342
|
+
{
|
|
343
|
+
id: "media",
|
|
344
|
+
author: "user",
|
|
345
|
+
content: {
|
|
346
|
+
parts: [
|
|
347
|
+
{ inline_data: { mime_type: "image/png", data: "aGVsbG8=" } },
|
|
348
|
+
{
|
|
349
|
+
file_data: {
|
|
350
|
+
mime_type: "application/pdf",
|
|
351
|
+
file_uri: "https://example.test/report.pdf",
|
|
352
|
+
},
|
|
353
|
+
},
|
|
354
|
+
],
|
|
355
|
+
},
|
|
356
|
+
},
|
|
357
|
+
],
|
|
358
|
+
}),
|
|
359
|
+
{ status: 200 },
|
|
360
|
+
),
|
|
361
|
+
);
|
|
362
|
+
const { load } = createAdkSessionAdapter(baseOptions);
|
|
363
|
+
const result = await load("s1");
|
|
364
|
+
expect(result.messages).toMatchObject([
|
|
365
|
+
{
|
|
366
|
+
type: "human",
|
|
367
|
+
content: [
|
|
368
|
+
{ type: "image", mimeType: "image/png", data: "aGVsbG8=" },
|
|
369
|
+
{
|
|
370
|
+
type: "file_url",
|
|
371
|
+
mimeType: "application/pdf",
|
|
372
|
+
url: "https://example.test/report.pdf",
|
|
373
|
+
},
|
|
374
|
+
],
|
|
375
|
+
},
|
|
376
|
+
]);
|
|
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
|
+
|
|
297
490
|
it("returns the per-turn state the events imply, not just the messages", async () => {
|
|
298
491
|
const session = {
|
|
299
492
|
id: "s1",
|
|
@@ -324,6 +517,85 @@ describe("createAdkSessionAdapter - load", () => {
|
|
|
324
517
|
expect(result.authRequests).toEqual([]);
|
|
325
518
|
});
|
|
326
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
|
+
|
|
327
599
|
it("passes an abort signal through to the request", async () => {
|
|
328
600
|
mockFetch.mockResolvedValueOnce(
|
|
329
601
|
new Response(JSON.stringify({ id: "s1", events: [] }), { status: 200 }),
|
|
@@ -543,10 +815,17 @@ describe("createAdkSessionAdapter - load replays tool confirmations", () => {
|
|
|
543
815
|
const result = await load("s1");
|
|
544
816
|
return {
|
|
545
817
|
messages: result.messages,
|
|
818
|
+
longRunningToolIds: result.longRunningToolIds,
|
|
546
819
|
approvals: projectAdkToolApprovals(result.messages).approvals,
|
|
547
820
|
};
|
|
548
821
|
};
|
|
549
822
|
|
|
823
|
+
it("settles an answered long-running tool on replay", async () => {
|
|
824
|
+
const { longRunningToolIds } = await loadApprovals({ confirmed: true });
|
|
825
|
+
|
|
826
|
+
expect(longRunningToolIds).toEqual([]);
|
|
827
|
+
});
|
|
828
|
+
|
|
550
829
|
it("keeps a user-authored confirmation reply as a tool message", async () => {
|
|
551
830
|
const { messages } = await loadApprovals({ confirmed: true });
|
|
552
831
|
|
|
@@ -639,6 +918,7 @@ describe("createAdkSessionAdapter - artifacts", () => {
|
|
|
639
918
|
"inline data",
|
|
640
919
|
{ inlineData: { mimeType: "image/png", data: "aGVsbG8=" } },
|
|
641
920
|
],
|
|
921
|
+
["file data", { fileData: { fileUri: "https://example.test/report.pdf" } }],
|
|
642
922
|
])("loads valid %s artifacts", async (_label, artifact) => {
|
|
643
923
|
mockFetch.mockResolvedValueOnce(
|
|
644
924
|
new Response(JSON.stringify(artifact), { status: 200 }),
|
|
@@ -649,6 +929,75 @@ describe("createAdkSessionAdapter - artifacts", () => {
|
|
|
649
929
|
await expect(artifacts.load("s1", "report.pdf")).resolves.toEqual(artifact);
|
|
650
930
|
});
|
|
651
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
|
+
|
|
652
1001
|
it("rejects an artifact without supported content", async () => {
|
|
653
1002
|
mockFetch.mockResolvedValueOnce(
|
|
654
1003
|
new Response(JSON.stringify({}), { status: 200 }),
|
|
@@ -657,7 +1006,7 @@ describe("createAdkSessionAdapter - artifacts", () => {
|
|
|
657
1006
|
const { artifacts } = createAdkSessionAdapter(baseOptions);
|
|
658
1007
|
|
|
659
1008
|
await expect(artifacts.load("s1", "report.pdf")).rejects.toThrow(
|
|
660
|
-
'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".',
|
|
661
1010
|
);
|
|
662
1011
|
});
|
|
663
1012
|
|
|
@@ -672,6 +1021,11 @@ describe("createAdkSessionAdapter - artifacts", () => {
|
|
|
672
1021
|
{ inlineData: { mimeType: "image/png" } },
|
|
673
1022
|
'Invalid ADK artifact load response: "inlineData" must contain string "mimeType" and "data" fields.',
|
|
674
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
|
+
],
|
|
675
1029
|
])("rejects malformed %s artifact content", async (_label, value, error) => {
|
|
676
1030
|
mockFetch.mockResolvedValueOnce(
|
|
677
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;
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
import { convertAdkMessage } from "./convertAdkMessages";
|
|
8
8
|
import type { AppendMessage } from "@assistant-ui/core";
|
|
9
9
|
import type { AdkMessage } from "./types";
|
|
10
|
+
import { contentToParts } from "./contentToParts";
|
|
10
11
|
|
|
11
12
|
const makeAppendMessage = (content: AppendMessage["content"]): AppendMessage =>
|
|
12
13
|
({
|
|
@@ -155,6 +156,75 @@ describe("getPendingCancellations", () => {
|
|
|
155
156
|
});
|
|
156
157
|
|
|
157
158
|
describe("getMessageContent", () => {
|
|
159
|
+
it("serializes data URL images as inline data", () => {
|
|
160
|
+
const content = getMessageContent(
|
|
161
|
+
makeAppendMessage([
|
|
162
|
+
{ type: "image", image: "data:image/png;base64,AAAA" },
|
|
163
|
+
]),
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
expect(contentToParts(content)).toEqual([
|
|
167
|
+
{ inlineData: { mimeType: "image/png", data: "AAAA" } },
|
|
168
|
+
]);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("infers an image MIME type when the data URL declares a generic type", () => {
|
|
172
|
+
const content = getMessageContent(
|
|
173
|
+
makeAppendMessage([
|
|
174
|
+
{
|
|
175
|
+
type: "image",
|
|
176
|
+
image: "data:application/octet-stream;base64,iVBORw0KGgo=",
|
|
177
|
+
},
|
|
178
|
+
]),
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
expect(contentToParts(content)).toEqual([
|
|
182
|
+
{ inlineData: { mimeType: "image/png", data: "iVBORw0KGgo=" } },
|
|
183
|
+
]);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("prefers an attachment's declared image MIME type", () => {
|
|
187
|
+
const message = makeAppendMessage([]);
|
|
188
|
+
const content = getMessageContent({
|
|
189
|
+
...message,
|
|
190
|
+
attachments: [
|
|
191
|
+
{
|
|
192
|
+
id: "attachment-1",
|
|
193
|
+
type: "image",
|
|
194
|
+
name: "photo.webp",
|
|
195
|
+
contentType: "image/webp",
|
|
196
|
+
status: { type: "complete" },
|
|
197
|
+
content: [{ type: "image", image: "data:image/png;base64,AAAA" }],
|
|
198
|
+
},
|
|
199
|
+
],
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
expect(contentToParts(content)).toEqual([
|
|
203
|
+
{ inlineData: { mimeType: "image/webp", data: "AAAA" } },
|
|
204
|
+
]);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("resolves wildcard attachment MIME types to a concrete image type", () => {
|
|
208
|
+
const message = makeAppendMessage([]);
|
|
209
|
+
const content = getMessageContent({
|
|
210
|
+
...message,
|
|
211
|
+
attachments: [
|
|
212
|
+
{
|
|
213
|
+
id: "attachment-1",
|
|
214
|
+
type: "image",
|
|
215
|
+
name: "photo.jpg",
|
|
216
|
+
contentType: "image/*",
|
|
217
|
+
status: { type: "complete" },
|
|
218
|
+
content: [{ type: "image", image: "data:image/jpeg;base64,AAAA" }],
|
|
219
|
+
},
|
|
220
|
+
],
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
expect(contentToParts(content)).toEqual([
|
|
224
|
+
{ inlineData: { mimeType: "image/jpeg", data: "AAAA" } },
|
|
225
|
+
]);
|
|
226
|
+
});
|
|
227
|
+
|
|
158
228
|
it("preserves file part data and mimeType end-to-end", () => {
|
|
159
229
|
const result = getMessageContent(
|
|
160
230
|
makeAppendMessage([
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
createToolCallCancellationStub,
|
|
9
9
|
parseDataUrl,
|
|
10
10
|
resolveFilePartSource,
|
|
11
|
+
resolveImageMediaType,
|
|
11
12
|
scanPendingToolCalls,
|
|
12
13
|
} from "@assistant-ui/core/internal";
|
|
13
14
|
import type { AdkMessage } from "./types";
|
|
@@ -15,16 +16,30 @@ import type { AdkMessage } from "./types";
|
|
|
15
16
|
/** Exported for unit tests. */
|
|
16
17
|
export const getMessageContent = (msg: AppendMessage) => {
|
|
17
18
|
const allContent = [
|
|
18
|
-
...msg.content,
|
|
19
|
-
...(msg.attachments?.flatMap((
|
|
19
|
+
...msg.content.map((part) => ({ part, contentType: undefined })),
|
|
20
|
+
...(msg.attachments?.flatMap((attachment) =>
|
|
21
|
+
attachment.content.map((part) => ({
|
|
22
|
+
part,
|
|
23
|
+
contentType: attachment.contentType,
|
|
24
|
+
})),
|
|
25
|
+
) ?? []),
|
|
20
26
|
];
|
|
21
|
-
const content = allContent.flatMap((part) => {
|
|
27
|
+
const content = allContent.flatMap(({ part, contentType }) => {
|
|
22
28
|
const type = part.type;
|
|
23
29
|
switch (type) {
|
|
24
30
|
case "text":
|
|
25
31
|
return { type: "text" as const, text: part.text };
|
|
26
|
-
case "image":
|
|
32
|
+
case "image": {
|
|
33
|
+
const parsed = parseDataUrl(part.image);
|
|
34
|
+
if (parsed) {
|
|
35
|
+
return {
|
|
36
|
+
type: "image" as const,
|
|
37
|
+
mimeType: resolveImageMediaType(part.image, contentType),
|
|
38
|
+
data: parsed.data,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
27
41
|
return { type: "image_url" as const, url: part.image };
|
|
42
|
+
}
|
|
28
43
|
case "file": {
|
|
29
44
|
const source = resolveFilePartSource(part);
|
|
30
45
|
if (source.kind === "url") {
|
|
@@ -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
|
+
};
|