@convex-dev/agent 0.6.0-alpha.0 → 0.6.0-beta.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 (75) hide show
  1. package/MIGRATION.md +153 -0
  2. package/dist/UIMessages.d.ts.map +1 -1
  3. package/dist/UIMessages.js +88 -0
  4. package/dist/UIMessages.js.map +1 -1
  5. package/dist/client/createTool.d.ts +18 -21
  6. package/dist/client/createTool.d.ts.map +1 -1
  7. package/dist/client/createTool.js +3 -2
  8. package/dist/client/createTool.js.map +1 -1
  9. package/dist/client/definePlaygroundAPI.d.ts +31 -31
  10. package/dist/client/index.d.ts +70 -23
  11. package/dist/client/index.d.ts.map +1 -1
  12. package/dist/client/index.js +126 -1
  13. package/dist/client/index.js.map +1 -1
  14. package/dist/client/messages.d.ts +9 -9
  15. package/dist/client/mockModel.d.ts.map +1 -1
  16. package/dist/client/mockModel.js +9 -2
  17. package/dist/client/mockModel.js.map +1 -1
  18. package/dist/client/search.d.ts +9 -9
  19. package/dist/client/search.d.ts.map +1 -1
  20. package/dist/client/search.js +12 -2
  21. package/dist/client/search.js.map +1 -1
  22. package/dist/client/start.d.ts +1 -1
  23. package/dist/client/start.d.ts.map +1 -1
  24. package/dist/client/start.js +29 -15
  25. package/dist/client/start.js.map +1 -1
  26. package/dist/client/streamText.d.ts.map +1 -1
  27. package/dist/client/streamText.js +37 -3
  28. package/dist/client/streamText.js.map +1 -1
  29. package/dist/client/streaming.d.ts +78 -67
  30. package/dist/client/streaming.d.ts.map +1 -1
  31. package/dist/client/streaming.js +64 -28
  32. package/dist/client/streaming.js.map +1 -1
  33. package/dist/client/types.d.ts +13 -12
  34. package/dist/client/types.d.ts.map +1 -1
  35. package/dist/component/_generated/component.d.ts +1 -0
  36. package/dist/component/_generated/component.d.ts.map +1 -1
  37. package/dist/component/messages.d.ts +107 -106
  38. package/dist/component/messages.d.ts.map +1 -1
  39. package/dist/component/messages.js +13 -3
  40. package/dist/component/messages.js.map +1 -1
  41. package/dist/component/schema.d.ts +40 -40
  42. package/dist/component/streams.d.ts +4 -4
  43. package/dist/component/threads.d.ts +17 -17
  44. package/dist/component/users.d.ts +3 -3
  45. package/dist/component/vector/index.d.ts +1 -1
  46. package/dist/deltas.d.ts.map +1 -1
  47. package/dist/deltas.js +0 -1
  48. package/dist/deltas.js.map +1 -1
  49. package/dist/mapping.d.ts +22 -0
  50. package/dist/mapping.d.ts.map +1 -1
  51. package/dist/mapping.js +99 -5
  52. package/dist/mapping.js.map +1 -1
  53. package/dist/react/useDeltaStreams.d.ts.map +1 -1
  54. package/dist/react/useDeltaStreams.js +5 -0
  55. package/dist/react/useDeltaStreams.js.map +1 -1
  56. package/dist/validators.d.ts +13 -13
  57. package/package.json +4 -2
  58. package/src/UIMessages.ts +126 -0
  59. package/src/client/approval.test.ts +494 -0
  60. package/src/client/createTool.ts +50 -52
  61. package/src/client/index.ts +170 -1
  62. package/src/client/mockModel.ts +9 -2
  63. package/src/client/search.test.ts +4 -5
  64. package/src/client/search.ts +18 -2
  65. package/src/client/start.ts +42 -25
  66. package/src/client/streamText.ts +36 -3
  67. package/src/client/streaming.integration.test.ts +1206 -0
  68. package/src/client/streaming.ts +67 -31
  69. package/src/client/types.ts +14 -12
  70. package/src/component/_generated/component.ts +53 -64
  71. package/src/component/messages.ts +12 -2
  72. package/src/deltas.ts +0 -1
  73. package/src/mapping.test.ts +143 -1
  74. package/src/mapping.ts +131 -6
  75. package/src/react/useDeltaStreams.ts +6 -0
package/src/mapping.ts CHANGED
@@ -139,6 +139,97 @@ export function docsToModelMessages(messages: MessageDoc[]): ModelMessage[] {
139
139
  .map(toModelMessage);
140
140
  }
141
141
 
142
+ /**
143
+ * Scan messages for unresolved `tool-approval-request` parts and inject
144
+ * synthetic `tool-approval-response` denials so that the AI SDK receives
145
+ * a complete history (every tool-call has a corresponding result or denial).
146
+ *
147
+ * This handles the case where a user sends a new message instead of
148
+ * resolving pending approvals — the old approvals are auto-denied rather
149
+ * than silently dropped.
150
+ */
151
+ export function autoDenyUnresolvedApprovals(
152
+ messages: ModelMessage[],
153
+ ): ModelMessage[] {
154
+ // Collect all approval requests: approvalId → { toolCallId, messageIndex }
155
+ const requests = new Map<
156
+ string,
157
+ { toolCallId: string; messageIndex: number }
158
+ >();
159
+ // Collect all resolved approval IDs
160
+ const resolvedIds = new Set<string>();
161
+
162
+ for (let i = 0; i < messages.length; i++) {
163
+ const msg = messages[i];
164
+ if (!Array.isArray(msg.content)) continue;
165
+ for (const part of msg.content as any[]) {
166
+ if (part.type === "tool-approval-request") {
167
+ requests.set(part.approvalId, {
168
+ toolCallId: part.toolCallId,
169
+ messageIndex: i,
170
+ });
171
+ } else if (part.type === "tool-approval-response") {
172
+ resolvedIds.add(part.approvalId);
173
+ }
174
+ }
175
+ }
176
+
177
+ // Find unresolved approvals
178
+ const unresolved: Array<{
179
+ approvalId: string;
180
+ toolCallId: string;
181
+ messageIndex: number;
182
+ }> = [];
183
+ for (const [approvalId, info] of requests) {
184
+ if (!resolvedIds.has(approvalId)) {
185
+ unresolved.push({ approvalId, ...info });
186
+ }
187
+ }
188
+
189
+ if (unresolved.length === 0) {
190
+ return messages;
191
+ }
192
+
193
+ // Group unresolved approvals by the assistant message index they came from
194
+ const byMessageIndex = new Map<
195
+ number,
196
+ Array<{ approvalId: string; toolCallId: string }>
197
+ >();
198
+ for (const entry of unresolved) {
199
+ console.warn(
200
+ `Auto-denying unresolved tool approval ${entry.approvalId} ` +
201
+ `(toolCallId: ${entry.toolCallId}): new generation started`,
202
+ );
203
+ let group = byMessageIndex.get(entry.messageIndex);
204
+ if (!group) {
205
+ group = [];
206
+ byMessageIndex.set(entry.messageIndex, group);
207
+ }
208
+ group.push(entry);
209
+ }
210
+
211
+ // Build result by inserting synthetic denial messages after each relevant
212
+ // assistant message
213
+ const result: ModelMessage[] = [];
214
+ for (let i = 0; i < messages.length; i++) {
215
+ result.push(messages[i]);
216
+ const group = byMessageIndex.get(i);
217
+ if (group) {
218
+ result.push({
219
+ role: "tool",
220
+ content: group.map((entry) => ({
221
+ type: "tool-approval-response" as const,
222
+ approvalId: entry.approvalId,
223
+ approved: false,
224
+ reason: "auto-denied: new generation started",
225
+ })),
226
+ });
227
+ }
228
+ }
229
+
230
+ return result;
231
+ }
232
+
142
233
  export function serializeUsage(usage: LanguageModelUsage): Usage {
143
234
  return {
144
235
  promptTokens: usage.inputTokens ?? 0,
@@ -206,11 +297,49 @@ export function toModelMessageWarnings(
206
297
  }) as any;
207
298
  }
208
299
 
300
+ /**
301
+ * Serialize explicitly provided response messages for a step.
302
+ * Used by the streaming/generation loop where the caller tracks which
303
+ * messages are new via slicing.
304
+ */
305
+ export async function serializeResponseMessages<TOOLS extends ToolSet>(
306
+ ctx: ActionCtx,
307
+ component: AgentComponent,
308
+ step: StepResult<TOOLS>,
309
+ model: ModelOrMetadata | undefined,
310
+ responseMessages: ModelMessage[],
311
+ ): Promise<{ messages: MessageWithMetadata[] }> {
312
+ return serializeStepMessages(ctx, component, step, model, responseMessages);
313
+ }
314
+
315
+ /**
316
+ * Serialize the new messages from a step using a heuristic to determine
317
+ * which response messages are new (last 1-2 messages).
318
+ */
209
319
  export async function serializeNewMessagesInStep<TOOLS extends ToolSet>(
210
320
  ctx: ActionCtx,
211
321
  component: AgentComponent,
212
322
  step: StepResult<TOOLS>,
213
323
  model: ModelOrMetadata | undefined,
324
+ ): Promise<{ messages: MessageWithMetadata[] }> {
325
+ const hasToolMessage = step.response.messages.at(-1)?.role === "tool";
326
+ let messagesToSerialize: ModelMessage[];
327
+ if (hasToolMessage) {
328
+ messagesToSerialize = step.response.messages.slice(-2);
329
+ } else if (step.content.length) {
330
+ messagesToSerialize = step.response.messages.slice(-1);
331
+ } else {
332
+ messagesToSerialize = [{ role: "assistant" as const, content: [] }];
333
+ }
334
+ return serializeStepMessages(ctx, component, step, model, messagesToSerialize);
335
+ }
336
+
337
+ async function serializeStepMessages<TOOLS extends ToolSet>(
338
+ ctx: ActionCtx,
339
+ component: AgentComponent,
340
+ step: StepResult<TOOLS>,
341
+ model: ModelOrMetadata | undefined,
342
+ messagesToSerialize: ModelMessage[],
214
343
  ): Promise<{ messages: MessageWithMetadata[] }> {
215
344
  // If there are tool results, there's another message with the tool results
216
345
  // ref: https://github.com/vercel/ai/blob/main/packages/ai/src/generate-text/to-response-messages.ts#L120
@@ -228,13 +357,9 @@ export async function serializeNewMessagesInStep<TOOLS extends ToolSet>(
228
357
  sources: hasToolMessage ? undefined : step.sources,
229
358
  } satisfies Omit<MessageWithMetadata, "message" | "text" | "fileIds">;
230
359
  const toolFields = { sources: step.sources };
360
+
231
361
  const messages: MessageWithMetadata[] = await Promise.all(
232
- (hasToolMessage
233
- ? step.response.messages.slice(-2)
234
- : step.content.length
235
- ? step.response.messages.slice(-1)
236
- : [{ role: "assistant" as const, content: [] }]
237
- ).map(async (msg): Promise<MessageWithMetadata> => {
362
+ messagesToSerialize.map(async (msg): Promise<MessageWithMetadata> => {
238
363
  const { message, fileIds } = await serializeMessage(ctx, component, msg);
239
364
  return parse(vMessageWithMetadata, {
240
365
  message,
@@ -83,6 +83,12 @@ export function useDeltaStreams<
83
83
  ),
84
84
  );
85
85
 
86
+ // When no active streams remain, clear the stale state so we stop
87
+ // returning old streaming UIMessages.
88
+ if (streamMessages !== undefined && streamMessages.length === 0) {
89
+ state.deltaStreams = undefined;
90
+ }
91
+
86
92
  // Get the deltas for all the active streams, if any.
87
93
  const cursorQuery = useQuery(
88
94
  query,