@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.
- package/dist/AdkEventAccumulator.d.ts +2 -0
- package/dist/AdkEventAccumulator.d.ts.map +1 -1
- package/dist/AdkEventAccumulator.js +21 -9
- package/dist/AdkEventAccumulator.js.map +1 -1
- package/dist/adkToolApproval.js +2 -2
- package/dist/adkToolApproval.js.map +1 -1
- package/dist/convertToAdkMessages.d.ts +49 -0
- package/dist/convertToAdkMessages.d.ts.map +1 -0
- package/dist/convertToAdkMessages.js +86 -0
- package/dist/convertToAdkMessages.js.map +1 -0
- package/dist/hooks.js +3 -3
- package/dist/hooks.js.map +1 -1
- package/dist/useAdkMessages.d.ts.map +1 -1
- package/dist/useAdkMessages.js +8 -17
- package/dist/useAdkMessages.js.map +1 -1
- package/dist/useAdkRuntime.d.ts.map +1 -1
- package/dist/useAdkRuntime.js +33 -126
- package/dist/useAdkRuntime.js.map +1 -1
- package/package.json +6 -7
- package/src/AdkEventAccumulator.test.ts +84 -1
- package/src/AdkEventAccumulator.ts +20 -7
- package/src/adkToolApproval.ts +2 -2
- package/src/{useAdkRuntime.test.ts → convertToAdkMessages.test.ts} +1 -1
- package/src/convertToAdkMessages.ts +139 -0
- package/src/hooks.ts +3 -3
- package/src/useAdkMessages.ts +11 -22
- package/src/useAdkRuntime.ts +37 -185
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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}` :
|
|
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}` :
|
|
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 ??
|
|
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 ??
|
|
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),
|
package/src/adkToolApproval.ts
CHANGED
|
@@ -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:
|
|
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 "./
|
|
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:
|
|
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:
|
|
101
|
+
id: generateId(),
|
|
102
102
|
type: "tool",
|
|
103
103
|
tool_call_id: toolCallId,
|
|
104
104
|
name: "adk_request_input",
|
package/src/useAdkMessages.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useState, useCallback, useEffect, useRef, useMemo } from "react";
|
|
2
|
-
import {
|
|
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
|
|
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) =>
|
|
34
|
+
callback: ((...args: TArgs) => unknown) | undefined,
|
|
38
35
|
...args: TArgs
|
|
39
|
-
) => {
|
|
40
|
-
|
|
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:
|
|
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 ??
|
|
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 ??
|
|
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 ??
|
|
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 ??
|
|
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",
|
package/src/useAdkRuntime.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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
|
-
|
|
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
|
-
|
|
394
|
-
|
|
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
|
-
|
|
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
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
},
|
|
462
|
+
create: createCloudThreadListAdapterCreateFallback(
|
|
463
|
+
create,
|
|
464
|
+
aui.threadListItem,
|
|
465
|
+
),
|
|
614
466
|
delete: deleteFn,
|
|
615
467
|
});
|
|
616
468
|
|