@convex-dev/agent 0.5.0-alpha.1 → 0.6.0-alpha.1
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/MIGRATION.md +153 -0
- package/README.md +32 -27
- package/dist/UIMessages.d.ts +46 -0
- package/dist/UIMessages.d.ts.map +1 -0
- package/dist/UIMessages.js +546 -0
- package/dist/UIMessages.js.map +1 -0
- package/dist/client/createTool.d.ts +126 -27
- package/dist/client/createTool.d.ts.map +1 -1
- package/dist/client/createTool.js +67 -12
- package/dist/client/createTool.js.map +1 -1
- package/dist/client/defaultComponent.d.ts +11 -0
- package/dist/client/defaultComponent.d.ts.map +1 -0
- package/dist/client/defaultComponent.js +7 -0
- package/dist/client/defaultComponent.js.map +1 -0
- package/dist/client/definePlaygroundAPI.d.ts +1335 -204
- package/dist/client/definePlaygroundAPI.d.ts.map +1 -1
- package/dist/client/definePlaygroundAPI.js +52 -28
- package/dist/client/definePlaygroundAPI.js.map +1 -1
- package/dist/client/files.d.ts +20 -7
- package/dist/client/files.d.ts.map +1 -1
- package/dist/client/files.js +68 -11
- package/dist/client/files.js.map +1 -1
- package/dist/client/index.d.ts +1116 -978
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +332 -747
- package/dist/client/index.js.map +1 -1
- package/dist/client/messages.d.ts +461 -0
- package/dist/client/messages.d.ts.map +1 -0
- package/dist/client/messages.js +106 -0
- package/dist/client/messages.js.map +1 -0
- package/dist/client/mockModel.d.ts +42 -0
- package/dist/client/mockModel.d.ts.map +1 -0
- package/dist/client/mockModel.js +182 -0
- package/dist/client/mockModel.js.map +1 -0
- package/dist/client/saveInputMessages.d.ts +20 -0
- package/dist/client/saveInputMessages.d.ts.map +1 -0
- package/dist/client/saveInputMessages.js +58 -0
- package/dist/client/saveInputMessages.js.map +1 -0
- package/dist/client/search.d.ts +350 -39
- package/dist/client/search.d.ts.map +1 -1
- package/dist/client/search.js +350 -39
- package/dist/client/search.js.map +1 -1
- package/dist/client/start.d.ts +84 -0
- package/dist/client/start.d.ts.map +1 -0
- package/dist/client/start.js +185 -0
- package/dist/client/start.js.map +1 -0
- package/dist/client/streamText.d.ts +46 -0
- package/dist/client/streamText.d.ts.map +1 -0
- package/dist/client/streamText.js +117 -0
- package/dist/client/streamText.js.map +1 -0
- package/dist/client/streaming.d.ts +3716 -32
- package/dist/client/streaming.d.ts.map +1 -1
- package/dist/client/streaming.js +161 -59
- package/dist/client/streaming.js.map +1 -1
- package/dist/client/threads.d.ts +46 -0
- package/dist/client/threads.d.ts.map +1 -0
- package/dist/client/threads.js +49 -0
- package/dist/client/threads.js.map +1 -0
- package/dist/client/types.d.ts +266 -128
- package/dist/client/types.d.ts.map +1 -1
- package/dist/client/utils.d.ts +4 -0
- package/dist/client/utils.d.ts.map +1 -0
- package/dist/client/utils.js +21 -0
- package/dist/client/utils.js.map +1 -0
- package/dist/component/_generated/api.d.ts +24 -2178
- package/dist/component/_generated/api.d.ts.map +1 -1
- package/dist/component/_generated/api.js +10 -1
- package/dist/component/_generated/api.js.map +1 -1
- package/dist/component/_generated/component.d.ts +3120 -0
- package/dist/component/_generated/component.d.ts.map +1 -0
- package/dist/component/_generated/component.js +11 -0
- package/dist/component/_generated/component.js.map +1 -0
- package/dist/component/_generated/dataModel.d.ts +4 -18
- package/dist/component/_generated/dataModel.d.ts.map +1 -0
- package/dist/component/_generated/dataModel.js +11 -0
- package/dist/component/_generated/dataModel.js.map +1 -0
- package/dist/component/_generated/server.d.ts +10 -38
- package/dist/component/_generated/server.d.ts.map +1 -1
- package/dist/component/_generated/server.js +9 -5
- package/dist/component/_generated/server.js.map +1 -1
- package/dist/component/files.d.ts +16 -10
- package/dist/component/files.d.ts.map +1 -1
- package/dist/component/files.js +10 -2
- package/dist/component/files.js.map +1 -1
- package/dist/component/messages.d.ts +2578 -366
- package/dist/component/messages.d.ts.map +1 -1
- package/dist/component/messages.js +397 -154
- package/dist/component/messages.js.map +1 -1
- package/dist/component/schema.d.ts +5697 -3584
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/schema.js +18 -41
- package/dist/component/schema.js.map +1 -1
- package/dist/component/streams.d.ts +39 -339
- package/dist/component/streams.d.ts.map +1 -1
- package/dist/component/streams.js +114 -73
- package/dist/component/streams.js.map +1 -1
- package/dist/component/threads.d.ts +13 -13
- package/dist/component/users.d.ts +7 -7
- package/dist/component/vector/index.d.ts +1 -1
- package/dist/component/vector/index.d.ts.map +1 -1
- package/dist/component/vector/index.js +1 -3
- package/dist/component/vector/index.js.map +1 -1
- package/dist/deltas.d.ts +43 -0
- package/dist/deltas.d.ts.map +1 -0
- package/dist/deltas.js +446 -0
- package/dist/deltas.js.map +1 -0
- package/dist/mapping.d.ts +38 -20
- package/dist/mapping.d.ts.map +1 -1
- package/dist/mapping.js +365 -97
- package/dist/mapping.js.map +1 -1
- package/dist/react/SmoothText.d.ts +5 -0
- package/dist/react/SmoothText.d.ts.map +1 -0
- package/dist/react/SmoothText.js +6 -0
- package/dist/react/SmoothText.js.map +1 -0
- package/dist/react/index.d.ts +5 -77
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +6 -160
- package/dist/react/index.js.map +1 -1
- package/dist/react/optimisticallySendMessage.d.ts +36 -3
- package/dist/react/optimisticallySendMessage.d.ts.map +1 -1
- package/dist/react/optimisticallySendMessage.js +35 -9
- package/dist/react/optimisticallySendMessage.js.map +1 -1
- package/dist/react/types.d.ts +4 -18
- package/dist/react/types.d.ts.map +1 -1
- package/dist/react/useDeltaStreams.d.ts +10 -0
- package/dist/react/useDeltaStreams.d.ts.map +1 -0
- package/dist/react/useDeltaStreams.js +106 -0
- package/dist/react/useDeltaStreams.js.map +1 -0
- package/dist/react/useSmoothText.d.ts +13 -12
- package/dist/react/useSmoothText.d.ts.map +1 -1
- package/dist/react/useSmoothText.js +32 -15
- package/dist/react/useSmoothText.js.map +1 -1
- package/dist/react/useStreamingUIMessages.d.ts +22 -0
- package/dist/react/useStreamingUIMessages.d.ts.map +1 -0
- package/dist/react/useStreamingUIMessages.js +92 -0
- package/dist/react/useStreamingUIMessages.js.map +1 -0
- package/dist/react/useThreadMessages.d.ts +104 -0
- package/dist/react/useThreadMessages.d.ts.map +1 -0
- package/dist/react/useThreadMessages.js +148 -0
- package/dist/react/useThreadMessages.js.map +1 -0
- package/dist/react/useUIMessages.d.ts +96 -0
- package/dist/react/useUIMessages.d.ts.map +1 -0
- package/dist/react/useUIMessages.js +108 -0
- package/dist/react/useUIMessages.js.map +1 -0
- package/dist/shared.d.ts +20 -4
- package/dist/shared.d.ts.map +1 -1
- package/dist/shared.js +45 -8
- package/dist/shared.js.map +1 -1
- package/dist/validators.d.ts +22981 -5666
- package/dist/validators.d.ts.map +1 -1
- package/dist/validators.js +245 -137
- package/dist/validators.js.map +1 -1
- package/package.json +101 -51
- package/src/UIMessages.combineUIMessages.test.ts +239 -0
- package/src/UIMessages.test.ts +273 -0
- package/src/UIMessages.ts +739 -0
- package/src/client/approval.test.ts +350 -0
- package/src/client/createTool.ts +291 -76
- package/src/client/defaultComponent.ts +17 -0
- package/src/client/definePlaygroundAPI.ts +67 -31
- package/src/client/files.ts +100 -20
- package/src/client/index.test.ts +40 -85
- package/src/client/index.ts +638 -1289
- package/src/client/messages.ts +237 -0
- package/src/client/mockModel.ts +252 -0
- package/src/client/saveInputMessages.test.ts +583 -0
- package/src/client/saveInputMessages.ts +101 -0
- package/src/client/search.test.ts +1207 -0
- package/src/client/search.ts +581 -70
- package/src/client/start.ts +327 -0
- package/src/client/streamText.ts +187 -0
- package/src/client/streaming.test.ts +186 -0
- package/src/client/streaming.ts +241 -97
- package/src/client/threads.ts +83 -0
- package/src/client/types.ts +370 -219
- package/src/client/utils.ts +27 -0
- package/src/component/_generated/api.ts +64 -0
- package/src/component/_generated/component.ts +4902 -0
- package/src/component/_generated/{server.d.ts → server.ts} +33 -21
- package/src/component/files.ts +11 -2
- package/src/component/messages.test.ts +195 -51
- package/src/component/messages.ts +500 -201
- package/src/component/schema.ts +20 -46
- package/src/component/setup.test.ts +7 -0
- package/src/component/streams.ts +184 -83
- package/src/component/users.test.ts +0 -1
- package/src/component/vector/index.ts +1 -3
- package/src/deltas.test.ts +626 -0
- package/src/deltas.ts +569 -0
- package/src/fromUIMessages.test.ts +497 -0
- package/src/mapping.test.ts +180 -6
- package/src/mapping.ts +479 -162
- package/src/react/SmoothText.tsx +9 -0
- package/src/react/index.ts +10 -230
- package/src/react/optimisticallySendMessage.ts +55 -12
- package/src/react/types.ts +6 -39
- package/src/react/useDeltaStreams.ts +160 -0
- package/src/react/useSmoothText.ts +56 -36
- package/src/react/useStreamingUIMessages.ts +143 -0
- package/src/react/useThreadMessages.ts +262 -0
- package/src/react/useUIMessages.test.ts +255 -0
- package/src/react/useUIMessages.ts +195 -0
- package/src/shared.ts +88 -12
- package/src/test.ts +18 -0
- package/src/toUIMessages.test.ts +1269 -0
- package/src/validators.test.ts +18 -19
- package/src/validators.ts +325 -185
- package/dist/client/_generated/_ignore.d.ts +0 -1
- package/dist/client/_generated/_ignore.d.ts.map +0 -1
- package/dist/client/_generated/_ignore.js +0 -3
- package/dist/client/_generated/_ignore.js.map +0 -1
- package/dist/client/listMessages.d.ts +0 -22
- package/dist/client/listMessages.d.ts.map +0 -1
- package/dist/client/listMessages.js +0 -25
- package/dist/client/listMessages.js.map +0 -1
- package/dist/package.json +0 -3
- package/dist/react/deltas.d.ts +0 -26
- package/dist/react/deltas.d.ts.map +0 -1
- package/dist/react/deltas.js +0 -384
- package/dist/react/deltas.js.map +0 -1
- package/dist/react/toUIMessages.d.ts +0 -15
- package/dist/react/toUIMessages.d.ts.map +0 -1
- package/dist/react/toUIMessages.js +0 -211
- package/dist/react/toUIMessages.js.map +0 -1
- package/src/client/listMessages.ts +0 -38
- package/src/component/_generated/api.d.ts +0 -2202
- package/src/component/_generated/api.js +0 -23
- package/src/component/_generated/server.js +0 -90
- package/src/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
- package/src/react/deltas.test.ts +0 -315
- package/src/react/deltas.ts +0 -478
- package/src/react/toUIMessages.test.ts +0 -420
- package/src/react/toUIMessages.ts +0 -253
- package/src/vitest.config.ts +0 -7
- /package/src/component/_generated/{dataModel.d.ts → dataModel.ts} +0 -0
|
@@ -0,0 +1,1269 @@
|
|
|
1
|
+
import { assert } from "convex-helpers";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { toUIMessages } from "./UIMessages.js";
|
|
4
|
+
import type { MessageDoc } from "./validators.js";
|
|
5
|
+
|
|
6
|
+
// Helper to create a base message doc
|
|
7
|
+
function baseMessageDoc(overrides: Partial<MessageDoc> = {}): MessageDoc {
|
|
8
|
+
return {
|
|
9
|
+
_id: "msg1",
|
|
10
|
+
_creationTime: Date.now(),
|
|
11
|
+
order: 0,
|
|
12
|
+
stepOrder: 0,
|
|
13
|
+
status: "success",
|
|
14
|
+
threadId: "thread1",
|
|
15
|
+
tool: false,
|
|
16
|
+
...overrides,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe("toUIMessages", () => {
|
|
21
|
+
it("handles user message", () => {
|
|
22
|
+
const messages = [
|
|
23
|
+
baseMessageDoc({
|
|
24
|
+
message: {
|
|
25
|
+
role: "user",
|
|
26
|
+
content: "Hello!",
|
|
27
|
+
},
|
|
28
|
+
text: "Hello!",
|
|
29
|
+
}),
|
|
30
|
+
];
|
|
31
|
+
const uiMessages = toUIMessages(messages);
|
|
32
|
+
expect(uiMessages).toHaveLength(1);
|
|
33
|
+
expect(uiMessages[0].role).toBe("user");
|
|
34
|
+
expect(uiMessages[0].text).toBe("Hello!");
|
|
35
|
+
expect(uiMessages[0].parts[0]).toEqual({ type: "text", text: "Hello!" });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("handles assistant message", () => {
|
|
39
|
+
const messages = [
|
|
40
|
+
baseMessageDoc({
|
|
41
|
+
message: {
|
|
42
|
+
role: "assistant",
|
|
43
|
+
content: "Hi, how can I help?",
|
|
44
|
+
},
|
|
45
|
+
text: "Hi, how can I help?",
|
|
46
|
+
}),
|
|
47
|
+
];
|
|
48
|
+
const uiMessages = toUIMessages(messages);
|
|
49
|
+
expect(uiMessages).toHaveLength(1);
|
|
50
|
+
expect(uiMessages[0].role).toBe("assistant");
|
|
51
|
+
expect(uiMessages[0].text).toBe("Hi, how can I help?");
|
|
52
|
+
expect(uiMessages[0].parts[0]).toEqual({
|
|
53
|
+
type: "text",
|
|
54
|
+
text: "Hi, how can I help?",
|
|
55
|
+
state: "done",
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("handles multiple messages", () => {
|
|
60
|
+
const messages = [
|
|
61
|
+
baseMessageDoc({
|
|
62
|
+
message: {
|
|
63
|
+
role: "user",
|
|
64
|
+
content: "Hello!",
|
|
65
|
+
},
|
|
66
|
+
text: "Hello!",
|
|
67
|
+
}),
|
|
68
|
+
baseMessageDoc({
|
|
69
|
+
message: {
|
|
70
|
+
role: "assistant",
|
|
71
|
+
content: [
|
|
72
|
+
{
|
|
73
|
+
type: "reasoning",
|
|
74
|
+
text: "I'm thinking...",
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
type: "redacted-reasoning",
|
|
78
|
+
data: "asdfasdfasdf",
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
type: "text",
|
|
82
|
+
text: "I'm thinking...",
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
type: "file",
|
|
86
|
+
mimeType: "text/plain",
|
|
87
|
+
data: "https://example.com/file.txt",
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
type: "tool-call",
|
|
91
|
+
toolName: "myTool",
|
|
92
|
+
toolCallId: "call1",
|
|
93
|
+
input: "an arg",
|
|
94
|
+
args: "an arg",
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
},
|
|
98
|
+
tool: true,
|
|
99
|
+
reasoning: "I'm thinking...",
|
|
100
|
+
text: "I'm thinking...",
|
|
101
|
+
}),
|
|
102
|
+
baseMessageDoc({
|
|
103
|
+
message: {
|
|
104
|
+
role: "tool",
|
|
105
|
+
content: [
|
|
106
|
+
{
|
|
107
|
+
type: "tool-result",
|
|
108
|
+
toolCallId: "call1",
|
|
109
|
+
toolName: "myTool",
|
|
110
|
+
output: {
|
|
111
|
+
type: "text",
|
|
112
|
+
value: "42",
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
},
|
|
117
|
+
tool: true,
|
|
118
|
+
}),
|
|
119
|
+
];
|
|
120
|
+
const uiMessages = toUIMessages(messages);
|
|
121
|
+
expect(uiMessages).toHaveLength(2);
|
|
122
|
+
expect(uiMessages[0].role).toBe("user");
|
|
123
|
+
expect(uiMessages[0].parts.filter((p) => p.type === "text")).toHaveLength(
|
|
124
|
+
1,
|
|
125
|
+
);
|
|
126
|
+
expect(uiMessages[1].role).toBe("assistant");
|
|
127
|
+
expect(
|
|
128
|
+
uiMessages[1].parts.filter((p) => p.type === "tool-myTool"),
|
|
129
|
+
).toHaveLength(1);
|
|
130
|
+
expect(
|
|
131
|
+
uiMessages[1].parts.filter((p) => p.type === "tool-myTool")[0],
|
|
132
|
+
).toMatchObject({
|
|
133
|
+
type: "tool-myTool",
|
|
134
|
+
toolCallId: "call1",
|
|
135
|
+
state: "output-available",
|
|
136
|
+
output: "42",
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("handles multiple text and reasoning parts", () => {
|
|
141
|
+
const messages = [
|
|
142
|
+
baseMessageDoc({
|
|
143
|
+
message: {
|
|
144
|
+
role: "assistant",
|
|
145
|
+
content: [
|
|
146
|
+
{
|
|
147
|
+
type: "reasoning",
|
|
148
|
+
text: "I'm thinking...",
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
type: "text",
|
|
152
|
+
text: "Here's one idea.",
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
type: "reasoning",
|
|
156
|
+
text: "I'm thinking...",
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
type: "text",
|
|
160
|
+
text: "Here's another idea.",
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
},
|
|
164
|
+
reasoning: "I'm thinking... I'm thinking...",
|
|
165
|
+
text: "Here's one idea. Here's another idea.",
|
|
166
|
+
}),
|
|
167
|
+
];
|
|
168
|
+
const uiMessages = toUIMessages(messages);
|
|
169
|
+
expect(uiMessages).toHaveLength(1);
|
|
170
|
+
expect(uiMessages[0].role).toBe("assistant");
|
|
171
|
+
expect(uiMessages[0].text).toBe("Here's one idea. Here's another idea.");
|
|
172
|
+
expect(uiMessages[0].parts.filter((p) => p.type === "reasoning")).toEqual([
|
|
173
|
+
{
|
|
174
|
+
providerOptions: undefined,
|
|
175
|
+
state: "done",
|
|
176
|
+
text: "I'm thinking...",
|
|
177
|
+
type: "reasoning",
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
providerOptions: undefined,
|
|
181
|
+
state: "done",
|
|
182
|
+
text: "I'm thinking...",
|
|
183
|
+
type: "reasoning",
|
|
184
|
+
},
|
|
185
|
+
]);
|
|
186
|
+
expect(uiMessages[0].parts[0].type).toBe("reasoning");
|
|
187
|
+
assert(uiMessages[0].parts[0].type === "reasoning");
|
|
188
|
+
expect(uiMessages[0].parts[0].text).toBe("I'm thinking...");
|
|
189
|
+
expect(uiMessages[0].parts[1].type).toBe("text");
|
|
190
|
+
assert(uiMessages[0].parts[1].type === "text");
|
|
191
|
+
expect(uiMessages[0].parts[1].text).toBe("Here's one idea.");
|
|
192
|
+
expect(uiMessages[0].parts[2].type).toBe("reasoning");
|
|
193
|
+
assert(uiMessages[0].parts[2].type === "reasoning");
|
|
194
|
+
expect(uiMessages[0].parts[2].text).toBe("I'm thinking...");
|
|
195
|
+
|
|
196
|
+
expect(uiMessages[0].parts.filter((p) => p.type === "text")).toHaveLength(
|
|
197
|
+
2,
|
|
198
|
+
);
|
|
199
|
+
expect(uiMessages[0].parts.filter((p) => p.type === "text")[0].text).toBe(
|
|
200
|
+
"Here's one idea.",
|
|
201
|
+
);
|
|
202
|
+
expect(uiMessages[0].parts.filter((p) => p.type === "text")[1].text).toBe(
|
|
203
|
+
"Here's another idea.",
|
|
204
|
+
);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("combines text from between messages", () => {
|
|
208
|
+
const messages = [
|
|
209
|
+
baseMessageDoc({
|
|
210
|
+
message: {
|
|
211
|
+
role: "assistant",
|
|
212
|
+
content: [
|
|
213
|
+
{
|
|
214
|
+
type: "reasoning",
|
|
215
|
+
text: "I'm thinking...",
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
type: "text",
|
|
219
|
+
text: "I'm going to ask a question.",
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
type: "tool-call",
|
|
223
|
+
input: "What's the meaning of life?",
|
|
224
|
+
args: "What's the meaning of life?",
|
|
225
|
+
toolCallId: "call1",
|
|
226
|
+
toolName: "myTool",
|
|
227
|
+
},
|
|
228
|
+
],
|
|
229
|
+
},
|
|
230
|
+
reasoning: "I'm thinking...",
|
|
231
|
+
text: "Here's one idea.",
|
|
232
|
+
tool: true,
|
|
233
|
+
order: 1,
|
|
234
|
+
stepOrder: 1,
|
|
235
|
+
}),
|
|
236
|
+
baseMessageDoc({
|
|
237
|
+
message: {
|
|
238
|
+
role: "tool",
|
|
239
|
+
content: [
|
|
240
|
+
{
|
|
241
|
+
type: "tool-result",
|
|
242
|
+
toolCallId: "call1",
|
|
243
|
+
toolName: "myTool",
|
|
244
|
+
output: {
|
|
245
|
+
type: "text",
|
|
246
|
+
value: "42",
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
],
|
|
250
|
+
},
|
|
251
|
+
text: "",
|
|
252
|
+
tool: true,
|
|
253
|
+
order: 1,
|
|
254
|
+
stepOrder: 2,
|
|
255
|
+
}),
|
|
256
|
+
baseMessageDoc({
|
|
257
|
+
message: {
|
|
258
|
+
role: "assistant",
|
|
259
|
+
content: [
|
|
260
|
+
{
|
|
261
|
+
type: "reasoning",
|
|
262
|
+
text: "Thinking again...",
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
type: "text",
|
|
266
|
+
text: "Ok now I know.",
|
|
267
|
+
},
|
|
268
|
+
],
|
|
269
|
+
},
|
|
270
|
+
text: "One last thing.",
|
|
271
|
+
order: 1,
|
|
272
|
+
stepOrder: 3,
|
|
273
|
+
}),
|
|
274
|
+
];
|
|
275
|
+
const uiMessages = toUIMessages(messages);
|
|
276
|
+
expect(uiMessages).toHaveLength(1);
|
|
277
|
+
expect(uiMessages[0].role).toBe("assistant");
|
|
278
|
+
expect(uiMessages[0].text).toBe(
|
|
279
|
+
"I'm going to ask a question. Ok now I know.",
|
|
280
|
+
);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it("handles system message", () => {
|
|
284
|
+
const messages = [
|
|
285
|
+
baseMessageDoc({
|
|
286
|
+
message: {
|
|
287
|
+
role: "system",
|
|
288
|
+
content: "System message here",
|
|
289
|
+
},
|
|
290
|
+
text: "System message here",
|
|
291
|
+
}),
|
|
292
|
+
];
|
|
293
|
+
const uiMessages = toUIMessages(messages);
|
|
294
|
+
expect(uiMessages).toHaveLength(1);
|
|
295
|
+
expect(uiMessages[0].role).toBe("system");
|
|
296
|
+
expect(uiMessages[0].text).toBe("System message here");
|
|
297
|
+
expect(uiMessages[0].parts[0]).toEqual({
|
|
298
|
+
type: "text",
|
|
299
|
+
text: "System message here",
|
|
300
|
+
state: "done",
|
|
301
|
+
providerMetadata: undefined,
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it("handles wrapped JSON tool output", () => {
|
|
306
|
+
const messages = [
|
|
307
|
+
baseMessageDoc({
|
|
308
|
+
message: {
|
|
309
|
+
role: "assistant",
|
|
310
|
+
content: [
|
|
311
|
+
{
|
|
312
|
+
type: "tool-call",
|
|
313
|
+
toolName: "myTool",
|
|
314
|
+
toolCallId: "call1",
|
|
315
|
+
input: { query: "test" },
|
|
316
|
+
args: { query: "test" },
|
|
317
|
+
},
|
|
318
|
+
],
|
|
319
|
+
},
|
|
320
|
+
tool: true,
|
|
321
|
+
}),
|
|
322
|
+
baseMessageDoc({
|
|
323
|
+
message: {
|
|
324
|
+
role: "tool",
|
|
325
|
+
content: [
|
|
326
|
+
{
|
|
327
|
+
type: "tool-result",
|
|
328
|
+
toolName: "myTool",
|
|
329
|
+
toolCallId: "call1",
|
|
330
|
+
output: {
|
|
331
|
+
type: "json",
|
|
332
|
+
value: { data: "wrapped result", success: true },
|
|
333
|
+
},
|
|
334
|
+
},
|
|
335
|
+
],
|
|
336
|
+
},
|
|
337
|
+
tool: true,
|
|
338
|
+
}),
|
|
339
|
+
];
|
|
340
|
+
const uiMessages = toUIMessages(messages);
|
|
341
|
+
expect(uiMessages).toHaveLength(1);
|
|
342
|
+
const toolPart = uiMessages[0].parts.find((p) => p.type === "tool-myTool");
|
|
343
|
+
expect(toolPart).toMatchObject({
|
|
344
|
+
type: "tool-myTool",
|
|
345
|
+
toolCallId: "call1",
|
|
346
|
+
state: "output-available",
|
|
347
|
+
input: { query: "test" },
|
|
348
|
+
output: { data: "wrapped result", success: true }, // Should be unwrapped
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it("handles tool call", () => {
|
|
353
|
+
const messages = [
|
|
354
|
+
baseMessageDoc({
|
|
355
|
+
message: {
|
|
356
|
+
role: "assistant",
|
|
357
|
+
content: [
|
|
358
|
+
{
|
|
359
|
+
type: "tool-call",
|
|
360
|
+
toolName: "myTool",
|
|
361
|
+
toolCallId: "call1",
|
|
362
|
+
input: "hi",
|
|
363
|
+
args: "hi",
|
|
364
|
+
},
|
|
365
|
+
],
|
|
366
|
+
},
|
|
367
|
+
text: "",
|
|
368
|
+
}),
|
|
369
|
+
];
|
|
370
|
+
const uiMessages = toUIMessages(messages);
|
|
371
|
+
expect(uiMessages).toHaveLength(1);
|
|
372
|
+
expect(uiMessages[0].role).toBe("assistant");
|
|
373
|
+
expect(
|
|
374
|
+
uiMessages[0].parts.filter((p) => p.type === "tool-myTool")[0],
|
|
375
|
+
).toMatchObject({
|
|
376
|
+
type: "tool-myTool",
|
|
377
|
+
toolCallId: "call1",
|
|
378
|
+
input: "hi",
|
|
379
|
+
state: "input-available",
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it("handles tool result", () => {
|
|
384
|
+
const messages = [
|
|
385
|
+
baseMessageDoc({
|
|
386
|
+
tool: true,
|
|
387
|
+
message: {
|
|
388
|
+
role: "assistant",
|
|
389
|
+
content: [
|
|
390
|
+
{
|
|
391
|
+
type: "tool-call",
|
|
392
|
+
toolName: "myTool",
|
|
393
|
+
toolCallId: "call1",
|
|
394
|
+
input: "",
|
|
395
|
+
args: "",
|
|
396
|
+
},
|
|
397
|
+
],
|
|
398
|
+
},
|
|
399
|
+
text: "",
|
|
400
|
+
}),
|
|
401
|
+
baseMessageDoc({
|
|
402
|
+
message: {
|
|
403
|
+
role: "tool",
|
|
404
|
+
content: [
|
|
405
|
+
{
|
|
406
|
+
type: "tool-result",
|
|
407
|
+
toolCallId: "call1",
|
|
408
|
+
toolName: "myTool",
|
|
409
|
+
result: "42",
|
|
410
|
+
},
|
|
411
|
+
],
|
|
412
|
+
},
|
|
413
|
+
text: "",
|
|
414
|
+
}),
|
|
415
|
+
];
|
|
416
|
+
const uiMessages = toUIMessages(messages);
|
|
417
|
+
expect(uiMessages).toHaveLength(1);
|
|
418
|
+
expect(uiMessages[0].role).toBe("assistant");
|
|
419
|
+
// Should have a tool-invocation part
|
|
420
|
+
expect(uiMessages[0].parts.some((p) => p.type === "tool-myTool")).toBe(
|
|
421
|
+
true,
|
|
422
|
+
);
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
it("does not duplicate text content", () => {
|
|
426
|
+
const messages = [
|
|
427
|
+
baseMessageDoc({
|
|
428
|
+
message: {
|
|
429
|
+
role: "assistant",
|
|
430
|
+
content: "Hello!",
|
|
431
|
+
},
|
|
432
|
+
text: "Hello!",
|
|
433
|
+
}),
|
|
434
|
+
];
|
|
435
|
+
const uiMessages = toUIMessages(messages);
|
|
436
|
+
// There should only be one text part
|
|
437
|
+
const textParts = uiMessages[0].parts.filter((p) => p.type === "text");
|
|
438
|
+
expect(textParts).toHaveLength(1);
|
|
439
|
+
expect(textParts[0].text).toBe("Hello!");
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
it("sets text field correctly when message has many parts with text part as final part", () => {
|
|
443
|
+
const messages = [
|
|
444
|
+
baseMessageDoc({
|
|
445
|
+
message: {
|
|
446
|
+
role: "assistant",
|
|
447
|
+
content: [
|
|
448
|
+
{
|
|
449
|
+
type: "reasoning",
|
|
450
|
+
text: "Let me think about this...",
|
|
451
|
+
},
|
|
452
|
+
{
|
|
453
|
+
type: "tool-call",
|
|
454
|
+
toolName: "calculator",
|
|
455
|
+
toolCallId: "call1",
|
|
456
|
+
input: { operation: "add", a: 2, b: 3 },
|
|
457
|
+
args: { operation: "add", a: 2, b: 3 },
|
|
458
|
+
},
|
|
459
|
+
{
|
|
460
|
+
type: "file",
|
|
461
|
+
mimeType: "application/json",
|
|
462
|
+
data: "some-file-data",
|
|
463
|
+
},
|
|
464
|
+
{
|
|
465
|
+
type: "text",
|
|
466
|
+
text: "Here's my final answer.",
|
|
467
|
+
},
|
|
468
|
+
],
|
|
469
|
+
},
|
|
470
|
+
text: "Here's my final answer.",
|
|
471
|
+
reasoning: "Let me think about this...",
|
|
472
|
+
}),
|
|
473
|
+
];
|
|
474
|
+
|
|
475
|
+
const uiMessages = toUIMessages(messages);
|
|
476
|
+
|
|
477
|
+
expect(uiMessages).toHaveLength(1);
|
|
478
|
+
expect(uiMessages[0].role).toBe("assistant");
|
|
479
|
+
expect(uiMessages[0].text).toBe("Here's my final answer.");
|
|
480
|
+
|
|
481
|
+
// Verify that the text part exists in parts
|
|
482
|
+
const textParts = uiMessages[0].parts.filter((p) => p.type === "text");
|
|
483
|
+
expect(textParts).toHaveLength(1);
|
|
484
|
+
expect(textParts[0].text).toBe("Here's my final answer.");
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
// Add more tests for array content, tool calls, etc. as needed
|
|
488
|
+
|
|
489
|
+
it("should update tool call state from input-available to output-available", () => {
|
|
490
|
+
const messages = [
|
|
491
|
+
baseMessageDoc({
|
|
492
|
+
message: {
|
|
493
|
+
role: "assistant",
|
|
494
|
+
content: [
|
|
495
|
+
{
|
|
496
|
+
type: "tool-call",
|
|
497
|
+
toolName: "calculator",
|
|
498
|
+
toolCallId: "call1",
|
|
499
|
+
input: { operation: "add", a: 1, b: 2 },
|
|
500
|
+
args: { operation: "add", a: 1, b: 2 },
|
|
501
|
+
},
|
|
502
|
+
],
|
|
503
|
+
},
|
|
504
|
+
tool: true,
|
|
505
|
+
}),
|
|
506
|
+
baseMessageDoc({
|
|
507
|
+
message: {
|
|
508
|
+
role: "tool",
|
|
509
|
+
content: [
|
|
510
|
+
{
|
|
511
|
+
type: "tool-result",
|
|
512
|
+
toolCallId: "call1",
|
|
513
|
+
toolName: "calculator",
|
|
514
|
+
result: { sum: 3 },
|
|
515
|
+
},
|
|
516
|
+
],
|
|
517
|
+
},
|
|
518
|
+
tool: true,
|
|
519
|
+
}),
|
|
520
|
+
];
|
|
521
|
+
|
|
522
|
+
const uiMessages = toUIMessages(messages);
|
|
523
|
+
|
|
524
|
+
// Should have one assistant message
|
|
525
|
+
expect(uiMessages).toHaveLength(1);
|
|
526
|
+
expect(uiMessages[0].role).toBe("assistant");
|
|
527
|
+
|
|
528
|
+
// Should have a single tool-calculator part (not separate tool-call and tool-result parts)
|
|
529
|
+
const toolParts = uiMessages[0].parts.filter(
|
|
530
|
+
(p) => p.type === "tool-calculator",
|
|
531
|
+
);
|
|
532
|
+
expect(toolParts).toHaveLength(1);
|
|
533
|
+
|
|
534
|
+
const toolPart = toolParts[0];
|
|
535
|
+
expect(toolPart).toMatchObject({
|
|
536
|
+
type: "tool-calculator",
|
|
537
|
+
toolCallId: "call1",
|
|
538
|
+
state: "output-available",
|
|
539
|
+
input: { operation: "add", a: 1, b: 2 },
|
|
540
|
+
output: { sum: 3 },
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
// Should NOT have a tool-call part (which is what currently happens)
|
|
544
|
+
const toolCallParts = uiMessages[0].parts.filter(
|
|
545
|
+
(p) => p.type === "tool-call",
|
|
546
|
+
);
|
|
547
|
+
expect(toolCallParts).toHaveLength(0);
|
|
548
|
+
});
|
|
549
|
+
it("sets text field correctly in multi-step tool-call + final-text flow", () => {
|
|
550
|
+
const messages = [
|
|
551
|
+
baseMessageDoc({
|
|
552
|
+
text: "what time is it in paris?",
|
|
553
|
+
message: {
|
|
554
|
+
role: "user",
|
|
555
|
+
content: "what time is it in paris?",
|
|
556
|
+
},
|
|
557
|
+
}),
|
|
558
|
+
baseMessageDoc({
|
|
559
|
+
tool: true,
|
|
560
|
+
finishReason: "tool-calls",
|
|
561
|
+
text: "",
|
|
562
|
+
stepOrder: 1,
|
|
563
|
+
message: {
|
|
564
|
+
role: "assistant",
|
|
565
|
+
content: [
|
|
566
|
+
{
|
|
567
|
+
type: "reasoning",
|
|
568
|
+
text: "**Finding the Time**\n\nI've pinpointed the core task: obtaining the current time in Paris. It involves using the `dateTime` tool. I've identified \"Europe/Paris\" as the necessary timezone identifier to provide to the tool. My next step is to test the tool.\n\n\n",
|
|
569
|
+
},
|
|
570
|
+
{
|
|
571
|
+
input: {
|
|
572
|
+
timezone: "Europe/Paris",
|
|
573
|
+
},
|
|
574
|
+
args: {
|
|
575
|
+
timezone: "Europe/Paris",
|
|
576
|
+
},
|
|
577
|
+
type: "tool-call",
|
|
578
|
+
toolName: "dateTime",
|
|
579
|
+
toolCallId: "tool_0_dateTime",
|
|
580
|
+
},
|
|
581
|
+
],
|
|
582
|
+
},
|
|
583
|
+
reasoning:
|
|
584
|
+
"**Finding the Time**\n\nI've pinpointed the core task: obtaining the current time in Paris. It involves using the `dateTime` tool. I've identified \"Europe/Paris\" as the necessary timezone identifier to provide to the tool. My next step is to test the tool.\n\n\n",
|
|
585
|
+
reasoningDetails: [
|
|
586
|
+
{
|
|
587
|
+
text: "**Finding the Time**\n\nI've pinpointed the core task: obtaining the current time in Paris. It involves using the `dateTime` tool. I've identified \"Europe/Paris\" as the necessary timezone identifier to provide to the tool. My next step is to test the tool.\n\n\n",
|
|
588
|
+
type: "reasoning",
|
|
589
|
+
},
|
|
590
|
+
],
|
|
591
|
+
warnings: [],
|
|
592
|
+
}),
|
|
593
|
+
baseMessageDoc({
|
|
594
|
+
tool: true,
|
|
595
|
+
stepOrder: 2,
|
|
596
|
+
message: {
|
|
597
|
+
role: "tool",
|
|
598
|
+
content: [
|
|
599
|
+
{
|
|
600
|
+
type: "tool-result",
|
|
601
|
+
toolCallId: "tool_0_dateTime",
|
|
602
|
+
toolName: "dateTime",
|
|
603
|
+
result: {
|
|
604
|
+
type: "json",
|
|
605
|
+
value: {
|
|
606
|
+
day: "20",
|
|
607
|
+
hours: 16,
|
|
608
|
+
minutes: 3,
|
|
609
|
+
month: "August",
|
|
610
|
+
year: "2025",
|
|
611
|
+
},
|
|
612
|
+
},
|
|
613
|
+
},
|
|
614
|
+
],
|
|
615
|
+
},
|
|
616
|
+
sources: [],
|
|
617
|
+
}),
|
|
618
|
+
baseMessageDoc({
|
|
619
|
+
finishReason: "stop",
|
|
620
|
+
text: "The time in Paris, France is 4:03 PM on August 20, 2025.",
|
|
621
|
+
stepOrder: 3,
|
|
622
|
+
message: {
|
|
623
|
+
role: "assistant",
|
|
624
|
+
content: [
|
|
625
|
+
{
|
|
626
|
+
type: "text",
|
|
627
|
+
text: "The time in Paris, France is 4:03 PM on August 20, 2025.",
|
|
628
|
+
},
|
|
629
|
+
],
|
|
630
|
+
},
|
|
631
|
+
reasoningDetails: [],
|
|
632
|
+
sources: [],
|
|
633
|
+
warnings: [],
|
|
634
|
+
}),
|
|
635
|
+
];
|
|
636
|
+
|
|
637
|
+
const uiMessages = toUIMessages(messages);
|
|
638
|
+
|
|
639
|
+
expect(uiMessages).toHaveLength(2);
|
|
640
|
+
expect(uiMessages[0].role).toBe("user");
|
|
641
|
+
expect(uiMessages[0].text).toBe("what time is it in paris?");
|
|
642
|
+
expect(uiMessages[1].role).toBe("assistant");
|
|
643
|
+
expect(uiMessages[1].text).toBe(
|
|
644
|
+
"The time in Paris, France is 4:03 PM on August 20, 2025.",
|
|
645
|
+
);
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
it("handles messages in reverse order (assistant response, then tool response, then tool call)", () => {
|
|
649
|
+
const messages = [
|
|
650
|
+
// Final assistant response comes first (stepOrder 3) - no tool flag since it's the final message
|
|
651
|
+
baseMessageDoc({
|
|
652
|
+
_id: "msg3",
|
|
653
|
+
order: 1,
|
|
654
|
+
stepOrder: 3,
|
|
655
|
+
finishReason: "stop",
|
|
656
|
+
text: "The result is 42.",
|
|
657
|
+
tool: false, // This is the final message without tool calls
|
|
658
|
+
message: {
|
|
659
|
+
role: "assistant",
|
|
660
|
+
content: [
|
|
661
|
+
{
|
|
662
|
+
type: "text",
|
|
663
|
+
text: "The result is 42.",
|
|
664
|
+
},
|
|
665
|
+
],
|
|
666
|
+
},
|
|
667
|
+
}),
|
|
668
|
+
// Tool result comes second (stepOrder 2)
|
|
669
|
+
baseMessageDoc({
|
|
670
|
+
_id: "msg2",
|
|
671
|
+
order: 1,
|
|
672
|
+
stepOrder: 2,
|
|
673
|
+
tool: true,
|
|
674
|
+
message: {
|
|
675
|
+
role: "tool",
|
|
676
|
+
content: [
|
|
677
|
+
{
|
|
678
|
+
type: "tool-result",
|
|
679
|
+
toolCallId: "call1",
|
|
680
|
+
toolName: "calculator",
|
|
681
|
+
output: {
|
|
682
|
+
type: "text",
|
|
683
|
+
value: "42",
|
|
684
|
+
},
|
|
685
|
+
},
|
|
686
|
+
],
|
|
687
|
+
},
|
|
688
|
+
}),
|
|
689
|
+
// Tool call comes last (stepOrder 1)
|
|
690
|
+
baseMessageDoc({
|
|
691
|
+
_id: "msg1",
|
|
692
|
+
order: 1,
|
|
693
|
+
stepOrder: 1,
|
|
694
|
+
tool: true,
|
|
695
|
+
text: "",
|
|
696
|
+
message: {
|
|
697
|
+
role: "assistant",
|
|
698
|
+
content: [
|
|
699
|
+
{
|
|
700
|
+
type: "tool-call",
|
|
701
|
+
toolName: "calculator",
|
|
702
|
+
toolCallId: "call1",
|
|
703
|
+
input: { operation: "add", a: 40, b: 2 },
|
|
704
|
+
args: { operation: "add", a: 40, b: 2 },
|
|
705
|
+
},
|
|
706
|
+
],
|
|
707
|
+
},
|
|
708
|
+
}),
|
|
709
|
+
];
|
|
710
|
+
|
|
711
|
+
const uiMessages = toUIMessages(messages);
|
|
712
|
+
|
|
713
|
+
expect(uiMessages).toHaveLength(1);
|
|
714
|
+
expect(uiMessages[0].role).toBe("assistant");
|
|
715
|
+
|
|
716
|
+
// Should concatenate text from all messages (only the final response has text)
|
|
717
|
+
expect(uiMessages[0].text).toBe("The result is 42.");
|
|
718
|
+
|
|
719
|
+
// Should use first message's fields (msg1 with stepOrder 1)
|
|
720
|
+
expect(uiMessages[0].id).toBe("msg1");
|
|
721
|
+
expect(uiMessages[0].order).toBe(1);
|
|
722
|
+
expect(uiMessages[0].stepOrder).toBe(1);
|
|
723
|
+
|
|
724
|
+
// Should have both tool call and result parts
|
|
725
|
+
const toolParts = uiMessages[0].parts.filter(
|
|
726
|
+
(p) => p.type === "tool-calculator",
|
|
727
|
+
);
|
|
728
|
+
expect(toolParts).toHaveLength(1);
|
|
729
|
+
expect(toolParts[0]).toMatchObject({
|
|
730
|
+
type: "tool-calculator",
|
|
731
|
+
toolCallId: "call1",
|
|
732
|
+
state: "output-available",
|
|
733
|
+
input: { operation: "add", a: 40, b: 2 },
|
|
734
|
+
output: "42",
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
// Should also have text part
|
|
738
|
+
const textParts = uiMessages[0].parts.filter((p) => p.type === "text");
|
|
739
|
+
expect(textParts).toHaveLength(1);
|
|
740
|
+
expect(textParts[0].text).toBe("The result is 42.");
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
it("shows output-error state when tool result has isError: true (issue #162)", () => {
|
|
744
|
+
const messages = [
|
|
745
|
+
// Tool call
|
|
746
|
+
baseMessageDoc({
|
|
747
|
+
_id: "msg1",
|
|
748
|
+
order: 1,
|
|
749
|
+
stepOrder: 1,
|
|
750
|
+
tool: true,
|
|
751
|
+
message: {
|
|
752
|
+
role: "assistant",
|
|
753
|
+
content: [
|
|
754
|
+
{
|
|
755
|
+
type: "tool-call",
|
|
756
|
+
toolName: "generateImage",
|
|
757
|
+
toolCallId: "call1",
|
|
758
|
+
input: { id: "invalid-id" },
|
|
759
|
+
args: { id: "invalid-id" },
|
|
760
|
+
},
|
|
761
|
+
],
|
|
762
|
+
},
|
|
763
|
+
}),
|
|
764
|
+
// Tool result with error
|
|
765
|
+
baseMessageDoc({
|
|
766
|
+
_id: "msg2",
|
|
767
|
+
order: 1,
|
|
768
|
+
stepOrder: 2,
|
|
769
|
+
tool: true,
|
|
770
|
+
message: {
|
|
771
|
+
role: "tool",
|
|
772
|
+
content: [
|
|
773
|
+
{
|
|
774
|
+
type: "tool-result",
|
|
775
|
+
toolCallId: "call1",
|
|
776
|
+
toolName: "generateImage",
|
|
777
|
+
output: {
|
|
778
|
+
type: "text",
|
|
779
|
+
value:
|
|
780
|
+
'ArgumentValidationError: Value does not match validator.\nPath: .id\nValue: "invalid-id"\nValidator: v.id("images")',
|
|
781
|
+
},
|
|
782
|
+
isError: true,
|
|
783
|
+
},
|
|
784
|
+
],
|
|
785
|
+
},
|
|
786
|
+
}),
|
|
787
|
+
];
|
|
788
|
+
|
|
789
|
+
const uiMessages = toUIMessages(messages);
|
|
790
|
+
|
|
791
|
+
expect(uiMessages).toHaveLength(1);
|
|
792
|
+
expect(uiMessages[0].role).toBe("assistant");
|
|
793
|
+
|
|
794
|
+
const toolParts = uiMessages[0].parts.filter(
|
|
795
|
+
(p) => p.type === "tool-generateImage",
|
|
796
|
+
);
|
|
797
|
+
expect(toolParts).toHaveLength(1);
|
|
798
|
+
|
|
799
|
+
const toolPart = toolParts[0] as any;
|
|
800
|
+
expect(toolPart.toolCallId).toBe("call1");
|
|
801
|
+
// Should show output-error, not output-available
|
|
802
|
+
expect(toolPart.state).toBe("output-error");
|
|
803
|
+
expect(toolPart.output).toContain("ArgumentValidationError");
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
it("shows output-error when tool result has isError: true without tool call present (issue #162)", () => {
|
|
807
|
+
// This simulates the case where the tool-call message wasn't saved
|
|
808
|
+
const messages = [
|
|
809
|
+
baseMessageDoc({
|
|
810
|
+
_id: "msg1",
|
|
811
|
+
order: 1,
|
|
812
|
+
stepOrder: 1,
|
|
813
|
+
tool: true,
|
|
814
|
+
message: {
|
|
815
|
+
role: "tool",
|
|
816
|
+
content: [
|
|
817
|
+
{
|
|
818
|
+
type: "tool-result",
|
|
819
|
+
toolCallId: "call1",
|
|
820
|
+
toolName: "generateImage",
|
|
821
|
+
output: {
|
|
822
|
+
type: "text",
|
|
823
|
+
value: "Error: Something went wrong",
|
|
824
|
+
},
|
|
825
|
+
isError: true,
|
|
826
|
+
},
|
|
827
|
+
],
|
|
828
|
+
},
|
|
829
|
+
}),
|
|
830
|
+
];
|
|
831
|
+
|
|
832
|
+
const uiMessages = toUIMessages(messages);
|
|
833
|
+
|
|
834
|
+
expect(uiMessages).toHaveLength(1);
|
|
835
|
+
expect(uiMessages[0].role).toBe("assistant");
|
|
836
|
+
|
|
837
|
+
const toolParts = uiMessages[0].parts.filter(
|
|
838
|
+
(p) => p.type === "tool-generateImage",
|
|
839
|
+
);
|
|
840
|
+
expect(toolParts).toHaveLength(1);
|
|
841
|
+
|
|
842
|
+
const toolPart = toolParts[0] as any;
|
|
843
|
+
expect(toolPart.state).toBe("output-error");
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
describe("userId preservation", () => {
|
|
847
|
+
it("preserves userId in user messages", () => {
|
|
848
|
+
const messages = [
|
|
849
|
+
baseMessageDoc({
|
|
850
|
+
userId: "user123",
|
|
851
|
+
message: {
|
|
852
|
+
role: "user",
|
|
853
|
+
content: "Hello!",
|
|
854
|
+
},
|
|
855
|
+
text: "Hello!",
|
|
856
|
+
}),
|
|
857
|
+
];
|
|
858
|
+
const uiMessages = toUIMessages(messages);
|
|
859
|
+
expect(uiMessages).toHaveLength(1);
|
|
860
|
+
expect(uiMessages[0].role).toBe("user");
|
|
861
|
+
expect(uiMessages[0].userId).toBe("user123");
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
it("preserves userId in system messages", () => {
|
|
865
|
+
const messages = [
|
|
866
|
+
baseMessageDoc({
|
|
867
|
+
userId: "user456",
|
|
868
|
+
message: {
|
|
869
|
+
role: "system",
|
|
870
|
+
content: "System prompt",
|
|
871
|
+
},
|
|
872
|
+
text: "System prompt",
|
|
873
|
+
}),
|
|
874
|
+
];
|
|
875
|
+
const uiMessages = toUIMessages(messages);
|
|
876
|
+
expect(uiMessages).toHaveLength(1);
|
|
877
|
+
expect(uiMessages[0].role).toBe("system");
|
|
878
|
+
expect(uiMessages[0].userId).toBe("user456");
|
|
879
|
+
});
|
|
880
|
+
|
|
881
|
+
it("preserves userId in assistant messages", () => {
|
|
882
|
+
const messages = [
|
|
883
|
+
baseMessageDoc({
|
|
884
|
+
userId: "user789",
|
|
885
|
+
message: {
|
|
886
|
+
role: "assistant",
|
|
887
|
+
content: "Hi there!",
|
|
888
|
+
},
|
|
889
|
+
text: "Hi there!",
|
|
890
|
+
}),
|
|
891
|
+
];
|
|
892
|
+
const uiMessages = toUIMessages(messages);
|
|
893
|
+
expect(uiMessages).toHaveLength(1);
|
|
894
|
+
expect(uiMessages[0].role).toBe("assistant");
|
|
895
|
+
expect(uiMessages[0].userId).toBe("user789");
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
it("preserves userId from first message in grouped assistant messages", () => {
|
|
899
|
+
const messages = [
|
|
900
|
+
baseMessageDoc({
|
|
901
|
+
_id: "msg1",
|
|
902
|
+
userId: "userA",
|
|
903
|
+
order: 1,
|
|
904
|
+
stepOrder: 1,
|
|
905
|
+
tool: true,
|
|
906
|
+
message: {
|
|
907
|
+
role: "assistant",
|
|
908
|
+
content: [
|
|
909
|
+
{
|
|
910
|
+
type: "tool-call",
|
|
911
|
+
toolName: "myTool",
|
|
912
|
+
toolCallId: "call1",
|
|
913
|
+
input: {},
|
|
914
|
+
args: {},
|
|
915
|
+
},
|
|
916
|
+
],
|
|
917
|
+
},
|
|
918
|
+
text: "",
|
|
919
|
+
}),
|
|
920
|
+
baseMessageDoc({
|
|
921
|
+
_id: "msg2",
|
|
922
|
+
userId: "userA",
|
|
923
|
+
order: 1,
|
|
924
|
+
stepOrder: 2,
|
|
925
|
+
tool: true,
|
|
926
|
+
message: {
|
|
927
|
+
role: "tool",
|
|
928
|
+
content: [
|
|
929
|
+
{
|
|
930
|
+
type: "tool-result",
|
|
931
|
+
toolCallId: "call1",
|
|
932
|
+
toolName: "myTool",
|
|
933
|
+
output: { type: "text", value: "result" },
|
|
934
|
+
},
|
|
935
|
+
],
|
|
936
|
+
},
|
|
937
|
+
text: "",
|
|
938
|
+
}),
|
|
939
|
+
baseMessageDoc({
|
|
940
|
+
_id: "msg3",
|
|
941
|
+
userId: "userA",
|
|
942
|
+
order: 1,
|
|
943
|
+
stepOrder: 3,
|
|
944
|
+
message: {
|
|
945
|
+
role: "assistant",
|
|
946
|
+
content: "Done!",
|
|
947
|
+
},
|
|
948
|
+
text: "Done!",
|
|
949
|
+
}),
|
|
950
|
+
];
|
|
951
|
+
const uiMessages = toUIMessages(messages);
|
|
952
|
+
expect(uiMessages).toHaveLength(1);
|
|
953
|
+
expect(uiMessages[0].role).toBe("assistant");
|
|
954
|
+
expect(uiMessages[0].userId).toBe("userA");
|
|
955
|
+
});
|
|
956
|
+
|
|
957
|
+
it("handles undefined userId gracefully", () => {
|
|
958
|
+
const messages = [
|
|
959
|
+
baseMessageDoc({
|
|
960
|
+
// No userId provided
|
|
961
|
+
message: {
|
|
962
|
+
role: "user",
|
|
963
|
+
content: "Hello!",
|
|
964
|
+
},
|
|
965
|
+
text: "Hello!",
|
|
966
|
+
}),
|
|
967
|
+
];
|
|
968
|
+
const uiMessages = toUIMessages(messages);
|
|
969
|
+
expect(uiMessages).toHaveLength(1);
|
|
970
|
+
expect(uiMessages[0].userId).toBeUndefined();
|
|
971
|
+
});
|
|
972
|
+
});
|
|
973
|
+
|
|
974
|
+
describe("tool approval workflow", () => {
|
|
975
|
+
it("sets state to approval-requested when tool-approval-request is present", () => {
|
|
976
|
+
const messages = [
|
|
977
|
+
baseMessageDoc({
|
|
978
|
+
_id: "msg1",
|
|
979
|
+
order: 1,
|
|
980
|
+
stepOrder: 1,
|
|
981
|
+
tool: true,
|
|
982
|
+
message: {
|
|
983
|
+
role: "assistant",
|
|
984
|
+
content: [
|
|
985
|
+
{
|
|
986
|
+
type: "tool-call",
|
|
987
|
+
toolName: "dangerousTool",
|
|
988
|
+
toolCallId: "call1",
|
|
989
|
+
input: { action: "delete" },
|
|
990
|
+
args: { action: "delete" },
|
|
991
|
+
},
|
|
992
|
+
{
|
|
993
|
+
type: "tool-approval-request",
|
|
994
|
+
approvalId: "approval1",
|
|
995
|
+
toolCallId: "call1",
|
|
996
|
+
},
|
|
997
|
+
],
|
|
998
|
+
},
|
|
999
|
+
}),
|
|
1000
|
+
];
|
|
1001
|
+
|
|
1002
|
+
const uiMessages = toUIMessages(messages);
|
|
1003
|
+
|
|
1004
|
+
expect(uiMessages).toHaveLength(1);
|
|
1005
|
+
const toolPart = uiMessages[0].parts.find(
|
|
1006
|
+
(p) => p.type === "tool-dangerousTool",
|
|
1007
|
+
) as any;
|
|
1008
|
+
expect(toolPart).toBeDefined();
|
|
1009
|
+
expect(toolPart.state).toBe("approval-requested");
|
|
1010
|
+
expect(toolPart.approval).toEqual({ id: "approval1" });
|
|
1011
|
+
});
|
|
1012
|
+
|
|
1013
|
+
it("sets state to approval-responded when tool-approval-response with approved: true", () => {
|
|
1014
|
+
const messages = [
|
|
1015
|
+
baseMessageDoc({
|
|
1016
|
+
_id: "msg1",
|
|
1017
|
+
order: 1,
|
|
1018
|
+
stepOrder: 1,
|
|
1019
|
+
tool: true,
|
|
1020
|
+
message: {
|
|
1021
|
+
role: "assistant",
|
|
1022
|
+
content: [
|
|
1023
|
+
{
|
|
1024
|
+
type: "tool-call",
|
|
1025
|
+
toolName: "dangerousTool",
|
|
1026
|
+
toolCallId: "call1",
|
|
1027
|
+
input: { action: "delete" },
|
|
1028
|
+
args: { action: "delete" },
|
|
1029
|
+
},
|
|
1030
|
+
{
|
|
1031
|
+
type: "tool-approval-request",
|
|
1032
|
+
approvalId: "approval1",
|
|
1033
|
+
toolCallId: "call1",
|
|
1034
|
+
},
|
|
1035
|
+
],
|
|
1036
|
+
},
|
|
1037
|
+
}),
|
|
1038
|
+
baseMessageDoc({
|
|
1039
|
+
_id: "msg2",
|
|
1040
|
+
order: 1,
|
|
1041
|
+
stepOrder: 2,
|
|
1042
|
+
tool: true,
|
|
1043
|
+
message: {
|
|
1044
|
+
role: "tool",
|
|
1045
|
+
content: [
|
|
1046
|
+
{
|
|
1047
|
+
type: "tool-approval-response",
|
|
1048
|
+
approvalId: "approval1",
|
|
1049
|
+
approved: true,
|
|
1050
|
+
reason: "User confirmed",
|
|
1051
|
+
},
|
|
1052
|
+
],
|
|
1053
|
+
},
|
|
1054
|
+
}),
|
|
1055
|
+
];
|
|
1056
|
+
|
|
1057
|
+
const uiMessages = toUIMessages(messages);
|
|
1058
|
+
|
|
1059
|
+
expect(uiMessages).toHaveLength(1);
|
|
1060
|
+
const toolPart = uiMessages[0].parts.find(
|
|
1061
|
+
(p) => p.type === "tool-dangerousTool",
|
|
1062
|
+
) as any;
|
|
1063
|
+
expect(toolPart).toBeDefined();
|
|
1064
|
+
expect(toolPart.state).toBe("approval-responded");
|
|
1065
|
+
expect(toolPart.approval).toEqual({
|
|
1066
|
+
id: "approval1",
|
|
1067
|
+
approved: true,
|
|
1068
|
+
reason: "User confirmed",
|
|
1069
|
+
});
|
|
1070
|
+
});
|
|
1071
|
+
|
|
1072
|
+
it("sets state to output-denied when tool-approval-response with approved: false", () => {
|
|
1073
|
+
const messages = [
|
|
1074
|
+
baseMessageDoc({
|
|
1075
|
+
_id: "msg1",
|
|
1076
|
+
order: 1,
|
|
1077
|
+
stepOrder: 1,
|
|
1078
|
+
tool: true,
|
|
1079
|
+
message: {
|
|
1080
|
+
role: "assistant",
|
|
1081
|
+
content: [
|
|
1082
|
+
{
|
|
1083
|
+
type: "tool-call",
|
|
1084
|
+
toolName: "dangerousTool",
|
|
1085
|
+
toolCallId: "call1",
|
|
1086
|
+
input: { action: "delete" },
|
|
1087
|
+
args: { action: "delete" },
|
|
1088
|
+
},
|
|
1089
|
+
{
|
|
1090
|
+
type: "tool-approval-request",
|
|
1091
|
+
approvalId: "approval1",
|
|
1092
|
+
toolCallId: "call1",
|
|
1093
|
+
},
|
|
1094
|
+
],
|
|
1095
|
+
},
|
|
1096
|
+
}),
|
|
1097
|
+
baseMessageDoc({
|
|
1098
|
+
_id: "msg2",
|
|
1099
|
+
order: 1,
|
|
1100
|
+
stepOrder: 2,
|
|
1101
|
+
tool: true,
|
|
1102
|
+
message: {
|
|
1103
|
+
role: "tool",
|
|
1104
|
+
content: [
|
|
1105
|
+
{
|
|
1106
|
+
type: "tool-approval-response",
|
|
1107
|
+
approvalId: "approval1",
|
|
1108
|
+
approved: false,
|
|
1109
|
+
reason: "User declined the operation",
|
|
1110
|
+
},
|
|
1111
|
+
],
|
|
1112
|
+
},
|
|
1113
|
+
}),
|
|
1114
|
+
];
|
|
1115
|
+
|
|
1116
|
+
const uiMessages = toUIMessages(messages);
|
|
1117
|
+
|
|
1118
|
+
expect(uiMessages).toHaveLength(1);
|
|
1119
|
+
const toolPart = uiMessages[0].parts.find(
|
|
1120
|
+
(p) => p.type === "tool-dangerousTool",
|
|
1121
|
+
) as any;
|
|
1122
|
+
expect(toolPart).toBeDefined();
|
|
1123
|
+
expect(toolPart.state).toBe("output-denied");
|
|
1124
|
+
expect(toolPart.approval).toEqual({
|
|
1125
|
+
id: "approval1",
|
|
1126
|
+
approved: false,
|
|
1127
|
+
reason: "User declined the operation",
|
|
1128
|
+
});
|
|
1129
|
+
});
|
|
1130
|
+
|
|
1131
|
+
it("sets state to output-denied when tool-result has execution-denied output", () => {
|
|
1132
|
+
const messages = [
|
|
1133
|
+
baseMessageDoc({
|
|
1134
|
+
_id: "msg1",
|
|
1135
|
+
order: 1,
|
|
1136
|
+
stepOrder: 1,
|
|
1137
|
+
tool: true,
|
|
1138
|
+
message: {
|
|
1139
|
+
role: "assistant",
|
|
1140
|
+
content: [
|
|
1141
|
+
{
|
|
1142
|
+
type: "tool-call",
|
|
1143
|
+
toolName: "dangerousTool",
|
|
1144
|
+
toolCallId: "call1",
|
|
1145
|
+
input: { action: "delete" },
|
|
1146
|
+
args: { action: "delete" },
|
|
1147
|
+
},
|
|
1148
|
+
],
|
|
1149
|
+
},
|
|
1150
|
+
}),
|
|
1151
|
+
baseMessageDoc({
|
|
1152
|
+
_id: "msg2",
|
|
1153
|
+
order: 1,
|
|
1154
|
+
stepOrder: 2,
|
|
1155
|
+
tool: true,
|
|
1156
|
+
message: {
|
|
1157
|
+
role: "tool",
|
|
1158
|
+
content: [
|
|
1159
|
+
{
|
|
1160
|
+
type: "tool-result",
|
|
1161
|
+
toolCallId: "call1",
|
|
1162
|
+
toolName: "dangerousTool",
|
|
1163
|
+
output: {
|
|
1164
|
+
type: "execution-denied",
|
|
1165
|
+
reason: "Tool execution was denied by the user",
|
|
1166
|
+
},
|
|
1167
|
+
},
|
|
1168
|
+
],
|
|
1169
|
+
},
|
|
1170
|
+
}),
|
|
1171
|
+
];
|
|
1172
|
+
|
|
1173
|
+
const uiMessages = toUIMessages(messages);
|
|
1174
|
+
|
|
1175
|
+
expect(uiMessages).toHaveLength(1);
|
|
1176
|
+
const toolPart = uiMessages[0].parts.find(
|
|
1177
|
+
(p) => p.type === "tool-dangerousTool",
|
|
1178
|
+
) as any;
|
|
1179
|
+
expect(toolPart).toBeDefined();
|
|
1180
|
+
expect(toolPart.state).toBe("output-denied");
|
|
1181
|
+
expect(toolPart.approval).toEqual({
|
|
1182
|
+
id: "",
|
|
1183
|
+
approved: false,
|
|
1184
|
+
reason: "Tool execution was denied by the user",
|
|
1185
|
+
});
|
|
1186
|
+
});
|
|
1187
|
+
|
|
1188
|
+
it("handles full approval flow: request → approved → executed → output-available", () => {
|
|
1189
|
+
const messages = [
|
|
1190
|
+
baseMessageDoc({
|
|
1191
|
+
_id: "msg1",
|
|
1192
|
+
order: 1,
|
|
1193
|
+
stepOrder: 1,
|
|
1194
|
+
tool: true,
|
|
1195
|
+
message: {
|
|
1196
|
+
role: "assistant",
|
|
1197
|
+
content: [
|
|
1198
|
+
{
|
|
1199
|
+
type: "tool-call",
|
|
1200
|
+
toolName: "dangerousTool",
|
|
1201
|
+
toolCallId: "call1",
|
|
1202
|
+
input: { action: "delete" },
|
|
1203
|
+
args: { action: "delete" },
|
|
1204
|
+
},
|
|
1205
|
+
{
|
|
1206
|
+
type: "tool-approval-request",
|
|
1207
|
+
approvalId: "approval1",
|
|
1208
|
+
toolCallId: "call1",
|
|
1209
|
+
},
|
|
1210
|
+
],
|
|
1211
|
+
},
|
|
1212
|
+
}),
|
|
1213
|
+
baseMessageDoc({
|
|
1214
|
+
_id: "msg2",
|
|
1215
|
+
order: 1,
|
|
1216
|
+
stepOrder: 2,
|
|
1217
|
+
tool: true,
|
|
1218
|
+
message: {
|
|
1219
|
+
role: "tool",
|
|
1220
|
+
content: [
|
|
1221
|
+
{
|
|
1222
|
+
type: "tool-approval-response",
|
|
1223
|
+
approvalId: "approval1",
|
|
1224
|
+
approved: true,
|
|
1225
|
+
},
|
|
1226
|
+
],
|
|
1227
|
+
},
|
|
1228
|
+
}),
|
|
1229
|
+
baseMessageDoc({
|
|
1230
|
+
_id: "msg3",
|
|
1231
|
+
order: 1,
|
|
1232
|
+
stepOrder: 3,
|
|
1233
|
+
tool: true,
|
|
1234
|
+
message: {
|
|
1235
|
+
role: "tool",
|
|
1236
|
+
content: [
|
|
1237
|
+
{
|
|
1238
|
+
type: "tool-result",
|
|
1239
|
+
toolCallId: "call1",
|
|
1240
|
+
toolName: "dangerousTool",
|
|
1241
|
+
output: {
|
|
1242
|
+
type: "json",
|
|
1243
|
+
value: { deleted: true },
|
|
1244
|
+
},
|
|
1245
|
+
},
|
|
1246
|
+
],
|
|
1247
|
+
},
|
|
1248
|
+
}),
|
|
1249
|
+
];
|
|
1250
|
+
|
|
1251
|
+
const uiMessages = toUIMessages(messages);
|
|
1252
|
+
|
|
1253
|
+
expect(uiMessages).toHaveLength(1);
|
|
1254
|
+
const toolPart = uiMessages[0].parts.find(
|
|
1255
|
+
(p) => p.type === "tool-dangerousTool",
|
|
1256
|
+
) as any;
|
|
1257
|
+
expect(toolPart).toBeDefined();
|
|
1258
|
+
// After tool-result, state should be output-available
|
|
1259
|
+
expect(toolPart.state).toBe("output-available");
|
|
1260
|
+
expect(toolPart.output).toEqual({ deleted: true });
|
|
1261
|
+
// approval should still be preserved from earlier
|
|
1262
|
+
expect(toolPart.approval).toEqual({
|
|
1263
|
+
id: "approval1",
|
|
1264
|
+
approved: true,
|
|
1265
|
+
reason: undefined,
|
|
1266
|
+
});
|
|
1267
|
+
});
|
|
1268
|
+
});
|
|
1269
|
+
});
|