@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 { 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();
|
|
@@ -933,6 +969,99 @@ describe("AdkEventAccumulator - actions tracking", () => {
|
|
|
933
969
|
);
|
|
934
970
|
expect(acc.getLongRunningToolIds()).toEqual(["lrt-1"]);
|
|
935
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
|
+
});
|
|
936
1065
|
});
|
|
937
1066
|
|
|
938
1067
|
describe("AdkEventAccumulator - special function calls", () => {
|
|
@@ -1044,6 +1173,153 @@ describe("AdkEventAccumulator - author/agent tracking", () => {
|
|
|
1044
1173
|
});
|
|
1045
1174
|
|
|
1046
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
|
+
|
|
1047
1323
|
it("normalizes function_call to functionCall in parts", () => {
|
|
1048
1324
|
const acc = new AdkEventAccumulator();
|
|
1049
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))
|
|
@@ -210,12 +227,18 @@ export class AdkEventAccumulator {
|
|
|
210
227
|
// How many assistant messages each event has opened, so a replay of that
|
|
211
228
|
// event opens them with the same ids.
|
|
212
229
|
private aiMessageOrdinals = new Map<string, number>();
|
|
213
|
-
constructor(
|
|
230
|
+
constructor(
|
|
231
|
+
initialMessages?: AdkMessage[],
|
|
232
|
+
initialLongRunningToolIds?: readonly string[],
|
|
233
|
+
) {
|
|
214
234
|
if (initialMessages) {
|
|
215
235
|
for (const msg of initialMessages) {
|
|
216
236
|
this.messagesMap.set(msg.id, msg);
|
|
217
237
|
}
|
|
218
238
|
}
|
|
239
|
+
if (initialLongRunningToolIds) {
|
|
240
|
+
this.pendingLongRunningToolIds = new Set(initialLongRunningToolIds);
|
|
241
|
+
}
|
|
219
242
|
}
|
|
220
243
|
|
|
221
244
|
processEvent(rawEvent: AdkEvent): AdkMessage[] {
|
|
@@ -359,8 +382,12 @@ export class AdkEventAccumulator {
|
|
|
359
382
|
tool_call_id: part.functionResponse.id,
|
|
360
383
|
name: part.functionResponse.name,
|
|
361
384
|
content: JSON.stringify(part.functionResponse.response),
|
|
362
|
-
status:
|
|
385
|
+
status: isAdkFunctionError(part.functionResponse.response)
|
|
386
|
+
? "error"
|
|
387
|
+
: "success",
|
|
363
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);
|
|
364
391
|
}
|
|
365
392
|
}
|
|
366
393
|
// The replies answer the preceding assistant turn, so they are emitted
|
|
@@ -541,7 +568,9 @@ export class AdkEventAccumulator {
|
|
|
541
568
|
tool_call_id: part.functionResponse.id ?? "",
|
|
542
569
|
name: part.functionResponse.name,
|
|
543
570
|
content: JSON.stringify(part.functionResponse.response),
|
|
544
|
-
status:
|
|
571
|
+
status: isAdkFunctionError(part.functionResponse.response)
|
|
572
|
+
? "error"
|
|
573
|
+
: "success",
|
|
545
574
|
};
|
|
546
575
|
this.messagesMap.set(toolMsg.id, toolMsg);
|
|
547
576
|
return;
|
|
@@ -294,6 +294,87 @@ describe("createAdkSessionAdapter - fetch", () => {
|
|
|
294
294
|
// ── load() ──
|
|
295
295
|
|
|
296
296
|
describe("createAdkSessionAdapter - load", () => {
|
|
297
|
+
it("restores tool failures from stored function responses", async () => {
|
|
298
|
+
mockFetch.mockResolvedValueOnce(
|
|
299
|
+
new Response(
|
|
300
|
+
JSON.stringify({
|
|
301
|
+
id: "s1",
|
|
302
|
+
events: [
|
|
303
|
+
{
|
|
304
|
+
id: "failed",
|
|
305
|
+
author: "user",
|
|
306
|
+
content: {
|
|
307
|
+
parts: [
|
|
308
|
+
{
|
|
309
|
+
functionResponse: {
|
|
310
|
+
id: "tc-1",
|
|
311
|
+
name: "search",
|
|
312
|
+
response: { error: "denied" },
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
],
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
],
|
|
319
|
+
}),
|
|
320
|
+
{ status: 200 },
|
|
321
|
+
),
|
|
322
|
+
);
|
|
323
|
+
const { load } = createAdkSessionAdapter(baseOptions);
|
|
324
|
+
const result = await load("s1");
|
|
325
|
+
expect(result.messages).toMatchObject([
|
|
326
|
+
{
|
|
327
|
+
type: "tool",
|
|
328
|
+
tool_call_id: "tc-1",
|
|
329
|
+
status: "error",
|
|
330
|
+
content: JSON.stringify({ error: "denied" }),
|
|
331
|
+
},
|
|
332
|
+
]);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it("restores snake_case image and file parts from session history", async () => {
|
|
336
|
+
mockFetch.mockResolvedValueOnce(
|
|
337
|
+
new Response(
|
|
338
|
+
JSON.stringify({
|
|
339
|
+
id: "s1",
|
|
340
|
+
events: [
|
|
341
|
+
{
|
|
342
|
+
id: "media",
|
|
343
|
+
author: "user",
|
|
344
|
+
content: {
|
|
345
|
+
parts: [
|
|
346
|
+
{ inline_data: { mime_type: "image/png", data: "aGVsbG8=" } },
|
|
347
|
+
{
|
|
348
|
+
file_data: {
|
|
349
|
+
mime_type: "application/pdf",
|
|
350
|
+
file_uri: "https://example.test/report.pdf",
|
|
351
|
+
},
|
|
352
|
+
},
|
|
353
|
+
],
|
|
354
|
+
},
|
|
355
|
+
},
|
|
356
|
+
],
|
|
357
|
+
}),
|
|
358
|
+
{ status: 200 },
|
|
359
|
+
),
|
|
360
|
+
);
|
|
361
|
+
const { load } = createAdkSessionAdapter(baseOptions);
|
|
362
|
+
const result = await load("s1");
|
|
363
|
+
expect(result.messages).toMatchObject([
|
|
364
|
+
{
|
|
365
|
+
type: "human",
|
|
366
|
+
content: [
|
|
367
|
+
{ type: "image", mimeType: "image/png", data: "aGVsbG8=" },
|
|
368
|
+
{
|
|
369
|
+
type: "file_url",
|
|
370
|
+
mimeType: "application/pdf",
|
|
371
|
+
url: "https://example.test/report.pdf",
|
|
372
|
+
},
|
|
373
|
+
],
|
|
374
|
+
},
|
|
375
|
+
]);
|
|
376
|
+
});
|
|
377
|
+
|
|
297
378
|
it("returns the per-turn state the events imply, not just the messages", async () => {
|
|
298
379
|
const session = {
|
|
299
380
|
id: "s1",
|
|
@@ -543,10 +624,17 @@ describe("createAdkSessionAdapter - load replays tool confirmations", () => {
|
|
|
543
624
|
const result = await load("s1");
|
|
544
625
|
return {
|
|
545
626
|
messages: result.messages,
|
|
627
|
+
longRunningToolIds: result.longRunningToolIds,
|
|
546
628
|
approvals: projectAdkToolApprovals(result.messages).approvals,
|
|
547
629
|
};
|
|
548
630
|
};
|
|
549
631
|
|
|
632
|
+
it("settles an answered long-running tool on replay", async () => {
|
|
633
|
+
const { longRunningToolIds } = await loadApprovals({ confirmed: true });
|
|
634
|
+
|
|
635
|
+
expect(longRunningToolIds).toEqual([]);
|
|
636
|
+
});
|
|
637
|
+
|
|
550
638
|
it("keeps a user-authored confirmation reply as a tool message", async () => {
|
|
551
639
|
const { messages } = await loadApprovals({ confirmed: true });
|
|
552
640
|
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
import { convertAdkMessage } from "./convertAdkMessages";
|
|
8
8
|
import type { AppendMessage } from "@assistant-ui/core";
|
|
9
9
|
import type { AdkMessage } from "./types";
|
|
10
|
+
import { contentToParts } from "./contentToParts";
|
|
10
11
|
|
|
11
12
|
const makeAppendMessage = (content: AppendMessage["content"]): AppendMessage =>
|
|
12
13
|
({
|
|
@@ -155,6 +156,75 @@ describe("getPendingCancellations", () => {
|
|
|
155
156
|
});
|
|
156
157
|
|
|
157
158
|
describe("getMessageContent", () => {
|
|
159
|
+
it("serializes data URL images as inline data", () => {
|
|
160
|
+
const content = getMessageContent(
|
|
161
|
+
makeAppendMessage([
|
|
162
|
+
{ type: "image", image: "data:image/png;base64,AAAA" },
|
|
163
|
+
]),
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
expect(contentToParts(content)).toEqual([
|
|
167
|
+
{ inlineData: { mimeType: "image/png", data: "AAAA" } },
|
|
168
|
+
]);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("infers an image MIME type when the data URL declares a generic type", () => {
|
|
172
|
+
const content = getMessageContent(
|
|
173
|
+
makeAppendMessage([
|
|
174
|
+
{
|
|
175
|
+
type: "image",
|
|
176
|
+
image: "data:application/octet-stream;base64,iVBORw0KGgo=",
|
|
177
|
+
},
|
|
178
|
+
]),
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
expect(contentToParts(content)).toEqual([
|
|
182
|
+
{ inlineData: { mimeType: "image/png", data: "iVBORw0KGgo=" } },
|
|
183
|
+
]);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("prefers an attachment's declared image MIME type", () => {
|
|
187
|
+
const message = makeAppendMessage([]);
|
|
188
|
+
const content = getMessageContent({
|
|
189
|
+
...message,
|
|
190
|
+
attachments: [
|
|
191
|
+
{
|
|
192
|
+
id: "attachment-1",
|
|
193
|
+
type: "image",
|
|
194
|
+
name: "photo.webp",
|
|
195
|
+
contentType: "image/webp",
|
|
196
|
+
status: { type: "complete" },
|
|
197
|
+
content: [{ type: "image", image: "data:image/png;base64,AAAA" }],
|
|
198
|
+
},
|
|
199
|
+
],
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
expect(contentToParts(content)).toEqual([
|
|
203
|
+
{ inlineData: { mimeType: "image/webp", data: "AAAA" } },
|
|
204
|
+
]);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("resolves wildcard attachment MIME types to a concrete image type", () => {
|
|
208
|
+
const message = makeAppendMessage([]);
|
|
209
|
+
const content = getMessageContent({
|
|
210
|
+
...message,
|
|
211
|
+
attachments: [
|
|
212
|
+
{
|
|
213
|
+
id: "attachment-1",
|
|
214
|
+
type: "image",
|
|
215
|
+
name: "photo.jpg",
|
|
216
|
+
contentType: "image/*",
|
|
217
|
+
status: { type: "complete" },
|
|
218
|
+
content: [{ type: "image", image: "data:image/jpeg;base64,AAAA" }],
|
|
219
|
+
},
|
|
220
|
+
],
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
expect(contentToParts(content)).toEqual([
|
|
224
|
+
{ inlineData: { mimeType: "image/jpeg", data: "AAAA" } },
|
|
225
|
+
]);
|
|
226
|
+
});
|
|
227
|
+
|
|
158
228
|
it("preserves file part data and mimeType end-to-end", () => {
|
|
159
229
|
const result = getMessageContent(
|
|
160
230
|
makeAppendMessage([
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
createToolCallCancellationStub,
|
|
9
9
|
parseDataUrl,
|
|
10
10
|
resolveFilePartSource,
|
|
11
|
+
resolveImageMediaType,
|
|
11
12
|
scanPendingToolCalls,
|
|
12
13
|
} from "@assistant-ui/core/internal";
|
|
13
14
|
import type { AdkMessage } from "./types";
|
|
@@ -15,16 +16,30 @@ import type { AdkMessage } from "./types";
|
|
|
15
16
|
/** Exported for unit tests. */
|
|
16
17
|
export const getMessageContent = (msg: AppendMessage) => {
|
|
17
18
|
const allContent = [
|
|
18
|
-
...msg.content,
|
|
19
|
-
...(msg.attachments?.flatMap((
|
|
19
|
+
...msg.content.map((part) => ({ part, contentType: undefined })),
|
|
20
|
+
...(msg.attachments?.flatMap((attachment) =>
|
|
21
|
+
attachment.content.map((part) => ({
|
|
22
|
+
part,
|
|
23
|
+
contentType: attachment.contentType,
|
|
24
|
+
})),
|
|
25
|
+
) ?? []),
|
|
20
26
|
];
|
|
21
|
-
const content = allContent.flatMap((part) => {
|
|
27
|
+
const content = allContent.flatMap(({ part, contentType }) => {
|
|
22
28
|
const type = part.type;
|
|
23
29
|
switch (type) {
|
|
24
30
|
case "text":
|
|
25
31
|
return { type: "text" as const, text: part.text };
|
|
26
|
-
case "image":
|
|
32
|
+
case "image": {
|
|
33
|
+
const parsed = parseDataUrl(part.image);
|
|
34
|
+
if (parsed) {
|
|
35
|
+
return {
|
|
36
|
+
type: "image" as const,
|
|
37
|
+
mimeType: resolveImageMediaType(part.image, contentType),
|
|
38
|
+
data: parsed.data,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
27
41
|
return { type: "image_url" as const, url: part.image };
|
|
42
|
+
}
|
|
28
43
|
case "file": {
|
|
29
44
|
const source = resolveFilePartSource(part);
|
|
30
45
|
if (source.kind === "url") {
|
|
@@ -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
|
});
|