@assistant-ui/react-google-adk 0.0.4 → 0.0.6
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.map +1 -1
- package/dist/AdkEventAccumulator.js +48 -4
- package/dist/AdkEventAccumulator.js.map +1 -1
- package/dist/hooks.d.ts +4 -1
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +18 -0
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/useAdkMessages.d.ts +1 -0
- package/dist/useAdkMessages.d.ts.map +1 -1
- package/dist/useAdkMessages.js +13 -0
- package/dist/useAdkMessages.js.map +1 -1
- package/dist/useAdkRuntime.d.ts.map +1 -1
- package/dist/useAdkRuntime.js +30 -14
- package/dist/useAdkRuntime.js.map +1 -1
- package/package.json +7 -7
- package/src/AdkEventAccumulator.test.ts +521 -2
- package/src/AdkEventAccumulator.ts +49 -4
- package/src/hooks.ts +28 -1
- package/src/index.ts +1 -0
- package/src/useAdkMessages.ts +17 -0
- package/src/useAdkRuntime.test.ts +138 -0
- package/src/useAdkRuntime.ts +37 -17
package/src/useAdkMessages.ts
CHANGED
|
@@ -64,6 +64,22 @@ export const useAdkMessages = ({
|
|
|
64
64
|
_setMessages(msgs);
|
|
65
65
|
}, []);
|
|
66
66
|
|
|
67
|
+
// Replace the message list AND reset derived per-turn HITL state.
|
|
68
|
+
// Used by truncation paths (edit, reload, load) so that stale interrupt
|
|
69
|
+
// markers and per-message metadata from the removed messages don't leak
|
|
70
|
+
// into the next turn.
|
|
71
|
+
const replaceMessages = useCallback(
|
|
72
|
+
(msgs: AdkMessage[]) => {
|
|
73
|
+
setMessagesImmediate(msgs);
|
|
74
|
+
setLongRunningToolIds([]);
|
|
75
|
+
setToolConfirmations([]);
|
|
76
|
+
setAuthRequests([]);
|
|
77
|
+
setEscalated(false);
|
|
78
|
+
setMessageMetadata(new Map());
|
|
79
|
+
},
|
|
80
|
+
[setMessagesImmediate],
|
|
81
|
+
);
|
|
82
|
+
|
|
67
83
|
const abortControllerRef = useRef<AbortController | null>(null);
|
|
68
84
|
|
|
69
85
|
const { onError, onCustomEvent, onAgentTransfer } = useMemo(
|
|
@@ -180,6 +196,7 @@ export const useAdkMessages = ({
|
|
|
180
196
|
sendMessage,
|
|
181
197
|
cancel,
|
|
182
198
|
setMessages: setMessagesImmediate,
|
|
199
|
+
replaceMessages,
|
|
183
200
|
};
|
|
184
201
|
};
|
|
185
202
|
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { getPendingCancellations, getPendingToolCalls } from "./useAdkRuntime";
|
|
3
|
+
import type { AdkMessage } from "./types";
|
|
4
|
+
|
|
5
|
+
const aiWithToolCalls = (
|
|
6
|
+
id: string,
|
|
7
|
+
toolCalls: Array<{ id: string; name: string }>,
|
|
8
|
+
): AdkMessage => ({
|
|
9
|
+
id,
|
|
10
|
+
type: "ai",
|
|
11
|
+
content: [],
|
|
12
|
+
tool_calls: toolCalls.map((tc) => ({
|
|
13
|
+
id: tc.id,
|
|
14
|
+
name: tc.name,
|
|
15
|
+
args: {},
|
|
16
|
+
argsText: "{}",
|
|
17
|
+
})),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const toolResponse = (
|
|
21
|
+
id: string,
|
|
22
|
+
toolCallId: string,
|
|
23
|
+
name: string,
|
|
24
|
+
): AdkMessage => ({
|
|
25
|
+
id,
|
|
26
|
+
type: "tool",
|
|
27
|
+
tool_call_id: toolCallId,
|
|
28
|
+
name,
|
|
29
|
+
content: JSON.stringify({ ok: true }),
|
|
30
|
+
status: "success",
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe("getPendingToolCalls", () => {
|
|
34
|
+
it("returns tool calls without matching tool responses", () => {
|
|
35
|
+
const messages: AdkMessage[] = [
|
|
36
|
+
aiWithToolCalls("ai-1", [
|
|
37
|
+
{ id: "tc-1", name: "tool_a" },
|
|
38
|
+
{ id: "tc-2", name: "tool_b" },
|
|
39
|
+
]),
|
|
40
|
+
];
|
|
41
|
+
expect(getPendingToolCalls(messages)).toEqual([
|
|
42
|
+
{ id: "tc-1", name: "tool_a", args: {}, argsText: "{}" },
|
|
43
|
+
{ id: "tc-2", name: "tool_b", args: {}, argsText: "{}" },
|
|
44
|
+
]);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("excludes tool calls that have a matching tool response", () => {
|
|
48
|
+
const messages: AdkMessage[] = [
|
|
49
|
+
aiWithToolCalls("ai-1", [
|
|
50
|
+
{ id: "tc-1", name: "tool_a" },
|
|
51
|
+
{ id: "tc-2", name: "tool_b" },
|
|
52
|
+
]),
|
|
53
|
+
toolResponse("t-1", "tc-1", "tool_a"),
|
|
54
|
+
];
|
|
55
|
+
expect(getPendingToolCalls(messages)).toEqual([
|
|
56
|
+
{ id: "tc-2", name: "tool_b", args: {}, argsText: "{}" },
|
|
57
|
+
]);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("returns empty for threads with no ai messages", () => {
|
|
61
|
+
expect(getPendingToolCalls([])).toEqual([]);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe("getPendingCancellations", () => {
|
|
66
|
+
it("emits a {cancelled:true} tool message for every pending tool call", () => {
|
|
67
|
+
const messages: AdkMessage[] = [
|
|
68
|
+
aiWithToolCalls("ai-1", [{ id: "tc-1", name: "tool_a" }]),
|
|
69
|
+
];
|
|
70
|
+
const result = getPendingCancellations(messages, []);
|
|
71
|
+
expect(result).toHaveLength(1);
|
|
72
|
+
expect(result[0]).toMatchObject({
|
|
73
|
+
type: "tool",
|
|
74
|
+
name: "tool_a",
|
|
75
|
+
tool_call_id: "tc-1",
|
|
76
|
+
content: JSON.stringify({ cancelled: true }),
|
|
77
|
+
status: "error",
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("skips tool calls whose id is in longRunningToolIds", () => {
|
|
82
|
+
const messages: AdkMessage[] = [
|
|
83
|
+
aiWithToolCalls("ai-1", [
|
|
84
|
+
{ id: "lrt-1", name: "adk_request_input" },
|
|
85
|
+
{ id: "tc-2", name: "regular_tool" },
|
|
86
|
+
]),
|
|
87
|
+
];
|
|
88
|
+
const result = getPendingCancellations(messages, ["lrt-1"]);
|
|
89
|
+
expect(result).toHaveLength(1);
|
|
90
|
+
expect(result[0]).toMatchObject({
|
|
91
|
+
type: "tool",
|
|
92
|
+
name: "regular_tool",
|
|
93
|
+
tool_call_id: "tc-2",
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("skips all three HITL interrupt types", () => {
|
|
98
|
+
const messages: AdkMessage[] = [
|
|
99
|
+
aiWithToolCalls("ai-1", [
|
|
100
|
+
{ id: "lrt-1", name: "adk_request_input" },
|
|
101
|
+
{ id: "lrt-2", name: "adk_request_confirmation" },
|
|
102
|
+
{ id: "lrt-3", name: "adk_request_credential" },
|
|
103
|
+
]),
|
|
104
|
+
];
|
|
105
|
+
const result = getPendingCancellations(messages, [
|
|
106
|
+
"lrt-1",
|
|
107
|
+
"lrt-2",
|
|
108
|
+
"lrt-3",
|
|
109
|
+
]);
|
|
110
|
+
expect(result).toEqual([]);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("returns empty when no tool calls are pending", () => {
|
|
114
|
+
const messages: AdkMessage[] = [
|
|
115
|
+
aiWithToolCalls("ai-1", [{ id: "tc-1", name: "tool_a" }]),
|
|
116
|
+
toolResponse("t-1", "tc-1", "tool_a"),
|
|
117
|
+
];
|
|
118
|
+
expect(getPendingCancellations(messages, [])).toEqual([]);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("cancels regular tool calls even when HITL interrupts are resolved", () => {
|
|
122
|
+
const messages: AdkMessage[] = [
|
|
123
|
+
aiWithToolCalls("ai-1", [
|
|
124
|
+
{ id: "lrt-1", name: "adk_request_input" },
|
|
125
|
+
{ id: "tc-2", name: "regular_tool" },
|
|
126
|
+
]),
|
|
127
|
+
toolResponse("t-1", "lrt-1", "adk_request_input"),
|
|
128
|
+
];
|
|
129
|
+
// lrt-1 lingering in longRunningToolIds is harmless because
|
|
130
|
+
// getPendingToolCalls already excluded it via the tool response.
|
|
131
|
+
const result = getPendingCancellations(messages, ["lrt-1"]);
|
|
132
|
+
expect(result).toHaveLength(1);
|
|
133
|
+
expect(result[0]).toMatchObject({
|
|
134
|
+
tool_call_id: "tc-2",
|
|
135
|
+
name: "regular_tool",
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
});
|
package/src/useAdkRuntime.ts
CHANGED
|
@@ -69,7 +69,8 @@ const getMessageContent = (msg: AppendMessage) => {
|
|
|
69
69
|
return content;
|
|
70
70
|
};
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
/** @internal — exported for unit tests. */
|
|
73
|
+
export const getPendingToolCalls = (messages: AdkMessage[]) => {
|
|
73
74
|
const pending = new Map<string, { id: string; name: string }>();
|
|
74
75
|
for (const msg of messages) {
|
|
75
76
|
if (msg.type === "ai" && msg.tool_calls) {
|
|
@@ -84,6 +85,35 @@ const getPendingToolCalls = (messages: AdkMessage[]) => {
|
|
|
84
85
|
return [...pending.values()];
|
|
85
86
|
};
|
|
86
87
|
|
|
88
|
+
/**
|
|
89
|
+
* @internal — exported for unit tests.
|
|
90
|
+
*
|
|
91
|
+
* Returns `{cancelled: true}` tool responses for pending tool calls when the
|
|
92
|
+
* user sends a new turn, EXCEPT for HITL interrupts marked via
|
|
93
|
+
* `long_running_tool_ids` (`adk_request_input`, `adk_request_confirmation`,
|
|
94
|
+
* `adk_request_credential`). Those must be answered through a dedicated tool
|
|
95
|
+
* UI + submit helper, not auto-cancelled.
|
|
96
|
+
*/
|
|
97
|
+
export const getPendingCancellations = (
|
|
98
|
+
messages: AdkMessage[],
|
|
99
|
+
longRunningToolIds: readonly string[],
|
|
100
|
+
): Array<AdkMessage & { type: "tool" }> => {
|
|
101
|
+
const longRunningSet = new Set(longRunningToolIds);
|
|
102
|
+
return getPendingToolCalls(messages)
|
|
103
|
+
.filter((t) => !longRunningSet.has(t.id))
|
|
104
|
+
.map(
|
|
105
|
+
(t) =>
|
|
106
|
+
({
|
|
107
|
+
id: uuidv4(),
|
|
108
|
+
type: "tool",
|
|
109
|
+
name: t.name,
|
|
110
|
+
tool_call_id: t.id,
|
|
111
|
+
content: JSON.stringify({ cancelled: true }),
|
|
112
|
+
status: "error",
|
|
113
|
+
}) satisfies AdkMessage & { type: "tool" },
|
|
114
|
+
);
|
|
115
|
+
};
|
|
116
|
+
|
|
87
117
|
const truncateAdkMessages = (
|
|
88
118
|
threadMessages: readonly ThreadMessage[],
|
|
89
119
|
parentId: string | null,
|
|
@@ -153,7 +183,7 @@ const useAdkRuntimeImpl = ({
|
|
|
153
183
|
messageMetadata,
|
|
154
184
|
sendMessage,
|
|
155
185
|
cancel,
|
|
156
|
-
|
|
186
|
+
replaceMessages,
|
|
157
187
|
} = useAdkMessages({
|
|
158
188
|
stream,
|
|
159
189
|
...(eventHandlers && { eventHandlers }),
|
|
@@ -239,17 +269,7 @@ const useAdkRuntimeImpl = ({
|
|
|
239
269
|
|
|
240
270
|
const cancellations =
|
|
241
271
|
autoCancelPendingToolCalls !== false
|
|
242
|
-
?
|
|
243
|
-
(t) =>
|
|
244
|
-
({
|
|
245
|
-
id: uuidv4(),
|
|
246
|
-
type: "tool",
|
|
247
|
-
name: t.name,
|
|
248
|
-
tool_call_id: t.id,
|
|
249
|
-
content: JSON.stringify({ cancelled: true }),
|
|
250
|
-
status: "error",
|
|
251
|
-
}) satisfies AdkMessage & { type: "tool" },
|
|
252
|
-
)
|
|
272
|
+
? getPendingCancellations(messages, longRunningToolIds)
|
|
253
273
|
: [];
|
|
254
274
|
|
|
255
275
|
return handleSendMessage(
|
|
@@ -271,7 +291,7 @@ const useAdkRuntimeImpl = ({
|
|
|
271
291
|
threadMessagesRef.current,
|
|
272
292
|
msg.parentId,
|
|
273
293
|
);
|
|
274
|
-
|
|
294
|
+
replaceMessages(truncated);
|
|
275
295
|
const externalId = aui.threadListItem().getState().externalId;
|
|
276
296
|
const checkpointId = externalId
|
|
277
297
|
? await getCheckpointId(externalId, truncated)
|
|
@@ -298,7 +318,7 @@ const useAdkRuntimeImpl = ({
|
|
|
298
318
|
threadMessagesRef.current,
|
|
299
319
|
parentId,
|
|
300
320
|
);
|
|
301
|
-
|
|
321
|
+
replaceMessages(truncated);
|
|
302
322
|
const externalId = aui.threadListItem().getState().externalId;
|
|
303
323
|
const checkpointId = externalId
|
|
304
324
|
? await getCheckpointId(externalId, truncated)
|
|
@@ -356,13 +376,13 @@ const useAdkRuntimeImpl = ({
|
|
|
356
376
|
|
|
357
377
|
loadFn(externalId).then(
|
|
358
378
|
({ messages: msgs }) => {
|
|
359
|
-
|
|
379
|
+
replaceMessages(msgs);
|
|
360
380
|
},
|
|
361
381
|
(e) => {
|
|
362
382
|
console.warn("Failed to load ADK session:", e);
|
|
363
383
|
},
|
|
364
384
|
);
|
|
365
|
-
}, [aui,
|
|
385
|
+
}, [aui, replaceMessages]);
|
|
366
386
|
}
|
|
367
387
|
|
|
368
388
|
return runtime;
|