@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.
- package/dist/AdkClient.js +15 -2
- package/dist/AdkClient.js.map +1 -1
- package/dist/AdkEventAccumulator.d.ts +1 -0
- package/dist/AdkEventAccumulator.d.ts.map +1 -1
- package/dist/AdkEventAccumulator.js +20 -4
- package/dist/AdkEventAccumulator.js.map +1 -1
- package/dist/AdkSessionAdapter.d.ts +4 -4
- package/dist/AdkSessionAdapter.d.ts.map +1 -1
- package/dist/AdkSessionAdapter.js +16 -3
- package/dist/AdkSessionAdapter.js.map +1 -1
- package/dist/adkExtras.d.ts +1 -1
- package/dist/adkExtras.d.ts.map +1 -1
- package/dist/adkExtras.js +1 -1
- package/dist/adkExtras.js.map +1 -1
- package/dist/convertAdkMessages.d.ts.map +1 -1
- package/dist/convertAdkMessages.js +18 -11
- package/dist/convertAdkMessages.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/types.d.ts +22 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/useAdkMessages.d.ts +2 -1
- package/dist/useAdkMessages.d.ts.map +1 -1
- package/dist/useAdkMessages.js +19 -1
- package/dist/useAdkMessages.js.map +1 -1
- package/dist/useAdkRuntime.d.ts +10 -4
- package/dist/useAdkRuntime.d.ts.map +1 -1
- package/dist/useAdkRuntime.js +79 -30
- package/dist/useAdkRuntime.js.map +1 -1
- package/package.json +7 -4
- package/src/AdkClient.test.ts +55 -0
- package/src/AdkClient.ts +16 -2
- package/src/AdkEventAccumulator.test.ts +127 -0
- package/src/AdkEventAccumulator.ts +31 -5
- package/src/AdkSessionAdapter.test.ts +43 -0
- package/src/AdkSessionAdapter.ts +21 -4
- package/src/adkExtras.ts +1 -1
- package/src/convertAdkMessages.test.ts +113 -0
- package/src/convertAdkMessages.ts +24 -4
- package/src/index.ts +1 -0
- package/src/types.ts +21 -0
- package/src/useAdkMessages.ts +24 -1
- package/src/useAdkRuntime.refetch.test.tsx +336 -0
- package/src/useAdkRuntime.test.ts +222 -0
- package/src/useAdkRuntime.ts +139 -34
package/src/useAdkRuntime.ts
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|