@assistant-ui/react-google-adk 0.0.28 → 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 +33 -12
- 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/hooks.d.ts.map +1 -1
- package/dist/hooks.js +6 -8
- package/dist/hooks.js.map +1 -1
- package/dist/parseAdkEvent.d.ts.map +1 -1
- package/dist/parseAdkEvent.js +15 -2
- package/dist/parseAdkEvent.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 +22 -10
- package/dist/useAdkMessages.js.map +1 -1
- package/dist/useAdkRuntime.d.ts.map +1 -1
- package/dist/useAdkRuntime.js +6 -2
- package/dist/useAdkRuntime.js.map +1 -1
- package/package.json +6 -6
- package/src/AdkClient.test.ts +192 -2
- package/src/AdkClient.ts +2 -1
- package/src/AdkEventAccumulator.test.ts +297 -0
- package/src/AdkEventAccumulator.ts +35 -5
- package/src/AdkSessionAdapter.test.ts +88 -0
- package/src/convertToAdkMessages.test.ts +70 -0
- package/src/convertToAdkMessages.ts +19 -4
- package/src/hooks.render.test.tsx +74 -0
- package/src/hooks.test.tsx +33 -0
- package/src/hooks.ts +17 -22
- package/src/parseAdkEvent.ts +36 -7
- 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 +247 -0
- package/src/useAdkMessages.ts +51 -15
- package/src/useAdkRuntime.replacement.test.tsx +138 -0
- package/src/useAdkRuntime.ts +5 -1
- package/src/useAdkRuntimeApproval.test.tsx +0 -1
package/src/AdkClient.test.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
2
|
import { createAdkStream } from "./AdkClient";
|
|
3
3
|
import { adkEventStream } from "./server/adkEventStream";
|
|
4
|
+
import { AdkEventAccumulator } from "./AdkEventAccumulator";
|
|
5
|
+
import { parseAdkRequest, toAdkContent } from "./server/parseAdkRequest";
|
|
4
6
|
import type { AdkEvent, AdkMessage, AdkSendMessageConfig } from "./types";
|
|
5
7
|
|
|
6
8
|
// ── Helpers ──
|
|
@@ -65,9 +67,103 @@ beforeEach(() => {
|
|
|
65
67
|
mockFetch.mockReset();
|
|
66
68
|
});
|
|
67
69
|
|
|
70
|
+
describe.each(["direct", "proxy", "proxy batch"] as const)(
|
|
71
|
+
"%s tool outcomes",
|
|
72
|
+
(mode) => {
|
|
73
|
+
it.each(["error", "success"] as const)(
|
|
74
|
+
"preserves %s through the runner request",
|
|
75
|
+
async (status) => {
|
|
76
|
+
mockFetch.mockResolvedValueOnce(sseResponse(sseBody("")));
|
|
77
|
+
const stream = createAdkStream(
|
|
78
|
+
mode === "direct"
|
|
79
|
+
? { api: "http://localhost:8000", appName: "app", userId: "user" }
|
|
80
|
+
: { api: "/api/adk" },
|
|
81
|
+
);
|
|
82
|
+
const messages: AdkMessage[] = [
|
|
83
|
+
{
|
|
84
|
+
id: "result",
|
|
85
|
+
type: "tool",
|
|
86
|
+
name: "search",
|
|
87
|
+
tool_call_id: "tc-1",
|
|
88
|
+
content: "permission denied",
|
|
89
|
+
status,
|
|
90
|
+
},
|
|
91
|
+
];
|
|
92
|
+
if (mode === "proxy batch")
|
|
93
|
+
messages.push({ id: "human", type: "human", content: "continue" });
|
|
94
|
+
const events = await stream(messages, makeConfig());
|
|
95
|
+
for await (const event of events) expect(event).toBeUndefined();
|
|
96
|
+
const body = JSON.parse(mockFetch.mock.calls[0]![1]!.body as string);
|
|
97
|
+
const content =
|
|
98
|
+
mode === "direct"
|
|
99
|
+
? body.newMessage
|
|
100
|
+
: toAdkContent(
|
|
101
|
+
await parseAdkRequest(
|
|
102
|
+
new Request("http://localhost/api/adk", {
|
|
103
|
+
method: "POST",
|
|
104
|
+
body: JSON.stringify(body),
|
|
105
|
+
headers: { "Content-Type": "application/json" },
|
|
106
|
+
}),
|
|
107
|
+
),
|
|
108
|
+
);
|
|
109
|
+
expect(content.parts[0]).toEqual({
|
|
110
|
+
functionResponse: {
|
|
111
|
+
id: "tc-1",
|
|
112
|
+
name: "search",
|
|
113
|
+
response:
|
|
114
|
+
status === "error"
|
|
115
|
+
? { error: "permission denied" }
|
|
116
|
+
: { result: "permission denied" },
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
},
|
|
120
|
+
);
|
|
121
|
+
},
|
|
122
|
+
);
|
|
123
|
+
|
|
68
124
|
// ── Proxy mode ──
|
|
69
125
|
|
|
70
126
|
describe("createAdkStream - proxy mode", () => {
|
|
127
|
+
it("accumulates snake_case image and file parts from SSE", async () => {
|
|
128
|
+
const event = {
|
|
129
|
+
id: "media",
|
|
130
|
+
author: "agent",
|
|
131
|
+
content: {
|
|
132
|
+
parts: [
|
|
133
|
+
{ inline_data: { mime_type: "image/png", data: "aGVsbG8=" } },
|
|
134
|
+
{
|
|
135
|
+
file_data: {
|
|
136
|
+
mime_type: "application/pdf",
|
|
137
|
+
file_uri: "https://example.test/report.pdf",
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
mockFetch.mockResolvedValueOnce(
|
|
144
|
+
sseResponse(sseBody(`data: ${JSON.stringify(event)}\n\n`)),
|
|
145
|
+
);
|
|
146
|
+
const stream = createAdkStream({ api: "/api/adk" });
|
|
147
|
+
const events = await stream(
|
|
148
|
+
[{ id: "human", type: "human", content: "show files" }],
|
|
149
|
+
makeConfig(),
|
|
150
|
+
);
|
|
151
|
+
const acc = new AdkEventAccumulator();
|
|
152
|
+
for await (const item of events) acc.processEvent(item);
|
|
153
|
+
expect(acc.getMessages()).toMatchObject([
|
|
154
|
+
{
|
|
155
|
+
content: [
|
|
156
|
+
{ type: "image", mimeType: "image/png", data: "aGVsbG8=" },
|
|
157
|
+
{
|
|
158
|
+
type: "file_url",
|
|
159
|
+
mimeType: "application/pdf",
|
|
160
|
+
url: "https://example.test/report.pdf",
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
},
|
|
164
|
+
]);
|
|
165
|
+
});
|
|
166
|
+
|
|
71
167
|
it("POSTs to the api URL directly", async () => {
|
|
72
168
|
mockFetch.mockResolvedValueOnce(sseResponse(sseBody("")));
|
|
73
169
|
|
|
@@ -378,7 +474,7 @@ describe("createAdkStream - direct mode", () => {
|
|
|
378
474
|
});
|
|
379
475
|
});
|
|
380
476
|
|
|
381
|
-
it("
|
|
477
|
+
it("wraps non-JSON tool content in a function response object", async () => {
|
|
382
478
|
mockFetch.mockResolvedValueOnce(sseResponse(sseBody("")));
|
|
383
479
|
|
|
384
480
|
const stream = createAdkStream({
|
|
@@ -401,9 +497,50 @@ describe("createAdkStream - direct mode", () => {
|
|
|
401
497
|
}
|
|
402
498
|
|
|
403
499
|
const body = JSON.parse(mockFetch.mock.calls[0]![1]?.body as string);
|
|
404
|
-
expect(body.newMessage.parts[0].functionResponse.response).
|
|
500
|
+
expect(body.newMessage.parts[0].functionResponse.response).toEqual({
|
|
501
|
+
result: "not-json",
|
|
502
|
+
});
|
|
405
503
|
});
|
|
406
504
|
|
|
505
|
+
it.each([
|
|
506
|
+
["false", { result: false }],
|
|
507
|
+
["0", { result: 0 }],
|
|
508
|
+
["null", { result: null }],
|
|
509
|
+
['"done"', { result: "done" }],
|
|
510
|
+
["[1,2]", { results: [1, 2] }],
|
|
511
|
+
])(
|
|
512
|
+
"wraps scalar or array tool result %s in direct mode",
|
|
513
|
+
async (content, response) => {
|
|
514
|
+
mockFetch.mockResolvedValueOnce(sseResponse(sseBody("")));
|
|
515
|
+
|
|
516
|
+
const stream = createAdkStream({
|
|
517
|
+
api: "http://localhost:8000",
|
|
518
|
+
appName: "app",
|
|
519
|
+
userId: "u",
|
|
520
|
+
});
|
|
521
|
+
const gen = await stream(
|
|
522
|
+
[
|
|
523
|
+
{
|
|
524
|
+
id: "t1",
|
|
525
|
+
type: "tool",
|
|
526
|
+
content,
|
|
527
|
+
tool_call_id: "tc-1",
|
|
528
|
+
name: "search",
|
|
529
|
+
},
|
|
530
|
+
],
|
|
531
|
+
makeConfig(),
|
|
532
|
+
);
|
|
533
|
+
for await (const _ of gen) {
|
|
534
|
+
/* noop */
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const body = JSON.parse(mockFetch.mock.calls[0]![1]?.body as string);
|
|
538
|
+
expect(body.newMessage.parts[0].functionResponse.response).toEqual(
|
|
539
|
+
response,
|
|
540
|
+
);
|
|
541
|
+
},
|
|
542
|
+
);
|
|
543
|
+
|
|
407
544
|
it("sends empty text part when no messages provided", async () => {
|
|
408
545
|
mockFetch.mockResolvedValueOnce(sseResponse(sseBody("")));
|
|
409
546
|
|
|
@@ -472,6 +609,59 @@ describe("createAdkStream - SSE parsing", () => {
|
|
|
472
609
|
},
|
|
473
610
|
);
|
|
474
611
|
|
|
612
|
+
it.each([
|
|
613
|
+
[{ content: [] }, "content", "an object"],
|
|
614
|
+
[{ content: { parts: 42 } }, "content.parts", "an array of objects"],
|
|
615
|
+
[{ content: { parts: [null] } }, "content.parts", "an array of objects"],
|
|
616
|
+
])(
|
|
617
|
+
"rejects malformed nested stream event content: %#",
|
|
618
|
+
async (event, field, expectation) => {
|
|
619
|
+
mockFetch.mockResolvedValueOnce(
|
|
620
|
+
sseResponse(sseBody(`data: ${JSON.stringify(event)}\n\n`)),
|
|
621
|
+
);
|
|
622
|
+
|
|
623
|
+
const stream = createAdkStream({ api: "/api/adk" });
|
|
624
|
+
const consume = async () => {
|
|
625
|
+
const gen = await stream(
|
|
626
|
+
[{ id: "m1", type: "human", content: "Hi" }],
|
|
627
|
+
makeConfig(),
|
|
628
|
+
);
|
|
629
|
+
for await (const _event of gen) {
|
|
630
|
+
void _event;
|
|
631
|
+
}
|
|
632
|
+
};
|
|
633
|
+
|
|
634
|
+
await expect(consume()).rejects.toThrow(
|
|
635
|
+
`Invalid ADK stream event: expected "${field}" to be ${expectation} when present.`,
|
|
636
|
+
);
|
|
637
|
+
},
|
|
638
|
+
);
|
|
639
|
+
|
|
640
|
+
it.each([
|
|
641
|
+
[{ id: "e1", content: null }, undefined],
|
|
642
|
+
[{ id: "e1", content: { role: "model", parts: null } }, { role: "model" }],
|
|
643
|
+
])(
|
|
644
|
+
"accepts null optional nested stream event content: %#",
|
|
645
|
+
async (event, expectedContent) => {
|
|
646
|
+
mockFetch.mockResolvedValueOnce(
|
|
647
|
+
sseResponse(sseBody(`data: ${JSON.stringify(event)}\n\n`)),
|
|
648
|
+
);
|
|
649
|
+
|
|
650
|
+
const stream = createAdkStream({ api: "/api/adk" });
|
|
651
|
+
const gen = await stream(
|
|
652
|
+
[{ id: "m1", type: "human", content: "Hi" }],
|
|
653
|
+
makeConfig(),
|
|
654
|
+
);
|
|
655
|
+
const collected: AdkEvent[] = [];
|
|
656
|
+
for await (const parsedEvent of gen) {
|
|
657
|
+
collected.push(parsedEvent);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
expect(collected).toHaveLength(1);
|
|
661
|
+
expect(collected[0]!.content).toEqual(expectedContent);
|
|
662
|
+
},
|
|
663
|
+
);
|
|
664
|
+
|
|
475
665
|
it("reports invalid JSON as an ADK stream event error", async () => {
|
|
476
666
|
mockFetch.mockResolvedValueOnce(sseResponse(sseBody("data: not-json\n\n")));
|
|
477
667
|
|
package/src/AdkClient.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { SSEEventDecoder } from "assistant-stream/utils";
|
|
2
2
|
import { contentToParts } from "./contentToParts";
|
|
3
3
|
import { parseAdkEventValue } from "./parseAdkEvent";
|
|
4
|
+
import { toAdkFunctionResponse } from "./toAdkFunctionResponse";
|
|
4
5
|
import { trimTrailingSlashes } from "./trimTrailingSlashes";
|
|
5
6
|
import type {
|
|
6
7
|
AdkEvent,
|
|
@@ -180,7 +181,7 @@ function messagesToContent(messages: AdkMessage[]): {
|
|
|
180
181
|
functionResponse: {
|
|
181
182
|
name: msg.name,
|
|
182
183
|
id: msg.tool_call_id,
|
|
183
|
-
response,
|
|
184
|
+
response: toAdkFunctionResponse(response, msg.status === "error"),
|
|
184
185
|
},
|
|
185
186
|
});
|
|
186
187
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
2
|
import { AdkEventAccumulator } from "./AdkEventAccumulator";
|
|
3
|
+
import { parseAdkEventValue } from "./parseAdkEvent";
|
|
3
4
|
import type { AdkEvent, AdkMessage, AdkMessageContentPart } from "./types";
|
|
4
5
|
|
|
5
6
|
const makeEvent = (overrides: Partial<AdkEvent> = {}): AdkEvent => ({
|
|
@@ -18,6 +19,41 @@ const makeTextEvent = (
|
|
|
18
19
|
content: { role: "model", parts: [{ text }] },
|
|
19
20
|
});
|
|
20
21
|
|
|
22
|
+
describe.each(["user", "agent"])("%s function response status", (author) => {
|
|
23
|
+
it.each([
|
|
24
|
+
[{ error: "denied" }, "error"],
|
|
25
|
+
[{ error: { message: "denied" }, output: "partial" }, "error"],
|
|
26
|
+
[{ error: null }, "error"],
|
|
27
|
+
[{ error: false }, "error"],
|
|
28
|
+
[{ error: "" }, "error"],
|
|
29
|
+
[{ output: { error: "application data" } }, "success"],
|
|
30
|
+
[{ result: "done" }, "success"],
|
|
31
|
+
[{}, "success"],
|
|
32
|
+
] as const)("classifies response %j as %s", (response, status) => {
|
|
33
|
+
const acc = new AdkEventAccumulator();
|
|
34
|
+
const messages = acc.processEvent(
|
|
35
|
+
makeEvent({
|
|
36
|
+
author,
|
|
37
|
+
content: {
|
|
38
|
+
parts: [
|
|
39
|
+
{
|
|
40
|
+
functionResponse: { id: "tc-1", name: "search", response },
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
}),
|
|
45
|
+
);
|
|
46
|
+
expect(messages).toMatchObject([
|
|
47
|
+
{
|
|
48
|
+
type: "tool",
|
|
49
|
+
tool_call_id: "tc-1",
|
|
50
|
+
status,
|
|
51
|
+
content: JSON.stringify(response),
|
|
52
|
+
},
|
|
53
|
+
]);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
21
57
|
describe("AdkEventAccumulator - text handling", () => {
|
|
22
58
|
it("accumulates a single non-partial text event into an AI message", () => {
|
|
23
59
|
const acc = new AdkEventAccumulator();
|
|
@@ -838,6 +874,27 @@ describe("AdkEventAccumulator - actions tracking", () => {
|
|
|
838
874
|
expect(acc.getArtifactDelta()).toEqual({ "file.txt": 1 });
|
|
839
875
|
});
|
|
840
876
|
|
|
877
|
+
it("preserves prototype-named state and artifact keys", () => {
|
|
878
|
+
const acc = new AdkEventAccumulator();
|
|
879
|
+
acc.processEvent(
|
|
880
|
+
makeEvent({
|
|
881
|
+
actions: {
|
|
882
|
+
stateDelta: JSON.parse('{"__proto__":"session"}'),
|
|
883
|
+
artifactDelta: JSON.parse('{"__proto__":1}'),
|
|
884
|
+
},
|
|
885
|
+
author: "agent",
|
|
886
|
+
content: { parts: [{ text: "x" }] },
|
|
887
|
+
}),
|
|
888
|
+
);
|
|
889
|
+
|
|
890
|
+
const stateDelta = acc.getStateDelta();
|
|
891
|
+
const artifactDelta = acc.getArtifactDelta();
|
|
892
|
+
expect(Object.hasOwn(stateDelta, "__proto__")).toBe(true);
|
|
893
|
+
expect(stateDelta["__proto__"]).toBe("session");
|
|
894
|
+
expect(Object.hasOwn(artifactDelta, "__proto__")).toBe(true);
|
|
895
|
+
expect(artifactDelta["__proto__"]).toBe(1);
|
|
896
|
+
});
|
|
897
|
+
|
|
841
898
|
it("tracks escalation flag", () => {
|
|
842
899
|
const acc = new AdkEventAccumulator();
|
|
843
900
|
expect(acc.isEscalated()).toBe(false);
|
|
@@ -912,6 +969,99 @@ describe("AdkEventAccumulator - actions tracking", () => {
|
|
|
912
969
|
);
|
|
913
970
|
expect(acc.getLongRunningToolIds()).toEqual(["lrt-1"]);
|
|
914
971
|
});
|
|
972
|
+
|
|
973
|
+
it("settles a long-running id when a user-authored response answers it", () => {
|
|
974
|
+
const acc = new AdkEventAccumulator();
|
|
975
|
+
acc.processEvent(
|
|
976
|
+
makeEvent({
|
|
977
|
+
author: "agent",
|
|
978
|
+
longRunningToolIds: ["lrt-1", "lrt-2"],
|
|
979
|
+
content: {
|
|
980
|
+
parts: [
|
|
981
|
+
{
|
|
982
|
+
functionCall: { name: "ask_for_approval", id: "lrt-1", args: {} },
|
|
983
|
+
},
|
|
984
|
+
{
|
|
985
|
+
functionCall: { name: "ask_for_approval", id: "lrt-2", args: {} },
|
|
986
|
+
},
|
|
987
|
+
],
|
|
988
|
+
},
|
|
989
|
+
}),
|
|
990
|
+
);
|
|
991
|
+
acc.processEvent(
|
|
992
|
+
makeEvent({
|
|
993
|
+
author: "user",
|
|
994
|
+
content: {
|
|
995
|
+
parts: [
|
|
996
|
+
{
|
|
997
|
+
functionResponse: {
|
|
998
|
+
name: "ask_for_approval",
|
|
999
|
+
id: "lrt-1",
|
|
1000
|
+
response: { status: "approved" },
|
|
1001
|
+
},
|
|
1002
|
+
},
|
|
1003
|
+
],
|
|
1004
|
+
},
|
|
1005
|
+
}),
|
|
1006
|
+
);
|
|
1007
|
+
expect(acc.getLongRunningToolIds()).toEqual(["lrt-2"]);
|
|
1008
|
+
});
|
|
1009
|
+
|
|
1010
|
+
it("keeps a long-running id pending through the interim response ADK authors", () => {
|
|
1011
|
+
const acc = new AdkEventAccumulator();
|
|
1012
|
+
acc.processEvent(
|
|
1013
|
+
makeEvent({
|
|
1014
|
+
author: "agent",
|
|
1015
|
+
longRunningToolIds: ["lrt-1"],
|
|
1016
|
+
content: {
|
|
1017
|
+
parts: [
|
|
1018
|
+
{
|
|
1019
|
+
functionCall: { name: "ask_for_approval", id: "lrt-1", args: {} },
|
|
1020
|
+
},
|
|
1021
|
+
],
|
|
1022
|
+
},
|
|
1023
|
+
}),
|
|
1024
|
+
);
|
|
1025
|
+
acc.processEvent(
|
|
1026
|
+
makeEvent({
|
|
1027
|
+
author: "agent",
|
|
1028
|
+
content: {
|
|
1029
|
+
parts: [
|
|
1030
|
+
{
|
|
1031
|
+
functionResponse: {
|
|
1032
|
+
name: "ask_for_approval",
|
|
1033
|
+
id: "lrt-1",
|
|
1034
|
+
response: { status: "pending" },
|
|
1035
|
+
},
|
|
1036
|
+
},
|
|
1037
|
+
],
|
|
1038
|
+
},
|
|
1039
|
+
}),
|
|
1040
|
+
);
|
|
1041
|
+
expect(acc.getLongRunningToolIds()).toEqual(["lrt-1"]);
|
|
1042
|
+
});
|
|
1043
|
+
|
|
1044
|
+
it("seeds and settles longRunningToolIds from a user response", () => {
|
|
1045
|
+
const acc = new AdkEventAccumulator([], ["lrt-1", "lrt-2"]);
|
|
1046
|
+
acc.processEvent(
|
|
1047
|
+
makeEvent({
|
|
1048
|
+
author: "user",
|
|
1049
|
+
content: {
|
|
1050
|
+
role: "user",
|
|
1051
|
+
parts: [
|
|
1052
|
+
{
|
|
1053
|
+
functionResponse: {
|
|
1054
|
+
id: "lrt-1",
|
|
1055
|
+
name: "adk_request_confirmation",
|
|
1056
|
+
response: { confirmed: true },
|
|
1057
|
+
},
|
|
1058
|
+
},
|
|
1059
|
+
],
|
|
1060
|
+
},
|
|
1061
|
+
}),
|
|
1062
|
+
);
|
|
1063
|
+
expect(acc.getLongRunningToolIds()).toEqual(["lrt-2"]);
|
|
1064
|
+
});
|
|
915
1065
|
});
|
|
916
1066
|
|
|
917
1067
|
describe("AdkEventAccumulator - special function calls", () => {
|
|
@@ -1023,6 +1173,153 @@ describe("AdkEventAccumulator - author/agent tracking", () => {
|
|
|
1023
1173
|
});
|
|
1024
1174
|
|
|
1025
1175
|
describe("AdkEventAccumulator - snake_case normalization", () => {
|
|
1176
|
+
describe.each(["user", "agent"])("%s media", (author) => {
|
|
1177
|
+
it.each([
|
|
1178
|
+
{
|
|
1179
|
+
part: { inline_data: { mime_type: "image/png", data: "aGVsbG8=" } },
|
|
1180
|
+
expected: { type: "image", mimeType: "image/png", data: "aGVsbG8=" },
|
|
1181
|
+
},
|
|
1182
|
+
{
|
|
1183
|
+
part: {
|
|
1184
|
+
inline_data: { mime_type: "application/pdf", data: "aGVsbG8=" },
|
|
1185
|
+
},
|
|
1186
|
+
expected: {
|
|
1187
|
+
type: "file",
|
|
1188
|
+
mimeType: "application/pdf",
|
|
1189
|
+
data: "aGVsbG8=",
|
|
1190
|
+
},
|
|
1191
|
+
},
|
|
1192
|
+
{
|
|
1193
|
+
part: {
|
|
1194
|
+
file_data: {
|
|
1195
|
+
mime_type: "image/png",
|
|
1196
|
+
file_uri: "https://example.test/image.png",
|
|
1197
|
+
},
|
|
1198
|
+
},
|
|
1199
|
+
expected: { type: "image_url", url: "https://example.test/image.png" },
|
|
1200
|
+
},
|
|
1201
|
+
{
|
|
1202
|
+
part: {
|
|
1203
|
+
file_data: {
|
|
1204
|
+
mime_type: "application/pdf",
|
|
1205
|
+
file_uri: "https://example.test/report.pdf",
|
|
1206
|
+
},
|
|
1207
|
+
},
|
|
1208
|
+
expected: {
|
|
1209
|
+
type: "file_url",
|
|
1210
|
+
mimeType: "application/pdf",
|
|
1211
|
+
url: "https://example.test/report.pdf",
|
|
1212
|
+
},
|
|
1213
|
+
},
|
|
1214
|
+
{
|
|
1215
|
+
part: { inlineData: { mime_type: "image/png", data: "aGVsbG8=" } },
|
|
1216
|
+
expected: { type: "image", mimeType: "image/png", data: "aGVsbG8=" },
|
|
1217
|
+
},
|
|
1218
|
+
{
|
|
1219
|
+
part: {
|
|
1220
|
+
fileData: {
|
|
1221
|
+
mime_type: "application/pdf",
|
|
1222
|
+
file_uri: "https://example.test/report.pdf",
|
|
1223
|
+
},
|
|
1224
|
+
},
|
|
1225
|
+
expected: {
|
|
1226
|
+
type: "file_url",
|
|
1227
|
+
mimeType: "application/pdf",
|
|
1228
|
+
url: "https://example.test/report.pdf",
|
|
1229
|
+
},
|
|
1230
|
+
},
|
|
1231
|
+
{
|
|
1232
|
+
part: { inline_data: { mimeType: "image/png", data: "aGVsbG8=" } },
|
|
1233
|
+
expected: { type: "image", mimeType: "image/png", data: "aGVsbG8=" },
|
|
1234
|
+
},
|
|
1235
|
+
{
|
|
1236
|
+
part: {
|
|
1237
|
+
file_data: {
|
|
1238
|
+
mimeType: "application/pdf",
|
|
1239
|
+
fileUri: "https://example.test/report.pdf",
|
|
1240
|
+
},
|
|
1241
|
+
},
|
|
1242
|
+
expected: {
|
|
1243
|
+
type: "file_url",
|
|
1244
|
+
mimeType: "application/pdf",
|
|
1245
|
+
url: "https://example.test/report.pdf",
|
|
1246
|
+
},
|
|
1247
|
+
},
|
|
1248
|
+
])(
|
|
1249
|
+
"normalizes media aliases without mutating the event: $part",
|
|
1250
|
+
({ part, expected }) => {
|
|
1251
|
+
const input = { id: "media", author, content: { parts: [part] } };
|
|
1252
|
+
const original = structuredClone(input);
|
|
1253
|
+
const event = parseAdkEventValue(input, "test");
|
|
1254
|
+
const messages = new AdkEventAccumulator().processEvent(event);
|
|
1255
|
+
expect(messages).toMatchObject([
|
|
1256
|
+
{ type: author === "user" ? "human" : "ai", content: [expected] },
|
|
1257
|
+
]);
|
|
1258
|
+
expect(input).toEqual(original);
|
|
1259
|
+
},
|
|
1260
|
+
);
|
|
1261
|
+
});
|
|
1262
|
+
|
|
1263
|
+
it("prefers camelCase media containers and nested values when both exist", () => {
|
|
1264
|
+
const event = parseAdkEventValue(
|
|
1265
|
+
{
|
|
1266
|
+
id: "media",
|
|
1267
|
+
author: "agent",
|
|
1268
|
+
content: {
|
|
1269
|
+
parts: [
|
|
1270
|
+
{
|
|
1271
|
+
inlineData: {
|
|
1272
|
+
mimeType: "image/png",
|
|
1273
|
+
mime_type: "application/pdf",
|
|
1274
|
+
data: "aGVsbG8=",
|
|
1275
|
+
},
|
|
1276
|
+
inline_data: { mime_type: "text/plain", data: "wrong" },
|
|
1277
|
+
},
|
|
1278
|
+
{
|
|
1279
|
+
fileData: {
|
|
1280
|
+
mimeType: "application/pdf",
|
|
1281
|
+
mime_type: "image/png",
|
|
1282
|
+
fileUri: "https://example.test/right.pdf",
|
|
1283
|
+
file_uri: "https://example.test/wrong.png",
|
|
1284
|
+
},
|
|
1285
|
+
file_data: { file_uri: "https://example.test/other.png" },
|
|
1286
|
+
},
|
|
1287
|
+
],
|
|
1288
|
+
},
|
|
1289
|
+
},
|
|
1290
|
+
"test",
|
|
1291
|
+
);
|
|
1292
|
+
expect(new AdkEventAccumulator().processEvent(event)).toMatchObject([
|
|
1293
|
+
{
|
|
1294
|
+
content: [
|
|
1295
|
+
{ type: "image", mimeType: "image/png", data: "aGVsbG8=" },
|
|
1296
|
+
{
|
|
1297
|
+
type: "file_url",
|
|
1298
|
+
mimeType: "application/pdf",
|
|
1299
|
+
url: "https://example.test/right.pdf",
|
|
1300
|
+
},
|
|
1301
|
+
],
|
|
1302
|
+
},
|
|
1303
|
+
]);
|
|
1304
|
+
});
|
|
1305
|
+
|
|
1306
|
+
it("leaves similarly named tool arguments untouched", () => {
|
|
1307
|
+
const args = { inline_data: { mime_type: "custom", file_uri: "opaque" } };
|
|
1308
|
+
const event = parseAdkEventValue(
|
|
1309
|
+
{
|
|
1310
|
+
id: "call",
|
|
1311
|
+
author: "agent",
|
|
1312
|
+
content: {
|
|
1313
|
+
parts: [{ function_call: { name: "test", id: "tc-1", args } }],
|
|
1314
|
+
},
|
|
1315
|
+
},
|
|
1316
|
+
"test",
|
|
1317
|
+
);
|
|
1318
|
+
expect(new AdkEventAccumulator().processEvent(event)).toMatchObject([
|
|
1319
|
+
{ tool_calls: [{ args }] },
|
|
1320
|
+
]);
|
|
1321
|
+
});
|
|
1322
|
+
|
|
1026
1323
|
it("normalizes function_call to functionCall in parts", () => {
|
|
1027
1324
|
const acc = new AdkEventAccumulator();
|
|
1028
1325
|
const msgs = acc.processEvent(
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { generateId } from "@assistant-ui/core";
|
|
2
|
+
import { isRecord } from "@assistant-ui/core/internal";
|
|
2
3
|
import type { MessageStatus } from "@assistant-ui/core";
|
|
3
4
|
import type {
|
|
4
5
|
AdkEvent,
|
|
@@ -11,6 +12,7 @@ import type {
|
|
|
11
12
|
AdkMessageMetadata,
|
|
12
13
|
} from "./types";
|
|
13
14
|
import type { ReadonlyJSONObject } from "assistant-stream/utils";
|
|
15
|
+
import { isAdkFunctionError } from "./toAdkFunctionResponse";
|
|
14
16
|
|
|
15
17
|
type InProgressMessage = AdkMessage & { type: "ai" };
|
|
16
18
|
|
|
@@ -125,6 +127,21 @@ const normalizeEventPart = (part: AdkEventPart): AdkEventPart => {
|
|
|
125
127
|
if ("inline_data" in p && !("inlineData" in p))
|
|
126
128
|
result.inlineData = p.inline_data;
|
|
127
129
|
if ("file_data" in p && !("fileData" in p)) result.fileData = p.file_data;
|
|
130
|
+
if (isRecord(result.inlineData)) {
|
|
131
|
+
const data = result.inlineData;
|
|
132
|
+
if ("mime_type" in data && !("mimeType" in data))
|
|
133
|
+
result.inlineData = { ...data, mimeType: data.mime_type };
|
|
134
|
+
}
|
|
135
|
+
if (isRecord(result.fileData)) {
|
|
136
|
+
const data = result.fileData;
|
|
137
|
+
result.fileData = {
|
|
138
|
+
...data,
|
|
139
|
+
...("mime_type" in data &&
|
|
140
|
+
!("mimeType" in data) && { mimeType: data.mime_type }),
|
|
141
|
+
...("file_uri" in data &&
|
|
142
|
+
!("fileUri" in data) && { fileUri: data.file_uri }),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
128
145
|
if ("executable_code" in p && !("executableCode" in p))
|
|
129
146
|
result.executableCode = p.executable_code;
|
|
130
147
|
if ("code_execution_result" in p && !("codeExecutionResult" in p))
|
|
@@ -194,8 +211,9 @@ export class AdkEventAccumulator {
|
|
|
194
211
|
private finalTextReplacedThisEvent = false;
|
|
195
212
|
private finalReasoningReplacedThisEvent = false;
|
|
196
213
|
private partialReasoningBuffer = "";
|
|
197
|
-
private accumulatedStateDelta: Record<string, unknown> =
|
|
198
|
-
private accumulatedArtifactDelta: Record<string, number> =
|
|
214
|
+
private accumulatedStateDelta: Record<string, unknown> = Object.create(null);
|
|
215
|
+
private accumulatedArtifactDelta: Record<string, number> =
|
|
216
|
+
Object.create(null);
|
|
199
217
|
private lastAgentInfo: {
|
|
200
218
|
name?: string | undefined;
|
|
201
219
|
branch?: string | undefined;
|
|
@@ -209,12 +227,18 @@ export class AdkEventAccumulator {
|
|
|
209
227
|
// How many assistant messages each event has opened, so a replay of that
|
|
210
228
|
// event opens them with the same ids.
|
|
211
229
|
private aiMessageOrdinals = new Map<string, number>();
|
|
212
|
-
constructor(
|
|
230
|
+
constructor(
|
|
231
|
+
initialMessages?: AdkMessage[],
|
|
232
|
+
initialLongRunningToolIds?: readonly string[],
|
|
233
|
+
) {
|
|
213
234
|
if (initialMessages) {
|
|
214
235
|
for (const msg of initialMessages) {
|
|
215
236
|
this.messagesMap.set(msg.id, msg);
|
|
216
237
|
}
|
|
217
238
|
}
|
|
239
|
+
if (initialLongRunningToolIds) {
|
|
240
|
+
this.pendingLongRunningToolIds = new Set(initialLongRunningToolIds);
|
|
241
|
+
}
|
|
218
242
|
}
|
|
219
243
|
|
|
220
244
|
processEvent(rawEvent: AdkEvent): AdkMessage[] {
|
|
@@ -358,8 +382,12 @@ export class AdkEventAccumulator {
|
|
|
358
382
|
tool_call_id: part.functionResponse.id,
|
|
359
383
|
name: part.functionResponse.name,
|
|
360
384
|
content: JSON.stringify(part.functionResponse.response),
|
|
361
|
-
status:
|
|
385
|
+
status: isAdkFunctionError(part.functionResponse.response)
|
|
386
|
+
? "error"
|
|
387
|
+
: "success",
|
|
362
388
|
});
|
|
389
|
+
// Only a user-authored response settles a long-running call; the response ADK authors for one is the tool's interim result.
|
|
390
|
+
this.pendingLongRunningToolIds.delete(part.functionResponse.id);
|
|
363
391
|
}
|
|
364
392
|
}
|
|
365
393
|
// The replies answer the preceding assistant turn, so they are emitted
|
|
@@ -540,7 +568,9 @@ export class AdkEventAccumulator {
|
|
|
540
568
|
tool_call_id: part.functionResponse.id ?? "",
|
|
541
569
|
name: part.functionResponse.name,
|
|
542
570
|
content: JSON.stringify(part.functionResponse.response),
|
|
543
|
-
status:
|
|
571
|
+
status: isAdkFunctionError(part.functionResponse.response)
|
|
572
|
+
? "error"
|
|
573
|
+
: "success",
|
|
544
574
|
};
|
|
545
575
|
this.messagesMap.set(toolMsg.id, toolMsg);
|
|
546
576
|
return;
|