@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,9 +1,11 @@
1
1
  import type {
2
+ ModelMessage,
2
3
  StepResult,
3
4
  StreamTextResult,
4
5
  ToolSet,
5
6
  UIMessage as AIUIMessage,
6
7
  } from "ai";
8
+ import type { Context } from "@ai-sdk/provider-utils";
7
9
  import { streamText as streamTextAi } from "ai";
8
10
  import {
9
11
  compressUIMessageChunks,
@@ -14,16 +16,31 @@ import {
14
16
  import type {
15
17
  ActionCtx,
16
18
  AgentComponent,
17
- AgentPrompt,
18
19
  GenerationOutputMetadata,
19
20
  Options,
20
- Output,
21
+ StreamingTextArgs,
21
22
  } from "./types.js";
23
+ import type { Output as AISDKOutput } from "ai";
22
24
  import { startGeneration } from "./start.js";
23
- import type { Agent } from "./index.js";
24
- import { getModelName, getProviderName } from "../shared.js";
25
+ import type { Agent } from "../index.js";
26
+ import { getModelName, getProviderName } from "../../shared.js";
25
27
  import { errorToString, willContinue } from "./utils.js";
26
28
 
29
+ /** Finish every abort cleanup path before surfacing an internal failure. */
30
+ export async function runAbortCleanup(cleanup: {
31
+ failCall: () => Promise<void>;
32
+ failStreamer: () => Promise<void>;
33
+ onAbort?: () => PromiseLike<void> | void;
34
+ }): Promise<void> {
35
+ const results = await Promise.allSettled([
36
+ cleanup.failCall(),
37
+ cleanup.failStreamer(),
38
+ ]);
39
+ await cleanup.onAbort?.();
40
+ const failure = results.find((result) => result.status === "rejected");
41
+ if (failure) throw failure.reason;
42
+ }
43
+
27
44
  /**
28
45
  * This behaves like {@link streamText} from the "ai" package except that
29
46
  * it add context based on the userId and threadId and saves the input and
@@ -32,8 +49,14 @@ import { errorToString, willContinue } from "./utils.js";
32
49
  * to a thread (and optionally userId).
33
50
  */
34
51
  export async function streamText<
35
- TOOLS extends ToolSet,
36
- OUTPUT extends Output<any, any, any> = never,
52
+ AgentTools extends ToolSet,
53
+ TOOLS extends ToolSet | undefined = undefined,
54
+ OUTPUT extends AISDKOutput.Output<any, any, any> = AISDKOutput.Output<
55
+ string,
56
+ string,
57
+ never
58
+ >,
59
+ RUNTIME_CONTEXT extends Context = Context,
37
60
  >(
38
61
  ctx: ActionCtx,
39
62
  component: AgentComponent,
@@ -41,17 +64,7 @@ export async function streamText<
41
64
  * The arguments to the streamText function, similar to the ai sdk's
42
65
  * {@link streamText} function, along with Agent prompt options.
43
66
  */
44
- streamTextArgs: AgentPrompt &
45
- Omit<
46
- Parameters<typeof streamTextAi<TOOLS, OUTPUT>>[0],
47
- "model" | "prompt" | "messages"
48
- > & {
49
- /**
50
- * The tools to use for the tool calls. This will override tools specified
51
- * in the Agent constructor or createThread / continueThread.
52
- */
53
- tools?: TOOLS;
54
- },
67
+ streamTextArgs: StreamingTextArgs<AgentTools, TOOLS, OUTPUT, RUNTIME_CONTEXT>,
55
68
  /**
56
69
  * The {@link ContextOptions} and {@link StorageOptions}
57
70
  * options to use for fetching contextual messages and saving input/output messages.
@@ -73,17 +86,49 @@ export async function streamText<
73
86
  saveStreamDeltas?: boolean | StreamingOptions;
74
87
  agentForToolCtx?: Agent;
75
88
  },
76
- ): Promise<StreamTextResult<TOOLS, OUTPUT> & GenerationOutputMetadata> {
89
+ ): Promise<
90
+ StreamTextResult<
91
+ TOOLS extends undefined ? AgentTools : TOOLS,
92
+ RUNTIME_CONTEXT,
93
+ OUTPUT
94
+ > &
95
+ GenerationOutputMetadata
96
+ > {
97
+ type Tools = TOOLS extends undefined ? AgentTools : TOOLS;
77
98
  const { threadId } = options ?? {};
78
99
  const { args, userId, order, stepOrder, promptMessageId, ...call } =
79
- await startGeneration(ctx, component, streamTextArgs, options);
100
+ await startGeneration<
101
+ StreamingTextArgs<AgentTools, TOOLS, OUTPUT, RUNTIME_CONTEXT>,
102
+ Tools,
103
+ object,
104
+ RUNTIME_CONTEXT
105
+ >(
106
+ ctx,
107
+ component,
108
+ streamTextArgs,
109
+ options,
110
+ "streamText",
111
+ );
80
112
 
81
- const steps: StepResult<TOOLS>[] = [];
113
+ const steps: StepResult<Tools, RUNTIME_CONTEXT>[] = [];
114
+ let initialResponseMessages: ModelMessage[] = [];
115
+ let initialResponseMessagesSaved = false;
116
+ const responseMessagesForStep = (
117
+ step: StepResult<Tools, RUNTIME_CONTEXT>,
118
+ ) => [
119
+ ...(initialResponseMessagesSaved ? [] : initialResponseMessages),
120
+ ...step.response.messages,
121
+ ];
82
122
 
83
123
  // Track the final step for atomic save with stream finish (issue #181).
84
124
  // Only used when streamText awaits stream consumption itself; the
85
125
  // `returnImmediately` path saves inline instead (see onStepFinish below).
86
- let pendingFinalStep: StepResult<TOOLS> | undefined;
126
+ let pendingFinalStep:
127
+ | {
128
+ step: StepResult<Tools, RUNTIME_CONTEXT>;
129
+ responseMessages: ModelMessage[];
130
+ }
131
+ | undefined;
87
132
 
88
133
  // Whether streamText will await stream consumption before returning.
89
134
  // When false (saveStreamDeltas.returnImmediately === true), we cannot
@@ -123,7 +168,7 @@ export async function streamText<
123
168
  )
124
169
  : undefined;
125
170
 
126
- const result = streamTextAi({
171
+ const result = streamTextAi<Tools, RUNTIME_CONTEXT, OUTPUT>({
127
172
  ...args,
128
173
  abortSignal: streamer?.abortController.signal ?? args.abortSignal,
129
174
  experimental_transform: mergeTransforms(
@@ -136,7 +181,19 @@ export async function streamText<
136
181
  await streamer?.fail(errorToString(error.error));
137
182
  return streamTextArgs.onError?.(error);
138
183
  },
184
+ onAbort: async (event) => {
185
+ const reason = args.abortSignal?.reason
186
+ ? errorToString(args.abortSignal.reason)
187
+ : "streamText aborted";
188
+ await runAbortCleanup({
189
+ failCall: () => call.fail(reason),
190
+ failStreamer: async () => streamer?.fail(reason),
191
+ onAbort: () => streamTextArgs.onAbort?.(event),
192
+ });
193
+ },
139
194
  prepareStep: async (options) => {
195
+ if (options.stepNumber === 0)
196
+ initialResponseMessages = [...options.responseMessages];
140
197
  const result = await streamTextArgs.prepareStep?.(options);
141
198
  if (result) {
142
199
  const model = result.model ?? options.model;
@@ -150,7 +207,7 @@ export async function streamText<
150
207
  }
151
208
  return undefined;
152
209
  },
153
- onStepFinish: async (step) => {
210
+ onStepEnd: async (step) => {
154
211
  steps.push(step);
155
212
  const createPendingMessage = await willContinue(steps, args.stopWhen);
156
213
  if (!createPendingMessage && streamer) {
@@ -159,22 +216,36 @@ export async function streamText<
159
216
  if (willAwaitStream) {
160
217
  // We're about to `await stream` below — defer the save so it
161
218
  // happens atomically with stream finish (issue #181).
162
- pendingFinalStep = step;
219
+ pendingFinalStep = {
220
+ step,
221
+ responseMessages: responseMessagesForStep(step),
222
+ };
163
223
  } else {
164
224
  // returnImmediately path: streamText is about to return without
165
225
  // awaiting consumption, so the deferred-save block below won't
166
226
  // see this step. Save inline now (issue #265).
167
227
  const finishStreamId = await streamer.getOrCreateStreamId();
168
- await call.save({ step }, false, finishStreamId);
228
+ await call.save(
229
+ { step, responseMessages: responseMessagesForStep(step) },
230
+ false,
231
+ finishStreamId,
232
+ );
233
+ initialResponseMessagesSaved = true;
169
234
  }
170
235
  } else {
171
- await call.save({ step }, createPendingMessage);
236
+ await call.save(
237
+ { step, responseMessages: responseMessagesForStep(step) },
238
+ createPendingMessage,
239
+ );
240
+ initialResponseMessagesSaved = true;
172
241
  }
173
- return args.onStepFinish?.(step);
242
+ return (streamTextArgs.onStepEnd ?? streamTextArgs.onStepFinish)?.(step);
174
243
  },
175
- }) as StreamTextResult<TOOLS, OUTPUT>;
244
+ } as Parameters<
245
+ typeof streamTextAi<Tools, RUNTIME_CONTEXT, OUTPUT>
246
+ >[0]) as StreamTextResult<Tools, RUNTIME_CONTEXT, OUTPUT>;
176
247
  const stream = streamer?.consumeStream(
177
- result.toUIMessageStream<AIUIMessage<TOOLS>>(),
248
+ result.toUIMessageStream<AIUIMessage<Tools>>(),
178
249
  );
179
250
  if (willAwaitStream) {
180
251
  try {
@@ -188,7 +259,7 @@ export async function streamText<
188
259
  // Save the deferred final step if it was already generated but not yet persisted
189
260
  if (pendingFinalStep) {
190
261
  try {
191
- await call.save({ step: pendingFinalStep }, false);
262
+ await call.save(pendingFinalStep, false);
192
263
  } catch (saveError) {
193
264
  console.error("Failed to save deferred final step:", saveError);
194
265
  }
@@ -201,7 +272,7 @@ export async function streamText<
201
272
  // If we deferred the final step save, do it now with atomic stream finish.
202
273
  if (pendingFinalStep && streamer) {
203
274
  const finishStreamId = await streamer.getOrCreateStreamId();
204
- await call.save({ step: pendingFinalStep }, false, finishStreamId);
275
+ await call.save(pendingFinalStep, false, finishStreamId);
205
276
  }
206
277
  const metadata: GenerationOutputMetadata = {
207
278
  promptMessageId,
@@ -1,5 +1,5 @@
1
1
  import { beforeEach, describe, expect, test } from "vitest";
2
- import { createThread } from "./index.js";
2
+ import { createThread } from "../../client/threads.js";
3
3
  import type { GenericSchema, SchemaDefinition } from "convex/server";
4
4
  import { streamText } from "ai";
5
5
  import { components, initConvexTest } from "./setup.test.js";
@@ -12,10 +12,9 @@ import {
12
12
  import {
13
13
  getParts,
14
14
  deriveUIMessagesFromDeltas,
15
- deriveUIMessagesFromTextStreamParts,
16
15
  } from "../deltas.js";
17
16
  import type { TestConvex } from "convex-test";
18
- import type { StreamDelta, StreamMessage } from "../validators.js";
17
+ import type { StreamDelta, StreamMessage } from "../../validators.js";
19
18
  import { dedupeMessages } from "../react/useUIMessages.js";
20
19
 
21
20
  const defaultTestOptions = {
@@ -152,9 +151,7 @@ describe("HTTP Streaming Initiation", () => {
152
151
 
153
152
  // Verify we can reconstruct the text from deltas
154
153
  const { parts } = getParts(deltas);
155
- const textParts = parts.filter(
156
- (p: any) => p.type === "text-delta",
157
- );
154
+ const textParts = parts.filter((p: any) => p.type === "text-delta");
158
155
  expect(textParts.length).toBeGreaterThan(0);
159
156
  });
160
157
  });
@@ -265,10 +262,9 @@ describe("Stream Exclusion Logic", () => {
265
262
  await streamer2.addParts([{ type: "start" }]);
266
263
 
267
264
  // Default list: only streaming
268
- const defaultStreams = await ctx.runQuery(
269
- components.agent.streams.list,
270
- { threadId },
271
- );
265
+ const defaultStreams = await ctx.runQuery(components.agent.streams.list, {
266
+ threadId,
267
+ });
272
268
  expect(defaultStreams).toHaveLength(1);
273
269
  expect(defaultStreams[0].status).toBe("streaming");
274
270
  expect(defaultStreams[0].order).toBe(1);
@@ -325,10 +321,10 @@ describe("Stream Exclusion Logic", () => {
325
321
  expect(finishedStreams[0].status).toBe("finished");
326
322
 
327
323
  // Query for only aborted
328
- const abortedStreams = await ctx.runQuery(
329
- components.agent.streams.list,
330
- { threadId, statuses: ["aborted"] },
331
- );
324
+ const abortedStreams = await ctx.runQuery(components.agent.streams.list, {
325
+ threadId,
326
+ statuses: ["aborted"],
327
+ });
332
328
  expect(abortedStreams).toHaveLength(1);
333
329
  expect(abortedStreams[0].status).toBe("aborted");
334
330
 
@@ -487,10 +483,10 @@ describe("Delta Stream Consumption", () => {
487
483
  expect(laterParts.length).toBeLessThanOrEqual(allParts.length);
488
484
 
489
485
  // Fetching from the end cursor should yield nothing
490
- const noDeltas = await ctx.runQuery(
491
- components.agent.streams.listDeltas,
492
- { threadId, cursors: [{ cursor: endCursor, streamId }] },
493
- );
486
+ const noDeltas = await ctx.runQuery(components.agent.streams.listDeltas, {
487
+ threadId,
488
+ cursors: [{ cursor: endCursor, streamId }],
489
+ });
494
490
  expect(noDeltas).toHaveLength(0);
495
491
  });
496
492
  });
@@ -526,16 +522,13 @@ describe("Delta Stream Consumption", () => {
526
522
  const id2 = streamer2.streamId!;
527
523
 
528
524
  // Fetch deltas for both streams simultaneously
529
- const deltas = await ctx.runQuery(
530
- components.agent.streams.listDeltas,
531
- {
532
- threadId,
533
- cursors: [
534
- { cursor: 0, streamId: id1 },
535
- { cursor: 0, streamId: id2 },
536
- ],
537
- },
538
- );
525
+ const deltas = await ctx.runQuery(components.agent.streams.listDeltas, {
526
+ threadId,
527
+ cursors: [
528
+ { cursor: 0, streamId: id1 },
529
+ { cursor: 0, streamId: id2 },
530
+ ],
531
+ });
539
532
 
540
533
  // Should have deltas for both streams
541
534
  const s1Deltas = deltas.filter((d) => d.streamId === id1);
@@ -568,10 +561,10 @@ describe("Delta Stream Consumption", () => {
568
561
  threadId,
569
562
  statuses: ["finished"],
570
563
  });
571
- const deltas = await ctx.runQuery(
572
- components.agent.streams.listDeltas,
573
- { threadId, cursors: [{ cursor: 0, streamId }] },
574
- );
564
+ const deltas = await ctx.runQuery(components.agent.streams.listDeltas, {
565
+ threadId,
566
+ cursors: [{ cursor: 0, streamId }],
567
+ });
575
568
 
576
569
  // Derive UI messages
577
570
  const uiMessages = await deriveUIMessagesFromDeltas(
@@ -615,10 +608,10 @@ describe("Delta Stream Consumption", () => {
615
608
  await streamer.consumeStream(result.toUIMessageStream());
616
609
  const streamId = streamer.streamId!;
617
610
 
618
- const deltas = await ctx.runQuery(
619
- components.agent.streams.listDeltas,
620
- { threadId, cursors: [{ cursor: 0, streamId }] },
621
- );
611
+ const deltas = await ctx.runQuery(components.agent.streams.listDeltas, {
612
+ threadId,
613
+ cursors: [{ cursor: 0, streamId }],
614
+ });
622
615
  const { parts } = getParts(deltas);
623
616
 
624
617
  // Compressed: all text-deltas for one text section should be merged
@@ -677,77 +670,6 @@ describe("Delta Stream Consumption", () => {
677
670
  expect((parts[0] as { type: string }).type).toBe("new");
678
671
  expect(cursor).toBe(6);
679
672
  });
680
-
681
- test("TextStreamPart format delta reconstruction with tool calls", () => {
682
- const streamId = "s1";
683
- const streamMessage: StreamMessage = {
684
- streamId,
685
- order: 1,
686
- stepOrder: 0,
687
- status: "streaming",
688
- };
689
- const deltas: StreamDelta[] = [
690
- {
691
- streamId,
692
- start: 0,
693
- end: 1,
694
- parts: [{ type: "text-delta", id: "txt-0", text: "Let me call a tool. " }],
695
- },
696
- {
697
- streamId,
698
- start: 1,
699
- end: 2,
700
- parts: [
701
- {
702
- type: "tool-call",
703
- toolCallId: "tc1",
704
- toolName: "search",
705
- input: { query: "hello" },
706
- },
707
- ],
708
- },
709
- {
710
- streamId,
711
- start: 2,
712
- end: 3,
713
- parts: [
714
- {
715
- type: "tool-result",
716
- toolCallId: "tc1",
717
- toolName: "search",
718
- output: "Found 3 results",
719
- },
720
- ],
721
- },
722
- {
723
- streamId,
724
- start: 3,
725
- end: 4,
726
- parts: [
727
- { type: "text-delta", id: "txt-1", text: "Here are the results." },
728
- ],
729
- },
730
- ];
731
-
732
- const [messages, , changed] = deriveUIMessagesFromTextStreamParts(
733
- "thread1",
734
- [streamMessage],
735
- [],
736
- deltas,
737
- );
738
-
739
- expect(messages).toHaveLength(1);
740
- expect(changed).toBe(true);
741
-
742
- const msg = messages[0];
743
- expect(msg.text).toContain("Let me call a tool.");
744
- expect(msg.text).toContain("Here are the results.");
745
-
746
- const toolParts = msg.parts.filter((p: any) =>
747
- p.type.startsWith("tool-"),
748
- );
749
- expect(toolParts.length).toBeGreaterThan(0);
750
- });
751
673
  });
752
674
 
753
675
  // ============================================================================
@@ -879,18 +801,21 @@ describe("Fallback Behavior", () => {
879
801
  order: 0,
880
802
  stepOrder: 0,
881
803
  status: "streaming",
804
+ format: "UIMessageChunk",
882
805
  };
883
806
  const finishedMsg: StreamMessage = {
884
807
  streamId: "s2",
885
808
  order: 1,
886
809
  stepOrder: 0,
887
810
  status: "finished",
811
+ format: "UIMessageChunk",
888
812
  };
889
813
  const abortedMsg: StreamMessage = {
890
814
  streamId: "s3",
891
815
  order: 2,
892
816
  stepOrder: 0,
893
817
  status: "aborted",
818
+ format: "UIMessageChunk",
894
819
  };
895
820
 
896
821
  const msgs = await deriveUIMessagesFromDeltas(
@@ -1001,10 +926,10 @@ describe("Stream Lifecycle Integration", () => {
1001
926
  expect(finished).toHaveLength(1);
1002
927
 
1003
928
  // 4. Derive UI messages from stored deltas
1004
- const deltas = await ctx.runQuery(
1005
- components.agent.streams.listDeltas,
1006
- { threadId, cursors: [{ cursor: 0, streamId }] },
1007
- );
929
+ const deltas = await ctx.runQuery(components.agent.streams.listDeltas, {
930
+ threadId,
931
+ cursors: [{ cursor: 0, streamId }],
932
+ });
1008
933
  const uiMessages = await deriveUIMessagesFromDeltas(
1009
934
  threadId,
1010
935
  finished,
@@ -1056,10 +981,10 @@ describe("Stream Lifecycle Integration", () => {
1056
981
  expect(aborted[0].status).toBe("aborted");
1057
982
 
1058
983
  // Even aborted streams have their deltas stored
1059
- const deltas = await ctx.runQuery(
1060
- components.agent.streams.listDeltas,
1061
- { threadId, cursors: [{ cursor: 0, streamId }] },
1062
- );
984
+ const deltas = await ctx.runQuery(components.agent.streams.listDeltas, {
985
+ threadId,
986
+ cursors: [{ cursor: 0, streamId }],
987
+ });
1063
988
  expect(deltas.length).toBeGreaterThan(0);
1064
989
  });
1065
990
  });
@@ -1,10 +1,14 @@
1
- import { beforeEach, describe, expect, test } from "vitest";
2
- import { createThread } from "./index.js";
1
+ import { beforeEach, describe, expect, test, vi } from "vitest";
2
+ import { createThread } from "../../client/threads.js";
3
+ import type { MutationCtx } from "../../client/types.js";
3
4
  import type { GenericSchema, SchemaDefinition } from "convex/server";
4
5
  import { streamText } from "ai";
5
6
  import { components, initConvexTest } from "./setup.test.js";
6
7
  import { mockModel } from "./mockModel.js";
7
- import { compressUIMessageChunks, DeltaStreamer } from "./streaming.js";
8
+ import {
9
+ compressUIMessageChunks,
10
+ DeltaStreamer,
11
+ } from "./streaming.js";
8
12
  import { getParts } from "../deltas.js";
9
13
  import type { TestConvex } from "convex-test";
10
14
 
@@ -181,6 +185,176 @@ describe("DeltaStreamer", () => {
181
185
  }
182
186
  });
183
187
  });
184
- // TODO: test errors & aborted states
188
+ test("honors a signal that was aborted before construction", async () => {
189
+ const abortController = new AbortController();
190
+ abortController.abort();
191
+
192
+ await t.run(async (ctx) => {
193
+ const streamer = new DeltaStreamer<string>(
194
+ components.agent,
195
+ ctx,
196
+ { ...defaultTestOptions, abortSignal: abortController.signal },
197
+ { ...testMetadata, threadId },
198
+ );
199
+
200
+ expect(streamer.abortController.signal.aborted).toBe(true);
201
+ await streamer.addParts(["ignored"]);
202
+ expect(streamer.streamId).toBeUndefined();
203
+ await expect(streamer.getOrCreateStreamId()).rejects.toThrow(
204
+ "Cannot create a stream after it has been aborted",
205
+ );
206
+ });
207
+ });
208
+
209
+ test("shares signal and fail cleanup while stream creation is in flight", async () => {
210
+ let resolveCreate!: (streamId: string) => void;
211
+ const creatingStream = new Promise<string>((resolve) => {
212
+ resolveCreate = resolve;
213
+ });
214
+ let resolveAbort!: () => void;
215
+ const abortingStream = new Promise<void>((resolve) => {
216
+ resolveAbort = resolve;
217
+ });
218
+ const runMutation = vi
219
+ .fn()
220
+ .mockImplementationOnce(() => creatingStream)
221
+ .mockImplementationOnce(() => abortingStream);
222
+ const abortController = new AbortController();
223
+ const streamer = new DeltaStreamer<string>(
224
+ components.agent,
225
+ { runMutation } as unknown as MutationCtx,
226
+ { ...defaultTestOptions, abortSignal: abortController.signal },
227
+ { ...testMetadata, threadId },
228
+ );
229
+
230
+ const streamId = streamer.getStreamId();
231
+ abortController.abort();
232
+ const failing = streamer.fail("creation failed");
233
+ let failSettled = false;
234
+ void failing.then(() => {
235
+ failSettled = true;
236
+ });
237
+ resolveCreate("stream-1");
238
+ await expect(streamId).resolves.toBe("stream-1");
239
+ await vi.waitFor(() => expect(runMutation).toHaveBeenCalledTimes(2));
240
+
241
+ expect(failSettled).toBe(false);
242
+ resolveAbort();
243
+ await failing;
244
+
245
+ expect(runMutation).toHaveBeenNthCalledWith(
246
+ 2,
247
+ components.agent.streams.abort,
248
+ { streamId: "stream-1", reason: "abortSignal" },
249
+ );
250
+ });
251
+
252
+ test("aborts the component stream when a delta write fails", async () => {
253
+ const runMutation = vi
254
+ .fn()
255
+ .mockResolvedValueOnce("stream-1")
256
+ .mockRejectedValueOnce(new Error("delta failed"))
257
+ .mockResolvedValueOnce(undefined);
258
+ let abortReason: string | undefined;
259
+ const streamer = new DeltaStreamer<string>(
260
+ components.agent,
261
+ { runMutation } as unknown as MutationCtx,
262
+ {
263
+ ...defaultTestOptions,
264
+ onAsyncAbort: async (reason) => {
265
+ abortReason = reason;
266
+ },
267
+ },
268
+ { ...testMetadata, threadId },
269
+ );
270
+
271
+ await streamer.addParts(["A"]);
272
+ await streamer.finish();
273
+
274
+ expect(abortReason).toBe("delta failed");
275
+ expect(runMutation).toHaveBeenNthCalledWith(
276
+ 3,
277
+ components.agent.streams.abort,
278
+ { streamId: "stream-1", reason: "delta failed" },
279
+ );
280
+ });
281
+
282
+ test("surfaces pending-message cleanup failure after aborting the stream", async () => {
283
+ const pendingMessageFailure = new Error("pending message cleanup failed");
284
+ let resolveComponentAbort!: () => void;
285
+ const componentAborted = new Promise<void>((resolve) => {
286
+ resolveComponentAbort = resolve;
287
+ });
288
+ const runMutation = vi
289
+ .fn()
290
+ .mockResolvedValueOnce("stream-1")
291
+ .mockRejectedValueOnce(new Error("delta failed"))
292
+ .mockImplementationOnce(() => {
293
+ resolveComponentAbort();
294
+ return Promise.resolve();
295
+ });
296
+ const streamer = new DeltaStreamer<string>(
297
+ components.agent,
298
+ { runMutation } as unknown as MutationCtx,
299
+ {
300
+ ...defaultTestOptions,
301
+ onAsyncAbort: async () => {
302
+ throw pendingMessageFailure;
303
+ },
304
+ },
305
+ { ...testMetadata, threadId },
306
+ );
307
+ const stream = {
308
+ async *[Symbol.asyncIterator]() {
309
+ yield "A";
310
+ await componentAborted;
311
+ },
312
+ } as unknown as Parameters<typeof streamer.consumeStream>[0];
313
+
314
+ await expect(streamer.consumeStream(stream)).rejects.toBe(
315
+ pendingMessageFailure,
316
+ );
317
+
318
+ expect(runMutation).toHaveBeenNthCalledWith(
319
+ 3,
320
+ components.agent.streams.abort,
321
+ { streamId: "stream-1", reason: "delta failed" },
322
+ );
323
+ });
324
+
325
+ test("finishes external abort cleanup when the active delta write fails", async () => {
326
+ const pendingMessageFailure = new Error("pending message cleanup failed");
327
+ let rejectDelta!: (error: Error) => void;
328
+ const deltaWrite = new Promise<never>((_, reject) => {
329
+ rejectDelta = reject;
330
+ });
331
+ const runMutation = vi
332
+ .fn()
333
+ .mockResolvedValueOnce("stream-1")
334
+ .mockImplementationOnce(() => deltaWrite)
335
+ .mockResolvedValueOnce(undefined);
336
+ const streamer = new DeltaStreamer<string>(
337
+ components.agent,
338
+ { runMutation } as unknown as MutationCtx,
339
+ {
340
+ ...defaultTestOptions,
341
+ onAsyncAbort: async () => {
342
+ throw pendingMessageFailure;
343
+ },
344
+ },
345
+ { ...testMetadata, threadId },
346
+ );
347
+
348
+ await streamer.addParts(["A"]);
349
+ const failing = streamer.fail("external abort");
350
+ rejectDelta(new Error("delta failed"));
351
+
352
+ await expect(failing).rejects.toBe(pendingMessageFailure);
353
+ expect(runMutation).toHaveBeenNthCalledWith(
354
+ 3,
355
+ components.agent.streams.abort,
356
+ { streamId: "stream-1", reason: "external abort" },
357
+ );
358
+ });
185
359
  // TODO: test fetching partial stream data - syncStreams w/ cursors
186
360
  });