@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.
- package/dist/AdkClient.d.ts.map +1 -1
- package/dist/AdkClient.js +13 -3
- package/dist/AdkClient.js.map +1 -1
- package/dist/AdkEventAccumulator.d.ts.map +1 -1
- package/dist/AdkEventAccumulator.js +11 -1
- package/dist/AdkEventAccumulator.js.map +1 -1
- package/dist/AdkSessionAdapter.d.ts.map +1 -1
- package/dist/AdkSessionAdapter.js +2 -1
- package/dist/AdkSessionAdapter.js.map +1 -1
- package/dist/adkToolApproval.d.ts +40 -0
- package/dist/adkToolApproval.d.ts.map +1 -0
- package/dist/adkToolApproval.js +166 -0
- package/dist/adkToolApproval.js.map +1 -0
- package/dist/convertAdkMessages.d.ts +3 -1
- package/dist/convertAdkMessages.d.ts.map +1 -1
- package/dist/convertAdkMessages.js +27 -16
- package/dist/convertAdkMessages.js.map +1 -1
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +2 -11
- package/dist/hooks.js.map +1 -1
- package/dist/parseAdkEvent.d.ts +6 -0
- package/dist/parseAdkEvent.d.ts.map +1 -0
- package/dist/parseAdkEvent.js +16 -0
- package/dist/parseAdkEvent.js.map +1 -0
- package/dist/server/parseAdkRequest.d.ts.map +1 -1
- package/dist/server/parseAdkRequest.js +79 -13
- package/dist/server/parseAdkRequest.js.map +1 -1
- package/dist/useAdkMessages.js +104 -64
- package/dist/useAdkMessages.js.map +1 -1
- package/dist/useAdkRuntime.d.ts.map +1 -1
- package/dist/useAdkRuntime.js +11 -3
- package/dist/useAdkRuntime.js.map +1 -1
- package/package.json +5 -5
- package/src/AdkClient.test.ts +137 -0
- package/src/AdkClient.ts +15 -3
- package/src/AdkEventAccumulator.test.ts +77 -0
- package/src/AdkEventAccumulator.ts +24 -1
- package/src/AdkSessionAdapter.test.ts +145 -1
- package/src/AdkSessionAdapter.ts +7 -2
- package/src/adkToolApproval.test.ts +345 -0
- package/src/adkToolApproval.ts +215 -0
- package/src/convertAdkMessages.ts +72 -43
- package/src/hooks.ts +4 -16
- package/src/parseAdkEvent.ts +40 -0
- package/src/server/parseAdkRequest.test.ts +141 -0
- package/src/server/parseAdkRequest.ts +145 -13
- package/src/useAdkMessages.test.ts +313 -0
- package/src/useAdkMessages.ts +62 -4
- package/src/useAdkRuntime.ts +29 -3
- 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
|
|
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
|
|
281
|
+
yield parseAdkEvent(event.data);
|
|
270
282
|
}
|
|
271
283
|
}
|
|
272
284
|
|
|
273
285
|
const trailing = sseDecoder.flush();
|
|
274
|
-
if (trailing !== null) yield
|
|
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
|
-
|
|
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 {
|
|
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(
|
package/src/AdkSessionAdapter.ts
CHANGED
|
@@ -6,7 +6,8 @@ import type {
|
|
|
6
6
|
RemoteThreadMetadata,
|
|
7
7
|
} from "@assistant-ui/core";
|
|
8
8
|
import { AdkEventAccumulator } from "./AdkEventAccumulator";
|
|
9
|
-
import
|
|
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
|
-
|
|
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: [] };
|