@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
package/src/AdkClient.test.ts
CHANGED
|
@@ -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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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)
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
}
|