@assistant-ui/react-google-adk 0.0.20 → 0.0.22

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 (41) hide show
  1. package/dist/AdkEventAccumulator.d.ts +1 -0
  2. package/dist/AdkEventAccumulator.d.ts.map +1 -1
  3. package/dist/AdkEventAccumulator.js +20 -4
  4. package/dist/AdkEventAccumulator.js.map +1 -1
  5. package/dist/AdkSessionAdapter.d.ts +4 -4
  6. package/dist/AdkSessionAdapter.d.ts.map +1 -1
  7. package/dist/AdkSessionAdapter.js +16 -3
  8. package/dist/AdkSessionAdapter.js.map +1 -1
  9. package/dist/convertAdkMessages.d.ts.map +1 -1
  10. package/dist/convertAdkMessages.js +18 -21
  11. package/dist/convertAdkMessages.js.map +1 -1
  12. package/dist/index.d.ts +2 -2
  13. package/dist/server/adkEventStream.d.ts.map +1 -1
  14. package/dist/server/adkEventStream.js +11 -1
  15. package/dist/server/adkEventStream.js.map +1 -1
  16. package/dist/types.d.ts +22 -1
  17. package/dist/types.d.ts.map +1 -1
  18. package/dist/types.js.map +1 -1
  19. package/dist/useAdkMessages.d.ts +2 -1
  20. package/dist/useAdkMessages.d.ts.map +1 -1
  21. package/dist/useAdkMessages.js +19 -1
  22. package/dist/useAdkMessages.js.map +1 -1
  23. package/dist/useAdkRuntime.d.ts +10 -4
  24. package/dist/useAdkRuntime.d.ts.map +1 -1
  25. package/dist/useAdkRuntime.js +68 -29
  26. package/dist/useAdkRuntime.js.map +1 -1
  27. package/package.json +8 -5
  28. package/src/AdkEventAccumulator.test.ts +127 -0
  29. package/src/AdkEventAccumulator.ts +31 -5
  30. package/src/AdkSessionAdapter.test.ts +43 -0
  31. package/src/AdkSessionAdapter.ts +21 -4
  32. package/src/convertAdkMessages.test.ts +40 -6
  33. package/src/convertAdkMessages.ts +16 -15
  34. package/src/index.ts +1 -0
  35. package/src/server/adkEventStream.test.ts +38 -0
  36. package/src/server/adkEventStream.ts +16 -1
  37. package/src/types.ts +21 -0
  38. package/src/useAdkMessages.ts +24 -1
  39. package/src/useAdkRuntime.refetch.test.tsx +336 -0
  40. package/src/useAdkRuntime.test.ts +161 -0
  41. package/src/useAdkRuntime.ts +127 -28
@@ -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,7 +13,7 @@ import {
13
13
  type ToolExecutionStatus,
14
14
  generateId,
15
15
  } from "@assistant-ui/core";
16
- import { parseDataUrl } from "@assistant-ui/core/internal";
16
+ import { httpUrlPattern, parseDataUrl } from "@assistant-ui/core/internal";
17
17
  import {
18
18
  useCloudThreadListAdapter,
19
19
  useRemoteThreadListRuntime,
@@ -25,6 +25,7 @@ import type { AssistantCloud } from "assistant-cloud";
25
25
  import type { RemoteThreadListAdapter } from "@assistant-ui/core";
26
26
  import type {
27
27
  AdkMessage,
28
+ AdkThreadSnapshot,
28
29
  AdkSendMessageConfig,
29
30
  AdkStreamCallback,
30
31
  OnAdkErrorCallback,
@@ -50,10 +51,19 @@ export const getMessageContent = (msg: AppendMessage) => {
50
51
  case "image":
51
52
  return { type: "image_url" as const, url: part.image };
52
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
+ }
53
61
  return {
54
62
  type: "file" as const,
55
63
  mimeType: part.mimeType,
56
- 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,
57
67
  ...(part.filename != null && { filename: part.filename }),
58
68
  };
59
69
  case "audio": {
@@ -171,7 +181,16 @@ export type UseAdkRuntimeOptions = ExternalStoreSharedOptions & {
171
181
  threadId: string,
172
182
  parentMessages: AdkMessage[],
173
183
  ) => Promise<string | null>;
174
- 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>;
175
194
  create?: () => Promise<{ externalId: string }>;
176
195
  delete?: (threadId: string) => Promise<void>;
177
196
  adapters?:
@@ -223,11 +242,26 @@ const useAdkRuntimeImpl = (options: UseAdkRuntimeOptions) => {
223
242
  cancel,
224
243
  setMessages,
225
244
  replaceMessages,
245
+ applySnapshot,
226
246
  } = useAdkMessages({
227
247
  stream,
228
248
  ...(eventHandlers && { eventHandlers }),
229
249
  });
230
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
+
231
265
  const [isRunning, setIsRunning] = useState(false);
232
266
  const [toolStatuses, setToolStatuses] = useState<
233
267
  Record<string, ToolExecutionStatus>
@@ -236,6 +270,8 @@ const useAdkRuntimeImpl = (options: UseAdkRuntimeOptions) => {
236
270
  (s) => s?.type === "executing",
237
271
  );
238
272
  const effectiveIsRunning = isRunning || hasExecutingTools;
273
+ const isRunningRef = useRef(effectiveIsRunning);
274
+ isRunningRef.current = effectiveIsRunning;
239
275
 
240
276
  const handleSendMessage = async (
241
277
  msgs: AdkMessage[],
@@ -302,9 +338,93 @@ const useAdkRuntimeImpl = (options: UseAdkRuntimeOptions) => {
302
338
  setMessages(nextMessages);
303
339
  };
304
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
+
305
424
  const runtime = useExternalStoreRuntime({
306
425
  ...pickExternalStoreSharedOptions(options),
307
426
  isRunning: effectiveIsRunning,
427
+ isLoading: isLoadingThread,
308
428
  messages: threadMessages,
309
429
  unstable_enableToolInvocations: true,
310
430
  setToolStatuses,
@@ -441,32 +561,11 @@ const useAdkRuntimeImpl = (options: UseAdkRuntimeOptions) => {
441
561
  cancel();
442
562
  }
443
563
  : undefined,
564
+ ...(load !== undefined && {
565
+ onRefetchThread: () => runLoad("reload"),
566
+ }),
444
567
  });
445
568
 
446
- {
447
- const loadRef = useRef(load);
448
- useEffect(() => {
449
- loadRef.current = load;
450
- });
451
-
452
- useEffect(() => {
453
- const loadFn = loadRef.current;
454
- if (!loadFn) return;
455
-
456
- const externalId = aui.threadListItem.getState().externalId;
457
- if (externalId == null) return;
458
-
459
- loadFn(externalId).then(
460
- ({ messages: msgs }) => {
461
- replaceMessages(msgs);
462
- },
463
- (e) => {
464
- console.warn("Failed to load ADK session:", e);
465
- },
466
- );
467
- }, [aui, replaceMessages]);
468
- }
469
-
470
569
  return runtime;
471
570
  };
472
571