@convex-dev/agent 0.6.3 → 0.7.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 (246) hide show
  1. package/MIGRATION.md +60 -116
  2. package/dist/client/messages.d.ts +1 -398
  3. package/dist/client/messages.d.ts.map +1 -1
  4. package/dist/client/messages.js +3 -47
  5. package/dist/client/messages.js.map +1 -1
  6. package/dist/client/types.d.ts +1 -455
  7. package/dist/client/types.d.ts.map +1 -1
  8. package/dist/component/_generated/component.d.ts +805 -45
  9. package/dist/component/_generated/component.d.ts.map +1 -1
  10. package/dist/component/apiKeys.js +5 -5
  11. package/dist/component/apiKeys.js.map +1 -1
  12. package/dist/component/files.d.ts.map +1 -1
  13. package/dist/component/files.js +13 -11
  14. package/dist/component/files.js.map +1 -1
  15. package/dist/component/messages.d.ts +1252 -201
  16. package/dist/component/messages.d.ts.map +1 -1
  17. package/dist/component/messages.js +83 -47
  18. package/dist/component/messages.js.map +1 -1
  19. package/dist/component/schema.d.ts +2593 -332
  20. package/dist/component/schema.d.ts.map +1 -1
  21. package/dist/component/streams.d.ts +7 -1
  22. package/dist/component/streams.d.ts.map +1 -1
  23. package/dist/component/streams.js +46 -47
  24. package/dist/component/streams.js.map +1 -1
  25. package/dist/component/threads.d.ts.map +1 -1
  26. package/dist/component/threads.js +8 -9
  27. package/dist/component/threads.js.map +1 -1
  28. package/dist/component/users.js +2 -2
  29. package/dist/component/users.js.map +1 -1
  30. package/dist/component/vector/index.d.ts.map +1 -1
  31. package/dist/component/vector/index.js +14 -8
  32. package/dist/component/vector/index.js.map +1 -1
  33. package/dist/react/index.d.ts +5 -5
  34. package/dist/react/index.d.ts.map +1 -1
  35. package/dist/react/index.js +5 -5
  36. package/dist/react/index.js.map +1 -1
  37. package/dist/shared.d.ts +19 -7
  38. package/dist/shared.d.ts.map +1 -1
  39. package/dist/shared.js +4 -4
  40. package/dist/shared.js.map +1 -1
  41. package/dist/streaming/materializePersistedUIMessageChunks.d.ts +16 -0
  42. package/dist/streaming/materializePersistedUIMessageChunks.d.ts.map +1 -0
  43. package/dist/streaming/materializePersistedUIMessageChunks.js +302 -0
  44. package/dist/streaming/materializePersistedUIMessageChunks.js.map +1 -0
  45. package/dist/streaming/persistedUIMessageChunks.d.ts +106 -0
  46. package/dist/streaming/persistedUIMessageChunks.d.ts.map +1 -0
  47. package/dist/streaming/persistedUIMessageChunks.js +475 -0
  48. package/dist/streaming/persistedUIMessageChunks.js.map +1 -0
  49. package/dist/validators.d.ts +10026 -1055
  50. package/dist/validators.d.ts.map +1 -1
  51. package/dist/validators.js +71 -9
  52. package/dist/validators.js.map +1 -1
  53. package/dist/{UIMessages.d.ts → vercel/UIMessages.d.ts} +1 -1
  54. package/dist/vercel/UIMessages.d.ts.map +1 -0
  55. package/dist/{UIMessages.js → vercel/UIMessages.js} +63 -1
  56. package/dist/vercel/UIMessages.js.map +1 -0
  57. package/dist/{client → vercel/client}/createTool.d.ts +23 -16
  58. package/dist/vercel/client/createTool.d.ts.map +1 -0
  59. package/dist/{client → vercel/client}/createTool.js +2 -0
  60. package/dist/vercel/client/createTool.js.map +1 -0
  61. package/dist/{client → vercel/client}/definePlaygroundAPI.d.ts +593 -134
  62. package/dist/{client → vercel/client}/definePlaygroundAPI.d.ts.map +1 -1
  63. package/dist/{client → vercel/client}/definePlaygroundAPI.js +13 -9
  64. package/dist/vercel/client/definePlaygroundAPI.js.map +1 -0
  65. package/dist/{client → vercel/client}/files.d.ts +2 -2
  66. package/dist/vercel/client/files.d.ts.map +1 -0
  67. package/dist/vercel/client/files.js.map +1 -0
  68. package/dist/vercel/client/messages.d.ts +542 -0
  69. package/dist/vercel/client/messages.d.ts.map +1 -0
  70. package/dist/vercel/client/messages.js +66 -0
  71. package/dist/vercel/client/messages.js.map +1 -0
  72. package/dist/{client → vercel/client}/mockModel.d.ts +17 -17
  73. package/dist/vercel/client/mockModel.d.ts.map +1 -0
  74. package/dist/{client → vercel/client}/mockModel.js +12 -13
  75. package/dist/vercel/client/mockModel.js.map +1 -0
  76. package/dist/{client → vercel/client}/saveInputMessages.d.ts +2 -2
  77. package/dist/vercel/client/saveInputMessages.d.ts.map +1 -0
  78. package/dist/vercel/client/saveInputMessages.js.map +1 -0
  79. package/dist/{client → vercel/client}/search.d.ts +119 -22
  80. package/dist/vercel/client/search.d.ts.map +1 -0
  81. package/dist/{client → vercel/client}/search.js +2 -2
  82. package/dist/vercel/client/search.js.map +1 -0
  83. package/dist/{client → vercel/client}/start.d.ts +26 -11
  84. package/dist/vercel/client/start.d.ts.map +1 -0
  85. package/dist/{client → vercel/client}/start.js +64 -22
  86. package/dist/vercel/client/start.js.map +1 -0
  87. package/dist/{client → vercel/client}/streamText.d.ts +13 -12
  88. package/dist/vercel/client/streamText.d.ts.map +1 -0
  89. package/dist/{client → vercel/client}/streamText.js +43 -9
  90. package/dist/vercel/client/streamText.js.map +1 -0
  91. package/dist/{client → vercel/client}/streaming.d.ts +1410 -183
  92. package/dist/vercel/client/streaming.d.ts.map +1 -0
  93. package/dist/{client → vercel/client}/streaming.js +120 -52
  94. package/dist/vercel/client/streaming.js.map +1 -0
  95. package/dist/vercel/client/types.d.ts +456 -0
  96. package/dist/vercel/client/types.d.ts.map +1 -0
  97. package/dist/vercel/client/types.js +3 -0
  98. package/dist/vercel/client/types.js.map +1 -0
  99. package/dist/{client → vercel/client}/utils.d.ts +3 -2
  100. package/dist/vercel/client/utils.d.ts.map +1 -0
  101. package/dist/{client → vercel/client}/utils.js +2 -2
  102. package/dist/vercel/client/utils.js.map +1 -0
  103. package/dist/vercel/deltas.d.ts +31 -0
  104. package/dist/vercel/deltas.d.ts.map +1 -0
  105. package/dist/vercel/deltas.js +525 -0
  106. package/dist/vercel/deltas.js.map +1 -0
  107. package/dist/{client → vercel}/index.d.ts +484 -97
  108. package/dist/vercel/index.d.ts.map +1 -0
  109. package/dist/{client → vercel}/index.js +65 -38
  110. package/dist/vercel/index.js.map +1 -0
  111. package/dist/{mapping.d.ts → vercel/mapping.d.ts} +5 -13
  112. package/dist/vercel/mapping.d.ts.map +1 -0
  113. package/dist/{mapping.js → vercel/mapping.js} +411 -112
  114. package/dist/vercel/mapping.js.map +1 -0
  115. package/dist/{react → vercel/react}/optimisticallySendMessage.d.ts +2 -2
  116. package/dist/vercel/react/optimisticallySendMessage.d.ts.map +1 -0
  117. package/dist/vercel/react/optimisticallySendMessage.js.map +1 -0
  118. package/dist/{react → vercel/react}/types.d.ts +1 -1
  119. package/dist/vercel/react/types.d.ts.map +1 -0
  120. package/dist/{react → vercel/react}/types.js.map +1 -1
  121. package/dist/{react → vercel/react}/useDeltaStreams.d.ts +1 -1
  122. package/dist/vercel/react/useDeltaStreams.d.ts.map +1 -0
  123. package/dist/{react → vercel/react}/useDeltaStreams.js +1 -1
  124. package/dist/vercel/react/useDeltaStreams.js.map +1 -0
  125. package/dist/{react → vercel/react}/useStreamingUIMessages.d.ts +1 -1
  126. package/dist/vercel/react/useStreamingUIMessages.d.ts.map +1 -0
  127. package/dist/vercel/react/useStreamingUIMessages.js +112 -0
  128. package/dist/vercel/react/useStreamingUIMessages.js.map +1 -0
  129. package/dist/{react → vercel/react}/useThreadMessages.d.ts +1 -1
  130. package/dist/vercel/react/useThreadMessages.d.ts.map +1 -0
  131. package/dist/{react → vercel/react}/useThreadMessages.js +1 -1
  132. package/dist/vercel/react/useThreadMessages.js.map +1 -0
  133. package/dist/{react → vercel/react}/useUIMessages.d.ts +2 -1
  134. package/dist/vercel/react/useUIMessages.d.ts.map +1 -0
  135. package/dist/{react → vercel/react}/useUIMessages.js +8 -4
  136. package/dist/vercel/react/useUIMessages.js.map +1 -0
  137. package/package.json +30 -22
  138. package/src/client/messages.ts +5 -107
  139. package/src/client/types.ts +0 -621
  140. package/src/component/_generated/component.ts +1089 -48
  141. package/src/component/apiKeys.ts +5 -5
  142. package/src/component/files.test.ts +1 -1
  143. package/src/component/files.ts +14 -12
  144. package/src/component/messages.test.ts +273 -1
  145. package/src/component/messages.ts +99 -57
  146. package/src/component/streams.ts +80 -64
  147. package/src/component/threads.ts +12 -9
  148. package/src/component/users.ts +2 -2
  149. package/src/component/vector/index.ts +14 -7
  150. package/src/eslintConfig.test.ts +44 -0
  151. package/src/react/index.ts +8 -5
  152. package/src/shared.ts +26 -36
  153. package/src/streaming/materializePersistedUIMessageChunks.test.ts +513 -0
  154. package/src/streaming/materializePersistedUIMessageChunks.ts +398 -0
  155. package/src/streaming/persistedUIMessageChunks.test.ts +205 -0
  156. package/src/streaming/persistedUIMessageChunks.ts +658 -0
  157. package/src/validators.test.ts +4 -1
  158. package/src/validators.ts +89 -8
  159. package/src/{UIMessages.combineUIMessages.test.ts → vercel/UIMessages.combineUIMessages.test.ts} +4 -1
  160. package/src/{UIMessages.ts → vercel/UIMessages.ts} +96 -5
  161. package/src/{client → vercel/client}/approval.test.ts +48 -19
  162. package/src/{client → vercel/client}/createTool.ts +63 -23
  163. package/src/{client → vercel/client}/definePlaygroundAPI.ts +44 -29
  164. package/src/{client → vercel/client}/files.ts +2 -2
  165. package/src/{client → vercel/client}/index.test.ts +162 -3
  166. package/src/vercel/client/messages.ts +182 -0
  167. package/src/{client → vercel/client}/mockModel.ts +38 -39
  168. package/src/{client → vercel/client}/saveInputMessages.test.ts +1 -1
  169. package/src/{client → vercel/client}/saveInputMessages.ts +7 -4
  170. package/src/{client → vercel/client}/search.test.ts +3 -3
  171. package/src/{client → vercel/client}/search.ts +4 -4
  172. package/src/{client → vercel/client}/setup.test.ts +6 -3
  173. package/src/vercel/client/start.test.ts +14 -0
  174. package/src/{client → vercel/client}/start.ts +147 -36
  175. package/src/vercel/client/streamText.test.ts +223 -0
  176. package/src/{client → vercel/client}/streamText.ts +102 -31
  177. package/src/{client → vercel/client}/streaming.integration.test.ts +40 -115
  178. package/src/{client → vercel/client}/streaming.test.ts +178 -4
  179. package/src/{client → vercel/client}/streaming.ts +114 -52
  180. package/src/vercel/client/types.test.ts +13 -0
  181. package/src/vercel/client/types.ts +679 -0
  182. package/src/{client → vercel/client}/utils.ts +13 -7
  183. package/src/vercel/deltas.test.ts +898 -0
  184. package/src/vercel/deltas.ts +633 -0
  185. package/src/{fromUIMessages.test.ts → vercel/fromUIMessages.test.ts} +17 -1
  186. package/src/{client → vercel}/index.ts +169 -62
  187. package/src/vercel/mapping.test.ts +965 -0
  188. package/src/{mapping.ts → vercel/mapping.ts} +501 -131
  189. package/src/{react → vercel/react}/optimisticallySendMessage.ts +2 -2
  190. package/src/{react → vercel/react}/types.ts +1 -1
  191. package/src/{react → vercel/react}/useDeltaStreams.ts +6 -2
  192. package/src/{react → vercel/react}/useStreamingUIMessages.ts +67 -35
  193. package/src/{react → vercel/react}/useThreadMessages.ts +2 -2
  194. package/src/{react → vercel/react}/useUIMessages.test.ts +80 -1
  195. package/src/{react → vercel/react}/useUIMessages.ts +13 -5
  196. package/src/{toUIMessages.test.ts → vercel/toUIMessages.test.ts} +49 -2
  197. package/dist/UIMessages.d.ts.map +0 -1
  198. package/dist/UIMessages.js.map +0 -1
  199. package/dist/client/createTool.d.ts.map +0 -1
  200. package/dist/client/createTool.js.map +0 -1
  201. package/dist/client/definePlaygroundAPI.js.map +0 -1
  202. package/dist/client/files.d.ts.map +0 -1
  203. package/dist/client/files.js.map +0 -1
  204. package/dist/client/index.d.ts.map +0 -1
  205. package/dist/client/index.js.map +0 -1
  206. package/dist/client/mockModel.d.ts.map +0 -1
  207. package/dist/client/mockModel.js.map +0 -1
  208. package/dist/client/saveInputMessages.d.ts.map +0 -1
  209. package/dist/client/saveInputMessages.js.map +0 -1
  210. package/dist/client/search.d.ts.map +0 -1
  211. package/dist/client/search.js.map +0 -1
  212. package/dist/client/start.d.ts.map +0 -1
  213. package/dist/client/start.js.map +0 -1
  214. package/dist/client/streamText.d.ts.map +0 -1
  215. package/dist/client/streamText.js.map +0 -1
  216. package/dist/client/streaming.d.ts.map +0 -1
  217. package/dist/client/streaming.js.map +0 -1
  218. package/dist/client/utils.d.ts.map +0 -1
  219. package/dist/client/utils.js.map +0 -1
  220. package/dist/deltas.d.ts +0 -43
  221. package/dist/deltas.d.ts.map +0 -1
  222. package/dist/deltas.js +0 -446
  223. package/dist/deltas.js.map +0 -1
  224. package/dist/mapping.d.ts.map +0 -1
  225. package/dist/mapping.js.map +0 -1
  226. package/dist/react/optimisticallySendMessage.d.ts.map +0 -1
  227. package/dist/react/optimisticallySendMessage.js.map +0 -1
  228. package/dist/react/types.d.ts.map +0 -1
  229. package/dist/react/useDeltaStreams.d.ts.map +0 -1
  230. package/dist/react/useDeltaStreams.js.map +0 -1
  231. package/dist/react/useStreamingUIMessages.d.ts.map +0 -1
  232. package/dist/react/useStreamingUIMessages.js +0 -92
  233. package/dist/react/useStreamingUIMessages.js.map +0 -1
  234. package/dist/react/useThreadMessages.d.ts.map +0 -1
  235. package/dist/react/useThreadMessages.js.map +0 -1
  236. package/dist/react/useUIMessages.d.ts.map +0 -1
  237. package/dist/react/useUIMessages.js.map +0 -1
  238. package/src/client/streamText.test.ts +0 -102
  239. package/src/deltas.test.ts +0 -626
  240. package/src/deltas.ts +0 -569
  241. package/src/mapping.test.ts +0 -402
  242. /package/dist/{client → vercel/client}/files.js +0 -0
  243. /package/dist/{client → vercel/client}/saveInputMessages.js +0 -0
  244. /package/dist/{react → vercel/react}/optimisticallySendMessage.js +0 -0
  245. /package/dist/{react → vercel/react}/types.js +0 -0
  246. /package/src/{UIMessages.test.ts → vercel/UIMessages.test.ts} +0 -0
@@ -1,402 +0,0 @@
1
- import { describe, test, expect, vi } from "vitest";
2
- import {
3
- guessMimeType,
4
- serializeDataOrUrl,
5
- toModelMessageDataOrUrl,
6
- serializeMessage,
7
- toModelMessage,
8
- serializeContent,
9
- toModelMessageContent,
10
- autoDenyUnresolvedApprovals,
11
- } from "./mapping.js";
12
- import { api } from "./component/_generated/api.js";
13
- import type { AgentComponent, ActionCtx } from "./client/types.js";
14
- import { vMessage, vToolResultPart } from "./validators.js";
15
- import fs from "fs";
16
- import path from "path";
17
- import type { SerializedContent } from "./mapping.js";
18
- import { validate } from "convex-helpers/validators";
19
- import type { ToolResultPart } from "ai";
20
- import type { Infer } from "convex/values";
21
-
22
- const testAssetsDir = path.join(__dirname, "../test-assets");
23
- const testFiles = [
24
- "book.svg",
25
- "bump.jpeg",
26
- "stack.png",
27
- "favicon.ico",
28
- "convex-logo.svg",
29
- "stack-light@3x.webp",
30
- ];
31
-
32
- function fileToArrayBuffer(filePath: string): ArrayBuffer {
33
- const buf = fs.readFileSync(filePath);
34
- return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
35
- }
36
-
37
- describe("mapping", () => {
38
- test("infers correct mimeType for all test-assets", () => {
39
- const expected: { [key: string]: string } = {
40
- "book.svg": "image/svg+xml", // <svg
41
- "bump.jpeg": "image/jpeg",
42
- "stack.png": "image/png",
43
- "favicon.ico": "application/octet-stream", // fallback for ico
44
- "convex-logo.svg": "image/svg+xml", // <?xm
45
- "stack-light@3x.webp": "image/webp",
46
- "cat.gif": "image/gif",
47
- };
48
- for (const file of testFiles) {
49
- const ab = fileToArrayBuffer(path.join(testAssetsDir, file));
50
- const mime = guessMimeType(ab);
51
- expect(mime).toBe(expected[file]);
52
- }
53
- });
54
-
55
- test("turns Uint8Array into ArrayBuffer and round-trips", () => {
56
- const arr = new Uint8Array([1, 2, 3, 4, 5]);
57
- // serializeDataOrUrl should return the same ArrayBuffer
58
- const ser = serializeDataOrUrl(arr);
59
- expect(ser).toBeInstanceOf(ArrayBuffer);
60
- expect(new Uint8Array(ser as ArrayBuffer)).toEqual(arr);
61
- // toModelMessageDataOrUrl should return the same ArrayBuffer
62
- const deser = toModelMessageDataOrUrl(ser);
63
- expect(deser).toBeInstanceOf(ArrayBuffer);
64
- expect(new Uint8Array(deser as ArrayBuffer)).toEqual(arr);
65
- });
66
-
67
- test("round-trip serialize/deserialize message", async () => {
68
- const message = {
69
- role: "user" as const,
70
- content: "hello world",
71
- providerOptions: {},
72
- };
73
- // Fake ctx and component
74
- const ctx = {
75
- runAction: async () => undefined,
76
- runMutation: async () => undefined,
77
- storage: {
78
- store: async () => "storageId",
79
- getUrl: async () => "https://example.com/file",
80
- delete: async () => undefined,
81
- },
82
- } as unknown as ActionCtx;
83
- const component = api as unknown as AgentComponent;
84
- const { message: ser } = await serializeMessage(ctx, component, message);
85
- // Use is for type validation
86
- expect(validate(vMessage, ser)).toBeTruthy();
87
- const round = toModelMessage(ser);
88
- expect(round).toEqual(message);
89
- });
90
-
91
- test("tool output round-trips", async () => {
92
- const toolResult = {
93
- type: "tool-result" as const,
94
- toolCallId: "tool-call-id",
95
- toolName: "tool-name",
96
- output: {
97
- type: "text",
98
- value: "hello world",
99
- },
100
- } satisfies ToolResultPart;
101
- const [result] = toModelMessageContent([toolResult]);
102
- expect(result).toMatchObject(toolResult);
103
- const {
104
- content: [roundtrip],
105
- } = await serializeContent({} as ActionCtx, {} as AgentComponent, [
106
- result as ToolResultPart,
107
- ]);
108
- expect(roundtrip).toMatchObject(toolResult);
109
- });
110
-
111
- test("tool results get normalized to output", async () => {
112
- const toolResult = {
113
- type: "tool-result" as const,
114
- toolCallId: "tool-call-id",
115
- toolName: "tool-name",
116
- result: "hello world",
117
- } satisfies Infer<typeof vToolResultPart>;
118
- const expected = {
119
- type: "tool-result",
120
- toolCallId: "tool-call-id",
121
- toolName: "tool-name",
122
- output: {
123
- type: "text",
124
- value: "hello world",
125
- },
126
- };
127
- const [deserialized] = toModelMessageContent([toolResult]);
128
- expect(deserialized).toMatchObject(expected);
129
- const {
130
- content: [serialized],
131
- } = await serializeContent({} as ActionCtx, {} as AgentComponent, [
132
- toolResult,
133
- ]);
134
- expect(serialized).toMatchObject(expected);
135
- });
136
-
137
- test("saving files returns fileIds when too big", async () => {
138
- // Make a big file
139
- const bigArr = new Uint8Array(1024 * 65).fill(1);
140
- const ab = bigArr.buffer.slice(
141
- bigArr.byteOffset,
142
- bigArr.byteOffset + bigArr.byteLength,
143
- );
144
- let called = false;
145
- const ctx = {
146
- runAction: async () => undefined,
147
- runMutation: async (_fn: unknown, _args: unknown) => {
148
- called = true;
149
- return { fileId: "file-123", storageId: "storage-123" };
150
- },
151
- storage: {
152
- store: async () => "storageId",
153
- getUrl: async () => "https://example.com/file",
154
- delete: async () => undefined,
155
- },
156
- } as unknown as ActionCtx;
157
- const component = api as unknown as AgentComponent;
158
- const content = [
159
- {
160
- type: "file" as const,
161
- data: ab,
162
- filename: "bigfile.bin",
163
- mimeType: "application/octet-stream",
164
- providerOptions: {},
165
- },
166
- ];
167
- const { content: ser, fileIds } = await serializeContent(
168
- ctx,
169
- component,
170
- content,
171
- );
172
- expect(called).toBe(true);
173
- expect(fileIds).toEqual(["file-123"]);
174
- // Should have replaced data with a URL
175
- const serArr = ser as SerializedContent;
176
- expect(typeof (serArr as { data: unknown }[])[0].data).toBe("string");
177
- expect((serArr as { data: unknown }[])[0].data as string).toMatch(
178
- /^https?:\/\//,
179
- );
180
- });
181
-
182
- test("sanity: fileIds are not returned for small files", async () => {
183
- const arr = new Uint8Array([1, 2, 3, 4, 5]);
184
- const ab = arr.buffer.slice(
185
- arr.byteOffset,
186
- arr.byteOffset + arr.byteLength,
187
- );
188
- const ctx = {
189
- runAction: async () => undefined,
190
- runMutation: async () => ({
191
- fileId: "file-123",
192
- storageId: "storage-123",
193
- }),
194
- storage: {
195
- store: async () => "storageId",
196
- getUrl: async () => "https://example.com/file",
197
- delete: async () => undefined,
198
- },
199
- } as unknown as ActionCtx;
200
- const component = api as unknown as AgentComponent;
201
- const content = [
202
- {
203
- type: "file" as const,
204
- data: ab,
205
- filename: "smallfile.bin",
206
- mimeType: "application/octet-stream",
207
- providerOptions: {},
208
- },
209
- ];
210
- const { fileIds } = await serializeContent(ctx, component, content);
211
- expect(fileIds).toBeUndefined();
212
- });
213
-
214
- test("tool-approval-request is preserved after serialization", async () => {
215
- const approvalRequest = {
216
- type: "tool-approval-request" as const,
217
- approvalId: "approval-123",
218
- toolCallId: "tool-call-456",
219
- };
220
- const { content } = await serializeContent(
221
- {} as ActionCtx,
222
- {} as AgentComponent,
223
- [approvalRequest],
224
- );
225
- expect(content).toHaveLength(1);
226
- expect((content as unknown[])[0]).toMatchObject(approvalRequest);
227
- });
228
-
229
- test("tool-approval-response with approved: true is preserved", async () => {
230
- const approvalResponse = {
231
- type: "tool-approval-response" as const,
232
- approvalId: "approval-123",
233
- approved: true,
234
- reason: "User approved",
235
- };
236
- const { content } = await serializeContent(
237
- {} as ActionCtx,
238
- {} as AgentComponent,
239
- [approvalResponse],
240
- );
241
- expect(content).toHaveLength(1);
242
- expect((content as unknown[])[0]).toMatchObject(approvalResponse);
243
- });
244
-
245
- test("tool-approval-response with approved: false is preserved", async () => {
246
- const approvalResponse = {
247
- type: "tool-approval-response" as const,
248
- approvalId: "approval-123",
249
- approved: false,
250
- reason: "User denied",
251
- providerExecuted: false,
252
- };
253
- const { content } = await serializeContent(
254
- {} as ActionCtx,
255
- {} as AgentComponent,
256
- [approvalResponse],
257
- );
258
- expect(content).toHaveLength(1);
259
- expect((content as unknown[])[0]).toMatchObject(approvalResponse);
260
- });
261
-
262
- describe("autoDenyUnresolvedApprovals", () => {
263
- test("returns messages unchanged when no unresolved approvals", () => {
264
- const messages = [
265
- { role: "user" as const, content: "hello" },
266
- {
267
- role: "assistant" as const,
268
- content: [
269
- { type: "tool-call", toolCallId: "tc1", toolName: "a", input: {} },
270
- { type: "tool-approval-request", approvalId: "ap1", toolCallId: "tc1" },
271
- ],
272
- },
273
- {
274
- role: "tool" as const,
275
- content: [
276
- { type: "tool-approval-response", approvalId: "ap1", approved: true },
277
- ],
278
- },
279
- ] as any;
280
-
281
- const result = autoDenyUnresolvedApprovals(messages);
282
- expect(result).toBe(messages); // same reference, no changes
283
- });
284
-
285
- test("injects synthetic denial for a single unresolved approval", () => {
286
- const messages = [
287
- { role: "user" as const, content: "hello" },
288
- {
289
- role: "assistant" as const,
290
- content: [
291
- { type: "tool-call", toolCallId: "tc1", toolName: "a", input: {} },
292
- { type: "tool-approval-request", approvalId: "ap1", toolCallId: "tc1" },
293
- ],
294
- },
295
- { role: "user" as const, content: "new message" },
296
- ] as any;
297
-
298
- const result = autoDenyUnresolvedApprovals(messages);
299
- expect(result).toHaveLength(4); // original 3 + 1 synthetic tool message
300
- // Synthetic denial should be inserted right after the assistant message (index 1)
301
- expect(result[2].role).toBe("tool");
302
- const denialContent = result[2].content as any[];
303
- expect(denialContent).toHaveLength(1);
304
- expect(denialContent[0].type).toBe("tool-approval-response");
305
- expect(denialContent[0].approvalId).toBe("ap1");
306
- expect(denialContent[0].approved).toBe(false);
307
- expect(denialContent[0].reason).toBe("auto-denied: new generation started");
308
- // The new user message should follow
309
- expect(result[3].role).toBe("user");
310
- expect(result[3].content).toBe("new message");
311
- });
312
-
313
- test("groups multiple unresolved approvals from the same step into a single synthetic message", () => {
314
- const messages = [
315
- {
316
- role: "assistant" as const,
317
- content: [
318
- { type: "tool-call", toolCallId: "tc1", toolName: "a", input: {} },
319
- { type: "tool-call", toolCallId: "tc2", toolName: "b", input: {} },
320
- { type: "tool-approval-request", approvalId: "ap1", toolCallId: "tc1" },
321
- { type: "tool-approval-request", approvalId: "ap2", toolCallId: "tc2" },
322
- ],
323
- },
324
- ] as any;
325
-
326
- const result = autoDenyUnresolvedApprovals(messages);
327
- expect(result).toHaveLength(2); // assistant + 1 synthetic tool message
328
- expect(result[1].role).toBe("tool");
329
- const denialContent = result[1].content as any[];
330
- expect(denialContent).toHaveLength(2);
331
- expect(denialContent[0].approvalId).toBe("ap1");
332
- expect(denialContent[0].approved).toBe(false);
333
- expect(denialContent[1].approvalId).toBe("ap2");
334
- expect(denialContent[1].approved).toBe(false);
335
- });
336
-
337
- test("only auto-denies unresolved approvals, leaves resolved ones alone", () => {
338
- const messages = [
339
- {
340
- role: "assistant" as const,
341
- content: [
342
- { type: "tool-call", toolCallId: "tc1", toolName: "a", input: {} },
343
- { type: "tool-call", toolCallId: "tc2", toolName: "b", input: {} },
344
- { type: "tool-approval-request", approvalId: "ap1", toolCallId: "tc1" },
345
- { type: "tool-approval-request", approvalId: "ap2", toolCallId: "tc2" },
346
- ],
347
- },
348
- {
349
- role: "tool" as const,
350
- content: [
351
- { type: "tool-approval-response", approvalId: "ap1", approved: true },
352
- ],
353
- },
354
- { role: "user" as const, content: "next question" },
355
- ] as any;
356
-
357
- const result = autoDenyUnresolvedApprovals(messages);
358
- // Should inject a denial for ap2 (unresolved) after the assistant message
359
- expect(result).toHaveLength(4); // assistant + existing tool + synthetic denial + user
360
- // The synthetic denial is inserted after the assistant (index 0)
361
- expect(result[0].role).toBe("assistant");
362
- expect(result[1].role).toBe("tool"); // synthetic denial for ap2
363
- const denialContent = result[1].content as any[];
364
- expect(denialContent).toHaveLength(1);
365
- expect(denialContent[0].approvalId).toBe("ap2");
366
- expect(denialContent[0].approved).toBe(false);
367
- // Original tool message (ap1 response) follows
368
- expect(result[2].role).toBe("tool");
369
- const originalToolContent = result[2].content as any[];
370
- expect(originalToolContent[0].approvalId).toBe("ap1");
371
- expect(originalToolContent[0].approved).toBe(true);
372
- // User message last
373
- expect(result[3].role).toBe("user");
374
- });
375
-
376
- test("emits console.warn for each auto-denied approval", () => {
377
- const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
378
- const messages = [
379
- {
380
- role: "assistant" as const,
381
- content: [
382
- { type: "tool-call", toolCallId: "tc1", toolName: "a", input: {} },
383
- { type: "tool-call", toolCallId: "tc2", toolName: "b", input: {} },
384
- { type: "tool-approval-request", approvalId: "ap1", toolCallId: "tc1" },
385
- { type: "tool-approval-request", approvalId: "ap2", toolCallId: "tc2" },
386
- ],
387
- },
388
- ] as any;
389
-
390
- autoDenyUnresolvedApprovals(messages);
391
-
392
- expect(warnSpy).toHaveBeenCalledTimes(2);
393
- expect(warnSpy).toHaveBeenCalledWith(
394
- expect.stringContaining("ap1"),
395
- );
396
- expect(warnSpy).toHaveBeenCalledWith(
397
- expect.stringContaining("ap2"),
398
- );
399
- warnSpy.mockRestore();
400
- });
401
- });
402
- });
File without changes
File without changes