@assistant-ui/react-google-adk 0.0.25 → 0.0.27
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 +2 -0
- package/dist/AdkEventAccumulator.d.ts.map +1 -1
- package/dist/AdkEventAccumulator.js +32 -10
- 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/convertToAdkMessages.d.ts +49 -0
- package/dist/convertToAdkMessages.d.ts.map +1 -0
- package/dist/convertToAdkMessages.js +86 -0
- package/dist/convertToAdkMessages.js.map +1 -0
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +5 -14
- 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.d.ts.map +1 -1
- package/dist/useAdkMessages.js +110 -79
- package/dist/useAdkMessages.js.map +1 -1
- package/dist/useAdkRuntime.d.ts.map +1 -1
- package/dist/useAdkRuntime.js +44 -129
- package/dist/useAdkRuntime.js.map +1 -1
- package/package.json +6 -7
- package/src/AdkClient.test.ts +137 -0
- package/src/AdkClient.ts +15 -3
- package/src/AdkEventAccumulator.test.ts +161 -1
- package/src/AdkEventAccumulator.ts +44 -8
- 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/{useAdkRuntime.test.ts → convertToAdkMessages.test.ts} +1 -1
- package/src/convertToAdkMessages.ts +139 -0
- package/src/hooks.ts +7 -19
- 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 +72 -25
- package/src/useAdkRuntime.ts +66 -188
- package/src/useAdkRuntimeApproval.test.tsx +344 -0
|
@@ -117,7 +117,7 @@ describe("AdkEventAccumulator - function calls", () => {
|
|
|
117
117
|
});
|
|
118
118
|
});
|
|
119
119
|
|
|
120
|
-
it("generates
|
|
120
|
+
it("generates an ID for functionCall without an id", () => {
|
|
121
121
|
const acc = new AdkEventAccumulator();
|
|
122
122
|
const msgs = acc.processEvent(
|
|
123
123
|
makeEvent({
|
|
@@ -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(
|
|
@@ -1400,3 +1477,86 @@ describe("AdkEventAccumulator - user message handling", () => {
|
|
|
1400
1477
|
});
|
|
1401
1478
|
});
|
|
1402
1479
|
});
|
|
1480
|
+
|
|
1481
|
+
describe("AdkEventAccumulator - multiple parts in one event", () => {
|
|
1482
|
+
it("keeps every text part of a single non-partial event in order", () => {
|
|
1483
|
+
const acc = new AdkEventAccumulator();
|
|
1484
|
+
const msgs = acc.processEvent(
|
|
1485
|
+
makeEvent({
|
|
1486
|
+
author: "agent",
|
|
1487
|
+
content: { role: "model", parts: [{ text: "A" }, { text: "B" }] },
|
|
1488
|
+
}),
|
|
1489
|
+
);
|
|
1490
|
+
|
|
1491
|
+
expect(msgs[0]?.content).toEqual([
|
|
1492
|
+
{ type: "text", text: "A" },
|
|
1493
|
+
{ type: "text", text: "B" },
|
|
1494
|
+
]);
|
|
1495
|
+
});
|
|
1496
|
+
|
|
1497
|
+
it("preserves the Gemini code-execution shape", () => {
|
|
1498
|
+
const acc = new AdkEventAccumulator();
|
|
1499
|
+
const msgs = acc.processEvent(
|
|
1500
|
+
makeEvent({
|
|
1501
|
+
author: "agent",
|
|
1502
|
+
content: {
|
|
1503
|
+
role: "model",
|
|
1504
|
+
parts: [
|
|
1505
|
+
{ text: "Here is the code:" },
|
|
1506
|
+
{ executableCode: { code: "print(1)", language: "python" } },
|
|
1507
|
+
{ codeExecutionResult: { outcome: "OUTCOME_OK", output: "1" } },
|
|
1508
|
+
{ text: "The result is 1." },
|
|
1509
|
+
],
|
|
1510
|
+
},
|
|
1511
|
+
}),
|
|
1512
|
+
);
|
|
1513
|
+
|
|
1514
|
+
expect(msgs[0]?.content).toEqual([
|
|
1515
|
+
{ type: "text", text: "Here is the code:" },
|
|
1516
|
+
{ type: "code", code: "print(1)", language: "python" },
|
|
1517
|
+
{ type: "code_result", output: "1", outcome: "OUTCOME_OK" },
|
|
1518
|
+
{ type: "text", text: "The result is 1." },
|
|
1519
|
+
]);
|
|
1520
|
+
});
|
|
1521
|
+
|
|
1522
|
+
it("keeps every reasoning part of a single non-partial event", () => {
|
|
1523
|
+
const acc = new AdkEventAccumulator();
|
|
1524
|
+
const msgs = acc.processEvent(
|
|
1525
|
+
makeEvent({
|
|
1526
|
+
author: "agent",
|
|
1527
|
+
content: {
|
|
1528
|
+
role: "model",
|
|
1529
|
+
parts: [
|
|
1530
|
+
{ text: "First thought", thought: true },
|
|
1531
|
+
{ text: "Second thought", thought: true },
|
|
1532
|
+
],
|
|
1533
|
+
},
|
|
1534
|
+
}),
|
|
1535
|
+
);
|
|
1536
|
+
|
|
1537
|
+
expect(msgs[0]?.content).toEqual([
|
|
1538
|
+
{ type: "reasoning", text: "First thought" },
|
|
1539
|
+
{ type: "reasoning", text: "Second thought" },
|
|
1540
|
+
]);
|
|
1541
|
+
});
|
|
1542
|
+
|
|
1543
|
+
it("still replaces the streamed buffer with the final text", () => {
|
|
1544
|
+
const acc = new AdkEventAccumulator();
|
|
1545
|
+
acc.processEvent(makeTextEvent("Hel", true));
|
|
1546
|
+
acc.processEvent(makeTextEvent("lo", true));
|
|
1547
|
+
const msgs = acc.processEvent(
|
|
1548
|
+
makeEvent({
|
|
1549
|
+
author: "agent",
|
|
1550
|
+
content: {
|
|
1551
|
+
role: "model",
|
|
1552
|
+
parts: [{ text: "Hello" }, { text: "Again" }],
|
|
1553
|
+
},
|
|
1554
|
+
}),
|
|
1555
|
+
);
|
|
1556
|
+
|
|
1557
|
+
expect(msgs[0]?.content).toEqual([
|
|
1558
|
+
{ type: "text", text: "Hello" },
|
|
1559
|
+
{ type: "text", text: "Again" },
|
|
1560
|
+
]);
|
|
1561
|
+
});
|
|
1562
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { generateId } from "@assistant-ui/core";
|
|
2
2
|
import type { MessageStatus } from "@assistant-ui/core";
|
|
3
3
|
import type {
|
|
4
4
|
AdkEvent,
|
|
@@ -26,10 +26,10 @@ type InProgressMessage = AdkMessage & { type: "ai" };
|
|
|
26
26
|
* event has already opened.
|
|
27
27
|
*/
|
|
28
28
|
const toolMessageId = (event: AdkEvent, partIndex: number): string =>
|
|
29
|
-
event.id ? `${event.id}:${partIndex}` :
|
|
29
|
+
event.id ? `${event.id}:${partIndex}` : generateId();
|
|
30
30
|
|
|
31
31
|
const aiMessageId = (event: AdkEvent, ordinal: number): string =>
|
|
32
|
-
event.id ? `${event.id}:ai${ordinal === 0 ? "" : ordinal}` :
|
|
32
|
+
event.id ? `${event.id}:ai${ordinal === 0 ? "" : ordinal}` : generateId();
|
|
33
33
|
|
|
34
34
|
const ADK_REQUEST_CONFIRMATION = "adk_request_confirmation";
|
|
35
35
|
const ADK_REQUEST_CREDENTIAL = "adk_request_credential";
|
|
@@ -191,6 +191,8 @@ export class AdkEventAccumulator {
|
|
|
191
191
|
private messagesMap = new Map<string, AdkMessage>();
|
|
192
192
|
private currentMessageId: string | null = null;
|
|
193
193
|
private partialTextBuffer = "";
|
|
194
|
+
private finalTextReplacedThisEvent = false;
|
|
195
|
+
private finalReasoningReplacedThisEvent = false;
|
|
194
196
|
private partialReasoningBuffer = "";
|
|
195
197
|
private accumulatedStateDelta: Record<string, unknown> = {};
|
|
196
198
|
private accumulatedArtifactDelta: Record<string, number> = {};
|
|
@@ -331,7 +333,8 @@ export class AdkEventAccumulator {
|
|
|
331
333
|
if (event.author === "user") {
|
|
332
334
|
this.finalizeCurrentMessage();
|
|
333
335
|
const humanParts: AdkMessageContentPart[] = [];
|
|
334
|
-
|
|
336
|
+
const toolMessages: AdkMessage[] = [];
|
|
337
|
+
for (const [index, part] of parts.entries()) {
|
|
335
338
|
if (part.text != null && !part.thought) {
|
|
336
339
|
humanParts.push({ type: "text", text: part.text });
|
|
337
340
|
} else if (part.inlineData) {
|
|
@@ -342,10 +345,32 @@ export class AdkEventAccumulator {
|
|
|
342
345
|
humanParts.push(
|
|
343
346
|
fileDataToPart(part.fileData.fileUri, part.fileData.mimeType),
|
|
344
347
|
);
|
|
348
|
+
} else if (part.functionResponse?.id) {
|
|
349
|
+
// ADK records tool confirmation and other client-supplied tool
|
|
350
|
+
// results as user-authored function responses, and its request
|
|
351
|
+
// confirmation processors search user events for them. Dropping
|
|
352
|
+
// them here would replay a settled gate as pending. A response
|
|
353
|
+
// carrying no id answers no call: core drops it as an orphan, and
|
|
354
|
+
// keeping it would let it settle the batch it was grouped into.
|
|
355
|
+
toolMessages.push({
|
|
356
|
+
id: toolMessageId(event, index),
|
|
357
|
+
type: "tool",
|
|
358
|
+
tool_call_id: part.functionResponse.id,
|
|
359
|
+
name: part.functionResponse.name,
|
|
360
|
+
content: JSON.stringify(part.functionResponse.response),
|
|
361
|
+
status: "success",
|
|
362
|
+
});
|
|
345
363
|
}
|
|
346
364
|
}
|
|
365
|
+
// The replies answer the preceding assistant turn, so they are emitted
|
|
366
|
+
// before any user content in the same event. A human message between the
|
|
367
|
+
// tool call and its reply splits them into separate converted messages,
|
|
368
|
+
// orphaning the reply and leaving its gate unsettled.
|
|
369
|
+
for (const toolMsg of toolMessages) {
|
|
370
|
+
this.messagesMap.set(toolMsg.id, toolMsg);
|
|
371
|
+
}
|
|
347
372
|
if (humanParts.length > 0) {
|
|
348
|
-
const id = event.id ??
|
|
373
|
+
const id = event.id ?? generateId();
|
|
349
374
|
const first = humanParts[0];
|
|
350
375
|
const content: string | AdkMessageContentPart[] =
|
|
351
376
|
humanParts.length === 1 && first?.type === "text"
|
|
@@ -364,6 +389,11 @@ export class AdkEventAccumulator {
|
|
|
364
389
|
}
|
|
365
390
|
}
|
|
366
391
|
|
|
392
|
+
// Replace-semantics close out the streamed partial buffer, which only
|
|
393
|
+
// the first final text/reasoning part of an event may do; later parts
|
|
394
|
+
// of the same event are distinct content and append.
|
|
395
|
+
this.finalTextReplacedThisEvent = false;
|
|
396
|
+
this.finalReasoningReplacedThisEvent = false;
|
|
367
397
|
for (const [index, part] of parts.entries()) {
|
|
368
398
|
this.processPart(part, event, index);
|
|
369
399
|
}
|
|
@@ -449,9 +479,12 @@ export class AdkEventAccumulator {
|
|
|
449
479
|
if (event.partial) {
|
|
450
480
|
this.partialReasoningBuffer += part.text;
|
|
451
481
|
this.replaceLastReasoningContent(msg, this.partialReasoningBuffer);
|
|
452
|
-
} else {
|
|
482
|
+
} else if (!this.finalReasoningReplacedThisEvent) {
|
|
483
|
+
this.finalReasoningReplacedThisEvent = true;
|
|
453
484
|
this.partialReasoningBuffer = "";
|
|
454
485
|
this.replaceLastReasoningContent(msg, part.text);
|
|
486
|
+
} else {
|
|
487
|
+
this.appendContent(msg, { type: "reasoning", text: part.text });
|
|
455
488
|
}
|
|
456
489
|
return;
|
|
457
490
|
}
|
|
@@ -462,9 +495,12 @@ export class AdkEventAccumulator {
|
|
|
462
495
|
if (event.partial) {
|
|
463
496
|
this.partialTextBuffer += part.text;
|
|
464
497
|
this.replaceLastTextContent(msg, this.partialTextBuffer);
|
|
465
|
-
} else {
|
|
498
|
+
} else if (!this.finalTextReplacedThisEvent) {
|
|
499
|
+
this.finalTextReplacedThisEvent = true;
|
|
466
500
|
this.partialTextBuffer = "";
|
|
467
501
|
this.replaceLastTextContent(msg, part.text);
|
|
502
|
+
} else {
|
|
503
|
+
this.appendContent(msg, { type: "text", text: part.text });
|
|
468
504
|
}
|
|
469
505
|
return;
|
|
470
506
|
}
|
|
@@ -474,7 +510,7 @@ export class AdkEventAccumulator {
|
|
|
474
510
|
if (event.partial) return;
|
|
475
511
|
const msg = this.getOrCreateAiMessage(event);
|
|
476
512
|
const toolCall: AdkToolCall = {
|
|
477
|
-
id: part.functionCall.id ??
|
|
513
|
+
id: part.functionCall.id ?? generateId(),
|
|
478
514
|
name: part.functionCall.name,
|
|
479
515
|
args: part.functionCall.args as ReadonlyJSONObject,
|
|
480
516
|
argsText: JSON.stringify(part.functionCall.args),
|
|
@@ -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: [] };
|