@assistant-ui/react-google-adk 0.0.18 → 0.0.20

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.
@@ -452,13 +452,6 @@ describe("AdkEventAccumulator - HITL requires-action", () => {
452
452
  expect(aiMsg.status).toBeUndefined();
453
453
  });
454
454
 
455
- it("matches tool call id to the longRunningToolIds entry", () => {
456
- const acc = new AdkEventAccumulator();
457
- const msgs = acc.processEvent(makeHitlEvent("tc-1"));
458
- const aiMsg = msgs[0] as AdkMessage & { type: "ai" };
459
- expect(aiMsg.tool_calls![0]!.id).toBe("tc-1");
460
- });
461
-
462
455
  it("stays pending across a subsequent bookkeeping event", () => {
463
456
  const acc = new AdkEventAccumulator();
464
457
  acc.processEvent(makeHitlEvent("tc-1", { author: "WorkflowA" }));
@@ -480,35 +473,6 @@ describe("AdkEventAccumulator - HITL requires-action", () => {
480
473
  expect(aiMsg!.status).toBeUndefined();
481
474
  });
482
475
 
483
- it("assigns manual complete status on non-HITL final event", () => {
484
- const acc = new AdkEventAccumulator();
485
- const msgs = acc.processEvent(
486
- makeEvent({
487
- author: "agent",
488
- content: { role: "model", parts: [{ text: "Done." }] },
489
- }),
490
- );
491
-
492
- expect(msgs[0]).toMatchObject({
493
- status: { type: "complete", reason: "stop" },
494
- });
495
- });
496
-
497
- it("assigns manual complete status on skipSummarization final event", () => {
498
- const acc = new AdkEventAccumulator();
499
- const msgs = acc.processEvent(
500
- makeEvent({
501
- author: "agent",
502
- actions: { skipSummarization: true },
503
- content: { role: "model", parts: [{ text: "skipped" }] },
504
- }),
505
- );
506
-
507
- expect(msgs[0]).toMatchObject({
508
- status: { type: "complete", reason: "stop" },
509
- });
510
- });
511
-
512
476
  it("prioritizes skipSummarization over longRunningToolIds", () => {
513
477
  const acc = new AdkEventAccumulator();
514
478
  const msgs = acc.processEvent(
@@ -224,20 +224,10 @@ describe("createAdkSessionAdapter - delete", () => {
224
224
  // ── adapter.rename / archive / unarchive ──
225
225
 
226
226
  describe("createAdkSessionAdapter - no-op methods", () => {
227
- it("rename resolves without error", async () => {
227
+ it("rename/archive/unarchive resolve without hitting the network", async () => {
228
228
  const { adapter } = createAdkSessionAdapter(baseOptions);
229
229
  await expect(adapter.rename("s1", "New Title")).resolves.toBeUndefined();
230
- expect(mockFetch).not.toHaveBeenCalled();
231
- });
232
-
233
- it("archive resolves without error", async () => {
234
- const { adapter } = createAdkSessionAdapter(baseOptions);
235
230
  await expect(adapter.archive("s1")).resolves.toBeUndefined();
236
- expect(mockFetch).not.toHaveBeenCalled();
237
- });
238
-
239
- it("unarchive resolves without error", async () => {
240
- const { adapter } = createAdkSessionAdapter(baseOptions);
241
231
  await expect(adapter.unarchive("s1")).resolves.toBeUndefined();
242
232
  expect(mockFetch).not.toHaveBeenCalled();
243
233
  });
package/src/adkExtras.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { createRuntimeExtras } from "@assistant-ui/core/internal";
1
+ import { createRuntimeExtras } from "@assistant-ui/core/react";
2
2
  import type { AdkRuntimeExtras } from "./types";
3
3
 
4
4
  export const adkExtras = createRuntimeExtras<AdkRuntimeExtras>("useAdkRuntime");
@@ -25,6 +25,72 @@ describe("convertAdkMessage - human messages", () => {
25
25
  content: [{ type: "text", text: "Hello" }],
26
26
  });
27
27
  });
28
+
29
+ it("restores an audio/mp3 file part as an audio message part", () => {
30
+ const msg: AdkMessage = {
31
+ id: "m1",
32
+ type: "human",
33
+ content: [{ type: "file", mimeType: "audio/mp3", data: "QUJD" }],
34
+ };
35
+ const result = convertAdkMessage(msg, {});
36
+ expect(result).toMatchObject({
37
+ role: "user",
38
+ content: [{ type: "audio", audio: { data: "QUJD", format: "mp3" } }],
39
+ });
40
+ });
41
+
42
+ it("restores an audio/wav file part as an audio message part", () => {
43
+ const msg: AdkMessage = {
44
+ id: "m1",
45
+ type: "human",
46
+ content: [{ type: "file", mimeType: "audio/wav", data: "QUJD" }],
47
+ };
48
+ const result = convertAdkMessage(msg, {});
49
+ expect(result).toMatchObject({
50
+ role: "user",
51
+ content: [{ type: "audio", audio: { data: "QUJD", format: "wav" } }],
52
+ });
53
+ });
54
+
55
+ it("keeps attachment-derived audio file parts (with filename) as file parts", () => {
56
+ const msg: AdkMessage = {
57
+ id: "m1",
58
+ type: "human",
59
+ content: [
60
+ {
61
+ type: "file",
62
+ mimeType: "audio/wav",
63
+ data: "QUJD",
64
+ filename: "memo.wav",
65
+ },
66
+ ],
67
+ };
68
+ const result = convertAdkMessage(msg, {});
69
+ expect(result).toMatchObject({
70
+ role: "user",
71
+ content: [
72
+ {
73
+ type: "file",
74
+ mimeType: "audio/wav",
75
+ data: "QUJD",
76
+ filename: "memo.wav",
77
+ },
78
+ ],
79
+ });
80
+ });
81
+
82
+ it("keeps file parts with other audio mime types as file parts", () => {
83
+ const msg: AdkMessage = {
84
+ id: "m1",
85
+ type: "human",
86
+ content: [{ type: "file", mimeType: "audio/ogg", data: "QUJD" }],
87
+ };
88
+ const result = convertAdkMessage(msg, {});
89
+ expect(result).toMatchObject({
90
+ role: "user",
91
+ content: [{ type: "file", mimeType: "audio/ogg", data: "QUJD" }],
92
+ });
93
+ });
28
94
  });
29
95
 
30
96
  describe("convertAdkMessage - ai messages", () => {
@@ -78,6 +144,19 @@ describe("convertAdkMessage - ai messages", () => {
78
144
  });
79
145
  });
80
146
 
147
+ it("keeps audio/mp3 file parts as file parts on assistant messages", () => {
148
+ const msg: AdkMessage = {
149
+ id: "m1",
150
+ type: "ai",
151
+ content: [{ type: "file", mimeType: "audio/mp3", data: "QUJD" }],
152
+ };
153
+ const result = convertAdkMessage(msg, {});
154
+ expect(result).toMatchObject({
155
+ role: "assistant",
156
+ content: [{ type: "file", mimeType: "audio/mp3", data: "QUJD" }],
157
+ });
158
+ });
159
+
81
160
  it("converts file content parts to file message parts", () => {
82
161
  const msg: AdkMessage = {
83
162
  id: "m1",
@@ -9,9 +9,13 @@ type ContentPart =
9
9
  | { type: "reasoning"; text: string }
10
10
  | { type: "image"; image: string }
11
11
  | { type: "file"; data: string; mimeType: string; filename?: string }
12
+ | { type: "audio"; audio: { data: string; format: "mp3" | "wav" } }
12
13
  | { type: "data"; name: string; data: unknown };
13
14
 
14
- const contentToParts = (content: AdkMessage["content"]): ContentPart[] => {
15
+ const contentToParts = (
16
+ content: AdkMessage["content"],
17
+ role: "user" | "assistant",
18
+ ): ContentPart[] => {
15
19
  if (typeof content === "string")
16
20
  return [{ type: "text" as const, text: content }];
17
21
 
@@ -29,13 +33,25 @@ const contentToParts = (content: AdkMessage["content"]): ContentPart[] => {
29
33
  };
30
34
  case "image_url":
31
35
  return { type: "image", image: part.url };
32
- case "file":
36
+ case "file": {
37
+ const format =
38
+ role === "user" && part.filename == null
39
+ ? part.mimeType === "audio/wav"
40
+ ? ("wav" as const)
41
+ : part.mimeType === "audio/mp3"
42
+ ? ("mp3" as const)
43
+ : null
44
+ : null;
45
+ if (format) {
46
+ return { type: "audio", audio: { data: part.data, format } };
47
+ }
33
48
  return {
34
49
  type: "file",
35
50
  data: part.data,
36
51
  mimeType: part.mimeType,
37
52
  ...(part.filename != null && { filename: part.filename }),
38
53
  };
54
+ }
39
55
  case "file_url":
40
56
  return {
41
57
  type: "data",
@@ -72,7 +88,7 @@ export const convertAdkMessage: useExternalMessageConverter.Callback<
72
88
  return {
73
89
  role: "user",
74
90
  id: message.id,
75
- content: contentToParts(message.content),
91
+ content: contentToParts(message.content, "user"),
76
92
  };
77
93
 
78
94
  case "ai": {
@@ -88,7 +104,10 @@ export const convertAdkMessage: useExternalMessageConverter.Callback<
88
104
  return {
89
105
  role: "assistant",
90
106
  id: message.id,
91
- content: [...contentToParts(message.content), ...toolCallParts],
107
+ content: [
108
+ ...contentToParts(message.content, "assistant"),
109
+ ...toolCallParts,
110
+ ],
92
111
  ...(message.status && { status: message.status }),
93
112
  ...(message.author && {
94
113
  metadata: {
@@ -106,7 +106,7 @@ export const useAdkMessages = ({
106
106
  ...config,
107
107
  abortSignal: abortController.signal,
108
108
  initialize: async () => {
109
- return await aui.threadListItem().initialize();
109
+ return await aui.threadListItem.initialize();
110
110
  },
111
111
  });
112
112
 
@@ -208,4 +208,65 @@ describe("getMessageContent", () => {
208
208
  { type: "file", mimeType: "application/pdf", data: "AAAA" },
209
209
  ]);
210
210
  });
211
+
212
+ it("forwards an audio part as a file block with the format-derived mime type", () => {
213
+ const result = getMessageContent(
214
+ makeAppendMessage([
215
+ { type: "audio", audio: { data: "QUJD", format: "mp3" } },
216
+ ]),
217
+ );
218
+ expect(result).toEqual([
219
+ { type: "file", mimeType: "audio/mp3", data: "QUJD" },
220
+ ]);
221
+ });
222
+
223
+ it("forwards a wav audio part with the audio/wav mime type", () => {
224
+ const result = getMessageContent(
225
+ makeAppendMessage([
226
+ { type: "audio", audio: { data: "QUJD", format: "wav" } },
227
+ ]),
228
+ );
229
+ expect(result).toEqual([
230
+ { type: "file", mimeType: "audio/wav", data: "QUJD" },
231
+ ]);
232
+ });
233
+
234
+ it("strips a data URL envelope from audio data", () => {
235
+ const result = getMessageContent(
236
+ makeAppendMessage([
237
+ {
238
+ type: "audio",
239
+ audio: { data: "data:audio/mp3;base64,QUJD", format: "mp3" },
240
+ },
241
+ ]),
242
+ );
243
+ expect(result).toEqual([
244
+ { type: "file", mimeType: "audio/mp3", data: "QUJD" },
245
+ ]);
246
+ });
247
+
248
+ it("skips data parts while keeping surrounding text", () => {
249
+ const result = getMessageContent(
250
+ makeAppendMessage([
251
+ { type: "text", text: "hi" },
252
+ { type: "data", name: "chart", data: { x: 1 } },
253
+ ]),
254
+ );
255
+ expect(result).toBe("hi");
256
+ });
257
+
258
+ it("returns empty content for a data-only message", () => {
259
+ const result = getMessageContent(
260
+ makeAppendMessage([{ type: "data", name: "chart", data: { x: 1 } }]),
261
+ );
262
+ expect(result).toEqual([]);
263
+ });
264
+
265
+ it("still throws on assistant-only part types", () => {
266
+ expect(() =>
267
+ getMessageContent(
268
+ makeAppendMessage([{ type: "reasoning", text: "thinking" }]),
269
+ ),
270
+ ).toThrow("Unsupported append message part type: reasoning");
271
+ });
211
272
  });
@@ -13,6 +13,7 @@ import {
13
13
  type ToolExecutionStatus,
14
14
  generateId,
15
15
  } from "@assistant-ui/core";
16
+ import { parseDataUrl } from "@assistant-ui/core/internal";
16
17
  import {
17
18
  useCloudThreadListAdapter,
18
19
  useRemoteThreadListRuntime,
@@ -41,7 +42,7 @@ export const getMessageContent = (msg: AppendMessage) => {
41
42
  ...msg.content,
42
43
  ...(msg.attachments?.flatMap((a) => a.content) ?? []),
43
44
  ];
44
- const content = allContent.map((part) => {
45
+ const content = allContent.flatMap((part) => {
45
46
  const type = part.type;
46
47
  switch (type) {
47
48
  case "text":
@@ -55,17 +56,22 @@ export const getMessageContent = (msg: AppendMessage) => {
55
56
  data: part.data,
56
57
  ...(part.filename != null && { filename: part.filename }),
57
58
  };
59
+ case "audio": {
60
+ const parsed = parseDataUrl(part.audio.data);
61
+ return {
62
+ type: "file" as const,
63
+ mimeType: `audio/${part.audio.format}`,
64
+ data: parsed?.data ?? part.audio.data,
65
+ };
66
+ }
67
+ case "data":
68
+ return [];
58
69
 
59
70
  case "tool-call":
60
71
  throw new Error("Tool call appends are not supported.");
61
72
 
62
73
  default: {
63
- const _exhaustiveCheck:
64
- | "reasoning"
65
- | "source"
66
- | "audio"
67
- | "data"
68
- | "generative-ui" = type;
74
+ const _exhaustiveCheck: "reasoning" | "source" | "generative-ui" = type;
69
75
  throw new Error(
70
76
  `Unsupported append message part type: ${_exhaustiveCheck}`,
71
77
  );
@@ -356,7 +362,7 @@ const useAdkRuntimeImpl = (options: UseAdkRuntimeOptions) => {
356
362
  setMessages(nextMessages);
357
363
  return;
358
364
  }
359
- const externalId = aui.threadListItem().getState().externalId;
365
+ const externalId = aui.threadListItem.getState().externalId;
360
366
  const checkpointId = externalId
361
367
  ? await getCheckpointId(externalId, truncated)
362
368
  : null;
@@ -397,7 +403,7 @@ const useAdkRuntimeImpl = (options: UseAdkRuntimeOptions) => {
397
403
  parentId,
398
404
  );
399
405
  replaceMessages(truncated);
400
- const externalId = aui.threadListItem().getState().externalId;
406
+ const externalId = aui.threadListItem.getState().externalId;
401
407
  const checkpointId = externalId
402
408
  ? await getCheckpointId(externalId, truncated)
403
409
  : null;
@@ -447,7 +453,7 @@ const useAdkRuntimeImpl = (options: UseAdkRuntimeOptions) => {
447
453
  const loadFn = loadRef.current;
448
454
  if (!loadFn) return;
449
455
 
450
- const externalId = aui.threadListItem().getState().externalId;
456
+ const externalId = aui.threadListItem.getState().externalId;
451
457
  if (externalId == null) return;
452
458
 
453
459
  loadFn(externalId).then(
@@ -477,7 +483,7 @@ export const useAdkRuntime = ({
477
483
  cloud,
478
484
  create: async () => {
479
485
  if (create) return create();
480
- if (aui.threadListItem.source) return aui.threadListItem().initialize();
486
+ if (aui.threadListItem.source) return aui.threadListItem.initialize();
481
487
  return { externalId: undefined };
482
488
  },
483
489
  delete: deleteFn,