@convex-dev/agent 0.6.4 → 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.
- package/MIGRATION.md +60 -116
- package/dist/client/messages.d.ts +1 -398
- package/dist/client/messages.d.ts.map +1 -1
- package/dist/client/messages.js +3 -47
- package/dist/client/messages.js.map +1 -1
- package/dist/client/types.d.ts +1 -455
- package/dist/client/types.d.ts.map +1 -1
- package/dist/component/_generated/component.d.ts +805 -45
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/messages.d.ts +1252 -201
- package/dist/component/messages.d.ts.map +1 -1
- package/dist/component/messages.js +49 -23
- package/dist/component/messages.js.map +1 -1
- package/dist/component/schema.d.ts +2593 -332
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/streams.d.ts +7 -1
- package/dist/component/streams.d.ts.map +1 -1
- package/dist/component/streams.js +24 -30
- package/dist/component/streams.js.map +1 -1
- package/dist/component/threads.d.ts.map +1 -1
- package/dist/component/threads.js +1 -2
- package/dist/component/threads.js.map +1 -1
- package/dist/react/index.d.ts +5 -5
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +5 -5
- package/dist/react/index.js.map +1 -1
- package/dist/shared.d.ts +19 -7
- package/dist/shared.d.ts.map +1 -1
- package/dist/shared.js +4 -4
- package/dist/shared.js.map +1 -1
- package/dist/streaming/materializePersistedUIMessageChunks.d.ts +16 -0
- package/dist/streaming/materializePersistedUIMessageChunks.d.ts.map +1 -0
- package/dist/streaming/materializePersistedUIMessageChunks.js +302 -0
- package/dist/streaming/materializePersistedUIMessageChunks.js.map +1 -0
- package/dist/streaming/persistedUIMessageChunks.d.ts +106 -0
- package/dist/streaming/persistedUIMessageChunks.d.ts.map +1 -0
- package/dist/streaming/persistedUIMessageChunks.js +475 -0
- package/dist/streaming/persistedUIMessageChunks.js.map +1 -0
- package/dist/validators.d.ts +10026 -1055
- package/dist/validators.d.ts.map +1 -1
- package/dist/validators.js +71 -9
- package/dist/validators.js.map +1 -1
- package/dist/{UIMessages.d.ts → vercel/UIMessages.d.ts} +1 -1
- package/dist/vercel/UIMessages.d.ts.map +1 -0
- package/dist/{UIMessages.js → vercel/UIMessages.js} +63 -1
- package/dist/vercel/UIMessages.js.map +1 -0
- package/dist/{client → vercel/client}/createTool.d.ts +23 -16
- package/dist/vercel/client/createTool.d.ts.map +1 -0
- package/dist/{client → vercel/client}/createTool.js +2 -0
- package/dist/vercel/client/createTool.js.map +1 -0
- package/dist/{client → vercel/client}/definePlaygroundAPI.d.ts +533 -79
- package/dist/{client → vercel/client}/definePlaygroundAPI.d.ts.map +1 -1
- package/dist/{client → vercel/client}/definePlaygroundAPI.js +8 -7
- package/dist/vercel/client/definePlaygroundAPI.js.map +1 -0
- package/dist/{client → vercel/client}/files.d.ts +2 -2
- package/dist/vercel/client/files.d.ts.map +1 -0
- package/dist/vercel/client/files.js.map +1 -0
- package/dist/vercel/client/messages.d.ts +542 -0
- package/dist/vercel/client/messages.d.ts.map +1 -0
- package/dist/vercel/client/messages.js +66 -0
- package/dist/vercel/client/messages.js.map +1 -0
- package/dist/{client → vercel/client}/mockModel.d.ts +17 -17
- package/dist/vercel/client/mockModel.d.ts.map +1 -0
- package/dist/{client → vercel/client}/mockModel.js +12 -13
- package/dist/vercel/client/mockModel.js.map +1 -0
- package/dist/{client → vercel/client}/saveInputMessages.d.ts +2 -2
- package/dist/vercel/client/saveInputMessages.d.ts.map +1 -0
- package/dist/vercel/client/saveInputMessages.js.map +1 -0
- package/dist/{client → vercel/client}/search.d.ts +119 -22
- package/dist/vercel/client/search.d.ts.map +1 -0
- package/dist/{client → vercel/client}/search.js +2 -2
- package/dist/vercel/client/search.js.map +1 -0
- package/dist/{client → vercel/client}/start.d.ts +26 -11
- package/dist/vercel/client/start.d.ts.map +1 -0
- package/dist/{client → vercel/client}/start.js +64 -22
- package/dist/vercel/client/start.js.map +1 -0
- package/dist/{client → vercel/client}/streamText.d.ts +13 -12
- package/dist/vercel/client/streamText.d.ts.map +1 -0
- package/dist/{client → vercel/client}/streamText.js +43 -9
- package/dist/vercel/client/streamText.js.map +1 -0
- package/dist/{client → vercel/client}/streaming.d.ts +1410 -183
- package/dist/vercel/client/streaming.d.ts.map +1 -0
- package/dist/{client → vercel/client}/streaming.js +120 -52
- package/dist/vercel/client/streaming.js.map +1 -0
- package/dist/vercel/client/types.d.ts +456 -0
- package/dist/vercel/client/types.d.ts.map +1 -0
- package/dist/vercel/client/types.js +3 -0
- package/dist/vercel/client/types.js.map +1 -0
- package/dist/{client → vercel/client}/utils.d.ts +3 -2
- package/dist/vercel/client/utils.d.ts.map +1 -0
- package/dist/{client → vercel/client}/utils.js +2 -2
- package/dist/vercel/client/utils.js.map +1 -0
- package/dist/{deltas.d.ts → vercel/deltas.d.ts} +3 -4
- package/dist/vercel/deltas.d.ts.map +1 -0
- package/dist/{deltas.js → vercel/deltas.js} +195 -99
- package/dist/vercel/deltas.js.map +1 -0
- package/dist/{client → vercel}/index.d.ts +482 -106
- package/dist/vercel/index.d.ts.map +1 -0
- package/dist/{client → vercel}/index.js +64 -46
- package/dist/vercel/index.js.map +1 -0
- package/dist/{mapping.d.ts → vercel/mapping.d.ts} +5 -19
- package/dist/vercel/mapping.d.ts.map +1 -0
- package/dist/{mapping.js → vercel/mapping.js} +411 -114
- package/dist/vercel/mapping.js.map +1 -0
- package/dist/{react → vercel/react}/optimisticallySendMessage.d.ts +2 -2
- package/dist/vercel/react/optimisticallySendMessage.d.ts.map +1 -0
- package/dist/vercel/react/optimisticallySendMessage.js.map +1 -0
- package/dist/{react → vercel/react}/types.d.ts +1 -1
- package/dist/vercel/react/types.d.ts.map +1 -0
- package/dist/{react → vercel/react}/types.js.map +1 -1
- package/dist/{react → vercel/react}/useDeltaStreams.d.ts +1 -1
- package/dist/vercel/react/useDeltaStreams.d.ts.map +1 -0
- package/dist/{react → vercel/react}/useDeltaStreams.js +1 -1
- package/dist/vercel/react/useDeltaStreams.js.map +1 -0
- package/dist/{react → vercel/react}/useStreamingUIMessages.d.ts +1 -1
- package/dist/vercel/react/useStreamingUIMessages.d.ts.map +1 -0
- package/dist/{react → vercel/react}/useStreamingUIMessages.js +7 -3
- package/dist/vercel/react/useStreamingUIMessages.js.map +1 -0
- package/dist/{react → vercel/react}/useThreadMessages.d.ts +1 -1
- package/dist/vercel/react/useThreadMessages.d.ts.map +1 -0
- package/dist/{react → vercel/react}/useThreadMessages.js +1 -1
- package/dist/vercel/react/useThreadMessages.js.map +1 -0
- package/dist/{react → vercel/react}/useUIMessages.d.ts +1 -1
- package/dist/vercel/react/useUIMessages.d.ts.map +1 -0
- package/dist/{react → vercel/react}/useUIMessages.js +1 -1
- package/dist/vercel/react/useUIMessages.js.map +1 -0
- package/package.json +29 -22
- package/src/client/messages.ts +5 -107
- package/src/client/types.ts +0 -608
- package/src/component/_generated/component.ts +1089 -48
- package/src/component/messages.test.ts +273 -1
- package/src/component/messages.ts +62 -32
- package/src/component/streams.ts +47 -47
- package/src/component/threads.ts +5 -2
- package/src/eslintConfig.test.ts +44 -0
- package/src/react/index.ts +8 -5
- package/src/shared.ts +26 -36
- package/src/streaming/materializePersistedUIMessageChunks.test.ts +513 -0
- package/src/streaming/materializePersistedUIMessageChunks.ts +398 -0
- package/src/streaming/persistedUIMessageChunks.test.ts +205 -0
- package/src/streaming/persistedUIMessageChunks.ts +658 -0
- package/src/validators.test.ts +4 -1
- package/src/validators.ts +89 -8
- package/src/{UIMessages.combineUIMessages.test.ts → vercel/UIMessages.combineUIMessages.test.ts} +4 -1
- package/src/{UIMessages.ts → vercel/UIMessages.ts} +95 -3
- package/src/{client → vercel/client}/approval.test.ts +23 -13
- package/src/{client → vercel/client}/createTool.ts +62 -22
- package/src/{client → vercel/client}/definePlaygroundAPI.ts +15 -16
- package/src/{client → vercel/client}/files.ts +2 -2
- package/src/{client → vercel/client}/index.test.ts +99 -31
- package/src/vercel/client/messages.ts +182 -0
- package/src/{client → vercel/client}/mockModel.ts +38 -39
- package/src/{client → vercel/client}/saveInputMessages.test.ts +1 -1
- package/src/{client → vercel/client}/saveInputMessages.ts +3 -3
- package/src/{client → vercel/client}/search.test.ts +3 -3
- package/src/{client → vercel/client}/search.ts +4 -4
- package/src/{client → vercel/client}/setup.test.ts +6 -3
- package/src/vercel/client/start.test.ts +14 -0
- package/src/{client → vercel/client}/start.ts +147 -36
- package/src/vercel/client/streamText.test.ts +223 -0
- package/src/{client → vercel/client}/streamText.ts +102 -31
- package/src/{client → vercel/client}/streaming.integration.test.ts +6 -3
- package/src/{client → vercel/client}/streaming.test.ts +178 -4
- package/src/{client → vercel/client}/streaming.ts +114 -52
- package/src/vercel/client/types.test.ts +13 -0
- package/src/vercel/client/types.ts +679 -0
- package/src/{client → vercel/client}/utils.ts +13 -7
- package/src/{deltas.test.ts → vercel/deltas.test.ts} +384 -93
- package/src/{deltas.ts → vercel/deltas.ts} +218 -115
- package/src/{fromUIMessages.test.ts → vercel/fromUIMessages.test.ts} +17 -1
- package/src/{client → vercel}/index.ts +167 -84
- package/src/{mapping.test.ts → vercel/mapping.test.ts} +377 -92
- package/src/{mapping.ts → vercel/mapping.ts} +501 -137
- package/src/{react → vercel/react}/optimisticallySendMessage.ts +2 -2
- package/src/{react → vercel/react}/types.ts +1 -1
- package/src/{react → vercel/react}/useDeltaStreams.ts +6 -2
- package/src/{react → vercel/react}/useStreamingUIMessages.ts +7 -3
- package/src/{react → vercel/react}/useThreadMessages.ts +2 -2
- package/src/{react → vercel/react}/useUIMessages.ts +2 -2
- package/src/{toUIMessages.test.ts → vercel/toUIMessages.test.ts} +49 -2
- package/dist/UIMessages.d.ts.map +0 -1
- package/dist/UIMessages.js.map +0 -1
- package/dist/client/createTool.d.ts.map +0 -1
- package/dist/client/createTool.js.map +0 -1
- package/dist/client/definePlaygroundAPI.js.map +0 -1
- package/dist/client/files.d.ts.map +0 -1
- package/dist/client/files.js.map +0 -1
- package/dist/client/index.d.ts.map +0 -1
- package/dist/client/index.js.map +0 -1
- package/dist/client/mockModel.d.ts.map +0 -1
- package/dist/client/mockModel.js.map +0 -1
- package/dist/client/saveInputMessages.d.ts.map +0 -1
- package/dist/client/saveInputMessages.js.map +0 -1
- package/dist/client/search.d.ts.map +0 -1
- package/dist/client/search.js.map +0 -1
- package/dist/client/start.d.ts.map +0 -1
- package/dist/client/start.js.map +0 -1
- package/dist/client/streamText.d.ts.map +0 -1
- package/dist/client/streamText.js.map +0 -1
- package/dist/client/streaming.d.ts.map +0 -1
- package/dist/client/streaming.js.map +0 -1
- package/dist/client/utils.d.ts.map +0 -1
- package/dist/client/utils.js.map +0 -1
- package/dist/deltas.d.ts.map +0 -1
- package/dist/deltas.js.map +0 -1
- package/dist/mapping.d.ts.map +0 -1
- package/dist/mapping.js.map +0 -1
- package/dist/react/optimisticallySendMessage.d.ts.map +0 -1
- package/dist/react/optimisticallySendMessage.js.map +0 -1
- package/dist/react/types.d.ts.map +0 -1
- package/dist/react/useDeltaStreams.d.ts.map +0 -1
- package/dist/react/useDeltaStreams.js.map +0 -1
- package/dist/react/useStreamingUIMessages.d.ts.map +0 -1
- package/dist/react/useStreamingUIMessages.js.map +0 -1
- package/dist/react/useThreadMessages.d.ts.map +0 -1
- package/dist/react/useThreadMessages.js.map +0 -1
- package/dist/react/useUIMessages.d.ts.map +0 -1
- package/dist/react/useUIMessages.js.map +0 -1
- package/src/client/streamText.test.ts +0 -102
- /package/dist/{client → vercel/client}/files.js +0 -0
- /package/dist/{client → vercel/client}/saveInputMessages.js +0 -0
- /package/dist/{react → vercel/react}/optimisticallySendMessage.js +0 -0
- /package/dist/{react → vercel/react}/types.js +0 -0
- /package/src/{UIMessages.test.ts → vercel/UIMessages.test.ts} +0 -0
- /package/src/{react → vercel/react}/useUIMessages.test.ts +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/// <reference types="vite/client" />
|
|
2
2
|
|
|
3
3
|
import { convexTest } from "convex-test";
|
|
4
|
-
import { describe, expect, test } from "vitest";
|
|
4
|
+
import { describe, expect, test, vi } from "vitest";
|
|
5
5
|
import { api } from "./_generated/api.js";
|
|
6
6
|
import type { Id } from "./_generated/dataModel.js";
|
|
7
7
|
import { getMaxMessage } from "./messages.js";
|
|
@@ -652,4 +652,276 @@ describe("agent", () => {
|
|
|
652
652
|
);
|
|
653
653
|
expect(remainingMessages.page).toHaveLength(1);
|
|
654
654
|
});
|
|
655
|
+
|
|
656
|
+
// Regression test for #256: when searching across threads with
|
|
657
|
+
// searchAllMessagesForUserId, the targetMessage's order should not filter
|
|
658
|
+
// out messages from other threads (their order sequences are independent).
|
|
659
|
+
test("textSearch returns cross-thread matches even when target order is lower", async () => {
|
|
660
|
+
const t = convexTest(schema, modules);
|
|
661
|
+
const userId = "user-256";
|
|
662
|
+
|
|
663
|
+
// Old thread: build up several messages so the matching one has a high order.
|
|
664
|
+
const oldThread = await t.mutation(api.threads.createThread, { userId });
|
|
665
|
+
for (let i = 0; i < 5; i++) {
|
|
666
|
+
await t.mutation(api.messages.addMessages, {
|
|
667
|
+
threadId: oldThread._id as Id<"threads">,
|
|
668
|
+
userId,
|
|
669
|
+
messages: [
|
|
670
|
+
{ message: { role: "user", content: `filler message ${i}` } },
|
|
671
|
+
],
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
await t.mutation(api.messages.addMessages, {
|
|
675
|
+
threadId: oldThread._id as Id<"threads">,
|
|
676
|
+
userId,
|
|
677
|
+
messages: [
|
|
678
|
+
{
|
|
679
|
+
message: {
|
|
680
|
+
role: "user",
|
|
681
|
+
content:
|
|
682
|
+
"tom and jerry are both amazing high-ticket coaches and educators",
|
|
683
|
+
},
|
|
684
|
+
},
|
|
685
|
+
],
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
// New thread: only one message — its order will be 0, lower than the
|
|
689
|
+
// matching message in the old thread.
|
|
690
|
+
const newThread = await t.mutation(api.threads.createThread, { userId });
|
|
691
|
+
const { messages: newMessages } = await t.mutation(
|
|
692
|
+
api.messages.addMessages,
|
|
693
|
+
{
|
|
694
|
+
threadId: newThread._id as Id<"threads">,
|
|
695
|
+
userId,
|
|
696
|
+
messages: [
|
|
697
|
+
{
|
|
698
|
+
message: {
|
|
699
|
+
role: "user",
|
|
700
|
+
content: "what do you remember about high-ticket coaches",
|
|
701
|
+
},
|
|
702
|
+
},
|
|
703
|
+
],
|
|
704
|
+
},
|
|
705
|
+
);
|
|
706
|
+
const targetMessageId = newMessages[0]._id as Id<"messages">;
|
|
707
|
+
|
|
708
|
+
const results = await t.query(api.messages.textSearch, {
|
|
709
|
+
searchAllMessagesForUserId: userId,
|
|
710
|
+
targetMessageId,
|
|
711
|
+
text: "high-ticket coaches",
|
|
712
|
+
limit: 10,
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
// The cross-thread match should NOT be filtered out by the target's order.
|
|
716
|
+
expect(
|
|
717
|
+
results.some((m) =>
|
|
718
|
+
m.text?.includes("tom and jerry are both amazing high-ticket coaches"),
|
|
719
|
+
),
|
|
720
|
+
).toBe(true);
|
|
721
|
+
// The target message itself must still be excluded.
|
|
722
|
+
expect(results.some((m) => m._id === targetMessageId)).toBe(false);
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
// Regression test for #256: same-thread order filter must still work even
|
|
726
|
+
// when searching across threads.
|
|
727
|
+
test("textSearch still filters same-thread results past the target order", async () => {
|
|
728
|
+
const t = convexTest(schema, modules);
|
|
729
|
+
const userId = "user-256-same";
|
|
730
|
+
|
|
731
|
+
const thread = await t.mutation(api.threads.createThread, { userId });
|
|
732
|
+
// earlier match
|
|
733
|
+
await t.mutation(api.messages.addMessages, {
|
|
734
|
+
threadId: thread._id as Id<"threads">,
|
|
735
|
+
userId,
|
|
736
|
+
messages: [
|
|
737
|
+
{ message: { role: "user", content: "earlier high-ticket match" } },
|
|
738
|
+
],
|
|
739
|
+
});
|
|
740
|
+
// target
|
|
741
|
+
const { messages: targetMessages } = await t.mutation(
|
|
742
|
+
api.messages.addMessages,
|
|
743
|
+
{
|
|
744
|
+
threadId: thread._id as Id<"threads">,
|
|
745
|
+
userId,
|
|
746
|
+
messages: [
|
|
747
|
+
{ message: { role: "user", content: "target high-ticket message" } },
|
|
748
|
+
],
|
|
749
|
+
},
|
|
750
|
+
);
|
|
751
|
+
// later match in same thread — should be filtered out
|
|
752
|
+
await t.mutation(api.messages.addMessages, {
|
|
753
|
+
threadId: thread._id as Id<"threads">,
|
|
754
|
+
userId,
|
|
755
|
+
messages: [
|
|
756
|
+
{ message: { role: "user", content: "later high-ticket match" } },
|
|
757
|
+
],
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
const results = await t.query(api.messages.textSearch, {
|
|
761
|
+
searchAllMessagesForUserId: userId,
|
|
762
|
+
targetMessageId: targetMessages[0]._id as Id<"messages">,
|
|
763
|
+
text: "high-ticket",
|
|
764
|
+
limit: 10,
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
expect(results.some((m) => m.text === "earlier high-ticket match")).toBe(
|
|
768
|
+
true,
|
|
769
|
+
);
|
|
770
|
+
expect(results.some((m) => m.text === "later high-ticket match")).toBe(
|
|
771
|
+
false,
|
|
772
|
+
);
|
|
773
|
+
expect(results.some((m) => m._id === targetMessages[0]._id)).toBe(false);
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
test("finalizeMessage commits a failed pending message when stream recovery is malformed", async () => {
|
|
777
|
+
const t = initConvexTest();
|
|
778
|
+
const thread = await t.mutation(api.threads.createThread, {
|
|
779
|
+
userId: "recovery-user",
|
|
780
|
+
});
|
|
781
|
+
const { messages } = await t.mutation(api.messages.addMessages, {
|
|
782
|
+
threadId: thread._id as Id<"threads">,
|
|
783
|
+
messages: [
|
|
784
|
+
{
|
|
785
|
+
message: { role: "assistant", content: [] },
|
|
786
|
+
status: "pending",
|
|
787
|
+
},
|
|
788
|
+
],
|
|
789
|
+
});
|
|
790
|
+
const pending = messages[0];
|
|
791
|
+
const streamId = await t.run(async (ctx) =>
|
|
792
|
+
ctx.db.insert("streamingMessages", {
|
|
793
|
+
threadId: thread._id as Id<"threads">,
|
|
794
|
+
order: pending.order,
|
|
795
|
+
stepOrder: pending.stepOrder,
|
|
796
|
+
format: "UIMessageChunk",
|
|
797
|
+
state: { kind: "aborted", reason: "interrupted" },
|
|
798
|
+
}),
|
|
799
|
+
);
|
|
800
|
+
await t.run(async (ctx) =>
|
|
801
|
+
ctx.db.insert("streamDeltas", {
|
|
802
|
+
streamId,
|
|
803
|
+
start: 0,
|
|
804
|
+
end: 1,
|
|
805
|
+
parts: [{ type: "text-delta", id: "missing", delta: "partial" }],
|
|
806
|
+
}),
|
|
807
|
+
);
|
|
808
|
+
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
809
|
+
|
|
810
|
+
try {
|
|
811
|
+
await expect(
|
|
812
|
+
t.mutation(api.messages.finalizeMessage, {
|
|
813
|
+
messageId: pending._id as Id<"messages">,
|
|
814
|
+
result: { status: "success" },
|
|
815
|
+
}),
|
|
816
|
+
).resolves.toBeNull();
|
|
817
|
+
} finally {
|
|
818
|
+
errorSpy.mockRestore();
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
const finalized = await t.run((ctx) =>
|
|
822
|
+
ctx.db.get("messages", pending._id as Id<"messages">),
|
|
823
|
+
);
|
|
824
|
+
expect(finalized).toMatchObject({
|
|
825
|
+
status: "failed",
|
|
826
|
+
error: "Failed to recover persisted assistant stream output",
|
|
827
|
+
message: { role: "assistant", content: [] },
|
|
828
|
+
});
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
test("finalizeMessage discards all recovered output when any stream is malformed", async () => {
|
|
832
|
+
const t = initConvexTest();
|
|
833
|
+
const thread = await t.mutation(api.threads.createThread, {
|
|
834
|
+
userId: "mixed-recovery-user",
|
|
835
|
+
});
|
|
836
|
+
const { messages } = await t.mutation(api.messages.addMessages, {
|
|
837
|
+
threadId: thread._id as Id<"threads">,
|
|
838
|
+
messages: [
|
|
839
|
+
{
|
|
840
|
+
message: { role: "assistant", content: [] },
|
|
841
|
+
status: "pending",
|
|
842
|
+
},
|
|
843
|
+
],
|
|
844
|
+
});
|
|
845
|
+
const pending = messages[0];
|
|
846
|
+
const embeddingId = await t.run(async (ctx) =>
|
|
847
|
+
ctx.db.insert("embeddings_128", {
|
|
848
|
+
model: "embedding-model",
|
|
849
|
+
table: "messages",
|
|
850
|
+
threadId: thread._id,
|
|
851
|
+
vector: Array.from({ length: 128 }, () => 0),
|
|
852
|
+
}),
|
|
853
|
+
);
|
|
854
|
+
await t.run((ctx) =>
|
|
855
|
+
ctx.db.patch("messages", pending._id as Id<"messages">, { embeddingId }),
|
|
856
|
+
);
|
|
857
|
+
|
|
858
|
+
const validStreamId = await t.run(async (ctx) =>
|
|
859
|
+
ctx.db.insert("streamingMessages", {
|
|
860
|
+
threadId: thread._id as Id<"threads">,
|
|
861
|
+
order: pending.order,
|
|
862
|
+
stepOrder: pending.stepOrder,
|
|
863
|
+
format: "UIMessageChunk",
|
|
864
|
+
state: { kind: "finished", endedAt: Date.now() },
|
|
865
|
+
}),
|
|
866
|
+
);
|
|
867
|
+
await t.run(async (ctx) =>
|
|
868
|
+
ctx.db.insert("streamDeltas", {
|
|
869
|
+
streamId: validStreamId,
|
|
870
|
+
start: 0,
|
|
871
|
+
end: 3,
|
|
872
|
+
parts: [
|
|
873
|
+
{ type: "text-start", id: "valid" },
|
|
874
|
+
{ type: "text-delta", id: "valid", delta: "must not be inserted" },
|
|
875
|
+
{ type: "text-end", id: "valid" },
|
|
876
|
+
],
|
|
877
|
+
}),
|
|
878
|
+
);
|
|
879
|
+
const malformedStreamId = await t.run(async (ctx) =>
|
|
880
|
+
ctx.db.insert("streamingMessages", {
|
|
881
|
+
threadId: thread._id as Id<"threads">,
|
|
882
|
+
order: pending.order,
|
|
883
|
+
stepOrder: pending.stepOrder,
|
|
884
|
+
format: "UIMessageChunk",
|
|
885
|
+
state: { kind: "aborted", reason: "interrupted" },
|
|
886
|
+
}),
|
|
887
|
+
);
|
|
888
|
+
await t.run(async (ctx) =>
|
|
889
|
+
ctx.db.insert("streamDeltas", {
|
|
890
|
+
streamId: malformedStreamId,
|
|
891
|
+
start: 0,
|
|
892
|
+
end: 1,
|
|
893
|
+
parts: [
|
|
894
|
+
{
|
|
895
|
+
type: "tool-input-delta",
|
|
896
|
+
toolCallId: "missing",
|
|
897
|
+
inputTextDelta: "{}",
|
|
898
|
+
},
|
|
899
|
+
],
|
|
900
|
+
}),
|
|
901
|
+
);
|
|
902
|
+
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
903
|
+
|
|
904
|
+
try {
|
|
905
|
+
await t.mutation(api.messages.finalizeMessage, {
|
|
906
|
+
messageId: pending._id as Id<"messages">,
|
|
907
|
+
result: { status: "failed", error: "provider failed" },
|
|
908
|
+
});
|
|
909
|
+
} finally {
|
|
910
|
+
errorSpy.mockRestore();
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
const state = await t.run(async (ctx) => ({
|
|
914
|
+
messages: await ctx.db.query("messages").collect(),
|
|
915
|
+
embedding: await ctx.db.get("embeddings_128", embeddingId),
|
|
916
|
+
}));
|
|
917
|
+
expect(state.messages).toHaveLength(1);
|
|
918
|
+
expect(state.messages[0]).toMatchObject({
|
|
919
|
+
_id: pending._id,
|
|
920
|
+
status: "failed",
|
|
921
|
+
error: "provider failed",
|
|
922
|
+
message: { role: "assistant", content: [] },
|
|
923
|
+
});
|
|
924
|
+
expect(state.messages[0].embeddingId).toBeUndefined();
|
|
925
|
+
expect(state.embedding).toBeNull();
|
|
926
|
+
});
|
|
655
927
|
});
|
|
@@ -83,6 +83,25 @@ export const messageStatuses = vMessageDoc.fields.status.members.map(
|
|
|
83
83
|
(m) => m.value,
|
|
84
84
|
);
|
|
85
85
|
|
|
86
|
+
const STREAM_RECOVERY_FAILURE =
|
|
87
|
+
"Failed to recover persisted assistant stream output";
|
|
88
|
+
|
|
89
|
+
async function markPendingMessageFailed(
|
|
90
|
+
ctx: MutationCtx,
|
|
91
|
+
message: Doc<"messages">,
|
|
92
|
+
error: string,
|
|
93
|
+
) {
|
|
94
|
+
if (message.embeddingId) {
|
|
95
|
+
const { tableName } = getVectorIdInfo(ctx, message.embeddingId);
|
|
96
|
+
await ctx.db.delete(tableName, message.embeddingId);
|
|
97
|
+
}
|
|
98
|
+
await ctx.db.patch("messages", message._id, {
|
|
99
|
+
status: "failed",
|
|
100
|
+
error,
|
|
101
|
+
embeddingId: undefined,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
86
105
|
export const deleteByOrder = mutation({
|
|
87
106
|
args: {
|
|
88
107
|
threadId: v.id("threads"),
|
|
@@ -390,11 +409,20 @@ export const finalizeMessage = mutation({
|
|
|
390
409
|
}
|
|
391
410
|
// See if we can add any in-progress data
|
|
392
411
|
if (!message.message?.content.length) {
|
|
393
|
-
const messages
|
|
394
|
-
ctx,
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
412
|
+
const { messages, materializationFailures } =
|
|
413
|
+
await getStreamingMessagesWithMetadata(ctx, message, result);
|
|
414
|
+
if (materializationFailures.length > 0) {
|
|
415
|
+
console.error(
|
|
416
|
+
"Failed to materialize persisted assistant streams",
|
|
417
|
+
materializationFailures,
|
|
418
|
+
);
|
|
419
|
+
await markPendingMessageFailed(
|
|
420
|
+
ctx,
|
|
421
|
+
message,
|
|
422
|
+
result.status === "failed" ? result.error : STREAM_RECOVERY_FAILURE,
|
|
423
|
+
);
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
398
426
|
if (messages.length > 0) {
|
|
399
427
|
await addMessagesHandler(ctx, {
|
|
400
428
|
messages,
|
|
@@ -409,15 +437,7 @@ export const finalizeMessage = mutation({
|
|
|
409
437
|
}
|
|
410
438
|
}
|
|
411
439
|
if (result.status === "failed") {
|
|
412
|
-
|
|
413
|
-
const { tableName } = getVectorIdInfo(ctx, message.embeddingId);
|
|
414
|
-
await ctx.db.delete(tableName, message.embeddingId);
|
|
415
|
-
}
|
|
416
|
-
await ctx.db.patch("messages", messageId, {
|
|
417
|
-
status: "failed",
|
|
418
|
-
error: result.error,
|
|
419
|
-
embeddingId: undefined,
|
|
420
|
-
});
|
|
440
|
+
await markPendingMessageFailed(ctx, message, result.error);
|
|
421
441
|
} else {
|
|
422
442
|
await ctx.db.patch("messages", messageId, { status: "success" });
|
|
423
443
|
}
|
|
@@ -830,16 +850,18 @@ export const _fetchSearchMessages = internalQuery({
|
|
|
830
850
|
),
|
|
831
851
|
)
|
|
832
852
|
)
|
|
833
|
-
.filter(
|
|
834
|
-
(m
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
853
|
+
.filter((m): m is Doc<"messages"> => {
|
|
854
|
+
if (m === undefined || m === null || m.tool) return false;
|
|
855
|
+
if (!beforeMessage) return true;
|
|
856
|
+
// The order filter is only meaningful within the same thread.
|
|
857
|
+
// Messages from other threads have independent order sequences.
|
|
858
|
+
if (m.threadId !== beforeMessage.threadId) return true;
|
|
859
|
+
return (
|
|
860
|
+
m.order < beforeMessage.order ||
|
|
861
|
+
(m.order === beforeMessage.order &&
|
|
862
|
+
m.stepOrder < beforeMessage.stepOrder)
|
|
863
|
+
);
|
|
864
|
+
})
|
|
843
865
|
.map(publicMessage);
|
|
844
866
|
messages.push(...(args.textSearchMessages ?? []));
|
|
845
867
|
// TODO: prioritize more recent messages
|
|
@@ -922,12 +944,16 @@ export const textSearch = query({
|
|
|
922
944
|
const targetMessage =
|
|
923
945
|
args.targetMessageId &&
|
|
924
946
|
(await ctx.db.get("messages", args.targetMessageId));
|
|
925
|
-
const order = targetMessage?.order;
|
|
926
947
|
const text = args.text || targetMessage?.text;
|
|
927
948
|
if (!text) {
|
|
928
949
|
console.warn("No text to search", targetMessage, args.text);
|
|
929
950
|
return [];
|
|
930
951
|
}
|
|
952
|
+
// When searching across threads (searchAllMessagesForUserId), the
|
|
953
|
+
// targetMessage's order is only meaningful within its own thread, so we
|
|
954
|
+
// can't apply it as a database-level filter. We still apply it post-fetch
|
|
955
|
+
// for same-thread results below.
|
|
956
|
+
const restrictOrderInDb = !args.searchAllMessagesForUserId && targetMessage;
|
|
931
957
|
const messages = await ctx.db
|
|
932
958
|
.query("messages")
|
|
933
959
|
.withSearchIndex("text_search", (q) =>
|
|
@@ -939,20 +965,24 @@ export const textSearch = query({
|
|
|
939
965
|
// eslint-disable-next-line @convex-dev/no-filter-in-query -- we can't do this in the search index, but text search ideally isn't for really old orders
|
|
940
966
|
.filter((q) => {
|
|
941
967
|
const qq = q.eq(q.field("tool"), false);
|
|
942
|
-
if (
|
|
943
|
-
return q.and(qq, q.lte(q.field("order"), order));
|
|
968
|
+
if (restrictOrderInDb) {
|
|
969
|
+
return q.and(qq, q.lte(q.field("order"), targetMessage.order));
|
|
944
970
|
}
|
|
945
971
|
return qq;
|
|
946
972
|
})
|
|
947
973
|
.take(args.limit);
|
|
948
974
|
return messages
|
|
949
|
-
.filter(
|
|
950
|
-
(
|
|
951
|
-
|
|
975
|
+
.filter((m) => {
|
|
976
|
+
if (!targetMessage) return true;
|
|
977
|
+
// Order is only meaningful within the same thread; cross-thread
|
|
978
|
+
// results have independent order sequences and should pass through.
|
|
979
|
+
if (m.threadId !== targetMessage.threadId) return true;
|
|
980
|
+
return (
|
|
952
981
|
m.order < targetMessage.order ||
|
|
953
982
|
(m.order === targetMessage.order &&
|
|
954
|
-
m.stepOrder < targetMessage.stepOrder)
|
|
955
|
-
|
|
983
|
+
m.stepOrder < targetMessage.stepOrder)
|
|
984
|
+
);
|
|
985
|
+
})
|
|
956
986
|
.map(publicMessage);
|
|
957
987
|
},
|
|
958
988
|
returns: v.array(vMessageDoc),
|
package/src/component/streams.ts
CHANGED
|
@@ -21,8 +21,10 @@ import { stream } from "convex-helpers/server/stream";
|
|
|
21
21
|
import { mergedStream } from "convex-helpers/server/stream";
|
|
22
22
|
import { paginator } from "convex-helpers/server/pagination";
|
|
23
23
|
import type { WithoutSystemFields } from "convex/server";
|
|
24
|
-
import {
|
|
25
|
-
|
|
24
|
+
import {
|
|
25
|
+
getPersistedUIMessageChunkParts,
|
|
26
|
+
projectPersistedUIMessageChunks,
|
|
27
|
+
} from "../streaming/materializePersistedUIMessageChunks.js";
|
|
26
28
|
|
|
27
29
|
const SECOND = 1000;
|
|
28
30
|
const MINUTE = 60 * SECOND;
|
|
@@ -536,7 +538,13 @@ export async function getStreamingMessagesWithMetadata(
|
|
|
536
538
|
stepOrder,
|
|
537
539
|
}: { threadId: Id<"threads">; order: number; stepOrder: number },
|
|
538
540
|
metadata: { status: "success" | "failed"; error?: string },
|
|
539
|
-
): Promise<
|
|
541
|
+
): Promise<{
|
|
542
|
+
messages: MessageWithMetadataInternal[];
|
|
543
|
+
materializationFailures: Array<{
|
|
544
|
+
streamId: Id<"streamingMessages">;
|
|
545
|
+
reason: string;
|
|
546
|
+
}>;
|
|
547
|
+
}> {
|
|
540
548
|
// See if there are any streaming messages for this order
|
|
541
549
|
const streamingMessages = await getStreamingMessages(
|
|
542
550
|
ctx,
|
|
@@ -544,50 +552,42 @@ export async function getStreamingMessagesWithMetadata(
|
|
|
544
552
|
order,
|
|
545
553
|
stepOrder,
|
|
546
554
|
);
|
|
547
|
-
const
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
.
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
const
|
|
557
|
-
|
|
558
|
-
[publicStreamMessage(streamingMessage)],
|
|
559
|
-
deltas,
|
|
560
|
-
);
|
|
555
|
+
const materializedStreams = await Promise.all(
|
|
556
|
+
streamingMessages.map(async (streamingMessage) => {
|
|
557
|
+
const deltas = await ctx.db
|
|
558
|
+
.query("streamDeltas")
|
|
559
|
+
.withIndex("streamId_start_end", (q) =>
|
|
560
|
+
q.eq("streamId", streamingMessage._id),
|
|
561
|
+
)
|
|
562
|
+
.take(1000);
|
|
563
|
+
try {
|
|
564
|
+
const streamMessage = publicStreamMessage(streamingMessage);
|
|
565
|
+
const { parts } = getPersistedUIMessageChunkParts(deltas);
|
|
561
566
|
// We don't save messages that have already been saved
|
|
562
567
|
const numToSkip = stepOrder - streamingMessage.stepOrder;
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
return messages;
|
|
589
|
-
}),
|
|
590
|
-
)
|
|
591
|
-
).flat();
|
|
592
|
-
return messages;
|
|
568
|
+
return {
|
|
569
|
+
messages: projectPersistedUIMessageChunks(
|
|
570
|
+
streamMessage,
|
|
571
|
+
parts,
|
|
572
|
+
metadata,
|
|
573
|
+
).slice(numToSkip),
|
|
574
|
+
failure: undefined,
|
|
575
|
+
};
|
|
576
|
+
} catch (error) {
|
|
577
|
+
return {
|
|
578
|
+
messages: [],
|
|
579
|
+
failure: {
|
|
580
|
+
streamId: streamingMessage._id,
|
|
581
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
582
|
+
},
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
}),
|
|
586
|
+
);
|
|
587
|
+
return {
|
|
588
|
+
messages: materializedStreams.flatMap(({ messages }) => messages),
|
|
589
|
+
materializationFailures: materializedStreams.flatMap(({ failure }) =>
|
|
590
|
+
failure ? [failure] : [],
|
|
591
|
+
),
|
|
592
|
+
};
|
|
593
593
|
}
|
package/src/component/threads.ts
CHANGED
|
@@ -3,8 +3,11 @@ import { paginator } from "convex-helpers/server/pagination";
|
|
|
3
3
|
import { partial } from "convex-helpers/validators";
|
|
4
4
|
import { paginationOptsValidator } from "convex/server";
|
|
5
5
|
import type { ObjectType } from "convex/values";
|
|
6
|
-
import {
|
|
7
|
-
|
|
6
|
+
import {
|
|
7
|
+
type ThreadDoc,
|
|
8
|
+
vPaginationResult,
|
|
9
|
+
vThreadDoc,
|
|
10
|
+
} from "../validators.js";
|
|
8
11
|
import { api, internal } from "./_generated/api.js";
|
|
9
12
|
import type { Doc } from "./_generated/dataModel.js";
|
|
10
13
|
import {
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Linter } from "eslint";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
|
|
4
|
+
describe("provider adapter boundary lint selectors", () => {
|
|
5
|
+
it("parse on Node 20 and reject every restricted dynamic import shape", async () => {
|
|
6
|
+
const configUrl = new URL("../eslint.config.js", import.meta.url).href;
|
|
7
|
+
const { default: eslintConfig } = (await import(configUrl)) as {
|
|
8
|
+
default: Array<{
|
|
9
|
+
files?: string[];
|
|
10
|
+
rules?: Record<string, unknown>;
|
|
11
|
+
}>;
|
|
12
|
+
};
|
|
13
|
+
const boundaryConfig = eslintConfig.find(
|
|
14
|
+
(config) =>
|
|
15
|
+
"files" in config &&
|
|
16
|
+
config.files?.some((pattern) => pattern.includes("src/streaming/")) &&
|
|
17
|
+
config.rules?.["no-restricted-syntax"],
|
|
18
|
+
);
|
|
19
|
+
const restrictedSyntax = boundaryConfig?.rules?.["no-restricted-syntax"];
|
|
20
|
+
expect(restrictedSyntax).toBeDefined();
|
|
21
|
+
|
|
22
|
+
const messages = new Linter({ configType: "flat" }).verify(
|
|
23
|
+
[
|
|
24
|
+
'import("ai");',
|
|
25
|
+
'import("ai/test");',
|
|
26
|
+
'import("@ai-sdk/provider");',
|
|
27
|
+
'import("vercel");',
|
|
28
|
+
'import("vercel/test");',
|
|
29
|
+
'import("../vercel");',
|
|
30
|
+
'import("../vercel/test");',
|
|
31
|
+
].join("\n"),
|
|
32
|
+
{
|
|
33
|
+
languageOptions: { ecmaVersion: 2020, sourceType: "module" },
|
|
34
|
+
rules: {
|
|
35
|
+
"no-restricted-syntax": restrictedSyntax as Linter.RuleEntry,
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
expect(messages).toHaveLength(7);
|
|
41
|
+
expect(messages.every((message) => message.ruleId === "no-restricted-syntax"))
|
|
42
|
+
.toBe(true);
|
|
43
|
+
});
|
|
44
|
+
});
|
package/src/react/index.ts
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { useMemo, useState } from "react";
|
|
3
|
-
export { toUIMessages, type UIMessage } from "../UIMessages.js";
|
|
3
|
+
export { toUIMessages, type UIMessage } from "../vercel/UIMessages.js";
|
|
4
4
|
|
|
5
|
-
export { optimisticallySendMessage } from "
|
|
5
|
+
export { optimisticallySendMessage } from "../vercel/react/optimisticallySendMessage.js";
|
|
6
6
|
export { useSmoothText } from "./useSmoothText.js";
|
|
7
7
|
export { SmoothText } from "./SmoothText.js";
|
|
8
8
|
export {
|
|
9
9
|
type ThreadMessagesQuery,
|
|
10
10
|
useThreadMessages,
|
|
11
11
|
useStreamingThreadMessages,
|
|
12
|
-
} from "
|
|
13
|
-
export {
|
|
14
|
-
|
|
12
|
+
} from "../vercel/react/useThreadMessages.js";
|
|
13
|
+
export {
|
|
14
|
+
type UIMessagesQuery,
|
|
15
|
+
useUIMessages,
|
|
16
|
+
} from "../vercel/react/useUIMessages.js";
|
|
17
|
+
export { useStreamingUIMessages } from "../vercel/react/useStreamingUIMessages.js";
|
|
15
18
|
|
|
16
19
|
/**
|
|
17
20
|
* @deprecated use useThreadMessages or useStreamingThreadMessages instead
|