@assistant-ui/react-google-adk 0.0.25 → 0.0.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/AdkClient.d.ts.map +1 -1
- package/dist/AdkClient.js +13 -3
- package/dist/AdkClient.js.map +1 -1
- package/dist/AdkEventAccumulator.d.ts.map +1 -1
- package/dist/AdkEventAccumulator.js +11 -1
- package/dist/AdkEventAccumulator.js.map +1 -1
- package/dist/AdkSessionAdapter.d.ts.map +1 -1
- package/dist/AdkSessionAdapter.js +2 -1
- package/dist/AdkSessionAdapter.js.map +1 -1
- package/dist/adkToolApproval.d.ts +40 -0
- package/dist/adkToolApproval.d.ts.map +1 -0
- package/dist/adkToolApproval.js +166 -0
- package/dist/adkToolApproval.js.map +1 -0
- package/dist/convertAdkMessages.d.ts +3 -1
- package/dist/convertAdkMessages.d.ts.map +1 -1
- package/dist/convertAdkMessages.js +27 -16
- package/dist/convertAdkMessages.js.map +1 -1
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +2 -11
- package/dist/hooks.js.map +1 -1
- package/dist/parseAdkEvent.d.ts +6 -0
- package/dist/parseAdkEvent.d.ts.map +1 -0
- package/dist/parseAdkEvent.js +16 -0
- package/dist/parseAdkEvent.js.map +1 -0
- package/dist/server/parseAdkRequest.d.ts.map +1 -1
- package/dist/server/parseAdkRequest.js +79 -13
- package/dist/server/parseAdkRequest.js.map +1 -1
- package/dist/useAdkMessages.js +104 -64
- package/dist/useAdkMessages.js.map +1 -1
- package/dist/useAdkRuntime.d.ts.map +1 -1
- package/dist/useAdkRuntime.js +11 -3
- package/dist/useAdkRuntime.js.map +1 -1
- package/package.json +5 -5
- package/src/AdkClient.test.ts +137 -0
- package/src/AdkClient.ts +15 -3
- package/src/AdkEventAccumulator.test.ts +77 -0
- package/src/AdkEventAccumulator.ts +24 -1
- package/src/AdkSessionAdapter.test.ts +145 -1
- package/src/AdkSessionAdapter.ts +7 -2
- package/src/adkToolApproval.test.ts +345 -0
- package/src/adkToolApproval.ts +215 -0
- package/src/convertAdkMessages.ts +72 -43
- package/src/hooks.ts +4 -16
- package/src/parseAdkEvent.ts +40 -0
- package/src/server/parseAdkRequest.test.ts +141 -0
- package/src/server/parseAdkRequest.ts +145 -13
- package/src/useAdkMessages.test.ts +313 -0
- package/src/useAdkMessages.ts +62 -4
- package/src/useAdkRuntime.ts +29 -3
- package/src/useAdkRuntimeApproval.test.tsx +344 -0
|
@@ -15,13 +15,18 @@ vi.mock("@assistant-ui/store", async (importOriginal) => ({
|
|
|
15
15
|
|
|
16
16
|
import {
|
|
17
17
|
messageToEvent,
|
|
18
|
+
messagesToEvents,
|
|
18
19
|
useAdkMessages,
|
|
19
20
|
type UseAdkMessagesOptions,
|
|
20
21
|
} from "./useAdkMessages";
|
|
22
|
+
import { projectAdkToolApprovals } from "./adkToolApproval";
|
|
23
|
+
import { createAdkStream } from "./AdkClient";
|
|
24
|
+
import { AdkEventAccumulator } from "./AdkEventAccumulator";
|
|
21
25
|
import type { AdkEvent, AdkMessage, AdkStreamCallback } from "./types";
|
|
22
26
|
|
|
23
27
|
afterEach(() => {
|
|
24
28
|
vi.restoreAllMocks();
|
|
29
|
+
vi.unstubAllGlobals();
|
|
25
30
|
});
|
|
26
31
|
|
|
27
32
|
describe("ADK runtime callbacks", () => {
|
|
@@ -125,6 +130,314 @@ describe("ADK runtime callbacks", () => {
|
|
|
125
130
|
});
|
|
126
131
|
});
|
|
127
132
|
|
|
133
|
+
describe("ADK stream lifecycle", () => {
|
|
134
|
+
it("aborts the active stream when the hook unmounts", async () => {
|
|
135
|
+
let runSignal: AbortSignal | undefined;
|
|
136
|
+
let resolveStarted!: () => void;
|
|
137
|
+
const started = new Promise<void>((resolve) => {
|
|
138
|
+
resolveStarted = resolve;
|
|
139
|
+
});
|
|
140
|
+
const stream: AdkStreamCallback = async function* (
|
|
141
|
+
_messages,
|
|
142
|
+
{ abortSignal },
|
|
143
|
+
) {
|
|
144
|
+
runSignal = abortSignal;
|
|
145
|
+
resolveStarted();
|
|
146
|
+
await new Promise<void>((resolve) => {
|
|
147
|
+
abortSignal.addEventListener("abort", () => resolve(), { once: true });
|
|
148
|
+
});
|
|
149
|
+
};
|
|
150
|
+
const { result, unmount } = renderHook(() => useAdkMessages({ stream }));
|
|
151
|
+
|
|
152
|
+
let sendPromise!: Promise<void>;
|
|
153
|
+
act(() => {
|
|
154
|
+
sendPromise = result.current.sendMessage(
|
|
155
|
+
[{ id: "user", type: "human", content: "hello" }],
|
|
156
|
+
{},
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
await started;
|
|
160
|
+
|
|
161
|
+
unmount();
|
|
162
|
+
|
|
163
|
+
expect(runSignal?.aborted).toBe(true);
|
|
164
|
+
await expect(sendPromise).resolves.toBeUndefined();
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
describe("optimistic confirmation replies", () => {
|
|
169
|
+
const confirmationCall = (id: string): AdkMessage => ({
|
|
170
|
+
id: `ai-${id}`,
|
|
171
|
+
type: "ai",
|
|
172
|
+
content: [],
|
|
173
|
+
tool_calls: [
|
|
174
|
+
{
|
|
175
|
+
id,
|
|
176
|
+
name: "adk_request_confirmation",
|
|
177
|
+
args: {
|
|
178
|
+
originalFunctionCall: { id: `orig-${id}`, name: "delete_file" },
|
|
179
|
+
toolConfirmation: { hint: "Delete?" },
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
],
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* ADK parses the text inside its client wrapper without a `try`, so a wrapper
|
|
187
|
+
* holding text that is not JSON raises and abandons the whole event.
|
|
188
|
+
*/
|
|
189
|
+
const UNREADABLE_REPLY = JSON.stringify({ response: "not json" });
|
|
190
|
+
|
|
191
|
+
const confirmationReply = (
|
|
192
|
+
id: string,
|
|
193
|
+
toolCallId: string,
|
|
194
|
+
content: string,
|
|
195
|
+
): AdkMessage => ({
|
|
196
|
+
id,
|
|
197
|
+
type: "tool",
|
|
198
|
+
tool_call_id: toolCallId,
|
|
199
|
+
name: "adk_request_confirmation",
|
|
200
|
+
content,
|
|
201
|
+
status: "success",
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const emptyStream: AdkStreamCallback = async function* () {};
|
|
205
|
+
|
|
206
|
+
const renderWithGates = async () => {
|
|
207
|
+
const { result } = renderHook(() =>
|
|
208
|
+
useAdkMessages({ stream: emptyStream }),
|
|
209
|
+
);
|
|
210
|
+
await act(async () => {
|
|
211
|
+
result.current.setMessages([
|
|
212
|
+
confirmationCall("conf-a"),
|
|
213
|
+
confirmationCall("conf-b"),
|
|
214
|
+
]);
|
|
215
|
+
});
|
|
216
|
+
return result;
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
it("keeps both gates pending when one send carries an unreadable reply", async () => {
|
|
220
|
+
const result = await renderWithGates();
|
|
221
|
+
|
|
222
|
+
await act(async () => {
|
|
223
|
+
await result.current.sendMessage(
|
|
224
|
+
[
|
|
225
|
+
confirmationReply(
|
|
226
|
+
"reply-a",
|
|
227
|
+
"conf-a",
|
|
228
|
+
JSON.stringify({ confirmed: true }),
|
|
229
|
+
),
|
|
230
|
+
confirmationReply("reply-b", "conf-b", UNREADABLE_REPLY),
|
|
231
|
+
],
|
|
232
|
+
{},
|
|
233
|
+
);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
expect([
|
|
237
|
+
// The confirmation and the call it gates share one approval object.
|
|
238
|
+
...new Set(
|
|
239
|
+
projectAdkToolApprovals(result.current.messages).approvals.values(),
|
|
240
|
+
),
|
|
241
|
+
]).toEqual([{ id: "conf-a" }, { id: "conf-b" }]);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("keeps both gates pending when an ai message sits between the replies", async () => {
|
|
245
|
+
const result = await renderWithGates();
|
|
246
|
+
|
|
247
|
+
await act(async () => {
|
|
248
|
+
await result.current.sendMessage(
|
|
249
|
+
[
|
|
250
|
+
confirmationReply(
|
|
251
|
+
"reply-a",
|
|
252
|
+
"conf-a",
|
|
253
|
+
JSON.stringify({ confirmed: true }),
|
|
254
|
+
),
|
|
255
|
+
{ id: "ai-interleaved", type: "ai", content: "thinking" },
|
|
256
|
+
confirmationReply("reply-b", "conf-b", UNREADABLE_REPLY),
|
|
257
|
+
],
|
|
258
|
+
{},
|
|
259
|
+
);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
expect([
|
|
263
|
+
// The confirmation and the call it gates share one approval object.
|
|
264
|
+
...new Set(
|
|
265
|
+
projectAdkToolApprovals(result.current.messages).approvals.values(),
|
|
266
|
+
),
|
|
267
|
+
]).toEqual([{ id: "conf-a" }, { id: "conf-b" }]);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("settles the readable reply when the two replies were sent separately", async () => {
|
|
271
|
+
const result = await renderWithGates();
|
|
272
|
+
|
|
273
|
+
await act(async () => {
|
|
274
|
+
await result.current.sendMessage(
|
|
275
|
+
[
|
|
276
|
+
confirmationReply(
|
|
277
|
+
"reply-a",
|
|
278
|
+
"conf-a",
|
|
279
|
+
JSON.stringify({ confirmed: true }),
|
|
280
|
+
),
|
|
281
|
+
],
|
|
282
|
+
{},
|
|
283
|
+
);
|
|
284
|
+
});
|
|
285
|
+
await act(async () => {
|
|
286
|
+
await result.current.sendMessage(
|
|
287
|
+
[confirmationReply("reply-b", "conf-b", UNREADABLE_REPLY)],
|
|
288
|
+
{},
|
|
289
|
+
);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
expect([
|
|
293
|
+
// The confirmation and the call it gates share one approval object.
|
|
294
|
+
...new Set(
|
|
295
|
+
projectAdkToolApprovals(result.current.messages).approvals.values(),
|
|
296
|
+
),
|
|
297
|
+
]).toEqual([{ id: "conf-a", approved: true }, { id: "conf-b" }]);
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
describe("messagesToEvents", () => {
|
|
302
|
+
const toolReply = (id: string, toolCallId: string): AdkMessage => ({
|
|
303
|
+
id,
|
|
304
|
+
type: "tool",
|
|
305
|
+
tool_call_id: toolCallId,
|
|
306
|
+
name: "adk_request_confirmation",
|
|
307
|
+
content: JSON.stringify({ confirmed: true }),
|
|
308
|
+
status: "success",
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it("emits nothing for an empty batch", () => {
|
|
312
|
+
// `onReload` sends no messages. The transport still puts an empty user
|
|
313
|
+
// content on the wire, but projecting one here would append an empty user
|
|
314
|
+
// bubble above every regenerated turn.
|
|
315
|
+
expect(messagesToEvents([])).toEqual([]);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it("merges the human/tool run across an interleaved ai message", () => {
|
|
319
|
+
const events = messagesToEvents([
|
|
320
|
+
toolReply("reply-a", "conf-a"),
|
|
321
|
+
{ id: "ai-interleaved", type: "ai", content: "thinking" },
|
|
322
|
+
toolReply("reply-b", "conf-b"),
|
|
323
|
+
]);
|
|
324
|
+
|
|
325
|
+
expect(events).toHaveLength(2);
|
|
326
|
+
expect(events[0]?.id).toBe("reply-a");
|
|
327
|
+
expect(
|
|
328
|
+
events[0]?.content?.parts?.map((p) => p.functionResponse?.id),
|
|
329
|
+
).toEqual(["conf-a", "conf-b"]);
|
|
330
|
+
expect(events[1]?.id).toBe("ai-interleaved");
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it("mirrors the transport's empty user content for an ai-only batch", async () => {
|
|
334
|
+
const aiOnly: AdkMessage[] = [
|
|
335
|
+
{ id: "ai-1", type: "ai", content: "one" },
|
|
336
|
+
{ id: "ai-2", type: "ai", content: "two" },
|
|
337
|
+
];
|
|
338
|
+
|
|
339
|
+
let sentBody = "";
|
|
340
|
+
vi.stubGlobal("fetch", async (_url: string, init: RequestInit) => {
|
|
341
|
+
sentBody = init.body as string;
|
|
342
|
+
return new Response(
|
|
343
|
+
new ReadableStream<Uint8Array>({
|
|
344
|
+
start: (controller) => controller.close(),
|
|
345
|
+
}),
|
|
346
|
+
{ status: 200, headers: { "Content-Type": "text/event-stream" } },
|
|
347
|
+
);
|
|
348
|
+
});
|
|
349
|
+
const stream = createAdkStream({
|
|
350
|
+
api: "http://localhost:8000",
|
|
351
|
+
appName: "app",
|
|
352
|
+
userId: "user-1",
|
|
353
|
+
});
|
|
354
|
+
for await (const _ of await stream(aiOnly, {
|
|
355
|
+
abortSignal: new AbortController().signal,
|
|
356
|
+
initialize: async () => ({ remoteId: "r1", externalId: "s1" }),
|
|
357
|
+
})) {
|
|
358
|
+
/* drain */
|
|
359
|
+
}
|
|
360
|
+
const sent = JSON.parse(sentBody);
|
|
361
|
+
|
|
362
|
+
const events = messagesToEvents(aiOnly);
|
|
363
|
+
const userEvent = events.find((e) => e.author === "user");
|
|
364
|
+
expect(sent.newMessage.parts).toEqual([{ text: "" }]);
|
|
365
|
+
expect(userEvent?.content?.parts).toEqual(sent.newMessage.parts);
|
|
366
|
+
expect(events.map((e) => e.id)).toEqual(["ai-1", "ai-2", userEvent?.id]);
|
|
367
|
+
|
|
368
|
+
const accumulator = new AdkEventAccumulator([]);
|
|
369
|
+
let messages: AdkMessage[] = [];
|
|
370
|
+
for (const event of events) messages = accumulator.processEvent(event);
|
|
371
|
+
expect(messages.filter((m) => m.type === "human")).toEqual([
|
|
372
|
+
{ id: userEvent?.id, type: "human", content: "" },
|
|
373
|
+
]);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it("emits the merged event at the position of the run it replaces", () => {
|
|
377
|
+
const events = messagesToEvents([
|
|
378
|
+
{ id: "ai-first", type: "ai", content: "thinking" },
|
|
379
|
+
{ id: "human-a", type: "human", content: "a" },
|
|
380
|
+
]);
|
|
381
|
+
|
|
382
|
+
expect(events.map((e) => e.id)).toEqual(["ai-first", "human-a"]);
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
describe("optimistic multi-message sends", () => {
|
|
387
|
+
const emptyStream: AdkStreamCallback = async function* () {};
|
|
388
|
+
|
|
389
|
+
it("does not duplicate later messages of a staged multi-human send", async () => {
|
|
390
|
+
const staged: AdkMessage[] = [
|
|
391
|
+
{ id: "human-a", type: "human", content: "a" },
|
|
392
|
+
{ id: "human-b", type: "human", content: "b" },
|
|
393
|
+
];
|
|
394
|
+
const { result } = renderHook(() =>
|
|
395
|
+
useAdkMessages({ stream: emptyStream }),
|
|
396
|
+
);
|
|
397
|
+
await act(async () => {
|
|
398
|
+
result.current.setMessages(staged);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
await act(async () => {
|
|
402
|
+
await result.current.sendMessage(staged, {});
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
expect(result.current.messages).toEqual([
|
|
406
|
+
{
|
|
407
|
+
id: "human-a",
|
|
408
|
+
type: "human",
|
|
409
|
+
content: [
|
|
410
|
+
{ type: "text", text: "a" },
|
|
411
|
+
{ type: "text", text: "b" },
|
|
412
|
+
],
|
|
413
|
+
},
|
|
414
|
+
]);
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it("keeps messages that are not part of the send", async () => {
|
|
418
|
+
const { result } = renderHook(() =>
|
|
419
|
+
useAdkMessages({ stream: emptyStream }),
|
|
420
|
+
);
|
|
421
|
+
await act(async () => {
|
|
422
|
+
result.current.setMessages([
|
|
423
|
+
{ id: "earlier", type: "human", content: "earlier" },
|
|
424
|
+
]);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
await act(async () => {
|
|
428
|
+
await result.current.sendMessage(
|
|
429
|
+
[{ id: "human-a", type: "human", content: "a" }],
|
|
430
|
+
{},
|
|
431
|
+
);
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
expect(result.current.messages.map((m) => m.id)).toEqual([
|
|
435
|
+
"earlier",
|
|
436
|
+
"human-a",
|
|
437
|
+
]);
|
|
438
|
+
});
|
|
439
|
+
});
|
|
440
|
+
|
|
128
441
|
describe("messageToEvent (contentToParts)", () => {
|
|
129
442
|
it("serializes a file content part as inlineData", () => {
|
|
130
443
|
const msg: AdkMessage = {
|
package/src/useAdkMessages.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useCallback, useRef, useMemo } from "react";
|
|
1
|
+
import { useState, useCallback, useEffect, useRef, useMemo } from "react";
|
|
2
2
|
import { v4 as uuidv4 } from "uuid";
|
|
3
3
|
import { useAui } from "@assistant-ui/store";
|
|
4
4
|
import { AdkEventAccumulator } from "./AdkEventAccumulator";
|
|
@@ -136,9 +136,16 @@ export const useAdkMessages = ({
|
|
|
136
136
|
m.id ? m : { ...m, id: uuidv4() },
|
|
137
137
|
) as AdkMessage[];
|
|
138
138
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
139
|
+
// A staged message is already in the thread under its own id, and the
|
|
140
|
+
// merged event below re-emits the whole batch under the first one. Seeding
|
|
141
|
+
// with the originals would leave every later staged id beside the merged
|
|
142
|
+
// copy of itself.
|
|
143
|
+
const resentIds = new Set(newMessagesWithId.map((m) => m.id));
|
|
144
|
+
const accumulator = new AdkEventAccumulator(
|
|
145
|
+
messagesRef.current.filter((m) => !resentIds.has(m.id)),
|
|
146
|
+
);
|
|
147
|
+
for (const event of messagesToEvents(newMessagesWithId)) {
|
|
148
|
+
accumulator.processEvent(event);
|
|
142
149
|
}
|
|
143
150
|
setMessagesImmediate(accumulator.getMessages());
|
|
144
151
|
|
|
@@ -238,6 +245,8 @@ export const useAdkMessages = ({
|
|
|
238
245
|
}
|
|
239
246
|
}, []);
|
|
240
247
|
|
|
248
|
+
useEffect(() => cancel, [cancel]);
|
|
249
|
+
|
|
241
250
|
return {
|
|
242
251
|
messages,
|
|
243
252
|
stateDelta,
|
|
@@ -256,6 +265,55 @@ export const useAdkMessages = ({
|
|
|
256
265
|
};
|
|
257
266
|
};
|
|
258
267
|
|
|
268
|
+
/**
|
|
269
|
+
* Transport sends every human and tool message of one `send` call as a single
|
|
270
|
+
* ADK `Content`, and ADK parses that event's function responses before running
|
|
271
|
+
* any tool, so the batch runs whole or not at all. The optimistic projection
|
|
272
|
+
* has to sit on the same boundary, so a run of those messages becomes one
|
|
273
|
+
* synthetic event whose parts come from the same per-message conversion.
|
|
274
|
+
*
|
|
275
|
+
* The transport drops `ai` messages from that `Content`, so one interleaved
|
|
276
|
+
* between two replies does not split the batch on the wire and must not split
|
|
277
|
+
* it here either. It still becomes its own event, placed after the merged one,
|
|
278
|
+
* so the optimistic projection keeps the assistant turn.
|
|
279
|
+
*
|
|
280
|
+
* @internal — exported for unit tests.
|
|
281
|
+
*/
|
|
282
|
+
export const messagesToEvents = (messages: AdkMessage[]): AdkEvent[] => {
|
|
283
|
+
// A reload sends no messages at all, and the empty user content the transport
|
|
284
|
+
// puts on the wire for it is not part of the optimistic view: projecting one
|
|
285
|
+
// would put an empty user bubble above every regenerated turn.
|
|
286
|
+
if (messages.length === 0) return [];
|
|
287
|
+
|
|
288
|
+
const events: AdkEvent[] = [];
|
|
289
|
+
const run: AdkMessage[] = [];
|
|
290
|
+
let runIndex = 0;
|
|
291
|
+
|
|
292
|
+
for (const msg of messages) {
|
|
293
|
+
if (msg.type === "ai") {
|
|
294
|
+
events.push(messageToEvent(msg));
|
|
295
|
+
} else {
|
|
296
|
+
if (run.length === 0) runIndex = events.length;
|
|
297
|
+
run.push(msg);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const parts = run.flatMap((m) => messageToEvent(m).content?.parts ?? []);
|
|
302
|
+
const human = run.find((m) => m.type === "human");
|
|
303
|
+
|
|
304
|
+
// A batch that contributes no part still reaches the wire: the transport
|
|
305
|
+
// sends an empty user `Content`, which a reload replays as an empty human
|
|
306
|
+
// message. Emitting it here keeps the optimistic view equal to that replay.
|
|
307
|
+
if (parts.length === 0) parts.push({ text: "" });
|
|
308
|
+
|
|
309
|
+
const event: AdkEvent = { id: (human ?? run[0])?.id ?? uuidv4() };
|
|
310
|
+
if (human || run.length === 0) event.author = "user";
|
|
311
|
+
event.content = { role: "user", parts };
|
|
312
|
+
events.splice(run.length > 0 ? runIndex : events.length, 0, event);
|
|
313
|
+
|
|
314
|
+
return events;
|
|
315
|
+
};
|
|
316
|
+
|
|
259
317
|
/** @internal — exported for unit tests. */
|
|
260
318
|
export const messageToEvent = (msg: AdkMessage): AdkEvent => {
|
|
261
319
|
if (msg.type === "human") {
|
package/src/useAdkRuntime.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback, useEffect, useRef, useState } from "react";
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
2
|
import {
|
|
3
3
|
getExternalStoreMessages,
|
|
4
4
|
pickExternalStoreSharedOptions,
|
|
@@ -33,7 +33,14 @@ import type {
|
|
|
33
33
|
OnAdkAgentTransferCallback,
|
|
34
34
|
} from "./types";
|
|
35
35
|
import { useAdkMessages } from "./useAdkMessages";
|
|
36
|
-
import {
|
|
36
|
+
import {
|
|
37
|
+
convertAdkMessage,
|
|
38
|
+
createAdkMessageConverter,
|
|
39
|
+
} from "./convertAdkMessages";
|
|
40
|
+
import {
|
|
41
|
+
projectAdkToolApprovals,
|
|
42
|
+
toAdkToolConfirmationReply,
|
|
43
|
+
} from "./adkToolApproval";
|
|
37
44
|
import { adkExtras } from "./adkExtras";
|
|
38
45
|
import { v4 as uuidv4 } from "uuid";
|
|
39
46
|
|
|
@@ -285,8 +292,21 @@ const useAdkRuntimeImpl = (options: UseAdkRuntimeOptions) => {
|
|
|
285
292
|
}
|
|
286
293
|
};
|
|
287
294
|
|
|
295
|
+
const { approvals: toolApprovals, key: toolApprovalsKey } =
|
|
296
|
+
projectAdkToolApprovals(messages);
|
|
297
|
+
const toolApprovalsRef = useRef(toolApprovals);
|
|
298
|
+
toolApprovalsRef.current = toolApprovals;
|
|
299
|
+
|
|
300
|
+
const messageConverter = useMemo(
|
|
301
|
+
() =>
|
|
302
|
+
toolApprovalsKey === ""
|
|
303
|
+
? convertAdkMessage
|
|
304
|
+
: createAdkMessageConverter(toolApprovalsRef.current),
|
|
305
|
+
[toolApprovalsKey],
|
|
306
|
+
);
|
|
307
|
+
|
|
288
308
|
const threadMessages = useExternalMessageConverter({
|
|
289
|
-
callback:
|
|
309
|
+
callback: messageConverter,
|
|
290
310
|
messages,
|
|
291
311
|
isRunning: effectiveIsRunning,
|
|
292
312
|
});
|
|
@@ -556,6 +576,12 @@ const useAdkRuntimeImpl = (options: UseAdkRuntimeOptions) => {
|
|
|
556
576
|
{},
|
|
557
577
|
);
|
|
558
578
|
},
|
|
579
|
+
onRespondToToolApproval: async (options) => {
|
|
580
|
+
await handleSendMessage(
|
|
581
|
+
[toAdkToolConfirmationReply(options, toolApprovalsRef.current)],
|
|
582
|
+
{},
|
|
583
|
+
);
|
|
584
|
+
},
|
|
559
585
|
onCancel: unstable_allowCancellation
|
|
560
586
|
? async () => {
|
|
561
587
|
cancel();
|