@convex-dev/agent 0.7.1 → 0.7.3

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 (90) hide show
  1. package/README.md +5 -8
  2. package/dist/client/messages.d.ts +10 -0
  3. package/dist/client/messages.d.ts.map +1 -1
  4. package/dist/client/messages.js +1 -0
  5. package/dist/client/messages.js.map +1 -1
  6. package/dist/component/_generated/component.d.ts +10 -8
  7. package/dist/component/_generated/component.d.ts.map +1 -1
  8. package/dist/component/messages.d.ts +340 -338
  9. package/dist/component/messages.d.ts.map +1 -1
  10. package/dist/component/messages.js +74 -14
  11. package/dist/component/messages.js.map +1 -1
  12. package/dist/component/schema.d.ts +726 -622
  13. package/dist/component/schema.d.ts.map +1 -1
  14. package/dist/component/streams.d.ts +6 -1
  15. package/dist/component/streams.d.ts.map +1 -1
  16. package/dist/component/streams.js +14 -15
  17. package/dist/component/streams.js.map +1 -1
  18. package/dist/component/threads.d.ts.map +1 -1
  19. package/dist/component/threads.js +1 -0
  20. package/dist/component/threads.js.map +1 -1
  21. package/dist/component/vector/index.d.ts +9 -9
  22. package/dist/component/vector/index.d.ts.map +1 -1
  23. package/dist/component/vector/tables.d.ts +5 -5
  24. package/dist/component/vector/tables.d.ts.map +1 -1
  25. package/dist/component/vector/tables.js +1 -1
  26. package/dist/component/vector/tables.js.map +1 -1
  27. package/dist/errors.d.ts +2 -0
  28. package/dist/errors.d.ts.map +1 -0
  29. package/dist/errors.js +106 -0
  30. package/dist/errors.js.map +1 -0
  31. package/dist/validators.d.ts +1952 -1952
  32. package/dist/vercel/client/definePlaygroundAPI.d.ts +132 -132
  33. package/dist/vercel/client/messages.d.ts +50 -32
  34. package/dist/vercel/client/messages.d.ts.map +1 -1
  35. package/dist/vercel/client/messages.js +2 -0
  36. package/dist/vercel/client/messages.js.map +1 -1
  37. package/dist/vercel/client/search.d.ts +475 -32
  38. package/dist/vercel/client/search.d.ts.map +1 -1
  39. package/dist/vercel/client/search.js +34 -9
  40. package/dist/vercel/client/search.js.map +1 -1
  41. package/dist/vercel/client/start.d.ts.map +1 -1
  42. package/dist/vercel/client/start.js +1 -0
  43. package/dist/vercel/client/start.js.map +1 -1
  44. package/dist/vercel/client/streamText.d.ts +1 -2
  45. package/dist/vercel/client/streamText.d.ts.map +1 -1
  46. package/dist/vercel/client/streamText.js +79 -22
  47. package/dist/vercel/client/streamText.js.map +1 -1
  48. package/dist/vercel/client/streaming.d.ts +326 -305
  49. package/dist/vercel/client/streaming.d.ts.map +1 -1
  50. package/dist/vercel/client/streaming.js +140 -25
  51. package/dist/vercel/client/streaming.js.map +1 -1
  52. package/dist/vercel/client/types.d.ts +2 -0
  53. package/dist/vercel/client/types.d.ts.map +1 -1
  54. package/dist/vercel/client/types.js.map +1 -1
  55. package/dist/vercel/client/utils.d.ts +9 -5
  56. package/dist/vercel/client/utils.d.ts.map +1 -1
  57. package/dist/vercel/client/utils.js +13 -13
  58. package/dist/vercel/client/utils.js.map +1 -1
  59. package/dist/vercel/index.d.ts +91 -90
  60. package/dist/vercel/index.d.ts.map +1 -1
  61. package/dist/vercel/index.js +2 -0
  62. package/dist/vercel/index.js.map +1 -1
  63. package/package.json +16 -9
  64. package/src/client/messages.ts +12 -0
  65. package/src/component/_generated/component.ts +10 -0
  66. package/src/component/messages.test.ts +404 -0
  67. package/src/component/messages.ts +94 -12
  68. package/src/component/streams.test.ts +54 -1
  69. package/src/component/streams.ts +20 -17
  70. package/src/component/threads.test.ts +39 -0
  71. package/src/component/threads.ts +1 -0
  72. package/src/component/vector/tables.ts +1 -1
  73. package/src/errors.test.ts +76 -0
  74. package/src/errors.ts +113 -0
  75. package/src/vercel/client/deltaFlush.test.ts +106 -0
  76. package/src/vercel/client/index.test.ts +47 -0
  77. package/src/vercel/client/messages.ts +20 -0
  78. package/src/vercel/client/search.test.ts +183 -8
  79. package/src/vercel/client/search.ts +46 -12
  80. package/src/vercel/client/start.ts +1 -0
  81. package/src/vercel/client/streamText.test.ts +577 -18
  82. package/src/vercel/client/streamText.ts +84 -32
  83. package/src/vercel/client/streaming.integration.test.ts +40 -3
  84. package/src/vercel/client/streaming.test.ts +101 -7
  85. package/src/vercel/client/streaming.throttle.test.ts +144 -0
  86. package/src/vercel/client/streaming.ts +161 -25
  87. package/src/vercel/client/types.ts +2 -0
  88. package/src/vercel/client/utils.test.ts +97 -0
  89. package/src/vercel/client/utils.ts +15 -15
  90. package/src/vercel/index.ts +2 -0
@@ -20,6 +20,7 @@ import {
20
20
  fetchContextMessages,
21
21
  filterOutOrphanedToolMessages,
22
22
  getPromptArray,
23
+ trimIncompleteOldestOrder,
23
24
  } from "./search.js";
24
25
  import { components, initConvexTest } from "./setup.test.js";
25
26
  import { createThread } from "../../client/threads.js";
@@ -31,13 +32,14 @@ const createMockMessageDoc = (
31
32
  role: "user" | "assistant" | "tool" | "system",
32
33
  content: any,
33
34
  order: number = 1,
35
+ stepOrder: number = 0,
34
36
  ): MessageDoc => ({
35
37
  _id: id,
36
38
  _creationTime: Date.now(),
37
39
  userId: "test-user",
38
40
  threadId: "test-thread",
39
41
  order,
40
- stepOrder: order,
42
+ stepOrder,
41
43
  status: "success",
42
44
  tool: false,
43
45
  message: { role, content },
@@ -127,6 +129,27 @@ describe("search.ts", () => {
127
129
  });
128
130
  });
129
131
 
132
+ describe("trimIncompleteOldestOrder", () => {
133
+ it("keeps a window that begins at the start of an order", () => {
134
+ const messages = [
135
+ createMockMessageDoc("user", "user", "Question", 1, 0),
136
+ createMockMessageDoc("assistant", "assistant", "Answer", 1, 1),
137
+ ];
138
+
139
+ expect(trimIncompleteOldestOrder(messages)).toEqual(messages);
140
+ });
141
+
142
+ it("omits only the incomplete oldest order", () => {
143
+ const messages = [
144
+ createMockMessageDoc("partial", "assistant", "Partial", 1, 2),
145
+ createMockMessageDoc("next-user", "user", "Next question", 2, 0),
146
+ createMockMessageDoc("next-answer", "assistant", "Next answer", 2, 1),
147
+ ];
148
+
149
+ expect(trimIncompleteOldestOrder(messages)).toEqual(messages.slice(1));
150
+ });
151
+ });
152
+
130
153
  describe("filterOutOrphanedToolMessages", () => {
131
154
  it("should keep non-tool messages", () => {
132
155
  const messages: MessageDoc[] = [
@@ -442,6 +465,7 @@ describe("search.ts", () => {
442
465
  expect(result.length).toBe(2);
443
466
  expect(result[0]._id).toBe("1"); // Should be reversed back to asc order
444
467
  expect(result[1]._id).toBe("2");
468
+ expect(mockCtx.runQuery).toHaveBeenCalledTimes(1);
445
469
  });
446
470
 
447
471
  it("should skip recent messages when recentMessages is 0", async () => {
@@ -560,11 +584,13 @@ describe("search.ts", () => {
560
584
  _id: "ctx1",
561
585
  message: { role: "user", content: "Context message 1" },
562
586
  order: 1,
587
+ stepOrder: 0,
563
588
  } as MessageDoc,
564
589
  {
565
590
  _id: "ctx2",
566
591
  message: { role: "assistant", content: "Context response 1" },
567
592
  order: 2,
593
+ stepOrder: 0,
568
594
  } as MessageDoc,
569
595
  ];
570
596
 
@@ -590,6 +616,143 @@ describe("search.ts", () => {
590
616
  });
591
617
  });
592
618
 
619
+ it("should complete an order truncated by the recent messages limit", async () => {
620
+ const toolCallId = "call_issue_309";
621
+ vi.mocked(mockCtx.runQuery)
622
+ .mockResolvedValueOnce({
623
+ page: [
624
+ createMockMessageDoc(
625
+ "assistant-final",
626
+ "assistant",
627
+ [{ type: "text", text: "The result is 72." }],
628
+ 1,
629
+ 3,
630
+ ),
631
+ createMockMessageDoc(
632
+ "tool-result",
633
+ "tool",
634
+ [
635
+ {
636
+ type: "tool-result",
637
+ toolCallId,
638
+ toolName: "getWeather",
639
+ output: {
640
+ type: "json",
641
+ value: { temperature: 72 },
642
+ },
643
+ },
644
+ ],
645
+ 1,
646
+ 2,
647
+ ),
648
+ createMockMessageDoc(
649
+ "assistant-tool-call",
650
+ "assistant",
651
+ [
652
+ {
653
+ type: "tool-call",
654
+ toolCallId,
655
+ toolName: "getWeather",
656
+ input: { city: "New York" },
657
+ },
658
+ ],
659
+ 1,
660
+ 1,
661
+ ),
662
+ ],
663
+ continueCursor: "complete-order",
664
+ isDone: false,
665
+ })
666
+ .mockResolvedValueOnce({
667
+ page: [
668
+ createMockMessageDoc(
669
+ "user",
670
+ "user",
671
+ "What is the weather?",
672
+ 1,
673
+ 0,
674
+ ),
675
+ ],
676
+ continueCursor: "done",
677
+ isDone: true,
678
+ });
679
+
680
+ const result = await fetchContextWithPrompt(
681
+ mockCtx,
682
+ components.agent,
683
+ {
684
+ ...baseArgs,
685
+ prompt: "What should I do next?",
686
+ messages: undefined,
687
+ promptMessageId: undefined,
688
+ contextOptions: { recentMessages: 3 },
689
+ },
690
+ );
691
+
692
+ expect(result.messages.map((message) => message.role)).toEqual([
693
+ "user",
694
+ "assistant",
695
+ "tool",
696
+ "assistant",
697
+ "user",
698
+ ]);
699
+ expect(mockCtx.runQuery).toHaveBeenNthCalledWith(2, expect.anything(), {
700
+ threadId: "thread123",
701
+ excludeToolMessages: undefined,
702
+ paginationOpts: { numItems: 1, cursor: "complete-order" },
703
+ upToAndIncludingMessageId: undefined,
704
+ order: "desc",
705
+ statuses: ["success"],
706
+ });
707
+ });
708
+
709
+ it("should omit an order when its boundary cannot be fetched", async () => {
710
+ vi.mocked(mockCtx.runQuery)
711
+ .mockResolvedValueOnce({
712
+ page: [
713
+ createMockMessageDoc(
714
+ "assistant-final",
715
+ "assistant",
716
+ "Partial answer",
717
+ 1,
718
+ 2,
719
+ ),
720
+ ],
721
+ continueCursor: "complete-order",
722
+ isDone: false,
723
+ })
724
+ .mockResolvedValueOnce({
725
+ page: [
726
+ createMockMessageDoc(
727
+ "assistant-step",
728
+ "assistant",
729
+ "Still partial",
730
+ 1,
731
+ 1,
732
+ ),
733
+ ],
734
+ continueCursor: "done",
735
+ isDone: true,
736
+ });
737
+
738
+ const result = await fetchContextWithPrompt(
739
+ mockCtx,
740
+ components.agent,
741
+ {
742
+ ...baseArgs,
743
+ prompt: "New prompt",
744
+ messages: undefined,
745
+ promptMessageId: undefined,
746
+ contextOptions: { recentMessages: 1 },
747
+ },
748
+ );
749
+
750
+ expect(result.messages).toEqual([
751
+ { role: "user", content: "New prompt" },
752
+ ]);
753
+ expect(mockCtx.runQuery).toHaveBeenCalledTimes(2);
754
+ });
755
+
593
756
  it("should handle input messages correctly", async () => {
594
757
  const inputMessages: ModelMessage[] = [
595
758
  { role: "user", content: "Input message 1" },
@@ -618,16 +781,19 @@ describe("search.ts", () => {
618
781
  _id: "msg1",
619
782
  message: { role: "user", content: "Before prompt" },
620
783
  order: 1,
784
+ stepOrder: 0,
621
785
  } as MessageDoc,
622
786
  {
623
787
  _id: "prompt-msg",
624
788
  message: { role: "user", content: "Original prompt" },
625
789
  order: 2,
790
+ stepOrder: 0,
626
791
  } as MessageDoc,
627
792
  {
628
793
  _id: "msg3",
629
794
  message: { role: "assistant", content: "After prompt" },
630
795
  order: 3,
796
+ stepOrder: 0,
631
797
  } as MessageDoc,
632
798
  ];
633
799
 
@@ -659,16 +825,19 @@ describe("search.ts", () => {
659
825
  _id: "msg1",
660
826
  message: { role: "user", content: "Before prompt" },
661
827
  order: 1,
828
+ stepOrder: 0,
662
829
  } as MessageDoc,
663
830
  {
664
831
  _id: "prompt-msg",
665
832
  message: { role: "user", content: "Original prompt" },
666
833
  order: 2,
834
+ stepOrder: 0,
667
835
  } as MessageDoc,
668
836
  {
669
837
  _id: "msg3",
670
838
  message: { role: "assistant", content: "After prompt" },
671
839
  order: 3,
840
+ stepOrder: 0,
672
841
  } as MessageDoc,
673
842
  ];
674
843
 
@@ -696,16 +865,19 @@ describe("search.ts", () => {
696
865
  _id: "ctx1",
697
866
  message: { role: "user", content: "Context 1" },
698
867
  order: 1,
868
+ stepOrder: 0,
699
869
  } as MessageDoc,
700
870
  {
701
871
  _id: "prompt-msg",
702
872
  message: { role: "user", content: "Prompt" },
703
873
  order: 3,
874
+ stepOrder: 0,
704
875
  } as MessageDoc,
705
876
  {
706
877
  _id: "ctx2",
707
878
  message: { role: "assistant", content: "Context 2" },
708
879
  order: 5,
880
+ stepOrder: 0,
709
881
  } as MessageDoc,
710
882
  ];
711
883
 
@@ -864,10 +1036,10 @@ describe("search.ts", () => {
864
1036
  });
865
1037
  });
866
1038
 
867
- it("should respect recentMessages limit", async () => {
1039
+ it("should extend recentMessages to complete an order", async () => {
868
1040
  const threadId = await createTestThread("user999");
869
1041
 
870
- // Create 5 messages but only fetch the most recent 2
1042
+ // Start with a two-message window that splits the previous order.
871
1043
  await createTestMessages(threadId, "user999", [
872
1044
  { role: "user", content: "Message 1", order: 1 },
873
1045
  { role: "assistant", content: "Response 1", order: 2 },
@@ -883,13 +1055,16 @@ describe("search.ts", () => {
883
1055
  prompt: "New prompt",
884
1056
  messages: undefined,
885
1057
  promptMessageId: undefined,
886
- contextOptions: { recentMessages: 2 }, // Only fetch 2 most recent
1058
+ contextOptions: { recentMessages: 2 },
887
1059
  });
888
1060
 
889
- expect(result.messages).toHaveLength(3); // 2 context + 1 prompt
890
- expect(result.messages[0].content).toBe("Response 2"); // 4th message
891
- expect(result.messages[1].content).toBe("Message 3"); // 5th message
892
- expect(result.messages[2]).toEqual({
1061
+ // The limit splits the previous order after its user message, so the
1062
+ // context extends backward to include the whole order.
1063
+ expect(result.messages).toHaveLength(4);
1064
+ expect(result.messages[0].content).toBe("Message 2");
1065
+ expect(result.messages[1].content).toBe("Response 2");
1066
+ expect(result.messages[2].content).toBe("Message 3");
1067
+ expect(result.messages[3]).toEqual({
893
1068
  role: "user",
894
1069
  content: "New prompt",
895
1070
  });
@@ -36,10 +36,24 @@ import {
36
36
  } from "../mapping.js";
37
37
 
38
38
  const DEFAULT_VECTOR_SCORE_THRESHOLD = 0.0;
39
+ // Bound the rare boundary-extension query; incomplete orders are trimmed below.
40
+ const MAX_ORDER_COMPLETION_MESSAGES = 1_000;
39
41
  // 10k characters should be more than enough for most cases, and stays under
40
42
  // the 8k token limit for some models.
41
43
  const MAX_EMBEDDING_TEXT_LENGTH = 10_000;
42
44
 
45
+ /**
46
+ * Omit the oldest order when a descending pagination window split it.
47
+ * Expects the messages to be sorted in ascending order.
48
+ */
49
+ export function trimIncompleteOldestOrder(docs: MessageDoc[]) {
50
+ const oldest = docs[0];
51
+ if (!oldest || oldest.stepOrder === 0) {
52
+ return docs;
53
+ }
54
+ return docs.filter((doc) => doc.order !== oldest.order);
55
+ }
56
+
43
57
  export type GetEmbedding = (text: string) => Promise<
44
58
  | {
45
59
  embedding: number[];
@@ -133,25 +147,45 @@ export async function fetchRecentAndSearchMessages(
133
147
  let included: Set<string> | undefined;
134
148
  let recentMessages: MessageDoc[] = [];
135
149
  let searchMessages: MessageDoc[] = [];
150
+ const threadId = args.threadId;
136
151
  const targetMessageId =
137
152
  args.targetMessageId ?? args.upToAndIncludingMessageId;
138
- if (args.threadId && opts.recentMessages !== 0) {
139
- const { page } = await ctx.runQuery(
140
- component.messages.listMessagesByThreadId,
141
- {
142
- threadId: args.threadId,
153
+ if (threadId && opts.recentMessages !== 0) {
154
+ const fetchRecentPage = (numItems: number, cursor: string | null) =>
155
+ ctx.runQuery(component.messages.listMessagesByThreadId, {
156
+ threadId,
143
157
  excludeToolMessages: opts.excludeToolMessages,
144
- paginationOpts: {
145
- numItems: opts.recentMessages ?? DEFAULT_RECENT_MESSAGES,
146
- cursor: null,
147
- },
158
+ paginationOpts: { numItems, cursor },
148
159
  upToAndIncludingMessageId: targetMessageId,
149
160
  order: "desc",
150
161
  statuses: ["success"],
151
- },
162
+ });
163
+ const firstPage = await fetchRecentPage(
164
+ opts.recentMessages ?? DEFAULT_RECENT_MESSAGES,
165
+ null,
152
166
  );
153
- included = new Set(page.map((m) => m._id));
154
- recentMessages = filterOutOrphanedToolMessages(sorted(page));
167
+ let page = firstPage.page;
168
+ const oldest = page.at(-1);
169
+ if (
170
+ oldest &&
171
+ oldest.stepOrder > 0 &&
172
+ !firstPage.isDone &&
173
+ firstPage.continueCursor
174
+ ) {
175
+ const completionPage = await fetchRecentPage(
176
+ Math.min(oldest.stepOrder, MAX_ORDER_COMPLETION_MESSAGES),
177
+ firstPage.continueCursor,
178
+ );
179
+ page = [
180
+ ...page,
181
+ ...completionPage.page.filter(
182
+ (message) => message.order === oldest.order,
183
+ ),
184
+ ];
185
+ }
186
+ const retained = trimIncompleteOldestOrder(sorted(page));
187
+ included = new Set(retained.map((m) => m._id));
188
+ recentMessages = filterOutOrphanedToolMessages(retained);
155
189
  }
156
190
  if (
157
191
  (opts.searchOptions?.textSearch || opts.searchOptions?.vectorSearch) &&
@@ -386,6 +386,7 @@ export async function startGeneration<
386
386
  threadId,
387
387
  agentName: opts.agentName,
388
388
  promptMessageId,
389
+ abandonIfPromptMissing: true,
389
390
  pendingMessageId,
390
391
  messages: serialized.messages,
391
392
  embeddings,