@convex-dev/agent 0.5.0-alpha.1 → 0.6.0-alpha.0

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.
Files changed (233) hide show
  1. package/README.md +32 -27
  2. package/dist/UIMessages.d.ts +46 -0
  3. package/dist/UIMessages.d.ts.map +1 -0
  4. package/dist/UIMessages.js +546 -0
  5. package/dist/UIMessages.js.map +1 -0
  6. package/dist/client/createTool.d.ts +129 -27
  7. package/dist/client/createTool.d.ts.map +1 -1
  8. package/dist/client/createTool.js +66 -12
  9. package/dist/client/createTool.js.map +1 -1
  10. package/dist/client/defaultComponent.d.ts +11 -0
  11. package/dist/client/defaultComponent.d.ts.map +1 -0
  12. package/dist/client/defaultComponent.js +7 -0
  13. package/dist/client/defaultComponent.js.map +1 -0
  14. package/dist/client/definePlaygroundAPI.d.ts +1323 -192
  15. package/dist/client/definePlaygroundAPI.d.ts.map +1 -1
  16. package/dist/client/definePlaygroundAPI.js +52 -28
  17. package/dist/client/definePlaygroundAPI.js.map +1 -1
  18. package/dist/client/files.d.ts +20 -7
  19. package/dist/client/files.d.ts.map +1 -1
  20. package/dist/client/files.js +68 -11
  21. package/dist/client/files.js.map +1 -1
  22. package/dist/client/index.d.ts +1056 -965
  23. package/dist/client/index.d.ts.map +1 -1
  24. package/dist/client/index.js +242 -748
  25. package/dist/client/index.js.map +1 -1
  26. package/dist/client/messages.d.ts +461 -0
  27. package/dist/client/messages.d.ts.map +1 -0
  28. package/dist/client/messages.js +106 -0
  29. package/dist/client/messages.js.map +1 -0
  30. package/dist/client/mockModel.d.ts +42 -0
  31. package/dist/client/mockModel.d.ts.map +1 -0
  32. package/dist/client/mockModel.js +175 -0
  33. package/dist/client/mockModel.js.map +1 -0
  34. package/dist/client/saveInputMessages.d.ts +20 -0
  35. package/dist/client/saveInputMessages.d.ts.map +1 -0
  36. package/dist/client/saveInputMessages.js +58 -0
  37. package/dist/client/saveInputMessages.js.map +1 -0
  38. package/dist/client/search.d.ts +346 -35
  39. package/dist/client/search.d.ts.map +1 -1
  40. package/dist/client/search.js +350 -39
  41. package/dist/client/search.js.map +1 -1
  42. package/dist/client/start.d.ts +84 -0
  43. package/dist/client/start.d.ts.map +1 -0
  44. package/dist/client/start.js +171 -0
  45. package/dist/client/start.js.map +1 -0
  46. package/dist/client/streamText.d.ts +46 -0
  47. package/dist/client/streamText.d.ts.map +1 -0
  48. package/dist/client/streamText.js +93 -0
  49. package/dist/client/streamText.js.map +1 -0
  50. package/dist/client/streaming.d.ts +3705 -32
  51. package/dist/client/streaming.d.ts.map +1 -1
  52. package/dist/client/streaming.js +141 -59
  53. package/dist/client/streaming.js.map +1 -1
  54. package/dist/client/threads.d.ts +46 -0
  55. package/dist/client/threads.d.ts.map +1 -0
  56. package/dist/client/threads.js +49 -0
  57. package/dist/client/threads.js.map +1 -0
  58. package/dist/client/types.d.ts +265 -128
  59. package/dist/client/types.d.ts.map +1 -1
  60. package/dist/client/utils.d.ts +4 -0
  61. package/dist/client/utils.d.ts.map +1 -0
  62. package/dist/client/utils.js +21 -0
  63. package/dist/client/utils.js.map +1 -0
  64. package/dist/component/_generated/api.d.ts +24 -2178
  65. package/dist/component/_generated/api.d.ts.map +1 -1
  66. package/dist/component/_generated/api.js +10 -1
  67. package/dist/component/_generated/api.js.map +1 -1
  68. package/dist/component/_generated/component.d.ts +3119 -0
  69. package/dist/component/_generated/component.d.ts.map +1 -0
  70. package/dist/component/_generated/component.js +11 -0
  71. package/dist/component/_generated/component.js.map +1 -0
  72. package/dist/component/_generated/dataModel.d.ts +4 -18
  73. package/dist/component/_generated/dataModel.d.ts.map +1 -0
  74. package/dist/component/_generated/dataModel.js +11 -0
  75. package/dist/component/_generated/dataModel.js.map +1 -0
  76. package/dist/component/_generated/server.d.ts +10 -38
  77. package/dist/component/_generated/server.d.ts.map +1 -1
  78. package/dist/component/_generated/server.js +9 -5
  79. package/dist/component/_generated/server.js.map +1 -1
  80. package/dist/component/files.d.ts +16 -10
  81. package/dist/component/files.d.ts.map +1 -1
  82. package/dist/component/files.js +10 -2
  83. package/dist/component/files.js.map +1 -1
  84. package/dist/component/messages.d.ts +2553 -342
  85. package/dist/component/messages.d.ts.map +1 -1
  86. package/dist/component/messages.js +387 -154
  87. package/dist/component/messages.js.map +1 -1
  88. package/dist/component/schema.d.ts +5697 -3584
  89. package/dist/component/schema.d.ts.map +1 -1
  90. package/dist/component/schema.js +18 -41
  91. package/dist/component/schema.js.map +1 -1
  92. package/dist/component/streams.d.ts +35 -335
  93. package/dist/component/streams.d.ts.map +1 -1
  94. package/dist/component/streams.js +114 -73
  95. package/dist/component/streams.js.map +1 -1
  96. package/dist/component/threads.d.ts +16 -16
  97. package/dist/component/users.d.ts +4 -4
  98. package/dist/component/vector/index.d.ts +1 -1
  99. package/dist/component/vector/index.d.ts.map +1 -1
  100. package/dist/component/vector/index.js +1 -3
  101. package/dist/component/vector/index.js.map +1 -1
  102. package/dist/deltas.d.ts +43 -0
  103. package/dist/deltas.d.ts.map +1 -0
  104. package/dist/deltas.js +447 -0
  105. package/dist/deltas.js.map +1 -0
  106. package/dist/mapping.d.ts +20 -20
  107. package/dist/mapping.d.ts.map +1 -1
  108. package/dist/mapping.js +313 -96
  109. package/dist/mapping.js.map +1 -1
  110. package/dist/react/SmoothText.d.ts +5 -0
  111. package/dist/react/SmoothText.d.ts.map +1 -0
  112. package/dist/react/SmoothText.js +6 -0
  113. package/dist/react/SmoothText.js.map +1 -0
  114. package/dist/react/index.d.ts +5 -77
  115. package/dist/react/index.d.ts.map +1 -1
  116. package/dist/react/index.js +6 -160
  117. package/dist/react/index.js.map +1 -1
  118. package/dist/react/optimisticallySendMessage.d.ts +36 -3
  119. package/dist/react/optimisticallySendMessage.d.ts.map +1 -1
  120. package/dist/react/optimisticallySendMessage.js +35 -9
  121. package/dist/react/optimisticallySendMessage.js.map +1 -1
  122. package/dist/react/types.d.ts +4 -18
  123. package/dist/react/types.d.ts.map +1 -1
  124. package/dist/react/useDeltaStreams.d.ts +10 -0
  125. package/dist/react/useDeltaStreams.d.ts.map +1 -0
  126. package/dist/react/useDeltaStreams.js +101 -0
  127. package/dist/react/useDeltaStreams.js.map +1 -0
  128. package/dist/react/useSmoothText.d.ts +13 -12
  129. package/dist/react/useSmoothText.d.ts.map +1 -1
  130. package/dist/react/useSmoothText.js +32 -15
  131. package/dist/react/useSmoothText.js.map +1 -1
  132. package/dist/react/useStreamingUIMessages.d.ts +22 -0
  133. package/dist/react/useStreamingUIMessages.d.ts.map +1 -0
  134. package/dist/react/useStreamingUIMessages.js +92 -0
  135. package/dist/react/useStreamingUIMessages.js.map +1 -0
  136. package/dist/react/useThreadMessages.d.ts +104 -0
  137. package/dist/react/useThreadMessages.d.ts.map +1 -0
  138. package/dist/react/useThreadMessages.js +148 -0
  139. package/dist/react/useThreadMessages.js.map +1 -0
  140. package/dist/react/useUIMessages.d.ts +96 -0
  141. package/dist/react/useUIMessages.d.ts.map +1 -0
  142. package/dist/react/useUIMessages.js +108 -0
  143. package/dist/react/useUIMessages.js.map +1 -0
  144. package/dist/shared.d.ts +20 -4
  145. package/dist/shared.d.ts.map +1 -1
  146. package/dist/shared.js +45 -8
  147. package/dist/shared.js.map +1 -1
  148. package/dist/validators.d.ts +22981 -5666
  149. package/dist/validators.d.ts.map +1 -1
  150. package/dist/validators.js +245 -137
  151. package/dist/validators.js.map +1 -1
  152. package/package.json +98 -50
  153. package/src/UIMessages.combineUIMessages.test.ts +239 -0
  154. package/src/UIMessages.test.ts +273 -0
  155. package/src/UIMessages.ts +739 -0
  156. package/src/client/createTool.ts +293 -76
  157. package/src/client/defaultComponent.ts +17 -0
  158. package/src/client/definePlaygroundAPI.ts +67 -31
  159. package/src/client/files.ts +100 -20
  160. package/src/client/index.test.ts +40 -85
  161. package/src/client/index.ts +520 -1290
  162. package/src/client/messages.ts +237 -0
  163. package/src/client/mockModel.ts +245 -0
  164. package/src/client/saveInputMessages.test.ts +583 -0
  165. package/src/client/saveInputMessages.ts +101 -0
  166. package/src/client/search.test.ts +1207 -0
  167. package/src/client/search.ts +577 -70
  168. package/src/client/start.ts +310 -0
  169. package/src/client/streamText.ts +163 -0
  170. package/src/client/streaming.test.ts +186 -0
  171. package/src/client/streaming.ts +219 -97
  172. package/src/client/threads.ts +83 -0
  173. package/src/client/types.ts +368 -219
  174. package/src/client/utils.ts +27 -0
  175. package/src/component/_generated/api.ts +64 -0
  176. package/src/component/_generated/component.ts +4913 -0
  177. package/src/component/_generated/{server.d.ts → server.ts} +33 -21
  178. package/src/component/files.ts +11 -2
  179. package/src/component/messages.test.ts +195 -51
  180. package/src/component/messages.ts +490 -201
  181. package/src/component/schema.ts +20 -46
  182. package/src/component/setup.test.ts +7 -0
  183. package/src/component/streams.ts +184 -83
  184. package/src/component/users.test.ts +0 -1
  185. package/src/component/vector/index.ts +1 -3
  186. package/src/deltas.test.ts +626 -0
  187. package/src/deltas.ts +570 -0
  188. package/src/fromUIMessages.test.ts +497 -0
  189. package/src/mapping.test.ts +103 -6
  190. package/src/mapping.ts +422 -161
  191. package/src/react/SmoothText.tsx +9 -0
  192. package/src/react/index.ts +10 -230
  193. package/src/react/optimisticallySendMessage.ts +55 -12
  194. package/src/react/types.ts +6 -39
  195. package/src/react/useDeltaStreams.ts +154 -0
  196. package/src/react/useSmoothText.ts +56 -36
  197. package/src/react/useStreamingUIMessages.ts +143 -0
  198. package/src/react/useThreadMessages.ts +262 -0
  199. package/src/react/useUIMessages.test.ts +255 -0
  200. package/src/react/useUIMessages.ts +195 -0
  201. package/src/shared.ts +88 -12
  202. package/src/test.ts +18 -0
  203. package/src/toUIMessages.test.ts +1269 -0
  204. package/src/validators.test.ts +18 -19
  205. package/src/validators.ts +325 -185
  206. package/dist/client/_generated/_ignore.d.ts +0 -1
  207. package/dist/client/_generated/_ignore.d.ts.map +0 -1
  208. package/dist/client/_generated/_ignore.js +0 -3
  209. package/dist/client/_generated/_ignore.js.map +0 -1
  210. package/dist/client/listMessages.d.ts +0 -22
  211. package/dist/client/listMessages.d.ts.map +0 -1
  212. package/dist/client/listMessages.js +0 -25
  213. package/dist/client/listMessages.js.map +0 -1
  214. package/dist/package.json +0 -3
  215. package/dist/react/deltas.d.ts +0 -26
  216. package/dist/react/deltas.d.ts.map +0 -1
  217. package/dist/react/deltas.js +0 -384
  218. package/dist/react/deltas.js.map +0 -1
  219. package/dist/react/toUIMessages.d.ts +0 -15
  220. package/dist/react/toUIMessages.d.ts.map +0 -1
  221. package/dist/react/toUIMessages.js +0 -211
  222. package/dist/react/toUIMessages.js.map +0 -1
  223. package/src/client/listMessages.ts +0 -38
  224. package/src/component/_generated/api.d.ts +0 -2202
  225. package/src/component/_generated/api.js +0 -23
  226. package/src/component/_generated/server.js +0 -90
  227. package/src/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
  228. package/src/react/deltas.test.ts +0 -315
  229. package/src/react/deltas.ts +0 -478
  230. package/src/react/toUIMessages.test.ts +0 -420
  231. package/src/react/toUIMessages.ts +0 -253
  232. package/src/vitest.config.ts +0 -7
  233. /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
+ });