@assistant-ui/react-google-adk 0.0.19 → 0.0.21

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.js +15 -2
  2. package/dist/AdkClient.js.map +1 -1
  3. package/dist/AdkEventAccumulator.d.ts +1 -0
  4. package/dist/AdkEventAccumulator.d.ts.map +1 -1
  5. package/dist/AdkEventAccumulator.js +20 -4
  6. package/dist/AdkEventAccumulator.js.map +1 -1
  7. package/dist/AdkSessionAdapter.d.ts +4 -4
  8. package/dist/AdkSessionAdapter.d.ts.map +1 -1
  9. package/dist/AdkSessionAdapter.js +16 -3
  10. package/dist/AdkSessionAdapter.js.map +1 -1
  11. package/dist/adkExtras.d.ts +1 -1
  12. package/dist/adkExtras.d.ts.map +1 -1
  13. package/dist/adkExtras.js +1 -1
  14. package/dist/adkExtras.js.map +1 -1
  15. package/dist/convertAdkMessages.d.ts.map +1 -1
  16. package/dist/convertAdkMessages.js +18 -11
  17. package/dist/convertAdkMessages.js.map +1 -1
  18. package/dist/index.d.ts +2 -2
  19. package/dist/types.d.ts +22 -1
  20. package/dist/types.d.ts.map +1 -1
  21. package/dist/types.js.map +1 -1
  22. package/dist/useAdkMessages.d.ts +2 -1
  23. package/dist/useAdkMessages.d.ts.map +1 -1
  24. package/dist/useAdkMessages.js +19 -1
  25. package/dist/useAdkMessages.js.map +1 -1
  26. package/dist/useAdkRuntime.d.ts +10 -4
  27. package/dist/useAdkRuntime.d.ts.map +1 -1
  28. package/dist/useAdkRuntime.js +79 -30
  29. package/dist/useAdkRuntime.js.map +1 -1
  30. package/package.json +7 -4
  31. package/src/AdkClient.test.ts +55 -0
  32. package/src/AdkClient.ts +16 -2
  33. package/src/AdkEventAccumulator.test.ts +127 -0
  34. package/src/AdkEventAccumulator.ts +31 -5
  35. package/src/AdkSessionAdapter.test.ts +43 -0
  36. package/src/AdkSessionAdapter.ts +21 -4
  37. package/src/adkExtras.ts +1 -1
  38. package/src/convertAdkMessages.test.ts +113 -0
  39. package/src/convertAdkMessages.ts +24 -4
  40. package/src/index.ts +1 -0
  41. package/src/types.ts +21 -0
  42. package/src/useAdkMessages.ts +24 -1
  43. package/src/useAdkRuntime.refetch.test.tsx +336 -0
  44. package/src/useAdkRuntime.test.ts +222 -0
  45. package/src/useAdkRuntime.ts +139 -34
@@ -1,4 +1,4 @@
1
- import { useEffect, useRef, useState } from "react";
1
+ import { useCallback, useEffect, useRef, useState } from "react";
2
2
  import {
3
3
  getExternalStoreMessages,
4
4
  pickExternalStoreSharedOptions,
@@ -13,6 +13,7 @@ import {
13
13
  type ToolExecutionStatus,
14
14
  generateId,
15
15
  } from "@assistant-ui/core";
16
+ import { httpUrlPattern, parseDataUrl } from "@assistant-ui/core/internal";
16
17
  import {
17
18
  useCloudThreadListAdapter,
18
19
  useRemoteThreadListRuntime,
@@ -24,6 +25,7 @@ import type { AssistantCloud } from "assistant-cloud";
24
25
  import type { RemoteThreadListAdapter } from "@assistant-ui/core";
25
26
  import type {
26
27
  AdkMessage,
28
+ AdkThreadSnapshot,
27
29
  AdkSendMessageConfig,
28
30
  AdkStreamCallback,
29
31
  OnAdkErrorCallback,
@@ -41,7 +43,7 @@ export const getMessageContent = (msg: AppendMessage) => {
41
43
  ...msg.content,
42
44
  ...(msg.attachments?.flatMap((a) => a.content) ?? []),
43
45
  ];
44
- const content = allContent.map((part) => {
46
+ const content = allContent.flatMap((part) => {
45
47
  const type = part.type;
46
48
  switch (type) {
47
49
  case "text":
@@ -49,23 +51,37 @@ export const getMessageContent = (msg: AppendMessage) => {
49
51
  case "image":
50
52
  return { type: "image_url" as const, url: part.image };
51
53
  case "file":
54
+ if (part.sourceType === "url" || httpUrlPattern.test(part.data)) {
55
+ return {
56
+ type: "file_url" as const,
57
+ url: part.data,
58
+ mimeType: part.mimeType,
59
+ };
60
+ }
52
61
  return {
53
62
  type: "file" as const,
54
63
  mimeType: part.mimeType,
55
- data: part.data,
64
+ // Lands in Gemini `inlineData.data`, which takes bare base64, so a
65
+ // data URL envelope is stripped rather than forwarded.
66
+ data: parseDataUrl(part.data)?.data ?? part.data,
56
67
  ...(part.filename != null && { filename: part.filename }),
57
68
  };
69
+ case "audio": {
70
+ const parsed = parseDataUrl(part.audio.data);
71
+ return {
72
+ type: "file" as const,
73
+ mimeType: `audio/${part.audio.format}`,
74
+ data: parsed?.data ?? part.audio.data,
75
+ };
76
+ }
77
+ case "data":
78
+ return [];
58
79
 
59
80
  case "tool-call":
60
81
  throw new Error("Tool call appends are not supported.");
61
82
 
62
83
  default: {
63
- const _exhaustiveCheck:
64
- | "reasoning"
65
- | "source"
66
- | "audio"
67
- | "data"
68
- | "generative-ui" = type;
84
+ const _exhaustiveCheck: "reasoning" | "source" | "generative-ui" = type;
69
85
  throw new Error(
70
86
  `Unsupported append message part type: ${_exhaustiveCheck}`,
71
87
  );
@@ -165,7 +181,16 @@ export type UseAdkRuntimeOptions = ExternalStoreSharedOptions & {
165
181
  threadId: string,
166
182
  parentMessages: AdkMessage[],
167
183
  ) => Promise<string | null>;
168
- load?: (threadId: string) => Promise<{ messages: AdkMessage[] }>;
184
+ /**
185
+ * Loads a thread's stored state. Called when the thread opens, and again for
186
+ * `threads.reloadMainThread()`, which refetches in place rather than
187
+ * remounting the runtime; the signal aborts a load the runtime no longer
188
+ * needs.
189
+ */
190
+ load?: (
191
+ threadId: string,
192
+ options?: { signal?: AbortSignal | undefined },
193
+ ) => Promise<AdkThreadSnapshot>;
169
194
  create?: () => Promise<{ externalId: string }>;
170
195
  delete?: (threadId: string) => Promise<void>;
171
196
  adapters?:
@@ -217,11 +242,26 @@ const useAdkRuntimeImpl = (options: UseAdkRuntimeOptions) => {
217
242
  cancel,
218
243
  setMessages,
219
244
  replaceMessages,
245
+ applySnapshot,
220
246
  } = useAdkMessages({
221
247
  stream,
222
248
  ...(eventHandlers && { eventHandlers }),
223
249
  });
224
250
 
251
+ const loadRef = useRef(load);
252
+ loadRef.current = load;
253
+ const loadControllerRef = useRef<{
254
+ controller: AbortController;
255
+ purpose: "initial" | "reload";
256
+ promise?: Promise<void> | undefined;
257
+ } | null>(null);
258
+ const messagesRef = useRef(messages);
259
+ messagesRef.current = messages;
260
+ const [isLoadingThread, setIsLoadingThread] = useState(
261
+ () =>
262
+ load !== undefined && aui.threadListItem.getState().externalId != null,
263
+ );
264
+
225
265
  const [isRunning, setIsRunning] = useState(false);
226
266
  const [toolStatuses, setToolStatuses] = useState<
227
267
  Record<string, ToolExecutionStatus>
@@ -230,6 +270,8 @@ const useAdkRuntimeImpl = (options: UseAdkRuntimeOptions) => {
230
270
  (s) => s?.type === "executing",
231
271
  );
232
272
  const effectiveIsRunning = isRunning || hasExecutingTools;
273
+ const isRunningRef = useRef(effectiveIsRunning);
274
+ isRunningRef.current = effectiveIsRunning;
233
275
 
234
276
  const handleSendMessage = async (
235
277
  msgs: AdkMessage[],
@@ -296,9 +338,93 @@ const useAdkRuntimeImpl = (options: UseAdkRuntimeOptions) => {
296
338
  setMessages(nextMessages);
297
339
  };
298
340
 
341
+ // The scoped client, not `aui` itself: useAui returns a render-bound
342
+ // instance, so depending on it would re-run the load on every render.
343
+ const threadListItem =
344
+ aui.threadListItem.source !== null ? aui.threadListItem : undefined;
345
+
346
+ const runLoad = useCallback(
347
+ (purpose: "initial" | "reload" = "initial") => {
348
+ const loadFn = loadRef.current;
349
+ if (!loadFn || !threadListItem) return Promise.resolve();
350
+
351
+ const externalId = threadListItem.getState().externalId;
352
+ if (externalId == null) return Promise.resolve();
353
+
354
+ // The initial load is already fetching what a refetch would ask for, and
355
+ // taking it over strands the thread's history if the refetch then fails.
356
+ if (
357
+ purpose === "reload" &&
358
+ loadControllerRef.current?.purpose === "initial"
359
+ )
360
+ return loadControllerRef.current.promise ?? Promise.resolve();
361
+
362
+ loadControllerRef.current?.controller.abort();
363
+ const controller = new AbortController();
364
+ const record: NonNullable<typeof loadControllerRef.current> = {
365
+ controller,
366
+ purpose,
367
+ };
368
+ loadControllerRef.current = record;
369
+
370
+ const messagesAtLoadStart = messagesRef.current;
371
+ if (purpose === "initial") setIsLoadingThread(true);
372
+
373
+ const task = loadFn(externalId, { signal: controller.signal })
374
+ .then((snapshot) => {
375
+ if (controller.signal.aborted) return;
376
+ // A snapshot the session assembled before a run cannot speak for what
377
+ // that run has since produced, and an ADK id cannot correlate a
378
+ // message sent optimistically with the one the session stored for it,
379
+ // so there is nothing here that could merge the two. A refetch that
380
+ // raced a run therefore defers to the run, whether the run started
381
+ // during the load or was already streaming when it began.
382
+ if (
383
+ purpose === "reload" &&
384
+ (isRunningRef.current ||
385
+ messagesRef.current !== messagesAtLoadStart)
386
+ )
387
+ return;
388
+ applySnapshot(snapshot);
389
+ })
390
+ .catch((error: unknown) => {
391
+ // Aborting a load the runtime no longer needs is not a failure.
392
+ if (controller.signal.aborted) return;
393
+ throw error;
394
+ })
395
+ .finally(() => {
396
+ if (loadControllerRef.current?.controller === controller) {
397
+ loadControllerRef.current = null;
398
+ }
399
+ if (controller.signal.aborted) return;
400
+ setIsLoadingThread(false);
401
+ });
402
+ record.promise = task;
403
+
404
+ // A refetch reports the failure to whoever awaited it; the initial load
405
+ // has no caller to tell.
406
+ if (purpose === "reload") return task;
407
+ return task.catch((e: unknown) => {
408
+ console.warn("Failed to load ADK session:", e);
409
+ });
410
+ },
411
+ [threadListItem, applySnapshot],
412
+ );
413
+
414
+ useEffect(() => {
415
+ runLoad();
416
+ return () => {
417
+ // Whatever is current, not this effect's own controller: a refetch swaps
418
+ // the ref, and one in flight at unmount must be aborted too.
419
+ loadControllerRef.current?.controller.abort();
420
+ setIsLoadingThread(false);
421
+ };
422
+ }, [runLoad]);
423
+
299
424
  const runtime = useExternalStoreRuntime({
300
425
  ...pickExternalStoreSharedOptions(options),
301
426
  isRunning: effectiveIsRunning,
427
+ isLoading: isLoadingThread,
302
428
  messages: threadMessages,
303
429
  unstable_enableToolInvocations: true,
304
430
  setToolStatuses,
@@ -435,32 +561,11 @@ const useAdkRuntimeImpl = (options: UseAdkRuntimeOptions) => {
435
561
  cancel();
436
562
  }
437
563
  : undefined,
564
+ ...(load !== undefined && {
565
+ onRefetchThread: () => runLoad("reload"),
566
+ }),
438
567
  });
439
568
 
440
- {
441
- const loadRef = useRef(load);
442
- useEffect(() => {
443
- loadRef.current = load;
444
- });
445
-
446
- useEffect(() => {
447
- const loadFn = loadRef.current;
448
- if (!loadFn) return;
449
-
450
- const externalId = aui.threadListItem.getState().externalId;
451
- if (externalId == null) return;
452
-
453
- loadFn(externalId).then(
454
- ({ messages: msgs }) => {
455
- replaceMessages(msgs);
456
- },
457
- (e) => {
458
- console.warn("Failed to load ADK session:", e);
459
- },
460
- );
461
- }, [aui, replaceMessages]);
462
- }
463
-
464
569
  return runtime;
465
570
  };
466
571