@assistant-ui/react-google-adk 0.0.29 → 0.0.30

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 (45) hide show
  1. package/dist/AdkClient.d.ts.map +1 -1
  2. package/dist/AdkClient.js +2 -1
  3. package/dist/AdkClient.js.map +1 -1
  4. package/dist/AdkEventAccumulator.d.ts +1 -1
  5. package/dist/AdkEventAccumulator.d.ts.map +1 -1
  6. package/dist/AdkEventAccumulator.js +31 -10
  7. package/dist/AdkEventAccumulator.js.map +1 -1
  8. package/dist/convertToAdkMessages.d.ts +10 -1
  9. package/dist/convertToAdkMessages.d.ts.map +1 -1
  10. package/dist/convertToAdkMessages.js +20 -6
  11. package/dist/convertToAdkMessages.js.map +1 -1
  12. package/dist/sdkIdentity.d.ts +6 -0
  13. package/dist/sdkIdentity.d.ts.map +1 -0
  14. package/dist/sdkIdentity.js +9 -0
  15. package/dist/sdkIdentity.js.map +1 -0
  16. package/dist/server/parseAdkRequest.d.ts.map +1 -1
  17. package/dist/server/parseAdkRequest.js +2 -1
  18. package/dist/server/parseAdkRequest.js.map +1 -1
  19. package/dist/toAdkFunctionResponse.d.ts +6 -0
  20. package/dist/toAdkFunctionResponse.d.ts.map +1 -0
  21. package/dist/toAdkFunctionResponse.js +11 -0
  22. package/dist/toAdkFunctionResponse.js.map +1 -0
  23. package/dist/useAdkMessages.d.ts.map +1 -1
  24. package/dist/useAdkMessages.js +14 -5
  25. package/dist/useAdkMessages.js.map +1 -1
  26. package/dist/useAdkRuntime.d.ts.map +1 -1
  27. package/dist/useAdkRuntime.js +2 -0
  28. package/dist/useAdkRuntime.js.map +1 -1
  29. package/package.json +5 -5
  30. package/src/AdkClient.test.ts +139 -2
  31. package/src/AdkClient.ts +2 -1
  32. package/src/AdkEventAccumulator.test.ts +276 -0
  33. package/src/AdkEventAccumulator.ts +32 -3
  34. package/src/AdkSessionAdapter.test.ts +88 -0
  35. package/src/convertToAdkMessages.test.ts +70 -0
  36. package/src/convertToAdkMessages.ts +19 -4
  37. package/src/sdkIdentity.ts +9 -0
  38. package/src/server/parseAdkRequest.test.ts +63 -0
  39. package/src/server/parseAdkRequest.ts +2 -1
  40. package/src/toAdkFunctionResponse.test.ts +46 -0
  41. package/src/toAdkFunctionResponse.ts +18 -0
  42. package/src/useAdkMessages.test.ts +148 -0
  43. package/src/useAdkMessages.ts +22 -6
  44. package/src/useAdkRuntime.ts +2 -0
  45. package/src/useAdkRuntimeApproval.test.tsx +0 -1
@@ -1,5 +1,6 @@
1
1
  import { describe, it, expect } from "vitest";
2
2
  import { AdkEventAccumulator } from "./AdkEventAccumulator";
3
+ import { parseAdkEventValue } from "./parseAdkEvent";
3
4
  import type { AdkEvent, AdkMessage, AdkMessageContentPart } from "./types";
4
5
 
5
6
  const makeEvent = (overrides: Partial<AdkEvent> = {}): AdkEvent => ({
@@ -18,6 +19,41 @@ const makeTextEvent = (
18
19
  content: { role: "model", parts: [{ text }] },
19
20
  });
20
21
 
22
+ describe.each(["user", "agent"])("%s function response status", (author) => {
23
+ it.each([
24
+ [{ error: "denied" }, "error"],
25
+ [{ error: { message: "denied" }, output: "partial" }, "error"],
26
+ [{ error: null }, "error"],
27
+ [{ error: false }, "error"],
28
+ [{ error: "" }, "error"],
29
+ [{ output: { error: "application data" } }, "success"],
30
+ [{ result: "done" }, "success"],
31
+ [{}, "success"],
32
+ ] as const)("classifies response %j as %s", (response, status) => {
33
+ const acc = new AdkEventAccumulator();
34
+ const messages = acc.processEvent(
35
+ makeEvent({
36
+ author,
37
+ content: {
38
+ parts: [
39
+ {
40
+ functionResponse: { id: "tc-1", name: "search", response },
41
+ },
42
+ ],
43
+ },
44
+ }),
45
+ );
46
+ expect(messages).toMatchObject([
47
+ {
48
+ type: "tool",
49
+ tool_call_id: "tc-1",
50
+ status,
51
+ content: JSON.stringify(response),
52
+ },
53
+ ]);
54
+ });
55
+ });
56
+
21
57
  describe("AdkEventAccumulator - text handling", () => {
22
58
  it("accumulates a single non-partial text event into an AI message", () => {
23
59
  const acc = new AdkEventAccumulator();
@@ -933,6 +969,99 @@ describe("AdkEventAccumulator - actions tracking", () => {
933
969
  );
934
970
  expect(acc.getLongRunningToolIds()).toEqual(["lrt-1"]);
935
971
  });
972
+
973
+ it("settles a long-running id when a user-authored response answers it", () => {
974
+ const acc = new AdkEventAccumulator();
975
+ acc.processEvent(
976
+ makeEvent({
977
+ author: "agent",
978
+ longRunningToolIds: ["lrt-1", "lrt-2"],
979
+ content: {
980
+ parts: [
981
+ {
982
+ functionCall: { name: "ask_for_approval", id: "lrt-1", args: {} },
983
+ },
984
+ {
985
+ functionCall: { name: "ask_for_approval", id: "lrt-2", args: {} },
986
+ },
987
+ ],
988
+ },
989
+ }),
990
+ );
991
+ acc.processEvent(
992
+ makeEvent({
993
+ author: "user",
994
+ content: {
995
+ parts: [
996
+ {
997
+ functionResponse: {
998
+ name: "ask_for_approval",
999
+ id: "lrt-1",
1000
+ response: { status: "approved" },
1001
+ },
1002
+ },
1003
+ ],
1004
+ },
1005
+ }),
1006
+ );
1007
+ expect(acc.getLongRunningToolIds()).toEqual(["lrt-2"]);
1008
+ });
1009
+
1010
+ it("keeps a long-running id pending through the interim response ADK authors", () => {
1011
+ const acc = new AdkEventAccumulator();
1012
+ acc.processEvent(
1013
+ makeEvent({
1014
+ author: "agent",
1015
+ longRunningToolIds: ["lrt-1"],
1016
+ content: {
1017
+ parts: [
1018
+ {
1019
+ functionCall: { name: "ask_for_approval", id: "lrt-1", args: {} },
1020
+ },
1021
+ ],
1022
+ },
1023
+ }),
1024
+ );
1025
+ acc.processEvent(
1026
+ makeEvent({
1027
+ author: "agent",
1028
+ content: {
1029
+ parts: [
1030
+ {
1031
+ functionResponse: {
1032
+ name: "ask_for_approval",
1033
+ id: "lrt-1",
1034
+ response: { status: "pending" },
1035
+ },
1036
+ },
1037
+ ],
1038
+ },
1039
+ }),
1040
+ );
1041
+ expect(acc.getLongRunningToolIds()).toEqual(["lrt-1"]);
1042
+ });
1043
+
1044
+ it("seeds and settles longRunningToolIds from a user response", () => {
1045
+ const acc = new AdkEventAccumulator([], ["lrt-1", "lrt-2"]);
1046
+ acc.processEvent(
1047
+ makeEvent({
1048
+ author: "user",
1049
+ content: {
1050
+ role: "user",
1051
+ parts: [
1052
+ {
1053
+ functionResponse: {
1054
+ id: "lrt-1",
1055
+ name: "adk_request_confirmation",
1056
+ response: { confirmed: true },
1057
+ },
1058
+ },
1059
+ ],
1060
+ },
1061
+ }),
1062
+ );
1063
+ expect(acc.getLongRunningToolIds()).toEqual(["lrt-2"]);
1064
+ });
936
1065
  });
937
1066
 
938
1067
  describe("AdkEventAccumulator - special function calls", () => {
@@ -1044,6 +1173,153 @@ describe("AdkEventAccumulator - author/agent tracking", () => {
1044
1173
  });
1045
1174
 
1046
1175
  describe("AdkEventAccumulator - snake_case normalization", () => {
1176
+ describe.each(["user", "agent"])("%s media", (author) => {
1177
+ it.each([
1178
+ {
1179
+ part: { inline_data: { mime_type: "image/png", data: "aGVsbG8=" } },
1180
+ expected: { type: "image", mimeType: "image/png", data: "aGVsbG8=" },
1181
+ },
1182
+ {
1183
+ part: {
1184
+ inline_data: { mime_type: "application/pdf", data: "aGVsbG8=" },
1185
+ },
1186
+ expected: {
1187
+ type: "file",
1188
+ mimeType: "application/pdf",
1189
+ data: "aGVsbG8=",
1190
+ },
1191
+ },
1192
+ {
1193
+ part: {
1194
+ file_data: {
1195
+ mime_type: "image/png",
1196
+ file_uri: "https://example.test/image.png",
1197
+ },
1198
+ },
1199
+ expected: { type: "image_url", url: "https://example.test/image.png" },
1200
+ },
1201
+ {
1202
+ part: {
1203
+ file_data: {
1204
+ mime_type: "application/pdf",
1205
+ file_uri: "https://example.test/report.pdf",
1206
+ },
1207
+ },
1208
+ expected: {
1209
+ type: "file_url",
1210
+ mimeType: "application/pdf",
1211
+ url: "https://example.test/report.pdf",
1212
+ },
1213
+ },
1214
+ {
1215
+ part: { inlineData: { mime_type: "image/png", data: "aGVsbG8=" } },
1216
+ expected: { type: "image", mimeType: "image/png", data: "aGVsbG8=" },
1217
+ },
1218
+ {
1219
+ part: {
1220
+ fileData: {
1221
+ mime_type: "application/pdf",
1222
+ file_uri: "https://example.test/report.pdf",
1223
+ },
1224
+ },
1225
+ expected: {
1226
+ type: "file_url",
1227
+ mimeType: "application/pdf",
1228
+ url: "https://example.test/report.pdf",
1229
+ },
1230
+ },
1231
+ {
1232
+ part: { inline_data: { mimeType: "image/png", data: "aGVsbG8=" } },
1233
+ expected: { type: "image", mimeType: "image/png", data: "aGVsbG8=" },
1234
+ },
1235
+ {
1236
+ part: {
1237
+ file_data: {
1238
+ mimeType: "application/pdf",
1239
+ fileUri: "https://example.test/report.pdf",
1240
+ },
1241
+ },
1242
+ expected: {
1243
+ type: "file_url",
1244
+ mimeType: "application/pdf",
1245
+ url: "https://example.test/report.pdf",
1246
+ },
1247
+ },
1248
+ ])(
1249
+ "normalizes media aliases without mutating the event: $part",
1250
+ ({ part, expected }) => {
1251
+ const input = { id: "media", author, content: { parts: [part] } };
1252
+ const original = structuredClone(input);
1253
+ const event = parseAdkEventValue(input, "test");
1254
+ const messages = new AdkEventAccumulator().processEvent(event);
1255
+ expect(messages).toMatchObject([
1256
+ { type: author === "user" ? "human" : "ai", content: [expected] },
1257
+ ]);
1258
+ expect(input).toEqual(original);
1259
+ },
1260
+ );
1261
+ });
1262
+
1263
+ it("prefers camelCase media containers and nested values when both exist", () => {
1264
+ const event = parseAdkEventValue(
1265
+ {
1266
+ id: "media",
1267
+ author: "agent",
1268
+ content: {
1269
+ parts: [
1270
+ {
1271
+ inlineData: {
1272
+ mimeType: "image/png",
1273
+ mime_type: "application/pdf",
1274
+ data: "aGVsbG8=",
1275
+ },
1276
+ inline_data: { mime_type: "text/plain", data: "wrong" },
1277
+ },
1278
+ {
1279
+ fileData: {
1280
+ mimeType: "application/pdf",
1281
+ mime_type: "image/png",
1282
+ fileUri: "https://example.test/right.pdf",
1283
+ file_uri: "https://example.test/wrong.png",
1284
+ },
1285
+ file_data: { file_uri: "https://example.test/other.png" },
1286
+ },
1287
+ ],
1288
+ },
1289
+ },
1290
+ "test",
1291
+ );
1292
+ expect(new AdkEventAccumulator().processEvent(event)).toMatchObject([
1293
+ {
1294
+ content: [
1295
+ { type: "image", mimeType: "image/png", data: "aGVsbG8=" },
1296
+ {
1297
+ type: "file_url",
1298
+ mimeType: "application/pdf",
1299
+ url: "https://example.test/right.pdf",
1300
+ },
1301
+ ],
1302
+ },
1303
+ ]);
1304
+ });
1305
+
1306
+ it("leaves similarly named tool arguments untouched", () => {
1307
+ const args = { inline_data: { mime_type: "custom", file_uri: "opaque" } };
1308
+ const event = parseAdkEventValue(
1309
+ {
1310
+ id: "call",
1311
+ author: "agent",
1312
+ content: {
1313
+ parts: [{ function_call: { name: "test", id: "tc-1", args } }],
1314
+ },
1315
+ },
1316
+ "test",
1317
+ );
1318
+ expect(new AdkEventAccumulator().processEvent(event)).toMatchObject([
1319
+ { tool_calls: [{ args }] },
1320
+ ]);
1321
+ });
1322
+
1047
1323
  it("normalizes function_call to functionCall in parts", () => {
1048
1324
  const acc = new AdkEventAccumulator();
1049
1325
  const msgs = acc.processEvent(
@@ -1,4 +1,5 @@
1
1
  import { generateId } from "@assistant-ui/core";
2
+ import { isRecord } from "@assistant-ui/core/internal";
2
3
  import type { MessageStatus } from "@assistant-ui/core";
3
4
  import type {
4
5
  AdkEvent,
@@ -11,6 +12,7 @@ import type {
11
12
  AdkMessageMetadata,
12
13
  } from "./types";
13
14
  import type { ReadonlyJSONObject } from "assistant-stream/utils";
15
+ import { isAdkFunctionError } from "./toAdkFunctionResponse";
14
16
 
15
17
  type InProgressMessage = AdkMessage & { type: "ai" };
16
18
 
@@ -125,6 +127,21 @@ const normalizeEventPart = (part: AdkEventPart): AdkEventPart => {
125
127
  if ("inline_data" in p && !("inlineData" in p))
126
128
  result.inlineData = p.inline_data;
127
129
  if ("file_data" in p && !("fileData" in p)) result.fileData = p.file_data;
130
+ if (isRecord(result.inlineData)) {
131
+ const data = result.inlineData;
132
+ if ("mime_type" in data && !("mimeType" in data))
133
+ result.inlineData = { ...data, mimeType: data.mime_type };
134
+ }
135
+ if (isRecord(result.fileData)) {
136
+ const data = result.fileData;
137
+ result.fileData = {
138
+ ...data,
139
+ ...("mime_type" in data &&
140
+ !("mimeType" in data) && { mimeType: data.mime_type }),
141
+ ...("file_uri" in data &&
142
+ !("fileUri" in data) && { fileUri: data.file_uri }),
143
+ };
144
+ }
128
145
  if ("executable_code" in p && !("executableCode" in p))
129
146
  result.executableCode = p.executable_code;
130
147
  if ("code_execution_result" in p && !("codeExecutionResult" in p))
@@ -210,12 +227,18 @@ export class AdkEventAccumulator {
210
227
  // How many assistant messages each event has opened, so a replay of that
211
228
  // event opens them with the same ids.
212
229
  private aiMessageOrdinals = new Map<string, number>();
213
- constructor(initialMessages?: AdkMessage[]) {
230
+ constructor(
231
+ initialMessages?: AdkMessage[],
232
+ initialLongRunningToolIds?: readonly string[],
233
+ ) {
214
234
  if (initialMessages) {
215
235
  for (const msg of initialMessages) {
216
236
  this.messagesMap.set(msg.id, msg);
217
237
  }
218
238
  }
239
+ if (initialLongRunningToolIds) {
240
+ this.pendingLongRunningToolIds = new Set(initialLongRunningToolIds);
241
+ }
219
242
  }
220
243
 
221
244
  processEvent(rawEvent: AdkEvent): AdkMessage[] {
@@ -359,8 +382,12 @@ export class AdkEventAccumulator {
359
382
  tool_call_id: part.functionResponse.id,
360
383
  name: part.functionResponse.name,
361
384
  content: JSON.stringify(part.functionResponse.response),
362
- status: "success",
385
+ status: isAdkFunctionError(part.functionResponse.response)
386
+ ? "error"
387
+ : "success",
363
388
  });
389
+ // Only a user-authored response settles a long-running call; the response ADK authors for one is the tool's interim result.
390
+ this.pendingLongRunningToolIds.delete(part.functionResponse.id);
364
391
  }
365
392
  }
366
393
  // The replies answer the preceding assistant turn, so they are emitted
@@ -541,7 +568,9 @@ export class AdkEventAccumulator {
541
568
  tool_call_id: part.functionResponse.id ?? "",
542
569
  name: part.functionResponse.name,
543
570
  content: JSON.stringify(part.functionResponse.response),
544
- status: "success",
571
+ status: isAdkFunctionError(part.functionResponse.response)
572
+ ? "error"
573
+ : "success",
545
574
  };
546
575
  this.messagesMap.set(toolMsg.id, toolMsg);
547
576
  return;
@@ -294,6 +294,87 @@ describe("createAdkSessionAdapter - fetch", () => {
294
294
  // ── load() ──
295
295
 
296
296
  describe("createAdkSessionAdapter - load", () => {
297
+ it("restores tool failures from stored function responses", async () => {
298
+ mockFetch.mockResolvedValueOnce(
299
+ new Response(
300
+ JSON.stringify({
301
+ id: "s1",
302
+ events: [
303
+ {
304
+ id: "failed",
305
+ author: "user",
306
+ content: {
307
+ parts: [
308
+ {
309
+ functionResponse: {
310
+ id: "tc-1",
311
+ name: "search",
312
+ response: { error: "denied" },
313
+ },
314
+ },
315
+ ],
316
+ },
317
+ },
318
+ ],
319
+ }),
320
+ { status: 200 },
321
+ ),
322
+ );
323
+ const { load } = createAdkSessionAdapter(baseOptions);
324
+ const result = await load("s1");
325
+ expect(result.messages).toMatchObject([
326
+ {
327
+ type: "tool",
328
+ tool_call_id: "tc-1",
329
+ status: "error",
330
+ content: JSON.stringify({ error: "denied" }),
331
+ },
332
+ ]);
333
+ });
334
+
335
+ it("restores snake_case image and file parts from session history", async () => {
336
+ mockFetch.mockResolvedValueOnce(
337
+ new Response(
338
+ JSON.stringify({
339
+ id: "s1",
340
+ events: [
341
+ {
342
+ id: "media",
343
+ author: "user",
344
+ content: {
345
+ parts: [
346
+ { inline_data: { mime_type: "image/png", data: "aGVsbG8=" } },
347
+ {
348
+ file_data: {
349
+ mime_type: "application/pdf",
350
+ file_uri: "https://example.test/report.pdf",
351
+ },
352
+ },
353
+ ],
354
+ },
355
+ },
356
+ ],
357
+ }),
358
+ { status: 200 },
359
+ ),
360
+ );
361
+ const { load } = createAdkSessionAdapter(baseOptions);
362
+ const result = await load("s1");
363
+ expect(result.messages).toMatchObject([
364
+ {
365
+ type: "human",
366
+ content: [
367
+ { type: "image", mimeType: "image/png", data: "aGVsbG8=" },
368
+ {
369
+ type: "file_url",
370
+ mimeType: "application/pdf",
371
+ url: "https://example.test/report.pdf",
372
+ },
373
+ ],
374
+ },
375
+ ]);
376
+ });
377
+
297
378
  it("returns the per-turn state the events imply, not just the messages", async () => {
298
379
  const session = {
299
380
  id: "s1",
@@ -543,10 +624,17 @@ describe("createAdkSessionAdapter - load replays tool confirmations", () => {
543
624
  const result = await load("s1");
544
625
  return {
545
626
  messages: result.messages,
627
+ longRunningToolIds: result.longRunningToolIds,
546
628
  approvals: projectAdkToolApprovals(result.messages).approvals,
547
629
  };
548
630
  };
549
631
 
632
+ it("settles an answered long-running tool on replay", async () => {
633
+ const { longRunningToolIds } = await loadApprovals({ confirmed: true });
634
+
635
+ expect(longRunningToolIds).toEqual([]);
636
+ });
637
+
550
638
  it("keeps a user-authored confirmation reply as a tool message", async () => {
551
639
  const { messages } = await loadApprovals({ confirmed: true });
552
640
 
@@ -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,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
+ };
@@ -239,6 +239,41 @@ describe("parseAdkRequest", () => {
239
239
  });
240
240
 
241
241
  describe("toAdkContent", () => {
242
+ it.each([
243
+ ["permission denied", { error: "permission denied" }],
244
+ [
245
+ { message: "permission denied" },
246
+ { error: { message: "permission denied" } },
247
+ ],
248
+ [["denied"], { error: ["denied"] }],
249
+ [null, { error: null }],
250
+ [false, { error: false }],
251
+ [0, { error: 0 }],
252
+ ["", { error: "" }],
253
+ [
254
+ { error: "denied", output: "partial" },
255
+ { error: "denied", output: "partial" },
256
+ ],
257
+ ])(
258
+ "preserves explicit tool failure %j in the function response",
259
+ async (result, response) => {
260
+ const parsed = await parseAdkRequest(
261
+ makeRequest({
262
+ type: "tool-result",
263
+ toolCallId: "tc-1",
264
+ toolName: "search",
265
+ result,
266
+ isError: true,
267
+ }),
268
+ );
269
+ expect(toAdkContent(parsed).parts).toEqual([
270
+ {
271
+ functionResponse: { name: "search", id: "tc-1", response },
272
+ },
273
+ ]);
274
+ },
275
+ );
276
+
242
277
  it("converts a text message to user content with text part", () => {
243
278
  const content = toAdkContent({
244
279
  type: "message",
@@ -292,4 +327,32 @@ describe("toAdkContent", () => {
292
327
  ],
293
328
  });
294
329
  });
330
+
331
+ it.each([
332
+ [false, { result: false }],
333
+ [0, { result: 0 }],
334
+ [null, { result: null }],
335
+ ["done", { result: "done" }],
336
+ [[1, 2], { results: [1, 2] }],
337
+ ])(
338
+ "wraps scalar or array tool result %j in a function response object",
339
+ (result, response) => {
340
+ const content = toAdkContent({
341
+ type: "tool-result",
342
+ toolCallId: "tc-1",
343
+ toolName: "search",
344
+ result,
345
+ isError: false,
346
+ config: {},
347
+ });
348
+
349
+ expect(content.parts[0]).toEqual({
350
+ functionResponse: {
351
+ name: "search",
352
+ id: "tc-1",
353
+ response,
354
+ },
355
+ });
356
+ },
357
+ );
295
358
  });