@assistant-ui/react-google-adk 0.0.25 → 0.0.26

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 (50) hide show
  1. package/dist/AdkClient.d.ts.map +1 -1
  2. package/dist/AdkClient.js +13 -3
  3. package/dist/AdkClient.js.map +1 -1
  4. package/dist/AdkEventAccumulator.d.ts.map +1 -1
  5. package/dist/AdkEventAccumulator.js +11 -1
  6. package/dist/AdkEventAccumulator.js.map +1 -1
  7. package/dist/AdkSessionAdapter.d.ts.map +1 -1
  8. package/dist/AdkSessionAdapter.js +2 -1
  9. package/dist/AdkSessionAdapter.js.map +1 -1
  10. package/dist/adkToolApproval.d.ts +40 -0
  11. package/dist/adkToolApproval.d.ts.map +1 -0
  12. package/dist/adkToolApproval.js +166 -0
  13. package/dist/adkToolApproval.js.map +1 -0
  14. package/dist/convertAdkMessages.d.ts +3 -1
  15. package/dist/convertAdkMessages.d.ts.map +1 -1
  16. package/dist/convertAdkMessages.js +27 -16
  17. package/dist/convertAdkMessages.js.map +1 -1
  18. package/dist/hooks.d.ts.map +1 -1
  19. package/dist/hooks.js +2 -11
  20. package/dist/hooks.js.map +1 -1
  21. package/dist/parseAdkEvent.d.ts +6 -0
  22. package/dist/parseAdkEvent.d.ts.map +1 -0
  23. package/dist/parseAdkEvent.js +16 -0
  24. package/dist/parseAdkEvent.js.map +1 -0
  25. package/dist/server/parseAdkRequest.d.ts.map +1 -1
  26. package/dist/server/parseAdkRequest.js +79 -13
  27. package/dist/server/parseAdkRequest.js.map +1 -1
  28. package/dist/useAdkMessages.js +104 -64
  29. package/dist/useAdkMessages.js.map +1 -1
  30. package/dist/useAdkRuntime.d.ts.map +1 -1
  31. package/dist/useAdkRuntime.js +11 -3
  32. package/dist/useAdkRuntime.js.map +1 -1
  33. package/package.json +5 -5
  34. package/src/AdkClient.test.ts +137 -0
  35. package/src/AdkClient.ts +15 -3
  36. package/src/AdkEventAccumulator.test.ts +77 -0
  37. package/src/AdkEventAccumulator.ts +24 -1
  38. package/src/AdkSessionAdapter.test.ts +145 -1
  39. package/src/AdkSessionAdapter.ts +7 -2
  40. package/src/adkToolApproval.test.ts +345 -0
  41. package/src/adkToolApproval.ts +215 -0
  42. package/src/convertAdkMessages.ts +72 -43
  43. package/src/hooks.ts +4 -16
  44. package/src/parseAdkEvent.ts +40 -0
  45. package/src/server/parseAdkRequest.test.ts +141 -0
  46. package/src/server/parseAdkRequest.ts +145 -13
  47. package/src/useAdkMessages.test.ts +313 -0
  48. package/src/useAdkMessages.ts +62 -4
  49. package/src/useAdkRuntime.ts +29 -3
  50. package/src/useAdkRuntimeApproval.test.tsx +344 -0
package/src/AdkClient.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { SSEEventDecoder } from "assistant-stream/utils";
2
2
  import { contentToParts } from "./contentToParts";
3
+ import { parseAdkEventValue } from "./parseAdkEvent";
3
4
  import { trimTrailingSlashes } from "./trimTrailingSlashes";
4
5
  import type {
5
6
  AdkEvent,
@@ -131,6 +132,17 @@ function validateEventStreamContentType(response: Response): void {
131
132
  }
132
133
  }
133
134
 
135
+ function parseAdkEvent(data: string): AdkEvent {
136
+ let value: unknown;
137
+ try {
138
+ value = JSON.parse(data);
139
+ } catch {
140
+ throw new Error("Invalid ADK stream event: expected valid JSON.");
141
+ }
142
+
143
+ return parseAdkEventValue(value, "Invalid ADK stream event");
144
+ }
145
+
134
146
  async function resolveHeaders(
135
147
  headers:
136
148
  | Record<string, string>
@@ -258,7 +270,7 @@ async function* parseSSEResponse(response: Response): AsyncGenerator<AdkEvent> {
258
270
  if (done) {
259
271
  shouldCancel = false;
260
272
  for (const event of sseDecoder.push(decoder.decode())) {
261
- yield JSON.parse(event.data) as AdkEvent;
273
+ yield parseAdkEvent(event.data);
262
274
  }
263
275
  break;
264
276
  }
@@ -266,12 +278,12 @@ async function* parseSSEResponse(response: Response): AsyncGenerator<AdkEvent> {
266
278
  for (const event of sseDecoder.push(
267
279
  decoder.decode(value, { stream: true }),
268
280
  )) {
269
- yield JSON.parse(event.data) as AdkEvent;
281
+ yield parseAdkEvent(event.data);
270
282
  }
271
283
  }
272
284
 
273
285
  const trailing = sseDecoder.flush();
274
- if (trailing !== null) yield JSON.parse(trailing.data) as AdkEvent;
286
+ if (trailing !== null) yield parseAdkEvent(trailing.data);
275
287
  } finally {
276
288
  try {
277
289
  if (shouldCancel) await reader.cancel().catch(() => undefined);
@@ -240,6 +240,29 @@ describe("AdkEventAccumulator - function responses", () => {
240
240
  });
241
241
  });
242
242
 
243
+ it("skips a user function response that answers no call", () => {
244
+ const acc = new AdkEventAccumulator();
245
+ const msgs = acc.processEvent(
246
+ makeEvent({
247
+ author: "user",
248
+ content: {
249
+ role: "user",
250
+ parts: [
251
+ {
252
+ functionResponse: {
253
+ name: "adk_request_confirmation",
254
+ response: { confirmed: true },
255
+ },
256
+ },
257
+ ],
258
+ },
259
+ }),
260
+ );
261
+ // Without an id it answers no call: core drops it as an orphan, and
262
+ // keeping it would let it settle the confirmation batch it grouped into.
263
+ expect(msgs.filter((m) => m.type === "tool")).toEqual([]);
264
+ });
265
+
243
266
  // A session load replays the stored events through a fresh accumulator.
244
267
  it("gives a tool message the same id on every replay of an event", () => {
245
268
  const event = makeEvent({
@@ -1091,6 +1114,60 @@ describe("AdkEventAccumulator - user message handling", () => {
1091
1114
  });
1092
1115
  });
1093
1116
 
1117
+ it("creates a tool message for a user-authored function response", () => {
1118
+ const acc = new AdkEventAccumulator();
1119
+ const msgs = acc.processEvent(
1120
+ makeEvent({
1121
+ author: "user",
1122
+ content: {
1123
+ role: "user",
1124
+ parts: [
1125
+ {
1126
+ functionResponse: {
1127
+ id: "tc-1",
1128
+ name: "adk_request_confirmation",
1129
+ response: { confirmed: true },
1130
+ },
1131
+ },
1132
+ ],
1133
+ },
1134
+ }),
1135
+ );
1136
+ expect(msgs).toHaveLength(1);
1137
+ expect(msgs[0]).toMatchObject({
1138
+ type: "tool",
1139
+ tool_call_id: "tc-1",
1140
+ name: "adk_request_confirmation",
1141
+ content: JSON.stringify({ confirmed: true }),
1142
+ status: "success",
1143
+ });
1144
+ });
1145
+
1146
+ it("orders function responses from one user event before its text", () => {
1147
+ const acc = new AdkEventAccumulator();
1148
+ const msgs = acc.processEvent(
1149
+ makeEvent({
1150
+ author: "user",
1151
+ content: {
1152
+ role: "user",
1153
+ parts: [
1154
+ { text: "go ahead" },
1155
+ {
1156
+ functionResponse: {
1157
+ id: "tc-1",
1158
+ name: "adk_request_confirmation",
1159
+ response: { confirmed: true },
1160
+ },
1161
+ },
1162
+ ],
1163
+ },
1164
+ }),
1165
+ );
1166
+ expect(msgs).toHaveLength(2);
1167
+ expect(msgs[0]).toMatchObject({ type: "tool", tool_call_id: "tc-1" });
1168
+ expect(msgs[1]).toMatchObject({ type: "human", content: "go ahead" });
1169
+ });
1170
+
1094
1171
  it("creates separate human and AI messages for a user/agent turn", () => {
1095
1172
  const acc = new AdkEventAccumulator();
1096
1173
  acc.processEvent(
@@ -331,7 +331,8 @@ export class AdkEventAccumulator {
331
331
  if (event.author === "user") {
332
332
  this.finalizeCurrentMessage();
333
333
  const humanParts: AdkMessageContentPart[] = [];
334
- for (const part of parts) {
334
+ const toolMessages: AdkMessage[] = [];
335
+ for (const [index, part] of parts.entries()) {
335
336
  if (part.text != null && !part.thought) {
336
337
  humanParts.push({ type: "text", text: part.text });
337
338
  } else if (part.inlineData) {
@@ -342,8 +343,30 @@ export class AdkEventAccumulator {
342
343
  humanParts.push(
343
344
  fileDataToPart(part.fileData.fileUri, part.fileData.mimeType),
344
345
  );
346
+ } else if (part.functionResponse?.id) {
347
+ // ADK records tool confirmation and other client-supplied tool
348
+ // results as user-authored function responses, and its request
349
+ // confirmation processors search user events for them. Dropping
350
+ // them here would replay a settled gate as pending. A response
351
+ // carrying no id answers no call: core drops it as an orphan, and
352
+ // keeping it would let it settle the batch it was grouped into.
353
+ toolMessages.push({
354
+ id: toolMessageId(event, index),
355
+ type: "tool",
356
+ tool_call_id: part.functionResponse.id,
357
+ name: part.functionResponse.name,
358
+ content: JSON.stringify(part.functionResponse.response),
359
+ status: "success",
360
+ });
345
361
  }
346
362
  }
363
+ // The replies answer the preceding assistant turn, so they are emitted
364
+ // before any user content in the same event. A human message between the
365
+ // tool call and its reply splits them into separate converted messages,
366
+ // orphaning the reply and leaving its gate unsettled.
367
+ for (const toolMsg of toolMessages) {
368
+ this.messagesMap.set(toolMsg.id, toolMsg);
369
+ }
347
370
  if (humanParts.length > 0) {
348
371
  const id = event.id ?? uuidv4();
349
372
  const first = humanParts[0];
@@ -1,5 +1,6 @@
1
- import { describe, it, expect, vi, beforeEach } from "vitest";
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
2
  import { createAdkSessionAdapter } from "./AdkSessionAdapter";
3
+ import { projectAdkToolApprovals } from "./adkToolApproval";
3
4
 
4
5
  // ── Helpers ──
5
6
 
@@ -410,6 +411,45 @@ describe("createAdkSessionAdapter - load", () => {
410
411
  );
411
412
  });
412
413
 
414
+ it("rejects malformed events inside a session history", async () => {
415
+ mockFetch.mockResolvedValueOnce(
416
+ new Response(JSON.stringify({ id: "s1", events: [{}] }), {
417
+ status: 200,
418
+ }),
419
+ );
420
+
421
+ const { load } = createAdkSessionAdapter(baseOptions);
422
+
423
+ await expect(load("s1")).rejects.toThrow(
424
+ "Invalid ADK session event at index 0: expected a non-empty object.",
425
+ );
426
+ });
427
+
428
+ it("rejects partial replay when a history event is malformed", async () => {
429
+ mockFetch.mockResolvedValueOnce(
430
+ new Response(
431
+ JSON.stringify({
432
+ id: "s1",
433
+ events: [
434
+ {
435
+ id: "e1",
436
+ author: "user",
437
+ content: { role: "user", parts: [{ text: "Hello" }] },
438
+ },
439
+ {},
440
+ ],
441
+ }),
442
+ { status: 200 },
443
+ ),
444
+ );
445
+
446
+ const { load } = createAdkSessionAdapter(baseOptions);
447
+
448
+ await expect(load("s1")).rejects.toThrow(
449
+ "Invalid ADK session event at index 1: expected a non-empty object.",
450
+ );
451
+ });
452
+
413
453
  it("throws when session fetch fails", async () => {
414
454
  mockFetch.mockResolvedValueOnce(
415
455
  new Response("Server error", { status: 500 }),
@@ -448,6 +488,110 @@ describe("createAdkSessionAdapter - load", () => {
448
488
  });
449
489
  });
450
490
 
491
+ describe("createAdkSessionAdapter - load replays tool confirmations", () => {
492
+ const CONFIRMATION_CALL = "conf-1";
493
+
494
+ const confirmationSession = (response: unknown) => ({
495
+ id: "s1",
496
+ events: [
497
+ {
498
+ id: "e1",
499
+ author: "agent",
500
+ longRunningToolIds: [CONFIRMATION_CALL],
501
+ content: {
502
+ role: "model",
503
+ parts: [
504
+ {
505
+ functionCall: {
506
+ id: CONFIRMATION_CALL,
507
+ name: "adk_request_confirmation",
508
+ args: {
509
+ originalFunctionCall: { id: "tc-1", name: "delete_file" },
510
+ toolConfirmation: { hint: "Delete /tmp/a?" },
511
+ },
512
+ },
513
+ },
514
+ ],
515
+ },
516
+ },
517
+ {
518
+ id: "e2",
519
+ author: "user",
520
+ content: {
521
+ role: "user",
522
+ parts: [
523
+ {
524
+ functionResponse: {
525
+ id: CONFIRMATION_CALL,
526
+ name: "adk_request_confirmation",
527
+ response,
528
+ },
529
+ },
530
+ ],
531
+ },
532
+ },
533
+ ],
534
+ });
535
+
536
+ const loadApprovals = async (response: unknown) => {
537
+ mockFetch.mockResolvedValueOnce(
538
+ new Response(JSON.stringify(confirmationSession(response)), {
539
+ status: 200,
540
+ }),
541
+ );
542
+ const { load } = createAdkSessionAdapter(baseOptions);
543
+ const result = await load("s1");
544
+ return {
545
+ messages: result.messages,
546
+ approvals: projectAdkToolApprovals(result.messages).approvals,
547
+ };
548
+ };
549
+
550
+ it("keeps a user-authored confirmation reply as a tool message", async () => {
551
+ const { messages } = await loadApprovals({ confirmed: true });
552
+
553
+ expect(messages.map((m) => m.type)).toEqual(["ai", "tool"]);
554
+ expect(messages[1]).toMatchObject({
555
+ type: "tool",
556
+ tool_call_id: CONFIRMATION_CALL,
557
+ name: "adk_request_confirmation",
558
+ });
559
+ });
560
+
561
+ it.each([
562
+ ["direct", { confirmed: true }, { approved: true }],
563
+ ["direct denial", { confirmed: false }, { approved: false }],
564
+ [
565
+ "wrapped",
566
+ { response: JSON.stringify({ confirmed: true }) },
567
+ { approved: true },
568
+ ],
569
+ [
570
+ "wrapped denial",
571
+ { response: JSON.stringify({ confirmed: false }) },
572
+ { approved: false },
573
+ ],
574
+ ])(
575
+ "projects a settled gate from a replayed %s reply",
576
+ async (_name, response, expected) => {
577
+ const { approvals } = await loadApprovals(response);
578
+
579
+ expect(approvals.get(CONFIRMATION_CALL)).toEqual({
580
+ id: CONFIRMATION_CALL,
581
+ ...expected,
582
+ });
583
+ },
584
+ );
585
+
586
+ it("leaves the gate pending when the replayed reply is unreadable", async () => {
587
+ const { approvals } = await loadApprovals({ response: "not json" });
588
+
589
+ expect(approvals.get(CONFIRMATION_CALL)).toEqual({
590
+ id: CONFIRMATION_CALL,
591
+ });
592
+ });
593
+ });
594
+
451
595
  describe("createAdkSessionAdapter - artifacts", () => {
452
596
  it("lists names from current and legacy artifact responses", async () => {
453
597
  mockFetch.mockResolvedValueOnce(
@@ -6,7 +6,8 @@ import type {
6
6
  RemoteThreadMetadata,
7
7
  } from "@assistant-ui/core";
8
8
  import { AdkEventAccumulator } from "./AdkEventAccumulator";
9
- import type { AdkEvent, AdkMessage, AdkThreadSnapshot } from "./types";
9
+ import { parseAdkEventValue } from "./parseAdkEvent";
10
+ import type { AdkMessage, AdkThreadSnapshot } from "./types";
10
11
  import { trimTrailingSlashes } from "./trimTrailingSlashes";
11
12
 
12
13
  export type AdkSessionAdapterOptions = {
@@ -323,7 +324,11 @@ export function createAdkSessionAdapter(
323
324
  );
324
325
  }
325
326
 
326
- const events = session.events as AdkEvent[] | undefined;
327
+ // Events carry ordered state and tool transitions, so partial replay can
328
+ // produce a plausible but incorrect thread.
329
+ const events = session.events?.map((event, index) =>
330
+ parseAdkEventValue(event, `Invalid ADK session event at index ${index}`),
331
+ );
327
332
 
328
333
  if (!events?.length) {
329
334
  return { messages: [] };