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

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 (35) hide show
  1. package/dist/AdkEventAccumulator.d.ts.map +1 -1
  2. package/dist/AdkEventAccumulator.js +4 -1
  3. package/dist/AdkEventAccumulator.js.map +1 -1
  4. package/dist/adkToolApproval.js +1 -1
  5. package/dist/adkToolApproval.js.map +1 -1
  6. package/dist/convertAdkMessages.d.ts.map +1 -1
  7. package/dist/convertAdkMessages.js +30 -19
  8. package/dist/convertAdkMessages.js.map +1 -1
  9. package/dist/convertToAdkMessages.d.ts.map +1 -1
  10. package/dist/convertToAdkMessages.js +2 -2
  11. package/dist/convertToAdkMessages.js.map +1 -1
  12. package/dist/sdkIdentity.js +1 -1
  13. package/dist/useAdkMessages.d.ts +23 -0
  14. package/dist/useAdkMessages.d.ts.map +1 -1
  15. package/dist/useAdkMessages.js +34 -9
  16. package/dist/useAdkMessages.js.map +1 -1
  17. package/dist/useAdkRuntime.d.ts.map +1 -1
  18. package/dist/useAdkRuntime.js +60 -9
  19. package/dist/useAdkRuntime.js.map +1 -1
  20. package/package.json +7 -6
  21. package/src/AdkEventAccumulator.test.ts +32 -0
  22. package/src/AdkEventAccumulator.ts +1 -0
  23. package/src/adkToolApproval.test.ts +19 -0
  24. package/src/adkToolApproval.ts +1 -1
  25. package/src/convertAdkMessages.test.ts +129 -0
  26. package/src/convertAdkMessages.ts +19 -1
  27. package/src/convertToAdkMessages.test.ts +14 -0
  28. package/src/convertToAdkMessages.ts +4 -1
  29. package/src/useAdkMessages.test.ts +55 -0
  30. package/src/useAdkMessages.ts +65 -6
  31. package/src/useAdkRuntime.cancellation.test.tsx +544 -0
  32. package/src/useAdkRuntime.fast-refresh.test.tsx +166 -0
  33. package/src/useAdkRuntime.refetch.test.tsx +1 -0
  34. package/src/useAdkRuntime.ts +91 -9
  35. package/src/useAdkRuntimeApproval.test.tsx +305 -36
@@ -1,6 +1,5 @@
1
1
  import {
2
2
  useCallback,
3
- useEffect,
4
3
  useInsertionEffect,
5
4
  useMemo,
6
5
  useRef,
@@ -22,6 +21,7 @@ import {
22
21
  import {
23
22
  createAbortableThreadLoad,
24
23
  createCloudThreadListAdapterCreateFallback,
24
+ isRecord,
25
25
  } from "@assistant-ui/core/internal";
26
26
  import {
27
27
  useCloudThreadListAdapter,
@@ -30,6 +30,7 @@ import {
30
30
  useExternalStoreRuntime,
31
31
  } from "@assistant-ui/core/react";
32
32
  import { useAui } from "@assistant-ui/store";
33
+ import { useReplaySafeEffect } from "@assistant-ui/store/internal";
33
34
  import type { AssistantCloud } from "assistant-cloud";
34
35
  import type { RemoteThreadListAdapter } from "@assistant-ui/core";
35
36
  import type {
@@ -41,7 +42,7 @@ import type {
41
42
  OnAdkCustomEventCallback,
42
43
  OnAdkAgentTransferCallback,
43
44
  } from "./types";
44
- import { useAdkMessages } from "./useAdkMessages";
45
+ import { useAdkMessagesInternal } from "./useAdkMessages";
45
46
  import {
46
47
  convertAdkMessage,
47
48
  createAdkMessageConverter,
@@ -123,6 +124,62 @@ const useAdkRuntimeImpl = (options: UseAdkRuntimeOptions) => {
123
124
  eventHandlers,
124
125
  } = options;
125
126
  const aui = useAui();
127
+ const runConfigByToolCallIdRef = useRef(new Map<string, unknown>());
128
+
129
+ const rememberMessageOwnership = useCallback(
130
+ (newMessages: AdkMessage[], runConfig: unknown) => {
131
+ const toolOwnership = runConfigByToolCallIdRef.current;
132
+ for (const message of newMessages) {
133
+ if (message.type !== "ai") continue;
134
+ for (const toolCall of message.tool_calls ?? []) {
135
+ if (!isRecord(toolCall)) continue;
136
+ if (!toolOwnership.has(toolCall.id)) {
137
+ toolOwnership.set(toolCall.id, runConfig);
138
+ }
139
+ }
140
+ }
141
+ },
142
+ [],
143
+ );
144
+
145
+ const seedMessageOwnership = useCallback((history: AdkMessage[]) => {
146
+ const currentOwnership = runConfigByToolCallIdRef.current;
147
+ const nextOwnership = new Map<string, unknown>();
148
+ for (const message of history) {
149
+ if (message.type !== "ai") continue;
150
+ for (const toolCall of message.tool_calls ?? []) {
151
+ if (!isRecord(toolCall)) continue;
152
+ // Loaded ids must remain present even without a local owner because
153
+ // streamed event windows use has() to avoid attributing them later.
154
+ nextOwnership.set(
155
+ toolCall.id,
156
+ currentOwnership.has(toolCall.id)
157
+ ? currentOwnership.get(toolCall.id)
158
+ : undefined,
159
+ );
160
+ }
161
+ }
162
+ runConfigByToolCallIdRef.current = nextOwnership;
163
+ }, []);
164
+
165
+ const pruneMessageOwnership = useCallback((history: AdkMessage[]) => {
166
+ const toolCallIds = new Set<string>();
167
+ for (const message of history) {
168
+ if (message.type !== "ai") continue;
169
+ for (const toolCall of message.tool_calls ?? []) {
170
+ if (!isRecord(toolCall)) continue;
171
+ toolCallIds.add(toolCall.id);
172
+ }
173
+ }
174
+ for (const id of runConfigByToolCallIdRef.current.keys()) {
175
+ if (!toolCallIds.has(id)) runConfigByToolCallIdRef.current.delete(id);
176
+ }
177
+ }, []);
178
+
179
+ const getToolRunConfig = useCallback((toolCallId: string) => {
180
+ return runConfigByToolCallIdRef.current.get(toolCallId);
181
+ }, []);
182
+
126
183
  const {
127
184
  messages,
128
185
  stateDelta,
@@ -136,22 +193,37 @@ const useAdkRuntimeImpl = (options: UseAdkRuntimeOptions) => {
136
193
  sendMessage,
137
194
  cancel,
138
195
  setMessages,
139
- replaceMessages,
140
- applySnapshot,
141
- } = useAdkMessages({
196
+ replaceMessages: replaceAdkMessages,
197
+ applySnapshot: applyAdkSnapshot,
198
+ } = useAdkMessagesInternal({
142
199
  stream,
143
200
  ...(eventHandlers && { eventHandlers }),
201
+ onMessages: rememberMessageOwnership,
144
202
  });
145
203
 
146
204
  const loadRef = useRef(load);
147
205
  useInsertionEffect(() => {
148
206
  loadRef.current = load;
149
207
  }, [load]);
150
- const loadController = useMemo(createAbortableThreadLoad, []);
208
+ const [loadController] = useState(createAbortableThreadLoad);
151
209
  const messagesRef = useRef(messages);
152
210
  useInsertionEffect(() => {
153
211
  messagesRef.current = messages;
154
212
  }, [messages]);
213
+ const applySnapshot = useCallback(
214
+ (snapshot: AdkThreadSnapshot) => {
215
+ seedMessageOwnership(snapshot.messages);
216
+ applyAdkSnapshot(snapshot);
217
+ },
218
+ [applyAdkSnapshot, seedMessageOwnership],
219
+ );
220
+ const replaceMessages = useCallback(
221
+ (nextMessages: AdkMessage[]) => {
222
+ pruneMessageOwnership(nextMessages);
223
+ replaceAdkMessages(nextMessages);
224
+ },
225
+ [pruneMessageOwnership, replaceAdkMessages],
226
+ );
155
227
  const [isLoadingThread, setIsLoadingThread] = useState(
156
228
  () =>
157
229
  load !== undefined && aui.threadListItem.getState().externalId != null,
@@ -175,10 +247,20 @@ const useAdkRuntimeImpl = (options: UseAdkRuntimeOptions) => {
175
247
  msgs: AdkMessage[],
176
248
  config: AdkSendMessageConfig,
177
249
  ) => {
250
+ const isToolContinuation =
251
+ msgs.length > 0 && msgs.every((msg) => msg.type === "tool");
252
+ const continuationConfig =
253
+ isToolContinuation && config.runConfig === undefined
254
+ ? {
255
+ ...config,
256
+ runConfig: getToolRunConfig(msgs[0]!.tool_call_id),
257
+ }
258
+ : config;
259
+
178
260
  const generation = ++runGenerationRef.current;
179
261
  try {
180
262
  setIsRunning(true);
181
- await sendMessage(msgs, config);
263
+ await sendMessage(msgs, continuationConfig);
182
264
  } finally {
183
265
  if (runGenerationRef.current === generation) setIsRunning(false);
184
266
  }
@@ -325,7 +407,7 @@ const useAdkRuntimeImpl = (options: UseAdkRuntimeOptions) => {
325
407
  [threadListItem, loadController, applySnapshot],
326
408
  );
327
409
 
328
- useEffect(() => {
410
+ useReplaySafeEffect(() => {
329
411
  runLoad();
330
412
  return () => {
331
413
  // Whatever is current, not this effect's own controller: a refetch swaps
@@ -333,7 +415,7 @@ const useAdkRuntimeImpl = (options: UseAdkRuntimeOptions) => {
333
415
  loadController.abort();
334
416
  setIsLoadingThread(false);
335
417
  };
336
- }, [loadController, runLoad]);
418
+ }, [threadListItem]);
337
419
 
338
420
  const runtime = useExternalStoreRuntime({
339
421
  ...pickExternalStoreSharedOptions(options),
@@ -1,17 +1,33 @@
1
1
  import { act, renderHook } from "@testing-library/react";
2
2
  import { afterEach, describe, expect, it, vi } from "vitest";
3
3
  import type {
4
+ AddToolResultOptions,
5
+ AppendMessage,
4
6
  RespondToToolApprovalOptions,
5
7
  ThreadMessage,
6
8
  ToolCallMessagePart,
7
9
  } from "@assistant-ui/core";
8
- import type { AdkMessage } from "./types";
10
+ import type { AdkMessage, AdkSendMessageConfig } from "./types";
9
11
 
10
- const mocks = vi.hoisted(() => ({
11
- adapters: [] as unknown[],
12
- sendMessage: vi.fn().mockResolvedValue(undefined),
13
- messages: [] as AdkMessage[],
14
- }));
12
+ const mocks = vi.hoisted(() => {
13
+ const threadListItem = {
14
+ source: null as object | null,
15
+ externalId: undefined as string | undefined,
16
+ getState: () => ({
17
+ externalId: threadListItem.externalId,
18
+ }),
19
+ initialize: vi.fn(),
20
+ };
21
+ return {
22
+ adapters: [] as unknown[],
23
+ sendMessage: vi.fn().mockResolvedValue(undefined),
24
+ messages: [] as AdkMessage[],
25
+ messageRunConfig: undefined as unknown,
26
+ streamedMessages: undefined as AdkMessage[] | undefined,
27
+ applySnapshot: vi.fn(),
28
+ threadListItem,
29
+ };
30
+ });
15
31
 
16
32
  vi.mock("@assistant-ui/core/react", async (importOriginal) => ({
17
33
  ...(await importOriginal<typeof import("@assistant-ui/core/react")>()),
@@ -27,49 +43,88 @@ vi.mock("@assistant-ui/core/react", async (importOriginal) => ({
27
43
  vi.mock("@assistant-ui/store", async (importOriginal) => ({
28
44
  ...(await importOriginal<typeof import("@assistant-ui/store")>()),
29
45
  useAui: () => ({
30
- threadListItem: {
31
- source: null,
32
- getState: () => ({ externalId: undefined }),
33
- initialize: vi.fn(),
34
- },
46
+ threadListItem: mocks.threadListItem,
35
47
  }),
36
48
  }));
37
49
 
38
- vi.mock("./useAdkMessages", async (importOriginal) => ({
39
- ...(await importOriginal<typeof import("./useAdkMessages")>()),
40
- useAdkMessages: () => ({
41
- messages: mocks.messages,
42
- stateDelta: {},
43
- agentInfo: {},
44
- longRunningToolIds: [],
45
- artifactDelta: {},
46
- // Deliberately empty: the projection must come from the transcript, not
47
- // from derived confirmation state that a mid-run accumulator swap drops.
48
- toolConfirmations: [],
49
- authRequests: [],
50
- escalated: false,
51
- messageMetadata: new Map(),
52
- sendMessage: mocks.sendMessage,
53
- cancel: vi.fn(),
54
- setMessages: vi.fn(),
55
- replaceMessages: vi.fn(),
56
- applySnapshot: vi.fn(),
57
- }),
58
- }));
50
+ vi.mock("./useAdkMessages", async (importOriginal) => {
51
+ const useMockAdkMessages = (options: {
52
+ onMessages?: (messages: AdkMessage[], runConfig: unknown) => void;
53
+ }) => {
54
+ options.onMessages?.(mocks.messages, mocks.messageRunConfig);
55
+ const sendMessage = async (
56
+ messages: AdkMessage[],
57
+ config: AdkSendMessageConfig,
58
+ ) => {
59
+ await mocks.sendMessage(messages, config);
60
+ if (mocks.streamedMessages) {
61
+ options.onMessages?.(mocks.streamedMessages, config.runConfig);
62
+ }
63
+ };
64
+ return {
65
+ messages: mocks.messages,
66
+ stateDelta: {},
67
+ agentInfo: {},
68
+ longRunningToolIds: [],
69
+ artifactDelta: {},
70
+ // Deliberately empty: the projection must come from the transcript, not
71
+ // from derived confirmation state that a mid-run accumulator swap drops.
72
+ toolConfirmations: [],
73
+ authRequests: [],
74
+ escalated: false,
75
+ messageMetadata: new Map(),
76
+ sendMessage,
77
+ cancel: vi.fn(),
78
+ setMessages: vi.fn(),
79
+ replaceMessages: vi.fn(),
80
+ applySnapshot: mocks.applySnapshot,
81
+ };
82
+ };
83
+
84
+ return {
85
+ ...(await importOriginal<typeof import("./useAdkMessages")>()),
86
+ useAdkMessages: useMockAdkMessages,
87
+ useAdkMessagesInternal: useMockAdkMessages,
88
+ };
89
+ });
59
90
 
60
91
  import { AdkEventAccumulator } from "./AdkEventAccumulator";
61
92
  import { useAdkRuntime } from "./useAdkRuntime";
62
93
 
63
- type ApprovalAdapter = {
94
+ type RuntimeAdapter = {
64
95
  messages: readonly ThreadMessage[];
96
+ extras: {
97
+ send: (
98
+ messages: AdkMessage[],
99
+ config: AdkSendMessageConfig,
100
+ ) => Promise<void>;
101
+ };
102
+ onNew?: (message: AppendMessage) => Promise<void> | void;
103
+ onEdit?: (message: AppendMessage) => Promise<void> | void;
104
+ onAddToolResult?: (options: AddToolResultOptions) => Promise<void> | void;
65
105
  onRespondToToolApproval?: (
66
106
  options: RespondToToolApprovalOptions,
67
107
  ) => Promise<void> | void;
108
+ onRefetchThread?: () => Promise<void> | void;
68
109
  };
69
110
 
70
111
  const CONFIRMATION_CALL = "adk-confirmation-1";
71
112
 
72
- const latestAdapter = () => mocks.adapters.at(-1) as ApprovalAdapter;
113
+ const latestAdapter = () => mocks.adapters.at(-1) as RuntimeAdapter;
114
+
115
+ const makeUserMessage = (
116
+ text: string,
117
+ runConfig: AppendMessage["runConfig"],
118
+ ): AppendMessage =>
119
+ ({
120
+ role: "user",
121
+ content: [{ type: "text", text }],
122
+ attachments: [],
123
+ parentId: null,
124
+ sourceId: null,
125
+ runConfig,
126
+ metadata: { custom: {} },
127
+ }) as unknown as AppendMessage;
73
128
 
74
129
  const makeConfirmationRequest = (): AdkMessage => ({
75
130
  id: "ai-1",
@@ -96,20 +151,230 @@ const approvalPart = () =>
96
151
 
97
152
  afterEach(() => {
98
153
  mocks.adapters.length = 0;
154
+ mocks.sendMessage.mockClear();
99
155
  mocks.messages = [];
156
+ mocks.messageRunConfig = undefined;
157
+ mocks.streamedMessages = undefined;
158
+ mocks.applySnapshot.mockReset();
159
+ mocks.threadListItem.source = null;
160
+ mocks.threadListItem.externalId = undefined;
100
161
  });
101
162
 
102
163
  describe("useAdkRuntime tool approvals", () => {
164
+ it("resumes a delayed tool result with its originating run config", async () => {
165
+ const runConfigA = { custom: { model: "model-a" } };
166
+ const runConfigB = { custom: { model: "model-b" } };
167
+ const { rerender } = renderHook(() => useAdkRuntime({ stream: vi.fn() }));
168
+
169
+ await act(async () => {
170
+ await latestAdapter().onNew!(makeUserMessage("first", runConfigA));
171
+ });
172
+
173
+ mocks.messages = [
174
+ { id: "u-1", type: "human", content: "first" },
175
+ {
176
+ id: "ai-1",
177
+ type: "ai",
178
+ content: [],
179
+ tool_calls: [
180
+ {
181
+ id: "tool-a",
182
+ name: "lookup",
183
+ args: {},
184
+ },
185
+ ],
186
+ },
187
+ ];
188
+ mocks.messageRunConfig = runConfigA;
189
+ rerender();
190
+
191
+ await act(async () => {
192
+ await latestAdapter().onNew!(makeUserMessage("second", runConfigB));
193
+ await latestAdapter().onAddToolResult!({
194
+ messageId: "ai-1",
195
+ toolCallId: "tool-a",
196
+ toolName: "lookup",
197
+ result: { value: "done" },
198
+ isError: false,
199
+ });
200
+ });
201
+
202
+ expect(
203
+ mocks.sendMessage.mock.calls.map((call) => call[1].runConfig),
204
+ ).toEqual([runConfigA, runConfigB, runConfigA]);
205
+ });
206
+
207
+ it("preserves pending tool ownership across a thread refetch", async () => {
208
+ const runConfig = { custom: { model: "model-a" } };
209
+ const laterRunConfig = { custom: { model: "model-b" } };
210
+ const currentMessages: AdkMessage[] = [
211
+ {
212
+ id: "ai-1",
213
+ type: "ai",
214
+ content: [],
215
+ tool_calls: [{ id: "tool-a", name: "lookup", args: {} }],
216
+ },
217
+ ];
218
+ const loadedMessages: AdkMessage[] = [
219
+ ...currentMessages,
220
+ {
221
+ id: "ai-2",
222
+ type: "ai",
223
+ content: [],
224
+ tool_calls: [
225
+ null as never,
226
+ { id: "tool-loaded", name: "lookup", args: {} },
227
+ ],
228
+ },
229
+ ];
230
+ const load = vi.fn(async () => ({ messages: loadedMessages }));
231
+ mocks.threadListItem.source = {};
232
+
233
+ const { rerender } = renderHook(() =>
234
+ useAdkRuntime({ stream: vi.fn(), load }),
235
+ );
236
+
237
+ await act(async () => {
238
+ await latestAdapter().onNew!(makeUserMessage("first", runConfig));
239
+ });
240
+ mocks.messages = currentMessages;
241
+ mocks.messageRunConfig = runConfig;
242
+ rerender();
243
+
244
+ mocks.threadListItem.externalId = "thread-a";
245
+ rerender();
246
+ await act(async () => {
247
+ await latestAdapter().onRefetchThread!();
248
+ });
249
+
250
+ await act(async () => {
251
+ mocks.streamedMessages = [loadedMessages.at(-1)!];
252
+ await latestAdapter().extras.send(
253
+ [{ id: "u-later", type: "human", content: "later" }],
254
+ { runConfig: laterRunConfig },
255
+ );
256
+ mocks.streamedMessages = undefined;
257
+ await latestAdapter().onAddToolResult!({
258
+ messageId: "ai-1",
259
+ toolCallId: "tool-a",
260
+ toolName: "lookup",
261
+ result: { value: "done" },
262
+ isError: false,
263
+ });
264
+ await latestAdapter().onAddToolResult!({
265
+ messageId: "ai-2",
266
+ toolCallId: "tool-loaded",
267
+ toolName: "lookup",
268
+ result: { value: "loaded" },
269
+ isError: false,
270
+ });
271
+ });
272
+
273
+ expect(
274
+ mocks.sendMessage.mock.calls.slice(-2).map((call) => call[1]),
275
+ ).toEqual([{ runConfig }, { runConfig: undefined }]);
276
+ });
277
+
278
+ it("tracks and prunes ownership around malformed tool-call entries", async () => {
279
+ const runConfig = { custom: { model: "model-a" } };
280
+ mocks.messages = [
281
+ { id: "u-1", type: "human", content: "first" },
282
+ {
283
+ id: "ai-1",
284
+ type: "ai",
285
+ content: [],
286
+ tool_calls: [null as never, { id: "tool-a", name: "lookup", args: {} }],
287
+ },
288
+ ];
289
+ mocks.messageRunConfig = runConfig;
290
+ renderHook(() =>
291
+ useAdkRuntime({
292
+ stream: vi.fn(),
293
+ getCheckpointId: vi.fn(async () => null),
294
+ }),
295
+ );
296
+
297
+ await act(async () => {
298
+ await latestAdapter().onEdit!({
299
+ ...makeUserMessage("edited", runConfig),
300
+ parentId: "ai-1",
301
+ });
302
+ await latestAdapter().onAddToolResult!({
303
+ messageId: "ai-1",
304
+ toolCallId: "tool-a",
305
+ toolName: "lookup",
306
+ result: { value: "done" },
307
+ isError: false,
308
+ });
309
+ });
310
+
311
+ expect(mocks.sendMessage.mock.calls.at(-1)![1]).toEqual({ runConfig });
312
+ });
313
+
314
+ it("attributes new tool calls to an explicitly configured continuation", async () => {
315
+ const runConfigA = { custom: { model: "model-a" } };
316
+ const runConfigB = { custom: { model: "model-b" } };
317
+ renderHook(() => useAdkRuntime({ stream: vi.fn() }));
318
+
319
+ await act(async () => {
320
+ await latestAdapter().onNew!(makeUserMessage("first", runConfigA));
321
+ mocks.streamedMessages = [
322
+ {
323
+ id: "ai-b",
324
+ type: "ai",
325
+ content: [],
326
+ tool_calls: [{ id: "tool-b", name: "lookup", args: {} }],
327
+ },
328
+ ];
329
+ await latestAdapter().extras.send(
330
+ [
331
+ {
332
+ id: "tool-result-a",
333
+ type: "tool",
334
+ tool_call_id: "tool-a",
335
+ name: "lookup",
336
+ content: JSON.stringify({ value: "first" }),
337
+ },
338
+ ],
339
+ { runConfig: runConfigB },
340
+ );
341
+ });
342
+
343
+ await act(async () => {
344
+ await latestAdapter().onAddToolResult!({
345
+ messageId: "ai-b",
346
+ toolCallId: "tool-b",
347
+ toolName: "lookup",
348
+ result: { value: "second" },
349
+ isError: false,
350
+ });
351
+ });
352
+
353
+ expect(mocks.sendMessage.mock.calls.at(-1)![1]).toEqual({
354
+ runConfig: runConfigB,
355
+ });
356
+ });
357
+
103
358
  it("exposes, answers, and settles the default approval seam across a rerender", async () => {
359
+ const runConfig = { custom: { model: "model-a" } };
360
+ const nextRunConfig = { custom: { model: "model-b" } };
104
361
  // Retained across the rerender: core caches converted messages by input
105
362
  // object, so only a rebuilt converter can surface the settled decision.
106
363
  const confirmationRequest = makeConfirmationRequest();
364
+ const { rerender } = renderHook(() => useAdkRuntime({ stream: vi.fn() }));
365
+
366
+ await act(async () => {
367
+ await latestAdapter().onNew!(
368
+ makeUserMessage("delete the file", runConfig),
369
+ );
370
+ });
371
+
107
372
  mocks.messages = [
108
373
  { id: "u-1", type: "human", content: "delete the file" },
109
374
  confirmationRequest,
110
375
  ];
111
-
112
- const { rerender } = renderHook(() => useAdkRuntime({ stream: vi.fn() }));
376
+ mocks.messageRunConfig = runConfig;
377
+ rerender();
113
378
 
114
379
  expect(latestAdapter().messages.at(-1)!.status).toMatchObject({
115
380
  type: "requires-action",
@@ -121,6 +386,9 @@ describe("useAdkRuntime tool approvals", () => {
121
386
  });
122
387
 
123
388
  await act(async () => {
389
+ await latestAdapter().onNew!(
390
+ makeUserMessage("start another run", nextRunConfig),
391
+ );
124
392
  await latestAdapter().onRespondToToolApproval!({
125
393
  approvalId: CONFIRMATION_CALL,
126
394
  approved: false,
@@ -135,6 +403,7 @@ describe("useAdkRuntime tool approvals", () => {
135
403
  content: JSON.stringify({ confirmed: false }),
136
404
  }),
137
405
  ]);
406
+ expect(mocks.sendMessage.mock.calls.at(-1)![1]).toEqual({ runConfig });
138
407
 
139
408
  mocks.messages = [
140
409
  ...mocks.messages,