@assistant-ui/ai-sdk 0.0.9 → 0.0.11
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/converters/convertMessage.d.ts.map +1 -1
- package/dist/converters/convertMessage.js +59 -6
- package/dist/converters/convertMessage.js.map +1 -1
- package/dist/converters/toCreateMessage.d.ts.map +1 -1
- package/dist/converters/toCreateMessage.js +2 -1
- package/dist/converters/toCreateMessage.js.map +1 -1
- package/dist/converters/toolApprovalAnswers.d.ts +3 -0
- package/dist/converters/toolApprovalAnswers.d.ts.map +1 -0
- package/dist/converters/toolApprovalAnswers.js +17 -0
- package/dist/converters/toolApprovalAnswers.js.map +1 -0
- package/dist/runtime/AISDKChat.js.map +1 -1
- package/dist/runtime/AISDKThreads.js.map +1 -1
- package/dist/runtime/sdkIdentity.js +1 -1
- package/dist/runtime/useAISDKRuntime.js.map +1 -1
- package/dist/runtime/useChatRuntime.js.map +1 -1
- package/dist/runtime/useExternalHistory.d.ts.map +1 -1
- package/dist/runtime/useExternalHistory.js +4 -0
- package/dist/runtime/useExternalHistory.js.map +1 -1
- package/dist/runtime/useResourceCleanup.js.map +1 -1
- package/dist/runtime/useStreamingTiming.js.map +1 -1
- package/dist/tools/generativeTools.d.ts.map +1 -1
- package/dist/tools/generativeTools.js +12 -1
- package/dist/tools/generativeTools.js.map +1 -1
- package/dist/transport/resumable.js.map +1 -1
- package/dist/usage.js.map +1 -1
- package/package.json +12 -11
- package/src/converters/convertMessage.test.ts +266 -0
- package/src/converters/convertMessage.ts +97 -4
- package/src/converters/toCreateMessage.test.ts +13 -0
- package/src/converters/toCreateMessage.ts +1 -0
- package/src/converters/toolApprovalAnswers.ts +27 -0
- package/src/runtime/useExternalHistory.test.ts +94 -0
- package/src/runtime/useExternalHistory.ts +4 -0
- package/src/tools/generativeTools.test.ts +189 -1
- package/src/tools/generativeTools.ts +23 -2
|
@@ -666,6 +666,119 @@ describe("AISDKMessageConverter", () => {
|
|
|
666
666
|
]);
|
|
667
667
|
});
|
|
668
668
|
|
|
669
|
+
it("reads questions and recorded answers from the approval descriptor for a custom response channel", () => {
|
|
670
|
+
const questions = [
|
|
671
|
+
{
|
|
672
|
+
id: "scope",
|
|
673
|
+
prompt: "Which files?",
|
|
674
|
+
header: "Scope",
|
|
675
|
+
options: [
|
|
676
|
+
{ id: "src", label: "src", description: "Sources" },
|
|
677
|
+
{ id: 1, label: "invalid" },
|
|
678
|
+
],
|
|
679
|
+
multiple: true,
|
|
680
|
+
},
|
|
681
|
+
{ id: "note", prompt: "Anything else?", allowFreeform: "yes" },
|
|
682
|
+
{ prompt: "No id" },
|
|
683
|
+
];
|
|
684
|
+
const parts = [
|
|
685
|
+
{
|
|
686
|
+
type: "tool-ask",
|
|
687
|
+
toolCallId: "tc-1",
|
|
688
|
+
state: "approval-requested",
|
|
689
|
+
input: {},
|
|
690
|
+
approval: {
|
|
691
|
+
id: "approval-1",
|
|
692
|
+
descriptor: { display: "questions", questions },
|
|
693
|
+
},
|
|
694
|
+
},
|
|
695
|
+
{
|
|
696
|
+
type: "tool-ask",
|
|
697
|
+
toolCallId: "tc-2",
|
|
698
|
+
state: "approval-responded",
|
|
699
|
+
input: {},
|
|
700
|
+
approval: {
|
|
701
|
+
id: "approval-2",
|
|
702
|
+
approved: true,
|
|
703
|
+
descriptor: {
|
|
704
|
+
display: "questions",
|
|
705
|
+
questions: [{ id: "note", prompt: "Anything else?" }],
|
|
706
|
+
answers: {
|
|
707
|
+
note: { text: "no", optionIds: [1, "x"] },
|
|
708
|
+
bad: "x",
|
|
709
|
+
empty: { optionIds: [1] },
|
|
710
|
+
},
|
|
711
|
+
},
|
|
712
|
+
},
|
|
713
|
+
},
|
|
714
|
+
];
|
|
715
|
+
|
|
716
|
+
const convert = (supportsRichToolApprovalResponses: boolean) => {
|
|
717
|
+
const metadata: AISDKMessageConverterMetadata = {
|
|
718
|
+
supportsRichToolApprovalResponses,
|
|
719
|
+
};
|
|
720
|
+
return AISDKMessageConverter.toThreadMessages(
|
|
721
|
+
[{ id: "a1", role: "assistant", parts } as any],
|
|
722
|
+
false,
|
|
723
|
+
metadata,
|
|
724
|
+
)[0]?.content.map((part) => (part as { approval?: any }).approval);
|
|
725
|
+
};
|
|
726
|
+
|
|
727
|
+
const [requested, responded] = convert(true)!;
|
|
728
|
+
expect(requested.display).toBe("questions");
|
|
729
|
+
expect(requested.questions).toEqual([
|
|
730
|
+
{
|
|
731
|
+
id: "scope",
|
|
732
|
+
prompt: "Which files?",
|
|
733
|
+
header: "Scope",
|
|
734
|
+
options: [{ id: "src", label: "src", description: "Sources" }],
|
|
735
|
+
multiple: true,
|
|
736
|
+
},
|
|
737
|
+
{ id: "note", prompt: "Anything else?" },
|
|
738
|
+
]);
|
|
739
|
+
expect(responded.approved).toBe(true);
|
|
740
|
+
expect(responded.answers).toEqual({
|
|
741
|
+
note: { text: "no", optionIds: ["x"] },
|
|
742
|
+
});
|
|
743
|
+
expect(
|
|
744
|
+
convertWith(JSON.parse('{"__proto__":{"text":"kept"}}')).answers
|
|
745
|
+
.__proto__,
|
|
746
|
+
).toEqual({ text: "kept" });
|
|
747
|
+
|
|
748
|
+
function convertWith(answers: unknown) {
|
|
749
|
+
const metadata: AISDKMessageConverterMetadata = {
|
|
750
|
+
supportsRichToolApprovalResponses: true,
|
|
751
|
+
};
|
|
752
|
+
return AISDKMessageConverter.toThreadMessages(
|
|
753
|
+
[
|
|
754
|
+
{
|
|
755
|
+
id: "a2",
|
|
756
|
+
role: "assistant",
|
|
757
|
+
parts: [
|
|
758
|
+
{
|
|
759
|
+
type: "tool-ask",
|
|
760
|
+
toolCallId: "tc-3",
|
|
761
|
+
state: "approval-responded",
|
|
762
|
+
input: {},
|
|
763
|
+
approval: {
|
|
764
|
+
id: "approval-3",
|
|
765
|
+
approved: true,
|
|
766
|
+
descriptor: { display: "questions", answers },
|
|
767
|
+
},
|
|
768
|
+
},
|
|
769
|
+
],
|
|
770
|
+
} as any,
|
|
771
|
+
],
|
|
772
|
+
false,
|
|
773
|
+
metadata,
|
|
774
|
+
)[0]?.content.map((part) => (part as { approval?: any }).approval)[0];
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
const [builtIn] = convert(false)!;
|
|
778
|
+
expect(builtIn).not.toHaveProperty("display");
|
|
779
|
+
expect(builtIn).not.toHaveProperty("questions");
|
|
780
|
+
});
|
|
781
|
+
|
|
669
782
|
it("ignores a non-boolean dismissible field from the approval descriptor", () => {
|
|
670
783
|
const metadata: AISDKMessageConverterMetadata = {
|
|
671
784
|
supportsRichToolApprovalResponses: true,
|
|
@@ -1416,6 +1529,123 @@ describe("AISDKMessageConverter", () => {
|
|
|
1416
1529
|
});
|
|
1417
1530
|
});
|
|
1418
1531
|
|
|
1532
|
+
it("forwards toolMetadata.app without a tool-supplied serverId", () => {
|
|
1533
|
+
const converted = AISDKMessageConverter.toThreadMessages([
|
|
1534
|
+
{
|
|
1535
|
+
id: "a1",
|
|
1536
|
+
role: "assistant",
|
|
1537
|
+
parts: [
|
|
1538
|
+
{
|
|
1539
|
+
type: "tool-search",
|
|
1540
|
+
toolCallId: "tc-1",
|
|
1541
|
+
state: "output-available",
|
|
1542
|
+
input: { query: "hi" },
|
|
1543
|
+
output: { results: [] },
|
|
1544
|
+
toolMetadata: {
|
|
1545
|
+
app: {
|
|
1546
|
+
resourceUri: "ui://example/search",
|
|
1547
|
+
mimeType: "text/html;profile=mcp-app",
|
|
1548
|
+
visibility: ["app", "model", "bogus"],
|
|
1549
|
+
serverId: "search-server",
|
|
1550
|
+
},
|
|
1551
|
+
},
|
|
1552
|
+
},
|
|
1553
|
+
],
|
|
1554
|
+
} as any,
|
|
1555
|
+
]);
|
|
1556
|
+
|
|
1557
|
+
const call = converted[0]?.content.find(
|
|
1558
|
+
(part): part is any => part.type === "tool-call",
|
|
1559
|
+
);
|
|
1560
|
+
expect(call?.mcp?.app).toEqual({
|
|
1561
|
+
resourceUri: "ui://example/search",
|
|
1562
|
+
mimeType: "text/html;profile=mcp-app",
|
|
1563
|
+
visibility: ["app", "model"],
|
|
1564
|
+
});
|
|
1565
|
+
});
|
|
1566
|
+
|
|
1567
|
+
it.each([undefined, "", "other-server"])(
|
|
1568
|
+
"keeps provider serverId when the tool supplies %s",
|
|
1569
|
+
(serverId) => {
|
|
1570
|
+
const converted = AISDKMessageConverter.toThreadMessages([
|
|
1571
|
+
{
|
|
1572
|
+
id: "a1",
|
|
1573
|
+
role: "assistant",
|
|
1574
|
+
parts: [
|
|
1575
|
+
{
|
|
1576
|
+
type: "tool-search",
|
|
1577
|
+
toolCallId: "tc-1",
|
|
1578
|
+
state: "output-available",
|
|
1579
|
+
input: { query: "hi" },
|
|
1580
|
+
output: { results: [] },
|
|
1581
|
+
toolMetadata: {
|
|
1582
|
+
app: {
|
|
1583
|
+
resourceUri: "ui://example/search",
|
|
1584
|
+
mimeType: "text/html;profile=mcp-app",
|
|
1585
|
+
serverId,
|
|
1586
|
+
},
|
|
1587
|
+
},
|
|
1588
|
+
callProviderMetadata: {
|
|
1589
|
+
mcp: {
|
|
1590
|
+
app: {
|
|
1591
|
+
resourceUri: "ui://example/search",
|
|
1592
|
+
serverId: "search-server",
|
|
1593
|
+
},
|
|
1594
|
+
},
|
|
1595
|
+
},
|
|
1596
|
+
},
|
|
1597
|
+
],
|
|
1598
|
+
} as any,
|
|
1599
|
+
]);
|
|
1600
|
+
|
|
1601
|
+
const call = converted[0]?.content.find(
|
|
1602
|
+
(part): part is any => part.type === "tool-call",
|
|
1603
|
+
);
|
|
1604
|
+
expect(call?.mcp?.app).toEqual({
|
|
1605
|
+
resourceUri: "ui://example/search",
|
|
1606
|
+
mimeType: "text/html;profile=mcp-app",
|
|
1607
|
+
serverId: "search-server",
|
|
1608
|
+
});
|
|
1609
|
+
},
|
|
1610
|
+
);
|
|
1611
|
+
|
|
1612
|
+
it("falls back to provider app metadata when toolMetadata.app has an invalid URI", () => {
|
|
1613
|
+
const converted = AISDKMessageConverter.toThreadMessages([
|
|
1614
|
+
{
|
|
1615
|
+
id: "a1",
|
|
1616
|
+
role: "assistant",
|
|
1617
|
+
parts: [
|
|
1618
|
+
{
|
|
1619
|
+
type: "tool-search",
|
|
1620
|
+
toolCallId: "tc-1",
|
|
1621
|
+
state: "output-available",
|
|
1622
|
+
input: { query: "hi" },
|
|
1623
|
+
output: { results: [] },
|
|
1624
|
+
toolMetadata: {
|
|
1625
|
+
app: { resourceUri: "https://example.com/search" },
|
|
1626
|
+
},
|
|
1627
|
+
callProviderMetadata: {
|
|
1628
|
+
mcp: {
|
|
1629
|
+
app: {
|
|
1630
|
+
resourceUri: "ui://example/search",
|
|
1631
|
+
mimeType: "text/html;profile=mcp-app",
|
|
1632
|
+
},
|
|
1633
|
+
},
|
|
1634
|
+
},
|
|
1635
|
+
},
|
|
1636
|
+
],
|
|
1637
|
+
} as any,
|
|
1638
|
+
]);
|
|
1639
|
+
|
|
1640
|
+
const call = converted[0]?.content.find(
|
|
1641
|
+
(part): part is any => part.type === "tool-call",
|
|
1642
|
+
);
|
|
1643
|
+
expect(call?.mcp?.app).toEqual({
|
|
1644
|
+
resourceUri: "ui://example/search",
|
|
1645
|
+
mimeType: "text/html;profile=mcp-app",
|
|
1646
|
+
});
|
|
1647
|
+
});
|
|
1648
|
+
|
|
1419
1649
|
it("omits an empty callProviderMetadata.mcp.app.serverId", () => {
|
|
1420
1650
|
const converted = AISDKMessageConverter.toThreadMessages([
|
|
1421
1651
|
{
|
|
@@ -1483,6 +1713,42 @@ describe("AISDKMessageConverter", () => {
|
|
|
1483
1713
|
expect(converted[0]?.content[2]).not.toHaveProperty("providerMetadata");
|
|
1484
1714
|
});
|
|
1485
1715
|
|
|
1716
|
+
it("keeps a step scoped reasoning block id off the part", () => {
|
|
1717
|
+
const converted = AISDKMessageConverter.toThreadMessages([
|
|
1718
|
+
{
|
|
1719
|
+
id: "a1",
|
|
1720
|
+
role: "assistant",
|
|
1721
|
+
parts: [
|
|
1722
|
+
{ type: "reasoning", id: "reasoning-0", text: "first step" },
|
|
1723
|
+
{ type: "text", text: "answer" },
|
|
1724
|
+
{ type: "reasoning", id: "reasoning-0", text: "second step" },
|
|
1725
|
+
],
|
|
1726
|
+
} as any,
|
|
1727
|
+
]);
|
|
1728
|
+
|
|
1729
|
+
expect(converted[0]?.content[0]).toMatchObject({ type: "reasoning" });
|
|
1730
|
+
expect(converted[0]?.content[0]).not.toHaveProperty("id");
|
|
1731
|
+
expect(converted[0]?.content[1]).not.toHaveProperty("id");
|
|
1732
|
+
expect(converted[0]?.content[2]).not.toHaveProperty("id");
|
|
1733
|
+
});
|
|
1734
|
+
|
|
1735
|
+
it("forwards data part ids", () => {
|
|
1736
|
+
const converted = AISDKMessageConverter.toThreadMessages([
|
|
1737
|
+
{
|
|
1738
|
+
id: "a1",
|
|
1739
|
+
role: "assistant",
|
|
1740
|
+
parts: [{ type: "data-chart", id: "data-1", data: { x: 1 } }],
|
|
1741
|
+
} as any,
|
|
1742
|
+
]);
|
|
1743
|
+
|
|
1744
|
+
expect(converted[0]?.content[0]).toMatchObject({
|
|
1745
|
+
type: "data",
|
|
1746
|
+
id: "data-1",
|
|
1747
|
+
name: "chart",
|
|
1748
|
+
data: { x: 1 },
|
|
1749
|
+
});
|
|
1750
|
+
});
|
|
1751
|
+
|
|
1486
1752
|
it("maps TextUIPart.state onto the per-part status", () => {
|
|
1487
1753
|
const converted = AISDKMessageConverter.toThreadMessages([
|
|
1488
1754
|
{
|
|
@@ -13,6 +13,8 @@ import {
|
|
|
13
13
|
isMcpAppUri,
|
|
14
14
|
type ReasoningMessagePart,
|
|
15
15
|
type ToolApprovalOption,
|
|
16
|
+
type ToolApprovalQuestion,
|
|
17
|
+
type ToolApprovalQuestionOption,
|
|
16
18
|
type ToolCallMessagePart,
|
|
17
19
|
type TextMessagePart,
|
|
18
20
|
type DataMessagePart,
|
|
@@ -26,6 +28,7 @@ import {
|
|
|
26
28
|
type RespondToToolApprovalOptions,
|
|
27
29
|
type Unstable_ToolInteractionLog,
|
|
28
30
|
} from "@assistant-ui/core";
|
|
31
|
+
import { normalizeToolApprovalAnswers } from "./toolApprovalAnswers";
|
|
29
32
|
import { stableStringifyToolArgs } from "@assistant-ui/core/internal";
|
|
30
33
|
import {
|
|
31
34
|
parsePartialJsonObject,
|
|
@@ -95,17 +98,45 @@ function extractMcpAppMetadata(
|
|
|
95
98
|
cache: Map<string, McpAppMetadata> | undefined,
|
|
96
99
|
): McpAppMetadata | undefined {
|
|
97
100
|
if (!part || typeof part !== "object") return undefined;
|
|
101
|
+
const toolMetadata = (part as { toolMetadata?: unknown }).toolMetadata;
|
|
102
|
+
const toolApp =
|
|
103
|
+
toolMetadata && typeof toolMetadata === "object"
|
|
104
|
+
? (toolMetadata as { app?: unknown }).app
|
|
105
|
+
: undefined;
|
|
98
106
|
const meta = (part as { callProviderMetadata?: unknown })
|
|
99
107
|
.callProviderMetadata;
|
|
100
108
|
const mcp =
|
|
101
109
|
meta && typeof meta === "object"
|
|
102
110
|
? (meta as { mcp?: unknown }).mcp
|
|
103
111
|
: undefined;
|
|
104
|
-
const
|
|
112
|
+
const providerApp =
|
|
105
113
|
mcp && typeof mcp === "object" ? (mcp as { app?: unknown }).app : undefined;
|
|
114
|
+
|
|
115
|
+
const apps: Record<string, unknown>[] = [];
|
|
116
|
+
for (const candidate of [toolApp, providerApp]) {
|
|
117
|
+
if (!candidate || typeof candidate !== "object") continue;
|
|
118
|
+
const candidateApp = candidate as Record<string, unknown>;
|
|
119
|
+
const resourceUri = candidateApp["resourceUri"];
|
|
120
|
+
if (typeof resourceUri !== "string" || !isMcpAppUri(resourceUri)) continue;
|
|
121
|
+
apps.push({
|
|
122
|
+
resourceUri,
|
|
123
|
+
...(typeof candidateApp["mimeType"] === "string" && {
|
|
124
|
+
mimeType: candidateApp["mimeType"],
|
|
125
|
+
}),
|
|
126
|
+
...(Array.isArray(candidateApp["visibility"]) && {
|
|
127
|
+
visibility: candidateApp["visibility"],
|
|
128
|
+
}),
|
|
129
|
+
...(candidate === providerApp &&
|
|
130
|
+
typeof candidateApp["serverId"] === "string" &&
|
|
131
|
+
candidateApp["serverId"].length > 0 && {
|
|
132
|
+
serverId: candidateApp["serverId"],
|
|
133
|
+
}),
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
106
137
|
let a: Record<string, unknown>;
|
|
107
|
-
if (
|
|
108
|
-
a =
|
|
138
|
+
if (apps.length > 0) {
|
|
139
|
+
a = { ...(apps[1] ?? {}), ...apps[0]! };
|
|
109
140
|
} else {
|
|
110
141
|
// MCP-UI tools surface the pointer on result._meta: canonical nested
|
|
111
142
|
// `ui.resourceUri`, or the deprecated flat `"ui/resourceUri"` key.
|
|
@@ -221,6 +252,51 @@ const normalizeToolApprovalOptions = (
|
|
|
221
252
|
});
|
|
222
253
|
};
|
|
223
254
|
|
|
255
|
+
const normalizeToolApprovalQuestions = (
|
|
256
|
+
questions: unknown,
|
|
257
|
+
): readonly ToolApprovalQuestion[] | undefined => {
|
|
258
|
+
if (!Array.isArray(questions)) return undefined;
|
|
259
|
+
|
|
260
|
+
return questions.flatMap<ToolApprovalQuestion>((value) => {
|
|
261
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return [];
|
|
262
|
+
const question = value as Record<string, unknown>;
|
|
263
|
+
if (typeof question.id !== "string" || typeof question.prompt !== "string")
|
|
264
|
+
return [];
|
|
265
|
+
const options = Array.isArray(question.options)
|
|
266
|
+
? question.options.flatMap<ToolApprovalQuestionOption>((option) => {
|
|
267
|
+
if (!option || typeof option !== "object" || Array.isArray(option))
|
|
268
|
+
return [];
|
|
269
|
+
const { id, label, description } = option as Record<string, unknown>;
|
|
270
|
+
if (typeof id !== "string" || typeof label !== "string") return [];
|
|
271
|
+
return [
|
|
272
|
+
{
|
|
273
|
+
id,
|
|
274
|
+
label,
|
|
275
|
+
...(typeof description === "string" && { description }),
|
|
276
|
+
},
|
|
277
|
+
];
|
|
278
|
+
})
|
|
279
|
+
: undefined;
|
|
280
|
+
|
|
281
|
+
return [
|
|
282
|
+
{
|
|
283
|
+
id: question.id,
|
|
284
|
+
prompt: question.prompt,
|
|
285
|
+
...(typeof question.header === "string" && {
|
|
286
|
+
header: question.header,
|
|
287
|
+
}),
|
|
288
|
+
...(options && { options }),
|
|
289
|
+
...(typeof question.multiple === "boolean" && {
|
|
290
|
+
multiple: question.multiple,
|
|
291
|
+
}),
|
|
292
|
+
...(typeof question.allowFreeform === "boolean" && {
|
|
293
|
+
allowFreeform: question.allowFreeform,
|
|
294
|
+
}),
|
|
295
|
+
},
|
|
296
|
+
];
|
|
297
|
+
});
|
|
298
|
+
};
|
|
299
|
+
|
|
224
300
|
const APPROVAL_DESCRIPTOR_FIELDS = [
|
|
225
301
|
"prompt",
|
|
226
302
|
"display",
|
|
@@ -229,6 +305,8 @@ const APPROVAL_DESCRIPTOR_FIELDS = [
|
|
|
229
305
|
"options",
|
|
230
306
|
"optionId",
|
|
231
307
|
"text",
|
|
308
|
+
"questions",
|
|
309
|
+
"answers",
|
|
232
310
|
"resolution",
|
|
233
311
|
] as const;
|
|
234
312
|
|
|
@@ -302,6 +380,8 @@ function getToolApprovalAndInterrupt(
|
|
|
302
380
|
options,
|
|
303
381
|
optionId,
|
|
304
382
|
text,
|
|
383
|
+
questions,
|
|
384
|
+
answers,
|
|
305
385
|
...additionalApprovalFields
|
|
306
386
|
} = response
|
|
307
387
|
? {
|
|
@@ -310,11 +390,18 @@ function getToolApprovalAndInterrupt(
|
|
|
310
390
|
...(response.reason != null && { reason: response.reason }),
|
|
311
391
|
...(response.optionId != null && { optionId: response.optionId }),
|
|
312
392
|
...(response.text != null && { text: response.text }),
|
|
393
|
+
...(response.answers != null && { answers: response.answers }),
|
|
313
394
|
}
|
|
314
395
|
: approval;
|
|
315
396
|
const normalizedOptions = supportsRichToolApprovalResponses
|
|
316
397
|
? normalizeToolApprovalOptions(options)
|
|
317
398
|
: undefined;
|
|
399
|
+
const normalizedQuestions = supportsRichToolApprovalResponses
|
|
400
|
+
? normalizeToolApprovalQuestions(questions)
|
|
401
|
+
: undefined;
|
|
402
|
+
const normalizedAnswers = supportsRichToolApprovalResponses
|
|
403
|
+
? normalizeToolApprovalAnswers(answers)
|
|
404
|
+
: undefined;
|
|
318
405
|
const requestReason = additionalApprovalFields.requestReason;
|
|
319
406
|
if (typeof id === "string")
|
|
320
407
|
return {
|
|
@@ -332,12 +419,15 @@ function getToolApprovalAndInterrupt(
|
|
|
332
419
|
...(supportsRichToolApprovalResponses && {
|
|
333
420
|
...((display === "decision" ||
|
|
334
421
|
display === "select" ||
|
|
335
|
-
display === "text"
|
|
422
|
+
display === "text" ||
|
|
423
|
+
display === "questions") && { display }),
|
|
336
424
|
...(typeof allowFreeform === "boolean" && { allowFreeform }),
|
|
337
425
|
...(typeof dismissible === "boolean" && { dismissible }),
|
|
338
426
|
...(normalizedOptions && { options: normalizedOptions }),
|
|
339
427
|
...(typeof optionId === "string" && { optionId }),
|
|
340
428
|
...(typeof text === "string" && { text }),
|
|
429
|
+
...(normalizedQuestions && { questions: normalizedQuestions }),
|
|
430
|
+
...(normalizedAnswers && { answers: normalizedAnswers }),
|
|
341
431
|
}),
|
|
342
432
|
...((resolution === "cancelled" || resolution === "expired") && {
|
|
343
433
|
resolution,
|
|
@@ -585,6 +675,9 @@ function convertParts(
|
|
|
585
675
|
if (part.type.startsWith("data-")) {
|
|
586
676
|
return {
|
|
587
677
|
type: "data",
|
|
678
|
+
...("id" in part && typeof part.id === "string"
|
|
679
|
+
? { id: part.id }
|
|
680
|
+
: undefined),
|
|
588
681
|
name: part.type.substring(5),
|
|
589
682
|
data: (part as any).data,
|
|
590
683
|
} satisfies DataMessagePart;
|
|
@@ -185,6 +185,19 @@ describe("toCreateMessage", () => {
|
|
|
185
185
|
]);
|
|
186
186
|
});
|
|
187
187
|
|
|
188
|
+
it("forwards a data part id", () => {
|
|
189
|
+
const message = {
|
|
190
|
+
...baseMessage,
|
|
191
|
+
content: [
|
|
192
|
+
{ type: "data", name: "workflow", id: "p1", data: { field: 1 } },
|
|
193
|
+
],
|
|
194
|
+
} as unknown as AppendMessage;
|
|
195
|
+
|
|
196
|
+
expect(toCreateMessage(message).parts).toEqual([
|
|
197
|
+
{ type: "data-workflow", id: "p1", data: { field: 1 } },
|
|
198
|
+
]);
|
|
199
|
+
});
|
|
200
|
+
|
|
188
201
|
it("converts a data part inside an attachment without throwing", () => {
|
|
189
202
|
const message = {
|
|
190
203
|
...baseMessage,
|
|
@@ -86,6 +86,7 @@ export const toCreateMessage = <UI_MESSAGE extends UIMessage = UIMessage>(
|
|
|
86
86
|
return {
|
|
87
87
|
type: `data-${part.name}`,
|
|
88
88
|
data: part.data,
|
|
89
|
+
...(part.id !== undefined && { id: part.id }),
|
|
89
90
|
};
|
|
90
91
|
default:
|
|
91
92
|
throw new Error(`Unsupported part type: ${part.type}`);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ToolApprovalAnswer } from "@assistant-ui/core";
|
|
2
|
+
|
|
3
|
+
export const normalizeToolApprovalAnswers = (
|
|
4
|
+
answers: unknown,
|
|
5
|
+
): Readonly<Record<string, ToolApprovalAnswer>> | undefined => {
|
|
6
|
+
if (!answers || typeof answers !== "object" || Array.isArray(answers))
|
|
7
|
+
return undefined;
|
|
8
|
+
|
|
9
|
+
return Object.fromEntries(
|
|
10
|
+
Object.entries(answers).flatMap(([id, value]) => {
|
|
11
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
12
|
+
return [];
|
|
13
|
+
const { optionIds, text } = value as Record<string, unknown>;
|
|
14
|
+
const answer: ToolApprovalAnswer = {
|
|
15
|
+
...(Array.isArray(optionIds) && {
|
|
16
|
+
optionIds: optionIds.filter(
|
|
17
|
+
(optionId): optionId is string => typeof optionId === "string",
|
|
18
|
+
),
|
|
19
|
+
}),
|
|
20
|
+
...(typeof text === "string" && { text }),
|
|
21
|
+
};
|
|
22
|
+
return answer.optionIds?.length || answer.text !== undefined
|
|
23
|
+
? [[id, answer]]
|
|
24
|
+
: [];
|
|
25
|
+
}),
|
|
26
|
+
);
|
|
27
|
+
};
|
|
@@ -1383,6 +1383,100 @@ describe("useExternalHistory persistence", () => {
|
|
|
1383
1383
|
);
|
|
1384
1384
|
});
|
|
1385
1385
|
|
|
1386
|
+
it("stores and restores questionnaire answers with the approval response", async () => {
|
|
1387
|
+
const answers = {
|
|
1388
|
+
scope: { optionIds: ["src", "tests"] },
|
|
1389
|
+
note: { text: "keep it short" },
|
|
1390
|
+
};
|
|
1391
|
+
const saved = new Map<string, RespondToToolApprovalOptions>();
|
|
1392
|
+
const writer = createPersistenceHarness(false, {
|
|
1393
|
+
toolApprovalResponses: saved,
|
|
1394
|
+
});
|
|
1395
|
+
const innerMessage: InnerMessage = {
|
|
1396
|
+
id: "inner-a",
|
|
1397
|
+
role: "assistant",
|
|
1398
|
+
parts: [
|
|
1399
|
+
{
|
|
1400
|
+
type: "tool-ask",
|
|
1401
|
+
toolCallId: "call-1",
|
|
1402
|
+
state: "approval-requested",
|
|
1403
|
+
approval: { id: "approval-1" },
|
|
1404
|
+
},
|
|
1405
|
+
],
|
|
1406
|
+
};
|
|
1407
|
+
const message = Object.assign(
|
|
1408
|
+
createAssistantMessage({ type: "complete", reason: "stop" }, [
|
|
1409
|
+
innerMessage,
|
|
1410
|
+
]),
|
|
1411
|
+
{
|
|
1412
|
+
content: [
|
|
1413
|
+
{
|
|
1414
|
+
type: "tool-call" as const,
|
|
1415
|
+
toolCallId: "call-1",
|
|
1416
|
+
toolName: "ask",
|
|
1417
|
+
args: {},
|
|
1418
|
+
argsText: "{}",
|
|
1419
|
+
result: undefined,
|
|
1420
|
+
isError: false,
|
|
1421
|
+
approval: { id: "approval-1" },
|
|
1422
|
+
},
|
|
1423
|
+
],
|
|
1424
|
+
},
|
|
1425
|
+
);
|
|
1426
|
+
await waitFor(() => expect(writer.load).toHaveBeenCalledTimes(1));
|
|
1427
|
+
saved.set("approval-1", {
|
|
1428
|
+
approvalId: "approval-1",
|
|
1429
|
+
approved: true,
|
|
1430
|
+
answers,
|
|
1431
|
+
});
|
|
1432
|
+
await writer.runCycle([message]);
|
|
1433
|
+
await waitFor(() =>
|
|
1434
|
+
expect(writer.append).toHaveBeenCalledWith({
|
|
1435
|
+
parentId: null,
|
|
1436
|
+
message: {
|
|
1437
|
+
...innerMessage,
|
|
1438
|
+
metadata: {
|
|
1439
|
+
__aui_toolApprovalResponses: {
|
|
1440
|
+
"approval-1": { approved: true, answers },
|
|
1441
|
+
},
|
|
1442
|
+
},
|
|
1443
|
+
},
|
|
1444
|
+
}),
|
|
1445
|
+
);
|
|
1446
|
+
writer.unmount();
|
|
1447
|
+
|
|
1448
|
+
const restored = new Map<string, RespondToToolApprovalOptions>();
|
|
1449
|
+
const reader = createPersistenceHarness(true, {
|
|
1450
|
+
loadMessages: {
|
|
1451
|
+
messages: [
|
|
1452
|
+
{
|
|
1453
|
+
parentId: null,
|
|
1454
|
+
message: {
|
|
1455
|
+
...innerMessage,
|
|
1456
|
+
metadata: {
|
|
1457
|
+
__aui_toolApprovalResponses: {
|
|
1458
|
+
"approval-1": {
|
|
1459
|
+
approved: true,
|
|
1460
|
+
answers: { ...answers, junk: { optionIds: [1] } },
|
|
1461
|
+
},
|
|
1462
|
+
},
|
|
1463
|
+
},
|
|
1464
|
+
},
|
|
1465
|
+
},
|
|
1466
|
+
],
|
|
1467
|
+
},
|
|
1468
|
+
toolApprovalResponses: restored,
|
|
1469
|
+
});
|
|
1470
|
+
await waitFor(() => expect(reader.load).toHaveBeenCalledTimes(1));
|
|
1471
|
+
await waitFor(() =>
|
|
1472
|
+
expect(restored.get("approval-1")).toEqual({
|
|
1473
|
+
approvalId: "approval-1",
|
|
1474
|
+
approved: true,
|
|
1475
|
+
answers,
|
|
1476
|
+
}),
|
|
1477
|
+
);
|
|
1478
|
+
});
|
|
1479
|
+
|
|
1386
1480
|
it("restores stored tool artifacts without returning metadata to the chat", async () => {
|
|
1387
1481
|
const toolArtifacts = new Map<string, unknown>();
|
|
1388
1482
|
const onSetMessages = vi.fn();
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
useCallback,
|
|
26
26
|
useMemo,
|
|
27
27
|
} from "react";
|
|
28
|
+
import { normalizeToolApprovalAnswers } from "../converters/toolApprovalAnswers";
|
|
28
29
|
|
|
29
30
|
export const toExportedMessageRepository = <TMessage>(
|
|
30
31
|
toThreadMessages: (messages: TMessage[]) => ThreadMessage[],
|
|
@@ -131,6 +132,7 @@ const collectToolApprovalResponses = (
|
|
|
131
132
|
approved: response.approved,
|
|
132
133
|
...(response.optionId != null && { optionId: response.optionId }),
|
|
133
134
|
...(response.text != null && { text: response.text }),
|
|
135
|
+
...(response.answers != null && { answers: response.answers }),
|
|
134
136
|
...(response.reason != null && { reason: response.reason }),
|
|
135
137
|
},
|
|
136
138
|
] as const,
|
|
@@ -246,6 +248,7 @@ const restoreToolData = <TMessage>(
|
|
|
246
248
|
if (toolApprovalResponses && isRecord(approvalResponses)) {
|
|
247
249
|
for (const [approvalId, value] of Object.entries(approvalResponses)) {
|
|
248
250
|
if (!isRecord(value) || typeof value.approved !== "boolean") continue;
|
|
251
|
+
const answers = normalizeToolApprovalAnswers(value.answers);
|
|
249
252
|
toolApprovalResponses.set(approvalId, {
|
|
250
253
|
approvalId,
|
|
251
254
|
approved: value.approved,
|
|
@@ -253,6 +256,7 @@ const restoreToolData = <TMessage>(
|
|
|
253
256
|
optionId: value.optionId,
|
|
254
257
|
}),
|
|
255
258
|
...(typeof value.text === "string" && { text: value.text }),
|
|
259
|
+
...(answers && Object.keys(answers).length > 0 && { answers }),
|
|
256
260
|
...(typeof value.reason === "string" && { reason: value.reason }),
|
|
257
261
|
});
|
|
258
262
|
}
|