@assistant-ui/react-google-adk 0.0.19 → 0.0.21
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.js +15 -2
- package/dist/AdkClient.js.map +1 -1
- package/dist/AdkEventAccumulator.d.ts +1 -0
- package/dist/AdkEventAccumulator.d.ts.map +1 -1
- package/dist/AdkEventAccumulator.js +20 -4
- package/dist/AdkEventAccumulator.js.map +1 -1
- package/dist/AdkSessionAdapter.d.ts +4 -4
- package/dist/AdkSessionAdapter.d.ts.map +1 -1
- package/dist/AdkSessionAdapter.js +16 -3
- package/dist/AdkSessionAdapter.js.map +1 -1
- package/dist/adkExtras.d.ts +1 -1
- package/dist/adkExtras.d.ts.map +1 -1
- package/dist/adkExtras.js +1 -1
- package/dist/adkExtras.js.map +1 -1
- package/dist/convertAdkMessages.d.ts.map +1 -1
- package/dist/convertAdkMessages.js +18 -11
- package/dist/convertAdkMessages.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/types.d.ts +22 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/useAdkMessages.d.ts +2 -1
- package/dist/useAdkMessages.d.ts.map +1 -1
- package/dist/useAdkMessages.js +19 -1
- package/dist/useAdkMessages.js.map +1 -1
- package/dist/useAdkRuntime.d.ts +10 -4
- package/dist/useAdkRuntime.d.ts.map +1 -1
- package/dist/useAdkRuntime.js +79 -30
- package/dist/useAdkRuntime.js.map +1 -1
- package/package.json +7 -4
- package/src/AdkClient.test.ts +55 -0
- package/src/AdkClient.ts +16 -2
- package/src/AdkEventAccumulator.test.ts +127 -0
- package/src/AdkEventAccumulator.ts +31 -5
- package/src/AdkSessionAdapter.test.ts +43 -0
- package/src/AdkSessionAdapter.ts +21 -4
- package/src/adkExtras.ts +1 -1
- package/src/convertAdkMessages.test.ts +113 -0
- package/src/convertAdkMessages.ts +24 -4
- package/src/index.ts +1 -0
- package/src/types.ts +21 -0
- package/src/useAdkMessages.ts +24 -1
- package/src/useAdkRuntime.refetch.test.tsx +336 -0
- package/src/useAdkRuntime.test.ts +222 -0
- package/src/useAdkRuntime.ts +139 -34
package/src/AdkClient.ts
CHANGED
|
@@ -243,10 +243,20 @@ async function* parseSSEResponse(response: Response): AsyncGenerator<AdkEvent> {
|
|
|
243
243
|
const decoder = new TextDecoder();
|
|
244
244
|
const sseDecoder = new SSEEventDecoder({ trailing: "dispatch" });
|
|
245
245
|
|
|
246
|
+
let shouldCancel = true;
|
|
246
247
|
try {
|
|
247
248
|
while (true) {
|
|
248
|
-
|
|
249
|
+
let result: ReadableStreamReadResult<Uint8Array>;
|
|
250
|
+
try {
|
|
251
|
+
result = await reader.read();
|
|
252
|
+
} catch (error) {
|
|
253
|
+
shouldCancel = false;
|
|
254
|
+
throw error;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const { done, value } = result;
|
|
249
258
|
if (done) {
|
|
259
|
+
shouldCancel = false;
|
|
250
260
|
for (const event of sseDecoder.push(decoder.decode())) {
|
|
251
261
|
yield JSON.parse(event.data) as AdkEvent;
|
|
252
262
|
}
|
|
@@ -263,6 +273,10 @@ async function* parseSSEResponse(response: Response): AsyncGenerator<AdkEvent> {
|
|
|
263
273
|
const trailing = sseDecoder.flush();
|
|
264
274
|
if (trailing !== null) yield JSON.parse(trailing.data) as AdkEvent;
|
|
265
275
|
} finally {
|
|
266
|
-
|
|
276
|
+
try {
|
|
277
|
+
if (shouldCancel) await reader.cancel().catch(() => undefined);
|
|
278
|
+
} finally {
|
|
279
|
+
reader.releaseLock();
|
|
280
|
+
}
|
|
267
281
|
}
|
|
268
282
|
}
|
|
@@ -239,6 +239,133 @@ describe("AdkEventAccumulator - function responses", () => {
|
|
|
239
239
|
content: JSON.stringify({ results: [] }),
|
|
240
240
|
});
|
|
241
241
|
});
|
|
242
|
+
|
|
243
|
+
// A session load replays the stored events through a fresh accumulator.
|
|
244
|
+
it("gives a tool message the same id on every replay of an event", () => {
|
|
245
|
+
const event = makeEvent({
|
|
246
|
+
id: "evt-tool",
|
|
247
|
+
author: "agent",
|
|
248
|
+
content: {
|
|
249
|
+
role: "model",
|
|
250
|
+
parts: [
|
|
251
|
+
{
|
|
252
|
+
functionResponse: {
|
|
253
|
+
name: "search",
|
|
254
|
+
id: "tc-1",
|
|
255
|
+
response: { results: [] },
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
],
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
const first = new AdkEventAccumulator().processEvent(event);
|
|
263
|
+
const second = new AdkEventAccumulator().processEvent(event);
|
|
264
|
+
|
|
265
|
+
expect(first[0]!.id).toBe(second[0]!.id);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it("keeps two tool messages from one event distinct", () => {
|
|
269
|
+
const msgs = new AdkEventAccumulator().processEvent(
|
|
270
|
+
makeEvent({
|
|
271
|
+
id: "evt-tool",
|
|
272
|
+
author: "agent",
|
|
273
|
+
content: {
|
|
274
|
+
role: "model",
|
|
275
|
+
parts: [
|
|
276
|
+
{
|
|
277
|
+
functionResponse: {
|
|
278
|
+
name: "search",
|
|
279
|
+
id: "tc-1",
|
|
280
|
+
response: { a: 1 },
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
functionResponse: {
|
|
285
|
+
name: "lookup",
|
|
286
|
+
id: "tc-2",
|
|
287
|
+
response: { b: 2 },
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
],
|
|
291
|
+
},
|
|
292
|
+
}),
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
const toolMessages = msgs.filter((m) => m.type === "tool");
|
|
296
|
+
expect(toolMessages).toHaveLength(2);
|
|
297
|
+
expect(toolMessages[0]!.id).not.toBe(toolMessages[1]!.id);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it("gives an assistant message the same id on every replay", () => {
|
|
301
|
+
const events = [
|
|
302
|
+
makeTextEvent("Hel", true),
|
|
303
|
+
makeTextEvent("lo", true),
|
|
304
|
+
makeTextEvent("Hello"),
|
|
305
|
+
];
|
|
306
|
+
const replay = () => {
|
|
307
|
+
const acc = new AdkEventAccumulator();
|
|
308
|
+
let msgs: AdkMessage[] = [];
|
|
309
|
+
for (const event of events) msgs = acc.processEvent(event);
|
|
310
|
+
return msgs;
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
expect(replay().map((m) => m.id)).toEqual(replay().map((m) => m.id));
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it("keeps two assistant messages opened by one event distinct and stable", () => {
|
|
317
|
+
const event = makeEvent({
|
|
318
|
+
id: "evt-mixed",
|
|
319
|
+
author: "agent",
|
|
320
|
+
content: {
|
|
321
|
+
role: "model",
|
|
322
|
+
parts: [
|
|
323
|
+
{ text: "before" },
|
|
324
|
+
{
|
|
325
|
+
functionResponse: {
|
|
326
|
+
name: "search",
|
|
327
|
+
id: "tc-1",
|
|
328
|
+
response: { ok: true },
|
|
329
|
+
},
|
|
330
|
+
},
|
|
331
|
+
{ text: "after" },
|
|
332
|
+
],
|
|
333
|
+
},
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
const first = new AdkEventAccumulator().processEvent(event);
|
|
337
|
+
const second = new AdkEventAccumulator().processEvent(event);
|
|
338
|
+
|
|
339
|
+
const aiIds = first.filter((m) => m.type === "ai").map((m) => m.id);
|
|
340
|
+
expect(aiIds.length).toBeGreaterThan(1);
|
|
341
|
+
expect(new Set(aiIds).size).toBe(aiIds.length);
|
|
342
|
+
expect(first.map((m) => m.id)).toEqual(second.map((m) => m.id));
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it("keeps two tool messages distinct when the payload omits response ids", () => {
|
|
346
|
+
const event = makeEvent({
|
|
347
|
+
id: "evt-tool",
|
|
348
|
+
author: "agent",
|
|
349
|
+
content: {
|
|
350
|
+
role: "model",
|
|
351
|
+
parts: [
|
|
352
|
+
{ functionResponse: { name: "search", response: { a: 1 } } },
|
|
353
|
+
{ functionResponse: { name: "lookup", response: { b: 2 } } },
|
|
354
|
+
],
|
|
355
|
+
},
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
const first = new AdkEventAccumulator()
|
|
359
|
+
.processEvent(event)
|
|
360
|
+
.filter((m) => m.type === "tool");
|
|
361
|
+
const second = new AdkEventAccumulator()
|
|
362
|
+
.processEvent(event)
|
|
363
|
+
.filter((m) => m.type === "tool");
|
|
364
|
+
|
|
365
|
+
expect(first).toHaveLength(2);
|
|
366
|
+
expect(first[0]!.id).not.toBe(first[1]!.id);
|
|
367
|
+
expect(first.map((m) => m.id)).toEqual(second.map((m) => m.id));
|
|
368
|
+
});
|
|
242
369
|
});
|
|
243
370
|
|
|
244
371
|
describe("AdkEventAccumulator - code execution", () => {
|
|
@@ -14,6 +14,23 @@ import type { ReadonlyJSONObject } from "assistant-stream/utils";
|
|
|
14
14
|
|
|
15
15
|
type InProgressMessage = AdkMessage & { type: "ai" };
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* A session load replays the stored events through a fresh accumulator, so a
|
|
19
|
+
* message needs an id derived from the event that carries it rather than one
|
|
20
|
+
* minted per replay. An event with no id of its own has never been through the
|
|
21
|
+
* session and has nothing stable to derive from, so it keeps a generated one.
|
|
22
|
+
*
|
|
23
|
+
* A human message keeps the bare event id it has always had. The other kinds
|
|
24
|
+
* take a suffixed namespace, since one event can carry several of them: a tool
|
|
25
|
+
* message by the index of its part, an assistant message by how many this
|
|
26
|
+
* event has already opened.
|
|
27
|
+
*/
|
|
28
|
+
const toolMessageId = (event: AdkEvent, partIndex: number): string =>
|
|
29
|
+
event.id ? `${event.id}:${partIndex}` : uuidv4();
|
|
30
|
+
|
|
31
|
+
const aiMessageId = (event: AdkEvent, ordinal: number): string =>
|
|
32
|
+
event.id ? `${event.id}:ai${ordinal === 0 ? "" : ordinal}` : uuidv4();
|
|
33
|
+
|
|
17
34
|
const ADK_REQUEST_CONFIRMATION = "adk_request_confirmation";
|
|
18
35
|
const ADK_REQUEST_CREDENTIAL = "adk_request_credential";
|
|
19
36
|
|
|
@@ -187,6 +204,9 @@ export class AdkEventAccumulator {
|
|
|
187
204
|
private authRequests: AdkAuthRequest[] = [];
|
|
188
205
|
private escalated = false;
|
|
189
206
|
private messageMetadataMap = new Map<string, AdkMessageMetadata>();
|
|
207
|
+
// How many assistant messages each event has opened, so a replay of that
|
|
208
|
+
// event opens them with the same ids.
|
|
209
|
+
private aiMessageOrdinals = new Map<string, number>();
|
|
190
210
|
constructor(initialMessages?: AdkMessage[]) {
|
|
191
211
|
if (initialMessages) {
|
|
192
212
|
for (const msg of initialMessages) {
|
|
@@ -344,8 +364,8 @@ export class AdkEventAccumulator {
|
|
|
344
364
|
}
|
|
345
365
|
}
|
|
346
366
|
|
|
347
|
-
for (const part of parts) {
|
|
348
|
-
this.processPart(part, event);
|
|
367
|
+
for (const [index, part] of parts.entries()) {
|
|
368
|
+
this.processPart(part, event, index);
|
|
349
369
|
}
|
|
350
370
|
|
|
351
371
|
// Track per-message metadata (grounding, citation, usage)
|
|
@@ -379,7 +399,11 @@ export class AdkEventAccumulator {
|
|
|
379
399
|
return this.getMessages();
|
|
380
400
|
}
|
|
381
401
|
|
|
382
|
-
private processPart(
|
|
402
|
+
private processPart(
|
|
403
|
+
part: AdkEventPart,
|
|
404
|
+
event: AdkEvent,
|
|
405
|
+
partIndex: number,
|
|
406
|
+
): void {
|
|
383
407
|
// Detect special ADK function calls
|
|
384
408
|
if (part.functionCall && !event.partial) {
|
|
385
409
|
const name = part.functionCall.name;
|
|
@@ -475,7 +499,7 @@ export class AdkEventAccumulator {
|
|
|
475
499
|
if (part.functionResponse) {
|
|
476
500
|
this.finalizeCurrentMessage();
|
|
477
501
|
const toolMsg: AdkMessage = {
|
|
478
|
-
id:
|
|
502
|
+
id: toolMessageId(event, partIndex),
|
|
479
503
|
type: "tool",
|
|
480
504
|
tool_call_id: part.functionResponse.id ?? "",
|
|
481
505
|
name: part.functionResponse.name,
|
|
@@ -559,7 +583,9 @@ export class AdkEventAccumulator {
|
|
|
559
583
|
}
|
|
560
584
|
}
|
|
561
585
|
|
|
562
|
-
const
|
|
586
|
+
const ordinal = this.aiMessageOrdinals.get(event.id ?? "") ?? 0;
|
|
587
|
+
this.aiMessageOrdinals.set(event.id ?? "", ordinal + 1);
|
|
588
|
+
const id = aiMessageId(event, ordinal);
|
|
563
589
|
const msg: InProgressMessage = {
|
|
564
590
|
id,
|
|
565
591
|
type: "ai",
|
|
@@ -293,6 +293,49 @@ describe("createAdkSessionAdapter - fetch", () => {
|
|
|
293
293
|
// ── load() ──
|
|
294
294
|
|
|
295
295
|
describe("createAdkSessionAdapter - load", () => {
|
|
296
|
+
it("returns the per-turn state the events imply, not just the messages", async () => {
|
|
297
|
+
const session = {
|
|
298
|
+
id: "s1",
|
|
299
|
+
events: [
|
|
300
|
+
{
|
|
301
|
+
id: "e1",
|
|
302
|
+
author: "agent",
|
|
303
|
+
longRunningToolIds: ["tc-1"],
|
|
304
|
+
actions: { stateDelta: { step: 2 }, escalate: true },
|
|
305
|
+
content: {
|
|
306
|
+
role: "model",
|
|
307
|
+
parts: [{ functionCall: { name: "search", id: "tc-1", args: {} } }],
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
],
|
|
311
|
+
};
|
|
312
|
+
mockFetch.mockResolvedValueOnce(
|
|
313
|
+
new Response(JSON.stringify(session), { status: 200 }),
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
const { load } = createAdkSessionAdapter(baseOptions);
|
|
317
|
+
const result = await load("s1");
|
|
318
|
+
|
|
319
|
+
expect(result.longRunningToolIds).toEqual(["tc-1"]);
|
|
320
|
+
expect(result.stateDelta).toEqual({ step: 2 });
|
|
321
|
+
expect(result.messageMetadata).toBeInstanceOf(Map);
|
|
322
|
+
expect(result.toolConfirmations).toEqual([]);
|
|
323
|
+
expect(result.authRequests).toEqual([]);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it("passes an abort signal through to the request", async () => {
|
|
327
|
+
mockFetch.mockResolvedValueOnce(
|
|
328
|
+
new Response(JSON.stringify({ id: "s1", events: [] }), { status: 200 }),
|
|
329
|
+
);
|
|
330
|
+
const controller = new AbortController();
|
|
331
|
+
|
|
332
|
+
const { load } = createAdkSessionAdapter(baseOptions);
|
|
333
|
+
await load("s1", { signal: controller.signal });
|
|
334
|
+
|
|
335
|
+
const init = mockFetch.mock.calls[0]![1] as RequestInit;
|
|
336
|
+
expect(init.signal).toBe(controller.signal);
|
|
337
|
+
});
|
|
338
|
+
|
|
296
339
|
it("reconstructs messages from session events", async () => {
|
|
297
340
|
const session = {
|
|
298
341
|
id: "s1",
|
package/src/AdkSessionAdapter.ts
CHANGED
|
@@ -6,7 +6,7 @@ import type {
|
|
|
6
6
|
RemoteThreadMetadata,
|
|
7
7
|
} from "@assistant-ui/core";
|
|
8
8
|
import { AdkEventAccumulator } from "./AdkEventAccumulator";
|
|
9
|
-
import type { AdkEvent, AdkMessage } from "./types";
|
|
9
|
+
import type { AdkEvent, AdkMessage, AdkThreadSnapshot } from "./types";
|
|
10
10
|
import { trimTrailingSlashes } from "./trimTrailingSlashes";
|
|
11
11
|
|
|
12
12
|
export type AdkSessionAdapterOptions = {
|
|
@@ -41,7 +41,10 @@ export type AdkArtifactData = {
|
|
|
41
41
|
|
|
42
42
|
type AdkSessionAdapterResult = {
|
|
43
43
|
adapter: RemoteThreadListAdapter;
|
|
44
|
-
load: (
|
|
44
|
+
load: (
|
|
45
|
+
sessionId: string,
|
|
46
|
+
options?: { signal?: AbortSignal | undefined },
|
|
47
|
+
) => Promise<AdkThreadSnapshot>;
|
|
45
48
|
artifacts: {
|
|
46
49
|
list: (sessionId: string) => Promise<string[]>;
|
|
47
50
|
load: (
|
|
@@ -302,10 +305,12 @@ export function createAdkSessionAdapter(
|
|
|
302
305
|
|
|
303
306
|
const load = async (
|
|
304
307
|
sessionId: string,
|
|
305
|
-
|
|
308
|
+
options?: { signal?: AbortSignal | undefined },
|
|
309
|
+
): Promise<AdkThreadSnapshot> => {
|
|
306
310
|
const headers = await getHeaders();
|
|
307
311
|
const res = await fetch(`${baseUrl}/${encodeURIComponent(sessionId)}`, {
|
|
308
312
|
headers,
|
|
313
|
+
...(options?.signal ? { signal: options.signal } : {}),
|
|
309
314
|
});
|
|
310
315
|
if (!res.ok) {
|
|
311
316
|
throw new Error(`Failed to load session: ${res.status}`);
|
|
@@ -329,7 +334,19 @@ export function createAdkSessionAdapter(
|
|
|
329
334
|
for (const event of events) {
|
|
330
335
|
messages = accumulator.processEvent(event);
|
|
331
336
|
}
|
|
332
|
-
|
|
337
|
+
// The per-turn state rides along, so a refetch can swap the thread over in
|
|
338
|
+
// one commit instead of reconstructing it from the messages alone.
|
|
339
|
+
return {
|
|
340
|
+
messages,
|
|
341
|
+
longRunningToolIds: accumulator.getLongRunningToolIds(),
|
|
342
|
+
toolConfirmations: accumulator.getToolConfirmations(),
|
|
343
|
+
authRequests: accumulator.getAuthRequests(),
|
|
344
|
+
escalated: accumulator.isEscalated(),
|
|
345
|
+
messageMetadata: accumulator.getMessageMetadata(),
|
|
346
|
+
stateDelta: accumulator.getStateDelta(),
|
|
347
|
+
artifactDelta: accumulator.getArtifactDelta(),
|
|
348
|
+
agentInfo: accumulator.getAgentInfo(),
|
|
349
|
+
};
|
|
333
350
|
};
|
|
334
351
|
|
|
335
352
|
const artifactBaseUrl = (sessionId: string) =>
|
package/src/adkExtras.ts
CHANGED
|
@@ -25,6 +25,106 @@ describe("convertAdkMessage - human messages", () => {
|
|
|
25
25
|
content: [{ type: "text", text: "Hello" }],
|
|
26
26
|
});
|
|
27
27
|
});
|
|
28
|
+
|
|
29
|
+
it("restores a file_url part as a file part stamped with sourceType url", () => {
|
|
30
|
+
const msg: AdkMessage = {
|
|
31
|
+
id: "m1",
|
|
32
|
+
type: "human",
|
|
33
|
+
content: [
|
|
34
|
+
{
|
|
35
|
+
type: "file_url",
|
|
36
|
+
url: "gs://bucket/report.pdf",
|
|
37
|
+
mimeType: "application/pdf",
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
};
|
|
41
|
+
const result = convertAdkMessage(msg, {});
|
|
42
|
+
expect(result).toMatchObject({
|
|
43
|
+
role: "user",
|
|
44
|
+
content: [
|
|
45
|
+
{
|
|
46
|
+
type: "file",
|
|
47
|
+
data: "gs://bucket/report.pdf",
|
|
48
|
+
mimeType: "application/pdf",
|
|
49
|
+
sourceType: "url",
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("falls back to application/octet-stream for file_url parts without mimeType", () => {
|
|
56
|
+
const msg: AdkMessage = {
|
|
57
|
+
id: "m1",
|
|
58
|
+
type: "human",
|
|
59
|
+
content: [{ type: "file_url", url: "gs://bucket/blob" }],
|
|
60
|
+
};
|
|
61
|
+
const result = convertAdkMessage(msg, {});
|
|
62
|
+
expect(result).toMatchObject({
|
|
63
|
+
role: "user",
|
|
64
|
+
content: [
|
|
65
|
+
{
|
|
66
|
+
type: "file",
|
|
67
|
+
data: "gs://bucket/blob",
|
|
68
|
+
mimeType: "application/octet-stream",
|
|
69
|
+
sourceType: "url",
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("keeps an audio file part as a file part", () => {
|
|
76
|
+
for (const mimeType of ["audio/mp3", "audio/wav"]) {
|
|
77
|
+
const msg: AdkMessage = {
|
|
78
|
+
id: "m1",
|
|
79
|
+
type: "human",
|
|
80
|
+
content: [{ type: "file", mimeType, data: "QUJD" }],
|
|
81
|
+
};
|
|
82
|
+
expect(convertAdkMessage(msg, {})).toMatchObject({
|
|
83
|
+
role: "user",
|
|
84
|
+
content: [{ type: "file", data: "QUJD", mimeType }],
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("keeps attachment-derived audio file parts (with filename) as file parts", () => {
|
|
90
|
+
const msg: AdkMessage = {
|
|
91
|
+
id: "m1",
|
|
92
|
+
type: "human",
|
|
93
|
+
content: [
|
|
94
|
+
{
|
|
95
|
+
type: "file",
|
|
96
|
+
mimeType: "audio/wav",
|
|
97
|
+
data: "QUJD",
|
|
98
|
+
filename: "memo.wav",
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
};
|
|
102
|
+
const result = convertAdkMessage(msg, {});
|
|
103
|
+
expect(result).toMatchObject({
|
|
104
|
+
role: "user",
|
|
105
|
+
content: [
|
|
106
|
+
{
|
|
107
|
+
type: "file",
|
|
108
|
+
mimeType: "audio/wav",
|
|
109
|
+
data: "QUJD",
|
|
110
|
+
filename: "memo.wav",
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("keeps file parts with other audio mime types as file parts", () => {
|
|
117
|
+
const msg: AdkMessage = {
|
|
118
|
+
id: "m1",
|
|
119
|
+
type: "human",
|
|
120
|
+
content: [{ type: "file", mimeType: "audio/ogg", data: "QUJD" }],
|
|
121
|
+
};
|
|
122
|
+
const result = convertAdkMessage(msg, {});
|
|
123
|
+
expect(result).toMatchObject({
|
|
124
|
+
role: "user",
|
|
125
|
+
content: [{ type: "file", mimeType: "audio/ogg", data: "QUJD" }],
|
|
126
|
+
});
|
|
127
|
+
});
|
|
28
128
|
});
|
|
29
129
|
|
|
30
130
|
describe("convertAdkMessage - ai messages", () => {
|
|
@@ -78,6 +178,19 @@ describe("convertAdkMessage - ai messages", () => {
|
|
|
78
178
|
});
|
|
79
179
|
});
|
|
80
180
|
|
|
181
|
+
it("keeps audio/mp3 file parts as file parts on assistant messages", () => {
|
|
182
|
+
const msg: AdkMessage = {
|
|
183
|
+
id: "m1",
|
|
184
|
+
type: "ai",
|
|
185
|
+
content: [{ type: "file", mimeType: "audio/mp3", data: "QUJD" }],
|
|
186
|
+
};
|
|
187
|
+
const result = convertAdkMessage(msg, {});
|
|
188
|
+
expect(result).toMatchObject({
|
|
189
|
+
role: "assistant",
|
|
190
|
+
content: [{ type: "file", mimeType: "audio/mp3", data: "QUJD" }],
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
81
194
|
it("converts file content parts to file message parts", () => {
|
|
82
195
|
const msg: AdkMessage = {
|
|
83
196
|
id: "m1",
|
|
@@ -8,10 +8,19 @@ type ContentPart =
|
|
|
8
8
|
| { type: "text"; text: string }
|
|
9
9
|
| { type: "reasoning"; text: string }
|
|
10
10
|
| { type: "image"; image: string }
|
|
11
|
-
| {
|
|
11
|
+
| {
|
|
12
|
+
type: "file";
|
|
13
|
+
data: string;
|
|
14
|
+
mimeType: string;
|
|
15
|
+
filename?: string;
|
|
16
|
+
sourceType?: "url";
|
|
17
|
+
}
|
|
12
18
|
| { type: "data"; name: string; data: unknown };
|
|
13
19
|
|
|
14
|
-
const contentToParts = (
|
|
20
|
+
const contentToParts = (
|
|
21
|
+
content: AdkMessage["content"],
|
|
22
|
+
role: "user" | "assistant",
|
|
23
|
+
): ContentPart[] => {
|
|
15
24
|
if (typeof content === "string")
|
|
16
25
|
return [{ type: "text" as const, text: content }];
|
|
17
26
|
|
|
@@ -37,6 +46,14 @@ const contentToParts = (content: AdkMessage["content"]): ContentPart[] => {
|
|
|
37
46
|
...(part.filename != null && { filename: part.filename }),
|
|
38
47
|
};
|
|
39
48
|
case "file_url":
|
|
49
|
+
if (role === "user") {
|
|
50
|
+
return {
|
|
51
|
+
type: "file",
|
|
52
|
+
data: part.url,
|
|
53
|
+
mimeType: part.mimeType ?? "application/octet-stream",
|
|
54
|
+
sourceType: "url",
|
|
55
|
+
};
|
|
56
|
+
}
|
|
40
57
|
return {
|
|
41
58
|
type: "data",
|
|
42
59
|
name: "file_url",
|
|
@@ -72,7 +89,7 @@ export const convertAdkMessage: useExternalMessageConverter.Callback<
|
|
|
72
89
|
return {
|
|
73
90
|
role: "user",
|
|
74
91
|
id: message.id,
|
|
75
|
-
content: contentToParts(message.content),
|
|
92
|
+
content: contentToParts(message.content, "user"),
|
|
76
93
|
};
|
|
77
94
|
|
|
78
95
|
case "ai": {
|
|
@@ -88,7 +105,10 @@ export const convertAdkMessage: useExternalMessageConverter.Callback<
|
|
|
88
105
|
return {
|
|
89
106
|
role: "assistant",
|
|
90
107
|
id: message.id,
|
|
91
|
-
content: [
|
|
108
|
+
content: [
|
|
109
|
+
...contentToParts(message.content, "assistant"),
|
|
110
|
+
...toolCallParts,
|
|
111
|
+
],
|
|
92
112
|
...(message.status && { status: message.status }),
|
|
93
113
|
...(message.author && {
|
|
94
114
|
metadata: {
|
package/src/index.ts
CHANGED
package/src/types.ts
CHANGED
|
@@ -287,3 +287,24 @@ export type AdkRuntimeExtras = {
|
|
|
287
287
|
escalated: boolean;
|
|
288
288
|
messageMetadata: Map<string, AdkMessageMetadata>;
|
|
289
289
|
};
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* What a session load reconstructs. The messages are the whole thread; the
|
|
293
|
+
* rest is the per-turn state the events imply, which a caller that replays
|
|
294
|
+
* them through {@link AdkEventAccumulator} already has. Omitting a field
|
|
295
|
+
* clears it, since a snapshot that cannot describe the state is not evidence
|
|
296
|
+
* that the state survived.
|
|
297
|
+
*/
|
|
298
|
+
export type AdkThreadSnapshot = {
|
|
299
|
+
messages: AdkMessage[];
|
|
300
|
+
longRunningToolIds?: string[] | undefined;
|
|
301
|
+
toolConfirmations?: AdkToolConfirmation[] | undefined;
|
|
302
|
+
authRequests?: AdkAuthRequest[] | undefined;
|
|
303
|
+
escalated?: boolean | undefined;
|
|
304
|
+
messageMetadata?: Map<string, AdkMessageMetadata> | undefined;
|
|
305
|
+
stateDelta?: Record<string, unknown> | undefined;
|
|
306
|
+
artifactDelta?: Record<string, number> | undefined;
|
|
307
|
+
agentInfo?:
|
|
308
|
+
| { name?: string | undefined; branch?: string | undefined }
|
|
309
|
+
| undefined;
|
|
310
|
+
};
|
package/src/useAdkMessages.ts
CHANGED
|
@@ -11,6 +11,7 @@ import type {
|
|
|
11
11
|
AdkStreamCallback,
|
|
12
12
|
AdkToolConfirmation,
|
|
13
13
|
AdkAuthRequest,
|
|
14
|
+
AdkThreadSnapshot,
|
|
14
15
|
OnAdkErrorCallback,
|
|
15
16
|
OnAdkCustomEventCallback,
|
|
16
17
|
OnAdkAgentTransferCallback,
|
|
@@ -62,8 +63,29 @@ export const useAdkMessages = ({
|
|
|
62
63
|
_setMessages(msgs);
|
|
63
64
|
}, []);
|
|
64
65
|
|
|
66
|
+
/**
|
|
67
|
+
* Swap the thread over to a loaded snapshot in one commit. Unlike
|
|
68
|
+
* {@link replaceMessages} this never passes through a cleared state, so a
|
|
69
|
+
* refetch that lands while a confirmation is on screen replaces it rather
|
|
70
|
+
* than blanking it first.
|
|
71
|
+
*/
|
|
72
|
+
const applySnapshot = useCallback(
|
|
73
|
+
(snapshot: AdkThreadSnapshot) => {
|
|
74
|
+
setMessagesImmediate(snapshot.messages);
|
|
75
|
+
setLongRunningToolIds(snapshot.longRunningToolIds ?? []);
|
|
76
|
+
setToolConfirmations(snapshot.toolConfirmations ?? []);
|
|
77
|
+
setAuthRequests(snapshot.authRequests ?? []);
|
|
78
|
+
setEscalated(snapshot.escalated ?? false);
|
|
79
|
+
setMessageMetadata(snapshot.messageMetadata ?? new Map());
|
|
80
|
+
setStateDelta(snapshot.stateDelta ?? {});
|
|
81
|
+
setArtifactDelta(snapshot.artifactDelta ?? {});
|
|
82
|
+
setAgentInfo(snapshot.agentInfo ?? {});
|
|
83
|
+
},
|
|
84
|
+
[setMessagesImmediate],
|
|
85
|
+
);
|
|
86
|
+
|
|
65
87
|
// Replace the message list AND reset derived per-turn HITL state.
|
|
66
|
-
// Used by truncation paths (edit, reload
|
|
88
|
+
// Used by truncation paths (edit, reload) so that stale interrupt
|
|
67
89
|
// markers and per-message metadata from the removed messages don't leak
|
|
68
90
|
// into the next turn.
|
|
69
91
|
const replaceMessages = useCallback(
|
|
@@ -195,6 +217,7 @@ export const useAdkMessages = ({
|
|
|
195
217
|
cancel,
|
|
196
218
|
setMessages: setMessagesImmediate,
|
|
197
219
|
replaceMessages,
|
|
220
|
+
applySnapshot,
|
|
198
221
|
};
|
|
199
222
|
};
|
|
200
223
|
|