@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,36 +1,87 @@
1
1
  import {
2
- stepCountIs,
3
- type CallSettings,
2
+ isStepCount,
4
3
  type GenerateObjectResult,
5
4
  type IdGenerator,
6
5
  type LanguageModel,
6
+ type Instructions,
7
7
  type ModelMessage,
8
8
  type StepResult,
9
9
  type StopCondition,
10
10
  type ToolSet,
11
11
  } from "ai";
12
+ import type { Context } from "@ai-sdk/provider-utils";
12
13
  import {
13
14
  serializeResponseMessages,
14
15
  serializeObjectResult,
15
16
  } from "../mapping.js";
16
17
  import { embedMessages, fetchContextWithPrompt } from "./search.js";
17
- import type { ActionCtx, AgentComponent, Config, Options } from "./types.js";
18
- import type { Message, MessageDoc } from "../validators.js";
18
+ import type {
19
+ ActionCtx,
20
+ AgentCallSettings,
21
+ AgentComponent,
22
+ Config,
23
+ Options,
24
+ } from "./types.js";
25
+ import type { Message, MessageDoc } from "../../validators.js";
19
26
  import {
20
27
  getModelName,
21
28
  getProviderName,
22
29
  type ModelOrMetadata,
23
- } from "../shared.js";
30
+ } from "../../shared.js";
24
31
  import { wrapTools, type ToolCtx } from "./createTool.js";
25
- import type { Agent } from "./index.js";
32
+ import type { Agent } from "../index.js";
26
33
  import { assert, omit } from "convex-helpers";
27
34
  import { saveInputMessages } from "./saveInputMessages.js";
28
35
  import type { GenericActionCtx, GenericDataModel } from "convex/server";
29
36
 
37
+ export function resolveUsageModel(
38
+ toSave:
39
+ | { step: { model?: ModelOrMetadata } }
40
+ | { object: unknown },
41
+ activeModel: ModelOrMetadata,
42
+ ): ModelOrMetadata {
43
+ return "step" in toSave ? (toSave.step.model ?? activeModel) : activeModel;
44
+ }
45
+
46
+ type RawRequestResponseInclude = Record<string, boolean>;
47
+
48
+ /**
49
+ * The raw handler promises request and, for non-streaming calls, response
50
+ * bodies. Preserve explicit caller choices while opting in to those bodies.
51
+ */
52
+ function rawRequestResponseInclude(
53
+ args: {
54
+ experimental_include?: RawRequestResponseInclude;
55
+ include?: RawRequestResponseInclude;
56
+ },
57
+ enabled: boolean,
58
+ operation:
59
+ | "generateText"
60
+ | "streamText"
61
+ | "generateObject"
62
+ | "streamObject"
63
+ | undefined,
64
+ ): RawRequestResponseInclude | undefined {
65
+ if (!enabled) return undefined;
66
+
67
+ const requested = {
68
+ ...args.experimental_include,
69
+ ...args.include,
70
+ };
71
+ return {
72
+ ...requested,
73
+ requestBody: requested.requestBody ?? true,
74
+ ...((operation === "generateText" || operation === "generateObject")
75
+ ? { responseBody: requested.responseBody ?? true }
76
+ : {}),
77
+ };
78
+ }
79
+
30
80
  export async function startGeneration<
31
81
  T,
32
82
  Tools extends ToolSet = ToolSet,
33
83
  CustomCtx extends object = object,
84
+ RUNTIME_CONTEXT extends Context = Context,
34
85
  >(
35
86
  ctx: ActionCtx & CustomCtx,
36
87
  component: AgentComponent,
@@ -74,13 +125,20 @@ export async function startGeneration<
74
125
  * the promptMessageId message, if provided.
75
126
  */
76
127
  messages?: (ModelMessage | Message)[];
128
+ instructions?: Instructions;
129
+ /** @deprecated Use instructions. */
130
+ system?: Instructions;
131
+ allowSystemInMessages?: boolean;
77
132
  /**
78
133
  * The abort signal to be passed to the LLM call. If triggered, it will
79
134
  * mark the pending message as failed. If the generation is asynchronously
80
135
  * aborted, it will trigger this signal when detected.
81
136
  */
82
137
  abortSignal?: AbortSignal;
83
- stopWhen?: StopCondition<Tools> | Array<StopCondition<Tools>>;
138
+ runtimeContext?: RUNTIME_CONTEXT;
139
+ stopWhen?:
140
+ | StopCondition<Tools, RUNTIME_CONTEXT>
141
+ | Array<StopCondition<Tools, RUNTIME_CONTEXT>>;
84
142
  _internal?: { generateId?: IdGenerator };
85
143
  },
86
144
  {
@@ -94,14 +152,20 @@ export async function startGeneration<
94
152
  agentName: string;
95
153
  agentForToolCtx?: Agent;
96
154
  },
155
+ _operation?:
156
+ | "generateText"
157
+ | "streamText"
158
+ | "generateObject"
159
+ | "streamObject",
97
160
  ): Promise<{
98
161
  args: T & {
99
- system?: string;
162
+ instructions?: Instructions;
100
163
  model: LanguageModel;
101
164
  messages: ModelMessage[];
102
165
  prompt?: never;
103
166
  tools?: Tools;
104
- } & CallSettings;
167
+ runtimeContext?: RUNTIME_CONTEXT;
168
+ } & AgentCallSettings;
105
169
  order: number;
106
170
  stepOrder: number;
107
171
  userId: string | undefined;
@@ -109,7 +173,10 @@ export async function startGeneration<
109
173
  updateModel: (model: ModelOrMetadata | undefined) => void;
110
174
  save: <TOOLS extends ToolSet>(
111
175
  toSave:
112
- | { step: StepResult<TOOLS> }
176
+ | {
177
+ step: StepResult<TOOLS, RUNTIME_CONTEXT>;
178
+ responseMessages?: ModelMessage[];
179
+ }
113
180
  | { object: GenerateObjectResult<unknown> },
114
181
  createPendingMessage?: boolean,
115
182
  finishStreamId?: string,
@@ -132,6 +199,15 @@ export async function startGeneration<
132
199
  prompt: args.prompt,
133
200
  promptMessageId: args.promptMessageId,
134
201
  });
202
+ const allowSystemInMessages = args.allowSystemInMessages ?? true;
203
+ if (
204
+ !allowSystemInMessages &&
205
+ context.messages.some((message) => message.role === "system")
206
+ ) {
207
+ throw new Error(
208
+ "System messages in assembled message history require allowSystemInMessages: true. Use instructions for top-level system guidance.",
209
+ );
210
+ }
135
211
 
136
212
  const saveMessages = opts.storageOptions?.saveMessages ?? "promptAndOutput";
137
213
  const { promptMessageId, pendingMessage, savedMessages } =
@@ -159,13 +235,22 @@ export async function startGeneration<
159
235
  assert(model, "model is required");
160
236
  let activeModel: ModelOrMetadata = model;
161
237
 
162
- const fail = async (reason: string) => {
163
- if (pendingMessageId) {
164
- await ctx.runMutation(component.messages.finalizeMessage, {
165
- messageId: pendingMessageId,
166
- result: { status: "failed", error: reason },
167
- });
238
+ // Both the caller's AbortSignal listener and AI SDK's onAbort can report
239
+ // the same cancellation. Share the one finalization instead of racing two
240
+ // mutations for the pending message.
241
+ let pendingMessageFailure: Promise<void> | undefined;
242
+ const fail = (reason: string): Promise<void> => {
243
+ if (!pendingMessageId) return Promise.resolve();
244
+ if (!pendingMessageFailure) {
245
+ const messageId = pendingMessageId;
246
+ pendingMessageFailure = ctx
247
+ .runMutation(component.messages.finalizeMessage, {
248
+ messageId,
249
+ result: { status: "failed", error: reason },
250
+ })
251
+ .then(() => undefined);
168
252
  }
253
+ return pendingMessageFailure;
169
254
  };
170
255
  if (args.abortSignal) {
171
256
  const abortSignal = args.abortSignal;
@@ -185,34 +270,52 @@ export async function startGeneration<
185
270
  agent: opts.agentForToolCtx,
186
271
  } satisfies ToolCtx;
187
272
  const tools = wrapTools(toolCtx, args.tools) as Tools;
273
+ const argsWithoutSystem = omit(
274
+ args as typeof args & { system?: Instructions },
275
+ ["system"],
276
+ );
277
+ const {
278
+ promptMessageId: _promptMessageId,
279
+ messages: _messages,
280
+ prompt: _prompt,
281
+ instructions: _instructions,
282
+ onStepFinish: _onStepFinish,
283
+ ...aiCallArgs
284
+ } = argsWithoutSystem as typeof argsWithoutSystem & {
285
+ onStepFinish?: unknown;
286
+ };
287
+ const include = rawRequestResponseInclude(
288
+ aiCallArgs as {
289
+ experimental_include?: RawRequestResponseInclude;
290
+ include?: RawRequestResponseInclude;
291
+ },
292
+ Boolean(opts.rawRequestResponseHandler),
293
+ _operation,
294
+ );
188
295
  const aiArgs = {
189
296
  ...opts.callSettings,
190
297
  providerOptions: opts.providerOptions,
191
- ...omit(args, ["promptMessageId", "messages", "prompt"]),
298
+ ...aiCallArgs,
192
299
  model,
193
300
  messages: context.messages,
301
+ instructions: args.instructions ?? args.system,
302
+ allowSystemInMessages,
194
303
  stopWhen:
195
- args.stopWhen ?? (opts.maxSteps ? stepCountIs(opts.maxSteps) : undefined),
304
+ args.stopWhen ?? (opts.maxSteps ? isStepCount(opts.maxSteps) : undefined),
196
305
  tools,
197
- } as T & {
306
+ ...(include ? { include } : {}),
307
+ } as unknown as T & {
198
308
  model: LanguageModel;
199
309
  messages: ModelMessage[];
200
310
  prompt?: never;
201
311
  tools?: Tools;
202
312
  _internal?: { generateId?: IdGenerator };
203
- } & CallSettings;
313
+ } & AgentCallSettings;
204
314
  // NOTE: We intentionally do NOT override _internal.generateId here.
205
315
  // The AI SDK uses generateId() for many internal IDs (approval IDs,
206
316
  // tool execution IDs, message IDs, etc.) and they must be unique.
207
317
  // The pending message is linked via the explicit `pendingMessageId`
208
318
  // parameter passed to addMessages in the save closure.
209
- // Track how many response messages we've already saved across steps.
210
- // step.response.messages is cumulative — each step appends to it.
211
- // We need to know which messages are new in each step to serialize
212
- // only the new ones (important for tool approval flows where the SDK
213
- // may add extra messages like approval tool-results).
214
- let previousResponseMessageCount = 0;
215
-
216
319
  return {
217
320
  args: aiArgs,
218
321
  order: order ?? 0,
@@ -228,7 +331,10 @@ export async function startGeneration<
228
331
  fail,
229
332
  save: async <TOOLS extends ToolSet>(
230
333
  toSave:
231
- | { step: StepResult<TOOLS> }
334
+ | {
335
+ step: StepResult<TOOLS, RUNTIME_CONTEXT>;
336
+ responseMessages?: ModelMessage[];
337
+ }
232
338
  | { object: GenerateObjectResult<unknown> },
233
339
  createPendingMessage?: boolean,
234
340
  /**
@@ -247,17 +353,20 @@ export async function startGeneration<
247
353
  activeModel,
248
354
  );
249
355
  } else {
250
- const allResponseMessages = toSave.step.response.messages;
251
- const newResponseMessages = allResponseMessages.slice(
252
- previousResponseMessageCount,
253
- );
254
- previousResponseMessageCount = allResponseMessages.length;
356
+ const newResponseMessages =
357
+ toSave.responseMessages ?? toSave.step.response.messages;
358
+ // Even an empty completed step needs a durable assistant result so
359
+ // the pending message can be finalized at the storage boundary.
360
+ const responseMessagesToSave: ModelMessage[] =
361
+ newResponseMessages.length > 0
362
+ ? newResponseMessages
363
+ : [{ role: "assistant", content: [] }];
255
364
  serialized = await serializeResponseMessages(
256
365
  ctx,
257
366
  component,
258
367
  toSave.step,
259
368
  activeModel,
260
- newResponseMessages,
369
+ responseMessagesToSave,
261
370
  );
262
371
  }
263
372
  const embeddings = await embedMessages(
@@ -294,6 +403,7 @@ export async function startGeneration<
294
403
  );
295
404
  } else {
296
405
  pendingMessageId = lastMessage._id;
406
+ pendingMessageFailure = undefined;
297
407
  savedMessages.push(...saved.messages.slice(0, -1));
298
408
  }
299
409
  } else {
@@ -312,12 +422,13 @@ export async function startGeneration<
312
422
  });
313
423
  }
314
424
  if (opts.usageHandler && output.usage) {
425
+ const usageModel = resolveUsageModel(toSave, activeModel);
315
426
  await opts.usageHandler(ctx, {
316
427
  userId,
317
428
  threadId,
318
429
  agentName: opts.agentName,
319
- model: getModelName(activeModel),
320
- provider: getProviderName(activeModel),
430
+ model: getModelName(usageModel),
431
+ provider: getProviderName(usageModel),
321
432
  usage: output.usage,
322
433
  providerMetadata: output.providerMetadata,
323
434
  });
@@ -0,0 +1,223 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { Agent, createThread } from "../index.js";
3
+ import {
4
+ defineSchema,
5
+ type DataModelFromSchemaDefinition,
6
+ type ApiFromModules,
7
+ type ActionBuilder,
8
+ actionGeneric,
9
+ anyApi,
10
+ } from "convex/server";
11
+ import { v } from "convex/values";
12
+ import { components, initConvexTest } from "./setup.test.js";
13
+ import { mockModel } from "./mockModel.js";
14
+ import { runAbortCleanup } from "./streamText.js";
15
+
16
+ const schema = defineSchema({});
17
+ type DataModel = DataModelFromSchemaDefinition<typeof schema>;
18
+ const action = actionGeneric as ActionBuilder<DataModel, "public">;
19
+
20
+ const FINAL_TEXT = "Hello from the model";
21
+
22
+ const agent = new Agent(components.agent, {
23
+ name: "stream-test",
24
+ languageModel: mockModel({
25
+ content: [{ type: "text", text: FINAL_TEXT }],
26
+ }),
27
+ });
28
+
29
+ const emptyAgent = new Agent(components.agent, {
30
+ name: "empty-stream-test",
31
+ languageModel: mockModel({
32
+ content: [],
33
+ providerMetadata: { mock: { emptyResponse: true } },
34
+ }),
35
+ });
36
+
37
+ // Action that exercises streamText with saveStreamDeltas.returnImmediately=true.
38
+ // It consumes the stream after streamText returns, simulating the HTTP response
39
+ // path described in issue #265.
40
+ export const streamTextReturnImmediately = action({
41
+ args: { threadId: v.string() },
42
+ handler: async (ctx, { threadId }) => {
43
+ const result = await agent.streamText(
44
+ ctx,
45
+ { threadId },
46
+ { prompt: "Test" },
47
+ {
48
+ saveStreamDeltas: {
49
+ returnImmediately: true,
50
+ chunking: "word",
51
+ throttleMs: 0,
52
+ },
53
+ },
54
+ );
55
+ // Drain the stream the way an HTTP response would. This triggers
56
+ // onStepFinish for every step, including the final one.
57
+ await result.consumeStream();
58
+ return { ok: true };
59
+ },
60
+ });
61
+
62
+ export const streamTextEmptyAwaited = action({
63
+ args: { threadId: v.string() },
64
+ handler: async (ctx, { threadId }) => {
65
+ await emptyAgent.streamText(
66
+ ctx,
67
+ { threadId },
68
+ { prompt: "Test" },
69
+ { saveStreamDeltas: true },
70
+ );
71
+ return { ok: true };
72
+ },
73
+ });
74
+
75
+ export const streamTextEmptyReturnImmediately = action({
76
+ args: { threadId: v.string() },
77
+ handler: async (ctx, { threadId }) => {
78
+ const result = await emptyAgent.streamText(
79
+ ctx,
80
+ { threadId },
81
+ { prompt: "Test" },
82
+ {
83
+ saveStreamDeltas: {
84
+ returnImmediately: true,
85
+ throttleMs: 0,
86
+ },
87
+ },
88
+ );
89
+ await result.consumeStream();
90
+ return { ok: true };
91
+ },
92
+ });
93
+
94
+ const testApi: ApiFromModules<{
95
+ fns: {
96
+ streamTextReturnImmediately: typeof streamTextReturnImmediately;
97
+ streamTextEmptyAwaited: typeof streamTextEmptyAwaited;
98
+ streamTextEmptyReturnImmediately: typeof streamTextEmptyReturnImmediately;
99
+ };
100
+ }>["fns"] = anyApi["streamText.test"] as any;
101
+
102
+ describe("streamText with saveStreamDeltas.returnImmediately (issue #265)", () => {
103
+ test("persists the final assistant text to the messages table", async () => {
104
+ const t = initConvexTest(schema);
105
+ const threadId = await t.run(async (ctx) =>
106
+ createThread(ctx, components.agent, { userId: "u1" }),
107
+ );
108
+
109
+ await t.action(testApi.streamTextReturnImmediately, { threadId });
110
+
111
+ // Allow any background work scheduled by consumeStream to settle.
112
+ await t.finishAllScheduledFunctions(() => {});
113
+
114
+ const messages = await t.run(async (ctx) =>
115
+ agent.listMessages(ctx, {
116
+ threadId,
117
+ paginationOpts: { cursor: null, numItems: 50 },
118
+ }),
119
+ );
120
+
121
+ const assistantTextMessages = messages.page.filter(
122
+ (m) =>
123
+ m.message?.role === "assistant" &&
124
+ typeof m.text === "string" &&
125
+ m.text.length > 0,
126
+ );
127
+ expect(
128
+ assistantTextMessages.length,
129
+ "expected at least one persisted assistant message with text",
130
+ ).toBeGreaterThan(0);
131
+
132
+ const combined = assistantTextMessages.map((m) => m.text).join("");
133
+ expect(combined).toContain(FINAL_TEXT);
134
+
135
+ // The stream should be marked finished, not stuck in "streaming".
136
+ const stillStreaming = await t.run(async (ctx) =>
137
+ ctx.runQuery(components.agent.streams.list, {
138
+ threadId,
139
+ statuses: ["streaming"],
140
+ }),
141
+ );
142
+ expect(
143
+ stillStreaming,
144
+ "stream should not be stuck in 'streaming' status",
145
+ ).toHaveLength(0);
146
+ });
147
+ });
148
+
149
+ describe("streamText abort cleanup", () => {
150
+ test("attempts every cleanup and rethrows the first internal failure", async () => {
151
+ const calls: string[] = [];
152
+ const firstFailure = new Error("failed pending message cleanup");
153
+
154
+ await expect(
155
+ runAbortCleanup({
156
+ failCall: async () => {
157
+ calls.push("call.fail");
158
+ throw firstFailure;
159
+ },
160
+ failStreamer: async () => {
161
+ calls.push("streamer.fail");
162
+ throw new Error("failed stream cleanup");
163
+ },
164
+ onAbort: () => {
165
+ calls.push("user.onAbort");
166
+ },
167
+ }),
168
+ ).rejects.toBe(firstFailure);
169
+
170
+ expect(calls).toEqual(["call.fail", "streamer.fail", "user.onAbort"]);
171
+ });
172
+ });
173
+
174
+ describe("streamText with an empty final step (issue #274)", () => {
175
+ test.each([
176
+ ["awaited", testApi.streamTextEmptyAwaited],
177
+ ["returnImmediately", testApi.streamTextEmptyReturnImmediately],
178
+ ])(
179
+ "finalizes the pending assistant message in the %s path",
180
+ async (_, fn) => {
181
+ const t = initConvexTest(schema);
182
+ const threadId = await t.run(async (ctx) =>
183
+ createThread(ctx, components.agent, { userId: "u1" }),
184
+ );
185
+
186
+ await t.action(fn, { threadId });
187
+ await t.finishAllScheduledFunctions(() => {});
188
+
189
+ const messages = await t.run(async (ctx) =>
190
+ emptyAgent.listMessages(ctx, {
191
+ threadId,
192
+ paginationOpts: { cursor: null, numItems: 50 },
193
+ }),
194
+ );
195
+
196
+ expect(
197
+ messages.page.filter((message) => message.status === "pending"),
198
+ ).toHaveLength(0);
199
+ expect(messages.page).toContainEqual(
200
+ expect.objectContaining({
201
+ status: "success",
202
+ message: { role: "assistant", content: [] },
203
+ model: "mock-model-id",
204
+ provider: "mock-provider",
205
+ providerMetadata: { mock: { emptyResponse: true } },
206
+ usage: expect.objectContaining({
207
+ promptTokens: 3,
208
+ completionTokens: 10,
209
+ totalTokens: 13,
210
+ }),
211
+ }),
212
+ );
213
+
214
+ const stillStreaming = await t.run(async (ctx) =>
215
+ ctx.runQuery(components.agent.streams.list, {
216
+ threadId,
217
+ statuses: ["streaming"],
218
+ }),
219
+ );
220
+ expect(stillStreaming).toHaveLength(0);
221
+ },
222
+ );
223
+ });