@assistant-ui/react-google-adk 0.0.34 → 0.0.36
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/README.md +12 -2
- package/dist/AdkClient.d.ts +4 -0
- package/dist/AdkClient.d.ts.map +1 -1
- package/dist/AdkClient.js +12 -7
- package/dist/AdkClient.js.map +1 -1
- package/dist/AdkSessionAdapter.d.ts.map +1 -1
- package/dist/AdkSessionAdapter.js +7 -7
- package/dist/AdkSessionAdapter.js.map +1 -1
- package/dist/AdkThreadController.d.ts +15 -0
- package/dist/AdkThreadController.d.ts.map +1 -0
- package/dist/AdkThreadController.js +35 -0
- package/dist/AdkThreadController.js.map +1 -0
- package/dist/adkThreadState.d.ts +54 -0
- package/dist/adkThreadState.d.ts.map +1 -0
- package/dist/adkThreadState.js +93 -0
- package/dist/adkThreadState.js.map +1 -0
- package/dist/convertToAdkMessages.js +1 -1
- package/dist/convertToAdkMessages.js.map +1 -1
- package/dist/sdkIdentity.js +1 -1
- package/dist/server/createAdkApiRoute.d.ts +37 -6
- package/dist/server/createAdkApiRoute.d.ts.map +1 -1
- package/dist/server/createAdkApiRoute.js +55 -5
- package/dist/server/createAdkApiRoute.js.map +1 -1
- package/dist/server/parseAdkRequest.d.ts +4 -1
- package/dist/server/parseAdkRequest.d.ts.map +1 -1
- package/dist/server/parseAdkRequest.js +5 -1
- package/dist/server/parseAdkRequest.js.map +1 -1
- package/dist/useAdkMessages.d.ts +29 -4
- package/dist/useAdkMessages.d.ts.map +1 -1
- package/dist/useAdkMessages.js +62 -76
- package/dist/useAdkMessages.js.map +1 -1
- package/dist/useAdkRuntime.d.ts +7 -1
- package/dist/useAdkRuntime.d.ts.map +1 -1
- package/dist/useAdkRuntime.js +182 -54
- package/dist/useAdkRuntime.js.map +1 -1
- package/package.json +6 -5
- package/src/AdkClient.test.ts +78 -2
- package/src/AdkClient.ts +24 -6
- package/src/AdkSessionAdapter.ts +1 -1
- package/src/AdkThreadController.test.ts +90 -0
- package/src/AdkThreadController.ts +45 -0
- package/src/adkThreadState.test.ts +207 -0
- package/src/adkThreadState.ts +124 -0
- package/src/convertToAdkMessages.test.ts +19 -0
- package/src/convertToAdkMessages.ts +1 -1
- package/src/hooks.test.tsx +1 -0
- package/src/server/createAdkApiRoute.controls.test.ts +66 -0
- package/src/server/createAdkApiRoute.test.ts +282 -0
- package/src/server/createAdkApiRoute.ts +119 -11
- package/src/server/parseAdkRequest.test.ts +11 -3
- package/src/server/parseAdkRequest.ts +7 -1
- package/src/useAdkMessages.test.ts +43 -0
- package/src/useAdkMessages.ts +89 -96
- package/src/useAdkRuntime.cancellation.test.tsx +4 -3
- package/src/useAdkRuntime.cloud-options.test.tsx +59 -0
- package/src/useAdkRuntime.refetch.test.tsx +548 -4
- package/src/useAdkRuntime.replacement.test.tsx +718 -1
- package/src/useAdkRuntime.ts +253 -75
- package/src/useAdkRuntimeApproval.test.tsx +390 -35
- package/dist/raceWithAbortSignal.d.ts +0 -2
- package/dist/raceWithAbortSignal.d.ts.map +0 -1
- package/dist/raceWithAbortSignal.js +0 -45
- package/dist/raceWithAbortSignal.js.map +0 -1
- package/src/raceWithAbortSignal.test.ts +0 -73
- package/src/raceWithAbortSignal.ts +0 -48
|
@@ -1,17 +1,48 @@
|
|
|
1
|
+
/** @vitest-environment jsdom */
|
|
1
2
|
import { act, renderHook } from "@testing-library/react";
|
|
2
3
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
3
4
|
import type {
|
|
5
|
+
AddToolResultOptions,
|
|
6
|
+
AppendMessage,
|
|
4
7
|
RespondToToolApprovalOptions,
|
|
5
8
|
ThreadMessage,
|
|
6
9
|
ToolCallMessagePart,
|
|
7
10
|
} from "@assistant-ui/core";
|
|
8
|
-
import type {
|
|
11
|
+
import type {
|
|
12
|
+
AdkMessage,
|
|
13
|
+
AdkSendMessageConfig,
|
|
14
|
+
AdkThreadSnapshot,
|
|
15
|
+
} from "./types";
|
|
9
16
|
|
|
10
|
-
const mocks = vi.hoisted(() =>
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
17
|
+
const mocks = vi.hoisted(() => {
|
|
18
|
+
const threadListItem = {
|
|
19
|
+
source: null as object | null,
|
|
20
|
+
id: "thread-a",
|
|
21
|
+
externalId: undefined as string | undefined,
|
|
22
|
+
getState: () => ({
|
|
23
|
+
id: threadListItem.id,
|
|
24
|
+
externalId: threadListItem.externalId,
|
|
25
|
+
}),
|
|
26
|
+
initialize: vi.fn(),
|
|
27
|
+
};
|
|
28
|
+
return {
|
|
29
|
+
adapters: [] as unknown[],
|
|
30
|
+
sendMessage: vi.fn().mockResolvedValue(undefined),
|
|
31
|
+
messages: [] as AdkMessage[],
|
|
32
|
+
messageRunConfig: undefined as unknown,
|
|
33
|
+
streamedMessages: undefined as AdkMessage[] | undefined,
|
|
34
|
+
applySnapshot: vi.fn(),
|
|
35
|
+
threadListItem,
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const mockController = {
|
|
40
|
+
subscribe: () => () => {},
|
|
41
|
+
getStagedMessageCount: () => 0,
|
|
42
|
+
getState: () => ({ messages: mocks.messages }),
|
|
43
|
+
dispatch: vi.fn(),
|
|
44
|
+
getStagedRun: () => null,
|
|
45
|
+
};
|
|
15
46
|
|
|
16
47
|
vi.mock("@assistant-ui/core/react", async (importOriginal) => ({
|
|
17
48
|
...(await importOriginal<typeof import("@assistant-ui/core/react")>()),
|
|
@@ -27,49 +58,96 @@ vi.mock("@assistant-ui/core/react", async (importOriginal) => ({
|
|
|
27
58
|
vi.mock("@assistant-ui/store", async (importOriginal) => ({
|
|
28
59
|
...(await importOriginal<typeof import("@assistant-ui/store")>()),
|
|
29
60
|
useAui: () => ({
|
|
30
|
-
threadListItem:
|
|
31
|
-
|
|
32
|
-
getState: () => ({
|
|
33
|
-
initialize: vi.fn(),
|
|
61
|
+
threadListItem: mocks.threadListItem,
|
|
62
|
+
threads: {
|
|
63
|
+
getState: () => ({ mainThreadId: mocks.threadListItem.id }),
|
|
34
64
|
},
|
|
35
65
|
}),
|
|
36
66
|
}));
|
|
37
67
|
|
|
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
|
-
}
|
|
68
|
+
vi.mock("./useAdkMessages", async (importOriginal) => {
|
|
69
|
+
const useMockAdkMessages = (options: {
|
|
70
|
+
onMessages?: (messages: AdkMessage[], runConfig: unknown) => void;
|
|
71
|
+
}) => {
|
|
72
|
+
options.onMessages?.(mocks.messages, mocks.messageRunConfig);
|
|
73
|
+
const sendMessage = async (
|
|
74
|
+
messages: AdkMessage[],
|
|
75
|
+
config: AdkSendMessageConfig,
|
|
76
|
+
) => {
|
|
77
|
+
await mocks.sendMessage(messages, config);
|
|
78
|
+
if (mocks.streamedMessages) {
|
|
79
|
+
options.onMessages?.(mocks.streamedMessages, config.runConfig);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
return {
|
|
83
|
+
controller: mockController,
|
|
84
|
+
messages: mocks.messages,
|
|
85
|
+
stateDelta: {},
|
|
86
|
+
agentInfo: {},
|
|
87
|
+
longRunningToolIds: [],
|
|
88
|
+
artifactDelta: {},
|
|
89
|
+
// Deliberately empty: the projection must come from the transcript, not
|
|
90
|
+
// from derived confirmation state that a mid-run accumulator swap drops.
|
|
91
|
+
toolConfirmations: [],
|
|
92
|
+
authRequests: [],
|
|
93
|
+
escalated: false,
|
|
94
|
+
messageMetadata: new Map(),
|
|
95
|
+
sendMessage,
|
|
96
|
+
cancel: vi.fn(),
|
|
97
|
+
setMessages: vi.fn(),
|
|
98
|
+
replaceMessages: vi.fn(),
|
|
99
|
+
applySnapshot: mocks.applySnapshot,
|
|
100
|
+
};
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
...(await importOriginal<typeof import("./useAdkMessages")>()),
|
|
105
|
+
useAdkMessages: useMockAdkMessages,
|
|
106
|
+
useAdkMessagesInternal: useMockAdkMessages,
|
|
107
|
+
};
|
|
108
|
+
});
|
|
59
109
|
|
|
60
110
|
import { AdkEventAccumulator } from "./AdkEventAccumulator";
|
|
61
111
|
import { useAdkRuntime } from "./useAdkRuntime";
|
|
62
112
|
|
|
63
|
-
type
|
|
113
|
+
type RuntimeAdapter = {
|
|
64
114
|
messages: readonly ThreadMessage[];
|
|
115
|
+
extras: {
|
|
116
|
+
send: (
|
|
117
|
+
messages: AdkMessage[],
|
|
118
|
+
config: AdkSendMessageConfig,
|
|
119
|
+
) => Promise<void>;
|
|
120
|
+
};
|
|
121
|
+
onNew?: (message: AppendMessage) => Promise<void> | void;
|
|
122
|
+
onEdit?: (message: AppendMessage) => Promise<void> | void;
|
|
123
|
+
onAddToolResult?: (options: AddToolResultOptions) => Promise<void> | void;
|
|
65
124
|
onRespondToToolApproval?: (
|
|
66
125
|
options: RespondToToolApprovalOptions,
|
|
67
126
|
) => Promise<void> | void;
|
|
127
|
+
onReload?: (
|
|
128
|
+
parentId: string | null,
|
|
129
|
+
config: { runConfig?: AppendMessage["runConfig"] },
|
|
130
|
+
) => Promise<void> | void;
|
|
131
|
+
onRefetchThread?: () => Promise<void> | void;
|
|
68
132
|
};
|
|
69
133
|
|
|
70
134
|
const CONFIRMATION_CALL = "adk-confirmation-1";
|
|
71
135
|
|
|
72
|
-
const latestAdapter = () => mocks.adapters.at(-1) as
|
|
136
|
+
const latestAdapter = () => mocks.adapters.at(-1) as RuntimeAdapter;
|
|
137
|
+
|
|
138
|
+
const makeUserMessage = (
|
|
139
|
+
text: string,
|
|
140
|
+
runConfig: AppendMessage["runConfig"],
|
|
141
|
+
): AppendMessage =>
|
|
142
|
+
({
|
|
143
|
+
role: "user",
|
|
144
|
+
content: [{ type: "text", text }],
|
|
145
|
+
attachments: [],
|
|
146
|
+
parentId: null,
|
|
147
|
+
sourceId: null,
|
|
148
|
+
runConfig,
|
|
149
|
+
metadata: { custom: {} },
|
|
150
|
+
}) as unknown as AppendMessage;
|
|
73
151
|
|
|
74
152
|
const makeConfirmationRequest = (): AdkMessage => ({
|
|
75
153
|
id: "ai-1",
|
|
@@ -96,20 +174,293 @@ const approvalPart = () =>
|
|
|
96
174
|
|
|
97
175
|
afterEach(() => {
|
|
98
176
|
mocks.adapters.length = 0;
|
|
177
|
+
mocks.sendMessage.mockClear();
|
|
99
178
|
mocks.messages = [];
|
|
179
|
+
mocks.messageRunConfig = undefined;
|
|
180
|
+
mocks.streamedMessages = undefined;
|
|
181
|
+
mocks.applySnapshot.mockReset();
|
|
182
|
+
mocks.threadListItem.source = null;
|
|
183
|
+
mocks.threadListItem.externalId = undefined;
|
|
100
184
|
});
|
|
101
185
|
|
|
102
186
|
describe("useAdkRuntime tool approvals", () => {
|
|
187
|
+
it.each([
|
|
188
|
+
"reload",
|
|
189
|
+
"tool result",
|
|
190
|
+
"approval response",
|
|
191
|
+
"extras send",
|
|
192
|
+
] as const)("waits for the initial load before %s", async (route) => {
|
|
193
|
+
let resolveLoad!: (snapshot: AdkThreadSnapshot) => void;
|
|
194
|
+
const pendingLoad = new Promise<AdkThreadSnapshot>((resolve) => {
|
|
195
|
+
resolveLoad = resolve;
|
|
196
|
+
});
|
|
197
|
+
const load = vi.fn(() => pendingLoad);
|
|
198
|
+
mocks.threadListItem.source = {};
|
|
199
|
+
mocks.threadListItem.externalId = "thread-a";
|
|
200
|
+
renderHook(() =>
|
|
201
|
+
useAdkRuntime({
|
|
202
|
+
stream: vi.fn(),
|
|
203
|
+
load,
|
|
204
|
+
getCheckpointId: vi.fn(async () => null),
|
|
205
|
+
}),
|
|
206
|
+
);
|
|
207
|
+
expect(load).toHaveBeenCalledOnce();
|
|
208
|
+
|
|
209
|
+
let action: Promise<void>;
|
|
210
|
+
switch (route) {
|
|
211
|
+
case "reload":
|
|
212
|
+
action = Promise.resolve(latestAdapter().onReload!(null, {}));
|
|
213
|
+
break;
|
|
214
|
+
case "tool result":
|
|
215
|
+
action = Promise.resolve(
|
|
216
|
+
latestAdapter().onAddToolResult!({
|
|
217
|
+
messageId: "ai-1",
|
|
218
|
+
toolCallId: "tool-a",
|
|
219
|
+
toolName: "lookup",
|
|
220
|
+
result: { value: "done" },
|
|
221
|
+
isError: false,
|
|
222
|
+
}),
|
|
223
|
+
);
|
|
224
|
+
break;
|
|
225
|
+
case "approval response":
|
|
226
|
+
action = Promise.resolve(
|
|
227
|
+
latestAdapter().onRespondToToolApproval!({
|
|
228
|
+
approvalId: CONFIRMATION_CALL,
|
|
229
|
+
approved: true,
|
|
230
|
+
}),
|
|
231
|
+
);
|
|
232
|
+
break;
|
|
233
|
+
case "extras send":
|
|
234
|
+
action = latestAdapter().extras.send(
|
|
235
|
+
[{ id: "new-user", type: "human", content: "new question" }],
|
|
236
|
+
{},
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
void action.catch(() => {});
|
|
240
|
+
await Promise.resolve();
|
|
241
|
+
expect(mocks.sendMessage).not.toHaveBeenCalled();
|
|
242
|
+
|
|
243
|
+
await act(async () => {
|
|
244
|
+
resolveLoad({ messages: [makeConfirmationRequest()] });
|
|
245
|
+
await action;
|
|
246
|
+
});
|
|
247
|
+
expect(mocks.sendMessage).toHaveBeenCalledOnce();
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("resumes a delayed tool result with its originating run config", async () => {
|
|
251
|
+
const runConfigA = { custom: { model: "model-a" } };
|
|
252
|
+
const runConfigB = { custom: { model: "model-b" } };
|
|
253
|
+
const { rerender } = renderHook(() => useAdkRuntime({ stream: vi.fn() }));
|
|
254
|
+
|
|
255
|
+
await act(async () => {
|
|
256
|
+
await latestAdapter().onNew!(makeUserMessage("first", runConfigA));
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
mocks.messages = [
|
|
260
|
+
{ id: "u-1", type: "human", content: "first" },
|
|
261
|
+
{
|
|
262
|
+
id: "ai-1",
|
|
263
|
+
type: "ai",
|
|
264
|
+
content: [],
|
|
265
|
+
tool_calls: [
|
|
266
|
+
{
|
|
267
|
+
id: "tool-a",
|
|
268
|
+
name: "lookup",
|
|
269
|
+
args: {},
|
|
270
|
+
},
|
|
271
|
+
],
|
|
272
|
+
},
|
|
273
|
+
];
|
|
274
|
+
mocks.messageRunConfig = runConfigA;
|
|
275
|
+
rerender();
|
|
276
|
+
|
|
277
|
+
await act(async () => {
|
|
278
|
+
await latestAdapter().onNew!(makeUserMessage("second", runConfigB));
|
|
279
|
+
await latestAdapter().onAddToolResult!({
|
|
280
|
+
messageId: "ai-1",
|
|
281
|
+
toolCallId: "tool-a",
|
|
282
|
+
toolName: "lookup",
|
|
283
|
+
result: { value: "done" },
|
|
284
|
+
isError: false,
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
expect(
|
|
289
|
+
mocks.sendMessage.mock.calls.map((call) => call[1].runConfig),
|
|
290
|
+
).toEqual([runConfigA, runConfigB, runConfigA]);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it("preserves pending tool ownership across a thread refetch", async () => {
|
|
294
|
+
const runConfig = { custom: { model: "model-a" } };
|
|
295
|
+
const laterRunConfig = { custom: { model: "model-b" } };
|
|
296
|
+
const currentMessages: AdkMessage[] = [
|
|
297
|
+
{
|
|
298
|
+
id: "ai-1",
|
|
299
|
+
type: "ai",
|
|
300
|
+
content: [],
|
|
301
|
+
tool_calls: [{ id: "tool-a", name: "lookup", args: {} }],
|
|
302
|
+
},
|
|
303
|
+
];
|
|
304
|
+
const loadedMessages: AdkMessage[] = [
|
|
305
|
+
...currentMessages,
|
|
306
|
+
{
|
|
307
|
+
id: "ai-2",
|
|
308
|
+
type: "ai",
|
|
309
|
+
content: [],
|
|
310
|
+
tool_calls: [
|
|
311
|
+
null as never,
|
|
312
|
+
{ id: "tool-loaded", name: "lookup", args: {} },
|
|
313
|
+
],
|
|
314
|
+
},
|
|
315
|
+
];
|
|
316
|
+
const load = vi.fn(async () => ({ messages: loadedMessages }));
|
|
317
|
+
mocks.threadListItem.source = {};
|
|
318
|
+
|
|
319
|
+
const { rerender } = renderHook(() =>
|
|
320
|
+
useAdkRuntime({ stream: vi.fn(), load }),
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
await act(async () => {
|
|
324
|
+
await latestAdapter().onNew!(makeUserMessage("first", runConfig));
|
|
325
|
+
});
|
|
326
|
+
mocks.messages = currentMessages;
|
|
327
|
+
mocks.messageRunConfig = runConfig;
|
|
328
|
+
rerender();
|
|
329
|
+
|
|
330
|
+
mocks.threadListItem.externalId = "thread-a";
|
|
331
|
+
rerender();
|
|
332
|
+
await act(async () => {
|
|
333
|
+
await latestAdapter().onRefetchThread!();
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
await act(async () => {
|
|
337
|
+
mocks.streamedMessages = [loadedMessages.at(-1)!];
|
|
338
|
+
await latestAdapter().extras.send(
|
|
339
|
+
[{ id: "u-later", type: "human", content: "later" }],
|
|
340
|
+
{ runConfig: laterRunConfig },
|
|
341
|
+
);
|
|
342
|
+
mocks.streamedMessages = undefined;
|
|
343
|
+
await latestAdapter().onAddToolResult!({
|
|
344
|
+
messageId: "ai-1",
|
|
345
|
+
toolCallId: "tool-a",
|
|
346
|
+
toolName: "lookup",
|
|
347
|
+
result: { value: "done" },
|
|
348
|
+
isError: false,
|
|
349
|
+
});
|
|
350
|
+
await latestAdapter().onAddToolResult!({
|
|
351
|
+
messageId: "ai-2",
|
|
352
|
+
toolCallId: "tool-loaded",
|
|
353
|
+
toolName: "lookup",
|
|
354
|
+
result: { value: "loaded" },
|
|
355
|
+
isError: false,
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
expect(
|
|
360
|
+
mocks.sendMessage.mock.calls.slice(-2).map((call) => call[1]),
|
|
361
|
+
).toEqual([{ runConfig }, { runConfig: undefined }]);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it("tracks and prunes ownership around malformed tool-call entries", async () => {
|
|
365
|
+
const runConfig = { custom: { model: "model-a" } };
|
|
366
|
+
mocks.messages = [
|
|
367
|
+
{ id: "u-1", type: "human", content: "first" },
|
|
368
|
+
{
|
|
369
|
+
id: "ai-1",
|
|
370
|
+
type: "ai",
|
|
371
|
+
content: [],
|
|
372
|
+
tool_calls: [null as never, { id: "tool-a", name: "lookup", args: {} }],
|
|
373
|
+
},
|
|
374
|
+
];
|
|
375
|
+
mocks.messageRunConfig = runConfig;
|
|
376
|
+
renderHook(() =>
|
|
377
|
+
useAdkRuntime({
|
|
378
|
+
stream: vi.fn(),
|
|
379
|
+
getCheckpointId: vi.fn(async () => null),
|
|
380
|
+
}),
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
await act(async () => {
|
|
384
|
+
await latestAdapter().onEdit!({
|
|
385
|
+
...makeUserMessage("edited", runConfig),
|
|
386
|
+
parentId: "ai-1",
|
|
387
|
+
});
|
|
388
|
+
await latestAdapter().onAddToolResult!({
|
|
389
|
+
messageId: "ai-1",
|
|
390
|
+
toolCallId: "tool-a",
|
|
391
|
+
toolName: "lookup",
|
|
392
|
+
result: { value: "done" },
|
|
393
|
+
isError: false,
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
expect(mocks.sendMessage.mock.calls.at(-1)![1]).toEqual({ runConfig });
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it("attributes new tool calls to an explicitly configured continuation", async () => {
|
|
401
|
+
const runConfigA = { custom: { model: "model-a" } };
|
|
402
|
+
const runConfigB = { custom: { model: "model-b" } };
|
|
403
|
+
renderHook(() => useAdkRuntime({ stream: vi.fn() }));
|
|
404
|
+
|
|
405
|
+
await act(async () => {
|
|
406
|
+
await latestAdapter().onNew!(makeUserMessage("first", runConfigA));
|
|
407
|
+
mocks.streamedMessages = [
|
|
408
|
+
{
|
|
409
|
+
id: "ai-b",
|
|
410
|
+
type: "ai",
|
|
411
|
+
content: [],
|
|
412
|
+
tool_calls: [{ id: "tool-b", name: "lookup", args: {} }],
|
|
413
|
+
},
|
|
414
|
+
];
|
|
415
|
+
await latestAdapter().extras.send(
|
|
416
|
+
[
|
|
417
|
+
{
|
|
418
|
+
id: "tool-result-a",
|
|
419
|
+
type: "tool",
|
|
420
|
+
tool_call_id: "tool-a",
|
|
421
|
+
name: "lookup",
|
|
422
|
+
content: JSON.stringify({ value: "first" }),
|
|
423
|
+
},
|
|
424
|
+
],
|
|
425
|
+
{ runConfig: runConfigB },
|
|
426
|
+
);
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
await act(async () => {
|
|
430
|
+
await latestAdapter().onAddToolResult!({
|
|
431
|
+
messageId: "ai-b",
|
|
432
|
+
toolCallId: "tool-b",
|
|
433
|
+
toolName: "lookup",
|
|
434
|
+
result: { value: "second" },
|
|
435
|
+
isError: false,
|
|
436
|
+
});
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
expect(mocks.sendMessage.mock.calls.at(-1)![1]).toEqual({
|
|
440
|
+
runConfig: runConfigB,
|
|
441
|
+
});
|
|
442
|
+
});
|
|
443
|
+
|
|
103
444
|
it("exposes, answers, and settles the default approval seam across a rerender", async () => {
|
|
445
|
+
const runConfig = { custom: { model: "model-a" } };
|
|
446
|
+
const nextRunConfig = { custom: { model: "model-b" } };
|
|
104
447
|
// Retained across the rerender: core caches converted messages by input
|
|
105
448
|
// object, so only a rebuilt converter can surface the settled decision.
|
|
106
449
|
const confirmationRequest = makeConfirmationRequest();
|
|
450
|
+
const { rerender } = renderHook(() => useAdkRuntime({ stream: vi.fn() }));
|
|
451
|
+
|
|
452
|
+
await act(async () => {
|
|
453
|
+
await latestAdapter().onNew!(
|
|
454
|
+
makeUserMessage("delete the file", runConfig),
|
|
455
|
+
);
|
|
456
|
+
});
|
|
457
|
+
|
|
107
458
|
mocks.messages = [
|
|
108
459
|
{ id: "u-1", type: "human", content: "delete the file" },
|
|
109
460
|
confirmationRequest,
|
|
110
461
|
];
|
|
111
|
-
|
|
112
|
-
|
|
462
|
+
mocks.messageRunConfig = runConfig;
|
|
463
|
+
rerender();
|
|
113
464
|
|
|
114
465
|
expect(latestAdapter().messages.at(-1)!.status).toMatchObject({
|
|
115
466
|
type: "requires-action",
|
|
@@ -121,6 +472,9 @@ describe("useAdkRuntime tool approvals", () => {
|
|
|
121
472
|
});
|
|
122
473
|
|
|
123
474
|
await act(async () => {
|
|
475
|
+
await latestAdapter().onNew!(
|
|
476
|
+
makeUserMessage("start another run", nextRunConfig),
|
|
477
|
+
);
|
|
124
478
|
await latestAdapter().onRespondToToolApproval!({
|
|
125
479
|
approvalId: CONFIRMATION_CALL,
|
|
126
480
|
approved: false,
|
|
@@ -135,6 +489,7 @@ describe("useAdkRuntime tool approvals", () => {
|
|
|
135
489
|
content: JSON.stringify({ confirmed: false }),
|
|
136
490
|
}),
|
|
137
491
|
]);
|
|
492
|
+
expect(mocks.sendMessage.mock.calls.at(-1)![1]).toEqual({ runConfig });
|
|
138
493
|
|
|
139
494
|
mocks.messages = [
|
|
140
495
|
...mocks.messages,
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"raceWithAbortSignal.d.ts","sourceRoot":"","sources":["../src/raceWithAbortSignal.ts"],"names":[],"mappings":"AAOA,eAAO,MAAM,mBAAmB,GAAI,CAAC,EACnC,QAAQ,WAAW,GAAG,SAAS,EAC/B,WAAW,MAAM,CAAC,GAAG,WAAW,CAAC,CAAC,CAAC,KAClC,OAAO,CAAC,CAAC,CAqCX,CAAC"}
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
//#region src/raceWithAbortSignal.ts
|
|
2
|
-
const getAbortReason = (signal) => {
|
|
3
|
-
if (signal.reason !== void 0) return signal.reason;
|
|
4
|
-
const error = /* @__PURE__ */ new Error("The operation was aborted");
|
|
5
|
-
error.name = "AbortError";
|
|
6
|
-
return error;
|
|
7
|
-
};
|
|
8
|
-
const raceWithAbortSignal = (signal, operation) => {
|
|
9
|
-
if (!signal) try {
|
|
10
|
-
return Promise.resolve(operation());
|
|
11
|
-
} catch (error) {
|
|
12
|
-
return Promise.reject(error);
|
|
13
|
-
}
|
|
14
|
-
if (signal.aborted) return Promise.reject(getAbortReason(signal));
|
|
15
|
-
return new Promise((resolve, reject) => {
|
|
16
|
-
let settled = false;
|
|
17
|
-
const cleanup = () => signal.removeEventListener("abort", handleAbort);
|
|
18
|
-
const resolveOnce = (value) => {
|
|
19
|
-
if (settled) return;
|
|
20
|
-
settled = true;
|
|
21
|
-
cleanup();
|
|
22
|
-
resolve(value);
|
|
23
|
-
};
|
|
24
|
-
const rejectOnce = (error) => {
|
|
25
|
-
if (settled) return;
|
|
26
|
-
settled = true;
|
|
27
|
-
cleanup();
|
|
28
|
-
reject(error);
|
|
29
|
-
};
|
|
30
|
-
const handleAbort = () => rejectOnce(getAbortReason(signal));
|
|
31
|
-
signal.addEventListener("abort", handleAbort, { once: true });
|
|
32
|
-
let result;
|
|
33
|
-
try {
|
|
34
|
-
result = operation();
|
|
35
|
-
} catch (error) {
|
|
36
|
-
rejectOnce(error);
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
Promise.resolve(result).then(resolveOnce, rejectOnce);
|
|
40
|
-
});
|
|
41
|
-
};
|
|
42
|
-
//#endregion
|
|
43
|
-
export { raceWithAbortSignal };
|
|
44
|
-
|
|
45
|
-
//# sourceMappingURL=raceWithAbortSignal.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"raceWithAbortSignal.js","names":[],"sources":["../src/raceWithAbortSignal.ts"],"sourcesContent":["const getAbortReason = (signal: AbortSignal): unknown => {\n if (signal.reason !== undefined) return signal.reason;\n const error = new Error(\"The operation was aborted\");\n error.name = \"AbortError\";\n return error;\n};\n\nexport const raceWithAbortSignal = <T>(\n signal: AbortSignal | undefined,\n operation: () => T | PromiseLike<T>,\n): Promise<T> => {\n if (!signal) {\n try {\n return Promise.resolve(operation());\n } catch (error) {\n return Promise.reject(error);\n }\n }\n if (signal.aborted) return Promise.reject(getAbortReason(signal));\n\n return new Promise<T>((resolve, reject) => {\n let settled = false;\n const cleanup = () => signal.removeEventListener(\"abort\", handleAbort);\n const resolveOnce = (value: T) => {\n if (settled) return;\n settled = true;\n cleanup();\n resolve(value);\n };\n const rejectOnce = (error: unknown) => {\n if (settled) return;\n settled = true;\n cleanup();\n reject(error);\n };\n const handleAbort = () => rejectOnce(getAbortReason(signal));\n\n signal.addEventListener(\"abort\", handleAbort, { once: true });\n let result: T | PromiseLike<T>;\n try {\n result = operation();\n } catch (error) {\n rejectOnce(error);\n return;\n }\n Promise.resolve(result).then(resolveOnce, rejectOnce);\n });\n};\n"],"mappings":";AAAA,MAAM,kBAAkB,WAAiC;CACvD,IAAI,OAAO,WAAW,KAAA,GAAW,OAAO,OAAO;CAC/C,MAAM,wBAAQ,IAAI,MAAM,2BAA2B;CACnD,MAAM,OAAO;CACb,OAAO;AACT;AAEA,MAAa,uBACX,QACA,cACe;CACf,IAAI,CAAC,QACH,IAAI;EACF,OAAO,QAAQ,QAAQ,UAAU,CAAC;CACpC,SAAS,OAAO;EACd,OAAO,QAAQ,OAAO,KAAK;CAC7B;CAEF,IAAI,OAAO,SAAS,OAAO,QAAQ,OAAO,eAAe,MAAM,CAAC;CAEhE,OAAO,IAAI,SAAY,SAAS,WAAW;EACzC,IAAI,UAAU;EACd,MAAM,gBAAgB,OAAO,oBAAoB,SAAS,WAAW;EACrE,MAAM,eAAe,UAAa;GAChC,IAAI,SAAS;GACb,UAAU;GACV,QAAQ;GACR,QAAQ,KAAK;EACf;EACA,MAAM,cAAc,UAAmB;GACrC,IAAI,SAAS;GACb,UAAU;GACV,QAAQ;GACR,OAAO,KAAK;EACd;EACA,MAAM,oBAAoB,WAAW,eAAe,MAAM,CAAC;EAE3D,OAAO,iBAAiB,SAAS,aAAa,EAAE,MAAM,KAAK,CAAC;EAC5D,IAAI;EACJ,IAAI;GACF,SAAS,UAAU;EACrB,SAAS,OAAO;GACd,WAAW,KAAK;GAChB;EACF;EACA,QAAQ,QAAQ,MAAM,CAAC,CAAC,KAAK,aAAa,UAAU;CACtD,CAAC;AACH"}
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, vi } from "vitest";
|
|
2
|
-
import { raceWithAbortSignal } from "./raceWithAbortSignal";
|
|
3
|
-
|
|
4
|
-
describe("raceWithAbortSignal", () => {
|
|
5
|
-
it("invokes the operation synchronously without a signal", async () => {
|
|
6
|
-
const order: string[] = [];
|
|
7
|
-
|
|
8
|
-
const result = raceWithAbortSignal(undefined, () => {
|
|
9
|
-
order.push("operation");
|
|
10
|
-
return "done";
|
|
11
|
-
});
|
|
12
|
-
order.push("after");
|
|
13
|
-
|
|
14
|
-
expect(order).toEqual(["operation", "after"]);
|
|
15
|
-
await expect(result).resolves.toBe("done");
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it("converts a synchronous operation error to a rejection", async () => {
|
|
19
|
-
const error = new Error("failed");
|
|
20
|
-
|
|
21
|
-
const result = raceWithAbortSignal(undefined, () => {
|
|
22
|
-
throw error;
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
await expect(result).rejects.toBe(error);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it("rejects a pending operation with the abort reason", async () => {
|
|
29
|
-
const controller = new AbortController();
|
|
30
|
-
const reason = new Error("cancelled");
|
|
31
|
-
let resolveOperation!: (value: string) => void;
|
|
32
|
-
const operation = new Promise<string>((resolve) => {
|
|
33
|
-
resolveOperation = resolve;
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
const result = raceWithAbortSignal(controller.signal, () => operation);
|
|
37
|
-
controller.abort(reason);
|
|
38
|
-
|
|
39
|
-
await expect(result).rejects.toBe(reason);
|
|
40
|
-
resolveOperation("late result");
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it("rejects before invoking an operation for an already aborted signal", async () => {
|
|
44
|
-
const controller = new AbortController();
|
|
45
|
-
const reason = new Error("already cancelled");
|
|
46
|
-
const operation = vi.fn(() => "done");
|
|
47
|
-
controller.abort(reason);
|
|
48
|
-
|
|
49
|
-
const result = raceWithAbortSignal(controller.signal, operation);
|
|
50
|
-
|
|
51
|
-
await expect(result).rejects.toBe(reason);
|
|
52
|
-
expect(operation).not.toHaveBeenCalled();
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it("removes the abort listener after the operation settles", async () => {
|
|
56
|
-
const controller = new AbortController();
|
|
57
|
-
const removeEventListener = vi.spyOn(
|
|
58
|
-
controller.signal,
|
|
59
|
-
"removeEventListener",
|
|
60
|
-
);
|
|
61
|
-
const result = raceWithAbortSignal(controller.signal, () => "done");
|
|
62
|
-
|
|
63
|
-
await expect(result).resolves.toBe("done");
|
|
64
|
-
expect(removeEventListener).toHaveBeenCalledOnce();
|
|
65
|
-
expect(removeEventListener).toHaveBeenCalledWith(
|
|
66
|
-
"abort",
|
|
67
|
-
expect.any(Function),
|
|
68
|
-
);
|
|
69
|
-
|
|
70
|
-
controller.abort(new Error("late abort"));
|
|
71
|
-
await expect(result).resolves.toBe("done");
|
|
72
|
-
});
|
|
73
|
-
});
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
const getAbortReason = (signal: AbortSignal): unknown => {
|
|
2
|
-
if (signal.reason !== undefined) return signal.reason;
|
|
3
|
-
const error = new Error("The operation was aborted");
|
|
4
|
-
error.name = "AbortError";
|
|
5
|
-
return error;
|
|
6
|
-
};
|
|
7
|
-
|
|
8
|
-
export const raceWithAbortSignal = <T>(
|
|
9
|
-
signal: AbortSignal | undefined,
|
|
10
|
-
operation: () => T | PromiseLike<T>,
|
|
11
|
-
): Promise<T> => {
|
|
12
|
-
if (!signal) {
|
|
13
|
-
try {
|
|
14
|
-
return Promise.resolve(operation());
|
|
15
|
-
} catch (error) {
|
|
16
|
-
return Promise.reject(error);
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
if (signal.aborted) return Promise.reject(getAbortReason(signal));
|
|
20
|
-
|
|
21
|
-
return new Promise<T>((resolve, reject) => {
|
|
22
|
-
let settled = false;
|
|
23
|
-
const cleanup = () => signal.removeEventListener("abort", handleAbort);
|
|
24
|
-
const resolveOnce = (value: T) => {
|
|
25
|
-
if (settled) return;
|
|
26
|
-
settled = true;
|
|
27
|
-
cleanup();
|
|
28
|
-
resolve(value);
|
|
29
|
-
};
|
|
30
|
-
const rejectOnce = (error: unknown) => {
|
|
31
|
-
if (settled) return;
|
|
32
|
-
settled = true;
|
|
33
|
-
cleanup();
|
|
34
|
-
reject(error);
|
|
35
|
-
};
|
|
36
|
-
const handleAbort = () => rejectOnce(getAbortReason(signal));
|
|
37
|
-
|
|
38
|
-
signal.addEventListener("abort", handleAbort, { once: true });
|
|
39
|
-
let result: T | PromiseLike<T>;
|
|
40
|
-
try {
|
|
41
|
-
result = operation();
|
|
42
|
-
} catch (error) {
|
|
43
|
-
rejectOnce(error);
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
Promise.resolve(result).then(resolveOnce, rejectOnce);
|
|
47
|
-
});
|
|
48
|
-
};
|