@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
@@ -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
  });
@@ -1,5 +1,6 @@
1
1
  import { isRecord } from "@assistant-ui/core/internal";
2
2
  import type { AdkSendMessageConfig } from "../types";
3
+ import { toAdkFunctionResponse } from "../toAdkFunctionResponse";
3
4
 
4
5
  type ParsedAdkRequest =
5
6
  | {
@@ -240,7 +241,7 @@ export const toAdkContent = (
240
241
  functionResponse: {
241
242
  name: parsed.toolName,
242
243
  id: parsed.toolCallId,
243
- response: parsed.result,
244
+ response: toAdkFunctionResponse(parsed.result, parsed.isError),
244
245
  },
245
246
  },
246
247
  ],
@@ -0,0 +1,69 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { toAdkStructuredEvents } from "./structuredEvents";
3
+ import type { AdkEvent } from "./types";
4
+
5
+ const makeEvent = (overrides: Partial<AdkEvent> = {}): AdkEvent => ({
6
+ id: "evt-1",
7
+ ...overrides,
8
+ });
9
+
10
+ describe("toAdkStructuredEvents", () => {
11
+ it("converts a function call with args", () => {
12
+ expect(
13
+ toAdkStructuredEvents(
14
+ makeEvent({
15
+ author: "agent",
16
+ content: {
17
+ role: "model",
18
+ parts: [
19
+ {
20
+ functionCall: { name: "search", id: "tc-1", args: { q: "x" } },
21
+ },
22
+ ],
23
+ },
24
+ }),
25
+ ),
26
+ ).toEqual([
27
+ {
28
+ type: "tool_call",
29
+ call: { name: "search", id: "tc-1", args: { q: "x" } },
30
+ },
31
+ ]);
32
+ });
33
+
34
+ it("defaults a function call without args to empty args", () => {
35
+ expect(
36
+ toAdkStructuredEvents(
37
+ makeEvent({
38
+ author: "agent",
39
+ content: {
40
+ role: "model",
41
+ parts: [{ functionCall: { name: "search", id: "tc-1" } }],
42
+ },
43
+ }),
44
+ ),
45
+ ).toEqual([
46
+ { type: "tool_call", call: { name: "search", id: "tc-1", args: {} } },
47
+ ]);
48
+ });
49
+
50
+ it("keeps the other parts of an event carrying a call without args", () => {
51
+ expect(
52
+ toAdkStructuredEvents(
53
+ makeEvent({
54
+ author: "agent",
55
+ content: {
56
+ role: "model",
57
+ parts: [
58
+ { functionCall: { name: "search" } },
59
+ { text: "still here" },
60
+ ],
61
+ },
62
+ }),
63
+ ),
64
+ ).toEqual([
65
+ { type: "tool_call", call: { name: "search", args: {} } },
66
+ { type: "content", content: "still here" },
67
+ ]);
68
+ });
69
+ });
@@ -28,7 +28,10 @@ export function toAdkStructuredEvents(event: AdkEvent): AdkStructuredEvent[] {
28
28
  if (part.functionCall) {
29
29
  const call: AdkStructuredEvent & { type: "tool_call" } = {
30
30
  type: "tool_call",
31
- call: { name: part.functionCall.name, args: part.functionCall.args },
31
+ call: {
32
+ name: part.functionCall.name,
33
+ args: part.functionCall.args ?? {},
34
+ },
32
35
  };
33
36
  if (part.functionCall.id != null) call.call.id = part.functionCall.id;
34
37
  result.push(call);
@@ -0,0 +1,46 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ isAdkFunctionError,
4
+ toAdkFunctionResponse,
5
+ } from "./toAdkFunctionResponse";
6
+
7
+ describe("ADK function response errors", () => {
8
+ it("retains an existing error envelope without mutating or nesting it", () => {
9
+ const response = Object.freeze({ error: "denied", output: "partial" });
10
+ expect(toAdkFunctionResponse(response, true)).toBe(response);
11
+ });
12
+
13
+ it("preserves a failure with no details through JSON serialization", () => {
14
+ const response = JSON.parse(
15
+ JSON.stringify(toAdkFunctionResponse(undefined, true)),
16
+ );
17
+ expect(response).toEqual({ error: null });
18
+ expect(isAdkFunctionError(response)).toBe(true);
19
+ });
20
+
21
+ it("does not classify an undefined error field as an on-wire error", () => {
22
+ expect(isAdkFunctionError({ error: undefined })).toBe(false);
23
+ expect(
24
+ isAdkFunctionError(JSON.parse(JSON.stringify({ error: undefined }))),
25
+ ).toBe(false);
26
+ });
27
+
28
+ it("does not classify inherited error fields as an on-wire error", () => {
29
+ const response = Object.create({ error: "inherited" });
30
+ response.output = "done";
31
+ expect(isAdkFunctionError(response)).toBe(false);
32
+ });
33
+
34
+ it.each([
35
+ ["done", { result: "done" }],
36
+ [[1, 2], { results: [1, 2] }],
37
+ [
38
+ { output: { error: "application data" } },
39
+ { output: { error: "application data" } },
40
+ ],
41
+ [null, { result: null }],
42
+ ])("leaves successful result %j unchanged", (result, expected) => {
43
+ expect(toAdkFunctionResponse(result)).toEqual(expected);
44
+ expect(isAdkFunctionError(toAdkFunctionResponse(result))).toBe(false);
45
+ });
46
+ });
@@ -0,0 +1,18 @@
1
+ import { isRecord } from "@assistant-ui/core/internal";
2
+
3
+ export const isAdkFunctionError = (response: unknown): boolean =>
4
+ isRecord(response) &&
5
+ Object.hasOwn(response, "error") &&
6
+ response.error !== undefined;
7
+
8
+ export const toAdkFunctionResponse = (
9
+ result: unknown,
10
+ isError = false,
11
+ ): Record<string, unknown> => {
12
+ if (isError && !isAdkFunctionError(result)) return { error: result ?? null };
13
+ return Array.isArray(result)
14
+ ? { results: result }
15
+ : isRecord(result)
16
+ ? result
17
+ : { result };
18
+ };
package/src/types.ts CHANGED
@@ -9,7 +9,7 @@ export type AdkEventPart = {
9
9
  functionCall?: {
10
10
  name: string;
11
11
  id?: string;
12
- args: Record<string, unknown>;
12
+ args?: Record<string, unknown>;
13
13
  };
14
14
  functionResponse?: {
15
15
  name: string;
@@ -17,11 +17,11 @@ import {
17
17
  messageToEvent,
18
18
  messagesToEvents,
19
19
  useAdkMessages,
20
- type UseAdkMessagesOptions,
21
20
  } from "./useAdkMessages";
22
21
  import { projectAdkToolApprovals } from "./adkToolApproval";
23
22
  import { createAdkStream } from "./AdkClient";
24
23
  import { AdkEventAccumulator } from "./AdkEventAccumulator";
24
+ import { getPendingCancellations } from "./convertToAdkMessages";
25
25
  import type { AdkEvent, AdkMessage, AdkStreamCallback } from "./types";
26
26
 
27
27
  afterEach(() => {
@@ -29,6 +29,53 @@ afterEach(() => {
29
29
  vi.unstubAllGlobals();
30
30
  });
31
31
 
32
+ describe("optimistic tool outcomes", () => {
33
+ it.each([false, true])(
34
+ "preserves failures alongside successful results (batch: %s)",
35
+ (batch) => {
36
+ const failed: AdkMessage = {
37
+ id: "failed",
38
+ type: "tool",
39
+ name: "search",
40
+ tool_call_id: "tc-error",
41
+ content: "denied",
42
+ status: "error",
43
+ };
44
+ const succeeded: AdkMessage = {
45
+ id: "succeeded",
46
+ type: "tool",
47
+ name: "search",
48
+ tool_call_id: "tc-ok",
49
+ content: "found",
50
+ status: "success",
51
+ };
52
+ const events = batch
53
+ ? messagesToEvents([
54
+ failed,
55
+ succeeded,
56
+ { id: "human", type: "human", content: "continue" },
57
+ ])
58
+ : [messageToEvent(failed), messageToEvent(succeeded)];
59
+ const acc = new AdkEventAccumulator();
60
+ for (const event of events) acc.processEvent(event);
61
+ expect(
62
+ acc.getMessages().filter((message) => message.type === "tool"),
63
+ ).toMatchObject([
64
+ {
65
+ tool_call_id: "tc-error",
66
+ status: "error",
67
+ content: JSON.stringify({ error: "denied" }),
68
+ },
69
+ {
70
+ tool_call_id: "tc-ok",
71
+ status: "success",
72
+ content: JSON.stringify({ result: "found" }),
73
+ },
74
+ ]);
75
+ },
76
+ );
77
+ });
78
+
32
79
  describe("ADK runtime callbacks", () => {
33
80
  it.each(["onAgentTransfer", "onCustomEvent", "onError"] as const)(
34
81
  "continues streaming when %s throws",
@@ -65,7 +112,7 @@ describe("ADK runtime callbacks", () => {
65
112
  };
66
113
  const eventHandlers = {
67
114
  [callbackName]: callback,
68
- } as UseAdkMessagesOptions["eventHandlers"];
115
+ };
69
116
  const { result } = renderHook(() =>
70
117
  useAdkMessages({ stream, eventHandlers }),
71
118
  );
@@ -315,6 +362,86 @@ describe("optimistic confirmation replies", () => {
315
362
  return result;
316
363
  };
317
364
 
365
+ it("preserves an unanswered gate across a reply run", async () => {
366
+ let run = 0;
367
+ const stream: AdkStreamCallback = async function* () {
368
+ run += 1;
369
+ if (run === 1) {
370
+ yield {
371
+ id: "gates",
372
+ author: "agent",
373
+ longRunningToolIds: ["conf-a", "conf-b"],
374
+ content: {
375
+ role: "model",
376
+ parts: [
377
+ {
378
+ functionCall: {
379
+ id: "conf-a",
380
+ name: "adk_request_confirmation",
381
+ args: {},
382
+ },
383
+ },
384
+ {
385
+ functionCall: {
386
+ id: "conf-b",
387
+ name: "adk_request_confirmation",
388
+ args: {},
389
+ },
390
+ },
391
+ ],
392
+ },
393
+ } satisfies AdkEvent;
394
+ } else {
395
+ yield {
396
+ id: "rerun",
397
+ author: "agent",
398
+ content: {
399
+ role: "user",
400
+ parts: [
401
+ {
402
+ functionResponse: {
403
+ id: "orig-conf-a",
404
+ name: "delete_file",
405
+ response: { result: "deleted" },
406
+ },
407
+ },
408
+ ],
409
+ },
410
+ } satisfies AdkEvent;
411
+ }
412
+ };
413
+ const { result } = renderHook(() => useAdkMessages({ stream }));
414
+
415
+ await act(async () => {
416
+ await result.current.sendMessage(
417
+ [{ id: "user-1", type: "human", content: "start" }],
418
+ {},
419
+ );
420
+ });
421
+ expect(result.current.longRunningToolIds).toEqual(["conf-a", "conf-b"]);
422
+
423
+ await act(async () => {
424
+ await result.current.sendMessage(
425
+ [
426
+ confirmationReply(
427
+ "reply-a",
428
+ "conf-a",
429
+ JSON.stringify({ confirmed: true }),
430
+ ),
431
+ ],
432
+ {},
433
+ );
434
+ });
435
+
436
+ expect(result.current.longRunningToolIds).toEqual(["conf-b"]);
437
+ expect(
438
+ getPendingCancellations(
439
+ result.current.messages,
440
+ result.current.longRunningToolIds,
441
+ ),
442
+ ).toEqual([]);
443
+ });
444
+
318
445
  it("keeps both gates pending when one send carries an unreadable reply", async () => {
319
446
  const result = await renderWithGates();
320
447
 
@@ -338,6 +465,10 @@ describe("optimistic confirmation replies", () => {
338
465
  projectAdkToolApprovals(result.current.messages).approvals.values(),
339
466
  ),
340
467
  ]).toEqual([{ id: "conf-a" }, { id: "conf-b" }]);
468
+ expect(result.current.toolConfirmations.map((c) => c.toolCallId)).toEqual([
469
+ "conf-a",
470
+ "conf-b",
471
+ ]);
341
472
  });
342
473
 
343
474
  it("keeps both gates pending when an ai message sits between the replies", async () => {
@@ -394,6 +525,102 @@ describe("optimistic confirmation replies", () => {
394
525
  projectAdkToolApprovals(result.current.messages).approvals.values(),
395
526
  ),
396
527
  ]).toEqual([{ id: "conf-a", approved: true }, { id: "conf-b" }]);
528
+ expect(result.current.toolConfirmations.map((c) => c.toolCallId)).toEqual([
529
+ "conf-b",
530
+ ]);
531
+ });
532
+ });
533
+
534
+ describe("pending requests across sends", () => {
535
+ it("keeps an unanswered request listed across sends until its reply is sent", async () => {
536
+ let run = 0;
537
+ const stream: AdkStreamCallback = async function* () {
538
+ run += 1;
539
+ if (run === 1) {
540
+ yield {
541
+ id: "requests",
542
+ author: "agent",
543
+ longRunningToolIds: ["conf-1", "cred-1"],
544
+ content: {
545
+ role: "model",
546
+ parts: [
547
+ {
548
+ functionCall: {
549
+ id: "conf-1",
550
+ name: "adk_request_confirmation",
551
+ args: {
552
+ originalFunctionCall: { id: "gated-1", name: "transfer" },
553
+ toolConfirmation: { hint: "Transfer?" },
554
+ },
555
+ },
556
+ },
557
+ {
558
+ functionCall: {
559
+ id: "cred-1",
560
+ name: "adk_request_credential",
561
+ args: {
562
+ function_call_id: "gated-2",
563
+ auth_config: { credentialKey: "k" },
564
+ },
565
+ },
566
+ },
567
+ ],
568
+ },
569
+ } satisfies AdkEvent;
570
+ } else if (run === 2) {
571
+ yield {
572
+ id: "answer",
573
+ author: "agent",
574
+ content: { role: "model", parts: [{ text: "still waiting" }] },
575
+ } satisfies AdkEvent;
576
+ }
577
+ };
578
+ const { result } = renderHook(() => useAdkMessages({ stream }));
579
+ const pending = () => ({
580
+ confirmations: result.current.toolConfirmations.map((c) => c.toolCallId),
581
+ authRequests: result.current.authRequests.map((r) => r.toolCallId),
582
+ });
583
+
584
+ await act(async () => {
585
+ await result.current.sendMessage(
586
+ [{ id: "user-1", type: "human", content: "start" }],
587
+ {},
588
+ );
589
+ });
590
+ expect(pending()).toEqual({
591
+ confirmations: ["conf-1"],
592
+ authRequests: ["cred-1"],
593
+ });
594
+
595
+ await act(async () => {
596
+ await result.current.sendMessage(
597
+ [{ id: "user-2", type: "human", content: "any news?" }],
598
+ {},
599
+ );
600
+ });
601
+ expect(pending()).toEqual({
602
+ confirmations: ["conf-1"],
603
+ authRequests: ["cred-1"],
604
+ });
605
+
606
+ await act(async () => {
607
+ await result.current.sendMessage(
608
+ [
609
+ {
610
+ id: "reply",
611
+ type: "tool",
612
+ tool_call_id: "cred-1",
613
+ name: "adk_request_credential",
614
+ content: JSON.stringify({
615
+ exchangedAuthCredential: { authType: "apiKey" },
616
+ }),
617
+ status: "success",
618
+ },
619
+ ],
620
+ {},
621
+ );
622
+ });
623
+ expect(pending()).toEqual({ confirmations: ["conf-1"], authRequests: [] });
397
624
  });
398
625
  });
399
626
 
@@ -538,6 +765,26 @@ describe("optimistic multi-message sends", () => {
538
765
  });
539
766
 
540
767
  describe("messageToEvent (contentToParts)", () => {
768
+ it.each([
769
+ ["scalar", "false", { result: false }],
770
+ ["array", "[1,2]", { results: [1, 2] }],
771
+ ])(
772
+ "normalizes an optimistic %s tool response",
773
+ (_label, content, response) => {
774
+ const event = messageToEvent({
775
+ id: "tool-1",
776
+ type: "tool",
777
+ content,
778
+ tool_call_id: "call-1",
779
+ name: "search",
780
+ });
781
+
782
+ expect(event.content?.parts?.[0]?.functionResponse?.response).toEqual(
783
+ response,
784
+ );
785
+ },
786
+ );
787
+
541
788
  it("serializes a file content part as inlineData", () => {
542
789
  const msg: AdkMessage = {
543
790
  id: "m1",
@@ -15,6 +15,7 @@ import {
15
15
  } from "@assistant-ui/core/internal";
16
16
  import { AdkEventAccumulator } from "./AdkEventAccumulator";
17
17
  import { contentToParts } from "./contentToParts";
18
+ import { toAdkFunctionResponse } from "./toAdkFunctionResponse";
18
19
  import type {
19
20
  AdkEvent,
20
21
  AdkMessage,
@@ -58,7 +59,7 @@ export const useAdkMessages = ({
58
59
  name?: string | undefined;
59
60
  branch?: string | undefined;
60
61
  }>({});
61
- const [longRunningToolIds, setLongRunningToolIds] = useState<string[]>([]);
62
+ const [longRunningToolIds, _setLongRunningToolIds] = useState<string[]>([]);
62
63
  const [artifactDelta, setArtifactDelta] = useState<Record<string, number>>(
63
64
  {},
64
65
  );
@@ -71,9 +72,9 @@ export const useAdkMessages = ({
71
72
  Map<string, AdkMessageMetadata>
72
73
  >(new Map());
73
74
  const lastTransferToAgentRef = useRef<string | undefined>(undefined);
74
- // setMessagesImmediate is the only writer of the messages state and publishes
75
- // this ref with it, so the ref never trails a commit.
75
+ // setMessagesImmediate and setLongRunningToolIds are the only writers of their state and publish these refs with it, so neither ref trails a commit.
76
76
  const messagesRef = useRef(messages);
77
+ const longRunningToolIdsRef = useRef(longRunningToolIds);
77
78
  const stateDeltaRef = useRef(stateDelta);
78
79
  useInsertionEffect(() => {
79
80
  stateDeltaRef.current = stateDelta;
@@ -91,6 +92,10 @@ export const useAdkMessages = ({
91
92
  messagesRef.current = msgs;
92
93
  _setMessages(msgs);
93
94
  }, []);
95
+ const setLongRunningToolIds = useCallback((ids: string[]) => {
96
+ longRunningToolIdsRef.current = ids;
97
+ _setLongRunningToolIds(ids);
98
+ }, []);
94
99
 
95
100
  /**
96
101
  * Swap the thread over to a loaded snapshot in one commit. Unlike
@@ -110,7 +115,7 @@ export const useAdkMessages = ({
110
115
  setArtifactDelta(snapshot.artifactDelta ?? {});
111
116
  setAgentInfo(snapshot.agentInfo ?? {});
112
117
  },
113
- [setMessagesImmediate],
118
+ [setLongRunningToolIds, setMessagesImmediate],
114
119
  );
115
120
 
116
121
  // Replace the message list AND reset derived per-turn HITL state.
@@ -126,7 +131,7 @@ export const useAdkMessages = ({
126
131
  setEscalated(false);
127
132
  setMessageMetadata(new Map());
128
133
  },
129
- [setMessagesImmediate],
134
+ [setLongRunningToolIds, setMessagesImmediate],
130
135
  );
131
136
 
132
137
  const abortControllerRef = useRef<AbortController | null>(null);
@@ -148,13 +153,25 @@ export const useAdkMessages = ({
148
153
  // with the originals would leave every later staged id beside the merged
149
154
  // copy of itself.
150
155
  const resentIds = new Set(newMessagesWithId.map((m) => m.id));
156
+ // The optimistic event for a tool-only batch carries no author, so the accumulator cannot settle the calls this send answers.
157
+ const answeredToolCallIds = new Set(
158
+ newMessagesWithId.flatMap((m) =>
159
+ m.type === "tool" ? [m.tool_call_id] : [],
160
+ ),
161
+ );
151
162
  const accumulator = new AdkEventAccumulator(
152
163
  messagesRef.current.filter((m) => !resentIds.has(m.id)),
164
+ longRunningToolIdsRef.current.filter(
165
+ (id) => !answeredToolCallIds.has(id),
166
+ ),
153
167
  );
154
168
  for (const event of messagesToEvents(newMessagesWithId)) {
155
169
  accumulator.processEvent(event);
156
170
  }
157
171
  setMessagesImmediate(accumulator.getMessages());
172
+ setLongRunningToolIds(accumulator.getLongRunningToolIds());
173
+ setToolConfirmations(accumulator.getToolConfirmations());
174
+ setAuthRequests(accumulator.getAuthRequests());
158
175
 
159
176
  // Google ADK replaces active runs, while React LangGraph queues sends.
160
177
  abortControllerRef.current?.abort();
@@ -255,6 +272,7 @@ export const useAdkMessages = ({
255
272
  [
256
273
  aui,
257
274
  setMessagesImmediate,
275
+ setLongRunningToolIds,
258
276
  stream,
259
277
  onError,
260
278
  onCustomEvent,
@@ -363,7 +381,7 @@ export const messageToEvent = (msg: AdkMessage): AdkEvent => {
363
381
  functionResponse: {
364
382
  name: msg.name,
365
383
  id: msg.tool_call_id,
366
- response,
384
+ response: toAdkFunctionResponse(response, msg.status === "error"),
367
385
  },
368
386
  },
369
387
  ],