@assistant-ui/react-google-adk 0.0.1

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 (81) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +156 -0
  3. package/dist/AdkClient.d.ts +45 -0
  4. package/dist/AdkClient.d.ts.map +1 -0
  5. package/dist/AdkClient.js +204 -0
  6. package/dist/AdkClient.js.map +1 -0
  7. package/dist/AdkEventAccumulator.d.ts +45 -0
  8. package/dist/AdkEventAccumulator.d.ts.map +1 -0
  9. package/dist/AdkEventAccumulator.js +508 -0
  10. package/dist/AdkEventAccumulator.js.map +1 -0
  11. package/dist/AdkSessionAdapter.d.ts +61 -0
  12. package/dist/AdkSessionAdapter.d.ts.map +1 -0
  13. package/dist/AdkSessionAdapter.js +159 -0
  14. package/dist/AdkSessionAdapter.js.map +1 -0
  15. package/dist/convertAdkMessages.d.ts +4 -0
  16. package/dist/convertAdkMessages.d.ts.map +1 -0
  17. package/dist/convertAdkMessages.js +75 -0
  18. package/dist/convertAdkMessages.js.map +1 -0
  19. package/dist/hooks.d.ts +50 -0
  20. package/dist/hooks.d.ts.map +1 -0
  21. package/dist/hooks.js +173 -0
  22. package/dist/hooks.js.map +1 -0
  23. package/dist/index.d.ts +11 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +10 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/server/adkEventStream.d.ts +42 -0
  28. package/dist/server/adkEventStream.d.ts.map +1 -0
  29. package/dist/server/adkEventStream.js +135 -0
  30. package/dist/server/adkEventStream.js.map +1 -0
  31. package/dist/server/createAdkApiRoute.d.ts +47 -0
  32. package/dist/server/createAdkApiRoute.d.ts.map +1 -0
  33. package/dist/server/createAdkApiRoute.js +41 -0
  34. package/dist/server/createAdkApiRoute.js.map +1 -0
  35. package/dist/server/index.d.ts +4 -0
  36. package/dist/server/index.d.ts.map +1 -0
  37. package/dist/server/index.js +4 -0
  38. package/dist/server/index.js.map +1 -0
  39. package/dist/server/parseAdkRequest.d.ts +56 -0
  40. package/dist/server/parseAdkRequest.d.ts.map +1 -0
  41. package/dist/server/parseAdkRequest.js +93 -0
  42. package/dist/server/parseAdkRequest.js.map +1 -0
  43. package/dist/structuredEvents.d.ts +7 -0
  44. package/dist/structuredEvents.d.ts.map +1 -0
  45. package/dist/structuredEvents.js +79 -0
  46. package/dist/structuredEvents.js.map +1 -0
  47. package/dist/types.d.ts +253 -0
  48. package/dist/types.d.ts.map +1 -0
  49. package/dist/types.js +14 -0
  50. package/dist/types.js.map +1 -0
  51. package/dist/useAdkMessages.d.ts +28 -0
  52. package/dist/useAdkMessages.d.ts.map +1 -0
  53. package/dist/useAdkMessages.js +198 -0
  54. package/dist/useAdkMessages.js.map +1 -0
  55. package/dist/useAdkRuntime.d.ts +36 -0
  56. package/dist/useAdkRuntime.d.ts.map +1 -0
  57. package/dist/useAdkRuntime.js +252 -0
  58. package/dist/useAdkRuntime.js.map +1 -0
  59. package/package.json +83 -0
  60. package/server/package.json +4 -0
  61. package/src/AdkClient.test.ts +662 -0
  62. package/src/AdkClient.ts +274 -0
  63. package/src/AdkEventAccumulator.test.ts +591 -0
  64. package/src/AdkEventAccumulator.ts +602 -0
  65. package/src/AdkSessionAdapter.test.ts +362 -0
  66. package/src/AdkSessionAdapter.ts +245 -0
  67. package/src/convertAdkMessages.test.ts +209 -0
  68. package/src/convertAdkMessages.ts +93 -0
  69. package/src/hooks.ts +217 -0
  70. package/src/index.ts +66 -0
  71. package/src/server/adkEventStream.test.ts +78 -0
  72. package/src/server/adkEventStream.ts +161 -0
  73. package/src/server/createAdkApiRoute.test.ts +370 -0
  74. package/src/server/createAdkApiRoute.ts +86 -0
  75. package/src/server/index.ts +6 -0
  76. package/src/server/parseAdkRequest.test.ts +152 -0
  77. package/src/server/parseAdkRequest.ts +122 -0
  78. package/src/structuredEvents.ts +81 -0
  79. package/src/types.ts +265 -0
  80. package/src/useAdkMessages.ts +259 -0
  81. package/src/useAdkRuntime.ts +398 -0
@@ -0,0 +1,662 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { createAdkStream } from "./AdkClient";
3
+ import type { AdkEvent, AdkMessage, AdkSendMessageConfig } from "./types";
4
+
5
+ // ── Helpers ──
6
+
7
+ const makeConfig = (
8
+ overrides: Partial<
9
+ AdkSendMessageConfig & {
10
+ abortSignal: AbortSignal;
11
+ initialize: () => Promise<{
12
+ remoteId: string;
13
+ externalId: string | undefined;
14
+ }>;
15
+ }
16
+ > = {},
17
+ ) => ({
18
+ abortSignal: new AbortController().signal,
19
+ initialize: vi.fn().mockResolvedValue({
20
+ remoteId: "r1",
21
+ externalId: "session-1",
22
+ }),
23
+ ...overrides,
24
+ });
25
+
26
+ /** Encode SSE text into a ReadableStream of Uint8Array chunks. */
27
+ const sseBody = (text: string): ReadableStream<Uint8Array> => {
28
+ const encoder = new TextEncoder();
29
+ return new ReadableStream({
30
+ start(controller) {
31
+ controller.enqueue(encoder.encode(text));
32
+ controller.close();
33
+ },
34
+ });
35
+ };
36
+
37
+ const mockFetch =
38
+ vi.fn<(input: RequestInfo | URL, init?: RequestInit) => Promise<Response>>();
39
+
40
+ beforeEach(() => {
41
+ vi.stubGlobal("fetch", mockFetch);
42
+ mockFetch.mockReset();
43
+ });
44
+
45
+ // ── Proxy mode ──
46
+
47
+ describe("createAdkStream - proxy mode", () => {
48
+ it("POSTs to the api URL directly", async () => {
49
+ mockFetch.mockResolvedValueOnce(new Response(sseBody(""), { status: 200 }));
50
+
51
+ const stream = createAdkStream({ api: "/api/adk" });
52
+ const messages: AdkMessage[] = [
53
+ { id: "m1", type: "human", content: "Hello" },
54
+ ];
55
+ const gen = await stream(messages, makeConfig());
56
+ // drain
57
+ for await (const _ of gen) {
58
+ /* noop */
59
+ }
60
+
61
+ expect(mockFetch).toHaveBeenCalledOnce();
62
+ const [url, init] = mockFetch.mock.calls[0]!;
63
+ expect(url).toBe("/api/adk");
64
+ expect(init?.method).toBe("POST");
65
+ const body = JSON.parse(init?.body as string);
66
+ expect(body).toMatchObject({ message: "Hello" });
67
+ });
68
+
69
+ it("sends runConfig and checkpointId in proxy body", async () => {
70
+ mockFetch.mockResolvedValueOnce(new Response(sseBody(""), { status: 200 }));
71
+
72
+ const stream = createAdkStream({ api: "/api/adk" });
73
+ const gen = await stream(
74
+ [{ id: "m1", type: "human", content: "Hi" }],
75
+ makeConfig({ runConfig: { temp: 0.5 }, checkpointId: "cp-1" }),
76
+ );
77
+ for await (const _ of gen) {
78
+ /* noop */
79
+ }
80
+
81
+ const body = JSON.parse(mockFetch.mock.calls[0]![1]?.body as string);
82
+ expect(body.runConfig).toEqual({ temp: 0.5 });
83
+ expect(body.checkpointId).toBe("cp-1");
84
+ });
85
+
86
+ it("sends a tool-result body when message type is tool", async () => {
87
+ mockFetch.mockResolvedValueOnce(new Response(sseBody(""), { status: 200 }));
88
+
89
+ const stream = createAdkStream({ api: "/api/adk" });
90
+ const messages: AdkMessage[] = [
91
+ {
92
+ id: "t1",
93
+ type: "tool",
94
+ content: '{"ok":true}',
95
+ tool_call_id: "tc-1",
96
+ name: "search",
97
+ status: "success",
98
+ },
99
+ ];
100
+ const gen = await stream(messages, makeConfig());
101
+ for await (const _ of gen) {
102
+ /* noop */
103
+ }
104
+
105
+ const body = JSON.parse(mockFetch.mock.calls[0]![1]?.body as string);
106
+ expect(body.type).toBe("tool-result");
107
+ expect(body.toolCallId).toBe("tc-1");
108
+ expect(body.toolName).toBe("search");
109
+ expect(body.result).toEqual({ ok: true });
110
+ expect(body.isError).toBe(false);
111
+ });
112
+
113
+ it("sends parts when message has multimodal content", async () => {
114
+ mockFetch.mockResolvedValueOnce(new Response(sseBody(""), { status: 200 }));
115
+
116
+ const stream = createAdkStream({ api: "/api/adk" });
117
+ const messages: AdkMessage[] = [
118
+ {
119
+ id: "m1",
120
+ type: "human",
121
+ content: [
122
+ { type: "text", text: "Look" },
123
+ { type: "image", mimeType: "image/png", data: "abc" },
124
+ ],
125
+ },
126
+ ];
127
+ const gen = await stream(messages, makeConfig());
128
+ for await (const _ of gen) {
129
+ /* noop */
130
+ }
131
+
132
+ const body = JSON.parse(mockFetch.mock.calls[0]![1]?.body as string);
133
+ expect(body.parts).toHaveLength(2);
134
+ expect(body.parts[0]).toEqual({ text: "Look" });
135
+ expect(body.parts[1]).toEqual({
136
+ inlineData: { mimeType: "image/png", data: "abc" },
137
+ });
138
+ });
139
+
140
+ it("sends parts array when multiple messages are provided", async () => {
141
+ mockFetch.mockResolvedValueOnce(new Response(sseBody(""), { status: 200 }));
142
+
143
+ const stream = createAdkStream({ api: "/api/adk" });
144
+ const messages: AdkMessage[] = [
145
+ {
146
+ id: "t1",
147
+ type: "tool",
148
+ content: '{"cancelled":true}',
149
+ tool_call_id: "tc-0",
150
+ name: "cancel",
151
+ status: "error",
152
+ },
153
+ { id: "m1", type: "human", content: "Hello" },
154
+ ];
155
+ const gen = await stream(messages, makeConfig());
156
+ for await (const _ of gen) {
157
+ /* noop */
158
+ }
159
+
160
+ const body = JSON.parse(mockFetch.mock.calls[0]![1]?.body as string);
161
+ expect(body.parts).toBeDefined();
162
+ expect(Array.isArray(body.parts)).toBe(true);
163
+ });
164
+
165
+ it("marks isError=true when tool status is error", async () => {
166
+ mockFetch.mockResolvedValueOnce(new Response(sseBody(""), { status: 200 }));
167
+
168
+ const stream = createAdkStream({ api: "/api/adk" });
169
+ const messages: AdkMessage[] = [
170
+ {
171
+ id: "t1",
172
+ type: "tool",
173
+ content: "failed",
174
+ tool_call_id: "tc-1",
175
+ name: "search",
176
+ status: "error",
177
+ },
178
+ ];
179
+ const gen = await stream(messages, makeConfig());
180
+ for await (const _ of gen) {
181
+ /* noop */
182
+ }
183
+
184
+ const body = JSON.parse(mockFetch.mock.calls[0]![1]?.body as string);
185
+ expect(body.isError).toBe(true);
186
+ });
187
+ });
188
+
189
+ // ── Direct mode ──
190
+
191
+ describe("createAdkStream - direct mode", () => {
192
+ it("POSTs to /run_sse with ADK-native body", async () => {
193
+ mockFetch.mockResolvedValueOnce(new Response(sseBody(""), { status: 200 }));
194
+
195
+ const stream = createAdkStream({
196
+ api: "http://localhost:8000",
197
+ appName: "my-app",
198
+ userId: "user-1",
199
+ });
200
+ const gen = await stream(
201
+ [{ id: "m1", type: "human", content: "Hello" }],
202
+ makeConfig(),
203
+ );
204
+ for await (const _ of gen) {
205
+ /* noop */
206
+ }
207
+
208
+ const [url, init] = mockFetch.mock.calls[0]!;
209
+ expect(url).toBe("http://localhost:8000/run_sse");
210
+
211
+ const body = JSON.parse(init?.body as string);
212
+ expect(body.appName).toBe("my-app");
213
+ expect(body.userId).toBe("user-1");
214
+ expect(body.sessionId).toBe("session-1");
215
+ expect(body.streaming).toBe(true);
216
+ expect(body.newMessage).toMatchObject({
217
+ role: "user",
218
+ parts: [{ text: "Hello" }],
219
+ });
220
+ });
221
+
222
+ it("calls config.initialize() to get the sessionId", async () => {
223
+ const initialize = vi
224
+ .fn()
225
+ .mockResolvedValue({ remoteId: "r1", externalId: "s-42" });
226
+ mockFetch.mockResolvedValueOnce(new Response(sseBody(""), { status: 200 }));
227
+
228
+ const stream = createAdkStream({
229
+ api: "http://localhost:8000",
230
+ appName: "app",
231
+ userId: "u",
232
+ });
233
+ const gen = await stream(
234
+ [{ id: "m1", type: "human", content: "Hi" }],
235
+ makeConfig({ initialize }),
236
+ );
237
+ for await (const _ of gen) {
238
+ /* noop */
239
+ }
240
+
241
+ expect(initialize).toHaveBeenCalledOnce();
242
+ const body = JSON.parse(mockFetch.mock.calls[0]![1]?.body as string);
243
+ expect(body.sessionId).toBe("s-42");
244
+ });
245
+
246
+ it("converts tool messages to functionResponse parts", async () => {
247
+ mockFetch.mockResolvedValueOnce(new Response(sseBody(""), { status: 200 }));
248
+
249
+ const stream = createAdkStream({
250
+ api: "http://localhost:8000",
251
+ appName: "app",
252
+ userId: "u",
253
+ });
254
+ const messages: AdkMessage[] = [
255
+ {
256
+ id: "t1",
257
+ type: "tool",
258
+ content: '{"result":"ok"}',
259
+ tool_call_id: "tc-1",
260
+ name: "search",
261
+ },
262
+ ];
263
+ const gen = await stream(messages, makeConfig());
264
+ for await (const _ of gen) {
265
+ /* noop */
266
+ }
267
+
268
+ const body = JSON.parse(mockFetch.mock.calls[0]![1]?.body as string);
269
+ expect(body.newMessage.parts[0]).toMatchObject({
270
+ functionResponse: {
271
+ name: "search",
272
+ id: "tc-1",
273
+ response: { result: "ok" },
274
+ },
275
+ });
276
+ });
277
+
278
+ it("falls back to raw string when tool content is not valid JSON", async () => {
279
+ mockFetch.mockResolvedValueOnce(new Response(sseBody(""), { status: 200 }));
280
+
281
+ const stream = createAdkStream({
282
+ api: "http://localhost:8000",
283
+ appName: "app",
284
+ userId: "u",
285
+ });
286
+ const messages: AdkMessage[] = [
287
+ {
288
+ id: "t1",
289
+ type: "tool",
290
+ content: "not-json",
291
+ tool_call_id: "tc-1",
292
+ name: "search",
293
+ },
294
+ ];
295
+ const gen = await stream(messages, makeConfig());
296
+ for await (const _ of gen) {
297
+ /* noop */
298
+ }
299
+
300
+ const body = JSON.parse(mockFetch.mock.calls[0]![1]?.body as string);
301
+ expect(body.newMessage.parts[0].functionResponse.response).toBe("not-json");
302
+ });
303
+
304
+ it("sends empty text part when no messages provided", async () => {
305
+ mockFetch.mockResolvedValueOnce(new Response(sseBody(""), { status: 200 }));
306
+
307
+ const stream = createAdkStream({
308
+ api: "http://localhost:8000",
309
+ appName: "app",
310
+ userId: "u",
311
+ });
312
+ const gen = await stream([], makeConfig());
313
+ for await (const _ of gen) {
314
+ /* noop */
315
+ }
316
+
317
+ const body = JSON.parse(mockFetch.mock.calls[0]![1]?.body as string);
318
+ expect(body.newMessage.parts).toEqual([{ text: "" }]);
319
+ });
320
+ });
321
+
322
+ // ── SSE parsing ──
323
+
324
+ describe("createAdkStream - SSE parsing", () => {
325
+ it("parses data lines into AdkEvent objects", async () => {
326
+ const events: AdkEvent[] = [
327
+ { id: "e1", content: { parts: [{ text: "hello" }] } },
328
+ { id: "e2", content: { parts: [{ text: "world" }] } },
329
+ ];
330
+ const text = events.map((e) => `data: ${JSON.stringify(e)}\n\n`).join("");
331
+ mockFetch.mockResolvedValueOnce(
332
+ new Response(sseBody(text), { status: 200 }),
333
+ );
334
+
335
+ const stream = createAdkStream({ api: "/api/adk" });
336
+ const gen = await stream(
337
+ [{ id: "m1", type: "human", content: "Hi" }],
338
+ makeConfig(),
339
+ );
340
+ const collected: AdkEvent[] = [];
341
+ for await (const evt of gen) {
342
+ collected.push(evt);
343
+ }
344
+
345
+ expect(collected).toHaveLength(2);
346
+ expect(collected[0]!.id).toBe("e1");
347
+ expect(collected[1]!.id).toBe("e2");
348
+ });
349
+
350
+ it("skips :ok SSE comments", async () => {
351
+ const text = `:ok\n\ndata: ${JSON.stringify({ id: "e1" })}\n\n`;
352
+ mockFetch.mockResolvedValueOnce(
353
+ new Response(sseBody(text), { status: 200 }),
354
+ );
355
+
356
+ const stream = createAdkStream({ api: "/api/adk" });
357
+ const gen = await stream(
358
+ [{ id: "m1", type: "human", content: "Hi" }],
359
+ makeConfig(),
360
+ );
361
+ const collected: AdkEvent[] = [];
362
+ for await (const evt of gen) {
363
+ collected.push(evt);
364
+ }
365
+
366
+ expect(collected).toHaveLength(1);
367
+ expect(collected[0]!.id).toBe("e1");
368
+ });
369
+
370
+ it("handles partial chunks that split across reads", async () => {
371
+ const event = { id: "e1", content: { parts: [{ text: "split" }] } };
372
+ const fullText = `data: ${JSON.stringify(event)}\n\n`;
373
+ const mid = Math.floor(fullText.length / 2);
374
+ const chunk1 = fullText.slice(0, mid);
375
+ const chunk2 = fullText.slice(mid);
376
+
377
+ const encoder = new TextEncoder();
378
+ const body = new ReadableStream<Uint8Array>({
379
+ start(controller) {
380
+ controller.enqueue(encoder.encode(chunk1));
381
+ controller.enqueue(encoder.encode(chunk2));
382
+ controller.close();
383
+ },
384
+ });
385
+ mockFetch.mockResolvedValueOnce(new Response(body, { status: 200 }));
386
+
387
+ const stream = createAdkStream({ api: "/api/adk" });
388
+ const gen = await stream(
389
+ [{ id: "m1", type: "human", content: "Hi" }],
390
+ makeConfig(),
391
+ );
392
+ const collected: AdkEvent[] = [];
393
+ for await (const evt of gen) {
394
+ collected.push(evt);
395
+ }
396
+
397
+ expect(collected).toHaveLength(1);
398
+ expect(collected[0]!.id).toBe("e1");
399
+ });
400
+
401
+ it("handles remaining buffer at end of stream", async () => {
402
+ // No trailing \n\n
403
+ const event = { id: "e1" };
404
+ const text = `data: ${JSON.stringify(event)}\n`;
405
+ mockFetch.mockResolvedValueOnce(
406
+ new Response(sseBody(text), { status: 200 }),
407
+ );
408
+
409
+ const stream = createAdkStream({ api: "/api/adk" });
410
+ const gen = await stream(
411
+ [{ id: "m1", type: "human", content: "Hi" }],
412
+ makeConfig(),
413
+ );
414
+ const collected: AdkEvent[] = [];
415
+ for await (const evt of gen) {
416
+ collected.push(evt);
417
+ }
418
+
419
+ expect(collected).toHaveLength(1);
420
+ expect(collected[0]!.id).toBe("e1");
421
+ });
422
+ });
423
+
424
+ // ── Error handling ──
425
+
426
+ describe("createAdkStream - error handling", () => {
427
+ it("throws when response is not ok", async () => {
428
+ mockFetch.mockResolvedValueOnce(
429
+ new Response("Not found", { status: 404, statusText: "Not Found" }),
430
+ );
431
+
432
+ const stream = createAdkStream({ api: "/api/adk" });
433
+ await expect(async () => {
434
+ const gen = await stream(
435
+ [{ id: "m1", type: "human", content: "Hi" }],
436
+ makeConfig(),
437
+ );
438
+ for await (const _ of gen) {
439
+ /* noop */
440
+ }
441
+ }).rejects.toThrow("ADK request failed: 404 Not Found");
442
+ });
443
+ });
444
+
445
+ // ── Headers ──
446
+
447
+ describe("createAdkStream - headers", () => {
448
+ it("sends static headers", async () => {
449
+ mockFetch.mockResolvedValueOnce(new Response(sseBody(""), { status: 200 }));
450
+
451
+ const stream = createAdkStream({
452
+ api: "/api/adk",
453
+ headers: { Authorization: "Bearer tok" },
454
+ });
455
+ const gen = await stream(
456
+ [{ id: "m1", type: "human", content: "Hi" }],
457
+ makeConfig(),
458
+ );
459
+ for await (const _ of gen) {
460
+ /* noop */
461
+ }
462
+
463
+ const headers = mockFetch.mock.calls[0]![1]?.headers as Record<
464
+ string,
465
+ string
466
+ >;
467
+ expect(headers.Authorization).toBe("Bearer tok");
468
+ expect(headers["Content-Type"]).toBe("application/json");
469
+ });
470
+
471
+ it("resolves dynamic headers from a function", async () => {
472
+ mockFetch.mockResolvedValueOnce(new Response(sseBody(""), { status: 200 }));
473
+
474
+ const stream = createAdkStream({
475
+ api: "/api/adk",
476
+ headers: () => ({ "X-Custom": "dynamic" }),
477
+ });
478
+ const gen = await stream(
479
+ [{ id: "m1", type: "human", content: "Hi" }],
480
+ makeConfig(),
481
+ );
482
+ for await (const _ of gen) {
483
+ /* noop */
484
+ }
485
+
486
+ const headers = mockFetch.mock.calls[0]![1]?.headers as Record<
487
+ string,
488
+ string
489
+ >;
490
+ expect(headers["X-Custom"]).toBe("dynamic");
491
+ });
492
+
493
+ it("resolves async dynamic headers", async () => {
494
+ mockFetch.mockResolvedValueOnce(new Response(sseBody(""), { status: 200 }));
495
+
496
+ const stream = createAdkStream({
497
+ api: "/api/adk",
498
+ headers: async () => ({ "X-Async": "yes" }),
499
+ });
500
+ const gen = await stream(
501
+ [{ id: "m1", type: "human", content: "Hi" }],
502
+ makeConfig(),
503
+ );
504
+ for await (const _ of gen) {
505
+ /* noop */
506
+ }
507
+
508
+ const headers = mockFetch.mock.calls[0]![1]?.headers as Record<
509
+ string,
510
+ string
511
+ >;
512
+ expect(headers["X-Async"]).toBe("yes");
513
+ });
514
+
515
+ it("sends no extra headers when headers option is undefined", async () => {
516
+ mockFetch.mockResolvedValueOnce(new Response(sseBody(""), { status: 200 }));
517
+
518
+ const stream = createAdkStream({ api: "/api/adk" });
519
+ const gen = await stream(
520
+ [{ id: "m1", type: "human", content: "Hi" }],
521
+ makeConfig(),
522
+ );
523
+ for await (const _ of gen) {
524
+ /* noop */
525
+ }
526
+
527
+ const headers = mockFetch.mock.calls[0]![1]?.headers as Record<
528
+ string,
529
+ string
530
+ >;
531
+ expect(Object.keys(headers)).toEqual(["Content-Type"]);
532
+ });
533
+ });
534
+
535
+ // ── AbortSignal ──
536
+
537
+ describe("createAdkStream - AbortSignal", () => {
538
+ it("forwards the AbortSignal to fetch", async () => {
539
+ mockFetch.mockResolvedValueOnce(new Response(sseBody(""), { status: 200 }));
540
+
541
+ const controller = new AbortController();
542
+ const stream = createAdkStream({ api: "/api/adk" });
543
+ const gen = await stream(
544
+ [{ id: "m1", type: "human", content: "Hi" }],
545
+ makeConfig({ abortSignal: controller.signal }),
546
+ );
547
+ for await (const _ of gen) {
548
+ /* noop */
549
+ }
550
+
551
+ expect(mockFetch.mock.calls[0]![1]?.signal).toBe(controller.signal);
552
+ });
553
+ });
554
+
555
+ // ── Content conversion helpers ──
556
+
557
+ describe("createAdkStream - content conversion", () => {
558
+ it("converts reasoning content parts to thought parts in direct mode", async () => {
559
+ mockFetch.mockResolvedValueOnce(new Response(sseBody(""), { status: 200 }));
560
+
561
+ const stream = createAdkStream({
562
+ api: "http://localhost:8000",
563
+ appName: "app",
564
+ userId: "u",
565
+ });
566
+ const messages: AdkMessage[] = [
567
+ {
568
+ id: "m1",
569
+ type: "human",
570
+ content: [{ type: "reasoning", text: "thinking..." }],
571
+ },
572
+ ];
573
+ const gen = await stream(messages, makeConfig());
574
+ for await (const _ of gen) {
575
+ /* noop */
576
+ }
577
+
578
+ const body = JSON.parse(mockFetch.mock.calls[0]![1]?.body as string);
579
+ expect(body.newMessage.parts[0]).toEqual({
580
+ text: "thinking...",
581
+ thought: true,
582
+ });
583
+ });
584
+
585
+ it("converts image_url content parts to fileData in direct mode", async () => {
586
+ mockFetch.mockResolvedValueOnce(new Response(sseBody(""), { status: 200 }));
587
+
588
+ const stream = createAdkStream({
589
+ api: "http://localhost:8000",
590
+ appName: "app",
591
+ userId: "u",
592
+ });
593
+ const messages: AdkMessage[] = [
594
+ {
595
+ id: "m1",
596
+ type: "human",
597
+ content: [{ type: "image_url", url: "https://example.com/img.png" }],
598
+ },
599
+ ];
600
+ const gen = await stream(messages, makeConfig());
601
+ for await (const _ of gen) {
602
+ /* noop */
603
+ }
604
+
605
+ const body = JSON.parse(mockFetch.mock.calls[0]![1]?.body as string);
606
+ expect(body.newMessage.parts[0]).toEqual({
607
+ fileData: { fileUri: "https://example.com/img.png" },
608
+ });
609
+ });
610
+
611
+ it("converts code content parts to executableCode", async () => {
612
+ mockFetch.mockResolvedValueOnce(new Response(sseBody(""), { status: 200 }));
613
+
614
+ const stream = createAdkStream({
615
+ api: "http://localhost:8000",
616
+ appName: "app",
617
+ userId: "u",
618
+ });
619
+ const messages: AdkMessage[] = [
620
+ {
621
+ id: "m1",
622
+ type: "human",
623
+ content: [{ type: "code", code: "print(1)", language: "python" }],
624
+ },
625
+ ];
626
+ const gen = await stream(messages, makeConfig());
627
+ for await (const _ of gen) {
628
+ /* noop */
629
+ }
630
+
631
+ const body = JSON.parse(mockFetch.mock.calls[0]![1]?.body as string);
632
+ expect(body.newMessage.parts[0]).toEqual({
633
+ executableCode: { code: "print(1)", language: "python" },
634
+ });
635
+ });
636
+
637
+ it("converts code_result content parts to codeExecutionResult", async () => {
638
+ mockFetch.mockResolvedValueOnce(new Response(sseBody(""), { status: 200 }));
639
+
640
+ const stream = createAdkStream({
641
+ api: "http://localhost:8000",
642
+ appName: "app",
643
+ userId: "u",
644
+ });
645
+ const messages: AdkMessage[] = [
646
+ {
647
+ id: "m1",
648
+ type: "human",
649
+ content: [{ type: "code_result", output: "1", outcome: "OUTCOME_OK" }],
650
+ },
651
+ ];
652
+ const gen = await stream(messages, makeConfig());
653
+ for await (const _ of gen) {
654
+ /* noop */
655
+ }
656
+
657
+ const body = JSON.parse(mockFetch.mock.calls[0]![1]?.body as string);
658
+ expect(body.newMessage.parts[0]).toEqual({
659
+ codeExecutionResult: { output: "1", outcome: "OUTCOME_OK" },
660
+ });
661
+ });
662
+ });