@assistant-ui/react-google-adk 0.0.28 → 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 (56) 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 +33 -12
  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/hooks.d.ts.map +1 -1
  13. package/dist/hooks.js +6 -8
  14. package/dist/hooks.js.map +1 -1
  15. package/dist/parseAdkEvent.d.ts.map +1 -1
  16. package/dist/parseAdkEvent.js +15 -2
  17. package/dist/parseAdkEvent.js.map +1 -1
  18. package/dist/sdkIdentity.d.ts +6 -0
  19. package/dist/sdkIdentity.d.ts.map +1 -0
  20. package/dist/sdkIdentity.js +9 -0
  21. package/dist/sdkIdentity.js.map +1 -0
  22. package/dist/server/parseAdkRequest.d.ts.map +1 -1
  23. package/dist/server/parseAdkRequest.js +2 -1
  24. package/dist/server/parseAdkRequest.js.map +1 -1
  25. package/dist/toAdkFunctionResponse.d.ts +6 -0
  26. package/dist/toAdkFunctionResponse.d.ts.map +1 -0
  27. package/dist/toAdkFunctionResponse.js +11 -0
  28. package/dist/toAdkFunctionResponse.js.map +1 -0
  29. package/dist/useAdkMessages.d.ts.map +1 -1
  30. package/dist/useAdkMessages.js +22 -10
  31. package/dist/useAdkMessages.js.map +1 -1
  32. package/dist/useAdkRuntime.d.ts.map +1 -1
  33. package/dist/useAdkRuntime.js +6 -2
  34. package/dist/useAdkRuntime.js.map +1 -1
  35. package/package.json +6 -6
  36. package/src/AdkClient.test.ts +192 -2
  37. package/src/AdkClient.ts +2 -1
  38. package/src/AdkEventAccumulator.test.ts +297 -0
  39. package/src/AdkEventAccumulator.ts +35 -5
  40. package/src/AdkSessionAdapter.test.ts +88 -0
  41. package/src/convertToAdkMessages.test.ts +70 -0
  42. package/src/convertToAdkMessages.ts +19 -4
  43. package/src/hooks.render.test.tsx +74 -0
  44. package/src/hooks.test.tsx +33 -0
  45. package/src/hooks.ts +17 -22
  46. package/src/parseAdkEvent.ts +36 -7
  47. package/src/sdkIdentity.ts +9 -0
  48. package/src/server/parseAdkRequest.test.ts +63 -0
  49. package/src/server/parseAdkRequest.ts +2 -1
  50. package/src/toAdkFunctionResponse.test.ts +46 -0
  51. package/src/toAdkFunctionResponse.ts +18 -0
  52. package/src/useAdkMessages.test.ts +247 -0
  53. package/src/useAdkMessages.ts +51 -15
  54. package/src/useAdkRuntime.replacement.test.tsx +138 -0
  55. package/src/useAdkRuntime.ts +5 -1
  56. package/src/useAdkRuntimeApproval.test.tsx +0 -1
@@ -1,6 +1,8 @@
1
1
  import { describe, it, expect, vi, beforeEach } from "vitest";
2
2
  import { createAdkStream } from "./AdkClient";
3
3
  import { adkEventStream } from "./server/adkEventStream";
4
+ import { AdkEventAccumulator } from "./AdkEventAccumulator";
5
+ import { parseAdkRequest, toAdkContent } from "./server/parseAdkRequest";
4
6
  import type { AdkEvent, AdkMessage, AdkSendMessageConfig } from "./types";
5
7
 
6
8
  // ── Helpers ──
@@ -65,9 +67,103 @@ beforeEach(() => {
65
67
  mockFetch.mockReset();
66
68
  });
67
69
 
70
+ describe.each(["direct", "proxy", "proxy batch"] as const)(
71
+ "%s tool outcomes",
72
+ (mode) => {
73
+ it.each(["error", "success"] as const)(
74
+ "preserves %s through the runner request",
75
+ async (status) => {
76
+ mockFetch.mockResolvedValueOnce(sseResponse(sseBody("")));
77
+ const stream = createAdkStream(
78
+ mode === "direct"
79
+ ? { api: "http://localhost:8000", appName: "app", userId: "user" }
80
+ : { api: "/api/adk" },
81
+ );
82
+ const messages: AdkMessage[] = [
83
+ {
84
+ id: "result",
85
+ type: "tool",
86
+ name: "search",
87
+ tool_call_id: "tc-1",
88
+ content: "permission denied",
89
+ status,
90
+ },
91
+ ];
92
+ if (mode === "proxy batch")
93
+ messages.push({ id: "human", type: "human", content: "continue" });
94
+ const events = await stream(messages, makeConfig());
95
+ for await (const event of events) expect(event).toBeUndefined();
96
+ const body = JSON.parse(mockFetch.mock.calls[0]![1]!.body as string);
97
+ const content =
98
+ mode === "direct"
99
+ ? body.newMessage
100
+ : toAdkContent(
101
+ await parseAdkRequest(
102
+ new Request("http://localhost/api/adk", {
103
+ method: "POST",
104
+ body: JSON.stringify(body),
105
+ headers: { "Content-Type": "application/json" },
106
+ }),
107
+ ),
108
+ );
109
+ expect(content.parts[0]).toEqual({
110
+ functionResponse: {
111
+ id: "tc-1",
112
+ name: "search",
113
+ response:
114
+ status === "error"
115
+ ? { error: "permission denied" }
116
+ : { result: "permission denied" },
117
+ },
118
+ });
119
+ },
120
+ );
121
+ },
122
+ );
123
+
68
124
  // ── Proxy mode ──
69
125
 
70
126
  describe("createAdkStream - proxy mode", () => {
127
+ it("accumulates snake_case image and file parts from SSE", async () => {
128
+ const event = {
129
+ id: "media",
130
+ author: "agent",
131
+ content: {
132
+ parts: [
133
+ { inline_data: { mime_type: "image/png", data: "aGVsbG8=" } },
134
+ {
135
+ file_data: {
136
+ mime_type: "application/pdf",
137
+ file_uri: "https://example.test/report.pdf",
138
+ },
139
+ },
140
+ ],
141
+ },
142
+ };
143
+ mockFetch.mockResolvedValueOnce(
144
+ sseResponse(sseBody(`data: ${JSON.stringify(event)}\n\n`)),
145
+ );
146
+ const stream = createAdkStream({ api: "/api/adk" });
147
+ const events = await stream(
148
+ [{ id: "human", type: "human", content: "show files" }],
149
+ makeConfig(),
150
+ );
151
+ const acc = new AdkEventAccumulator();
152
+ for await (const item of events) acc.processEvent(item);
153
+ expect(acc.getMessages()).toMatchObject([
154
+ {
155
+ content: [
156
+ { type: "image", mimeType: "image/png", data: "aGVsbG8=" },
157
+ {
158
+ type: "file_url",
159
+ mimeType: "application/pdf",
160
+ url: "https://example.test/report.pdf",
161
+ },
162
+ ],
163
+ },
164
+ ]);
165
+ });
166
+
71
167
  it("POSTs to the api URL directly", async () => {
72
168
  mockFetch.mockResolvedValueOnce(sseResponse(sseBody("")));
73
169
 
@@ -378,7 +474,7 @@ describe("createAdkStream - direct mode", () => {
378
474
  });
379
475
  });
380
476
 
381
- it("falls back to raw string when tool content is not valid JSON", async () => {
477
+ it("wraps non-JSON tool content in a function response object", async () => {
382
478
  mockFetch.mockResolvedValueOnce(sseResponse(sseBody("")));
383
479
 
384
480
  const stream = createAdkStream({
@@ -401,9 +497,50 @@ describe("createAdkStream - direct mode", () => {
401
497
  }
402
498
 
403
499
  const body = JSON.parse(mockFetch.mock.calls[0]![1]?.body as string);
404
- expect(body.newMessage.parts[0].functionResponse.response).toBe("not-json");
500
+ expect(body.newMessage.parts[0].functionResponse.response).toEqual({
501
+ result: "not-json",
502
+ });
405
503
  });
406
504
 
505
+ it.each([
506
+ ["false", { result: false }],
507
+ ["0", { result: 0 }],
508
+ ["null", { result: null }],
509
+ ['"done"', { result: "done" }],
510
+ ["[1,2]", { results: [1, 2] }],
511
+ ])(
512
+ "wraps scalar or array tool result %s in direct mode",
513
+ async (content, response) => {
514
+ mockFetch.mockResolvedValueOnce(sseResponse(sseBody("")));
515
+
516
+ const stream = createAdkStream({
517
+ api: "http://localhost:8000",
518
+ appName: "app",
519
+ userId: "u",
520
+ });
521
+ const gen = await stream(
522
+ [
523
+ {
524
+ id: "t1",
525
+ type: "tool",
526
+ content,
527
+ tool_call_id: "tc-1",
528
+ name: "search",
529
+ },
530
+ ],
531
+ makeConfig(),
532
+ );
533
+ for await (const _ of gen) {
534
+ /* noop */
535
+ }
536
+
537
+ const body = JSON.parse(mockFetch.mock.calls[0]![1]?.body as string);
538
+ expect(body.newMessage.parts[0].functionResponse.response).toEqual(
539
+ response,
540
+ );
541
+ },
542
+ );
543
+
407
544
  it("sends empty text part when no messages provided", async () => {
408
545
  mockFetch.mockResolvedValueOnce(sseResponse(sseBody("")));
409
546
 
@@ -472,6 +609,59 @@ describe("createAdkStream - SSE parsing", () => {
472
609
  },
473
610
  );
474
611
 
612
+ it.each([
613
+ [{ content: [] }, "content", "an object"],
614
+ [{ content: { parts: 42 } }, "content.parts", "an array of objects"],
615
+ [{ content: { parts: [null] } }, "content.parts", "an array of objects"],
616
+ ])(
617
+ "rejects malformed nested stream event content: %#",
618
+ async (event, field, expectation) => {
619
+ mockFetch.mockResolvedValueOnce(
620
+ sseResponse(sseBody(`data: ${JSON.stringify(event)}\n\n`)),
621
+ );
622
+
623
+ const stream = createAdkStream({ api: "/api/adk" });
624
+ const consume = async () => {
625
+ const gen = await stream(
626
+ [{ id: "m1", type: "human", content: "Hi" }],
627
+ makeConfig(),
628
+ );
629
+ for await (const _event of gen) {
630
+ void _event;
631
+ }
632
+ };
633
+
634
+ await expect(consume()).rejects.toThrow(
635
+ `Invalid ADK stream event: expected "${field}" to be ${expectation} when present.`,
636
+ );
637
+ },
638
+ );
639
+
640
+ it.each([
641
+ [{ id: "e1", content: null }, undefined],
642
+ [{ id: "e1", content: { role: "model", parts: null } }, { role: "model" }],
643
+ ])(
644
+ "accepts null optional nested stream event content: %#",
645
+ async (event, expectedContent) => {
646
+ mockFetch.mockResolvedValueOnce(
647
+ sseResponse(sseBody(`data: ${JSON.stringify(event)}\n\n`)),
648
+ );
649
+
650
+ const stream = createAdkStream({ api: "/api/adk" });
651
+ const gen = await stream(
652
+ [{ id: "m1", type: "human", content: "Hi" }],
653
+ makeConfig(),
654
+ );
655
+ const collected: AdkEvent[] = [];
656
+ for await (const parsedEvent of gen) {
657
+ collected.push(parsedEvent);
658
+ }
659
+
660
+ expect(collected).toHaveLength(1);
661
+ expect(collected[0]!.content).toEqual(expectedContent);
662
+ },
663
+ );
664
+
475
665
  it("reports invalid JSON as an ADK stream event error", async () => {
476
666
  mockFetch.mockResolvedValueOnce(sseResponse(sseBody("data: not-json\n\n")));
477
667
 
package/src/AdkClient.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { SSEEventDecoder } from "assistant-stream/utils";
2
2
  import { contentToParts } from "./contentToParts";
3
3
  import { parseAdkEventValue } from "./parseAdkEvent";
4
+ import { toAdkFunctionResponse } from "./toAdkFunctionResponse";
4
5
  import { trimTrailingSlashes } from "./trimTrailingSlashes";
5
6
  import type {
6
7
  AdkEvent,
@@ -180,7 +181,7 @@ function messagesToContent(messages: AdkMessage[]): {
180
181
  functionResponse: {
181
182
  name: msg.name,
182
183
  id: msg.tool_call_id,
183
- response,
184
+ response: toAdkFunctionResponse(response, msg.status === "error"),
184
185
  },
185
186
  });
186
187
  }
@@ -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();
@@ -838,6 +874,27 @@ describe("AdkEventAccumulator - actions tracking", () => {
838
874
  expect(acc.getArtifactDelta()).toEqual({ "file.txt": 1 });
839
875
  });
840
876
 
877
+ it("preserves prototype-named state and artifact keys", () => {
878
+ const acc = new AdkEventAccumulator();
879
+ acc.processEvent(
880
+ makeEvent({
881
+ actions: {
882
+ stateDelta: JSON.parse('{"__proto__":"session"}'),
883
+ artifactDelta: JSON.parse('{"__proto__":1}'),
884
+ },
885
+ author: "agent",
886
+ content: { parts: [{ text: "x" }] },
887
+ }),
888
+ );
889
+
890
+ const stateDelta = acc.getStateDelta();
891
+ const artifactDelta = acc.getArtifactDelta();
892
+ expect(Object.hasOwn(stateDelta, "__proto__")).toBe(true);
893
+ expect(stateDelta["__proto__"]).toBe("session");
894
+ expect(Object.hasOwn(artifactDelta, "__proto__")).toBe(true);
895
+ expect(artifactDelta["__proto__"]).toBe(1);
896
+ });
897
+
841
898
  it("tracks escalation flag", () => {
842
899
  const acc = new AdkEventAccumulator();
843
900
  expect(acc.isEscalated()).toBe(false);
@@ -912,6 +969,99 @@ describe("AdkEventAccumulator - actions tracking", () => {
912
969
  );
913
970
  expect(acc.getLongRunningToolIds()).toEqual(["lrt-1"]);
914
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
+ });
915
1065
  });
916
1066
 
917
1067
  describe("AdkEventAccumulator - special function calls", () => {
@@ -1023,6 +1173,153 @@ describe("AdkEventAccumulator - author/agent tracking", () => {
1023
1173
  });
1024
1174
 
1025
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
+
1026
1323
  it("normalizes function_call to functionCall in parts", () => {
1027
1324
  const acc = new AdkEventAccumulator();
1028
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))
@@ -194,8 +211,9 @@ export class AdkEventAccumulator {
194
211
  private finalTextReplacedThisEvent = false;
195
212
  private finalReasoningReplacedThisEvent = false;
196
213
  private partialReasoningBuffer = "";
197
- private accumulatedStateDelta: Record<string, unknown> = {};
198
- private accumulatedArtifactDelta: Record<string, number> = {};
214
+ private accumulatedStateDelta: Record<string, unknown> = Object.create(null);
215
+ private accumulatedArtifactDelta: Record<string, number> =
216
+ Object.create(null);
199
217
  private lastAgentInfo: {
200
218
  name?: string | undefined;
201
219
  branch?: string | undefined;
@@ -209,12 +227,18 @@ export class AdkEventAccumulator {
209
227
  // How many assistant messages each event has opened, so a replay of that
210
228
  // event opens them with the same ids.
211
229
  private aiMessageOrdinals = new Map<string, number>();
212
- constructor(initialMessages?: AdkMessage[]) {
230
+ constructor(
231
+ initialMessages?: AdkMessage[],
232
+ initialLongRunningToolIds?: readonly string[],
233
+ ) {
213
234
  if (initialMessages) {
214
235
  for (const msg of initialMessages) {
215
236
  this.messagesMap.set(msg.id, msg);
216
237
  }
217
238
  }
239
+ if (initialLongRunningToolIds) {
240
+ this.pendingLongRunningToolIds = new Set(initialLongRunningToolIds);
241
+ }
218
242
  }
219
243
 
220
244
  processEvent(rawEvent: AdkEvent): AdkMessage[] {
@@ -358,8 +382,12 @@ export class AdkEventAccumulator {
358
382
  tool_call_id: part.functionResponse.id,
359
383
  name: part.functionResponse.name,
360
384
  content: JSON.stringify(part.functionResponse.response),
361
- status: "success",
385
+ status: isAdkFunctionError(part.functionResponse.response)
386
+ ? "error"
387
+ : "success",
362
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);
363
391
  }
364
392
  }
365
393
  // The replies answer the preceding assistant turn, so they are emitted
@@ -540,7 +568,9 @@ export class AdkEventAccumulator {
540
568
  tool_call_id: part.functionResponse.id ?? "",
541
569
  name: part.functionResponse.name,
542
570
  content: JSON.stringify(part.functionResponse.response),
543
- status: "success",
571
+ status: isAdkFunctionError(part.functionResponse.response)
572
+ ? "error"
573
+ : "success",
544
574
  };
545
575
  this.messagesMap.set(toolMsg.id, toolMsg);
546
576
  return;