@convex-dev/agent 0.6.0-alpha.1 → 0.6.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 (181) hide show
  1. package/package.json +1 -1
  2. package/src/UIMessages.ts +135 -0
  3. package/src/client/index.ts +56 -14
  4. package/src/client/search.test.ts +4 -5
  5. package/src/client/search.ts +45 -4
  6. package/src/client/streaming.integration.test.ts +1206 -0
  7. package/src/client/streaming.ts +9 -2
  8. package/src/mapping.test.ts +136 -71
  9. package/src/mapping.ts +81 -28
  10. package/dist/UIMessages.d.ts +0 -46
  11. package/dist/UIMessages.d.ts.map +0 -1
  12. package/dist/UIMessages.js +0 -546
  13. package/dist/UIMessages.js.map +0 -1
  14. package/dist/client/createTool.d.ts +0 -167
  15. package/dist/client/createTool.d.ts.map +0 -1
  16. package/dist/client/createTool.js +0 -116
  17. package/dist/client/createTool.js.map +0 -1
  18. package/dist/client/defaultComponent.d.ts +0 -11
  19. package/dist/client/defaultComponent.d.ts.map +0 -1
  20. package/dist/client/defaultComponent.js +0 -7
  21. package/dist/client/defaultComponent.js.map +0 -1
  22. package/dist/client/definePlaygroundAPI.d.ts +0 -1725
  23. package/dist/client/definePlaygroundAPI.d.ts.map +0 -1
  24. package/dist/client/definePlaygroundAPI.js +0 -271
  25. package/dist/client/definePlaygroundAPI.js.map +0 -1
  26. package/dist/client/files.d.ts +0 -69
  27. package/dist/client/files.d.ts.map +0 -1
  28. package/dist/client/files.js +0 -181
  29. package/dist/client/files.js.map +0 -1
  30. package/dist/client/index.d.ts +0 -2091
  31. package/dist/client/index.d.ts.map +0 -1
  32. package/dist/client/index.js +0 -895
  33. package/dist/client/index.js.map +0 -1
  34. package/dist/client/messages.d.ts +0 -461
  35. package/dist/client/messages.d.ts.map +0 -1
  36. package/dist/client/messages.js +0 -106
  37. package/dist/client/messages.js.map +0 -1
  38. package/dist/client/mockModel.d.ts +0 -42
  39. package/dist/client/mockModel.d.ts.map +0 -1
  40. package/dist/client/mockModel.js +0 -182
  41. package/dist/client/mockModel.js.map +0 -1
  42. package/dist/client/saveInputMessages.d.ts +0 -20
  43. package/dist/client/saveInputMessages.d.ts.map +0 -1
  44. package/dist/client/saveInputMessages.js +0 -58
  45. package/dist/client/saveInputMessages.js.map +0 -1
  46. package/dist/client/search.d.ts +0 -493
  47. package/dist/client/search.d.ts.map +0 -1
  48. package/dist/client/search.js +0 -425
  49. package/dist/client/search.js.map +0 -1
  50. package/dist/client/start.d.ts +0 -84
  51. package/dist/client/start.d.ts.map +0 -1
  52. package/dist/client/start.js +0 -185
  53. package/dist/client/start.js.map +0 -1
  54. package/dist/client/streamText.d.ts +0 -46
  55. package/dist/client/streamText.d.ts.map +0 -1
  56. package/dist/client/streamText.js +0 -117
  57. package/dist/client/streamText.js.map +0 -1
  58. package/dist/client/streaming.d.ts +0 -3778
  59. package/dist/client/streaming.d.ts.map +0 -1
  60. package/dist/client/streaming.js +0 -314
  61. package/dist/client/streaming.js.map +0 -1
  62. package/dist/client/threads.d.ts +0 -46
  63. package/dist/client/threads.d.ts.map +0 -1
  64. package/dist/client/threads.js +0 -49
  65. package/dist/client/threads.js.map +0 -1
  66. package/dist/client/types.d.ts +0 -461
  67. package/dist/client/types.d.ts.map +0 -1
  68. package/dist/client/types.js +0 -2
  69. package/dist/client/types.js.map +0 -1
  70. package/dist/client/utils.d.ts +0 -4
  71. package/dist/client/utils.d.ts.map +0 -1
  72. package/dist/client/utils.js +0 -21
  73. package/dist/client/utils.js.map +0 -1
  74. package/dist/component/_generated/api.d.ts +0 -48
  75. package/dist/component/_generated/api.d.ts.map +0 -1
  76. package/dist/component/_generated/api.js +0 -31
  77. package/dist/component/_generated/api.js.map +0 -1
  78. package/dist/component/_generated/component.d.ts +0 -3120
  79. package/dist/component/_generated/component.d.ts.map +0 -1
  80. package/dist/component/_generated/component.js +0 -11
  81. package/dist/component/_generated/component.js.map +0 -1
  82. package/dist/component/_generated/dataModel.d.ts +0 -46
  83. package/dist/component/_generated/dataModel.d.ts.map +0 -1
  84. package/dist/component/_generated/dataModel.js +0 -11
  85. package/dist/component/_generated/dataModel.js.map +0 -1
  86. package/dist/component/_generated/server.d.ts +0 -121
  87. package/dist/component/_generated/server.d.ts.map +0 -1
  88. package/dist/component/_generated/server.js +0 -78
  89. package/dist/component/_generated/server.js.map +0 -1
  90. package/dist/component/apiKeys.d.ts +0 -11
  91. package/dist/component/apiKeys.d.ts.map +0 -1
  92. package/dist/component/apiKeys.js +0 -69
  93. package/dist/component/apiKeys.js.map +0 -1
  94. package/dist/component/convex.config.d.ts +0 -3
  95. package/dist/component/convex.config.d.ts.map +0 -1
  96. package/dist/component/convex.config.js +0 -3
  97. package/dist/component/convex.config.js.map +0 -1
  98. package/dist/component/files.d.ts +0 -97
  99. package/dist/component/files.d.ts.map +0 -1
  100. package/dist/component/files.js +0 -190
  101. package/dist/component/files.js.map +0 -1
  102. package/dist/component/messages.d.ts +0 -3851
  103. package/dist/component/messages.d.ts.map +0 -1
  104. package/dist/component/messages.js +0 -757
  105. package/dist/component/messages.js.map +0 -1
  106. package/dist/component/schema.d.ts +0 -8026
  107. package/dist/component/schema.d.ts.map +0 -1
  108. package/dist/component/schema.js +0 -147
  109. package/dist/component/schema.js.map +0 -1
  110. package/dist/component/streams.d.ts +0 -128
  111. package/dist/component/streams.d.ts.map +0 -1
  112. package/dist/component/streams.js +0 -413
  113. package/dist/component/streams.js.map +0 -1
  114. package/dist/component/threads.d.ts +0 -115
  115. package/dist/component/threads.d.ts.map +0 -1
  116. package/dist/component/threads.js +0 -208
  117. package/dist/component/threads.js.map +0 -1
  118. package/dist/component/users.d.ts +0 -52
  119. package/dist/component/users.d.ts.map +0 -1
  120. package/dist/component/users.js +0 -229
  121. package/dist/component/users.js.map +0 -1
  122. package/dist/component/vector/index.d.ts +0 -61
  123. package/dist/component/vector/index.d.ts.map +0 -1
  124. package/dist/component/vector/index.js +0 -146
  125. package/dist/component/vector/index.js.map +0 -1
  126. package/dist/component/vector/tables.d.ts +0 -58
  127. package/dist/component/vector/tables.d.ts.map +0 -1
  128. package/dist/component/vector/tables.js +0 -56
  129. package/dist/component/vector/tables.js.map +0 -1
  130. package/dist/deltas.d.ts +0 -43
  131. package/dist/deltas.d.ts.map +0 -1
  132. package/dist/deltas.js +0 -446
  133. package/dist/deltas.js.map +0 -1
  134. package/dist/mapping.d.ts +0 -72
  135. package/dist/mapping.d.ts.map +0 -1
  136. package/dist/mapping.js +0 -677
  137. package/dist/mapping.js.map +0 -1
  138. package/dist/react/SmoothText.d.ts +0 -5
  139. package/dist/react/SmoothText.d.ts.map +0 -1
  140. package/dist/react/SmoothText.js +0 -6
  141. package/dist/react/SmoothText.js.map +0 -1
  142. package/dist/react/index.d.ts +0 -25
  143. package/dist/react/index.d.ts.map +0 -1
  144. package/dist/react/index.js +0 -70
  145. package/dist/react/index.js.map +0 -1
  146. package/dist/react/optimisticallySendMessage.d.ts +0 -42
  147. package/dist/react/optimisticallySendMessage.d.ts.map +0 -1
  148. package/dist/react/optimisticallySendMessage.js +0 -74
  149. package/dist/react/optimisticallySendMessage.js.map +0 -1
  150. package/dist/react/types.d.ts +0 -12
  151. package/dist/react/types.d.ts.map +0 -1
  152. package/dist/react/types.js +0 -2
  153. package/dist/react/types.js.map +0 -1
  154. package/dist/react/useDeltaStreams.d.ts +0 -10
  155. package/dist/react/useDeltaStreams.d.ts.map +0 -1
  156. package/dist/react/useDeltaStreams.js +0 -106
  157. package/dist/react/useDeltaStreams.js.map +0 -1
  158. package/dist/react/useSmoothText.d.ts +0 -27
  159. package/dist/react/useSmoothText.d.ts.map +0 -1
  160. package/dist/react/useSmoothText.js +0 -68
  161. package/dist/react/useSmoothText.js.map +0 -1
  162. package/dist/react/useStreamingUIMessages.d.ts +0 -22
  163. package/dist/react/useStreamingUIMessages.d.ts.map +0 -1
  164. package/dist/react/useStreamingUIMessages.js +0 -92
  165. package/dist/react/useStreamingUIMessages.js.map +0 -1
  166. package/dist/react/useThreadMessages.d.ts +0 -104
  167. package/dist/react/useThreadMessages.d.ts.map +0 -1
  168. package/dist/react/useThreadMessages.js +0 -148
  169. package/dist/react/useThreadMessages.js.map +0 -1
  170. package/dist/react/useUIMessages.d.ts +0 -96
  171. package/dist/react/useUIMessages.d.ts.map +0 -1
  172. package/dist/react/useUIMessages.js +0 -108
  173. package/dist/react/useUIMessages.js.map +0 -1
  174. package/dist/shared.d.ts +0 -26
  175. package/dist/shared.d.ts.map +0 -1
  176. package/dist/shared.js +0 -67
  177. package/dist/shared.js.map +0 -1
  178. package/dist/validators.d.ts +0 -24516
  179. package/dist/validators.d.ts.map +0 -1
  180. package/dist/validators.js +0 -475
  181. package/dist/validators.js.map +0 -1
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  "email": "support@convex.dev",
8
8
  "url": "https://github.com/get-convex/agent/issues"
9
9
  },
10
- "version": "0.6.0-alpha.1",
10
+ "version": "0.6.0",
11
11
  "license": "Apache-2.0",
12
12
  "keywords": [
13
13
  "convex",
package/src/UIMessages.ts CHANGED
@@ -396,6 +396,54 @@ function createAssistantUIMessage<
396
396
  ? ("streaming" as const)
397
397
  : lastMessage.status;
398
398
 
399
+ <<<<<<< Updated upstream
400
+ =======
401
+ // Extract approval parts from raw message content for UI rendering
402
+ type ApprovalPart =
403
+ | { type: "tool-approval-request"; approvalId: string; toolCallId: string }
404
+ | {
405
+ type: "tool-approval-response";
406
+ approvalId: string;
407
+ approved: boolean;
408
+ reason?: string;
409
+ };
410
+ const approvalParts: ApprovalPart[] = [];
411
+
412
+ // Extract execution-denied tool results from raw content for UI rendering
413
+ // (these are converted to text format for provider compatibility in start.ts)
414
+ type ExecutionDeniedInfo = {
415
+ toolCallId: string;
416
+ reason?: string;
417
+ };
418
+ const executionDeniedResults: ExecutionDeniedInfo[] = [];
419
+
420
+ for (const message of group) {
421
+ const rawContent = message.message?.content;
422
+ if (Array.isArray(rawContent)) {
423
+ for (const part of rawContent) {
424
+ if (
425
+ part.type === "tool-approval-request" ||
426
+ part.type === "tool-approval-response"
427
+ ) {
428
+ approvalParts.push(part as ApprovalPart);
429
+ }
430
+ // Check for execution-denied in tool-result outputs
431
+ if (
432
+ part.type === "tool-result" &&
433
+ typeof part.output === "object" &&
434
+ part.output !== null &&
435
+ (part.output as { type?: string }).type === "execution-denied"
436
+ ) {
437
+ executionDeniedResults.push({
438
+ toolCallId: part.toolCallId as string,
439
+ reason: (part.output as { reason?: string }).reason,
440
+ });
441
+ }
442
+ }
443
+ }
444
+ }
445
+
446
+ >>>>>>> Stashed changes
399
447
  // Collect all parts from all messages
400
448
  const allParts: UIMessage<METADATA, DATA_PARTS, TOOLS>["parts"] = [];
401
449
 
@@ -477,6 +525,12 @@ function createAssistantUIMessage<
477
525
  break;
478
526
  }
479
527
  case "tool-result": {
528
+ <<<<<<< Updated upstream
529
+ =======
530
+ // Note: execution-denied outputs are handled separately via pre-extraction
531
+ // from raw content (converted to text format for providers in start.ts).
532
+ // See executionDeniedResults processing at the end of this function.
533
+ >>>>>>> Stashed changes
480
534
  const typedPart = contentPart as unknown as ToolResultPart & {
481
535
  output: { type: string; value?: unknown; reason?: string };
482
536
  };
@@ -642,6 +696,87 @@ function createAssistantUIMessage<
642
696
  }
643
697
  }
644
698
 
699
+ <<<<<<< Updated upstream
700
+ =======
701
+ // Final output states that should not be overwritten by approval processing
702
+ const finalStates = new Set([
703
+ "output-available",
704
+ "output-error",
705
+ "output-denied",
706
+ ]);
707
+
708
+ // Process approval parts to update tool call states
709
+ for (const approvalPart of approvalParts) {
710
+ if (approvalPart.type === "tool-approval-request") {
711
+ const toolCallPart = allParts.find(
712
+ (part) =>
713
+ "toolCallId" in part && part.toolCallId === approvalPart.toolCallId,
714
+ ) as ToolUIPart | undefined;
715
+
716
+ if (toolCallPart) {
717
+ // Always set approval info (needed for response matching), but only
718
+ // update state if not in a final state
719
+ (toolCallPart as ToolUIPart & { approval?: object }).approval = {
720
+ id: approvalPart.approvalId,
721
+ };
722
+ if (!finalStates.has(toolCallPart.state)) {
723
+ toolCallPart.state = "approval-requested";
724
+ }
725
+ }
726
+ } else if (approvalPart.type === "tool-approval-response") {
727
+ const toolCallPart = allParts.find(
728
+ (part) =>
729
+ "approval" in part &&
730
+ (part as ToolUIPart & { approval?: { id: string } }).approval?.id ===
731
+ approvalPart.approvalId,
732
+ ) as ToolUIPart | undefined;
733
+
734
+ if (toolCallPart) {
735
+ // Always update approval info, but only update state if not in a final state
736
+ (toolCallPart as ToolUIPart & { approval?: object }).approval = {
737
+ id: approvalPart.approvalId,
738
+ approved: approvalPart.approved,
739
+ reason: approvalPart.reason,
740
+ };
741
+ if (!finalStates.has(toolCallPart.state)) {
742
+ if (approvalPart.approved) {
743
+ toolCallPart.state = "approval-responded";
744
+ } else {
745
+ toolCallPart.state = "output-denied";
746
+ }
747
+ }
748
+ }
749
+ }
750
+ }
751
+
752
+ // Process execution-denied results to update tool call states
753
+ for (const denied of executionDeniedResults) {
754
+ const toolCallPart = allParts.find(
755
+ (part) =>
756
+ "toolCallId" in part && part.toolCallId === denied.toolCallId,
757
+ ) as ToolUIPart | undefined;
758
+
759
+ if (toolCallPart) {
760
+ toolCallPart.state = "output-denied";
761
+ if (!("approval" in toolCallPart) || !toolCallPart.approval) {
762
+ (toolCallPart as ToolUIPart & { approval?: object }).approval = {
763
+ id: "",
764
+ approved: false,
765
+ reason: denied.reason,
766
+ };
767
+ } else {
768
+ const approval = (
769
+ toolCallPart as ToolUIPart & {
770
+ approval: { approved?: boolean; reason?: string };
771
+ }
772
+ ).approval;
773
+ approval.approved = false;
774
+ approval.reason = denied.reason;
775
+ }
776
+ }
777
+ }
778
+
779
+ >>>>>>> Stashed changes
645
780
  return {
646
781
  ...common,
647
782
  role: "assistant",
@@ -1070,10 +1070,35 @@ export class Agent<
1070
1070
  reason?: string;
1071
1071
  },
1072
1072
  ): Promise<{ messageId: string }> {
1073
- const promptMessageId = await this.getApprovalRequestMessageId(ctx, {
1074
- threadId: args.threadId,
1073
+ const { promptMessageId, existingResponseMessage } =
1074
+ await this.findApprovalContext(ctx, {
1075
+ threadId: args.threadId,
1076
+ approvalId: args.approvalId,
1077
+ });
1078
+
1079
+ const newPart = {
1080
+ type: "tool-approval-response" as const,
1075
1081
  approvalId: args.approvalId,
1076
- });
1082
+ approved: args.approved,
1083
+ reason: args.reason,
1084
+ };
1085
+
1086
+ // Merge into an existing approval-response message for this step
1087
+ // so the AI SDK sees a single tool message per step.
1088
+ if (existingResponseMessage) {
1089
+ const existingContent = existingResponseMessage.message?.content;
1090
+ const mergedContent = Array.isArray(existingContent)
1091
+ ? [...(existingContent as any[]), newPart]
1092
+ : [newPart];
1093
+ await this.updateMessage(ctx, {
1094
+ messageId: existingResponseMessage._id,
1095
+ patch: {
1096
+ message: { role: "tool", content: mergedContent },
1097
+ status: "success",
1098
+ },
1099
+ });
1100
+ return { messageId: existingResponseMessage._id };
1101
+ }
1077
1102
 
1078
1103
  const { messageId } = await this.saveMessage(ctx, {
1079
1104
  threadId: args.threadId,
@@ -1081,28 +1106,25 @@ export class Agent<
1081
1106
  skipEmbeddings: true,
1082
1107
  message: {
1083
1108
  role: "tool",
1084
- content: [
1085
- {
1086
- type: "tool-approval-response",
1087
- approvalId: args.approvalId,
1088
- approved: args.approved,
1089
- reason: args.reason,
1090
- },
1091
- ],
1109
+ content: [newPart],
1092
1110
  },
1093
1111
  });
1094
1112
  return { messageId };
1095
1113
  }
1096
1114
 
1097
- private async getApprovalRequestMessageId(
1115
+ private async findApprovalContext(
1098
1116
  ctx: MutationCtx,
1099
1117
  args: { threadId: string; approvalId: string },
1100
- ): Promise<string> {
1118
+ ): Promise<{
1119
+ promptMessageId: string;
1120
+ existingResponseMessage: MessageDoc | undefined;
1121
+ }> {
1101
1122
  // NOTE: This pagination returns messages in descending order (newest first).
1102
1123
  // The "already handled" check (tool-approval-response) relies on seeing
1103
1124
  // responses before their corresponding requests. If the pagination order
1104
1125
  // changes, this logic will need to be updated.
1105
1126
  let cursor: string | null = null;
1127
+ let existingResponseMessage: MessageDoc | undefined;
1106
1128
  do {
1107
1129
  const page = await this.listMessages(ctx, {
1108
1130
  threadId: args.threadId,
@@ -1111,6 +1133,19 @@ export class Agent<
1111
1133
  for (const message of page.page) {
1112
1134
  const content = message.message?.content;
1113
1135
  if (!Array.isArray(content)) continue;
1136
+ // Check if this assistant message starts a different approval step.
1137
+ // If so, any response message we've seen so far belongs to a newer
1138
+ // step — reset it so we don't merge across step boundaries.
1139
+ if (
1140
+ message.message?.role === "assistant" &&
1141
+ content.some(
1142
+ (p: any) =>
1143
+ p.type === "tool-approval-request" &&
1144
+ p.approvalId !== args.approvalId,
1145
+ )
1146
+ ) {
1147
+ existingResponseMessage = undefined;
1148
+ }
1114
1149
  for (const part of content) {
1115
1150
  const typedPart = part as { type?: unknown; approvalId?: unknown };
1116
1151
  if (
@@ -1119,11 +1154,18 @@ export class Agent<
1119
1154
  ) {
1120
1155
  throw new Error(`Approval ${args.approvalId} was already handled`);
1121
1156
  }
1157
+ // Track the most recent tool-approval-response message for merging
1158
+ if (
1159
+ typedPart.type === "tool-approval-response" &&
1160
+ !existingResponseMessage
1161
+ ) {
1162
+ existingResponseMessage = message;
1163
+ }
1122
1164
  if (
1123
1165
  typedPart.type === "tool-approval-request" &&
1124
1166
  typedPart.approvalId === args.approvalId
1125
1167
  ) {
1126
- return message._id;
1168
+ return { promptMessageId: message._id, existingResponseMessage };
1127
1169
  }
1128
1170
  }
1129
1171
  }
@@ -292,7 +292,7 @@ describe("search.ts", () => {
292
292
  expect(result[1]._id).toBe("2");
293
293
  });
294
294
 
295
- it("should filter out tool calls with approval request but NO approval response", () => {
295
+ it("should keep tool calls with approval request but NO approval response (auto-deny handles them)", () => {
296
296
  const messages: MessageDoc[] = [
297
297
  {
298
298
  _id: "1",
@@ -321,19 +321,18 @@ describe("search.ts", () => {
321
321
 
322
322
  const result = filterOutOrphanedToolMessages(messages);
323
323
  expect(result).toHaveLength(1);
324
- // The assistant message should have the tool-call filtered out
324
+ // The assistant message should keep the tool-call (auto-deny resolves it downstream)
325
325
  const assistantContent = result[0].message?.content;
326
326
  expect(Array.isArray(assistantContent)).toBe(true);
327
327
  if (Array.isArray(assistantContent)) {
328
- // Text and approval-request should remain, but tool-call should be filtered
329
- expect(assistantContent).toHaveLength(2);
328
+ expect(assistantContent).toHaveLength(3);
330
329
  expect(assistantContent.find((p) => p.type === "text")).toBeDefined();
331
330
  expect(
332
331
  assistantContent.find((p) => p.type === "tool-approval-request"),
333
332
  ).toBeDefined();
334
333
  expect(
335
334
  assistantContent.find((p) => p.type === "tool-call"),
336
- ).toBeUndefined();
335
+ ).toBeDefined();
337
336
  }
338
337
  });
339
338
 
@@ -30,11 +30,33 @@ import type {
30
30
  } from "./types.js";
31
31
  import { inlineMessagesFiles } from "./files.js";
32
32
  import {
33
+ autoDenyUnresolvedApprovals,
33
34
  docsToModelMessages,
34
- mergeApprovalResponseMessages,
35
35
  toModelMessage,
36
36
  } from "../mapping.js";
37
37
 
38
+ /**
39
+ * Filter out approval parts from messages before sending to AI providers.
40
+ * Providers like Anthropic don't understand tool-approval-request and
41
+ * tool-approval-response parts and will error if they are included.
42
+ */
43
+ function filterApprovalPartsForProvider(messages: ModelMessage[]): ModelMessage[] {
44
+ return messages.map((message) => {
45
+ if (!Array.isArray(message.content)) {
46
+ return message;
47
+ }
48
+ const filteredContent = message.content.filter(
49
+ (part) =>
50
+ (part as { type: string }).type !== "tool-approval-request" &&
51
+ (part as { type: string }).type !== "tool-approval-response",
52
+ );
53
+ return {
54
+ ...message,
55
+ content: filteredContent,
56
+ } as typeof message;
57
+ });
58
+ }
59
+
38
60
  const DEFAULT_VECTOR_SCORE_THRESHOLD = 0.0;
39
61
  // 10k characters should be more than enough for most cases, and stays under
40
62
  // the 8k token limit for some models.
@@ -234,7 +256,11 @@ export async function fetchRecentAndSearchMessages(
234
256
  searchMessages = filterOutOrphanedToolMessages(
235
257
  sorted(searchResults.filter((m) => !included?.has(m._id))),
236
258
  );
259
+ searchMessages = filterApprovalPartsForProvider(searchMessages);
237
260
  }
261
+
262
+ // filter out approval parts before returning
263
+ searchMessages = filterApprovalPartsForProvider(searchMessages);
238
264
  // Ensure we don't include tool messages without a corresponding tool call
239
265
  return { recentMessages, searchMessages };
240
266
  }
@@ -289,6 +315,12 @@ export function filterOutOrphanedToolMessages(docs: MessageDoc[]) {
289
315
  return approvalId !== undefined && approvalResponseIds.has(approvalId);
290
316
  };
291
317
 
318
+ // Helper: check if tool call has a pending approval request
319
+ // (auto-deny handles these downstream, so they must survive the filter)
320
+ const hasApprovalRequest = (toolCallId: string) => {
321
+ return approvalRequestsByToolCallId.has(toolCallId);
322
+ };
323
+
292
324
  for (const doc of docs) {
293
325
  if (
294
326
  doc.message?.role === "assistant" &&
@@ -298,7 +330,8 @@ export function filterOutOrphanedToolMessages(docs: MessageDoc[]) {
298
330
  (p) =>
299
331
  p.type !== "tool-call" ||
300
332
  toolResultIds.has(p.toolCallId) ||
301
- hasApprovalResponse(p.toolCallId),
333
+ hasApprovalResponse(p.toolCallId) ||
334
+ hasApprovalRequest(p.toolCallId),
302
335
  );
303
336
  if (content.length) {
304
337
  result.push({
@@ -641,13 +674,13 @@ export async function fetchContextWithPrompt(
641
674
  const inputPrompt = promptArray.map(toModelMessage);
642
675
  const existingResponses = docsToModelMessages(existingResponseDocs);
643
676
 
644
- const allMessages = mergeApprovalResponseMessages([
677
+ const allMessages = [
645
678
  ...search,
646
679
  ...recent,
647
680
  ...inputMessages,
648
681
  ...inputPrompt,
649
682
  ...existingResponses,
650
- ]);
683
+ ];
651
684
  let processedMessages = args.contextHandler
652
685
  ? await args.contextHandler(ctx, {
653
686
  allMessages,
@@ -661,11 +694,19 @@ export async function fetchContextWithPrompt(
661
694
  })
662
695
  : allMessages;
663
696
 
697
+ // Post-process: auto-deny unresolved approvals so the AI SDK sees a
698
+ // complete history. Applied after contextHandler so custom handlers
699
+ // don't need to handle this.
700
+ processedMessages = autoDenyUnresolvedApprovals(processedMessages);
701
+
664
702
  // Process messages to inline localhost files (if not, file urls pointing to localhost will be sent to LLM providers)
665
703
  if (process.env.CONVEX_CLOUD_URL?.startsWith("http://127.0.0.1")) {
666
704
  processedMessages = await inlineMessagesFiles(processedMessages);
667
705
  }
668
706
 
707
+ // Filter out approval parts before sending to AI providers
708
+ processedMessages = filterApprovalPartsForProvider(processedMessages);
709
+
669
710
  return {
670
711
  messages: processedMessages,
671
712
  order: promptMessage?.order,