@assistant-ui/react-google-adk 0.0.20 → 0.0.22
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/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/convertAdkMessages.d.ts.map +1 -1
- package/dist/convertAdkMessages.js +18 -21
- package/dist/convertAdkMessages.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/server/adkEventStream.d.ts.map +1 -1
- package/dist/server/adkEventStream.js +11 -1
- package/dist/server/adkEventStream.js.map +1 -1
- 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 +68 -29
- package/dist/useAdkRuntime.js.map +1 -1
- package/package.json +8 -5
- 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/convertAdkMessages.test.ts +40 -6
- package/src/convertAdkMessages.ts +16 -15
- package/src/index.ts +1 -0
- package/src/server/adkEventStream.test.ts +38 -0
- package/src/server/adkEventStream.ts +16 -1
- 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 +161 -0
- package/src/useAdkRuntime.ts +127 -28
|
@@ -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) =>
|
|
@@ -26,32 +26,66 @@ describe("convertAdkMessage - human messages", () => {
|
|
|
26
26
|
});
|
|
27
27
|
});
|
|
28
28
|
|
|
29
|
-
it("restores
|
|
29
|
+
it("restores a file_url part as a file part stamped with sourceType url", () => {
|
|
30
30
|
const msg: AdkMessage = {
|
|
31
31
|
id: "m1",
|
|
32
32
|
type: "human",
|
|
33
|
-
content: [
|
|
33
|
+
content: [
|
|
34
|
+
{
|
|
35
|
+
type: "file_url",
|
|
36
|
+
url: "gs://bucket/report.pdf",
|
|
37
|
+
mimeType: "application/pdf",
|
|
38
|
+
},
|
|
39
|
+
],
|
|
34
40
|
};
|
|
35
41
|
const result = convertAdkMessage(msg, {});
|
|
36
42
|
expect(result).toMatchObject({
|
|
37
43
|
role: "user",
|
|
38
|
-
content: [
|
|
44
|
+
content: [
|
|
45
|
+
{
|
|
46
|
+
type: "file",
|
|
47
|
+
data: "gs://bucket/report.pdf",
|
|
48
|
+
mimeType: "application/pdf",
|
|
49
|
+
sourceType: "url",
|
|
50
|
+
},
|
|
51
|
+
],
|
|
39
52
|
});
|
|
40
53
|
});
|
|
41
54
|
|
|
42
|
-
it("
|
|
55
|
+
it("falls back to application/octet-stream for file_url parts without mimeType", () => {
|
|
43
56
|
const msg: AdkMessage = {
|
|
44
57
|
id: "m1",
|
|
45
58
|
type: "human",
|
|
46
|
-
content: [{ type: "
|
|
59
|
+
content: [{ type: "file_url", url: "gs://bucket/blob" }],
|
|
47
60
|
};
|
|
48
61
|
const result = convertAdkMessage(msg, {});
|
|
49
62
|
expect(result).toMatchObject({
|
|
50
63
|
role: "user",
|
|
51
|
-
content: [
|
|
64
|
+
content: [
|
|
65
|
+
{
|
|
66
|
+
type: "file",
|
|
67
|
+
data: "gs://bucket/blob",
|
|
68
|
+
mimeType: "application/octet-stream",
|
|
69
|
+
sourceType: "url",
|
|
70
|
+
},
|
|
71
|
+
],
|
|
52
72
|
});
|
|
53
73
|
});
|
|
54
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
|
+
|
|
55
89
|
it("keeps attachment-derived audio file parts (with filename) as file parts", () => {
|
|
56
90
|
const msg: AdkMessage = {
|
|
57
91
|
id: "m1",
|
|
@@ -8,8 +8,13 @@ type ContentPart =
|
|
|
8
8
|
| { type: "text"; text: string }
|
|
9
9
|
| { type: "reasoning"; text: string }
|
|
10
10
|
| { type: "image"; image: string }
|
|
11
|
-
| {
|
|
12
|
-
|
|
11
|
+
| {
|
|
12
|
+
type: "file";
|
|
13
|
+
data: string;
|
|
14
|
+
mimeType: string;
|
|
15
|
+
filename?: string;
|
|
16
|
+
sourceType?: "url";
|
|
17
|
+
}
|
|
13
18
|
| { type: "data"; name: string; data: unknown };
|
|
14
19
|
|
|
15
20
|
const contentToParts = (
|
|
@@ -33,26 +38,22 @@ const contentToParts = (
|
|
|
33
38
|
};
|
|
34
39
|
case "image_url":
|
|
35
40
|
return { type: "image", image: part.url };
|
|
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
|
-
}
|
|
41
|
+
case "file":
|
|
48
42
|
return {
|
|
49
43
|
type: "file",
|
|
50
44
|
data: part.data,
|
|
51
45
|
mimeType: part.mimeType,
|
|
52
46
|
...(part.filename != null && { filename: part.filename }),
|
|
53
47
|
};
|
|
54
|
-
}
|
|
55
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
|
+
}
|
|
56
57
|
return {
|
|
57
58
|
type: "data",
|
|
58
59
|
name: "file_url",
|
package/src/index.ts
CHANGED
|
@@ -73,4 +73,42 @@ describe("adkEventStream", () => {
|
|
|
73
73
|
await readSSE(response);
|
|
74
74
|
expect(onError).toHaveBeenCalledTimes(1);
|
|
75
75
|
});
|
|
76
|
+
|
|
77
|
+
it.each([
|
|
78
|
+
[
|
|
79
|
+
"throws",
|
|
80
|
+
() => {
|
|
81
|
+
throw new Error("callback failed");
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
[
|
|
85
|
+
"rejects",
|
|
86
|
+
async () => {
|
|
87
|
+
throw new Error("callback failed");
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
])(
|
|
91
|
+
"still emits the stream error when onError %s",
|
|
92
|
+
async (_behavior, onError) => {
|
|
93
|
+
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
94
|
+
async function* throwingGen() {
|
|
95
|
+
throw new Error("stream failed");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
const response = adkEventStream(throwingGen() as any, { onError });
|
|
100
|
+
const text = await readSSE(response);
|
|
101
|
+
await vi.waitFor(() => expect(errorSpy).toHaveBeenCalledTimes(1));
|
|
102
|
+
|
|
103
|
+
const lines = text.split("\n\n").filter((l) => l.startsWith("data: "));
|
|
104
|
+
expect(lines).toHaveLength(1);
|
|
105
|
+
expect(JSON.parse(lines[0]!.slice(6))).toMatchObject({
|
|
106
|
+
errorCode: "STREAM_ERROR",
|
|
107
|
+
errorMessage: "stream failed",
|
|
108
|
+
});
|
|
109
|
+
} finally {
|
|
110
|
+
errorSpy.mockRestore();
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
);
|
|
76
114
|
});
|
|
@@ -101,6 +101,21 @@ export type AdkEventStreamOptions = {
|
|
|
101
101
|
onError?: (error: unknown) => void;
|
|
102
102
|
};
|
|
103
103
|
|
|
104
|
+
const reportCallbackError = (error: unknown) => {
|
|
105
|
+
console.error("[react-google-adk] onError callback threw an error", error);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const notifyError = (
|
|
109
|
+
callback: AdkEventStreamOptions["onError"],
|
|
110
|
+
error: unknown,
|
|
111
|
+
) => {
|
|
112
|
+
try {
|
|
113
|
+
void Promise.resolve(callback?.(error)).catch(reportCallbackError);
|
|
114
|
+
} catch (callbackError) {
|
|
115
|
+
reportCallbackError(callbackError);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
104
119
|
/**
|
|
105
120
|
* Converts an AsyncGenerator of ADK SDK Events into an SSE Response.
|
|
106
121
|
*
|
|
@@ -131,7 +146,7 @@ export const adkEventStream = (
|
|
|
131
146
|
}
|
|
132
147
|
} catch (e) {
|
|
133
148
|
if (!cancelled) {
|
|
134
|
-
options?.onError
|
|
149
|
+
notifyError(options?.onError, e);
|
|
135
150
|
const errorEvent: AdkEvent = {
|
|
136
151
|
id: "",
|
|
137
152
|
errorCode: "STREAM_ERROR",
|
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
|
|