@assistant-ui/react-google-adk 0.0.29 → 0.0.30
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 +2 -1
- package/dist/AdkClient.js.map +1 -1
- package/dist/AdkEventAccumulator.d.ts +1 -1
- package/dist/AdkEventAccumulator.d.ts.map +1 -1
- package/dist/AdkEventAccumulator.js +31 -10
- package/dist/AdkEventAccumulator.js.map +1 -1
- package/dist/convertToAdkMessages.d.ts +10 -1
- package/dist/convertToAdkMessages.d.ts.map +1 -1
- package/dist/convertToAdkMessages.js +20 -6
- package/dist/convertToAdkMessages.js.map +1 -1
- package/dist/sdkIdentity.d.ts +6 -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/parseAdkRequest.d.ts.map +1 -1
- package/dist/server/parseAdkRequest.js +2 -1
- package/dist/server/parseAdkRequest.js.map +1 -1
- package/dist/toAdkFunctionResponse.d.ts +6 -0
- package/dist/toAdkFunctionResponse.d.ts.map +1 -0
- package/dist/toAdkFunctionResponse.js +11 -0
- package/dist/toAdkFunctionResponse.js.map +1 -0
- package/dist/useAdkMessages.d.ts.map +1 -1
- package/dist/useAdkMessages.js +14 -5
- package/dist/useAdkMessages.js.map +1 -1
- package/dist/useAdkRuntime.d.ts.map +1 -1
- package/dist/useAdkRuntime.js +2 -0
- package/dist/useAdkRuntime.js.map +1 -1
- package/package.json +5 -5
- package/src/AdkClient.test.ts +139 -2
- package/src/AdkClient.ts +2 -1
- package/src/AdkEventAccumulator.test.ts +276 -0
- package/src/AdkEventAccumulator.ts +32 -3
- package/src/AdkSessionAdapter.test.ts +88 -0
- package/src/convertToAdkMessages.test.ts +70 -0
- package/src/convertToAdkMessages.ts +19 -4
- package/src/sdkIdentity.ts +9 -0
- package/src/server/parseAdkRequest.test.ts +63 -0
- package/src/server/parseAdkRequest.ts +2 -1
- package/src/toAdkFunctionResponse.test.ts +46 -0
- package/src/toAdkFunctionResponse.ts +18 -0
- package/src/useAdkMessages.test.ts +148 -0
- package/src/useAdkMessages.ts +22 -6
- package/src/useAdkRuntime.ts +2 -0
- package/src/useAdkRuntimeApproval.test.tsx +0 -1
|
@@ -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,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
|
+
};
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
import { projectAdkToolApprovals } from "./adkToolApproval";
|
|
23
23
|
import { createAdkStream } from "./AdkClient";
|
|
24
24
|
import { AdkEventAccumulator } from "./AdkEventAccumulator";
|
|
25
|
+
import { getPendingCancellations } from "./convertToAdkMessages";
|
|
25
26
|
import type { AdkEvent, AdkMessage, AdkStreamCallback } from "./types";
|
|
26
27
|
|
|
27
28
|
afterEach(() => {
|
|
@@ -29,6 +30,53 @@ afterEach(() => {
|
|
|
29
30
|
vi.unstubAllGlobals();
|
|
30
31
|
});
|
|
31
32
|
|
|
33
|
+
describe("optimistic tool outcomes", () => {
|
|
34
|
+
it.each([false, true])(
|
|
35
|
+
"preserves failures alongside successful results (batch: %s)",
|
|
36
|
+
(batch) => {
|
|
37
|
+
const failed: AdkMessage = {
|
|
38
|
+
id: "failed",
|
|
39
|
+
type: "tool",
|
|
40
|
+
name: "search",
|
|
41
|
+
tool_call_id: "tc-error",
|
|
42
|
+
content: "denied",
|
|
43
|
+
status: "error",
|
|
44
|
+
};
|
|
45
|
+
const succeeded: AdkMessage = {
|
|
46
|
+
id: "succeeded",
|
|
47
|
+
type: "tool",
|
|
48
|
+
name: "search",
|
|
49
|
+
tool_call_id: "tc-ok",
|
|
50
|
+
content: "found",
|
|
51
|
+
status: "success",
|
|
52
|
+
};
|
|
53
|
+
const events = batch
|
|
54
|
+
? messagesToEvents([
|
|
55
|
+
failed,
|
|
56
|
+
succeeded,
|
|
57
|
+
{ id: "human", type: "human", content: "continue" },
|
|
58
|
+
])
|
|
59
|
+
: [messageToEvent(failed), messageToEvent(succeeded)];
|
|
60
|
+
const acc = new AdkEventAccumulator();
|
|
61
|
+
for (const event of events) acc.processEvent(event);
|
|
62
|
+
expect(
|
|
63
|
+
acc.getMessages().filter((message) => message.type === "tool"),
|
|
64
|
+
).toMatchObject([
|
|
65
|
+
{
|
|
66
|
+
tool_call_id: "tc-error",
|
|
67
|
+
status: "error",
|
|
68
|
+
content: JSON.stringify({ error: "denied" }),
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
tool_call_id: "tc-ok",
|
|
72
|
+
status: "success",
|
|
73
|
+
content: JSON.stringify({ result: "found" }),
|
|
74
|
+
},
|
|
75
|
+
]);
|
|
76
|
+
},
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
|
|
32
80
|
describe("ADK runtime callbacks", () => {
|
|
33
81
|
it.each(["onAgentTransfer", "onCustomEvent", "onError"] as const)(
|
|
34
82
|
"continues streaming when %s throws",
|
|
@@ -315,6 +363,86 @@ describe("optimistic confirmation replies", () => {
|
|
|
315
363
|
return result;
|
|
316
364
|
};
|
|
317
365
|
|
|
366
|
+
it("preserves an unanswered gate across a reply run", async () => {
|
|
367
|
+
let run = 0;
|
|
368
|
+
const stream: AdkStreamCallback = async function* () {
|
|
369
|
+
run += 1;
|
|
370
|
+
if (run === 1) {
|
|
371
|
+
yield {
|
|
372
|
+
id: "gates",
|
|
373
|
+
author: "agent",
|
|
374
|
+
longRunningToolIds: ["conf-a", "conf-b"],
|
|
375
|
+
content: {
|
|
376
|
+
role: "model",
|
|
377
|
+
parts: [
|
|
378
|
+
{
|
|
379
|
+
functionCall: {
|
|
380
|
+
id: "conf-a",
|
|
381
|
+
name: "adk_request_confirmation",
|
|
382
|
+
args: {},
|
|
383
|
+
},
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
functionCall: {
|
|
387
|
+
id: "conf-b",
|
|
388
|
+
name: "adk_request_confirmation",
|
|
389
|
+
args: {},
|
|
390
|
+
},
|
|
391
|
+
},
|
|
392
|
+
],
|
|
393
|
+
},
|
|
394
|
+
} satisfies AdkEvent;
|
|
395
|
+
} else {
|
|
396
|
+
yield {
|
|
397
|
+
id: "rerun",
|
|
398
|
+
author: "agent",
|
|
399
|
+
content: {
|
|
400
|
+
role: "user",
|
|
401
|
+
parts: [
|
|
402
|
+
{
|
|
403
|
+
functionResponse: {
|
|
404
|
+
id: "orig-conf-a",
|
|
405
|
+
name: "delete_file",
|
|
406
|
+
response: { result: "deleted" },
|
|
407
|
+
},
|
|
408
|
+
},
|
|
409
|
+
],
|
|
410
|
+
},
|
|
411
|
+
} satisfies AdkEvent;
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
const { result } = renderHook(() => useAdkMessages({ stream }));
|
|
415
|
+
|
|
416
|
+
await act(async () => {
|
|
417
|
+
await result.current.sendMessage(
|
|
418
|
+
[{ id: "user-1", type: "human", content: "start" }],
|
|
419
|
+
{},
|
|
420
|
+
);
|
|
421
|
+
});
|
|
422
|
+
expect(result.current.longRunningToolIds).toEqual(["conf-a", "conf-b"]);
|
|
423
|
+
|
|
424
|
+
await act(async () => {
|
|
425
|
+
await result.current.sendMessage(
|
|
426
|
+
[
|
|
427
|
+
confirmationReply(
|
|
428
|
+
"reply-a",
|
|
429
|
+
"conf-a",
|
|
430
|
+
JSON.stringify({ confirmed: true }),
|
|
431
|
+
),
|
|
432
|
+
],
|
|
433
|
+
{},
|
|
434
|
+
);
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
expect(result.current.longRunningToolIds).toEqual(["conf-b"]);
|
|
438
|
+
expect(
|
|
439
|
+
getPendingCancellations(
|
|
440
|
+
result.current.messages,
|
|
441
|
+
result.current.longRunningToolIds,
|
|
442
|
+
),
|
|
443
|
+
).toEqual([]);
|
|
444
|
+
});
|
|
445
|
+
|
|
318
446
|
it("keeps both gates pending when one send carries an unreadable reply", async () => {
|
|
319
447
|
const result = await renderWithGates();
|
|
320
448
|
|
|
@@ -538,6 +666,26 @@ describe("optimistic multi-message sends", () => {
|
|
|
538
666
|
});
|
|
539
667
|
|
|
540
668
|
describe("messageToEvent (contentToParts)", () => {
|
|
669
|
+
it.each([
|
|
670
|
+
["scalar", "false", { result: false }],
|
|
671
|
+
["array", "[1,2]", { results: [1, 2] }],
|
|
672
|
+
])(
|
|
673
|
+
"normalizes an optimistic %s tool response",
|
|
674
|
+
(_label, content, response) => {
|
|
675
|
+
const event = messageToEvent({
|
|
676
|
+
id: "tool-1",
|
|
677
|
+
type: "tool",
|
|
678
|
+
content,
|
|
679
|
+
tool_call_id: "call-1",
|
|
680
|
+
name: "search",
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
expect(event.content?.parts[0]?.functionResponse?.response).toEqual(
|
|
684
|
+
response,
|
|
685
|
+
);
|
|
686
|
+
},
|
|
687
|
+
);
|
|
688
|
+
|
|
541
689
|
it("serializes a file content part as inlineData", () => {
|
|
542
690
|
const msg: AdkMessage = {
|
|
543
691
|
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,23 @@ 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());
|
|
158
173
|
|
|
159
174
|
// Google ADK replaces active runs, while React LangGraph queues sends.
|
|
160
175
|
abortControllerRef.current?.abort();
|
|
@@ -255,6 +270,7 @@ export const useAdkMessages = ({
|
|
|
255
270
|
[
|
|
256
271
|
aui,
|
|
257
272
|
setMessagesImmediate,
|
|
273
|
+
setLongRunningToolIds,
|
|
258
274
|
stream,
|
|
259
275
|
onError,
|
|
260
276
|
onCustomEvent,
|
|
@@ -363,7 +379,7 @@ export const messageToEvent = (msg: AdkMessage): AdkEvent => {
|
|
|
363
379
|
functionResponse: {
|
|
364
380
|
name: msg.name,
|
|
365
381
|
id: msg.tool_call_id,
|
|
366
|
-
response,
|
|
382
|
+
response: toAdkFunctionResponse(response, msg.status === "error"),
|
|
367
383
|
},
|
|
368
384
|
},
|
|
369
385
|
],
|
package/src/useAdkRuntime.ts
CHANGED
|
@@ -57,6 +57,7 @@ import {
|
|
|
57
57
|
toAdkToolConfirmationReply,
|
|
58
58
|
} from "./adkToolApproval";
|
|
59
59
|
import { adkExtras } from "./adkExtras";
|
|
60
|
+
import { ADK_SDK } from "./sdkIdentity";
|
|
60
61
|
|
|
61
62
|
export type UseAdkRuntimeOptions = ExternalStoreSharedOptions & {
|
|
62
63
|
stream: AdkStreamCallback;
|
|
@@ -504,6 +505,7 @@ export const useAdkRuntime = ({
|
|
|
504
505
|
}: UseAdkRuntimeOptions) => {
|
|
505
506
|
const aui = useAui();
|
|
506
507
|
const cloudAdapter = useCloudThreadListAdapter({
|
|
508
|
+
sdk: ADK_SDK,
|
|
507
509
|
cloud,
|
|
508
510
|
create: createCloudThreadListAdapterCreateFallback(
|
|
509
511
|
create,
|