@assistant-ui/react-google-adk 0.0.34 → 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 +29 -4
  29. package/dist/useAdkMessages.d.ts.map +1 -1
  30. package/dist/useAdkMessages.js +62 -76
  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 +182 -54
  35. package/dist/useAdkRuntime.js.map +1 -1
  36. package/package.json +6 -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 +43 -0
  53. package/src/useAdkMessages.ts +89 -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 +253 -75
  59. package/src/useAdkRuntimeApproval.test.tsx +390 -35
  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
@@ -4,6 +4,7 @@ import {
4
4
  useMemo,
5
5
  useRef,
6
6
  useState,
7
+ useSyncExternalStore,
7
8
  } from "react";
8
9
  import {
9
10
  pickExternalStoreSharedOptions,
@@ -21,6 +22,9 @@ import {
21
22
  import {
22
23
  createAbortableThreadLoad,
23
24
  createCloudThreadListAdapterCreateFallback,
25
+ isRecord,
26
+ RunLeases,
27
+ type RunLease,
24
28
  } from "@assistant-ui/core/internal";
25
29
  import {
26
30
  useCloudThreadListAdapter,
@@ -41,7 +45,7 @@ import type {
41
45
  OnAdkCustomEventCallback,
42
46
  OnAdkAgentTransferCallback,
43
47
  } from "./types";
44
- import { useAdkMessages } from "./useAdkMessages";
48
+ import { useAdkMessagesInternal } from "./useAdkMessages";
45
49
  import {
46
50
  convertAdkMessage,
47
51
  createAdkMessageConverter,
@@ -71,6 +75,7 @@ export type UseAdkRuntimeOptions = ExternalStoreSharedOptions & {
71
75
  */
72
76
  onThreadIdChange?: ((threadId: string | undefined) => void) | undefined;
73
77
  autoCancelPendingToolCalls?: boolean | undefined;
78
+ /** @deprecated Experimental since 2025-01-03. Not scheduled for removal; the API may change in any release. */
74
79
  unstable_allowCancellation?: boolean | undefined;
75
80
  getCheckpointId?: (
76
81
  threadId: string,
@@ -105,6 +110,11 @@ export type UseAdkRuntimeOptions = ExternalStoreSharedOptions & {
105
110
  }
106
111
  | undefined;
107
112
  cloud?: AssistantCloud | undefined;
113
+ /**
114
+ * Stable identity for the account or workspace owning Cloud runtime state.
115
+ * Provide it from the first render and change it when that scope changes.
116
+ */
117
+ scopeId?: string | undefined;
108
118
  /**
109
119
  * A `RemoteThreadListAdapter` to use instead of the cloud adapter.
110
120
  * Use with `createAdkSessionAdapter` for ADK session-backed persistence.
@@ -123,7 +133,64 @@ const useAdkRuntimeImpl = (options: UseAdkRuntimeOptions) => {
123
133
  eventHandlers,
124
134
  } = options;
125
135
  const aui = useAui();
136
+ const runConfigByToolCallIdRef = useRef(new Map<string, unknown>());
137
+
138
+ const rememberMessageOwnership = useCallback(
139
+ (newMessages: AdkMessage[], runConfig: unknown) => {
140
+ const toolOwnership = runConfigByToolCallIdRef.current;
141
+ for (const message of newMessages) {
142
+ if (message.type !== "ai") continue;
143
+ for (const toolCall of message.tool_calls ?? []) {
144
+ if (!isRecord(toolCall)) continue;
145
+ if (!toolOwnership.has(toolCall.id)) {
146
+ toolOwnership.set(toolCall.id, runConfig);
147
+ }
148
+ }
149
+ }
150
+ },
151
+ [],
152
+ );
153
+
154
+ const seedMessageOwnership = useCallback((history: AdkMessage[]) => {
155
+ const currentOwnership = runConfigByToolCallIdRef.current;
156
+ const nextOwnership = new Map<string, unknown>();
157
+ for (const message of history) {
158
+ if (message.type !== "ai") continue;
159
+ for (const toolCall of message.tool_calls ?? []) {
160
+ if (!isRecord(toolCall)) continue;
161
+ // Loaded ids must remain present even without a local owner because
162
+ // streamed event windows use has() to avoid attributing them later.
163
+ nextOwnership.set(
164
+ toolCall.id,
165
+ currentOwnership.has(toolCall.id)
166
+ ? currentOwnership.get(toolCall.id)
167
+ : undefined,
168
+ );
169
+ }
170
+ }
171
+ runConfigByToolCallIdRef.current = nextOwnership;
172
+ }, []);
173
+
174
+ const pruneMessageOwnership = useCallback((history: AdkMessage[]) => {
175
+ const toolCallIds = new Set<string>();
176
+ for (const message of history) {
177
+ if (message.type !== "ai") continue;
178
+ for (const toolCall of message.tool_calls ?? []) {
179
+ if (!isRecord(toolCall)) continue;
180
+ toolCallIds.add(toolCall.id);
181
+ }
182
+ }
183
+ for (const id of runConfigByToolCallIdRef.current.keys()) {
184
+ if (!toolCallIds.has(id)) runConfigByToolCallIdRef.current.delete(id);
185
+ }
186
+ }, []);
187
+
188
+ const getToolRunConfig = useCallback((toolCallId: string) => {
189
+ return runConfigByToolCallIdRef.current.get(toolCallId);
190
+ }, []);
191
+
126
192
  const {
193
+ controller,
127
194
  messages,
128
195
  stateDelta,
129
196
  agentInfo,
@@ -136,11 +203,12 @@ const useAdkRuntimeImpl = (options: UseAdkRuntimeOptions) => {
136
203
  sendMessage,
137
204
  cancel,
138
205
  setMessages,
139
- replaceMessages,
140
- applySnapshot,
141
- } = useAdkMessages({
206
+ replaceMessages: replaceAdkMessages,
207
+ applySnapshot: applyAdkSnapshot,
208
+ } = useAdkMessagesInternal({
142
209
  stream,
143
210
  ...(eventHandlers && { eventHandlers }),
211
+ onMessages: rememberMessageOwnership,
144
212
  });
145
213
 
146
214
  const loadRef = useRef(load);
@@ -148,10 +216,40 @@ const useAdkRuntimeImpl = (options: UseAdkRuntimeOptions) => {
148
216
  loadRef.current = load;
149
217
  }, [load]);
150
218
  const [loadController] = useState(createAbortableThreadLoad);
219
+ const initialLoadRef = useRef<{
220
+ promise: Promise<void>;
221
+ active: boolean;
222
+ snapshot: AdkThreadSnapshot | undefined;
223
+ } | null>(null);
224
+ const waitForInitialLoad = () => {
225
+ const load = initialLoadRef.current;
226
+ if (!load) return undefined;
227
+ return load.promise.then(() => ({
228
+ active:
229
+ load.active &&
230
+ (!threadListItem ||
231
+ aui.threads.getState().mainThreadId === threadListItem.getState().id),
232
+ snapshot: load.snapshot,
233
+ }));
234
+ };
151
235
  const messagesRef = useRef(messages);
152
236
  useInsertionEffect(() => {
153
237
  messagesRef.current = messages;
154
238
  }, [messages]);
239
+ const applySnapshot = useCallback(
240
+ (snapshot: AdkThreadSnapshot) => {
241
+ seedMessageOwnership(snapshot.messages);
242
+ applyAdkSnapshot(snapshot);
243
+ },
244
+ [applyAdkSnapshot, seedMessageOwnership],
245
+ );
246
+ const replaceMessages = useCallback(
247
+ (nextMessages: AdkMessage[]) => {
248
+ pruneMessageOwnership(nextMessages);
249
+ replaceAdkMessages(nextMessages);
250
+ },
251
+ [pruneMessageOwnership, replaceAdkMessages],
252
+ );
155
253
  const [isLoadingThread, setIsLoadingThread] = useState(
156
254
  () =>
157
255
  load !== undefined && aui.threadListItem.getState().externalId != null,
@@ -169,21 +267,47 @@ const useAdkRuntimeImpl = (options: UseAdkRuntimeOptions) => {
169
267
  useInsertionEffect(() => {
170
268
  isRunningRef.current = effectiveIsRunning;
171
269
  }, [effectiveIsRunning]);
172
- const runGenerationRef = useRef(0);
270
+ const [runLeases] = useState(() => new RunLeases());
271
+ const reloadLookupRef = useRef<{
272
+ lease: RunLease;
273
+ beforeReload: AdkThreadSnapshot;
274
+ } | null>(null);
173
275
 
174
- const handleSendMessage = async (
175
- msgs: AdkMessage[],
176
- config: AdkSendMessageConfig,
276
+ const runExclusive = async (
277
+ run: (isCurrent: () => boolean) => Promise<void>,
177
278
  ) => {
178
- const generation = ++runGenerationRef.current;
279
+ const lease = runLeases.begin();
179
280
  try {
180
281
  setIsRunning(true);
181
- await sendMessage(msgs, config);
282
+ await run(lease.isCurrent);
182
283
  } finally {
183
- if (runGenerationRef.current === generation) setIsRunning(false);
284
+ if (lease.isCurrent()) setIsRunning(false);
184
285
  }
185
286
  };
186
287
 
288
+ const handleSendMessage = (
289
+ msgs: AdkMessage[],
290
+ config: AdkSendMessageConfig,
291
+ ) => {
292
+ const isToolContinuation =
293
+ msgs.length > 0 && msgs.every((msg) => msg.type === "tool");
294
+ const continuationConfig =
295
+ isToolContinuation && config.runConfig === undefined
296
+ ? {
297
+ ...config,
298
+ runConfig: getToolRunConfig(msgs[0]!.tool_call_id),
299
+ }
300
+ : config;
301
+
302
+ return runExclusive(() => sendMessage(msgs, continuationConfig));
303
+ };
304
+
305
+ const stopRun = () => {
306
+ runLeases.invalidate();
307
+ setIsRunning(false);
308
+ cancel();
309
+ };
310
+
187
311
  const { approvals: toolApprovals, key: toolApprovalsKey } =
188
312
  projectAdkToolApprovals(messages);
189
313
  // The messageConverter memo below reads this during render, where the ref
@@ -233,43 +357,20 @@ const useAdkRuntimeImpl = (options: UseAdkRuntimeOptions) => {
233
357
  adkMessagesRef.current = messages;
234
358
  }, [messages]);
235
359
 
236
- const stagedMessagesRef = useRef(
237
- new Map<
238
- string,
239
- {
240
- message: AdkMessage & { id: string };
241
- runConfig: AppendMessage["runConfig"];
242
- }
243
- >(),
360
+ const stagedMessageCount = useSyncExternalStore(
361
+ controller.subscribe,
362
+ controller.getStagedMessageCount,
363
+ controller.getStagedMessageCount,
244
364
  );
245
- const [stagedMessageCount, setStagedMessageCount] = useState(0);
246
365
  const hasStagedMessages = stagedMessageCount > 0;
247
366
 
248
- const getStagedRun = (parentId: string | null) => {
249
- if (!parentId || !stagedMessagesRef.current.has(parentId)) return null;
250
-
251
- const staged: AdkMessage[] = [];
252
- for (const message of adkMessagesRef.current) {
253
- if (message.id && stagedMessagesRef.current.has(message.id)) {
254
- staged.push(stagedMessagesRef.current.get(message.id)!.message);
255
- }
256
- if (message.id === parentId) break;
257
- }
258
-
259
- return {
260
- messages: staged,
261
- runConfig: stagedMessagesRef.current.get(parentId)!.runConfig,
262
- };
263
- };
264
-
265
367
  const stageUserMessage = (msg: AppendMessage) => {
266
368
  const stagedMessage = toAdkUserMessage(msg);
267
- stagedMessagesRef.current.set(stagedMessage.id, {
268
- message: stagedMessage,
269
- runConfig: msg.runConfig,
369
+ controller.dispatch({
370
+ type: "staged.stage",
371
+ entry: { message: stagedMessage, runConfig: msg.runConfig },
270
372
  });
271
- setStagedMessageCount(stagedMessagesRef.current.size);
272
- const nextMessages = [...adkMessagesRef.current, stagedMessage];
373
+ const nextMessages = [...controller.getState().messages, stagedMessage];
273
374
  adkMessagesRef.current = nextMessages;
274
375
  setMessages(nextMessages);
275
376
  };
@@ -312,7 +413,14 @@ const useAdkRuntimeImpl = (options: UseAdkRuntimeOptions) => {
312
413
  messagesRef.current !== messagesAtLoadStart)
313
414
  )
314
415
  return;
416
+ reloadLookupRef.current = null;
315
417
  applySnapshot(snapshot);
418
+ messagesRef.current = snapshot.messages;
419
+ adkMessagesRef.current = snapshot.messages;
420
+ longRunningToolIdsRef.current = snapshot.longRunningToolIds ?? [];
421
+ if (purpose === "initial" && initialLoadRef.current) {
422
+ initialLoadRef.current.snapshot = snapshot;
423
+ }
316
424
  },
317
425
  onSettled: () => {
318
426
  setIsLoadingThread(false);
@@ -326,11 +434,26 @@ const useAdkRuntimeImpl = (options: UseAdkRuntimeOptions) => {
326
434
  );
327
435
 
328
436
  useReplaySafeEffect(() => {
329
- runLoad();
437
+ let release!: () => void;
438
+ const barrier = {
439
+ promise: new Promise<void>((resolve) => {
440
+ release = resolve;
441
+ }),
442
+ active: true,
443
+ snapshot: undefined as AdkThreadSnapshot | undefined,
444
+ };
445
+ initialLoadRef.current = barrier;
446
+ const settle = () => {
447
+ if (initialLoadRef.current === barrier) initialLoadRef.current = null;
448
+ release();
449
+ };
450
+ void runLoad().then(settle, settle);
330
451
  return () => {
452
+ barrier.active = false;
331
453
  // Whatever is current, not this effect's own controller: a refetch swaps
332
454
  // the ref, and one in flight at unmount must be aborted too.
333
455
  loadController.abort();
456
+ settle();
334
457
  setIsLoadingThread(false);
335
458
  };
336
459
  }, [threadListItem]);
@@ -353,9 +476,17 @@ const useAdkRuntimeImpl = (options: UseAdkRuntimeOptions) => {
353
476
  authRequests,
354
477
  escalated,
355
478
  messageMetadata,
356
- send: handleSendMessage,
479
+ send: (messages, config) => {
480
+ const initialLoad = waitForInitialLoad();
481
+ if (!initialLoad) return handleSendMessage(messages, config);
482
+ return initialLoad.then(({ active }) =>
483
+ active ? handleSendMessage(messages, config) : undefined,
484
+ );
485
+ },
357
486
  }),
358
487
  onNew: async (msg) => {
488
+ const initialLoad = await waitForInitialLoad();
489
+ if (initialLoad && !initialLoad.active) return;
359
490
  if (!(msg.startRun ?? msg.role === "user")) {
360
491
  stageUserMessage(msg);
361
492
  return;
@@ -363,7 +494,11 @@ const useAdkRuntimeImpl = (options: UseAdkRuntimeOptions) => {
363
494
 
364
495
  const cancellations =
365
496
  autoCancelPendingToolCalls !== false
366
- ? getPendingCancellations(messages, longRunningToolIds)
497
+ ? getPendingCancellations(
498
+ initialLoad?.snapshot?.messages ?? messagesRef.current,
499
+ initialLoad?.snapshot?.longRunningToolIds ??
500
+ longRunningToolIdsRef.current,
501
+ )
367
502
  : [];
368
503
 
369
504
  return handleSendMessage(
@@ -380,6 +515,9 @@ const useAdkRuntimeImpl = (options: UseAdkRuntimeOptions) => {
380
515
  },
381
516
  onEdit: getCheckpointId
382
517
  ? async (msg) => {
518
+ const initialLoad = waitForInitialLoad();
519
+ if (initialLoad && !(await initialLoad).active) return;
520
+ stopRun();
383
521
  const truncated = truncateAdkMessages(
384
522
  threadMessagesRef.current,
385
523
  msg.parentId,
@@ -387,44 +525,41 @@ const useAdkRuntimeImpl = (options: UseAdkRuntimeOptions) => {
387
525
  replaceMessages(truncated);
388
526
  if (!(msg.startRun ?? msg.role === "user")) {
389
527
  const stagedMessage = toAdkUserMessage(msg);
390
- stagedMessagesRef.current.set(stagedMessage.id, {
391
- message: stagedMessage,
392
- runConfig: msg.runConfig,
528
+ controller.dispatch({
529
+ type: "staged.stage",
530
+ entry: { message: stagedMessage, runConfig: msg.runConfig },
393
531
  });
394
- setStagedMessageCount(stagedMessagesRef.current.size);
395
532
  const nextMessages = [...truncated, stagedMessage];
396
533
  adkMessagesRef.current = nextMessages;
397
534
  setMessages(nextMessages);
398
535
  return;
399
536
  }
537
+ const editedMessage = toAdkUserMessage(msg);
538
+ setMessages([...truncated, editedMessage]);
400
539
  const externalId = aui.threadListItem.getState().externalId;
401
- const checkpointId = externalId
402
- ? await getCheckpointId(externalId, truncated)
403
- : null;
404
- return handleSendMessage(
405
- [
406
- {
407
- id: generateId(),
408
- type: "human",
409
- content: getMessageContent(msg),
410
- },
411
- ],
412
- {
540
+ return runExclusive(async (isCurrent) => {
541
+ const checkpointId = externalId
542
+ ? await getCheckpointId(externalId, truncated)
543
+ : null;
544
+ if (!isCurrent()) return;
545
+ await sendMessage([editedMessage], {
413
546
  runConfig: msg.runConfig,
414
547
  ...(checkpointId && { checkpointId }),
415
- },
416
- );
548
+ });
549
+ });
417
550
  }
418
551
  : undefined,
419
552
  ...(getCheckpointId || hasStagedMessages
420
553
  ? {
421
554
  onReload: async (parentId, config) => {
422
- const stagedRun = getStagedRun(parentId);
555
+ const initialLoad = waitForInitialLoad();
556
+ if (initialLoad && !(await initialLoad).active) return;
557
+ const stagedRun = controller.getStagedRun(parentId);
423
558
  if (stagedRun) {
424
- for (const message of stagedRun.messages) {
425
- stagedMessagesRef.current.delete(message.id);
426
- }
427
- setStagedMessageCount(stagedMessagesRef.current.size);
559
+ controller.dispatch({
560
+ type: "staged.unstage",
561
+ ids: stagedRun.messages.map((message) => message.id!),
562
+ });
428
563
  return handleSendMessage(stagedRun.messages, {
429
564
  runConfig: config.runConfig ?? stagedRun.runConfig,
430
565
  });
@@ -433,18 +568,48 @@ const useAdkRuntimeImpl = (options: UseAdkRuntimeOptions) => {
433
568
  if (!getCheckpointId)
434
569
  throw new Error("Runtime does not support reloading messages.");
435
570
 
571
+ stopRun();
572
+ const beforeReload: AdkThreadSnapshot = {
573
+ messages: adkMessagesRef.current,
574
+ longRunningToolIds,
575
+ toolConfirmations,
576
+ authRequests,
577
+ escalated,
578
+ messageMetadata,
579
+ stateDelta,
580
+ artifactDelta,
581
+ agentInfo,
582
+ };
436
583
  const truncated = truncateAdkMessages(
437
584
  threadMessagesRef.current,
438
585
  parentId,
439
586
  );
440
587
  replaceMessages(truncated);
441
588
  const externalId = aui.threadListItem.getState().externalId;
442
- const checkpointId = externalId
443
- ? await getCheckpointId(externalId, truncated)
444
- : null;
445
- return handleSendMessage([], {
446
- runConfig: config.runConfig,
447
- ...(checkpointId && { checkpointId }),
589
+ return runExclusive(async (isCurrent) => {
590
+ const lookup = {
591
+ lease: runLeases.current(),
592
+ beforeReload,
593
+ };
594
+ reloadLookupRef.current = lookup;
595
+ let checkpointId: string | null;
596
+ try {
597
+ checkpointId = externalId
598
+ ? await getCheckpointId(externalId, truncated)
599
+ : null;
600
+ } catch (error) {
601
+ if (isCurrent() && reloadLookupRef.current === lookup)
602
+ applySnapshot(beforeReload);
603
+ throw error;
604
+ } finally {
605
+ if (reloadLookupRef.current === lookup)
606
+ reloadLookupRef.current = null;
607
+ }
608
+ if (!isCurrent()) return;
609
+ await sendMessage([], {
610
+ runConfig: config.runConfig,
611
+ ...(checkpointId && { checkpointId }),
612
+ });
448
613
  });
449
614
  },
450
615
  }
@@ -456,6 +621,8 @@ const useAdkRuntimeImpl = (options: UseAdkRuntimeOptions) => {
456
621
  isError,
457
622
  artifact,
458
623
  }) => {
624
+ const initialLoad = waitForInitialLoad();
625
+ if (initialLoad && !(await initialLoad).active) return;
459
626
  await handleSendMessage(
460
627
  [
461
628
  {
@@ -472,6 +639,8 @@ const useAdkRuntimeImpl = (options: UseAdkRuntimeOptions) => {
472
639
  );
473
640
  },
474
641
  onRespondToToolApproval: async (options) => {
642
+ const initialLoad = waitForInitialLoad();
643
+ if (initialLoad && !(await initialLoad).active) return;
475
644
  await handleSendMessage(
476
645
  [
477
646
  toAdkToolConfirmationReply(
@@ -484,7 +653,14 @@ const useAdkRuntimeImpl = (options: UseAdkRuntimeOptions) => {
484
653
  },
485
654
  onCancel: unstable_allowCancellation
486
655
  ? async () => {
487
- cancel();
656
+ const lookup = reloadLookupRef.current;
657
+ const beforeReload = lookup?.lease.isCurrent()
658
+ ? lookup.beforeReload
659
+ : undefined;
660
+ stopRun();
661
+ // A reload stopped before it sent leaves the ADK session holding the
662
+ // turn it removed, so the thread shows that turn again.
663
+ if (beforeReload) applySnapshot(beforeReload);
488
664
  }
489
665
  : undefined,
490
666
  ...(load !== undefined && {
@@ -497,6 +673,7 @@ const useAdkRuntimeImpl = (options: UseAdkRuntimeOptions) => {
497
673
 
498
674
  export const useAdkRuntime = ({
499
675
  cloud,
676
+ scopeId,
500
677
  sessionAdapter,
501
678
  create,
502
679
  delete: deleteFn,
@@ -507,6 +684,7 @@ export const useAdkRuntime = ({
507
684
  const cloudAdapter = useCloudThreadListAdapter({
508
685
  sdk: ADK_SDK,
509
686
  cloud,
687
+ scopeId,
510
688
  create: createCloudThreadListAdapterCreateFallback(
511
689
  create,
512
690
  aui.threadListItem,