@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.
Files changed (49) hide show
  1. package/dist/AdkClient.d.ts +0 -1
  2. package/dist/AdkClient.d.ts.map +1 -1
  3. package/dist/AdkClient.js +22 -10
  4. package/dist/AdkClient.js.map +1 -1
  5. package/dist/AdkEventAccumulator.d.ts +0 -1
  6. package/dist/AdkEventAccumulator.d.ts.map +1 -1
  7. package/dist/AdkSessionAdapter.d.ts +0 -1
  8. package/dist/AdkSessionAdapter.d.ts.map +1 -1
  9. package/dist/AdkSessionAdapter.js +59 -11
  10. package/dist/AdkSessionAdapter.js.map +1 -1
  11. package/dist/adkExtras.d.ts +0 -1
  12. package/dist/adkExtras.d.ts.map +1 -1
  13. package/dist/contentToParts.d.ts +0 -1
  14. package/dist/contentToParts.d.ts.map +1 -1
  15. package/dist/convertAdkMessages.d.ts +0 -1
  16. package/dist/convertAdkMessages.d.ts.map +1 -1
  17. package/dist/hooks.d.ts +0 -1
  18. package/dist/hooks.d.ts.map +1 -1
  19. package/dist/server/adkEventStream.d.ts.map +1 -1
  20. package/dist/server/createAdkApiRoute.d.ts +0 -1
  21. package/dist/server/createAdkApiRoute.d.ts.map +1 -1
  22. package/dist/server/parseAdkRequest.d.ts +0 -1
  23. package/dist/server/parseAdkRequest.d.ts.map +1 -1
  24. package/dist/server/parseAdkRequest.js +2 -2
  25. package/dist/server/parseAdkRequest.js.map +1 -1
  26. package/dist/structuredEvents.d.ts +0 -1
  27. package/dist/structuredEvents.d.ts.map +1 -1
  28. package/dist/trimTrailingSlashes.d.ts +5 -0
  29. package/dist/trimTrailingSlashes.d.ts.map +1 -0
  30. package/dist/trimTrailingSlashes.js +10 -0
  31. package/dist/trimTrailingSlashes.js.map +1 -0
  32. package/dist/types.d.ts +1 -2
  33. package/dist/types.d.ts.map +1 -1
  34. package/dist/useAdkMessages.d.ts +1 -5
  35. package/dist/useAdkMessages.d.ts.map +1 -1
  36. package/dist/useAdkMessages.js +2 -1
  37. package/dist/useAdkMessages.js.map +1 -1
  38. package/dist/useAdkRuntime.d.ts +1 -9
  39. package/dist/useAdkRuntime.d.ts.map +1 -1
  40. package/dist/useAdkRuntime.js +2 -1
  41. package/dist/useAdkRuntime.js.map +1 -1
  42. package/package.json +7 -7
  43. package/src/AdkClient.test.ts +256 -32
  44. package/src/AdkClient.ts +41 -20
  45. package/src/AdkSessionAdapter.test.ts +225 -5
  46. package/src/AdkSessionAdapter.ts +151 -20
  47. package/src/server/parseAdkRequest.test.ts +4 -2
  48. package/src/server/parseAdkRequest.ts +4 -2
  49. 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("returns an empty ReadableStream", async () => {
249
+ it("closes its title stream when title generation is unsupported", async () => {
214
250
  const { adapter } = createAdkSessionAdapter(baseOptions);
215
- const result = await adapter.generateTitle("s1", []);
216
- expect(result).toBeInstanceOf(ReadableStream);
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
- // ── URL encoding ──
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 }),
@@ -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 baseUrl = `${apiUrl}/apps/${encodeURIComponent(appName)}/users/${encodeURIComponent(userId)}/sessions`;
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()) as Array<{
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()) as { id: string };
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(new ReadableStream<AssistantStreamChunk>());
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()) as { id: string };
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()) as {
187
- id: string;
188
- events?: AdkEvent[];
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 (!session.events?.length) {
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 session.events) {
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
- const data = (await res.json()) as Array<{ filename: string }>;
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()) as AdkArtifactData;
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()) as number[];
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
- "Request body must be a JSON object",
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("Invalid JSON in request body");
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("Request body must be a JSON object");
55
+ throw new Error("Google ADK proxy request body must be a JSON object");
54
56
  }
55
57
 
56
58
  const config: AdkSendMessageConfig = {};
@@ -0,0 +1,5 @@
1
+ export const trimTrailingSlashes = (value: string) => {
2
+ let end = value.length;
3
+ while (end > 0 && value[end - 1] === "/") end -= 1;
4
+ return value.slice(0, end);
5
+ };