@assistant-ui/react-google-adk 0.0.29 → 0.0.31
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 +2 -3
- package/dist/AdkClient.d.ts.map +1 -1
- package/dist/AdkClient.js +2 -1
- package/dist/AdkClient.js.map +1 -1
- package/dist/AdkEventAccumulator.d.ts +4 -6
- package/dist/AdkEventAccumulator.d.ts.map +1 -1
- package/dist/AdkEventAccumulator.js +80 -95
- package/dist/AdkEventAccumulator.js.map +1 -1
- package/dist/AdkSessionAdapter.d.ts +7 -4
- package/dist/AdkSessionAdapter.d.ts.map +1 -1
- package/dist/AdkSessionAdapter.js +7 -4
- package/dist/AdkSessionAdapter.js.map +1 -1
- package/dist/adkExtras.d.ts +1 -2
- package/dist/adkExtras.d.ts.map +1 -1
- package/dist/adkToolApproval.d.ts +6 -7
- package/dist/adkToolApproval.d.ts.map +1 -1
- package/dist/adkToolApproval.js +1 -3
- package/dist/adkToolApproval.js.map +1 -1
- package/dist/contentToParts.d.ts +1 -2
- package/dist/contentToParts.d.ts.map +1 -1
- package/dist/convertAdkMessages.d.ts +2 -3
- package/dist/convertAdkMessages.d.ts.map +1 -1
- package/dist/convertToAdkMessages.d.ts +15 -7
- package/dist/convertToAdkMessages.d.ts.map +1 -1
- package/dist/convertToAdkMessages.js +20 -6
- package/dist/convertToAdkMessages.js.map +1 -1
- package/dist/hooks.d.ts +15 -16
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +1 -1
- package/dist/normalizeAdkPart.d.ts +4 -0
- package/dist/normalizeAdkPart.d.ts.map +1 -0
- package/dist/normalizeAdkPart.js +31 -0
- package/dist/normalizeAdkPart.js.map +1 -0
- package/dist/parseAdkEvent.d.ts +1 -2
- package/dist/parseAdkEvent.d.ts.map +1 -1
- package/dist/sdkIdentity.d.ts +5 -0
- package/dist/sdkIdentity.d.ts.map +1 -0
- package/dist/sdkIdentity.js +9 -0
- package/dist/sdkIdentity.js.map +1 -0
- package/dist/server/adkEventStream.d.ts +2 -3
- package/dist/server/adkEventStream.d.ts.map +1 -1
- package/dist/server/createAdkApiRoute.d.ts +2 -3
- package/dist/server/createAdkApiRoute.d.ts.map +1 -1
- package/dist/server/parseAdkRequest.d.ts +2 -3
- package/dist/server/parseAdkRequest.d.ts.map +1 -1
- package/dist/server/parseAdkRequest.js +2 -1
- package/dist/server/parseAdkRequest.js.map +1 -1
- package/dist/structuredEvents.d.ts +1 -2
- package/dist/structuredEvents.d.ts.map +1 -1
- package/dist/structuredEvents.js +1 -1
- package/dist/structuredEvents.js.map +1 -1
- package/dist/toAdkFunctionResponse.d.ts +5 -0
- package/dist/toAdkFunctionResponse.d.ts.map +1 -0
- package/dist/toAdkFunctionResponse.js +11 -0
- package/dist/toAdkFunctionResponse.js.map +1 -0
- package/dist/trimTrailingSlashes.d.ts +1 -2
- package/dist/trimTrailingSlashes.d.ts.map +1 -1
- package/dist/types.d.ts +22 -23
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/useAdkMessages.d.ts +2 -3
- package/dist/useAdkMessages.d.ts.map +1 -1
- package/dist/useAdkMessages.js +16 -5
- package/dist/useAdkMessages.js.map +1 -1
- package/dist/useAdkRuntime.d.ts +2 -3
- package/dist/useAdkRuntime.d.ts.map +1 -1
- package/dist/useAdkRuntime.js +3 -1
- package/dist/useAdkRuntime.js.map +1 -1
- package/package.json +10 -9
- package/src/AdkClient.test.ts +139 -2
- package/src/AdkClient.ts +2 -1
- package/src/AdkEventAccumulator.test.ts +588 -11
- package/src/AdkEventAccumulator.ts +85 -130
- package/src/AdkSessionAdapter.test.ts +355 -1
- package/src/AdkSessionAdapter.ts +24 -5
- package/src/adkToolApproval.test.ts +0 -4
- package/src/adkToolApproval.ts +1 -3
- package/src/convertToAdkMessages.test.ts +70 -0
- package/src/convertToAdkMessages.ts +19 -4
- package/src/normalizeAdkPart.ts +35 -0
- package/src/sdkIdentity.ts +9 -0
- package/src/server/parseAdkRequest.test.ts +63 -0
- package/src/server/parseAdkRequest.ts +2 -1
- package/src/structuredEvents.test.ts +69 -0
- package/src/structuredEvents.ts +4 -1
- package/src/toAdkFunctionResponse.test.ts +46 -0
- package/src/toAdkFunctionResponse.ts +18 -0
- package/src/types.ts +1 -1
- package/src/useAdkMessages.test.ts +249 -2
- package/src/useAdkMessages.ts +24 -6
- package/src/useAdkRuntime.replacement.test.tsx +11 -7
- package/src/useAdkRuntime.ts +2 -0
- package/src/useAdkRuntimeApproval.test.tsx +0 -1
|
@@ -239,6 +239,41 @@ describe("parseAdkRequest", () => {
|
|
|
239
239
|
});
|
|
240
240
|
|
|
241
241
|
describe("toAdkContent", () => {
|
|
242
|
+
it.each([
|
|
243
|
+
["permission denied", { error: "permission denied" }],
|
|
244
|
+
[
|
|
245
|
+
{ message: "permission denied" },
|
|
246
|
+
{ error: { message: "permission denied" } },
|
|
247
|
+
],
|
|
248
|
+
[["denied"], { error: ["denied"] }],
|
|
249
|
+
[null, { error: null }],
|
|
250
|
+
[false, { error: false }],
|
|
251
|
+
[0, { error: 0 }],
|
|
252
|
+
["", { error: "" }],
|
|
253
|
+
[
|
|
254
|
+
{ error: "denied", output: "partial" },
|
|
255
|
+
{ error: "denied", output: "partial" },
|
|
256
|
+
],
|
|
257
|
+
])(
|
|
258
|
+
"preserves explicit tool failure %j in the function response",
|
|
259
|
+
async (result, response) => {
|
|
260
|
+
const parsed = await parseAdkRequest(
|
|
261
|
+
makeRequest({
|
|
262
|
+
type: "tool-result",
|
|
263
|
+
toolCallId: "tc-1",
|
|
264
|
+
toolName: "search",
|
|
265
|
+
result,
|
|
266
|
+
isError: true,
|
|
267
|
+
}),
|
|
268
|
+
);
|
|
269
|
+
expect(toAdkContent(parsed).parts).toEqual([
|
|
270
|
+
{
|
|
271
|
+
functionResponse: { name: "search", id: "tc-1", response },
|
|
272
|
+
},
|
|
273
|
+
]);
|
|
274
|
+
},
|
|
275
|
+
);
|
|
276
|
+
|
|
242
277
|
it("converts a text message to user content with text part", () => {
|
|
243
278
|
const content = toAdkContent({
|
|
244
279
|
type: "message",
|
|
@@ -292,4 +327,32 @@ describe("toAdkContent", () => {
|
|
|
292
327
|
],
|
|
293
328
|
});
|
|
294
329
|
});
|
|
330
|
+
|
|
331
|
+
it.each([
|
|
332
|
+
[false, { result: false }],
|
|
333
|
+
[0, { result: 0 }],
|
|
334
|
+
[null, { result: null }],
|
|
335
|
+
["done", { result: "done" }],
|
|
336
|
+
[[1, 2], { results: [1, 2] }],
|
|
337
|
+
])(
|
|
338
|
+
"wraps scalar or array tool result %j in a function response object",
|
|
339
|
+
(result, response) => {
|
|
340
|
+
const content = toAdkContent({
|
|
341
|
+
type: "tool-result",
|
|
342
|
+
toolCallId: "tc-1",
|
|
343
|
+
toolName: "search",
|
|
344
|
+
result,
|
|
345
|
+
isError: false,
|
|
346
|
+
config: {},
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
expect(content.parts[0]).toEqual({
|
|
350
|
+
functionResponse: {
|
|
351
|
+
name: "search",
|
|
352
|
+
id: "tc-1",
|
|
353
|
+
response,
|
|
354
|
+
},
|
|
355
|
+
});
|
|
356
|
+
},
|
|
357
|
+
);
|
|
295
358
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isRecord } from "@assistant-ui/core/internal";
|
|
2
2
|
import type { AdkSendMessageConfig } from "../types";
|
|
3
|
+
import { toAdkFunctionResponse } from "../toAdkFunctionResponse";
|
|
3
4
|
|
|
4
5
|
type ParsedAdkRequest =
|
|
5
6
|
| {
|
|
@@ -240,7 +241,7 @@ export const toAdkContent = (
|
|
|
240
241
|
functionResponse: {
|
|
241
242
|
name: parsed.toolName,
|
|
242
243
|
id: parsed.toolCallId,
|
|
243
|
-
response: parsed.result,
|
|
244
|
+
response: toAdkFunctionResponse(parsed.result, parsed.isError),
|
|
244
245
|
},
|
|
245
246
|
},
|
|
246
247
|
],
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { toAdkStructuredEvents } from "./structuredEvents";
|
|
3
|
+
import type { AdkEvent } from "./types";
|
|
4
|
+
|
|
5
|
+
const makeEvent = (overrides: Partial<AdkEvent> = {}): AdkEvent => ({
|
|
6
|
+
id: "evt-1",
|
|
7
|
+
...overrides,
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
describe("toAdkStructuredEvents", () => {
|
|
11
|
+
it("converts a function call with args", () => {
|
|
12
|
+
expect(
|
|
13
|
+
toAdkStructuredEvents(
|
|
14
|
+
makeEvent({
|
|
15
|
+
author: "agent",
|
|
16
|
+
content: {
|
|
17
|
+
role: "model",
|
|
18
|
+
parts: [
|
|
19
|
+
{
|
|
20
|
+
functionCall: { name: "search", id: "tc-1", args: { q: "x" } },
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
},
|
|
24
|
+
}),
|
|
25
|
+
),
|
|
26
|
+
).toEqual([
|
|
27
|
+
{
|
|
28
|
+
type: "tool_call",
|
|
29
|
+
call: { name: "search", id: "tc-1", args: { q: "x" } },
|
|
30
|
+
},
|
|
31
|
+
]);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("defaults a function call without args to empty args", () => {
|
|
35
|
+
expect(
|
|
36
|
+
toAdkStructuredEvents(
|
|
37
|
+
makeEvent({
|
|
38
|
+
author: "agent",
|
|
39
|
+
content: {
|
|
40
|
+
role: "model",
|
|
41
|
+
parts: [{ functionCall: { name: "search", id: "tc-1" } }],
|
|
42
|
+
},
|
|
43
|
+
}),
|
|
44
|
+
),
|
|
45
|
+
).toEqual([
|
|
46
|
+
{ type: "tool_call", call: { name: "search", id: "tc-1", args: {} } },
|
|
47
|
+
]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("keeps the other parts of an event carrying a call without args", () => {
|
|
51
|
+
expect(
|
|
52
|
+
toAdkStructuredEvents(
|
|
53
|
+
makeEvent({
|
|
54
|
+
author: "agent",
|
|
55
|
+
content: {
|
|
56
|
+
role: "model",
|
|
57
|
+
parts: [
|
|
58
|
+
{ functionCall: { name: "search" } },
|
|
59
|
+
{ text: "still here" },
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
}),
|
|
63
|
+
),
|
|
64
|
+
).toEqual([
|
|
65
|
+
{ type: "tool_call", call: { name: "search", args: {} } },
|
|
66
|
+
{ type: "content", content: "still here" },
|
|
67
|
+
]);
|
|
68
|
+
});
|
|
69
|
+
});
|
package/src/structuredEvents.ts
CHANGED
|
@@ -28,7 +28,10 @@ export function toAdkStructuredEvents(event: AdkEvent): AdkStructuredEvent[] {
|
|
|
28
28
|
if (part.functionCall) {
|
|
29
29
|
const call: AdkStructuredEvent & { type: "tool_call" } = {
|
|
30
30
|
type: "tool_call",
|
|
31
|
-
call: {
|
|
31
|
+
call: {
|
|
32
|
+
name: part.functionCall.name,
|
|
33
|
+
args: part.functionCall.args ?? {},
|
|
34
|
+
},
|
|
32
35
|
};
|
|
33
36
|
if (part.functionCall.id != null) call.call.id = part.functionCall.id;
|
|
34
37
|
result.push(call);
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
isAdkFunctionError,
|
|
4
|
+
toAdkFunctionResponse,
|
|
5
|
+
} from "./toAdkFunctionResponse";
|
|
6
|
+
|
|
7
|
+
describe("ADK function response errors", () => {
|
|
8
|
+
it("retains an existing error envelope without mutating or nesting it", () => {
|
|
9
|
+
const response = Object.freeze({ error: "denied", output: "partial" });
|
|
10
|
+
expect(toAdkFunctionResponse(response, true)).toBe(response);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("preserves a failure with no details through JSON serialization", () => {
|
|
14
|
+
const response = JSON.parse(
|
|
15
|
+
JSON.stringify(toAdkFunctionResponse(undefined, true)),
|
|
16
|
+
);
|
|
17
|
+
expect(response).toEqual({ error: null });
|
|
18
|
+
expect(isAdkFunctionError(response)).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("does not classify an undefined error field as an on-wire error", () => {
|
|
22
|
+
expect(isAdkFunctionError({ error: undefined })).toBe(false);
|
|
23
|
+
expect(
|
|
24
|
+
isAdkFunctionError(JSON.parse(JSON.stringify({ error: undefined }))),
|
|
25
|
+
).toBe(false);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("does not classify inherited error fields as an on-wire error", () => {
|
|
29
|
+
const response = Object.create({ error: "inherited" });
|
|
30
|
+
response.output = "done";
|
|
31
|
+
expect(isAdkFunctionError(response)).toBe(false);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it.each([
|
|
35
|
+
["done", { result: "done" }],
|
|
36
|
+
[[1, 2], { results: [1, 2] }],
|
|
37
|
+
[
|
|
38
|
+
{ output: { error: "application data" } },
|
|
39
|
+
{ output: { error: "application data" } },
|
|
40
|
+
],
|
|
41
|
+
[null, { result: null }],
|
|
42
|
+
])("leaves successful result %j unchanged", (result, expected) => {
|
|
43
|
+
expect(toAdkFunctionResponse(result)).toEqual(expected);
|
|
44
|
+
expect(isAdkFunctionError(toAdkFunctionResponse(result))).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { isRecord } from "@assistant-ui/core/internal";
|
|
2
|
+
|
|
3
|
+
export const isAdkFunctionError = (response: unknown): boolean =>
|
|
4
|
+
isRecord(response) &&
|
|
5
|
+
Object.hasOwn(response, "error") &&
|
|
6
|
+
response.error !== undefined;
|
|
7
|
+
|
|
8
|
+
export const toAdkFunctionResponse = (
|
|
9
|
+
result: unknown,
|
|
10
|
+
isError = false,
|
|
11
|
+
): Record<string, unknown> => {
|
|
12
|
+
if (isError && !isAdkFunctionError(result)) return { error: result ?? null };
|
|
13
|
+
return Array.isArray(result)
|
|
14
|
+
? { results: result }
|
|
15
|
+
: isRecord(result)
|
|
16
|
+
? result
|
|
17
|
+
: { result };
|
|
18
|
+
};
|
package/src/types.ts
CHANGED
|
@@ -17,11 +17,11 @@ import {
|
|
|
17
17
|
messageToEvent,
|
|
18
18
|
messagesToEvents,
|
|
19
19
|
useAdkMessages,
|
|
20
|
-
type UseAdkMessagesOptions,
|
|
21
20
|
} from "./useAdkMessages";
|
|
22
21
|
import { projectAdkToolApprovals } from "./adkToolApproval";
|
|
23
22
|
import { createAdkStream } from "./AdkClient";
|
|
24
23
|
import { AdkEventAccumulator } from "./AdkEventAccumulator";
|
|
24
|
+
import { getPendingCancellations } from "./convertToAdkMessages";
|
|
25
25
|
import type { AdkEvent, AdkMessage, AdkStreamCallback } from "./types";
|
|
26
26
|
|
|
27
27
|
afterEach(() => {
|
|
@@ -29,6 +29,53 @@ afterEach(() => {
|
|
|
29
29
|
vi.unstubAllGlobals();
|
|
30
30
|
});
|
|
31
31
|
|
|
32
|
+
describe("optimistic tool outcomes", () => {
|
|
33
|
+
it.each([false, true])(
|
|
34
|
+
"preserves failures alongside successful results (batch: %s)",
|
|
35
|
+
(batch) => {
|
|
36
|
+
const failed: AdkMessage = {
|
|
37
|
+
id: "failed",
|
|
38
|
+
type: "tool",
|
|
39
|
+
name: "search",
|
|
40
|
+
tool_call_id: "tc-error",
|
|
41
|
+
content: "denied",
|
|
42
|
+
status: "error",
|
|
43
|
+
};
|
|
44
|
+
const succeeded: AdkMessage = {
|
|
45
|
+
id: "succeeded",
|
|
46
|
+
type: "tool",
|
|
47
|
+
name: "search",
|
|
48
|
+
tool_call_id: "tc-ok",
|
|
49
|
+
content: "found",
|
|
50
|
+
status: "success",
|
|
51
|
+
};
|
|
52
|
+
const events = batch
|
|
53
|
+
? messagesToEvents([
|
|
54
|
+
failed,
|
|
55
|
+
succeeded,
|
|
56
|
+
{ id: "human", type: "human", content: "continue" },
|
|
57
|
+
])
|
|
58
|
+
: [messageToEvent(failed), messageToEvent(succeeded)];
|
|
59
|
+
const acc = new AdkEventAccumulator();
|
|
60
|
+
for (const event of events) acc.processEvent(event);
|
|
61
|
+
expect(
|
|
62
|
+
acc.getMessages().filter((message) => message.type === "tool"),
|
|
63
|
+
).toMatchObject([
|
|
64
|
+
{
|
|
65
|
+
tool_call_id: "tc-error",
|
|
66
|
+
status: "error",
|
|
67
|
+
content: JSON.stringify({ error: "denied" }),
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
tool_call_id: "tc-ok",
|
|
71
|
+
status: "success",
|
|
72
|
+
content: JSON.stringify({ result: "found" }),
|
|
73
|
+
},
|
|
74
|
+
]);
|
|
75
|
+
},
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
|
|
32
79
|
describe("ADK runtime callbacks", () => {
|
|
33
80
|
it.each(["onAgentTransfer", "onCustomEvent", "onError"] as const)(
|
|
34
81
|
"continues streaming when %s throws",
|
|
@@ -65,7 +112,7 @@ describe("ADK runtime callbacks", () => {
|
|
|
65
112
|
};
|
|
66
113
|
const eventHandlers = {
|
|
67
114
|
[callbackName]: callback,
|
|
68
|
-
}
|
|
115
|
+
};
|
|
69
116
|
const { result } = renderHook(() =>
|
|
70
117
|
useAdkMessages({ stream, eventHandlers }),
|
|
71
118
|
);
|
|
@@ -315,6 +362,86 @@ describe("optimistic confirmation replies", () => {
|
|
|
315
362
|
return result;
|
|
316
363
|
};
|
|
317
364
|
|
|
365
|
+
it("preserves an unanswered gate across a reply run", async () => {
|
|
366
|
+
let run = 0;
|
|
367
|
+
const stream: AdkStreamCallback = async function* () {
|
|
368
|
+
run += 1;
|
|
369
|
+
if (run === 1) {
|
|
370
|
+
yield {
|
|
371
|
+
id: "gates",
|
|
372
|
+
author: "agent",
|
|
373
|
+
longRunningToolIds: ["conf-a", "conf-b"],
|
|
374
|
+
content: {
|
|
375
|
+
role: "model",
|
|
376
|
+
parts: [
|
|
377
|
+
{
|
|
378
|
+
functionCall: {
|
|
379
|
+
id: "conf-a",
|
|
380
|
+
name: "adk_request_confirmation",
|
|
381
|
+
args: {},
|
|
382
|
+
},
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
functionCall: {
|
|
386
|
+
id: "conf-b",
|
|
387
|
+
name: "adk_request_confirmation",
|
|
388
|
+
args: {},
|
|
389
|
+
},
|
|
390
|
+
},
|
|
391
|
+
],
|
|
392
|
+
},
|
|
393
|
+
} satisfies AdkEvent;
|
|
394
|
+
} else {
|
|
395
|
+
yield {
|
|
396
|
+
id: "rerun",
|
|
397
|
+
author: "agent",
|
|
398
|
+
content: {
|
|
399
|
+
role: "user",
|
|
400
|
+
parts: [
|
|
401
|
+
{
|
|
402
|
+
functionResponse: {
|
|
403
|
+
id: "orig-conf-a",
|
|
404
|
+
name: "delete_file",
|
|
405
|
+
response: { result: "deleted" },
|
|
406
|
+
},
|
|
407
|
+
},
|
|
408
|
+
],
|
|
409
|
+
},
|
|
410
|
+
} satisfies AdkEvent;
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
const { result } = renderHook(() => useAdkMessages({ stream }));
|
|
414
|
+
|
|
415
|
+
await act(async () => {
|
|
416
|
+
await result.current.sendMessage(
|
|
417
|
+
[{ id: "user-1", type: "human", content: "start" }],
|
|
418
|
+
{},
|
|
419
|
+
);
|
|
420
|
+
});
|
|
421
|
+
expect(result.current.longRunningToolIds).toEqual(["conf-a", "conf-b"]);
|
|
422
|
+
|
|
423
|
+
await act(async () => {
|
|
424
|
+
await result.current.sendMessage(
|
|
425
|
+
[
|
|
426
|
+
confirmationReply(
|
|
427
|
+
"reply-a",
|
|
428
|
+
"conf-a",
|
|
429
|
+
JSON.stringify({ confirmed: true }),
|
|
430
|
+
),
|
|
431
|
+
],
|
|
432
|
+
{},
|
|
433
|
+
);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
expect(result.current.longRunningToolIds).toEqual(["conf-b"]);
|
|
437
|
+
expect(
|
|
438
|
+
getPendingCancellations(
|
|
439
|
+
result.current.messages,
|
|
440
|
+
result.current.longRunningToolIds,
|
|
441
|
+
),
|
|
442
|
+
).toEqual([]);
|
|
443
|
+
});
|
|
444
|
+
|
|
318
445
|
it("keeps both gates pending when one send carries an unreadable reply", async () => {
|
|
319
446
|
const result = await renderWithGates();
|
|
320
447
|
|
|
@@ -338,6 +465,10 @@ describe("optimistic confirmation replies", () => {
|
|
|
338
465
|
projectAdkToolApprovals(result.current.messages).approvals.values(),
|
|
339
466
|
),
|
|
340
467
|
]).toEqual([{ id: "conf-a" }, { id: "conf-b" }]);
|
|
468
|
+
expect(result.current.toolConfirmations.map((c) => c.toolCallId)).toEqual([
|
|
469
|
+
"conf-a",
|
|
470
|
+
"conf-b",
|
|
471
|
+
]);
|
|
341
472
|
});
|
|
342
473
|
|
|
343
474
|
it("keeps both gates pending when an ai message sits between the replies", async () => {
|
|
@@ -394,6 +525,102 @@ describe("optimistic confirmation replies", () => {
|
|
|
394
525
|
projectAdkToolApprovals(result.current.messages).approvals.values(),
|
|
395
526
|
),
|
|
396
527
|
]).toEqual([{ id: "conf-a", approved: true }, { id: "conf-b" }]);
|
|
528
|
+
expect(result.current.toolConfirmations.map((c) => c.toolCallId)).toEqual([
|
|
529
|
+
"conf-b",
|
|
530
|
+
]);
|
|
531
|
+
});
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
describe("pending requests across sends", () => {
|
|
535
|
+
it("keeps an unanswered request listed across sends until its reply is sent", async () => {
|
|
536
|
+
let run = 0;
|
|
537
|
+
const stream: AdkStreamCallback = async function* () {
|
|
538
|
+
run += 1;
|
|
539
|
+
if (run === 1) {
|
|
540
|
+
yield {
|
|
541
|
+
id: "requests",
|
|
542
|
+
author: "agent",
|
|
543
|
+
longRunningToolIds: ["conf-1", "cred-1"],
|
|
544
|
+
content: {
|
|
545
|
+
role: "model",
|
|
546
|
+
parts: [
|
|
547
|
+
{
|
|
548
|
+
functionCall: {
|
|
549
|
+
id: "conf-1",
|
|
550
|
+
name: "adk_request_confirmation",
|
|
551
|
+
args: {
|
|
552
|
+
originalFunctionCall: { id: "gated-1", name: "transfer" },
|
|
553
|
+
toolConfirmation: { hint: "Transfer?" },
|
|
554
|
+
},
|
|
555
|
+
},
|
|
556
|
+
},
|
|
557
|
+
{
|
|
558
|
+
functionCall: {
|
|
559
|
+
id: "cred-1",
|
|
560
|
+
name: "adk_request_credential",
|
|
561
|
+
args: {
|
|
562
|
+
function_call_id: "gated-2",
|
|
563
|
+
auth_config: { credentialKey: "k" },
|
|
564
|
+
},
|
|
565
|
+
},
|
|
566
|
+
},
|
|
567
|
+
],
|
|
568
|
+
},
|
|
569
|
+
} satisfies AdkEvent;
|
|
570
|
+
} else if (run === 2) {
|
|
571
|
+
yield {
|
|
572
|
+
id: "answer",
|
|
573
|
+
author: "agent",
|
|
574
|
+
content: { role: "model", parts: [{ text: "still waiting" }] },
|
|
575
|
+
} satisfies AdkEvent;
|
|
576
|
+
}
|
|
577
|
+
};
|
|
578
|
+
const { result } = renderHook(() => useAdkMessages({ stream }));
|
|
579
|
+
const pending = () => ({
|
|
580
|
+
confirmations: result.current.toolConfirmations.map((c) => c.toolCallId),
|
|
581
|
+
authRequests: result.current.authRequests.map((r) => r.toolCallId),
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
await act(async () => {
|
|
585
|
+
await result.current.sendMessage(
|
|
586
|
+
[{ id: "user-1", type: "human", content: "start" }],
|
|
587
|
+
{},
|
|
588
|
+
);
|
|
589
|
+
});
|
|
590
|
+
expect(pending()).toEqual({
|
|
591
|
+
confirmations: ["conf-1"],
|
|
592
|
+
authRequests: ["cred-1"],
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
await act(async () => {
|
|
596
|
+
await result.current.sendMessage(
|
|
597
|
+
[{ id: "user-2", type: "human", content: "any news?" }],
|
|
598
|
+
{},
|
|
599
|
+
);
|
|
600
|
+
});
|
|
601
|
+
expect(pending()).toEqual({
|
|
602
|
+
confirmations: ["conf-1"],
|
|
603
|
+
authRequests: ["cred-1"],
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
await act(async () => {
|
|
607
|
+
await result.current.sendMessage(
|
|
608
|
+
[
|
|
609
|
+
{
|
|
610
|
+
id: "reply",
|
|
611
|
+
type: "tool",
|
|
612
|
+
tool_call_id: "cred-1",
|
|
613
|
+
name: "adk_request_credential",
|
|
614
|
+
content: JSON.stringify({
|
|
615
|
+
exchangedAuthCredential: { authType: "apiKey" },
|
|
616
|
+
}),
|
|
617
|
+
status: "success",
|
|
618
|
+
},
|
|
619
|
+
],
|
|
620
|
+
{},
|
|
621
|
+
);
|
|
622
|
+
});
|
|
623
|
+
expect(pending()).toEqual({ confirmations: ["conf-1"], authRequests: [] });
|
|
397
624
|
});
|
|
398
625
|
});
|
|
399
626
|
|
|
@@ -538,6 +765,26 @@ describe("optimistic multi-message sends", () => {
|
|
|
538
765
|
});
|
|
539
766
|
|
|
540
767
|
describe("messageToEvent (contentToParts)", () => {
|
|
768
|
+
it.each([
|
|
769
|
+
["scalar", "false", { result: false }],
|
|
770
|
+
["array", "[1,2]", { results: [1, 2] }],
|
|
771
|
+
])(
|
|
772
|
+
"normalizes an optimistic %s tool response",
|
|
773
|
+
(_label, content, response) => {
|
|
774
|
+
const event = messageToEvent({
|
|
775
|
+
id: "tool-1",
|
|
776
|
+
type: "tool",
|
|
777
|
+
content,
|
|
778
|
+
tool_call_id: "call-1",
|
|
779
|
+
name: "search",
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
expect(event.content?.parts?.[0]?.functionResponse?.response).toEqual(
|
|
783
|
+
response,
|
|
784
|
+
);
|
|
785
|
+
},
|
|
786
|
+
);
|
|
787
|
+
|
|
541
788
|
it("serializes a file content part as inlineData", () => {
|
|
542
789
|
const msg: AdkMessage = {
|
|
543
790
|
id: "m1",
|
package/src/useAdkMessages.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
} from "@assistant-ui/core/internal";
|
|
16
16
|
import { AdkEventAccumulator } from "./AdkEventAccumulator";
|
|
17
17
|
import { contentToParts } from "./contentToParts";
|
|
18
|
+
import { toAdkFunctionResponse } from "./toAdkFunctionResponse";
|
|
18
19
|
import type {
|
|
19
20
|
AdkEvent,
|
|
20
21
|
AdkMessage,
|
|
@@ -58,7 +59,7 @@ export const useAdkMessages = ({
|
|
|
58
59
|
name?: string | undefined;
|
|
59
60
|
branch?: string | undefined;
|
|
60
61
|
}>({});
|
|
61
|
-
const [longRunningToolIds,
|
|
62
|
+
const [longRunningToolIds, _setLongRunningToolIds] = useState<string[]>([]);
|
|
62
63
|
const [artifactDelta, setArtifactDelta] = useState<Record<string, number>>(
|
|
63
64
|
{},
|
|
64
65
|
);
|
|
@@ -71,9 +72,9 @@ export const useAdkMessages = ({
|
|
|
71
72
|
Map<string, AdkMessageMetadata>
|
|
72
73
|
>(new Map());
|
|
73
74
|
const lastTransferToAgentRef = useRef<string | undefined>(undefined);
|
|
74
|
-
// setMessagesImmediate
|
|
75
|
-
// this ref with it, so the ref never trails a commit.
|
|
75
|
+
// setMessagesImmediate and setLongRunningToolIds are the only writers of their state and publish these refs with it, so neither ref trails a commit.
|
|
76
76
|
const messagesRef = useRef(messages);
|
|
77
|
+
const longRunningToolIdsRef = useRef(longRunningToolIds);
|
|
77
78
|
const stateDeltaRef = useRef(stateDelta);
|
|
78
79
|
useInsertionEffect(() => {
|
|
79
80
|
stateDeltaRef.current = stateDelta;
|
|
@@ -91,6 +92,10 @@ export const useAdkMessages = ({
|
|
|
91
92
|
messagesRef.current = msgs;
|
|
92
93
|
_setMessages(msgs);
|
|
93
94
|
}, []);
|
|
95
|
+
const setLongRunningToolIds = useCallback((ids: string[]) => {
|
|
96
|
+
longRunningToolIdsRef.current = ids;
|
|
97
|
+
_setLongRunningToolIds(ids);
|
|
98
|
+
}, []);
|
|
94
99
|
|
|
95
100
|
/**
|
|
96
101
|
* Swap the thread over to a loaded snapshot in one commit. Unlike
|
|
@@ -110,7 +115,7 @@ export const useAdkMessages = ({
|
|
|
110
115
|
setArtifactDelta(snapshot.artifactDelta ?? {});
|
|
111
116
|
setAgentInfo(snapshot.agentInfo ?? {});
|
|
112
117
|
},
|
|
113
|
-
[setMessagesImmediate],
|
|
118
|
+
[setLongRunningToolIds, setMessagesImmediate],
|
|
114
119
|
);
|
|
115
120
|
|
|
116
121
|
// Replace the message list AND reset derived per-turn HITL state.
|
|
@@ -126,7 +131,7 @@ export const useAdkMessages = ({
|
|
|
126
131
|
setEscalated(false);
|
|
127
132
|
setMessageMetadata(new Map());
|
|
128
133
|
},
|
|
129
|
-
[setMessagesImmediate],
|
|
134
|
+
[setLongRunningToolIds, setMessagesImmediate],
|
|
130
135
|
);
|
|
131
136
|
|
|
132
137
|
const abortControllerRef = useRef<AbortController | null>(null);
|
|
@@ -148,13 +153,25 @@ export const useAdkMessages = ({
|
|
|
148
153
|
// with the originals would leave every later staged id beside the merged
|
|
149
154
|
// copy of itself.
|
|
150
155
|
const resentIds = new Set(newMessagesWithId.map((m) => m.id));
|
|
156
|
+
// The optimistic event for a tool-only batch carries no author, so the accumulator cannot settle the calls this send answers.
|
|
157
|
+
const answeredToolCallIds = new Set(
|
|
158
|
+
newMessagesWithId.flatMap((m) =>
|
|
159
|
+
m.type === "tool" ? [m.tool_call_id] : [],
|
|
160
|
+
),
|
|
161
|
+
);
|
|
151
162
|
const accumulator = new AdkEventAccumulator(
|
|
152
163
|
messagesRef.current.filter((m) => !resentIds.has(m.id)),
|
|
164
|
+
longRunningToolIdsRef.current.filter(
|
|
165
|
+
(id) => !answeredToolCallIds.has(id),
|
|
166
|
+
),
|
|
153
167
|
);
|
|
154
168
|
for (const event of messagesToEvents(newMessagesWithId)) {
|
|
155
169
|
accumulator.processEvent(event);
|
|
156
170
|
}
|
|
157
171
|
setMessagesImmediate(accumulator.getMessages());
|
|
172
|
+
setLongRunningToolIds(accumulator.getLongRunningToolIds());
|
|
173
|
+
setToolConfirmations(accumulator.getToolConfirmations());
|
|
174
|
+
setAuthRequests(accumulator.getAuthRequests());
|
|
158
175
|
|
|
159
176
|
// Google ADK replaces active runs, while React LangGraph queues sends.
|
|
160
177
|
abortControllerRef.current?.abort();
|
|
@@ -255,6 +272,7 @@ export const useAdkMessages = ({
|
|
|
255
272
|
[
|
|
256
273
|
aui,
|
|
257
274
|
setMessagesImmediate,
|
|
275
|
+
setLongRunningToolIds,
|
|
258
276
|
stream,
|
|
259
277
|
onError,
|
|
260
278
|
onCustomEvent,
|
|
@@ -363,7 +381,7 @@ export const messageToEvent = (msg: AdkMessage): AdkEvent => {
|
|
|
363
381
|
functionResponse: {
|
|
364
382
|
name: msg.name,
|
|
365
383
|
id: msg.tool_call_id,
|
|
366
|
-
response,
|
|
384
|
+
response: toAdkFunctionResponse(response, msg.status === "error"),
|
|
367
385
|
},
|
|
368
386
|
},
|
|
369
387
|
],
|