@assistant-ui/react-google-adk 0.0.33 → 0.0.35
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 +4 -1
- package/dist/AdkEventAccumulator.js.map +1 -1
- package/dist/adkToolApproval.js +1 -1
- package/dist/adkToolApproval.js.map +1 -1
- package/dist/convertAdkMessages.d.ts.map +1 -1
- package/dist/convertAdkMessages.js +30 -19
- package/dist/convertAdkMessages.js.map +1 -1
- package/dist/convertToAdkMessages.d.ts.map +1 -1
- package/dist/convertToAdkMessages.js +2 -2
- package/dist/convertToAdkMessages.js.map +1 -1
- package/dist/sdkIdentity.js +1 -1
- package/dist/useAdkMessages.d.ts +23 -0
- package/dist/useAdkMessages.d.ts.map +1 -1
- package/dist/useAdkMessages.js +34 -9
- package/dist/useAdkMessages.js.map +1 -1
- package/dist/useAdkRuntime.d.ts.map +1 -1
- package/dist/useAdkRuntime.js +60 -9
- package/dist/useAdkRuntime.js.map +1 -1
- package/package.json +7 -6
- package/src/AdkEventAccumulator.test.ts +32 -0
- package/src/AdkEventAccumulator.ts +1 -0
- package/src/adkToolApproval.test.ts +19 -0
- package/src/adkToolApproval.ts +1 -1
- package/src/convertAdkMessages.test.ts +129 -0
- package/src/convertAdkMessages.ts +19 -1
- package/src/convertToAdkMessages.test.ts +14 -0
- package/src/convertToAdkMessages.ts +4 -1
- package/src/useAdkMessages.test.ts +55 -0
- package/src/useAdkMessages.ts +65 -6
- package/src/useAdkRuntime.cancellation.test.tsx +544 -0
- package/src/useAdkRuntime.fast-refresh.test.tsx +166 -0
- package/src/useAdkRuntime.refetch.test.tsx +1 -0
- package/src/useAdkRuntime.ts +91 -9
- package/src/useAdkRuntimeApproval.test.tsx +305 -36
package/src/useAdkRuntime.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
2
|
useCallback,
|
|
3
|
-
useEffect,
|
|
4
3
|
useInsertionEffect,
|
|
5
4
|
useMemo,
|
|
6
5
|
useRef,
|
|
@@ -22,6 +21,7 @@ import {
|
|
|
22
21
|
import {
|
|
23
22
|
createAbortableThreadLoad,
|
|
24
23
|
createCloudThreadListAdapterCreateFallback,
|
|
24
|
+
isRecord,
|
|
25
25
|
} from "@assistant-ui/core/internal";
|
|
26
26
|
import {
|
|
27
27
|
useCloudThreadListAdapter,
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
useExternalStoreRuntime,
|
|
31
31
|
} from "@assistant-ui/core/react";
|
|
32
32
|
import { useAui } from "@assistant-ui/store";
|
|
33
|
+
import { useReplaySafeEffect } from "@assistant-ui/store/internal";
|
|
33
34
|
import type { AssistantCloud } from "assistant-cloud";
|
|
34
35
|
import type { RemoteThreadListAdapter } from "@assistant-ui/core";
|
|
35
36
|
import type {
|
|
@@ -41,7 +42,7 @@ import type {
|
|
|
41
42
|
OnAdkCustomEventCallback,
|
|
42
43
|
OnAdkAgentTransferCallback,
|
|
43
44
|
} from "./types";
|
|
44
|
-
import {
|
|
45
|
+
import { useAdkMessagesInternal } from "./useAdkMessages";
|
|
45
46
|
import {
|
|
46
47
|
convertAdkMessage,
|
|
47
48
|
createAdkMessageConverter,
|
|
@@ -123,6 +124,62 @@ const useAdkRuntimeImpl = (options: UseAdkRuntimeOptions) => {
|
|
|
123
124
|
eventHandlers,
|
|
124
125
|
} = options;
|
|
125
126
|
const aui = useAui();
|
|
127
|
+
const runConfigByToolCallIdRef = useRef(new Map<string, unknown>());
|
|
128
|
+
|
|
129
|
+
const rememberMessageOwnership = useCallback(
|
|
130
|
+
(newMessages: AdkMessage[], runConfig: unknown) => {
|
|
131
|
+
const toolOwnership = runConfigByToolCallIdRef.current;
|
|
132
|
+
for (const message of newMessages) {
|
|
133
|
+
if (message.type !== "ai") continue;
|
|
134
|
+
for (const toolCall of message.tool_calls ?? []) {
|
|
135
|
+
if (!isRecord(toolCall)) continue;
|
|
136
|
+
if (!toolOwnership.has(toolCall.id)) {
|
|
137
|
+
toolOwnership.set(toolCall.id, runConfig);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
[],
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
const seedMessageOwnership = useCallback((history: AdkMessage[]) => {
|
|
146
|
+
const currentOwnership = runConfigByToolCallIdRef.current;
|
|
147
|
+
const nextOwnership = new Map<string, unknown>();
|
|
148
|
+
for (const message of history) {
|
|
149
|
+
if (message.type !== "ai") continue;
|
|
150
|
+
for (const toolCall of message.tool_calls ?? []) {
|
|
151
|
+
if (!isRecord(toolCall)) continue;
|
|
152
|
+
// Loaded ids must remain present even without a local owner because
|
|
153
|
+
// streamed event windows use has() to avoid attributing them later.
|
|
154
|
+
nextOwnership.set(
|
|
155
|
+
toolCall.id,
|
|
156
|
+
currentOwnership.has(toolCall.id)
|
|
157
|
+
? currentOwnership.get(toolCall.id)
|
|
158
|
+
: undefined,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
runConfigByToolCallIdRef.current = nextOwnership;
|
|
163
|
+
}, []);
|
|
164
|
+
|
|
165
|
+
const pruneMessageOwnership = useCallback((history: AdkMessage[]) => {
|
|
166
|
+
const toolCallIds = new Set<string>();
|
|
167
|
+
for (const message of history) {
|
|
168
|
+
if (message.type !== "ai") continue;
|
|
169
|
+
for (const toolCall of message.tool_calls ?? []) {
|
|
170
|
+
if (!isRecord(toolCall)) continue;
|
|
171
|
+
toolCallIds.add(toolCall.id);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
for (const id of runConfigByToolCallIdRef.current.keys()) {
|
|
175
|
+
if (!toolCallIds.has(id)) runConfigByToolCallIdRef.current.delete(id);
|
|
176
|
+
}
|
|
177
|
+
}, []);
|
|
178
|
+
|
|
179
|
+
const getToolRunConfig = useCallback((toolCallId: string) => {
|
|
180
|
+
return runConfigByToolCallIdRef.current.get(toolCallId);
|
|
181
|
+
}, []);
|
|
182
|
+
|
|
126
183
|
const {
|
|
127
184
|
messages,
|
|
128
185
|
stateDelta,
|
|
@@ -136,22 +193,37 @@ const useAdkRuntimeImpl = (options: UseAdkRuntimeOptions) => {
|
|
|
136
193
|
sendMessage,
|
|
137
194
|
cancel,
|
|
138
195
|
setMessages,
|
|
139
|
-
replaceMessages,
|
|
140
|
-
applySnapshot,
|
|
141
|
-
} =
|
|
196
|
+
replaceMessages: replaceAdkMessages,
|
|
197
|
+
applySnapshot: applyAdkSnapshot,
|
|
198
|
+
} = useAdkMessagesInternal({
|
|
142
199
|
stream,
|
|
143
200
|
...(eventHandlers && { eventHandlers }),
|
|
201
|
+
onMessages: rememberMessageOwnership,
|
|
144
202
|
});
|
|
145
203
|
|
|
146
204
|
const loadRef = useRef(load);
|
|
147
205
|
useInsertionEffect(() => {
|
|
148
206
|
loadRef.current = load;
|
|
149
207
|
}, [load]);
|
|
150
|
-
const loadController =
|
|
208
|
+
const [loadController] = useState(createAbortableThreadLoad);
|
|
151
209
|
const messagesRef = useRef(messages);
|
|
152
210
|
useInsertionEffect(() => {
|
|
153
211
|
messagesRef.current = messages;
|
|
154
212
|
}, [messages]);
|
|
213
|
+
const applySnapshot = useCallback(
|
|
214
|
+
(snapshot: AdkThreadSnapshot) => {
|
|
215
|
+
seedMessageOwnership(snapshot.messages);
|
|
216
|
+
applyAdkSnapshot(snapshot);
|
|
217
|
+
},
|
|
218
|
+
[applyAdkSnapshot, seedMessageOwnership],
|
|
219
|
+
);
|
|
220
|
+
const replaceMessages = useCallback(
|
|
221
|
+
(nextMessages: AdkMessage[]) => {
|
|
222
|
+
pruneMessageOwnership(nextMessages);
|
|
223
|
+
replaceAdkMessages(nextMessages);
|
|
224
|
+
},
|
|
225
|
+
[pruneMessageOwnership, replaceAdkMessages],
|
|
226
|
+
);
|
|
155
227
|
const [isLoadingThread, setIsLoadingThread] = useState(
|
|
156
228
|
() =>
|
|
157
229
|
load !== undefined && aui.threadListItem.getState().externalId != null,
|
|
@@ -175,10 +247,20 @@ const useAdkRuntimeImpl = (options: UseAdkRuntimeOptions) => {
|
|
|
175
247
|
msgs: AdkMessage[],
|
|
176
248
|
config: AdkSendMessageConfig,
|
|
177
249
|
) => {
|
|
250
|
+
const isToolContinuation =
|
|
251
|
+
msgs.length > 0 && msgs.every((msg) => msg.type === "tool");
|
|
252
|
+
const continuationConfig =
|
|
253
|
+
isToolContinuation && config.runConfig === undefined
|
|
254
|
+
? {
|
|
255
|
+
...config,
|
|
256
|
+
runConfig: getToolRunConfig(msgs[0]!.tool_call_id),
|
|
257
|
+
}
|
|
258
|
+
: config;
|
|
259
|
+
|
|
178
260
|
const generation = ++runGenerationRef.current;
|
|
179
261
|
try {
|
|
180
262
|
setIsRunning(true);
|
|
181
|
-
await sendMessage(msgs,
|
|
263
|
+
await sendMessage(msgs, continuationConfig);
|
|
182
264
|
} finally {
|
|
183
265
|
if (runGenerationRef.current === generation) setIsRunning(false);
|
|
184
266
|
}
|
|
@@ -325,7 +407,7 @@ const useAdkRuntimeImpl = (options: UseAdkRuntimeOptions) => {
|
|
|
325
407
|
[threadListItem, loadController, applySnapshot],
|
|
326
408
|
);
|
|
327
409
|
|
|
328
|
-
|
|
410
|
+
useReplaySafeEffect(() => {
|
|
329
411
|
runLoad();
|
|
330
412
|
return () => {
|
|
331
413
|
// Whatever is current, not this effect's own controller: a refetch swaps
|
|
@@ -333,7 +415,7 @@ const useAdkRuntimeImpl = (options: UseAdkRuntimeOptions) => {
|
|
|
333
415
|
loadController.abort();
|
|
334
416
|
setIsLoadingThread(false);
|
|
335
417
|
};
|
|
336
|
-
}, [
|
|
418
|
+
}, [threadListItem]);
|
|
337
419
|
|
|
338
420
|
const runtime = useExternalStoreRuntime({
|
|
339
421
|
...pickExternalStoreSharedOptions(options),
|
|
@@ -1,17 +1,33 @@
|
|
|
1
1
|
import { act, renderHook } from "@testing-library/react";
|
|
2
2
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
3
3
|
import type {
|
|
4
|
+
AddToolResultOptions,
|
|
5
|
+
AppendMessage,
|
|
4
6
|
RespondToToolApprovalOptions,
|
|
5
7
|
ThreadMessage,
|
|
6
8
|
ToolCallMessagePart,
|
|
7
9
|
} from "@assistant-ui/core";
|
|
8
|
-
import type { AdkMessage } from "./types";
|
|
10
|
+
import type { AdkMessage, AdkSendMessageConfig } from "./types";
|
|
9
11
|
|
|
10
|
-
const mocks = vi.hoisted(() =>
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
const mocks = vi.hoisted(() => {
|
|
13
|
+
const threadListItem = {
|
|
14
|
+
source: null as object | null,
|
|
15
|
+
externalId: undefined as string | undefined,
|
|
16
|
+
getState: () => ({
|
|
17
|
+
externalId: threadListItem.externalId,
|
|
18
|
+
}),
|
|
19
|
+
initialize: vi.fn(),
|
|
20
|
+
};
|
|
21
|
+
return {
|
|
22
|
+
adapters: [] as unknown[],
|
|
23
|
+
sendMessage: vi.fn().mockResolvedValue(undefined),
|
|
24
|
+
messages: [] as AdkMessage[],
|
|
25
|
+
messageRunConfig: undefined as unknown,
|
|
26
|
+
streamedMessages: undefined as AdkMessage[] | undefined,
|
|
27
|
+
applySnapshot: vi.fn(),
|
|
28
|
+
threadListItem,
|
|
29
|
+
};
|
|
30
|
+
});
|
|
15
31
|
|
|
16
32
|
vi.mock("@assistant-ui/core/react", async (importOriginal) => ({
|
|
17
33
|
...(await importOriginal<typeof import("@assistant-ui/core/react")>()),
|
|
@@ -27,49 +43,88 @@ vi.mock("@assistant-ui/core/react", async (importOriginal) => ({
|
|
|
27
43
|
vi.mock("@assistant-ui/store", async (importOriginal) => ({
|
|
28
44
|
...(await importOriginal<typeof import("@assistant-ui/store")>()),
|
|
29
45
|
useAui: () => ({
|
|
30
|
-
threadListItem:
|
|
31
|
-
source: null,
|
|
32
|
-
getState: () => ({ externalId: undefined }),
|
|
33
|
-
initialize: vi.fn(),
|
|
34
|
-
},
|
|
46
|
+
threadListItem: mocks.threadListItem,
|
|
35
47
|
}),
|
|
36
48
|
}));
|
|
37
49
|
|
|
38
|
-
vi.mock("./useAdkMessages", async (importOriginal) =>
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
50
|
+
vi.mock("./useAdkMessages", async (importOriginal) => {
|
|
51
|
+
const useMockAdkMessages = (options: {
|
|
52
|
+
onMessages?: (messages: AdkMessage[], runConfig: unknown) => void;
|
|
53
|
+
}) => {
|
|
54
|
+
options.onMessages?.(mocks.messages, mocks.messageRunConfig);
|
|
55
|
+
const sendMessage = async (
|
|
56
|
+
messages: AdkMessage[],
|
|
57
|
+
config: AdkSendMessageConfig,
|
|
58
|
+
) => {
|
|
59
|
+
await mocks.sendMessage(messages, config);
|
|
60
|
+
if (mocks.streamedMessages) {
|
|
61
|
+
options.onMessages?.(mocks.streamedMessages, config.runConfig);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
return {
|
|
65
|
+
messages: mocks.messages,
|
|
66
|
+
stateDelta: {},
|
|
67
|
+
agentInfo: {},
|
|
68
|
+
longRunningToolIds: [],
|
|
69
|
+
artifactDelta: {},
|
|
70
|
+
// Deliberately empty: the projection must come from the transcript, not
|
|
71
|
+
// from derived confirmation state that a mid-run accumulator swap drops.
|
|
72
|
+
toolConfirmations: [],
|
|
73
|
+
authRequests: [],
|
|
74
|
+
escalated: false,
|
|
75
|
+
messageMetadata: new Map(),
|
|
76
|
+
sendMessage,
|
|
77
|
+
cancel: vi.fn(),
|
|
78
|
+
setMessages: vi.fn(),
|
|
79
|
+
replaceMessages: vi.fn(),
|
|
80
|
+
applySnapshot: mocks.applySnapshot,
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
...(await importOriginal<typeof import("./useAdkMessages")>()),
|
|
86
|
+
useAdkMessages: useMockAdkMessages,
|
|
87
|
+
useAdkMessagesInternal: useMockAdkMessages,
|
|
88
|
+
};
|
|
89
|
+
});
|
|
59
90
|
|
|
60
91
|
import { AdkEventAccumulator } from "./AdkEventAccumulator";
|
|
61
92
|
import { useAdkRuntime } from "./useAdkRuntime";
|
|
62
93
|
|
|
63
|
-
type
|
|
94
|
+
type RuntimeAdapter = {
|
|
64
95
|
messages: readonly ThreadMessage[];
|
|
96
|
+
extras: {
|
|
97
|
+
send: (
|
|
98
|
+
messages: AdkMessage[],
|
|
99
|
+
config: AdkSendMessageConfig,
|
|
100
|
+
) => Promise<void>;
|
|
101
|
+
};
|
|
102
|
+
onNew?: (message: AppendMessage) => Promise<void> | void;
|
|
103
|
+
onEdit?: (message: AppendMessage) => Promise<void> | void;
|
|
104
|
+
onAddToolResult?: (options: AddToolResultOptions) => Promise<void> | void;
|
|
65
105
|
onRespondToToolApproval?: (
|
|
66
106
|
options: RespondToToolApprovalOptions,
|
|
67
107
|
) => Promise<void> | void;
|
|
108
|
+
onRefetchThread?: () => Promise<void> | void;
|
|
68
109
|
};
|
|
69
110
|
|
|
70
111
|
const CONFIRMATION_CALL = "adk-confirmation-1";
|
|
71
112
|
|
|
72
|
-
const latestAdapter = () => mocks.adapters.at(-1) as
|
|
113
|
+
const latestAdapter = () => mocks.adapters.at(-1) as RuntimeAdapter;
|
|
114
|
+
|
|
115
|
+
const makeUserMessage = (
|
|
116
|
+
text: string,
|
|
117
|
+
runConfig: AppendMessage["runConfig"],
|
|
118
|
+
): AppendMessage =>
|
|
119
|
+
({
|
|
120
|
+
role: "user",
|
|
121
|
+
content: [{ type: "text", text }],
|
|
122
|
+
attachments: [],
|
|
123
|
+
parentId: null,
|
|
124
|
+
sourceId: null,
|
|
125
|
+
runConfig,
|
|
126
|
+
metadata: { custom: {} },
|
|
127
|
+
}) as unknown as AppendMessage;
|
|
73
128
|
|
|
74
129
|
const makeConfirmationRequest = (): AdkMessage => ({
|
|
75
130
|
id: "ai-1",
|
|
@@ -96,20 +151,230 @@ const approvalPart = () =>
|
|
|
96
151
|
|
|
97
152
|
afterEach(() => {
|
|
98
153
|
mocks.adapters.length = 0;
|
|
154
|
+
mocks.sendMessage.mockClear();
|
|
99
155
|
mocks.messages = [];
|
|
156
|
+
mocks.messageRunConfig = undefined;
|
|
157
|
+
mocks.streamedMessages = undefined;
|
|
158
|
+
mocks.applySnapshot.mockReset();
|
|
159
|
+
mocks.threadListItem.source = null;
|
|
160
|
+
mocks.threadListItem.externalId = undefined;
|
|
100
161
|
});
|
|
101
162
|
|
|
102
163
|
describe("useAdkRuntime tool approvals", () => {
|
|
164
|
+
it("resumes a delayed tool result with its originating run config", async () => {
|
|
165
|
+
const runConfigA = { custom: { model: "model-a" } };
|
|
166
|
+
const runConfigB = { custom: { model: "model-b" } };
|
|
167
|
+
const { rerender } = renderHook(() => useAdkRuntime({ stream: vi.fn() }));
|
|
168
|
+
|
|
169
|
+
await act(async () => {
|
|
170
|
+
await latestAdapter().onNew!(makeUserMessage("first", runConfigA));
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
mocks.messages = [
|
|
174
|
+
{ id: "u-1", type: "human", content: "first" },
|
|
175
|
+
{
|
|
176
|
+
id: "ai-1",
|
|
177
|
+
type: "ai",
|
|
178
|
+
content: [],
|
|
179
|
+
tool_calls: [
|
|
180
|
+
{
|
|
181
|
+
id: "tool-a",
|
|
182
|
+
name: "lookup",
|
|
183
|
+
args: {},
|
|
184
|
+
},
|
|
185
|
+
],
|
|
186
|
+
},
|
|
187
|
+
];
|
|
188
|
+
mocks.messageRunConfig = runConfigA;
|
|
189
|
+
rerender();
|
|
190
|
+
|
|
191
|
+
await act(async () => {
|
|
192
|
+
await latestAdapter().onNew!(makeUserMessage("second", runConfigB));
|
|
193
|
+
await latestAdapter().onAddToolResult!({
|
|
194
|
+
messageId: "ai-1",
|
|
195
|
+
toolCallId: "tool-a",
|
|
196
|
+
toolName: "lookup",
|
|
197
|
+
result: { value: "done" },
|
|
198
|
+
isError: false,
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
expect(
|
|
203
|
+
mocks.sendMessage.mock.calls.map((call) => call[1].runConfig),
|
|
204
|
+
).toEqual([runConfigA, runConfigB, runConfigA]);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("preserves pending tool ownership across a thread refetch", async () => {
|
|
208
|
+
const runConfig = { custom: { model: "model-a" } };
|
|
209
|
+
const laterRunConfig = { custom: { model: "model-b" } };
|
|
210
|
+
const currentMessages: AdkMessage[] = [
|
|
211
|
+
{
|
|
212
|
+
id: "ai-1",
|
|
213
|
+
type: "ai",
|
|
214
|
+
content: [],
|
|
215
|
+
tool_calls: [{ id: "tool-a", name: "lookup", args: {} }],
|
|
216
|
+
},
|
|
217
|
+
];
|
|
218
|
+
const loadedMessages: AdkMessage[] = [
|
|
219
|
+
...currentMessages,
|
|
220
|
+
{
|
|
221
|
+
id: "ai-2",
|
|
222
|
+
type: "ai",
|
|
223
|
+
content: [],
|
|
224
|
+
tool_calls: [
|
|
225
|
+
null as never,
|
|
226
|
+
{ id: "tool-loaded", name: "lookup", args: {} },
|
|
227
|
+
],
|
|
228
|
+
},
|
|
229
|
+
];
|
|
230
|
+
const load = vi.fn(async () => ({ messages: loadedMessages }));
|
|
231
|
+
mocks.threadListItem.source = {};
|
|
232
|
+
|
|
233
|
+
const { rerender } = renderHook(() =>
|
|
234
|
+
useAdkRuntime({ stream: vi.fn(), load }),
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
await act(async () => {
|
|
238
|
+
await latestAdapter().onNew!(makeUserMessage("first", runConfig));
|
|
239
|
+
});
|
|
240
|
+
mocks.messages = currentMessages;
|
|
241
|
+
mocks.messageRunConfig = runConfig;
|
|
242
|
+
rerender();
|
|
243
|
+
|
|
244
|
+
mocks.threadListItem.externalId = "thread-a";
|
|
245
|
+
rerender();
|
|
246
|
+
await act(async () => {
|
|
247
|
+
await latestAdapter().onRefetchThread!();
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
await act(async () => {
|
|
251
|
+
mocks.streamedMessages = [loadedMessages.at(-1)!];
|
|
252
|
+
await latestAdapter().extras.send(
|
|
253
|
+
[{ id: "u-later", type: "human", content: "later" }],
|
|
254
|
+
{ runConfig: laterRunConfig },
|
|
255
|
+
);
|
|
256
|
+
mocks.streamedMessages = undefined;
|
|
257
|
+
await latestAdapter().onAddToolResult!({
|
|
258
|
+
messageId: "ai-1",
|
|
259
|
+
toolCallId: "tool-a",
|
|
260
|
+
toolName: "lookup",
|
|
261
|
+
result: { value: "done" },
|
|
262
|
+
isError: false,
|
|
263
|
+
});
|
|
264
|
+
await latestAdapter().onAddToolResult!({
|
|
265
|
+
messageId: "ai-2",
|
|
266
|
+
toolCallId: "tool-loaded",
|
|
267
|
+
toolName: "lookup",
|
|
268
|
+
result: { value: "loaded" },
|
|
269
|
+
isError: false,
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
expect(
|
|
274
|
+
mocks.sendMessage.mock.calls.slice(-2).map((call) => call[1]),
|
|
275
|
+
).toEqual([{ runConfig }, { runConfig: undefined }]);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it("tracks and prunes ownership around malformed tool-call entries", async () => {
|
|
279
|
+
const runConfig = { custom: { model: "model-a" } };
|
|
280
|
+
mocks.messages = [
|
|
281
|
+
{ id: "u-1", type: "human", content: "first" },
|
|
282
|
+
{
|
|
283
|
+
id: "ai-1",
|
|
284
|
+
type: "ai",
|
|
285
|
+
content: [],
|
|
286
|
+
tool_calls: [null as never, { id: "tool-a", name: "lookup", args: {} }],
|
|
287
|
+
},
|
|
288
|
+
];
|
|
289
|
+
mocks.messageRunConfig = runConfig;
|
|
290
|
+
renderHook(() =>
|
|
291
|
+
useAdkRuntime({
|
|
292
|
+
stream: vi.fn(),
|
|
293
|
+
getCheckpointId: vi.fn(async () => null),
|
|
294
|
+
}),
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
await act(async () => {
|
|
298
|
+
await latestAdapter().onEdit!({
|
|
299
|
+
...makeUserMessage("edited", runConfig),
|
|
300
|
+
parentId: "ai-1",
|
|
301
|
+
});
|
|
302
|
+
await latestAdapter().onAddToolResult!({
|
|
303
|
+
messageId: "ai-1",
|
|
304
|
+
toolCallId: "tool-a",
|
|
305
|
+
toolName: "lookup",
|
|
306
|
+
result: { value: "done" },
|
|
307
|
+
isError: false,
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
expect(mocks.sendMessage.mock.calls.at(-1)![1]).toEqual({ runConfig });
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it("attributes new tool calls to an explicitly configured continuation", async () => {
|
|
315
|
+
const runConfigA = { custom: { model: "model-a" } };
|
|
316
|
+
const runConfigB = { custom: { model: "model-b" } };
|
|
317
|
+
renderHook(() => useAdkRuntime({ stream: vi.fn() }));
|
|
318
|
+
|
|
319
|
+
await act(async () => {
|
|
320
|
+
await latestAdapter().onNew!(makeUserMessage("first", runConfigA));
|
|
321
|
+
mocks.streamedMessages = [
|
|
322
|
+
{
|
|
323
|
+
id: "ai-b",
|
|
324
|
+
type: "ai",
|
|
325
|
+
content: [],
|
|
326
|
+
tool_calls: [{ id: "tool-b", name: "lookup", args: {} }],
|
|
327
|
+
},
|
|
328
|
+
];
|
|
329
|
+
await latestAdapter().extras.send(
|
|
330
|
+
[
|
|
331
|
+
{
|
|
332
|
+
id: "tool-result-a",
|
|
333
|
+
type: "tool",
|
|
334
|
+
tool_call_id: "tool-a",
|
|
335
|
+
name: "lookup",
|
|
336
|
+
content: JSON.stringify({ value: "first" }),
|
|
337
|
+
},
|
|
338
|
+
],
|
|
339
|
+
{ runConfig: runConfigB },
|
|
340
|
+
);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
await act(async () => {
|
|
344
|
+
await latestAdapter().onAddToolResult!({
|
|
345
|
+
messageId: "ai-b",
|
|
346
|
+
toolCallId: "tool-b",
|
|
347
|
+
toolName: "lookup",
|
|
348
|
+
result: { value: "second" },
|
|
349
|
+
isError: false,
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
expect(mocks.sendMessage.mock.calls.at(-1)![1]).toEqual({
|
|
354
|
+
runConfig: runConfigB,
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
|
|
103
358
|
it("exposes, answers, and settles the default approval seam across a rerender", async () => {
|
|
359
|
+
const runConfig = { custom: { model: "model-a" } };
|
|
360
|
+
const nextRunConfig = { custom: { model: "model-b" } };
|
|
104
361
|
// Retained across the rerender: core caches converted messages by input
|
|
105
362
|
// object, so only a rebuilt converter can surface the settled decision.
|
|
106
363
|
const confirmationRequest = makeConfirmationRequest();
|
|
364
|
+
const { rerender } = renderHook(() => useAdkRuntime({ stream: vi.fn() }));
|
|
365
|
+
|
|
366
|
+
await act(async () => {
|
|
367
|
+
await latestAdapter().onNew!(
|
|
368
|
+
makeUserMessage("delete the file", runConfig),
|
|
369
|
+
);
|
|
370
|
+
});
|
|
371
|
+
|
|
107
372
|
mocks.messages = [
|
|
108
373
|
{ id: "u-1", type: "human", content: "delete the file" },
|
|
109
374
|
confirmationRequest,
|
|
110
375
|
];
|
|
111
|
-
|
|
112
|
-
|
|
376
|
+
mocks.messageRunConfig = runConfig;
|
|
377
|
+
rerender();
|
|
113
378
|
|
|
114
379
|
expect(latestAdapter().messages.at(-1)!.status).toMatchObject({
|
|
115
380
|
type: "requires-action",
|
|
@@ -121,6 +386,9 @@ describe("useAdkRuntime tool approvals", () => {
|
|
|
121
386
|
});
|
|
122
387
|
|
|
123
388
|
await act(async () => {
|
|
389
|
+
await latestAdapter().onNew!(
|
|
390
|
+
makeUserMessage("start another run", nextRunConfig),
|
|
391
|
+
);
|
|
124
392
|
await latestAdapter().onRespondToToolApproval!({
|
|
125
393
|
approvalId: CONFIRMATION_CALL,
|
|
126
394
|
approved: false,
|
|
@@ -135,6 +403,7 @@ describe("useAdkRuntime tool approvals", () => {
|
|
|
135
403
|
content: JSON.stringify({ confirmed: false }),
|
|
136
404
|
}),
|
|
137
405
|
]);
|
|
406
|
+
expect(mocks.sendMessage.mock.calls.at(-1)![1]).toEqual({ runConfig });
|
|
138
407
|
|
|
139
408
|
mocks.messages = [
|
|
140
409
|
...mocks.messages,
|