@assistant-ui/react-google-adk 0.0.35 → 0.0.36

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 (65) hide show
  1. package/README.md +12 -2
  2. package/dist/AdkClient.d.ts +4 -0
  3. package/dist/AdkClient.d.ts.map +1 -1
  4. package/dist/AdkClient.js +12 -7
  5. package/dist/AdkClient.js.map +1 -1
  6. package/dist/AdkSessionAdapter.d.ts.map +1 -1
  7. package/dist/AdkSessionAdapter.js +7 -7
  8. package/dist/AdkSessionAdapter.js.map +1 -1
  9. package/dist/AdkThreadController.d.ts +15 -0
  10. package/dist/AdkThreadController.d.ts.map +1 -0
  11. package/dist/AdkThreadController.js +35 -0
  12. package/dist/AdkThreadController.js.map +1 -0
  13. package/dist/adkThreadState.d.ts +54 -0
  14. package/dist/adkThreadState.d.ts.map +1 -0
  15. package/dist/adkThreadState.js +93 -0
  16. package/dist/adkThreadState.js.map +1 -0
  17. package/dist/convertToAdkMessages.js +1 -1
  18. package/dist/convertToAdkMessages.js.map +1 -1
  19. package/dist/sdkIdentity.js +1 -1
  20. package/dist/server/createAdkApiRoute.d.ts +37 -6
  21. package/dist/server/createAdkApiRoute.d.ts.map +1 -1
  22. package/dist/server/createAdkApiRoute.js +55 -5
  23. package/dist/server/createAdkApiRoute.js.map +1 -1
  24. package/dist/server/parseAdkRequest.d.ts +4 -1
  25. package/dist/server/parseAdkRequest.d.ts.map +1 -1
  26. package/dist/server/parseAdkRequest.js +5 -1
  27. package/dist/server/parseAdkRequest.js.map +1 -1
  28. package/dist/useAdkMessages.d.ts +9 -7
  29. package/dist/useAdkMessages.d.ts.map +1 -1
  30. package/dist/useAdkMessages.js +55 -77
  31. package/dist/useAdkMessages.js.map +1 -1
  32. package/dist/useAdkRuntime.d.ts +7 -1
  33. package/dist/useAdkRuntime.d.ts.map +1 -1
  34. package/dist/useAdkRuntime.js +134 -56
  35. package/dist/useAdkRuntime.js.map +1 -1
  36. package/package.json +5 -5
  37. package/src/AdkClient.test.ts +78 -2
  38. package/src/AdkClient.ts +24 -6
  39. package/src/AdkSessionAdapter.ts +1 -1
  40. package/src/AdkThreadController.test.ts +90 -0
  41. package/src/AdkThreadController.ts +45 -0
  42. package/src/adkThreadState.test.ts +207 -0
  43. package/src/adkThreadState.ts +124 -0
  44. package/src/convertToAdkMessages.test.ts +19 -0
  45. package/src/convertToAdkMessages.ts +1 -1
  46. package/src/hooks.test.tsx +1 -0
  47. package/src/server/createAdkApiRoute.controls.test.ts +66 -0
  48. package/src/server/createAdkApiRoute.test.ts +282 -0
  49. package/src/server/createAdkApiRoute.ts +119 -11
  50. package/src/server/parseAdkRequest.test.ts +11 -3
  51. package/src/server/parseAdkRequest.ts +7 -1
  52. package/src/useAdkMessages.test.ts +1 -0
  53. package/src/useAdkMessages.ts +61 -96
  54. package/src/useAdkRuntime.cancellation.test.tsx +4 -3
  55. package/src/useAdkRuntime.cloud-options.test.tsx +59 -0
  56. package/src/useAdkRuntime.refetch.test.tsx +548 -4
  57. package/src/useAdkRuntime.replacement.test.tsx +718 -1
  58. package/src/useAdkRuntime.ts +169 -73
  59. package/src/useAdkRuntimeApproval.test.tsx +87 -1
  60. package/dist/raceWithAbortSignal.d.ts +0 -2
  61. package/dist/raceWithAbortSignal.d.ts.map +0 -1
  62. package/dist/raceWithAbortSignal.js +0 -45
  63. package/dist/raceWithAbortSignal.js.map +0 -1
  64. package/src/raceWithAbortSignal.test.ts +0 -73
  65. package/src/raceWithAbortSignal.ts +0 -48
@@ -2,9 +2,9 @@ import { isRecord } from "@assistant-ui/core/internal";
2
2
  import {
3
3
  useState,
4
4
  useCallback,
5
- useInsertionEffect,
6
5
  useRef,
7
6
  useMemo,
7
+ useSyncExternalStore,
8
8
  } from "react";
9
9
  import { generateId } from "@assistant-ui/core";
10
10
  import { useReplaySafeEffect } from "@assistant-ui/store/internal";
@@ -15,16 +15,14 @@ import {
15
15
  openAbortableIterable,
16
16
  } from "@assistant-ui/core/internal";
17
17
  import { AdkEventAccumulator } from "./AdkEventAccumulator";
18
+ import { AdkThreadController } from "./AdkThreadController";
18
19
  import { contentToParts } from "./contentToParts";
19
20
  import { toAdkFunctionResponse } from "./toAdkFunctionResponse";
20
21
  import type {
21
22
  AdkEvent,
22
23
  AdkMessage,
23
- AdkMessageMetadata,
24
24
  AdkSendMessageConfig,
25
25
  AdkStreamCallback,
26
- AdkToolConfirmation,
27
- AdkAuthRequest,
28
26
  AdkThreadSnapshot,
29
27
  OnAdkErrorCallback,
30
28
  OnAdkCustomEventCallback,
@@ -59,49 +57,29 @@ const useAdkMessagesInternal = ({
59
57
  eventHandlers,
60
58
  onMessages,
61
59
  }: UseAdkMessagesInternalOptions) => {
62
- const [messages, _setMessages] = useState<AdkMessage[]>([]);
63
- const [stateDelta, setStateDelta] = useState<Record<string, unknown>>({});
64
- const [agentInfo, setAgentInfo] = useState<{
65
- name?: string | undefined;
66
- branch?: string | undefined;
67
- }>({});
68
- const [longRunningToolIds, _setLongRunningToolIds] = useState<string[]>([]);
69
- const [artifactDelta, setArtifactDelta] = useState<Record<string, number>>(
70
- {},
60
+ const [controller] = useState(() => new AdkThreadController());
61
+ const {
62
+ messages,
63
+ stateDelta,
64
+ agentInfo,
65
+ longRunningToolIds,
66
+ artifactDelta,
67
+ toolConfirmations,
68
+ authRequests,
69
+ escalated,
70
+ messageMetadata,
71
+ } = useSyncExternalStore(
72
+ controller.subscribe,
73
+ controller.getState,
74
+ controller.getState,
71
75
  );
72
- const [toolConfirmations, setToolConfirmations] = useState<
73
- AdkToolConfirmation[]
74
- >([]);
75
- const [authRequests, setAuthRequests] = useState<AdkAuthRequest[]>([]);
76
- const [escalated, setEscalated] = useState(false);
77
- const [messageMetadata, setMessageMetadata] = useState<
78
- Map<string, AdkMessageMetadata>
79
- >(new Map());
80
- // setMessagesImmediate and setLongRunningToolIds are the only writers of their state and publish these refs with it, so neither ref trails a commit.
81
- const messagesRef = useRef(messages);
82
- const longRunningToolIdsRef = useRef(longRunningToolIds);
83
- const stateDeltaRef = useRef(stateDelta);
84
- useInsertionEffect(() => {
85
- stateDeltaRef.current = stateDelta;
86
- }, [stateDelta]);
87
- const artifactDeltaRef = useRef(artifactDelta);
88
- useInsertionEffect(() => {
89
- artifactDeltaRef.current = artifactDelta;
90
- }, [artifactDelta]);
91
- const messageMetadataRef = useRef(messageMetadata);
92
- useInsertionEffect(() => {
93
- messageMetadataRef.current = messageMetadata;
94
- }, [messageMetadata]);
95
-
96
- const setMessagesImmediate = useCallback((msgs: AdkMessage[]) => {
97
- messagesRef.current = msgs;
98
- _setMessages(msgs);
99
- }, []);
100
- const setLongRunningToolIds = useCallback((ids: string[]) => {
101
- longRunningToolIdsRef.current = ids;
102
- _setLongRunningToolIds(ids);
103
- }, []);
104
76
 
77
+ const setMessagesImmediate = useCallback(
78
+ (msgs: AdkMessage[]) => {
79
+ controller.dispatch({ type: "messages.set", messages: msgs });
80
+ },
81
+ [controller],
82
+ );
105
83
  /**
106
84
  * Swap the thread over to a loaded snapshot in one commit. Unlike
107
85
  * {@link replaceMessages} this never passes through a cleared state, so a
@@ -110,17 +88,9 @@ const useAdkMessagesInternal = ({
110
88
  */
111
89
  const applySnapshot = useCallback(
112
90
  (snapshot: AdkThreadSnapshot) => {
113
- setMessagesImmediate(snapshot.messages);
114
- setLongRunningToolIds(snapshot.longRunningToolIds ?? []);
115
- setToolConfirmations(snapshot.toolConfirmations ?? []);
116
- setAuthRequests(snapshot.authRequests ?? []);
117
- setEscalated(snapshot.escalated ?? false);
118
- setMessageMetadata(snapshot.messageMetadata ?? new Map());
119
- setStateDelta(snapshot.stateDelta ?? {});
120
- setArtifactDelta(snapshot.artifactDelta ?? {});
121
- setAgentInfo(snapshot.agentInfo ?? {});
91
+ controller.dispatch({ type: "snapshot.applied", snapshot });
122
92
  },
123
- [setLongRunningToolIds, setMessagesImmediate],
93
+ [controller],
124
94
  );
125
95
 
126
96
  // Replace the message list AND reset derived per-turn HITL state.
@@ -129,14 +99,9 @@ const useAdkMessagesInternal = ({
129
99
  // into the next turn.
130
100
  const replaceMessages = useCallback(
131
101
  (msgs: AdkMessage[]) => {
132
- setMessagesImmediate(msgs);
133
- setLongRunningToolIds([]);
134
- setToolConfirmations([]);
135
- setAuthRequests([]);
136
- setEscalated(false);
137
- setMessageMetadata(new Map());
102
+ controller.dispatch({ type: "messages.replaced", messages: msgs });
138
103
  },
139
- [setLongRunningToolIds, setMessagesImmediate],
104
+ [controller],
140
105
  );
141
106
 
142
107
  const abortControllerRef = useRef<AbortController | null>(null);
@@ -164,9 +129,10 @@ const useAdkMessagesInternal = ({
164
129
  m.type === "tool" ? [m.tool_call_id] : [],
165
130
  ),
166
131
  );
132
+ const currentState = controller.getState();
167
133
  const accumulator = new AdkEventAccumulator(
168
- messagesRef.current.filter((m) => !resentIds.has(m.id)),
169
- longRunningToolIdsRef.current.filter(
134
+ currentState.messages.filter((m) => !resentIds.has(m.id)),
135
+ currentState.longRunningToolIds.filter(
170
136
  (id) => !answeredToolCallIds.has(id),
171
137
  ),
172
138
  );
@@ -176,10 +142,13 @@ const useAdkMessagesInternal = ({
176
142
  const initialMessages = accumulator.getMessages();
177
143
  const initialMessageIds = new Set(initialMessages.map((m) => m.id));
178
144
  const initialLongRunningToolIds = accumulator.getLongRunningToolIds();
179
- setMessagesImmediate(initialMessages);
180
- setLongRunningToolIds(initialLongRunningToolIds);
181
- setToolConfirmations(accumulator.getToolConfirmations());
182
- setAuthRequests(accumulator.getAuthRequests());
145
+ controller.dispatch({
146
+ type: "run.started",
147
+ messages: initialMessages,
148
+ longRunningToolIds: initialLongRunningToolIds,
149
+ toolConfirmations: accumulator.getToolConfirmations(),
150
+ authRequests: accumulator.getAuthRequests(),
151
+ });
183
152
  let lastTransferToAgent: string | undefined;
184
153
 
185
154
  // Google ADK replaces active runs, while React LangGraph queues sends.
@@ -222,28 +191,20 @@ const useAdkMessagesInternal = ({
222
191
  if (affectedMessages.length > 0) {
223
192
  onMessages?.(affectedMessages, config.runConfig);
224
193
  }
225
- setMessagesImmediate(updatedMessages);
226
- setStateDelta({
227
- ...stateDeltaRef.current,
228
- ...accumulator.getStateDelta(),
229
- });
230
- setAgentInfo(accumulator.getAgentInfo());
231
- setLongRunningToolIds(accumulator.getLongRunningToolIds());
232
- setArtifactDelta({
233
- ...artifactDeltaRef.current,
234
- ...accumulator.getArtifactDelta(),
194
+ controller.dispatch({
195
+ type: "event.published",
196
+ state: {
197
+ messages: updatedMessages,
198
+ stateDelta: accumulator.getStateDelta(),
199
+ agentInfo: accumulator.getAgentInfo(),
200
+ longRunningToolIds: accumulator.getLongRunningToolIds(),
201
+ artifactDelta: accumulator.getArtifactDelta(),
202
+ toolConfirmations: accumulator.getToolConfirmations(),
203
+ authRequests: accumulator.getAuthRequests(),
204
+ escalated: accumulator.isEscalated(),
205
+ messageMetadata: accumulator.getMessageMetadata(),
206
+ },
235
207
  });
236
- setToolConfirmations(accumulator.getToolConfirmations());
237
- setAuthRequests(accumulator.getAuthRequests());
238
- setEscalated(accumulator.isEscalated());
239
- {
240
- const newMeta = accumulator.getMessageMetadata();
241
- if (newMeta.size > 0) {
242
- setMessageMetadata(
243
- new Map([...messageMetadataRef.current, ...newMeta]),
244
- );
245
- }
246
- }
247
208
 
248
209
  const transfer = accumulator.getLastTransferToAgent();
249
210
  if (transfer && transfer !== lastTransferToAgent) {
@@ -286,12 +247,13 @@ const useAdkMessagesInternal = ({
286
247
  } finally {
287
248
  if (abortControllerRef.current === abortController) {
288
249
  if (abortController.signal.aborted) {
289
- setLongRunningToolIds(
290
- accumulator
250
+ controller.dispatch({
251
+ type: "longRunningToolIds.set",
252
+ ids: accumulator
291
253
  .getLongRunningToolIds()
292
254
  .filter((id) => initialLongRunningToolIds.includes(id)),
293
- );
294
- const updatedMessages = messagesRef.current;
255
+ });
256
+ const updatedMessages = controller.getState().messages;
295
257
  const lastAssistantMessage = updatedMessages.findLast(
296
258
  (m) => m.type === "ai",
297
259
  );
@@ -318,8 +280,8 @@ const useAdkMessagesInternal = ({
318
280
  },
319
281
  [
320
282
  aui,
283
+ controller,
321
284
  setMessagesImmediate,
322
- setLongRunningToolIds,
323
285
  stream,
324
286
  onError,
325
287
  onCustomEvent,
@@ -337,6 +299,7 @@ const useAdkMessagesInternal = ({
337
299
  useReplaySafeEffect(() => cancel, []);
338
300
 
339
301
  return {
302
+ controller,
340
303
  messages,
341
304
  stateDelta,
342
305
  agentInfo,
@@ -357,11 +320,13 @@ const useAdkMessagesInternal = ({
357
320
  export const useAdkMessages = ({
358
321
  stream,
359
322
  eventHandlers,
360
- }: UseAdkMessagesOptions) =>
361
- useAdkMessagesInternal({
323
+ }: UseAdkMessagesOptions) => {
324
+ const { controller: _controller, ...result } = useAdkMessagesInternal({
362
325
  stream,
363
326
  ...(eventHandlers !== undefined && { eventHandlers }),
364
327
  });
328
+ return result;
329
+ };
365
330
 
366
331
  export { useAdkMessagesInternal };
367
332
 
@@ -11,6 +11,7 @@ import type {
11
11
  import { useAdkRuntime } from "./useAdkRuntime";
12
12
  import { useAdkLongRunningToolIds, useAdkSend } from "./hooks";
13
13
  import { AdkEventAccumulator } from "./AdkEventAccumulator";
14
+ import { settleOutsideAct } from "./tests/settleOutsideAct";
14
15
  import type { AdkEvent, AdkStreamCallback, AdkThreadSnapshot } from "./types";
15
16
 
16
17
  const makeThreadListAdapter = (): RemoteThreadListAdapter => ({
@@ -85,9 +86,9 @@ const renderAdkRuntime = async (
85
86
  render(<Inner />);
86
87
  });
87
88
  await waitFor(() => expect(capture.runtime).not.toBeNull());
88
- await act(async () => {
89
- await capture.runtime!.threads.switchToThread("adk-1");
90
- });
89
+ await settleOutsideAct(() =>
90
+ capture.runtime!.threads.switchToThread("adk-1"),
91
+ );
91
92
 
92
93
  return capture;
93
94
  };
@@ -0,0 +1,59 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import { renderHook } from "@testing-library/react";
4
+ import { describe, expect, it, vi } from "vitest";
5
+ import type { AssistantCloud } from "assistant-cloud";
6
+ import type { AssistantRuntime } from "@assistant-ui/core";
7
+
8
+ const mocks = vi.hoisted(() => ({
9
+ cloudAdapter: {},
10
+ runtime: {},
11
+ useCloudThreadListAdapter: vi.fn(() => ({})),
12
+ useRemoteThreadListRuntime: vi.fn(() => ({})),
13
+ }));
14
+
15
+ vi.mock("@assistant-ui/core/react", async (importOriginal) => ({
16
+ ...(await importOriginal<typeof import("@assistant-ui/core/react")>()),
17
+ useCloudThreadListAdapter: mocks.useCloudThreadListAdapter,
18
+ useRemoteThreadListRuntime: mocks.useRemoteThreadListRuntime,
19
+ }));
20
+
21
+ vi.mock("@assistant-ui/store", async (importOriginal) => ({
22
+ ...(await importOriginal<typeof import("@assistant-ui/store")>()),
23
+ useAui: () => ({
24
+ threadListItem: {
25
+ source: null,
26
+ getState: () => ({ externalId: undefined }),
27
+ initialize: vi.fn(),
28
+ },
29
+ }),
30
+ }));
31
+
32
+ import { ADK_SDK } from "./sdkIdentity";
33
+ import { useAdkRuntime } from "./useAdkRuntime";
34
+
35
+ describe("useAdkRuntime Cloud options", () => {
36
+ it("forwards the Cloud scope to the thread-list adapter", () => {
37
+ const cloud = {} as AssistantCloud;
38
+ mocks.useCloudThreadListAdapter.mockReturnValue(mocks.cloudAdapter);
39
+ mocks.useRemoteThreadListRuntime.mockReturnValue(
40
+ mocks.runtime as AssistantRuntime,
41
+ );
42
+
43
+ renderHook(() =>
44
+ useAdkRuntime({
45
+ stream: vi.fn(),
46
+ cloud,
47
+ scopeId: "workspace-1",
48
+ }),
49
+ );
50
+
51
+ expect(mocks.useCloudThreadListAdapter).toHaveBeenCalledWith(
52
+ expect.objectContaining({
53
+ cloud,
54
+ scopeId: "workspace-1",
55
+ sdk: ADK_SDK,
56
+ }),
57
+ );
58
+ });
59
+ });