@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 { 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,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
+ };
@@ -22,6 +22,7 @@ import {
22
22
  import { projectAdkToolApprovals } from "./adkToolApproval";
23
23
  import { createAdkStream } from "./AdkClient";
24
24
  import { AdkEventAccumulator } from "./AdkEventAccumulator";
25
+ import { getPendingCancellations } from "./convertToAdkMessages";
25
26
  import type { AdkEvent, AdkMessage, AdkStreamCallback } from "./types";
26
27
 
27
28
  afterEach(() => {
@@ -29,6 +30,53 @@ afterEach(() => {
29
30
  vi.unstubAllGlobals();
30
31
  });
31
32
 
33
+ describe("optimistic tool outcomes", () => {
34
+ it.each([false, true])(
35
+ "preserves failures alongside successful results (batch: %s)",
36
+ (batch) => {
37
+ const failed: AdkMessage = {
38
+ id: "failed",
39
+ type: "tool",
40
+ name: "search",
41
+ tool_call_id: "tc-error",
42
+ content: "denied",
43
+ status: "error",
44
+ };
45
+ const succeeded: AdkMessage = {
46
+ id: "succeeded",
47
+ type: "tool",
48
+ name: "search",
49
+ tool_call_id: "tc-ok",
50
+ content: "found",
51
+ status: "success",
52
+ };
53
+ const events = batch
54
+ ? messagesToEvents([
55
+ failed,
56
+ succeeded,
57
+ { id: "human", type: "human", content: "continue" },
58
+ ])
59
+ : [messageToEvent(failed), messageToEvent(succeeded)];
60
+ const acc = new AdkEventAccumulator();
61
+ for (const event of events) acc.processEvent(event);
62
+ expect(
63
+ acc.getMessages().filter((message) => message.type === "tool"),
64
+ ).toMatchObject([
65
+ {
66
+ tool_call_id: "tc-error",
67
+ status: "error",
68
+ content: JSON.stringify({ error: "denied" }),
69
+ },
70
+ {
71
+ tool_call_id: "tc-ok",
72
+ status: "success",
73
+ content: JSON.stringify({ result: "found" }),
74
+ },
75
+ ]);
76
+ },
77
+ );
78
+ });
79
+
32
80
  describe("ADK runtime callbacks", () => {
33
81
  it.each(["onAgentTransfer", "onCustomEvent", "onError"] as const)(
34
82
  "continues streaming when %s throws",
@@ -315,6 +363,86 @@ describe("optimistic confirmation replies", () => {
315
363
  return result;
316
364
  };
317
365
 
366
+ it("preserves an unanswered gate across a reply run", async () => {
367
+ let run = 0;
368
+ const stream: AdkStreamCallback = async function* () {
369
+ run += 1;
370
+ if (run === 1) {
371
+ yield {
372
+ id: "gates",
373
+ author: "agent",
374
+ longRunningToolIds: ["conf-a", "conf-b"],
375
+ content: {
376
+ role: "model",
377
+ parts: [
378
+ {
379
+ functionCall: {
380
+ id: "conf-a",
381
+ name: "adk_request_confirmation",
382
+ args: {},
383
+ },
384
+ },
385
+ {
386
+ functionCall: {
387
+ id: "conf-b",
388
+ name: "adk_request_confirmation",
389
+ args: {},
390
+ },
391
+ },
392
+ ],
393
+ },
394
+ } satisfies AdkEvent;
395
+ } else {
396
+ yield {
397
+ id: "rerun",
398
+ author: "agent",
399
+ content: {
400
+ role: "user",
401
+ parts: [
402
+ {
403
+ functionResponse: {
404
+ id: "orig-conf-a",
405
+ name: "delete_file",
406
+ response: { result: "deleted" },
407
+ },
408
+ },
409
+ ],
410
+ },
411
+ } satisfies AdkEvent;
412
+ }
413
+ };
414
+ const { result } = renderHook(() => useAdkMessages({ stream }));
415
+
416
+ await act(async () => {
417
+ await result.current.sendMessage(
418
+ [{ id: "user-1", type: "human", content: "start" }],
419
+ {},
420
+ );
421
+ });
422
+ expect(result.current.longRunningToolIds).toEqual(["conf-a", "conf-b"]);
423
+
424
+ await act(async () => {
425
+ await result.current.sendMessage(
426
+ [
427
+ confirmationReply(
428
+ "reply-a",
429
+ "conf-a",
430
+ JSON.stringify({ confirmed: true }),
431
+ ),
432
+ ],
433
+ {},
434
+ );
435
+ });
436
+
437
+ expect(result.current.longRunningToolIds).toEqual(["conf-b"]);
438
+ expect(
439
+ getPendingCancellations(
440
+ result.current.messages,
441
+ result.current.longRunningToolIds,
442
+ ),
443
+ ).toEqual([]);
444
+ });
445
+
318
446
  it("keeps both gates pending when one send carries an unreadable reply", async () => {
319
447
  const result = await renderWithGates();
320
448
 
@@ -538,6 +666,26 @@ describe("optimistic multi-message sends", () => {
538
666
  });
539
667
 
540
668
  describe("messageToEvent (contentToParts)", () => {
669
+ it.each([
670
+ ["scalar", "false", { result: false }],
671
+ ["array", "[1,2]", { results: [1, 2] }],
672
+ ])(
673
+ "normalizes an optimistic %s tool response",
674
+ (_label, content, response) => {
675
+ const event = messageToEvent({
676
+ id: "tool-1",
677
+ type: "tool",
678
+ content,
679
+ tool_call_id: "call-1",
680
+ name: "search",
681
+ });
682
+
683
+ expect(event.content?.parts[0]?.functionResponse?.response).toEqual(
684
+ response,
685
+ );
686
+ },
687
+ );
688
+
541
689
  it("serializes a file content part as inlineData", () => {
542
690
  const msg: AdkMessage = {
543
691
  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,23 @@ 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());
158
173
 
159
174
  // Google ADK replaces active runs, while React LangGraph queues sends.
160
175
  abortControllerRef.current?.abort();
@@ -255,6 +270,7 @@ export const useAdkMessages = ({
255
270
  [
256
271
  aui,
257
272
  setMessagesImmediate,
273
+ setLongRunningToolIds,
258
274
  stream,
259
275
  onError,
260
276
  onCustomEvent,
@@ -363,7 +379,7 @@ export const messageToEvent = (msg: AdkMessage): AdkEvent => {
363
379
  functionResponse: {
364
380
  name: msg.name,
365
381
  id: msg.tool_call_id,
366
- response,
382
+ response: toAdkFunctionResponse(response, msg.status === "error"),
367
383
  },
368
384
  },
369
385
  ],
@@ -57,6 +57,7 @@ import {
57
57
  toAdkToolConfirmationReply,
58
58
  } from "./adkToolApproval";
59
59
  import { adkExtras } from "./adkExtras";
60
+ import { ADK_SDK } from "./sdkIdentity";
60
61
 
61
62
  export type UseAdkRuntimeOptions = ExternalStoreSharedOptions & {
62
63
  stream: AdkStreamCallback;
@@ -504,6 +505,7 @@ export const useAdkRuntime = ({
504
505
  }: UseAdkRuntimeOptions) => {
505
506
  const aui = useAui();
506
507
  const cloudAdapter = useCloudThreadListAdapter({
508
+ sdk: ADK_SDK,
507
509
  cloud,
508
510
  create: createCloudThreadListAdapterCreateFallback(
509
511
  create,
@@ -97,7 +97,6 @@ const approvalPart = () =>
97
97
  afterEach(() => {
98
98
  mocks.adapters.length = 0;
99
99
  mocks.messages = [];
100
- vi.clearAllMocks();
101
100
  });
102
101
 
103
102
  describe("useAdkRuntime tool approvals", () => {