@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.
Files changed (93) hide show
  1. package/dist/AdkClient.d.ts +2 -3
  2. package/dist/AdkClient.d.ts.map +1 -1
  3. package/dist/AdkClient.js +2 -1
  4. package/dist/AdkClient.js.map +1 -1
  5. package/dist/AdkEventAccumulator.d.ts +4 -6
  6. package/dist/AdkEventAccumulator.d.ts.map +1 -1
  7. package/dist/AdkEventAccumulator.js +80 -95
  8. package/dist/AdkEventAccumulator.js.map +1 -1
  9. package/dist/AdkSessionAdapter.d.ts +7 -4
  10. package/dist/AdkSessionAdapter.d.ts.map +1 -1
  11. package/dist/AdkSessionAdapter.js +7 -4
  12. package/dist/AdkSessionAdapter.js.map +1 -1
  13. package/dist/adkExtras.d.ts +1 -2
  14. package/dist/adkExtras.d.ts.map +1 -1
  15. package/dist/adkToolApproval.d.ts +6 -7
  16. package/dist/adkToolApproval.d.ts.map +1 -1
  17. package/dist/adkToolApproval.js +1 -3
  18. package/dist/adkToolApproval.js.map +1 -1
  19. package/dist/contentToParts.d.ts +1 -2
  20. package/dist/contentToParts.d.ts.map +1 -1
  21. package/dist/convertAdkMessages.d.ts +2 -3
  22. package/dist/convertAdkMessages.d.ts.map +1 -1
  23. package/dist/convertToAdkMessages.d.ts +15 -7
  24. package/dist/convertToAdkMessages.d.ts.map +1 -1
  25. package/dist/convertToAdkMessages.js +20 -6
  26. package/dist/convertToAdkMessages.js.map +1 -1
  27. package/dist/hooks.d.ts +15 -16
  28. package/dist/hooks.d.ts.map +1 -1
  29. package/dist/hooks.js +1 -1
  30. package/dist/normalizeAdkPart.d.ts +4 -0
  31. package/dist/normalizeAdkPart.d.ts.map +1 -0
  32. package/dist/normalizeAdkPart.js +31 -0
  33. package/dist/normalizeAdkPart.js.map +1 -0
  34. package/dist/parseAdkEvent.d.ts +1 -2
  35. package/dist/parseAdkEvent.d.ts.map +1 -1
  36. package/dist/sdkIdentity.d.ts +5 -0
  37. package/dist/sdkIdentity.d.ts.map +1 -0
  38. package/dist/sdkIdentity.js +9 -0
  39. package/dist/sdkIdentity.js.map +1 -0
  40. package/dist/server/adkEventStream.d.ts +2 -3
  41. package/dist/server/adkEventStream.d.ts.map +1 -1
  42. package/dist/server/createAdkApiRoute.d.ts +2 -3
  43. package/dist/server/createAdkApiRoute.d.ts.map +1 -1
  44. package/dist/server/parseAdkRequest.d.ts +2 -3
  45. package/dist/server/parseAdkRequest.d.ts.map +1 -1
  46. package/dist/server/parseAdkRequest.js +2 -1
  47. package/dist/server/parseAdkRequest.js.map +1 -1
  48. package/dist/structuredEvents.d.ts +1 -2
  49. package/dist/structuredEvents.d.ts.map +1 -1
  50. package/dist/structuredEvents.js +1 -1
  51. package/dist/structuredEvents.js.map +1 -1
  52. package/dist/toAdkFunctionResponse.d.ts +5 -0
  53. package/dist/toAdkFunctionResponse.d.ts.map +1 -0
  54. package/dist/toAdkFunctionResponse.js +11 -0
  55. package/dist/toAdkFunctionResponse.js.map +1 -0
  56. package/dist/trimTrailingSlashes.d.ts +1 -2
  57. package/dist/trimTrailingSlashes.d.ts.map +1 -1
  58. package/dist/types.d.ts +22 -23
  59. package/dist/types.d.ts.map +1 -1
  60. package/dist/types.js.map +1 -1
  61. package/dist/useAdkMessages.d.ts +2 -3
  62. package/dist/useAdkMessages.d.ts.map +1 -1
  63. package/dist/useAdkMessages.js +16 -5
  64. package/dist/useAdkMessages.js.map +1 -1
  65. package/dist/useAdkRuntime.d.ts +2 -3
  66. package/dist/useAdkRuntime.d.ts.map +1 -1
  67. package/dist/useAdkRuntime.js +3 -1
  68. package/dist/useAdkRuntime.js.map +1 -1
  69. package/package.json +10 -9
  70. package/src/AdkClient.test.ts +139 -2
  71. package/src/AdkClient.ts +2 -1
  72. package/src/AdkEventAccumulator.test.ts +588 -11
  73. package/src/AdkEventAccumulator.ts +85 -130
  74. package/src/AdkSessionAdapter.test.ts +355 -1
  75. package/src/AdkSessionAdapter.ts +24 -5
  76. package/src/adkToolApproval.test.ts +0 -4
  77. package/src/adkToolApproval.ts +1 -3
  78. package/src/convertToAdkMessages.test.ts +70 -0
  79. package/src/convertToAdkMessages.ts +19 -4
  80. package/src/normalizeAdkPart.ts +35 -0
  81. package/src/sdkIdentity.ts +9 -0
  82. package/src/server/parseAdkRequest.test.ts +63 -0
  83. package/src/server/parseAdkRequest.ts +2 -1
  84. package/src/structuredEvents.test.ts +69 -0
  85. package/src/structuredEvents.ts +4 -1
  86. package/src/toAdkFunctionResponse.test.ts +46 -0
  87. package/src/toAdkFunctionResponse.ts +18 -0
  88. package/src/types.ts +1 -1
  89. package/src/useAdkMessages.test.ts +249 -2
  90. package/src/useAdkMessages.ts +24 -6
  91. package/src/useAdkRuntime.replacement.test.tsx +11 -7
  92. package/src/useAdkRuntime.ts +2 -0
  93. 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 "inlineData".',
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 }),
@@ -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 "inlineData".',
138
+ 'Invalid ADK artifact load response: expected an object containing "text", "inlineData", or "fileData".',
137
139
  );
138
140
  }
139
141
 
140
- const { text, inlineData } = value;
141
- if (text === undefined && inlineData === undefined) {
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 "inlineData".',
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
- return value as AdkArtifactData;
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" }),
@@ -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((a) => a.content) ?? []),
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
+ };
@@ -0,0 +1,9 @@
1
+ import type { SdkIdentity } from "assistant-cloud";
2
+
3
+ export const ADK_SDK: SdkIdentity = {
4
+ name: "@assistant-ui/react-google-adk",
5
+ version:
6
+ typeof __AUI_PACKAGE_VERSION__ === "string"
7
+ ? __AUI_PACKAGE_VERSION__
8
+ : "0.0.0",
9
+ };