@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,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();
@@ -210,6 +246,78 @@ describe("AdkEventAccumulator - function calls", () => {
210
246
  );
211
247
  expect(acc.getToolConfirmations()).toHaveLength(0);
212
248
  });
249
+
250
+ const requestEvent = (name: string) =>
251
+ makeEvent({
252
+ author: "agent",
253
+ content: {
254
+ role: "model",
255
+ parts: [{ functionCall: { name, id: "rc-1" } }, { text: "still here" }],
256
+ },
257
+ });
258
+
259
+ it("keeps an args-less confirmation call answerable", () => {
260
+ const acc = new AdkEventAccumulator();
261
+ const msgs = acc.processEvent(requestEvent("adk_request_confirmation"));
262
+
263
+ expect(acc.getToolConfirmations()).toEqual([
264
+ {
265
+ toolCallId: "rc-1",
266
+ toolName: "",
267
+ args: {},
268
+ hint: "",
269
+ confirmed: false,
270
+ },
271
+ ]);
272
+ expect((msgs[0] as AdkMessage & { type: "ai" }).tool_calls).toEqual([
273
+ {
274
+ id: "rc-1",
275
+ name: "adk_request_confirmation",
276
+ args: {},
277
+ argsText: "{}",
278
+ },
279
+ ]);
280
+ expect(msgs[0]).toMatchObject({
281
+ content: [{ type: "text", text: "still here" }],
282
+ });
283
+ });
284
+
285
+ it("keeps an args-less credential call answerable", () => {
286
+ const acc = new AdkEventAccumulator();
287
+ const msgs = acc.processEvent(requestEvent("adk_request_credential"));
288
+
289
+ expect(acc.getAuthRequests()).toEqual([
290
+ { toolCallId: "rc-1", authConfig: undefined },
291
+ ]);
292
+ expect((msgs[0] as AdkMessage & { type: "ai" }).tool_calls).toEqual([
293
+ {
294
+ id: "rc-1",
295
+ name: "adk_request_credential",
296
+ args: {},
297
+ argsText: "{}",
298
+ },
299
+ ]);
300
+ expect(msgs[0]).toMatchObject({
301
+ content: [{ type: "text", text: "still here" }],
302
+ });
303
+ });
304
+
305
+ it("defaults an ordinary call without args to empty args", () => {
306
+ const acc = new AdkEventAccumulator();
307
+ const msgs = acc.processEvent(
308
+ makeEvent({
309
+ author: "agent",
310
+ content: {
311
+ role: "model",
312
+ parts: [{ functionCall: { name: "search", id: "tc-1" } }],
313
+ },
314
+ }),
315
+ );
316
+
317
+ expect((msgs[0] as AdkMessage & { type: "ai" }).tool_calls).toEqual([
318
+ { id: "tc-1", name: "search", args: {}, argsText: "{}" },
319
+ ]);
320
+ });
213
321
  });
214
322
 
215
323
  describe("AdkEventAccumulator - function responses", () => {
@@ -933,6 +1041,99 @@ describe("AdkEventAccumulator - actions tracking", () => {
933
1041
  );
934
1042
  expect(acc.getLongRunningToolIds()).toEqual(["lrt-1"]);
935
1043
  });
1044
+
1045
+ it("settles a long-running id when a user-authored response answers it", () => {
1046
+ const acc = new AdkEventAccumulator();
1047
+ acc.processEvent(
1048
+ makeEvent({
1049
+ author: "agent",
1050
+ longRunningToolIds: ["lrt-1", "lrt-2"],
1051
+ content: {
1052
+ parts: [
1053
+ {
1054
+ functionCall: { name: "ask_for_approval", id: "lrt-1", args: {} },
1055
+ },
1056
+ {
1057
+ functionCall: { name: "ask_for_approval", id: "lrt-2", args: {} },
1058
+ },
1059
+ ],
1060
+ },
1061
+ }),
1062
+ );
1063
+ acc.processEvent(
1064
+ makeEvent({
1065
+ author: "user",
1066
+ content: {
1067
+ parts: [
1068
+ {
1069
+ functionResponse: {
1070
+ name: "ask_for_approval",
1071
+ id: "lrt-1",
1072
+ response: { status: "approved" },
1073
+ },
1074
+ },
1075
+ ],
1076
+ },
1077
+ }),
1078
+ );
1079
+ expect(acc.getLongRunningToolIds()).toEqual(["lrt-2"]);
1080
+ });
1081
+
1082
+ it("keeps a long-running id pending through the interim response ADK authors", () => {
1083
+ const acc = new AdkEventAccumulator();
1084
+ acc.processEvent(
1085
+ makeEvent({
1086
+ author: "agent",
1087
+ longRunningToolIds: ["lrt-1"],
1088
+ content: {
1089
+ parts: [
1090
+ {
1091
+ functionCall: { name: "ask_for_approval", id: "lrt-1", args: {} },
1092
+ },
1093
+ ],
1094
+ },
1095
+ }),
1096
+ );
1097
+ acc.processEvent(
1098
+ makeEvent({
1099
+ author: "agent",
1100
+ content: {
1101
+ parts: [
1102
+ {
1103
+ functionResponse: {
1104
+ name: "ask_for_approval",
1105
+ id: "lrt-1",
1106
+ response: { status: "pending" },
1107
+ },
1108
+ },
1109
+ ],
1110
+ },
1111
+ }),
1112
+ );
1113
+ expect(acc.getLongRunningToolIds()).toEqual(["lrt-1"]);
1114
+ });
1115
+
1116
+ it("seeds and settles longRunningToolIds from a user response", () => {
1117
+ const acc = new AdkEventAccumulator([], ["lrt-1", "lrt-2"]);
1118
+ acc.processEvent(
1119
+ makeEvent({
1120
+ author: "user",
1121
+ content: {
1122
+ role: "user",
1123
+ parts: [
1124
+ {
1125
+ functionResponse: {
1126
+ id: "lrt-1",
1127
+ name: "adk_request_confirmation",
1128
+ response: { confirmed: true },
1129
+ },
1130
+ },
1131
+ ],
1132
+ },
1133
+ }),
1134
+ );
1135
+ expect(acc.getLongRunningToolIds()).toEqual(["lrt-2"]);
1136
+ });
936
1137
  });
937
1138
 
938
1139
  describe("AdkEventAccumulator - special function calls", () => {
@@ -973,21 +1174,176 @@ describe("AdkEventAccumulator - special function calls", () => {
973
1174
  });
974
1175
  });
975
1176
 
976
- it("records auth request from adk_request_credential", () => {
1177
+ it.each([
1178
+ [
1179
+ "snake_case",
1180
+ { function_call_id: "tc-original", auth_config: { type: "oauth2" } },
1181
+ ],
1182
+ [
1183
+ "camelCase",
1184
+ { functionCallId: "tc-original", authConfig: { type: "oauth2" } },
1185
+ ],
1186
+ ])(
1187
+ "records an auth request under its adk_request_credential call id (%s args)",
1188
+ (_, args) => {
1189
+ const acc = new AdkEventAccumulator();
1190
+ acc.processEvent(
1191
+ makeEvent({
1192
+ author: "agent",
1193
+ content: {
1194
+ role: "model",
1195
+ parts: [
1196
+ {
1197
+ functionCall: {
1198
+ name: "adk_request_credential",
1199
+ id: "cred-1",
1200
+ args,
1201
+ },
1202
+ },
1203
+ ],
1204
+ },
1205
+ }),
1206
+ );
1207
+ expect(acc.getAuthRequests()).toEqual([
1208
+ { toolCallId: "cred-1", authConfig: { type: "oauth2" } },
1209
+ ]);
1210
+ },
1211
+ );
1212
+ });
1213
+
1214
+ describe.each([
1215
+ {
1216
+ name: "adk_request_confirmation",
1217
+ args: {
1218
+ originalFunctionCall: { id: "gated", name: "transfer", args: {} },
1219
+ toolConfirmation: { hint: "Transfer?" },
1220
+ },
1221
+ actions: {
1222
+ requestedToolConfirmations: { gated: { hint: "Transfer?" } },
1223
+ },
1224
+ reply: { confirmed: true },
1225
+ pending: (acc: AdkEventAccumulator) =>
1226
+ acc.getToolConfirmations().map((c) => c.toolCallId),
1227
+ },
1228
+ {
1229
+ name: "adk_request_credential",
1230
+ args: { function_call_id: "gated", auth_config: { credentialKey: "k" } },
1231
+ actions: {
1232
+ requestedAuthConfigs: {
1233
+ gated: { credentialKey: "k" },
1234
+ "gated-2": { credentialKey: "k" },
1235
+ },
1236
+ },
1237
+ reply: { exchangedAuthCredential: { authType: "apiKey", apiKey: "key" } },
1238
+ pending: (acc: AdkEventAccumulator) =>
1239
+ acc.getAuthRequests().map((r) => r.toolCallId),
1240
+ },
1241
+ ])("AdkEventAccumulator - pending $name requests", (request) => {
1242
+ const requestEvents: AdkEvent[] = [
1243
+ makeEvent({
1244
+ id: "call",
1245
+ author: "agent",
1246
+ content: {
1247
+ role: "model",
1248
+ parts: [
1249
+ { functionCall: { name: "transfer", id: "gated", args: {} } },
1250
+ { functionCall: { name: "transfer", id: "gated-2", args: {} } },
1251
+ ],
1252
+ },
1253
+ }),
1254
+ makeEvent({
1255
+ id: "request",
1256
+ author: "agent",
1257
+ longRunningToolIds: ["req"],
1258
+ actions: request.actions,
1259
+ content: {
1260
+ role: "user",
1261
+ parts: [
1262
+ {
1263
+ functionCall: { name: request.name, id: "req", args: request.args },
1264
+ },
1265
+ ],
1266
+ },
1267
+ }),
1268
+ makeEvent({
1269
+ id: "interim",
1270
+ author: "agent",
1271
+ actions: request.actions,
1272
+ content: {
1273
+ role: "user",
1274
+ parts: [
1275
+ {
1276
+ functionResponse: {
1277
+ id: "gated",
1278
+ name: "transfer",
1279
+ response: { status: "pending" },
1280
+ },
1281
+ },
1282
+ ],
1283
+ },
1284
+ }),
1285
+ ];
1286
+ const replyPart = {
1287
+ functionResponse: {
1288
+ id: "req",
1289
+ name: request.name,
1290
+ response: request.reply,
1291
+ },
1292
+ };
1293
+
1294
+ it("lists the request once, under its call, until the user replies to that call", () => {
977
1295
  const acc = new AdkEventAccumulator();
1296
+ for (const event of requestEvents) acc.processEvent(event);
1297
+ expect(request.pending(acc)).toEqual(["req"]);
1298
+
978
1299
  acc.processEvent(
979
1300
  makeEvent({
1301
+ id: "reply",
1302
+ author: "user",
1303
+ content: { role: "user", parts: [replyPart] },
1304
+ }),
1305
+ );
1306
+ expect(request.pending(acc)).toEqual([]);
1307
+ });
1308
+
1309
+ it("carries the request into an accumulator seeded with the thread, which an optimistic reply settles", () => {
1310
+ const replayed = new AdkEventAccumulator();
1311
+ let messages: AdkMessage[] = [];
1312
+ for (const event of requestEvents) messages = replayed.processEvent(event);
1313
+
1314
+ const acc = new AdkEventAccumulator(messages);
1315
+ expect(request.pending(acc)).toEqual(["req"]);
1316
+
1317
+ acc.processEvent(
1318
+ makeEvent({
1319
+ id: "optimistic",
1320
+ content: { role: "user", parts: [replyPart] },
1321
+ }),
1322
+ );
1323
+ expect(request.pending(acc)).toEqual([]);
1324
+ });
1325
+ });
1326
+
1327
+ describe("AdkEventAccumulator - confirmation replies", () => {
1328
+ it.each([
1329
+ [["req"], "an unreadable reply", { response: "not json" }],
1330
+ [[], "a denial", { confirmed: false }],
1331
+ ])("lists %j after %s", (pending, _, response) => {
1332
+ const acc = new AdkEventAccumulator();
1333
+ acc.processEvent(
1334
+ makeEvent({
1335
+ id: "request",
980
1336
  author: "agent",
1337
+ longRunningToolIds: ["req"],
981
1338
  content: {
982
1339
  role: "model",
983
1340
  parts: [
984
1341
  {
985
1342
  functionCall: {
986
- name: "adk_request_credential",
987
- id: "cred-1",
1343
+ name: "adk_request_confirmation",
1344
+ id: "req",
988
1345
  args: {
989
- function_call_id: "tc-original",
990
- auth_config: { type: "oauth2" },
1346
+ originalFunctionCall: { id: "gated", name: "transfer" },
991
1347
  },
992
1348
  },
993
1349
  },
@@ -995,12 +1351,27 @@ describe("AdkEventAccumulator - special function calls", () => {
995
1351
  },
996
1352
  }),
997
1353
  );
998
- const reqs = acc.getAuthRequests();
999
- expect(reqs).toHaveLength(1);
1000
- expect(reqs[0]).toMatchObject({
1001
- toolCallId: "tc-original",
1002
- authConfig: { type: "oauth2" },
1003
- });
1354
+ acc.processEvent(
1355
+ makeEvent({
1356
+ id: "reply",
1357
+ author: "user",
1358
+ content: {
1359
+ role: "user",
1360
+ parts: [
1361
+ {
1362
+ functionResponse: {
1363
+ id: "req",
1364
+ name: "adk_request_confirmation",
1365
+ response,
1366
+ },
1367
+ },
1368
+ ],
1369
+ },
1370
+ }),
1371
+ );
1372
+ expect(acc.getToolConfirmations().map((c) => c.toolCallId)).toEqual(
1373
+ pending,
1374
+ );
1004
1375
  });
1005
1376
  });
1006
1377
 
@@ -1044,6 +1415,153 @@ describe("AdkEventAccumulator - author/agent tracking", () => {
1044
1415
  });
1045
1416
 
1046
1417
  describe("AdkEventAccumulator - snake_case normalization", () => {
1418
+ describe.each(["user", "agent"])("%s media", (author) => {
1419
+ it.each([
1420
+ {
1421
+ part: { inline_data: { mime_type: "image/png", data: "aGVsbG8=" } },
1422
+ expected: { type: "image", mimeType: "image/png", data: "aGVsbG8=" },
1423
+ },
1424
+ {
1425
+ part: {
1426
+ inline_data: { mime_type: "application/pdf", data: "aGVsbG8=" },
1427
+ },
1428
+ expected: {
1429
+ type: "file",
1430
+ mimeType: "application/pdf",
1431
+ data: "aGVsbG8=",
1432
+ },
1433
+ },
1434
+ {
1435
+ part: {
1436
+ file_data: {
1437
+ mime_type: "image/png",
1438
+ file_uri: "https://example.test/image.png",
1439
+ },
1440
+ },
1441
+ expected: { type: "image_url", url: "https://example.test/image.png" },
1442
+ },
1443
+ {
1444
+ part: {
1445
+ file_data: {
1446
+ mime_type: "application/pdf",
1447
+ file_uri: "https://example.test/report.pdf",
1448
+ },
1449
+ },
1450
+ expected: {
1451
+ type: "file_url",
1452
+ mimeType: "application/pdf",
1453
+ url: "https://example.test/report.pdf",
1454
+ },
1455
+ },
1456
+ {
1457
+ part: { inlineData: { mime_type: "image/png", data: "aGVsbG8=" } },
1458
+ expected: { type: "image", mimeType: "image/png", data: "aGVsbG8=" },
1459
+ },
1460
+ {
1461
+ part: {
1462
+ fileData: {
1463
+ mime_type: "application/pdf",
1464
+ file_uri: "https://example.test/report.pdf",
1465
+ },
1466
+ },
1467
+ expected: {
1468
+ type: "file_url",
1469
+ mimeType: "application/pdf",
1470
+ url: "https://example.test/report.pdf",
1471
+ },
1472
+ },
1473
+ {
1474
+ part: { inline_data: { mimeType: "image/png", data: "aGVsbG8=" } },
1475
+ expected: { type: "image", mimeType: "image/png", data: "aGVsbG8=" },
1476
+ },
1477
+ {
1478
+ part: {
1479
+ file_data: {
1480
+ mimeType: "application/pdf",
1481
+ fileUri: "https://example.test/report.pdf",
1482
+ },
1483
+ },
1484
+ expected: {
1485
+ type: "file_url",
1486
+ mimeType: "application/pdf",
1487
+ url: "https://example.test/report.pdf",
1488
+ },
1489
+ },
1490
+ ])(
1491
+ "normalizes media aliases without mutating the event: $part",
1492
+ ({ part, expected }) => {
1493
+ const input = { id: "media", author, content: { parts: [part] } };
1494
+ const original = structuredClone(input);
1495
+ const event = parseAdkEventValue(input, "test");
1496
+ const messages = new AdkEventAccumulator().processEvent(event);
1497
+ expect(messages).toMatchObject([
1498
+ { type: author === "user" ? "human" : "ai", content: [expected] },
1499
+ ]);
1500
+ expect(input).toEqual(original);
1501
+ },
1502
+ );
1503
+ });
1504
+
1505
+ it("prefers camelCase media containers and nested values when both exist", () => {
1506
+ const event = parseAdkEventValue(
1507
+ {
1508
+ id: "media",
1509
+ author: "agent",
1510
+ content: {
1511
+ parts: [
1512
+ {
1513
+ inlineData: {
1514
+ mimeType: "image/png",
1515
+ mime_type: "application/pdf",
1516
+ data: "aGVsbG8=",
1517
+ },
1518
+ inline_data: { mime_type: "text/plain", data: "wrong" },
1519
+ },
1520
+ {
1521
+ fileData: {
1522
+ mimeType: "application/pdf",
1523
+ mime_type: "image/png",
1524
+ fileUri: "https://example.test/right.pdf",
1525
+ file_uri: "https://example.test/wrong.png",
1526
+ },
1527
+ file_data: { file_uri: "https://example.test/other.png" },
1528
+ },
1529
+ ],
1530
+ },
1531
+ },
1532
+ "test",
1533
+ );
1534
+ expect(new AdkEventAccumulator().processEvent(event)).toMatchObject([
1535
+ {
1536
+ content: [
1537
+ { type: "image", mimeType: "image/png", data: "aGVsbG8=" },
1538
+ {
1539
+ type: "file_url",
1540
+ mimeType: "application/pdf",
1541
+ url: "https://example.test/right.pdf",
1542
+ },
1543
+ ],
1544
+ },
1545
+ ]);
1546
+ });
1547
+
1548
+ it("leaves similarly named tool arguments untouched", () => {
1549
+ const args = { inline_data: { mime_type: "custom", file_uri: "opaque" } };
1550
+ const event = parseAdkEventValue(
1551
+ {
1552
+ id: "call",
1553
+ author: "agent",
1554
+ content: {
1555
+ parts: [{ function_call: { name: "test", id: "tc-1", args } }],
1556
+ },
1557
+ },
1558
+ "test",
1559
+ );
1560
+ expect(new AdkEventAccumulator().processEvent(event)).toMatchObject([
1561
+ { tool_calls: [{ args }] },
1562
+ ]);
1563
+ });
1564
+
1047
1565
  it("normalizes function_call to functionCall in parts", () => {
1048
1566
  const acc = new AdkEventAccumulator();
1049
1567
  const msgs = acc.processEvent(
@@ -1415,6 +1933,65 @@ describe("AdkEventAccumulator - user message handling", () => {
1415
1933
  });
1416
1934
  });
1417
1935
 
1936
+ it.each([
1937
+ {
1938
+ name: "inlineData without mimeType",
1939
+ part: { inlineData: { data: "abc123" } },
1940
+ },
1941
+ {
1942
+ name: "inlineData without data",
1943
+ part: { inlineData: { mimeType: "image/png" } },
1944
+ },
1945
+ {
1946
+ name: "fileData without fileUri",
1947
+ part: { fileData: { mimeType: "image/png" } },
1948
+ },
1949
+ ])("skips malformed $name while preserving valid content", ({ part }) => {
1950
+ const acc = new AdkEventAccumulator();
1951
+ const msgs = acc.processEvent(
1952
+ makeEvent({
1953
+ author: "agent",
1954
+ content: {
1955
+ role: "model",
1956
+ parts: [part as any, { text: "still here" }],
1957
+ },
1958
+ }),
1959
+ );
1960
+
1961
+ expect(msgs).toMatchObject([
1962
+ { type: "ai", content: [{ type: "text", text: "still here" }] },
1963
+ ]);
1964
+ });
1965
+
1966
+ it("skips malformed user media without creating an empty human message", () => {
1967
+ const acc = new AdkEventAccumulator();
1968
+ const malformedMedia = [
1969
+ { inlineData: { data: "abc123" } },
1970
+ { fileData: { mimeType: "application/pdf" } },
1971
+ ] as any[];
1972
+
1973
+ expect(
1974
+ acc.processEvent(
1975
+ makeEvent({
1976
+ author: "user",
1977
+ content: { role: "user", parts: malformedMedia },
1978
+ }),
1979
+ ),
1980
+ ).toEqual([]);
1981
+
1982
+ const msgs = acc.processEvent(
1983
+ makeEvent({
1984
+ author: "user",
1985
+ content: {
1986
+ role: "user",
1987
+ parts: [...malformedMedia, { text: "still here" }],
1988
+ },
1989
+ }),
1990
+ );
1991
+
1992
+ expect(msgs).toMatchObject([{ type: "human", content: "still here" }]);
1993
+ });
1994
+
1418
1995
  it("tool result events (no author, role:'user') still create tool messages", () => {
1419
1996
  // Regression: the user-author check must not hijack tool events.
1420
1997
  // messageToEvent for `type:'tool'` emits events without `author`,