@assistant-ui/react-google-adk 0.0.26 → 0.0.27

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.
@@ -1,4 +1,4 @@
1
- import { v4 as uuidv4 } from "uuid";
1
+ import { generateId } from "@assistant-ui/core";
2
2
  import type { MessageStatus } from "@assistant-ui/core";
3
3
  import type {
4
4
  AdkEvent,
@@ -26,10 +26,10 @@ type InProgressMessage = AdkMessage & { type: "ai" };
26
26
  * event has already opened.
27
27
  */
28
28
  const toolMessageId = (event: AdkEvent, partIndex: number): string =>
29
- event.id ? `${event.id}:${partIndex}` : uuidv4();
29
+ event.id ? `${event.id}:${partIndex}` : generateId();
30
30
 
31
31
  const aiMessageId = (event: AdkEvent, ordinal: number): string =>
32
- event.id ? `${event.id}:ai${ordinal === 0 ? "" : ordinal}` : uuidv4();
32
+ event.id ? `${event.id}:ai${ordinal === 0 ? "" : ordinal}` : generateId();
33
33
 
34
34
  const ADK_REQUEST_CONFIRMATION = "adk_request_confirmation";
35
35
  const ADK_REQUEST_CREDENTIAL = "adk_request_credential";
@@ -191,6 +191,8 @@ export class AdkEventAccumulator {
191
191
  private messagesMap = new Map<string, AdkMessage>();
192
192
  private currentMessageId: string | null = null;
193
193
  private partialTextBuffer = "";
194
+ private finalTextReplacedThisEvent = false;
195
+ private finalReasoningReplacedThisEvent = false;
194
196
  private partialReasoningBuffer = "";
195
197
  private accumulatedStateDelta: Record<string, unknown> = {};
196
198
  private accumulatedArtifactDelta: Record<string, number> = {};
@@ -368,7 +370,7 @@ export class AdkEventAccumulator {
368
370
  this.messagesMap.set(toolMsg.id, toolMsg);
369
371
  }
370
372
  if (humanParts.length > 0) {
371
- const id = event.id ?? uuidv4();
373
+ const id = event.id ?? generateId();
372
374
  const first = humanParts[0];
373
375
  const content: string | AdkMessageContentPart[] =
374
376
  humanParts.length === 1 && first?.type === "text"
@@ -387,6 +389,11 @@ export class AdkEventAccumulator {
387
389
  }
388
390
  }
389
391
 
392
+ // Replace-semantics close out the streamed partial buffer, which only
393
+ // the first final text/reasoning part of an event may do; later parts
394
+ // of the same event are distinct content and append.
395
+ this.finalTextReplacedThisEvent = false;
396
+ this.finalReasoningReplacedThisEvent = false;
390
397
  for (const [index, part] of parts.entries()) {
391
398
  this.processPart(part, event, index);
392
399
  }
@@ -472,9 +479,12 @@ export class AdkEventAccumulator {
472
479
  if (event.partial) {
473
480
  this.partialReasoningBuffer += part.text;
474
481
  this.replaceLastReasoningContent(msg, this.partialReasoningBuffer);
475
- } else {
482
+ } else if (!this.finalReasoningReplacedThisEvent) {
483
+ this.finalReasoningReplacedThisEvent = true;
476
484
  this.partialReasoningBuffer = "";
477
485
  this.replaceLastReasoningContent(msg, part.text);
486
+ } else {
487
+ this.appendContent(msg, { type: "reasoning", text: part.text });
478
488
  }
479
489
  return;
480
490
  }
@@ -485,9 +495,12 @@ export class AdkEventAccumulator {
485
495
  if (event.partial) {
486
496
  this.partialTextBuffer += part.text;
487
497
  this.replaceLastTextContent(msg, this.partialTextBuffer);
488
- } else {
498
+ } else if (!this.finalTextReplacedThisEvent) {
499
+ this.finalTextReplacedThisEvent = true;
489
500
  this.partialTextBuffer = "";
490
501
  this.replaceLastTextContent(msg, part.text);
502
+ } else {
503
+ this.appendContent(msg, { type: "text", text: part.text });
491
504
  }
492
505
  return;
493
506
  }
@@ -497,7 +510,7 @@ export class AdkEventAccumulator {
497
510
  if (event.partial) return;
498
511
  const msg = this.getOrCreateAiMessage(event);
499
512
  const toolCall: AdkToolCall = {
500
- id: part.functionCall.id ?? uuidv4(),
513
+ id: part.functionCall.id ?? generateId(),
501
514
  name: part.functionCall.name,
502
515
  args: part.functionCall.args as ReadonlyJSONObject,
503
516
  argsText: JSON.stringify(part.functionCall.args),
@@ -1,9 +1,9 @@
1
+ import { generateId } from "@assistant-ui/core";
1
2
  import type {
2
3
  RespondToToolApprovalOptions,
3
4
  ToolCallMessagePart,
4
5
  } from "@assistant-ui/core";
5
6
  import type { ReadonlyJSONValue } from "assistant-stream/utils";
6
- import { v4 as uuidv4 } from "uuid";
7
7
  import type { AdkMessage } from "./types";
8
8
 
9
9
  export type AdkToolApproval = NonNullable<ToolCallMessagePart["approval"]>;
@@ -185,7 +185,7 @@ export const toAdkConfirmationReply = (
185
185
  confirmed: boolean,
186
186
  payload?: ReadonlyJSONValue,
187
187
  ): AdkMessage & { type: "tool" } => ({
188
- id: uuidv4(),
188
+ id: generateId(),
189
189
  type: "tool",
190
190
  tool_call_id: toolCallId,
191
191
  name: ADK_REQUEST_CONFIRMATION,
@@ -3,7 +3,7 @@ import {
3
3
  getMessageContent,
4
4
  getPendingCancellations,
5
5
  getPendingToolCalls,
6
- } from "./useAdkRuntime";
6
+ } from "./convertToAdkMessages";
7
7
  import { convertAdkMessage } from "./convertAdkMessages";
8
8
  import type { AppendMessage } from "@assistant-ui/core";
9
9
  import type { AdkMessage } from "./types";
@@ -0,0 +1,139 @@
1
+ import {
2
+ generateId,
3
+ getExternalStoreMessages,
4
+ type AppendMessage,
5
+ type ThreadMessage,
6
+ } from "@assistant-ui/core";
7
+ import {
8
+ createToolCallCancellationStub,
9
+ parseDataUrl,
10
+ resolveFilePartSource,
11
+ scanPendingToolCalls,
12
+ } from "@assistant-ui/core/internal";
13
+ import type { AdkMessage } from "./types";
14
+
15
+ /** Exported for unit tests. */
16
+ export const getMessageContent = (msg: AppendMessage) => {
17
+ const allContent = [
18
+ ...msg.content,
19
+ ...(msg.attachments?.flatMap((a) => a.content) ?? []),
20
+ ];
21
+ const content = allContent.flatMap((part) => {
22
+ const type = part.type;
23
+ switch (type) {
24
+ case "text":
25
+ return { type: "text" as const, text: part.text };
26
+ case "image":
27
+ return { type: "image_url" as const, url: part.image };
28
+ case "file": {
29
+ const source = resolveFilePartSource(part);
30
+ if (source.kind === "url") {
31
+ return {
32
+ type: "file_url" as const,
33
+ url: source.url,
34
+ mimeType: part.mimeType,
35
+ };
36
+ }
37
+ return {
38
+ type: "file" as const,
39
+ mimeType: part.mimeType,
40
+ // Lands in Gemini `inlineData.data`, which takes bare base64, so a
41
+ // data URL envelope is stripped rather than forwarded.
42
+ data: source.data,
43
+ ...(part.filename != null && { filename: part.filename }),
44
+ };
45
+ }
46
+ case "audio": {
47
+ const parsed = parseDataUrl(part.audio.data);
48
+ return {
49
+ type: "file" as const,
50
+ mimeType: `audio/${part.audio.format}`,
51
+ data: parsed?.data ?? part.audio.data,
52
+ };
53
+ }
54
+ case "data":
55
+ return [];
56
+
57
+ case "tool-call":
58
+ throw new Error("Tool call appends are not supported.");
59
+
60
+ default: {
61
+ const _exhaustiveCheck: "reasoning" | "source" | "generative-ui" = type;
62
+ throw new Error(
63
+ `Unsupported append message part type: ${_exhaustiveCheck}`,
64
+ );
65
+ }
66
+ }
67
+ });
68
+
69
+ if (content.length === 1 && content[0]?.type === "text") {
70
+ return content[0].text ?? "";
71
+ }
72
+
73
+ return content;
74
+ };
75
+
76
+ /** Exported for unit tests. */
77
+ export const getPendingToolCalls = (messages: AdkMessage[]) => {
78
+ return scanPendingToolCalls(
79
+ messages,
80
+ (message) => {
81
+ if (message.type === "ai") {
82
+ return { toolCalls: message.tool_calls ?? [] };
83
+ }
84
+ if (message.type === "tool") {
85
+ return { toolCallId: message.tool_call_id };
86
+ }
87
+ return undefined;
88
+ },
89
+ (toolCall) => toolCall.id,
90
+ );
91
+ };
92
+
93
+ /**
94
+ * Exported for unit tests.
95
+ *
96
+ * Returns `{cancelled: true}` tool responses for pending tool calls when the
97
+ * user sends a new turn, EXCEPT for HITL interrupts marked via
98
+ * `long_running_tool_ids` (`adk_request_input`, `adk_request_confirmation`,
99
+ * `adk_request_credential`). Those must be answered through a dedicated tool
100
+ * UI + submit helper, not auto-cancelled.
101
+ */
102
+ export const getPendingCancellations = (
103
+ messages: AdkMessage[],
104
+ longRunningToolIds: readonly string[],
105
+ ): Array<AdkMessage & { type: "tool" }> => {
106
+ const longRunningSet = new Set(longRunningToolIds);
107
+ return getPendingToolCalls(messages)
108
+ .filter((t) => !longRunningSet.has(t.id))
109
+ .map(
110
+ (t) =>
111
+ ({
112
+ id: generateId(),
113
+ ...createToolCallCancellationStub(t),
114
+ }) satisfies AdkMessage & { type: "tool" },
115
+ );
116
+ };
117
+
118
+ export const truncateAdkMessages = (
119
+ threadMessages: readonly ThreadMessage[],
120
+ parentId: string | null,
121
+ ): AdkMessage[] => {
122
+ if (parentId === null) return [];
123
+ const parentIndex = threadMessages.findIndex((m) => m.id === parentId);
124
+ if (parentIndex === -1) return [];
125
+ const truncated: AdkMessage[] = [];
126
+ for (let i = 0; i <= parentIndex && i < threadMessages.length; i++) {
127
+ truncated.push(...getExternalStoreMessages<AdkMessage>(threadMessages[i]!));
128
+ }
129
+ return truncated;
130
+ };
131
+
132
+ export const toAdkUserMessage = (
133
+ msg: AppendMessage,
134
+ id = generateId(),
135
+ ): AdkMessage & { type: "human"; id: string } => ({
136
+ id,
137
+ type: "human",
138
+ content: getMessageContent(msg),
139
+ });
package/src/hooks.ts CHANGED
@@ -1,5 +1,5 @@
1
+ import { generateId } from "@assistant-ui/core";
1
2
  import { useAui } from "@assistant-ui/store";
2
- import { v4 as uuidv4 } from "uuid";
3
3
  import type { ReadonlyJSONValue } from "assistant-stream/utils";
4
4
  import { adkExtras } from "./adkExtras";
5
5
  import { toAdkConfirmationReply } from "./adkToolApproval";
@@ -79,7 +79,7 @@ export const useAdkSubmitAuth = () => {
79
79
  adkExtras.get(aui).send(
80
80
  [
81
81
  {
82
- id: uuidv4(),
82
+ id: generateId(),
83
83
  type: "tool",
84
84
  tool_call_id: toolCallId,
85
85
  name: "adk_request_credential",
@@ -98,7 +98,7 @@ export const useAdkSubmitInput = () => {
98
98
  adkExtras.get(aui).send(
99
99
  [
100
100
  {
101
- id: uuidv4(),
101
+ id: generateId(),
102
102
  type: "tool",
103
103
  tool_call_id: toolCallId,
104
104
  name: "adk_request_input",
@@ -1,6 +1,7 @@
1
1
  import { useState, useCallback, useEffect, useRef, useMemo } from "react";
2
- import { v4 as uuidv4 } from "uuid";
2
+ import { generateId } from "@assistant-ui/core";
3
3
  import { useAui } from "@assistant-ui/store";
4
+ import { invokeUserCallback } from "@assistant-ui/core/internal";
4
5
  import { AdkEventAccumulator } from "./AdkEventAccumulator";
5
6
  import { contentToParts } from "./contentToParts";
6
7
  import type {
@@ -28,24 +29,12 @@ export type UseAdkMessagesOptions = {
28
29
 
29
30
  type AdkRuntimeCallbackName = "onError" | "onCustomEvent" | "onAgentTransfer";
30
31
 
31
- const reportCallbackError = (name: AdkRuntimeCallbackName, error: unknown) => {
32
- console.error(`[react-google-adk] ${name} callback threw an error`, error);
33
- };
34
-
35
- const invokeAdkRuntimeCallback = <TArgs extends unknown[]>(
32
+ const invokeAdkRuntimeCallback = <TArgs extends readonly unknown[]>(
36
33
  name: AdkRuntimeCallbackName,
37
- callback: ((...args: TArgs) => void | Promise<void>) | undefined,
34
+ callback: ((...args: TArgs) => unknown) | undefined,
38
35
  ...args: TArgs
39
- ) => {
40
- if (!callback) return;
41
-
42
- try {
43
- void Promise.resolve(callback(...args)).catch((error) => {
44
- reportCallbackError(name, error);
45
- });
46
- } catch (error) {
47
- reportCallbackError(name, error);
48
- }
36
+ ): void => {
37
+ void invokeUserCallback("react-google-adk", name, callback, ...args);
49
38
  };
50
39
 
51
40
  export const useAdkMessages = ({
@@ -133,7 +122,7 @@ export const useAdkMessages = ({
133
122
  const sendMessage = useCallback(
134
123
  async (newMessages: AdkMessage[], config: AdkSendMessageConfig) => {
135
124
  const newMessagesWithId = newMessages.map((m) =>
136
- m.id ? m : { ...m, id: uuidv4() },
125
+ m.id ? m : { ...m, id: generateId() },
137
126
  ) as AdkMessage[];
138
127
 
139
128
  // A staged message is already in the thread under its own id, and the
@@ -306,7 +295,7 @@ export const messagesToEvents = (messages: AdkMessage[]): AdkEvent[] => {
306
295
  // message. Emitting it here keeps the optimistic view equal to that replay.
307
296
  if (parts.length === 0) parts.push({ text: "" });
308
297
 
309
- const event: AdkEvent = { id: (human ?? run[0])?.id ?? uuidv4() };
298
+ const event: AdkEvent = { id: (human ?? run[0])?.id ?? generateId() };
310
299
  if (human || run.length === 0) event.author = "user";
311
300
  event.content = { role: "user", parts };
312
301
  events.splice(run.length > 0 ? runIndex : events.length, 0, event);
@@ -318,7 +307,7 @@ export const messagesToEvents = (messages: AdkMessage[]): AdkEvent[] => {
318
307
  export const messageToEvent = (msg: AdkMessage): AdkEvent => {
319
308
  if (msg.type === "human") {
320
309
  return {
321
- id: msg.id ?? uuidv4(),
310
+ id: msg.id ?? generateId(),
322
311
  author: "user",
323
312
  content: { role: "user", parts: contentToParts(msg.content) },
324
313
  };
@@ -332,7 +321,7 @@ export const messageToEvent = (msg: AdkMessage): AdkEvent => {
332
321
  response = msg.content;
333
322
  }
334
323
  return {
335
- id: msg.id ?? uuidv4(),
324
+ id: msg.id ?? generateId(),
336
325
  content: {
337
326
  role: "user",
338
327
  parts: [
@@ -348,7 +337,7 @@ export const messageToEvent = (msg: AdkMessage): AdkEvent => {
348
337
  };
349
338
  }
350
339
 
351
- const result: AdkEvent = { id: msg.id ?? uuidv4() };
340
+ const result: AdkEvent = { id: msg.id ?? generateId() };
352
341
  if (msg.author != null) result.author = msg.author;
353
342
  result.content = {
354
343
  role: "model",
@@ -1,6 +1,5 @@
1
1
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
2
  import {
3
- getExternalStoreMessages,
4
3
  pickExternalStoreSharedOptions,
5
4
  type AttachmentAdapter,
6
5
  type DictationAdapter,
@@ -9,11 +8,13 @@ import {
9
8
  type RealtimeVoiceAdapter,
10
9
  type SpeechSynthesisAdapter,
11
10
  type AppendMessage,
12
- type ThreadMessage,
13
11
  type ToolExecutionStatus,
14
12
  generateId,
15
13
  } from "@assistant-ui/core";
16
- import { httpUrlPattern, parseDataUrl } from "@assistant-ui/core/internal";
14
+ import {
15
+ createAbortableThreadLoad,
16
+ createCloudThreadListAdapterCreateFallback,
17
+ } from "@assistant-ui/core/internal";
17
18
  import {
18
19
  useCloudThreadListAdapter,
19
20
  useRemoteThreadListRuntime,
@@ -37,139 +38,17 @@ import {
37
38
  convertAdkMessage,
38
39
  createAdkMessageConverter,
39
40
  } from "./convertAdkMessages";
41
+ import {
42
+ getMessageContent,
43
+ getPendingCancellations,
44
+ toAdkUserMessage,
45
+ truncateAdkMessages,
46
+ } from "./convertToAdkMessages";
40
47
  import {
41
48
  projectAdkToolApprovals,
42
49
  toAdkToolConfirmationReply,
43
50
  } from "./adkToolApproval";
44
51
  import { adkExtras } from "./adkExtras";
45
- import { v4 as uuidv4 } from "uuid";
46
-
47
- /** @internal — exported for unit tests. */
48
- export const getMessageContent = (msg: AppendMessage) => {
49
- const allContent = [
50
- ...msg.content,
51
- ...(msg.attachments?.flatMap((a) => a.content) ?? []),
52
- ];
53
- const content = allContent.flatMap((part) => {
54
- const type = part.type;
55
- switch (type) {
56
- case "text":
57
- return { type: "text" as const, text: part.text };
58
- case "image":
59
- return { type: "image_url" as const, url: part.image };
60
- case "file":
61
- if (part.sourceType === "url" || httpUrlPattern.test(part.data)) {
62
- return {
63
- type: "file_url" as const,
64
- url: part.data,
65
- mimeType: part.mimeType,
66
- };
67
- }
68
- return {
69
- type: "file" as const,
70
- mimeType: part.mimeType,
71
- // Lands in Gemini `inlineData.data`, which takes bare base64, so a
72
- // data URL envelope is stripped rather than forwarded.
73
- data: parseDataUrl(part.data)?.data ?? part.data,
74
- ...(part.filename != null && { filename: part.filename }),
75
- };
76
- case "audio": {
77
- const parsed = parseDataUrl(part.audio.data);
78
- return {
79
- type: "file" as const,
80
- mimeType: `audio/${part.audio.format}`,
81
- data: parsed?.data ?? part.audio.data,
82
- };
83
- }
84
- case "data":
85
- return [];
86
-
87
- case "tool-call":
88
- throw new Error("Tool call appends are not supported.");
89
-
90
- default: {
91
- const _exhaustiveCheck: "reasoning" | "source" | "generative-ui" = type;
92
- throw new Error(
93
- `Unsupported append message part type: ${_exhaustiveCheck}`,
94
- );
95
- }
96
- }
97
- });
98
-
99
- if (content.length === 1 && content[0]?.type === "text") {
100
- return content[0].text ?? "";
101
- }
102
-
103
- return content;
104
- };
105
-
106
- /** @internal — exported for unit tests. */
107
- export const getPendingToolCalls = (messages: AdkMessage[]) => {
108
- const pending = new Map<string, { id: string; name: string }>();
109
- for (const msg of messages) {
110
- if (msg.type === "ai" && msg.tool_calls) {
111
- for (const tc of msg.tool_calls) {
112
- pending.set(tc.id, tc);
113
- }
114
- }
115
- if (msg.type === "tool") {
116
- pending.delete(msg.tool_call_id);
117
- }
118
- }
119
- return [...pending.values()];
120
- };
121
-
122
- /**
123
- * @internal — exported for unit tests.
124
- *
125
- * Returns `{cancelled: true}` tool responses for pending tool calls when the
126
- * user sends a new turn, EXCEPT for HITL interrupts marked via
127
- * `long_running_tool_ids` (`adk_request_input`, `adk_request_confirmation`,
128
- * `adk_request_credential`). Those must be answered through a dedicated tool
129
- * UI + submit helper, not auto-cancelled.
130
- */
131
- export const getPendingCancellations = (
132
- messages: AdkMessage[],
133
- longRunningToolIds: readonly string[],
134
- ): Array<AdkMessage & { type: "tool" }> => {
135
- const longRunningSet = new Set(longRunningToolIds);
136
- return getPendingToolCalls(messages)
137
- .filter((t) => !longRunningSet.has(t.id))
138
- .map(
139
- (t) =>
140
- ({
141
- id: uuidv4(),
142
- type: "tool",
143
- name: t.name,
144
- tool_call_id: t.id,
145
- content: JSON.stringify({ cancelled: true }),
146
- status: "error",
147
- }) satisfies AdkMessage & { type: "tool" },
148
- );
149
- };
150
-
151
- const truncateAdkMessages = (
152
- threadMessages: readonly ThreadMessage[],
153
- parentId: string | null,
154
- ): AdkMessage[] => {
155
- if (parentId === null) return [];
156
- const parentIndex = threadMessages.findIndex((m) => m.id === parentId);
157
- if (parentIndex === -1) return [];
158
- const truncated: AdkMessage[] = [];
159
- for (let i = 0; i <= parentIndex && i < threadMessages.length; i++) {
160
- truncated.push(...getExternalStoreMessages<AdkMessage>(threadMessages[i]!));
161
- }
162
- return truncated;
163
- };
164
-
165
- const toAdkUserMessage = (
166
- msg: AppendMessage,
167
- id = generateId(),
168
- ): AdkMessage & { type: "human"; id: string } => ({
169
- id,
170
- type: "human",
171
- content: getMessageContent(msg),
172
- });
173
52
 
174
53
  export type UseAdkRuntimeOptions = ExternalStoreSharedOptions & {
175
54
  stream: AdkStreamCallback;
@@ -257,11 +136,7 @@ const useAdkRuntimeImpl = (options: UseAdkRuntimeOptions) => {
257
136
 
258
137
  const loadRef = useRef(load);
259
138
  loadRef.current = load;
260
- const loadControllerRef = useRef<{
261
- controller: AbortController;
262
- purpose: "initial" | "reload";
263
- promise?: Promise<void> | undefined;
264
- } | null>(null);
139
+ const loadController = useMemo(createAbortableThreadLoad, []);
265
140
  const messagesRef = useRef(messages);
266
141
  messagesRef.current = messages;
267
142
  const [isLoadingThread, setIsLoadingThread] = useState(
@@ -373,26 +248,17 @@ const useAdkRuntimeImpl = (options: UseAdkRuntimeOptions) => {
373
248
 
374
249
  // The initial load is already fetching what a refetch would ask for, and
375
250
  // taking it over strands the thread's history if the refetch then fails.
376
- if (
377
- purpose === "reload" &&
378
- loadControllerRef.current?.purpose === "initial"
379
- )
380
- return loadControllerRef.current.promise ?? Promise.resolve();
381
-
382
- loadControllerRef.current?.controller.abort();
383
- const controller = new AbortController();
384
- const record: NonNullable<typeof loadControllerRef.current> = {
385
- controller,
251
+ // Aborting a load the runtime no longer needs is not a failure.
252
+ // A refetch reports the failure to whoever awaited it; the initial load
253
+ // has no caller to tell.
254
+ return loadController.run({
386
255
  purpose,
387
- };
388
- loadControllerRef.current = record;
389
-
390
- const messagesAtLoadStart = messagesRef.current;
391
- if (purpose === "initial") setIsLoadingThread(true);
256
+ load: async (signal) => {
257
+ const messagesAtLoadStart = messagesRef.current;
258
+ if (purpose === "initial") setIsLoadingThread(true);
392
259
 
393
- const task = loadFn(externalId, { signal: controller.signal })
394
- .then((snapshot) => {
395
- if (controller.signal.aborted) return;
260
+ const snapshot = await loadFn(externalId, { signal });
261
+ if (signal.aborted) return;
396
262
  // A snapshot the session assembled before a run cannot speak for what
397
263
  // that run has since produced, and an ADK id cannot correlate a
398
264
  // message sent optimistically with the one the session stored for it,
@@ -406,29 +272,16 @@ const useAdkRuntimeImpl = (options: UseAdkRuntimeOptions) => {
406
272
  )
407
273
  return;
408
274
  applySnapshot(snapshot);
409
- })
410
- .catch((error: unknown) => {
411
- // Aborting a load the runtime no longer needs is not a failure.
412
- if (controller.signal.aborted) return;
413
- throw error;
414
- })
415
- .finally(() => {
416
- if (loadControllerRef.current?.controller === controller) {
417
- loadControllerRef.current = null;
418
- }
419
- if (controller.signal.aborted) return;
275
+ },
276
+ onSettled: () => {
420
277
  setIsLoadingThread(false);
421
- });
422
- record.promise = task;
423
-
424
- // A refetch reports the failure to whoever awaited it; the initial load
425
- // has no caller to tell.
426
- if (purpose === "reload") return task;
427
- return task.catch((e: unknown) => {
428
- console.warn("Failed to load ADK session:", e);
278
+ },
279
+ onInitialError: (error) => {
280
+ console.warn("Failed to load ADK session:", error);
281
+ },
429
282
  });
430
283
  },
431
- [threadListItem, applySnapshot],
284
+ [threadListItem, loadController, applySnapshot],
432
285
  );
433
286
 
434
287
  useEffect(() => {
@@ -436,14 +289,14 @@ const useAdkRuntimeImpl = (options: UseAdkRuntimeOptions) => {
436
289
  return () => {
437
290
  // Whatever is current, not this effect's own controller: a refetch swaps
438
291
  // the ref, and one in flight at unmount must be aborted too.
439
- loadControllerRef.current?.controller.abort();
292
+ loadController.abort();
440
293
  setIsLoadingThread(false);
441
294
  };
442
- }, [runLoad]);
295
+ }, [loadController, runLoad]);
443
296
 
444
297
  const runtime = useExternalStoreRuntime({
445
298
  ...pickExternalStoreSharedOptions(options),
446
- isRunning: effectiveIsRunning,
299
+ isRunning,
447
300
  isLoading: isLoadingThread,
448
301
  messages: threadMessages,
449
302
  unstable_enableToolInvocations: true,
@@ -475,7 +328,7 @@ const useAdkRuntimeImpl = (options: UseAdkRuntimeOptions) => {
475
328
  [
476
329
  ...cancellations,
477
330
  {
478
- id: uuidv4(),
331
+ id: generateId(),
479
332
  type: "human",
480
333
  content: getMessageContent(msg),
481
334
  },
@@ -509,7 +362,7 @@ const useAdkRuntimeImpl = (options: UseAdkRuntimeOptions) => {
509
362
  return handleSendMessage(
510
363
  [
511
364
  {
512
- id: uuidv4(),
365
+ id: generateId(),
513
366
  type: "human",
514
367
  content: getMessageContent(msg),
515
368
  },
@@ -564,7 +417,7 @@ const useAdkRuntimeImpl = (options: UseAdkRuntimeOptions) => {
564
417
  await handleSendMessage(
565
418
  [
566
419
  {
567
- id: uuidv4(),
420
+ id: generateId(),
568
421
  type: "tool",
569
422
  name: toolName,
570
423
  tool_call_id: toolCallId,
@@ -606,11 +459,10 @@ export const useAdkRuntime = ({
606
459
  const aui = useAui();
607
460
  const cloudAdapter = useCloudThreadListAdapter({
608
461
  cloud,
609
- create: async () => {
610
- if (create) return create();
611
- if (aui.threadListItem.source) return aui.threadListItem.initialize();
612
- return { externalId: undefined };
613
- },
462
+ create: createCloudThreadListAdapterCreateFallback(
463
+ create,
464
+ aui.threadListItem,
465
+ ),
614
466
  delete: deleteFn,
615
467
  });
616
468