@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
@@ -34,6 +34,28 @@ const sseBody = (text: string): ReadableStream<Uint8Array> => {
34
34
  });
35
35
  };
36
36
 
37
+ const sseResponse = (
38
+ body: BodyInit | null,
39
+ contentType: string | null = "text/event-stream",
40
+ ): Response =>
41
+ new Response(body, {
42
+ status: 200,
43
+ ...(contentType === null
44
+ ? {}
45
+ : { headers: { "Content-Type": contentType } }),
46
+ });
47
+
48
+ const nextWithTimeout = async <T>(
49
+ promise: Promise<IteratorResult<T>>,
50
+ ): Promise<IteratorResult<T> | "timeout"> => {
51
+ return Promise.race([
52
+ promise,
53
+ new Promise<"timeout">((resolve) => {
54
+ setTimeout(() => resolve("timeout"), 100);
55
+ }),
56
+ ]);
57
+ };
58
+
37
59
  const mockFetch =
38
60
  vi.fn<(input: RequestInfo | URL, init?: RequestInit) => Promise<Response>>();
39
61
 
@@ -46,7 +68,7 @@ beforeEach(() => {
46
68
 
47
69
  describe("createAdkStream - proxy mode", () => {
48
70
  it("POSTs to the api URL directly", async () => {
49
- mockFetch.mockResolvedValueOnce(new Response(sseBody(""), { status: 200 }));
71
+ mockFetch.mockResolvedValueOnce(sseResponse(sseBody("")));
50
72
 
51
73
  const stream = createAdkStream({ api: "/api/adk" });
52
74
  const messages: AdkMessage[] = [
@@ -67,7 +89,7 @@ describe("createAdkStream - proxy mode", () => {
67
89
  });
68
90
 
69
91
  it("sends runConfig and checkpointId in proxy body", async () => {
70
- mockFetch.mockResolvedValueOnce(new Response(sseBody(""), { status: 200 }));
92
+ mockFetch.mockResolvedValueOnce(sseResponse(sseBody("")));
71
93
 
72
94
  const stream = createAdkStream({ api: "/api/adk" });
73
95
  const gen = await stream(
@@ -84,7 +106,7 @@ describe("createAdkStream - proxy mode", () => {
84
106
  });
85
107
 
86
108
  it("sends a tool-result body when message type is tool", async () => {
87
- mockFetch.mockResolvedValueOnce(new Response(sseBody(""), { status: 200 }));
109
+ mockFetch.mockResolvedValueOnce(sseResponse(sseBody("")));
88
110
 
89
111
  const stream = createAdkStream({ api: "/api/adk" });
90
112
  const messages: AdkMessage[] = [
@@ -111,7 +133,7 @@ describe("createAdkStream - proxy mode", () => {
111
133
  });
112
134
 
113
135
  it("sends parts when message has multimodal content", async () => {
114
- mockFetch.mockResolvedValueOnce(new Response(sseBody(""), { status: 200 }));
136
+ mockFetch.mockResolvedValueOnce(sseResponse(sseBody("")));
115
137
 
116
138
  const stream = createAdkStream({ api: "/api/adk" });
117
139
  const messages: AdkMessage[] = [
@@ -138,7 +160,7 @@ describe("createAdkStream - proxy mode", () => {
138
160
  });
139
161
 
140
162
  it("sends file parts as inlineData in proxy mode", async () => {
141
- mockFetch.mockResolvedValueOnce(new Response(sseBody(""), { status: 200 }));
163
+ mockFetch.mockResolvedValueOnce(sseResponse(sseBody("")));
142
164
 
143
165
  const stream = createAdkStream({ api: "/api/adk" });
144
166
  const messages: AdkMessage[] = [
@@ -169,7 +191,7 @@ describe("createAdkStream - proxy mode", () => {
169
191
  });
170
192
 
171
193
  it("sends parts array when multiple messages are provided", async () => {
172
- mockFetch.mockResolvedValueOnce(new Response(sseBody(""), { status: 200 }));
194
+ mockFetch.mockResolvedValueOnce(sseResponse(sseBody("")));
173
195
 
174
196
  const stream = createAdkStream({ api: "/api/adk" });
175
197
  const messages: AdkMessage[] = [
@@ -194,7 +216,7 @@ describe("createAdkStream - proxy mode", () => {
194
216
  });
195
217
 
196
218
  it("marks isError=true when tool status is error", async () => {
197
- mockFetch.mockResolvedValueOnce(new Response(sseBody(""), { status: 200 }));
219
+ mockFetch.mockResolvedValueOnce(sseResponse(sseBody("")));
198
220
 
199
221
  const stream = createAdkStream({ api: "/api/adk" });
200
222
  const messages: AdkMessage[] = [
@@ -220,8 +242,35 @@ describe("createAdkStream - proxy mode", () => {
220
242
  // ── Direct mode ──
221
243
 
222
244
  describe("createAdkStream - direct mode", () => {
245
+ it("rejects direct mode with an empty appName", () => {
246
+ expect(() =>
247
+ createAdkStream({
248
+ api: "http://localhost:8000",
249
+ appName: "",
250
+ userId: "user-1",
251
+ }),
252
+ ).toThrow('createAdkStream direct mode requires a non-empty "appName".');
253
+ expect(mockFetch).not.toHaveBeenCalled();
254
+ });
255
+
256
+ it.each([
257
+ ["missing", undefined],
258
+ ["empty", ""],
259
+ ])("rejects direct mode with a %s userId", (_label, userId) => {
260
+ expect(() =>
261
+ createAdkStream({
262
+ api: "http://localhost:8000",
263
+ appName: "my-app",
264
+ userId,
265
+ }),
266
+ ).toThrow(
267
+ 'createAdkStream direct mode requires "userId" when "appName" is provided.',
268
+ );
269
+ expect(mockFetch).not.toHaveBeenCalled();
270
+ });
271
+
223
272
  it("POSTs to /run_sse with ADK-native body", async () => {
224
- mockFetch.mockResolvedValueOnce(new Response(sseBody(""), { status: 200 }));
273
+ mockFetch.mockResolvedValueOnce(sseResponse(sseBody("")));
225
274
 
226
275
  const stream = createAdkStream({
227
276
  api: "http://localhost:8000",
@@ -250,11 +299,33 @@ describe("createAdkStream - direct mode", () => {
250
299
  });
251
300
  });
252
301
 
302
+ it.each(["http://localhost:8000/", "http://localhost:8000//"])(
303
+ "normalizes trailing slashes in the api URL: %s",
304
+ async (api) => {
305
+ mockFetch.mockResolvedValueOnce(sseResponse(sseBody("")));
306
+
307
+ const stream = createAdkStream({
308
+ api,
309
+ appName: "my-app",
310
+ userId: "user-1",
311
+ });
312
+ const gen = await stream(
313
+ [{ id: "m1", type: "human", content: "Hello" }],
314
+ makeConfig(),
315
+ );
316
+ for await (const _ of gen) {
317
+ /* noop */
318
+ }
319
+
320
+ expect(mockFetch.mock.calls[0]![0]).toBe("http://localhost:8000/run_sse");
321
+ },
322
+ );
323
+
253
324
  it("calls config.initialize() to get the sessionId", async () => {
254
325
  const initialize = vi
255
326
  .fn()
256
327
  .mockResolvedValue({ remoteId: "r1", externalId: "s-42" });
257
- mockFetch.mockResolvedValueOnce(new Response(sseBody(""), { status: 200 }));
328
+ mockFetch.mockResolvedValueOnce(sseResponse(sseBody("")));
258
329
 
259
330
  const stream = createAdkStream({
260
331
  api: "http://localhost:8000",
@@ -275,7 +346,7 @@ describe("createAdkStream - direct mode", () => {
275
346
  });
276
347
 
277
348
  it("converts tool messages to functionResponse parts", async () => {
278
- mockFetch.mockResolvedValueOnce(new Response(sseBody(""), { status: 200 }));
349
+ mockFetch.mockResolvedValueOnce(sseResponse(sseBody("")));
279
350
 
280
351
  const stream = createAdkStream({
281
352
  api: "http://localhost:8000",
@@ -307,7 +378,7 @@ describe("createAdkStream - direct mode", () => {
307
378
  });
308
379
 
309
380
  it("falls back to raw string when tool content is not valid JSON", async () => {
310
- mockFetch.mockResolvedValueOnce(new Response(sseBody(""), { status: 200 }));
381
+ mockFetch.mockResolvedValueOnce(sseResponse(sseBody("")));
311
382
 
312
383
  const stream = createAdkStream({
313
384
  api: "http://localhost:8000",
@@ -333,7 +404,7 @@ describe("createAdkStream - direct mode", () => {
333
404
  });
334
405
 
335
406
  it("sends empty text part when no messages provided", async () => {
336
- mockFetch.mockResolvedValueOnce(new Response(sseBody(""), { status: 200 }));
407
+ mockFetch.mockResolvedValueOnce(sseResponse(sseBody("")));
337
408
 
338
409
  const stream = createAdkStream({
339
410
  api: "http://localhost:8000",
@@ -359,9 +430,7 @@ describe("createAdkStream - SSE parsing", () => {
359
430
  { id: "e2", content: { parts: [{ text: "world" }] } },
360
431
  ];
361
432
  const text = events.map((e) => `data: ${JSON.stringify(e)}\n\n`).join("");
362
- mockFetch.mockResolvedValueOnce(
363
- new Response(sseBody(text), { status: 200 }),
364
- );
433
+ mockFetch.mockResolvedValueOnce(sseResponse(sseBody(text)));
365
434
 
366
435
  const stream = createAdkStream({ api: "/api/adk" });
367
436
  const gen = await stream(
@@ -378,11 +447,47 @@ describe("createAdkStream - SSE parsing", () => {
378
447
  expect(collected[1]!.id).toBe("e2");
379
448
  });
380
449
 
450
+ it("accepts parameterized event-stream content types", async () => {
451
+ const event: AdkEvent = { id: "e1" };
452
+ mockFetch.mockResolvedValueOnce(
453
+ sseResponse(
454
+ sseBody(`data: ${JSON.stringify(event)}\n\n`),
455
+ "Text/Event-Stream; charset=utf-8",
456
+ ),
457
+ );
458
+
459
+ const stream = createAdkStream({ api: "/api/adk" });
460
+ const gen = await stream(
461
+ [{ id: "m1", type: "human", content: "Hi" }],
462
+ makeConfig(),
463
+ );
464
+ const collected: AdkEvent[] = [];
465
+ for await (const value of gen) collected.push(value);
466
+
467
+ expect(collected).toEqual([event]);
468
+ });
469
+
381
470
  it("skips :ok SSE comments", async () => {
382
471
  const text = `:ok\n\ndata: ${JSON.stringify({ id: "e1" })}\n\n`;
383
- mockFetch.mockResolvedValueOnce(
384
- new Response(sseBody(text), { status: 200 }),
472
+ mockFetch.mockResolvedValueOnce(sseResponse(sseBody(text)));
473
+
474
+ const stream = createAdkStream({ api: "/api/adk" });
475
+ const gen = await stream(
476
+ [{ id: "m1", type: "human", content: "Hi" }],
477
+ makeConfig(),
385
478
  );
479
+ const collected: AdkEvent[] = [];
480
+ for await (const evt of gen) {
481
+ collected.push(evt);
482
+ }
483
+
484
+ expect(collected).toHaveLength(1);
485
+ expect(collected[0]!.id).toBe("e1");
486
+ });
487
+
488
+ it("parses data fields without a space after the colon", async () => {
489
+ const text = `data:${JSON.stringify({ id: "e1" })}\n\n`;
490
+ mockFetch.mockResolvedValueOnce(sseResponse(sseBody(text)));
386
491
 
387
492
  const stream = createAdkStream({ api: "/api/adk" });
388
493
  const gen = await stream(
@@ -398,6 +503,56 @@ describe("createAdkStream - SSE parsing", () => {
398
503
  expect(collected[0]!.id).toBe("e1");
399
504
  });
400
505
 
506
+ it("parses CR-delimited SSE events", async () => {
507
+ const events: AdkEvent[] = [{ id: "e1" }, { id: "e2" }];
508
+ const text = events.map((e) => `data: ${JSON.stringify(e)}\r\r`).join("");
509
+ mockFetch.mockResolvedValueOnce(sseResponse(sseBody(text)));
510
+
511
+ const stream = createAdkStream({ api: "/api/adk" });
512
+ const gen = await stream(
513
+ [{ id: "m1", type: "human", content: "Hi" }],
514
+ makeConfig(),
515
+ );
516
+ const collected: AdkEvent[] = [];
517
+ for await (const evt of gen) {
518
+ collected.push(evt);
519
+ }
520
+
521
+ expect(collected).toHaveLength(2);
522
+ expect(collected[0]!.id).toBe("e1");
523
+ expect(collected[1]!.id).toBe("e2");
524
+ });
525
+
526
+ it("emits CRLF-delimited SSE events before the response closes", async () => {
527
+ const encoder = new TextEncoder();
528
+ let controller!: ReadableStreamDefaultController<Uint8Array>;
529
+ const body = new ReadableStream<Uint8Array>({
530
+ start(nextController) {
531
+ controller = nextController;
532
+ },
533
+ });
534
+ mockFetch.mockResolvedValueOnce(sseResponse(body));
535
+
536
+ const stream = createAdkStream({ api: "/api/adk" });
537
+ const gen = await stream(
538
+ [{ id: "m1", type: "human", content: "Hi" }],
539
+ makeConfig(),
540
+ );
541
+
542
+ const first = gen.next();
543
+ controller.enqueue(
544
+ encoder.encode(`data: ${JSON.stringify({ id: "e1" })}\r`),
545
+ );
546
+ controller.enqueue(encoder.encode("\n\r\n"));
547
+
548
+ await expect(nextWithTimeout(first)).resolves.toMatchObject({
549
+ done: false,
550
+ value: { id: "e1" },
551
+ });
552
+
553
+ controller.close();
554
+ });
555
+
401
556
  it("handles partial chunks that split across reads", async () => {
402
557
  const event = { id: "e1", content: { parts: [{ text: "split" }] } };
403
558
  const fullText = `data: ${JSON.stringify(event)}\n\n`;
@@ -413,7 +568,34 @@ describe("createAdkStream - SSE parsing", () => {
413
568
  controller.close();
414
569
  },
415
570
  });
416
- mockFetch.mockResolvedValueOnce(new Response(body, { status: 200 }));
571
+ mockFetch.mockResolvedValueOnce(sseResponse(body));
572
+
573
+ const stream = createAdkStream({ api: "/api/adk" });
574
+ const gen = await stream(
575
+ [{ id: "m1", type: "human", content: "Hi" }],
576
+ makeConfig(),
577
+ );
578
+ const collected: AdkEvent[] = [];
579
+ for await (const evt of gen) {
580
+ collected.push(evt);
581
+ }
582
+
583
+ expect(collected).toHaveLength(1);
584
+ expect(collected[0]!.id).toBe("e1");
585
+ });
586
+
587
+ it("parses CR-delimited events split across chunks", async () => {
588
+ const encoder = new TextEncoder();
589
+ const body = new ReadableStream<Uint8Array>({
590
+ start(controller) {
591
+ controller.enqueue(
592
+ encoder.encode(`data: ${JSON.stringify({ id: "e1" })}\r`),
593
+ );
594
+ controller.enqueue(encoder.encode("\r"));
595
+ controller.close();
596
+ },
597
+ });
598
+ mockFetch.mockResolvedValueOnce(sseResponse(body));
417
599
 
418
600
  const stream = createAdkStream({ api: "/api/adk" });
419
601
  const gen = await stream(
@@ -433,9 +615,7 @@ describe("createAdkStream - SSE parsing", () => {
433
615
  // No trailing \n\n
434
616
  const event = { id: "e1" };
435
617
  const text = `data: ${JSON.stringify(event)}\n`;
436
- mockFetch.mockResolvedValueOnce(
437
- new Response(sseBody(text), { status: 200 }),
438
- );
618
+ mockFetch.mockResolvedValueOnce(sseResponse(sseBody(text)));
439
619
 
440
620
  const stream = createAdkStream({ api: "/api/adk" });
441
621
  const gen = await stream(
@@ -471,13 +651,57 @@ describe("createAdkStream - error handling", () => {
471
651
  }
472
652
  }).rejects.toThrow("ADK request failed: 404 Not Found");
473
653
  });
654
+
655
+ it("rejects successful HTML responses and cancels their body", async () => {
656
+ const cancelBody = vi.fn();
657
+ const body = new ReadableStream<Uint8Array>({
658
+ start(controller) {
659
+ controller.enqueue(new TextEncoder().encode("<html>Sign in</html>"));
660
+ },
661
+ cancel: cancelBody,
662
+ });
663
+ mockFetch.mockResolvedValueOnce(
664
+ sseResponse(body, "text/html; charset=utf-8"),
665
+ );
666
+
667
+ const stream = createAdkStream({ api: "/api/adk" });
668
+ await expect(async () => {
669
+ const gen = await stream(
670
+ [{ id: "m1", type: "human", content: "Hi" }],
671
+ makeConfig(),
672
+ );
673
+ for await (const _ of gen) {
674
+ /* noop */
675
+ }
676
+ }).rejects.toThrow(
677
+ 'Expected ADK stream response Content-Type "text/event-stream", received "text/html; charset=utf-8"',
678
+ );
679
+ expect(cancelBody).toHaveBeenCalledOnce();
680
+ });
681
+
682
+ it("rejects successful responses without a content type", async () => {
683
+ mockFetch.mockResolvedValueOnce(sseResponse(sseBody(""), null));
684
+
685
+ const stream = createAdkStream({ api: "/api/adk" });
686
+ await expect(async () => {
687
+ const gen = await stream(
688
+ [{ id: "m1", type: "human", content: "Hi" }],
689
+ makeConfig(),
690
+ );
691
+ for await (const _ of gen) {
692
+ /* noop */
693
+ }
694
+ }).rejects.toThrow(
695
+ 'Expected ADK stream response Content-Type "text/event-stream", received no Content-Type header',
696
+ );
697
+ });
474
698
  });
475
699
 
476
700
  // ── Headers ──
477
701
 
478
702
  describe("createAdkStream - headers", () => {
479
703
  it("sends static headers", async () => {
480
- mockFetch.mockResolvedValueOnce(new Response(sseBody(""), { status: 200 }));
704
+ mockFetch.mockResolvedValueOnce(sseResponse(sseBody("")));
481
705
 
482
706
  const stream = createAdkStream({
483
707
  api: "/api/adk",
@@ -500,7 +724,7 @@ describe("createAdkStream - headers", () => {
500
724
  });
501
725
 
502
726
  it("resolves dynamic headers from a function", async () => {
503
- mockFetch.mockResolvedValueOnce(new Response(sseBody(""), { status: 200 }));
727
+ mockFetch.mockResolvedValueOnce(sseResponse(sseBody("")));
504
728
 
505
729
  const stream = createAdkStream({
506
730
  api: "/api/adk",
@@ -522,7 +746,7 @@ describe("createAdkStream - headers", () => {
522
746
  });
523
747
 
524
748
  it("resolves async dynamic headers", async () => {
525
- mockFetch.mockResolvedValueOnce(new Response(sseBody(""), { status: 200 }));
749
+ mockFetch.mockResolvedValueOnce(sseResponse(sseBody("")));
526
750
 
527
751
  const stream = createAdkStream({
528
752
  api: "/api/adk",
@@ -544,7 +768,7 @@ describe("createAdkStream - headers", () => {
544
768
  });
545
769
 
546
770
  it("sends no extra headers when headers option is undefined", async () => {
547
- mockFetch.mockResolvedValueOnce(new Response(sseBody(""), { status: 200 }));
771
+ mockFetch.mockResolvedValueOnce(sseResponse(sseBody("")));
548
772
 
549
773
  const stream = createAdkStream({ api: "/api/adk" });
550
774
  const gen = await stream(
@@ -567,7 +791,7 @@ describe("createAdkStream - headers", () => {
567
791
 
568
792
  describe("createAdkStream - AbortSignal", () => {
569
793
  it("forwards the AbortSignal to fetch", async () => {
570
- mockFetch.mockResolvedValueOnce(new Response(sseBody(""), { status: 200 }));
794
+ mockFetch.mockResolvedValueOnce(sseResponse(sseBody("")));
571
795
 
572
796
  const controller = new AbortController();
573
797
  const stream = createAdkStream({ api: "/api/adk" });
@@ -587,7 +811,7 @@ describe("createAdkStream - AbortSignal", () => {
587
811
 
588
812
  describe("createAdkStream - content conversion", () => {
589
813
  it("converts reasoning content parts to thought parts in direct mode", async () => {
590
- mockFetch.mockResolvedValueOnce(new Response(sseBody(""), { status: 200 }));
814
+ mockFetch.mockResolvedValueOnce(sseResponse(sseBody("")));
591
815
 
592
816
  const stream = createAdkStream({
593
817
  api: "http://localhost:8000",
@@ -614,7 +838,7 @@ describe("createAdkStream - content conversion", () => {
614
838
  });
615
839
 
616
840
  it("converts image_url content parts to fileData in direct mode", async () => {
617
- mockFetch.mockResolvedValueOnce(new Response(sseBody(""), { status: 200 }));
841
+ mockFetch.mockResolvedValueOnce(sseResponse(sseBody("")));
618
842
 
619
843
  const stream = createAdkStream({
620
844
  api: "http://localhost:8000",
@@ -640,7 +864,7 @@ describe("createAdkStream - content conversion", () => {
640
864
  });
641
865
 
642
866
  it("converts file content parts to inlineData in direct mode", async () => {
643
- mockFetch.mockResolvedValueOnce(new Response(sseBody(""), { status: 200 }));
867
+ mockFetch.mockResolvedValueOnce(sseResponse(sseBody("")));
644
868
 
645
869
  const stream = createAdkStream({
646
870
  api: "http://localhost:8000",
@@ -673,7 +897,7 @@ describe("createAdkStream - content conversion", () => {
673
897
  });
674
898
 
675
899
  it("converts file_url content parts to fileData with mimeType in direct mode", async () => {
676
- mockFetch.mockResolvedValueOnce(new Response(sseBody(""), { status: 200 }));
900
+ mockFetch.mockResolvedValueOnce(sseResponse(sseBody("")));
677
901
 
678
902
  const stream = createAdkStream({
679
903
  api: "http://localhost:8000",
@@ -708,7 +932,7 @@ describe("createAdkStream - content conversion", () => {
708
932
  });
709
933
 
710
934
  it("converts code content parts to executableCode", async () => {
711
- mockFetch.mockResolvedValueOnce(new Response(sseBody(""), { status: 200 }));
935
+ mockFetch.mockResolvedValueOnce(sseResponse(sseBody("")));
712
936
 
713
937
  const stream = createAdkStream({
714
938
  api: "http://localhost:8000",
@@ -734,7 +958,7 @@ describe("createAdkStream - content conversion", () => {
734
958
  });
735
959
 
736
960
  it("converts code_result content parts to codeExecutionResult", async () => {
737
- mockFetch.mockResolvedValueOnce(new Response(sseBody(""), { status: 200 }));
961
+ mockFetch.mockResolvedValueOnce(sseResponse(sseBody("")));
738
962
 
739
963
  const stream = createAdkStream({
740
964
  api: "http://localhost:8000",
package/src/AdkClient.ts CHANGED
@@ -1,4 +1,6 @@
1
+ import { SSEEventDecoder } from "assistant-stream/utils";
1
2
  import { contentToParts } from "./contentToParts";
3
+ import { trimTrailingSlashes } from "./trimTrailingSlashes";
2
4
  import type {
3
5
  AdkEvent,
4
6
  AdkEventPart,
@@ -58,7 +60,18 @@ export type CreateAdkStreamOptions = {
58
60
  export function createAdkStream(
59
61
  options: CreateAdkStreamOptions,
60
62
  ): AdkStreamCallback {
63
+ if (options.appName === "") {
64
+ throw new Error(
65
+ 'createAdkStream direct mode requires a non-empty "appName".',
66
+ );
67
+ }
68
+
61
69
  const isDirect = options.appName != null;
70
+ if (isDirect && (options.userId == null || options.userId === "")) {
71
+ throw new Error(
72
+ 'createAdkStream direct mode requires "userId" when "appName" is provided.',
73
+ );
74
+ }
62
75
 
63
76
  return async function* (messages, config) {
64
77
  const headers = await resolveHeaders(options.headers);
@@ -68,7 +81,7 @@ export function createAdkStream(
68
81
 
69
82
  if (isDirect) {
70
83
  // Direct mode: POST to ADK server's /run_sse
71
- url = `${options.api}/run_sse`;
84
+ url = `${trimTrailingSlashes(options.api)}/run_sse`;
72
85
  const { externalId } = await config.initialize();
73
86
  body = {
74
87
  appName: options.appName,
@@ -97,12 +110,27 @@ export function createAdkStream(
97
110
  );
98
111
  }
99
112
 
113
+ validateEventStreamContentType(response);
100
114
  yield* parseSSEResponse(response);
101
115
  };
102
116
  }
103
117
 
104
118
  // ── Internal helpers ──
105
119
 
120
+ function validateEventStreamContentType(response: Response): void {
121
+ const contentType = response.headers.get("Content-Type");
122
+ const mediaType = contentType?.split(";", 1)[0]?.trim().toLowerCase();
123
+ if (mediaType !== "text/event-stream") {
124
+ const received = contentType
125
+ ? `"${contentType}"`
126
+ : "no Content-Type header";
127
+ void response.body?.cancel().catch(() => undefined);
128
+ throw new Error(
129
+ `Expected ADK stream response Content-Type "text/event-stream", received ${received}`,
130
+ );
131
+ }
132
+ }
133
+
106
134
  async function resolveHeaders(
107
135
  headers:
108
136
  | Record<string, string>
@@ -213,34 +241,27 @@ function messagesToProxyBody(
213
241
  async function* parseSSEResponse(response: Response): AsyncGenerator<AdkEvent> {
214
242
  const reader = response.body!.getReader();
215
243
  const decoder = new TextDecoder();
216
- let buffer = "";
244
+ const sseDecoder = new SSEEventDecoder({ trailing: "dispatch" });
217
245
 
218
246
  try {
219
247
  while (true) {
220
248
  const { done, value } = await reader.read();
221
- if (done) break;
222
-
223
- buffer += decoder.decode(value, { stream: true });
224
- const parts = buffer.split("\n\n");
225
- buffer = parts.pop() ?? "";
226
-
227
- for (const part of parts) {
228
- for (const line of part.split("\n")) {
229
- if (line.startsWith("data: ")) {
230
- yield JSON.parse(line.slice(6)) as AdkEvent;
231
- }
249
+ if (done) {
250
+ for (const event of sseDecoder.push(decoder.decode())) {
251
+ yield JSON.parse(event.data) as AdkEvent;
232
252
  }
253
+ break;
233
254
  }
234
- }
235
255
 
236
- // Handle remaining buffer
237
- if (buffer.trim()) {
238
- for (const line of buffer.split("\n")) {
239
- if (line.startsWith("data: ")) {
240
- yield JSON.parse(line.slice(6)) as AdkEvent;
241
- }
256
+ for (const event of sseDecoder.push(
257
+ decoder.decode(value, { stream: true }),
258
+ )) {
259
+ yield JSON.parse(event.data) as AdkEvent;
242
260
  }
243
261
  }
262
+
263
+ const trailing = sseDecoder.flush();
264
+ if (trailing !== null) yield JSON.parse(trailing.data) as AdkEvent;
244
265
  } finally {
245
266
  reader.releaseLock();
246
267
  }