@assistant-ui/react-google-adk 0.0.16 → 0.0.18
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 +0 -1
- package/dist/AdkClient.d.ts.map +1 -1
- package/dist/AdkClient.js +22 -10
- package/dist/AdkClient.js.map +1 -1
- package/dist/AdkEventAccumulator.d.ts +0 -1
- package/dist/AdkEventAccumulator.d.ts.map +1 -1
- package/dist/AdkSessionAdapter.d.ts +0 -1
- package/dist/AdkSessionAdapter.d.ts.map +1 -1
- package/dist/AdkSessionAdapter.js +59 -11
- package/dist/AdkSessionAdapter.js.map +1 -1
- package/dist/adkExtras.d.ts +0 -1
- package/dist/adkExtras.d.ts.map +1 -1
- package/dist/contentToParts.d.ts +0 -1
- package/dist/contentToParts.d.ts.map +1 -1
- package/dist/convertAdkMessages.d.ts +0 -1
- package/dist/convertAdkMessages.d.ts.map +1 -1
- package/dist/hooks.d.ts +0 -1
- package/dist/hooks.d.ts.map +1 -1
- package/dist/server/adkEventStream.d.ts.map +1 -1
- package/dist/server/createAdkApiRoute.d.ts +0 -1
- package/dist/server/createAdkApiRoute.d.ts.map +1 -1
- package/dist/server/parseAdkRequest.d.ts +0 -1
- package/dist/server/parseAdkRequest.d.ts.map +1 -1
- package/dist/server/parseAdkRequest.js +2 -2
- package/dist/server/parseAdkRequest.js.map +1 -1
- package/dist/structuredEvents.d.ts +0 -1
- package/dist/structuredEvents.d.ts.map +1 -1
- package/dist/trimTrailingSlashes.d.ts +5 -0
- package/dist/trimTrailingSlashes.d.ts.map +1 -0
- package/dist/trimTrailingSlashes.js +10 -0
- package/dist/trimTrailingSlashes.js.map +1 -0
- package/dist/types.d.ts +1 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/useAdkMessages.d.ts +1 -5
- package/dist/useAdkMessages.d.ts.map +1 -1
- package/dist/useAdkMessages.js +2 -1
- package/dist/useAdkMessages.js.map +1 -1
- package/dist/useAdkRuntime.d.ts +1 -9
- package/dist/useAdkRuntime.d.ts.map +1 -1
- package/dist/useAdkRuntime.js +2 -1
- package/dist/useAdkRuntime.js.map +1 -1
- package/package.json +7 -7
- package/src/AdkClient.test.ts +256 -32
- package/src/AdkClient.ts +41 -20
- package/src/AdkSessionAdapter.test.ts +225 -5
- package/src/AdkSessionAdapter.ts +151 -20
- package/src/server/parseAdkRequest.test.ts +4 -2
- package/src/server/parseAdkRequest.ts +4 -2
- package/src/trimTrailingSlashes.ts +5 -0
|
@@ -62,6 +62,30 @@ describe("createAdkSessionAdapter - list", () => {
|
|
|
62
62
|
expect(result.threads).toHaveLength(0);
|
|
63
63
|
});
|
|
64
64
|
|
|
65
|
+
it("rejects a successful response that is not a session array", async () => {
|
|
66
|
+
mockFetch.mockResolvedValueOnce(
|
|
67
|
+
new Response(JSON.stringify({}), { status: 200 }),
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const { adapter } = createAdkSessionAdapter(baseOptions);
|
|
71
|
+
|
|
72
|
+
await expect(adapter.list()).rejects.toThrow(
|
|
73
|
+
"Invalid ADK session list response: expected an array of sessions.",
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("rejects session list entries without a valid id", async () => {
|
|
78
|
+
mockFetch.mockResolvedValueOnce(
|
|
79
|
+
new Response(JSON.stringify([{ id: "s1" }, {}]), { status: 200 }),
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const { adapter } = createAdkSessionAdapter(baseOptions);
|
|
83
|
+
|
|
84
|
+
await expect(adapter.list()).rejects.toThrow(
|
|
85
|
+
'Invalid ADK session list response: session at index 1 must have a non-empty string "id".',
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
|
|
65
89
|
it("throws when response is not ok", async () => {
|
|
66
90
|
mockFetch.mockResolvedValueOnce(
|
|
67
91
|
new Response("Server error", { status: 500 }),
|
|
@@ -139,6 +163,18 @@ describe("createAdkSessionAdapter - initialize", () => {
|
|
|
139
163
|
"Failed to create session: 403",
|
|
140
164
|
);
|
|
141
165
|
});
|
|
166
|
+
|
|
167
|
+
it("rejects a successful response without a session id", async () => {
|
|
168
|
+
mockFetch.mockResolvedValueOnce(
|
|
169
|
+
new Response(JSON.stringify({}), { status: 200 }),
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
const { adapter } = createAdkSessionAdapter(baseOptions);
|
|
173
|
+
|
|
174
|
+
await expect(adapter.initialize("thread-1")).rejects.toThrow(
|
|
175
|
+
'Invalid ADK session create response: expected an object with a non-empty string "id".',
|
|
176
|
+
);
|
|
177
|
+
});
|
|
142
178
|
});
|
|
143
179
|
|
|
144
180
|
// ── adapter.delete() ──
|
|
@@ -210,10 +246,14 @@ describe("createAdkSessionAdapter - no-op methods", () => {
|
|
|
210
246
|
// ── adapter.generateTitle ──
|
|
211
247
|
|
|
212
248
|
describe("createAdkSessionAdapter - generateTitle", () => {
|
|
213
|
-
it("
|
|
249
|
+
it("closes its title stream when title generation is unsupported", async () => {
|
|
214
250
|
const { adapter } = createAdkSessionAdapter(baseOptions);
|
|
215
|
-
const
|
|
216
|
-
|
|
251
|
+
const stream = await adapter.generateTitle("s1", []);
|
|
252
|
+
|
|
253
|
+
await expect(stream.getReader().read()).resolves.toEqual({
|
|
254
|
+
done: true,
|
|
255
|
+
value: undefined,
|
|
256
|
+
});
|
|
217
257
|
});
|
|
218
258
|
});
|
|
219
259
|
|
|
@@ -246,6 +286,18 @@ describe("createAdkSessionAdapter - fetch", () => {
|
|
|
246
286
|
"Session not found: 404",
|
|
247
287
|
);
|
|
248
288
|
});
|
|
289
|
+
|
|
290
|
+
it("rejects a successful response without a session id", async () => {
|
|
291
|
+
mockFetch.mockResolvedValueOnce(
|
|
292
|
+
new Response(JSON.stringify({}), { status: 200 }),
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
const { adapter } = createAdkSessionAdapter(baseOptions);
|
|
296
|
+
|
|
297
|
+
await expect(adapter.fetch("s1")).rejects.toThrow(
|
|
298
|
+
'Invalid ADK session fetch response: expected an object with a non-empty string "id".',
|
|
299
|
+
);
|
|
300
|
+
});
|
|
249
301
|
});
|
|
250
302
|
|
|
251
303
|
// ── load() ──
|
|
@@ -301,6 +353,30 @@ describe("createAdkSessionAdapter - load", () => {
|
|
|
301
353
|
expect(result.messages).toEqual([]);
|
|
302
354
|
});
|
|
303
355
|
|
|
356
|
+
it("rejects a successful response without a session id", async () => {
|
|
357
|
+
mockFetch.mockResolvedValueOnce(
|
|
358
|
+
new Response(JSON.stringify({}), { status: 200 }),
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
const { load } = createAdkSessionAdapter(baseOptions);
|
|
362
|
+
|
|
363
|
+
await expect(load("s1")).rejects.toThrow(
|
|
364
|
+
'Invalid ADK session load response: expected an object with a non-empty string "id".',
|
|
365
|
+
);
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it("rejects a session with a malformed events field", async () => {
|
|
369
|
+
mockFetch.mockResolvedValueOnce(
|
|
370
|
+
new Response(JSON.stringify({ id: "s1", events: {} }), { status: 200 }),
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
const { load } = createAdkSessionAdapter(baseOptions);
|
|
374
|
+
|
|
375
|
+
await expect(load("s1")).rejects.toThrow(
|
|
376
|
+
'Invalid ADK session load response: expected "events" to be an array when present.',
|
|
377
|
+
);
|
|
378
|
+
});
|
|
379
|
+
|
|
304
380
|
it("throws when session fetch fails", async () => {
|
|
305
381
|
mockFetch.mockResolvedValueOnce(
|
|
306
382
|
new Response("Server error", { status: 500 }),
|
|
@@ -339,9 +415,153 @@ describe("createAdkSessionAdapter - load", () => {
|
|
|
339
415
|
});
|
|
340
416
|
});
|
|
341
417
|
|
|
342
|
-
|
|
418
|
+
describe("createAdkSessionAdapter - artifacts", () => {
|
|
419
|
+
it("lists names from current and legacy artifact responses", async () => {
|
|
420
|
+
mockFetch.mockResolvedValueOnce(
|
|
421
|
+
new Response(JSON.stringify(["report.pdf", { filename: "chart.png" }]), {
|
|
422
|
+
status: 200,
|
|
423
|
+
}),
|
|
424
|
+
);
|
|
425
|
+
|
|
426
|
+
const { artifacts } = createAdkSessionAdapter(baseOptions);
|
|
427
|
+
|
|
428
|
+
await expect(artifacts.list("s1")).resolves.toEqual([
|
|
429
|
+
"report.pdf",
|
|
430
|
+
"chart.png",
|
|
431
|
+
]);
|
|
432
|
+
expect(mockFetch.mock.calls[0]![0]).toBe(`${expectedBaseUrl}/s1/artifacts`);
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
it("rejects an artifact list that is not an array", async () => {
|
|
436
|
+
mockFetch.mockResolvedValueOnce(
|
|
437
|
+
new Response(JSON.stringify({}), { status: 200 }),
|
|
438
|
+
);
|
|
439
|
+
|
|
440
|
+
const { artifacts } = createAdkSessionAdapter(baseOptions);
|
|
441
|
+
|
|
442
|
+
await expect(artifacts.list("s1")).rejects.toThrow(
|
|
443
|
+
"Invalid ADK artifact list response: expected an array of artifact names.",
|
|
444
|
+
);
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
it("rejects malformed artifact list entries", async () => {
|
|
448
|
+
mockFetch.mockResolvedValueOnce(
|
|
449
|
+
new Response(JSON.stringify(["report.pdf", {}]), { status: 200 }),
|
|
450
|
+
);
|
|
451
|
+
|
|
452
|
+
const { artifacts } = createAdkSessionAdapter(baseOptions);
|
|
453
|
+
|
|
454
|
+
await expect(artifacts.list("s1")).rejects.toThrow(
|
|
455
|
+
'Invalid ADK artifact list response: artifact at index 1 must be a non-empty string or an object with a non-empty string "filename".',
|
|
456
|
+
);
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
it.each([
|
|
460
|
+
["text", { text: "artifact contents" }],
|
|
461
|
+
[
|
|
462
|
+
"inline data",
|
|
463
|
+
{ inlineData: { mimeType: "image/png", data: "aGVsbG8=" } },
|
|
464
|
+
],
|
|
465
|
+
])("loads valid %s artifacts", async (_label, artifact) => {
|
|
466
|
+
mockFetch.mockResolvedValueOnce(
|
|
467
|
+
new Response(JSON.stringify(artifact), { status: 200 }),
|
|
468
|
+
);
|
|
469
|
+
|
|
470
|
+
const { artifacts } = createAdkSessionAdapter(baseOptions);
|
|
471
|
+
|
|
472
|
+
await expect(artifacts.load("s1", "report.pdf")).resolves.toEqual(artifact);
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
it("rejects an artifact without supported content", async () => {
|
|
476
|
+
mockFetch.mockResolvedValueOnce(
|
|
477
|
+
new Response(JSON.stringify({}), { status: 200 }),
|
|
478
|
+
);
|
|
479
|
+
|
|
480
|
+
const { artifacts } = createAdkSessionAdapter(baseOptions);
|
|
481
|
+
|
|
482
|
+
await expect(artifacts.load("s1", "report.pdf")).rejects.toThrow(
|
|
483
|
+
'Invalid ADK artifact load response: expected an object containing "text" or "inlineData".',
|
|
484
|
+
);
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
it.each([
|
|
488
|
+
[
|
|
489
|
+
"text",
|
|
490
|
+
{ text: 42 },
|
|
491
|
+
'Invalid ADK artifact load response: "text" must be a string when present.',
|
|
492
|
+
],
|
|
493
|
+
[
|
|
494
|
+
"inline data",
|
|
495
|
+
{ inlineData: { mimeType: "image/png" } },
|
|
496
|
+
'Invalid ADK artifact load response: "inlineData" must contain string "mimeType" and "data" fields.',
|
|
497
|
+
],
|
|
498
|
+
])("rejects malformed %s artifact content", async (_label, value, error) => {
|
|
499
|
+
mockFetch.mockResolvedValueOnce(
|
|
500
|
+
new Response(JSON.stringify(value), { status: 200 }),
|
|
501
|
+
);
|
|
502
|
+
|
|
503
|
+
const { artifacts } = createAdkSessionAdapter(baseOptions);
|
|
504
|
+
|
|
505
|
+
await expect(artifacts.load("s1", "report.pdf")).rejects.toThrow(error);
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
it("lists artifact versions", async () => {
|
|
509
|
+
mockFetch.mockResolvedValueOnce(
|
|
510
|
+
new Response(JSON.stringify([0, 1, 2]), { status: 200 }),
|
|
511
|
+
);
|
|
512
|
+
|
|
513
|
+
const { artifacts } = createAdkSessionAdapter(baseOptions);
|
|
514
|
+
|
|
515
|
+
await expect(artifacts.listVersions("s1", "report.pdf")).resolves.toEqual([
|
|
516
|
+
0, 1, 2,
|
|
517
|
+
]);
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
it("rejects an artifact version list that is not an array", async () => {
|
|
521
|
+
mockFetch.mockResolvedValueOnce(
|
|
522
|
+
new Response(JSON.stringify({}), { status: 200 }),
|
|
523
|
+
);
|
|
524
|
+
|
|
525
|
+
const { artifacts } = createAdkSessionAdapter(baseOptions);
|
|
526
|
+
|
|
527
|
+
await expect(artifacts.listVersions("s1", "report.pdf")).rejects.toThrow(
|
|
528
|
+
"Invalid ADK artifact versions response: expected an array of version numbers.",
|
|
529
|
+
);
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
it("rejects malformed artifact version entries", async () => {
|
|
533
|
+
mockFetch.mockResolvedValueOnce(
|
|
534
|
+
new Response(JSON.stringify([0, "1"]), { status: 200 }),
|
|
535
|
+
);
|
|
536
|
+
|
|
537
|
+
const { artifacts } = createAdkSessionAdapter(baseOptions);
|
|
538
|
+
|
|
539
|
+
await expect(artifacts.listVersions("s1", "report.pdf")).rejects.toThrow(
|
|
540
|
+
"Invalid ADK artifact versions response: version at index 1 must be a non-negative integer.",
|
|
541
|
+
);
|
|
542
|
+
});
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
// ── URL construction ──
|
|
546
|
+
|
|
547
|
+
describe("createAdkSessionAdapter - URL construction", () => {
|
|
548
|
+
it.each(["http://localhost:8000/", "http://localhost:8000//"])(
|
|
549
|
+
"normalizes trailing slashes in apiUrl: %s",
|
|
550
|
+
async (apiUrl) => {
|
|
551
|
+
mockFetch.mockResolvedValueOnce(
|
|
552
|
+
new Response(JSON.stringify([]), { status: 200 }),
|
|
553
|
+
);
|
|
554
|
+
|
|
555
|
+
const { adapter } = createAdkSessionAdapter({
|
|
556
|
+
...baseOptions,
|
|
557
|
+
apiUrl,
|
|
558
|
+
});
|
|
559
|
+
await adapter.list();
|
|
560
|
+
|
|
561
|
+
expect(mockFetch.mock.calls[0]![0]).toBe(expectedBaseUrl);
|
|
562
|
+
},
|
|
563
|
+
);
|
|
343
564
|
|
|
344
|
-
describe("createAdkSessionAdapter - URL encoding", () => {
|
|
345
565
|
it("encodes special characters in appName and userId", async () => {
|
|
346
566
|
mockFetch.mockResolvedValueOnce(
|
|
347
567
|
new Response(JSON.stringify([]), { status: 200 }),
|
package/src/AdkSessionAdapter.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
} from "@assistant-ui/core";
|
|
8
8
|
import { AdkEventAccumulator } from "./AdkEventAccumulator";
|
|
9
9
|
import type { AdkEvent, AdkMessage } from "./types";
|
|
10
|
+
import { trimTrailingSlashes } from "./trimTrailingSlashes";
|
|
10
11
|
|
|
11
12
|
export type AdkSessionAdapterOptions = {
|
|
12
13
|
/**
|
|
@@ -56,6 +57,130 @@ type AdkSessionAdapterResult = {
|
|
|
56
57
|
};
|
|
57
58
|
};
|
|
58
59
|
|
|
60
|
+
type AdkSessionResponse = {
|
|
61
|
+
id: string;
|
|
62
|
+
events?: unknown;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
66
|
+
typeof value === "object" && value !== null && !Array.isArray(value);
|
|
67
|
+
|
|
68
|
+
const isAdkSessionResponse = (value: unknown): value is AdkSessionResponse => {
|
|
69
|
+
if (!isRecord(value)) return false;
|
|
70
|
+
|
|
71
|
+
const id = value.id;
|
|
72
|
+
return typeof id === "string" && id.length > 0;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const parseAdkSessionResponse = (
|
|
76
|
+
value: unknown,
|
|
77
|
+
operation: "create" | "fetch" | "load",
|
|
78
|
+
): AdkSessionResponse => {
|
|
79
|
+
if (!isAdkSessionResponse(value)) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
`Invalid ADK session ${operation} response: expected an object with a non-empty string "id".`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
return value;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const parseAdkSessionListResponse = (value: unknown): AdkSessionResponse[] => {
|
|
88
|
+
if (!Array.isArray(value)) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
"Invalid ADK session list response: expected an array of sessions.",
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return value.map((session, index) => {
|
|
95
|
+
if (!isAdkSessionResponse(session)) {
|
|
96
|
+
throw new Error(
|
|
97
|
+
`Invalid ADK session list response: session at index ${index} must have a non-empty string "id".`,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
return session;
|
|
101
|
+
});
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const parseAdkArtifactListResponse = (value: unknown): string[] => {
|
|
105
|
+
if (!Array.isArray(value)) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
"Invalid ADK artifact list response: expected an array of artifact names.",
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return value.map((artifact, index) => {
|
|
112
|
+
if (typeof artifact === "string" && artifact.length > 0) {
|
|
113
|
+
return artifact;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (isRecord(artifact)) {
|
|
117
|
+
const filename = artifact.filename;
|
|
118
|
+
if (typeof filename === "string" && filename.length > 0) {
|
|
119
|
+
return filename;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
throw new Error(
|
|
124
|
+
`Invalid ADK artifact list response: artifact at index ${index} must be a non-empty string or an object with a non-empty string "filename".`,
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const parseAdkArtifactResponse = (value: unknown): AdkArtifactData => {
|
|
130
|
+
if (!isRecord(value)) {
|
|
131
|
+
throw new Error(
|
|
132
|
+
'Invalid ADK artifact load response: expected an object containing "text" or "inlineData".',
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const { text, inlineData } = value;
|
|
137
|
+
if (text === undefined && inlineData === undefined) {
|
|
138
|
+
throw new Error(
|
|
139
|
+
'Invalid ADK artifact load response: expected an object containing "text" or "inlineData".',
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (text !== undefined && typeof text !== "string") {
|
|
144
|
+
throw new Error(
|
|
145
|
+
'Invalid ADK artifact load response: "text" must be a string when present.',
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (
|
|
150
|
+
inlineData !== undefined &&
|
|
151
|
+
(!isRecord(inlineData) ||
|
|
152
|
+
typeof inlineData.mimeType !== "string" ||
|
|
153
|
+
typeof inlineData.data !== "string")
|
|
154
|
+
) {
|
|
155
|
+
throw new Error(
|
|
156
|
+
'Invalid ADK artifact load response: "inlineData" must contain string "mimeType" and "data" fields.',
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return value as AdkArtifactData;
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const parseAdkArtifactVersionsResponse = (value: unknown): number[] => {
|
|
164
|
+
if (!Array.isArray(value)) {
|
|
165
|
+
throw new Error(
|
|
166
|
+
"Invalid ADK artifact versions response: expected an array of version numbers.",
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return value.map((version, index) => {
|
|
171
|
+
if (
|
|
172
|
+
typeof version !== "number" ||
|
|
173
|
+
!Number.isInteger(version) ||
|
|
174
|
+
version < 0
|
|
175
|
+
) {
|
|
176
|
+
throw new Error(
|
|
177
|
+
`Invalid ADK artifact versions response: version at index ${index} must be a non-negative integer.`,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
return version;
|
|
181
|
+
});
|
|
182
|
+
};
|
|
183
|
+
|
|
59
184
|
/**
|
|
60
185
|
* Creates a `RemoteThreadListAdapter` backed by ADK's session REST API,
|
|
61
186
|
* plus a `load` function that reconstructs messages from session events.
|
|
@@ -79,7 +204,8 @@ export function createAdkSessionAdapter(
|
|
|
79
204
|
options: AdkSessionAdapterOptions,
|
|
80
205
|
): AdkSessionAdapterResult {
|
|
81
206
|
const { apiUrl, appName, userId } = options;
|
|
82
|
-
const
|
|
207
|
+
const normalizedApiUrl = trimTrailingSlashes(apiUrl);
|
|
208
|
+
const baseUrl = `${normalizedApiUrl}/apps/${encodeURIComponent(appName)}/users/${encodeURIComponent(userId)}/sessions`;
|
|
83
209
|
|
|
84
210
|
const getHeaders = async (): Promise<Record<string, string>> => {
|
|
85
211
|
if (!options.headers) return {};
|
|
@@ -94,12 +220,7 @@ export function createAdkSessionAdapter(
|
|
|
94
220
|
if (!res.ok) {
|
|
95
221
|
throw new Error(`Failed to list sessions: ${res.status}`);
|
|
96
222
|
}
|
|
97
|
-
const data = (await res.json())
|
|
98
|
-
id: string;
|
|
99
|
-
app_name?: string;
|
|
100
|
-
user_id?: string;
|
|
101
|
-
last_update_time?: number;
|
|
102
|
-
}>;
|
|
223
|
+
const data = parseAdkSessionListResponse(await res.json());
|
|
103
224
|
|
|
104
225
|
const threads: RemoteThreadMetadata[] = data.map((session) => ({
|
|
105
226
|
status: "regular" as const,
|
|
@@ -123,7 +244,7 @@ export function createAdkSessionAdapter(
|
|
|
123
244
|
if (!res.ok) {
|
|
124
245
|
throw new Error(`Failed to create session: ${res.status}`);
|
|
125
246
|
}
|
|
126
|
-
const session = (await res.json())
|
|
247
|
+
const session = parseAdkSessionResponse(await res.json(), "create");
|
|
127
248
|
return { remoteId: session.id, externalId: session.id };
|
|
128
249
|
},
|
|
129
250
|
|
|
@@ -152,7 +273,13 @@ export function createAdkSessionAdapter(
|
|
|
152
273
|
|
|
153
274
|
generateTitle(): Promise<AssistantStream> {
|
|
154
275
|
// Title generation not supported without assistant-cloud
|
|
155
|
-
return Promise.resolve(
|
|
276
|
+
return Promise.resolve(
|
|
277
|
+
new ReadableStream<AssistantStreamChunk>({
|
|
278
|
+
start(controller) {
|
|
279
|
+
controller.close();
|
|
280
|
+
},
|
|
281
|
+
}),
|
|
282
|
+
);
|
|
156
283
|
},
|
|
157
284
|
|
|
158
285
|
async fetch(threadId: string): Promise<RemoteThreadMetadata> {
|
|
@@ -163,7 +290,7 @@ export function createAdkSessionAdapter(
|
|
|
163
290
|
if (!res.ok) {
|
|
164
291
|
throw new Error(`Session not found: ${res.status}`);
|
|
165
292
|
}
|
|
166
|
-
const session = (await res.json())
|
|
293
|
+
const session = parseAdkSessionResponse(await res.json(), "fetch");
|
|
167
294
|
return {
|
|
168
295
|
status: "regular",
|
|
169
296
|
remoteId: session.id,
|
|
@@ -183,18 +310,23 @@ export function createAdkSessionAdapter(
|
|
|
183
310
|
if (!res.ok) {
|
|
184
311
|
throw new Error(`Failed to load session: ${res.status}`);
|
|
185
312
|
}
|
|
186
|
-
const session = (await res.json())
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
313
|
+
const session = parseAdkSessionResponse(await res.json(), "load");
|
|
314
|
+
|
|
315
|
+
if (session.events !== undefined && !Array.isArray(session.events)) {
|
|
316
|
+
throw new Error(
|
|
317
|
+
'Invalid ADK session load response: expected "events" to be an array when present.',
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const events = session.events as AdkEvent[] | undefined;
|
|
190
322
|
|
|
191
|
-
if (!
|
|
323
|
+
if (!events?.length) {
|
|
192
324
|
return { messages: [] };
|
|
193
325
|
}
|
|
194
326
|
|
|
195
327
|
const accumulator = new AdkEventAccumulator();
|
|
196
328
|
let messages: AdkMessage[] = [];
|
|
197
|
-
for (const event of
|
|
329
|
+
for (const event of events) {
|
|
198
330
|
messages = accumulator.processEvent(event);
|
|
199
331
|
}
|
|
200
332
|
return { messages };
|
|
@@ -208,8 +340,7 @@ export function createAdkSessionAdapter(
|
|
|
208
340
|
const headers = await getHeaders();
|
|
209
341
|
const res = await fetch(artifactBaseUrl(sessionId), { headers });
|
|
210
342
|
if (!res.ok) throw new Error(`Failed to list artifacts: ${res.status}`);
|
|
211
|
-
|
|
212
|
-
return data.map((a) => a.filename);
|
|
343
|
+
return parseAdkArtifactListResponse(await res.json());
|
|
213
344
|
},
|
|
214
345
|
|
|
215
346
|
async load(sessionId, artifactName, version?) {
|
|
@@ -218,7 +349,7 @@ export function createAdkSessionAdapter(
|
|
|
218
349
|
if (version != null) url += `/versions/${version}`;
|
|
219
350
|
const res = await fetch(url, { headers });
|
|
220
351
|
if (!res.ok) throw new Error(`Failed to load artifact: ${res.status}`);
|
|
221
|
-
return (await res.json())
|
|
352
|
+
return parseAdkArtifactResponse(await res.json());
|
|
222
353
|
},
|
|
223
354
|
|
|
224
355
|
async listVersions(sessionId, artifactName) {
|
|
@@ -228,7 +359,7 @@ export function createAdkSessionAdapter(
|
|
|
228
359
|
if (!res.ok) {
|
|
229
360
|
throw new Error(`Failed to list artifact versions: ${res.status}`);
|
|
230
361
|
}
|
|
231
|
-
return (await res.json())
|
|
362
|
+
return parseAdkArtifactVersionsResponse(await res.json());
|
|
232
363
|
},
|
|
233
364
|
|
|
234
365
|
async delete(sessionId, artifactName) {
|
|
@@ -85,12 +85,14 @@ describe("parseAdkRequest", () => {
|
|
|
85
85
|
method: "POST",
|
|
86
86
|
body: "not json",
|
|
87
87
|
});
|
|
88
|
-
await expect(parseAdkRequest(req)).rejects.toThrow(
|
|
88
|
+
await expect(parseAdkRequest(req)).rejects.toThrow(
|
|
89
|
+
'Invalid JSON in Google ADK proxy request body. Expected a JSON object like {"message":"Hello"} or {"type":"tool-result",...}.',
|
|
90
|
+
);
|
|
89
91
|
});
|
|
90
92
|
|
|
91
93
|
it("throws on non-object body", async () => {
|
|
92
94
|
await expect(parseAdkRequest(makeRequest([1, 2, 3]))).rejects.toThrow(
|
|
93
|
-
"
|
|
95
|
+
"Google ADK proxy request body must be a JSON object",
|
|
94
96
|
);
|
|
95
97
|
});
|
|
96
98
|
});
|
|
@@ -46,11 +46,13 @@ export const parseAdkRequest = async (
|
|
|
46
46
|
try {
|
|
47
47
|
body = (await request.json()) as Record<string, unknown>;
|
|
48
48
|
} catch {
|
|
49
|
-
throw new Error(
|
|
49
|
+
throw new Error(
|
|
50
|
+
'Invalid JSON in Google ADK proxy request body. Expected a JSON object like {"message":"Hello"} or {"type":"tool-result",...}.',
|
|
51
|
+
);
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
53
|
-
throw new Error("
|
|
55
|
+
throw new Error("Google ADK proxy request body must be a JSON object");
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
const config: AdkSendMessageConfig = {};
|