@convex-dev/agent 0.1.14 → 0.1.15-alpha.1
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/dist/client/index.d.ts +496 -174
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +120 -165
- package/dist/client/index.js.map +1 -1
- package/dist/client/listMessages.d.ts +22 -0
- package/dist/client/listMessages.d.ts.map +1 -0
- package/dist/client/listMessages.js +25 -0
- package/dist/client/listMessages.js.map +1 -0
- package/dist/client/search.d.ts +162 -0
- package/dist/client/search.d.ts.map +1 -0
- package/dist/client/search.js +113 -0
- package/dist/client/search.js.map +1 -0
- package/dist/client/streaming.d.ts +17 -3
- package/dist/client/streaming.d.ts.map +1 -1
- package/dist/client/streaming.js +39 -1
- package/dist/client/streaming.js.map +1 -1
- package/dist/client/types.d.ts +5 -1
- package/dist/client/types.d.ts.map +1 -1
- package/dist/component/_generated/api.d.ts +14 -3
- package/dist/component/messages.d.ts +4 -4
- package/dist/component/messages.d.ts.map +1 -1
- package/dist/component/messages.js +12 -7
- package/dist/component/messages.js.map +1 -1
- package/dist/component/schema.d.ts +38 -28
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/schema.js +3 -2
- package/dist/component/schema.js.map +1 -1
- package/dist/component/streams.d.ts +7 -0
- package/dist/component/streams.d.ts.map +1 -1
- package/dist/component/streams.js +49 -22
- package/dist/component/streams.js.map +1 -1
- package/dist/component/vector/index.js +3 -3
- package/dist/component/vector/index.js.map +1 -1
- package/dist/react/deltas.d.ts.map +1 -1
- package/dist/react/deltas.js +2 -2
- package/dist/react/deltas.js.map +1 -1
- package/dist/react/index.d.ts +4 -2
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +22 -9
- package/dist/react/index.js.map +1 -1
- package/dist/react/optimisticallySendMessage.js +1 -1
- package/dist/react/optimisticallySendMessage.js.map +1 -1
- package/dist/validators.d.ts +49 -55
- package/dist/validators.d.ts.map +1 -1
- package/dist/validators.js +3 -8
- package/dist/validators.js.map +1 -1
- package/package.json +4 -3
- package/src/client/index.ts +249 -268
- package/src/client/listMessages.ts +38 -0
- package/src/client/search.ts +172 -0
- package/src/client/streaming.ts +56 -3
- package/src/client/types.ts +5 -1
- package/src/component/_generated/api.d.ts +14 -3
- package/src/component/messages.ts +13 -7
- package/src/component/schema.ts +3 -2
- package/src/component/streams.ts +85 -39
- package/src/component/vector/index.ts +4 -4
- package/src/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/src/react/deltas.ts +4 -2
- package/src/react/index.ts +23 -11
- package/src/react/optimisticallySendMessage.ts +1 -1
- package/src/validators.ts +8 -10
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { PaginationOptions, PaginationResult } from "convex/server";
|
|
2
|
+
import type { AgentComponent, RunQueryCtx } from "./types.js";
|
|
3
|
+
import type { MessageStatus } from "../validators.js";
|
|
4
|
+
import type { MessageDoc } from "../component/schema.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* List messages from a thread.
|
|
8
|
+
* @param ctx A ctx object from a query, mutation, or action.
|
|
9
|
+
* @param component The agent component, usually `components.agent`.
|
|
10
|
+
* @param args.threadId The thread to list messages from.
|
|
11
|
+
* @param args.paginationOpts Pagination options (e.g. via usePaginatedQuery).
|
|
12
|
+
* @param args.excludeToolMessages Whether to exclude tool messages.
|
|
13
|
+
* False by default.
|
|
14
|
+
* @param args.statuses What statuses to include. All by default.
|
|
15
|
+
* @returns The MessageDoc's in a format compatible with usePaginatedQuery.
|
|
16
|
+
*/
|
|
17
|
+
export async function listMessages(
|
|
18
|
+
ctx: RunQueryCtx,
|
|
19
|
+
component: AgentComponent,
|
|
20
|
+
args: {
|
|
21
|
+
threadId: string;
|
|
22
|
+
paginationOpts: PaginationOptions;
|
|
23
|
+
excludeToolMessages?: boolean;
|
|
24
|
+
statuses?: MessageStatus[];
|
|
25
|
+
}
|
|
26
|
+
): Promise<PaginationResult<MessageDoc>> {
|
|
27
|
+
if (args.paginationOpts.numItems === 0) {
|
|
28
|
+
return {
|
|
29
|
+
page: [],
|
|
30
|
+
isDone: true,
|
|
31
|
+
continueCursor: args.paginationOpts.cursor ?? "",
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
return ctx.runQuery(component.messages.listMessagesByThreadId, {
|
|
35
|
+
order: "desc",
|
|
36
|
+
...args,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AgentComponent,
|
|
3
|
+
ContextOptions,
|
|
4
|
+
RunActionCtx,
|
|
5
|
+
RunQueryCtx,
|
|
6
|
+
} from "./types.js";
|
|
7
|
+
import type { MessageDoc } from "../component/schema.js";
|
|
8
|
+
import type { CoreMessage } from "ai";
|
|
9
|
+
import { assert } from "convex-helpers";
|
|
10
|
+
import {
|
|
11
|
+
DEFAULT_MESSAGE_RANGE,
|
|
12
|
+
DEFAULT_RECENT_MESSAGES,
|
|
13
|
+
extractText,
|
|
14
|
+
} from "../shared.js";
|
|
15
|
+
|
|
16
|
+
const DEFAULT_VECTOR_SCORE_THRESHOLD = 0.0;
|
|
17
|
+
|
|
18
|
+
export type GetEmbedding = (text: string) => Promise<{
|
|
19
|
+
vector: number[];
|
|
20
|
+
vectorModel: string;
|
|
21
|
+
vectorScoreThreshold?: number;
|
|
22
|
+
}>;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Fetch the context messages for a thread.
|
|
26
|
+
* @param ctx Either a query, mutation, or action ctx.
|
|
27
|
+
* If it is not an action context, you can't do text or
|
|
28
|
+
* vector search.
|
|
29
|
+
* @param args The associated thread, user, message
|
|
30
|
+
* @returns
|
|
31
|
+
*/
|
|
32
|
+
export async function fetchContextMessages(
|
|
33
|
+
ctx: RunQueryCtx | RunActionCtx,
|
|
34
|
+
component: AgentComponent,
|
|
35
|
+
args: {
|
|
36
|
+
userId: string | undefined;
|
|
37
|
+
threadId: string | undefined;
|
|
38
|
+
messages: CoreMessage[];
|
|
39
|
+
/**
|
|
40
|
+
* If provided, it will search for messages up to and including this message.
|
|
41
|
+
* Note: if this is far in the past, text and vector search results may be more
|
|
42
|
+
* limited, as it's post-filtering the results.
|
|
43
|
+
*/
|
|
44
|
+
upToAndIncludingMessageId?: string;
|
|
45
|
+
contextOptions: ContextOptions;
|
|
46
|
+
getEmbedding?: GetEmbedding;
|
|
47
|
+
}
|
|
48
|
+
): Promise<MessageDoc[]> {
|
|
49
|
+
assert(args.userId || args.threadId, "Specify userId or threadId");
|
|
50
|
+
const opts = args.contextOptions;
|
|
51
|
+
// Fetch the latest messages from the thread
|
|
52
|
+
let included: Set<string> | undefined;
|
|
53
|
+
const contextMessages: MessageDoc[] = [];
|
|
54
|
+
if (
|
|
55
|
+
args.threadId &&
|
|
56
|
+
(opts.recentMessages !== 0 || args.upToAndIncludingMessageId)
|
|
57
|
+
) {
|
|
58
|
+
const { page } = await ctx.runQuery(
|
|
59
|
+
component.messages.listMessagesByThreadId,
|
|
60
|
+
{
|
|
61
|
+
threadId: args.threadId,
|
|
62
|
+
excludeToolMessages: opts.excludeToolMessages,
|
|
63
|
+
paginationOpts: {
|
|
64
|
+
numItems: opts.recentMessages ?? DEFAULT_RECENT_MESSAGES,
|
|
65
|
+
cursor: null,
|
|
66
|
+
},
|
|
67
|
+
upToAndIncludingMessageId: args.upToAndIncludingMessageId,
|
|
68
|
+
order: "desc",
|
|
69
|
+
statuses: ["success"],
|
|
70
|
+
}
|
|
71
|
+
);
|
|
72
|
+
included = new Set(page.map((m) => m._id));
|
|
73
|
+
contextMessages.push(
|
|
74
|
+
// Reverse since we fetched in descending order
|
|
75
|
+
...page.reverse()
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
if (opts.searchOptions?.textSearch || opts.searchOptions?.vectorSearch) {
|
|
79
|
+
const targetMessage = contextMessages.find(
|
|
80
|
+
(m) => m._id === args.upToAndIncludingMessageId
|
|
81
|
+
)?.message;
|
|
82
|
+
const messagesToSearch = targetMessage ? [targetMessage] : args.messages;
|
|
83
|
+
if (!("runAction" in ctx)) {
|
|
84
|
+
throw new Error("searchUserMessages only works in an action");
|
|
85
|
+
}
|
|
86
|
+
const lastMessage = messagesToSearch.at(-1)!;
|
|
87
|
+
assert(lastMessage, "No messages to search");
|
|
88
|
+
const text = extractText(lastMessage);
|
|
89
|
+
assert(text, `No text to search in message ${JSON.stringify(lastMessage)}`);
|
|
90
|
+
assert(
|
|
91
|
+
!args.contextOptions?.searchOptions?.vectorSearch || "runAction" in ctx,
|
|
92
|
+
"You must do vector search from an action"
|
|
93
|
+
);
|
|
94
|
+
if (opts.searchOptions?.vectorSearch && !args.getEmbedding) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
"You must provide an embedding and embeddingModel to use vector search"
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
const embeddingFields = opts.searchOptions?.vectorSearch
|
|
100
|
+
? await args.getEmbedding?.(text)
|
|
101
|
+
: undefined;
|
|
102
|
+
const searchMessages = await ctx.runAction(
|
|
103
|
+
component.messages.searchMessages,
|
|
104
|
+
{
|
|
105
|
+
searchAllMessagesForUserId: opts?.searchOtherThreads
|
|
106
|
+
? args.userId ??
|
|
107
|
+
(args.threadId &&
|
|
108
|
+
(
|
|
109
|
+
await ctx.runQuery(component.threads.getThread, {
|
|
110
|
+
threadId: args.threadId,
|
|
111
|
+
})
|
|
112
|
+
)?.userId)
|
|
113
|
+
: undefined,
|
|
114
|
+
threadId: args.threadId,
|
|
115
|
+
beforeMessageId: args.upToAndIncludingMessageId,
|
|
116
|
+
limit: opts.searchOptions?.limit ?? 10,
|
|
117
|
+
messageRange: {
|
|
118
|
+
...DEFAULT_MESSAGE_RANGE,
|
|
119
|
+
...opts.searchOptions?.messageRange,
|
|
120
|
+
},
|
|
121
|
+
text,
|
|
122
|
+
vectorScoreThreshold:
|
|
123
|
+
opts.searchOptions?.vectorScoreThreshold ??
|
|
124
|
+
DEFAULT_VECTOR_SCORE_THRESHOLD,
|
|
125
|
+
...embeddingFields,
|
|
126
|
+
}
|
|
127
|
+
);
|
|
128
|
+
// TODO: track what messages we used for context
|
|
129
|
+
contextMessages.unshift(
|
|
130
|
+
...searchMessages.filter((m) => !included?.has(m._id))
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
// Ensure we don't include tool messages without a corresponding tool call
|
|
134
|
+
return filterOutOrphanedToolMessages(
|
|
135
|
+
contextMessages.sort((a, b) =>
|
|
136
|
+
// Sort the raw MessageDocs by order and stepOrder
|
|
137
|
+
a.order === b.order ? a.stepOrder - b.stepOrder : a.order - b.order
|
|
138
|
+
)
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Filter out tool messages that don't have both a tool call and response.
|
|
144
|
+
* @param docs The messages to filter.
|
|
145
|
+
* @returns The filtered messages.
|
|
146
|
+
*/
|
|
147
|
+
export function filterOutOrphanedToolMessages(docs: MessageDoc[]) {
|
|
148
|
+
const toolCallIds = new Set<string>();
|
|
149
|
+
const result: MessageDoc[] = [];
|
|
150
|
+
for (const doc of docs) {
|
|
151
|
+
if (
|
|
152
|
+
doc.message?.role === "assistant" &&
|
|
153
|
+
Array.isArray(doc.message.content)
|
|
154
|
+
) {
|
|
155
|
+
for (const content of doc.message.content) {
|
|
156
|
+
if (content.type === "tool-call") {
|
|
157
|
+
toolCallIds.add(content.toolCallId);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
result.push(doc);
|
|
161
|
+
} else if (doc.message?.role === "tool") {
|
|
162
|
+
if (doc.message.content.every((c) => toolCallIds.has(c.toolCallId))) {
|
|
163
|
+
result.push(doc);
|
|
164
|
+
} else {
|
|
165
|
+
console.debug("Filtering out orphaned tool message", doc);
|
|
166
|
+
}
|
|
167
|
+
} else {
|
|
168
|
+
result.push(doc);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return result;
|
|
172
|
+
}
|
package/src/client/streaming.ts
CHANGED
|
@@ -6,11 +6,58 @@ import {
|
|
|
6
6
|
} from "ai";
|
|
7
7
|
import type {
|
|
8
8
|
ProviderOptions,
|
|
9
|
+
StreamArgs,
|
|
9
10
|
StreamDelta,
|
|
10
11
|
TextStreamPart,
|
|
11
12
|
} from "../validators.js";
|
|
12
|
-
import type {
|
|
13
|
-
import type {
|
|
13
|
+
import type { MessageDoc } from "../component/schema.js";
|
|
14
|
+
import type {
|
|
15
|
+
AgentComponent,
|
|
16
|
+
RunActionCtx,
|
|
17
|
+
RunQueryCtx,
|
|
18
|
+
SyncStreamsReturnValue,
|
|
19
|
+
} from "./types.js";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A function that handles fetching stream deltas, used with the React hooks
|
|
23
|
+
* `useThreadMessages` or `useStreamingThreadMessages`.
|
|
24
|
+
* @param ctx A ctx object from a query, mutation, or action.
|
|
25
|
+
* @param component The agent component, usually `components.agent`.
|
|
26
|
+
* @param args.threadId The thread to sync streams for.
|
|
27
|
+
* @param args.streamArgs The stream arguments with per-stream cursors.
|
|
28
|
+
* @returns The deltas for each stream from their existing cursor.
|
|
29
|
+
*/
|
|
30
|
+
export async function syncStreams(
|
|
31
|
+
ctx: RunQueryCtx,
|
|
32
|
+
component: AgentComponent,
|
|
33
|
+
args: {
|
|
34
|
+
threadId: string;
|
|
35
|
+
streamArgs: StreamArgs | undefined;
|
|
36
|
+
// By default, only streaming messages are included.
|
|
37
|
+
includeStatuses?: ("streaming" | "finished" | "aborted")[];
|
|
38
|
+
}
|
|
39
|
+
): Promise<SyncStreamsReturnValue | undefined> {
|
|
40
|
+
if (!args.streamArgs) return undefined;
|
|
41
|
+
if (args.streamArgs.kind === "list") {
|
|
42
|
+
return {
|
|
43
|
+
kind: "list",
|
|
44
|
+
messages: await ctx.runQuery(component.streams.list, {
|
|
45
|
+
threadId: args.threadId,
|
|
46
|
+
startOrder: args.streamArgs.startOrder,
|
|
47
|
+
statuses: args.includeStatuses,
|
|
48
|
+
}),
|
|
49
|
+
};
|
|
50
|
+
} else {
|
|
51
|
+
return {
|
|
52
|
+
kind: "deltas",
|
|
53
|
+
deltas: await ctx.runQuery(component.streams.listDeltas, {
|
|
54
|
+
threadId: args.threadId,
|
|
55
|
+
cursors: args.streamArgs.cursors,
|
|
56
|
+
}),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
14
61
|
|
|
15
62
|
export type StreamingOptions = {
|
|
16
63
|
/**
|
|
@@ -96,7 +143,13 @@ export class DeltaStreamer {
|
|
|
96
143
|
this.#nextStepOrder = (metadata.stepOrder ?? 0) + 1;
|
|
97
144
|
this.abortController = new AbortController();
|
|
98
145
|
if (metadata.abortSignal) {
|
|
99
|
-
metadata.abortSignal.addEventListener("abort", () => {
|
|
146
|
+
metadata.abortSignal.addEventListener("abort", async () => {
|
|
147
|
+
if (this.streamId) {
|
|
148
|
+
await this.ctx.runMutation(this.component.streams.abort, {
|
|
149
|
+
streamId: this.streamId,
|
|
150
|
+
reason: "abortSignal",
|
|
151
|
+
});
|
|
152
|
+
}
|
|
100
153
|
this.abortController.abort();
|
|
101
154
|
});
|
|
102
155
|
}
|
package/src/client/types.ts
CHANGED
|
@@ -77,6 +77,10 @@ export type ContextOptions = {
|
|
|
77
77
|
* At least one of textSearch or vectorSearch must be true.
|
|
78
78
|
*/
|
|
79
79
|
vectorSearch?: boolean;
|
|
80
|
+
/**
|
|
81
|
+
* The score threshold for vector search. Default is 0.0.
|
|
82
|
+
*/
|
|
83
|
+
vectorScoreThreshold?: number;
|
|
80
84
|
/**
|
|
81
85
|
* What messages around the search results to include.
|
|
82
86
|
* Default: { before: 2, after: 1 }
|
|
@@ -144,7 +148,7 @@ export type UsageHandler = (
|
|
|
144
148
|
) => void | Promise<void>;
|
|
145
149
|
|
|
146
150
|
export type RawRequestResponseHandler = (
|
|
147
|
-
ctx:
|
|
151
|
+
ctx: ActionCtx,
|
|
148
152
|
args: {
|
|
149
153
|
userId: string | undefined;
|
|
150
154
|
threadId: string | undefined;
|
|
@@ -900,13 +900,13 @@ export type Mounts = {
|
|
|
900
900
|
"public",
|
|
901
901
|
{
|
|
902
902
|
beforeMessageId?: string;
|
|
903
|
+
embedding?: Array<number>;
|
|
904
|
+
embeddingModel?: string;
|
|
903
905
|
limit: number;
|
|
904
906
|
messageRange?: { after: number; before: number };
|
|
905
907
|
searchAllMessagesForUserId?: string;
|
|
906
908
|
text?: string;
|
|
907
909
|
threadId?: string;
|
|
908
|
-
vector?: Array<number>;
|
|
909
|
-
vectorModel?: string;
|
|
910
910
|
vectorScoreThreshold?: number;
|
|
911
911
|
},
|
|
912
912
|
Array<{
|
|
@@ -1433,6 +1433,12 @@ export type Mounts = {
|
|
|
1433
1433
|
>;
|
|
1434
1434
|
};
|
|
1435
1435
|
streams: {
|
|
1436
|
+
abort: FunctionReference<
|
|
1437
|
+
"mutation",
|
|
1438
|
+
"public",
|
|
1439
|
+
{ reason: string; streamId: string },
|
|
1440
|
+
null
|
|
1441
|
+
>;
|
|
1436
1442
|
addDelta: FunctionReference<
|
|
1437
1443
|
"mutation",
|
|
1438
1444
|
"public",
|
|
@@ -1588,13 +1594,18 @@ export type Mounts = {
|
|
|
1588
1594
|
list: FunctionReference<
|
|
1589
1595
|
"query",
|
|
1590
1596
|
"public",
|
|
1591
|
-
{
|
|
1597
|
+
{
|
|
1598
|
+
startOrder?: number;
|
|
1599
|
+
statuses?: Array<"streaming" | "finished" | "aborted">;
|
|
1600
|
+
threadId: string;
|
|
1601
|
+
},
|
|
1592
1602
|
Array<{
|
|
1593
1603
|
agentName?: string;
|
|
1594
1604
|
model?: string;
|
|
1595
1605
|
order: number;
|
|
1596
1606
|
provider?: string;
|
|
1597
1607
|
providerOptions?: Record<string, Record<string, any>>;
|
|
1608
|
+
status: "streaming" | "finished" | "aborted";
|
|
1598
1609
|
stepOrder: number;
|
|
1599
1610
|
streamId: string;
|
|
1600
1611
|
userId?: string;
|
|
@@ -13,7 +13,6 @@ import {
|
|
|
13
13
|
vMessageStatus,
|
|
14
14
|
vMessageWithMetadataInternal,
|
|
15
15
|
vPaginationResult,
|
|
16
|
-
vSearchOptions,
|
|
17
16
|
} from "../validators.js";
|
|
18
17
|
import { api, internal } from "./_generated/api.js";
|
|
19
18
|
import type { Doc, Id } from "./_generated/dataModel.js";
|
|
@@ -467,7 +466,14 @@ export const searchMessages = action({
|
|
|
467
466
|
threadId: v.optional(v.id("threads")),
|
|
468
467
|
searchAllMessagesForUserId: v.optional(v.string()),
|
|
469
468
|
beforeMessageId: v.optional(v.id("messages")),
|
|
470
|
-
|
|
469
|
+
embedding: v.optional(v.array(v.number())),
|
|
470
|
+
embeddingModel: v.optional(v.string()),
|
|
471
|
+
text: v.optional(v.string()),
|
|
472
|
+
limit: v.number(),
|
|
473
|
+
vectorScoreThreshold: v.optional(v.number()),
|
|
474
|
+
messageRange: v.optional(
|
|
475
|
+
v.object({ before: v.number(), after: v.number() })
|
|
476
|
+
),
|
|
471
477
|
},
|
|
472
478
|
returns: v.array(vMessageDoc),
|
|
473
479
|
handler: async (ctx, args): Promise<MessageDoc[]> => {
|
|
@@ -486,15 +492,15 @@ export const searchMessages = action({
|
|
|
486
492
|
beforeMessageId: args.beforeMessageId,
|
|
487
493
|
});
|
|
488
494
|
}
|
|
489
|
-
if (args.
|
|
490
|
-
const dimension = args.
|
|
495
|
+
if (args.embedding) {
|
|
496
|
+
const dimension = args.embedding.length as VectorDimension;
|
|
491
497
|
if (!VectorDimensions.includes(dimension)) {
|
|
492
|
-
throw new Error(`Unsupported
|
|
498
|
+
throw new Error(`Unsupported embedding dimension: ${dimension}`);
|
|
493
499
|
}
|
|
494
500
|
const vectors = (
|
|
495
|
-
await searchVectors(ctx, args.
|
|
501
|
+
await searchVectors(ctx, args.embedding, {
|
|
496
502
|
dimension,
|
|
497
|
-
model: args.
|
|
503
|
+
model: args.embeddingModel ?? "unknown",
|
|
498
504
|
table: "messages",
|
|
499
505
|
searchAllMessagesForUserId: args.searchAllMessagesForUserId,
|
|
500
506
|
threadId: args.threadId,
|
package/src/component/schema.ts
CHANGED
|
@@ -113,10 +113,11 @@ export const schema = defineSchema({
|
|
|
113
113
|
v.object({
|
|
114
114
|
kind: v.literal("finished"),
|
|
115
115
|
endedAt: v.number(),
|
|
116
|
+
cleanupFnId: v.optional(v.id("_scheduled_functions")),
|
|
116
117
|
}),
|
|
117
118
|
v.object({
|
|
118
|
-
kind: v.literal("
|
|
119
|
-
|
|
119
|
+
kind: v.literal("aborted"),
|
|
120
|
+
reason: v.string(),
|
|
120
121
|
})
|
|
121
122
|
),
|
|
122
123
|
})
|
package/src/component/streams.ts
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
vStreamMessage,
|
|
7
7
|
} from "../validators.js";
|
|
8
8
|
import { api, internal } from "./_generated/api.js";
|
|
9
|
-
import type { Id } from "./_generated/dataModel.js";
|
|
9
|
+
import type { Doc, Id } from "./_generated/dataModel.js";
|
|
10
10
|
import {
|
|
11
11
|
internalMutation,
|
|
12
12
|
mutation,
|
|
@@ -106,33 +106,87 @@ export const create = mutation({
|
|
|
106
106
|
export const list = query({
|
|
107
107
|
args: {
|
|
108
108
|
threadId: v.id("threads"),
|
|
109
|
+
startOrder: v.optional(v.number()),
|
|
110
|
+
statuses: v.optional(
|
|
111
|
+
v.array(
|
|
112
|
+
v.union(
|
|
113
|
+
v.literal("streaming"),
|
|
114
|
+
v.literal("finished"),
|
|
115
|
+
v.literal("aborted")
|
|
116
|
+
)
|
|
117
|
+
)
|
|
118
|
+
),
|
|
109
119
|
},
|
|
110
120
|
returns: v.array(vStreamMessage),
|
|
111
121
|
handler: async (ctx, args) => {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
.
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
122
|
+
const statuses = args.statuses ?? ["streaming"];
|
|
123
|
+
const messages = await mergedStream(
|
|
124
|
+
statuses.map((status) =>
|
|
125
|
+
stream(ctx.db, schema)
|
|
126
|
+
.query("streamingMessages")
|
|
127
|
+
.withIndex("threadId_state_order_stepOrder", (q) =>
|
|
128
|
+
q
|
|
129
|
+
.eq("threadId", args.threadId)
|
|
130
|
+
.eq("state.kind", status)
|
|
131
|
+
.gte("order", args.startOrder ?? 0)
|
|
132
|
+
)
|
|
133
|
+
.order("desc")
|
|
134
|
+
),
|
|
135
|
+
["order", "stepOrder"]
|
|
136
|
+
).take(100);
|
|
137
|
+
|
|
138
|
+
return messages.map((m) => ({
|
|
139
|
+
streamId: m._id,
|
|
140
|
+
status: m.state.kind,
|
|
141
|
+
...pick(m, [
|
|
142
|
+
"order",
|
|
143
|
+
"stepOrder",
|
|
144
|
+
"userId",
|
|
145
|
+
"agentName",
|
|
146
|
+
"model",
|
|
147
|
+
"provider",
|
|
148
|
+
"providerOptions",
|
|
149
|
+
]),
|
|
150
|
+
}));
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
export const abort = mutation({
|
|
155
|
+
args: {
|
|
156
|
+
streamId: v.id("streamingMessages"),
|
|
157
|
+
reason: v.string(),
|
|
158
|
+
},
|
|
159
|
+
returns: v.null(),
|
|
160
|
+
handler: async (ctx, args) => {
|
|
161
|
+
const stream = await ctx.db.get(args.streamId);
|
|
162
|
+
if (!stream) {
|
|
163
|
+
throw new Error(`Stream not found: ${args.streamId}`);
|
|
164
|
+
}
|
|
165
|
+
if (stream.state.kind !== "streaming") {
|
|
166
|
+
console.warn(
|
|
167
|
+
`Stream trying to abort but not currently streaming (${stream.state.kind}): ${args.streamId}`
|
|
132
168
|
);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
await cleanupTimeoutFn(ctx, stream);
|
|
172
|
+
await ctx.db.patch(args.streamId, {
|
|
173
|
+
state: { kind: "aborted", reason: args.reason },
|
|
174
|
+
});
|
|
133
175
|
},
|
|
134
176
|
});
|
|
135
177
|
|
|
178
|
+
async function cleanupTimeoutFn(
|
|
179
|
+
ctx: MutationCtx,
|
|
180
|
+
stream: Doc<"streamingMessages">
|
|
181
|
+
) {
|
|
182
|
+
if (stream.state.kind === "streaming" && stream.state.timeoutFnId) {
|
|
183
|
+
const timeoutFn = await ctx.db.system.get(stream.state.timeoutFnId);
|
|
184
|
+
if (timeoutFn?.state.kind === "pending") {
|
|
185
|
+
await ctx.scheduler.cancel(stream.state.timeoutFnId);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
136
190
|
export const finish = mutation({
|
|
137
191
|
args: {
|
|
138
192
|
streamId: v.id("streamingMessages"),
|
|
@@ -153,20 +207,15 @@ export const finish = mutation({
|
|
|
153
207
|
);
|
|
154
208
|
return;
|
|
155
209
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
if (timeoutFn?.state.kind === "pending") {
|
|
159
|
-
await ctx.scheduler.cancel(stream.state.timeoutFnId);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
await ctx.db.patch(args.streamId, {
|
|
163
|
-
state: { kind: "finished", endedAt: Date.now() },
|
|
164
|
-
});
|
|
165
|
-
await ctx.scheduler.runAfter(
|
|
210
|
+
await cleanupTimeoutFn(ctx, stream);
|
|
211
|
+
const cleanupFnId = await ctx.scheduler.runAfter(
|
|
166
212
|
DELETE_STREAM_DELAY,
|
|
167
213
|
api.streams.deleteStreamAsync,
|
|
168
214
|
{ streamId: args.streamId }
|
|
169
215
|
);
|
|
216
|
+
await ctx.db.patch(args.streamId, {
|
|
217
|
+
state: { kind: "finished", endedAt: Date.now(), cleanupFnId },
|
|
218
|
+
});
|
|
170
219
|
},
|
|
171
220
|
});
|
|
172
221
|
|
|
@@ -223,8 +272,8 @@ export const timeoutStream = internalMutation({
|
|
|
223
272
|
}
|
|
224
273
|
await ctx.db.patch(args.streamId, {
|
|
225
274
|
state: {
|
|
226
|
-
kind: "
|
|
227
|
-
|
|
275
|
+
kind: "aborted",
|
|
276
|
+
reason: "timeout",
|
|
228
277
|
},
|
|
229
278
|
});
|
|
230
279
|
},
|
|
@@ -245,12 +294,9 @@ async function deletePageForStreamId(
|
|
|
245
294
|
if (deltas.isDone) {
|
|
246
295
|
const stream = await ctx.db.get(args.streamId);
|
|
247
296
|
if (stream) {
|
|
248
|
-
|
|
249
|
-
if (state.kind === "
|
|
250
|
-
|
|
251
|
-
if (timeoutFn?.state.kind === "pending") {
|
|
252
|
-
await ctx.scheduler.cancel(state.timeoutFnId);
|
|
253
|
-
}
|
|
297
|
+
await cleanupTimeoutFn(ctx, stream);
|
|
298
|
+
if (stream.state.kind === "finished" && stream.state.cleanupFnId) {
|
|
299
|
+
await ctx.scheduler.cancel(stream.state.cleanupFnId);
|
|
254
300
|
}
|
|
255
301
|
await ctx.db.delete(args.streamId);
|
|
256
302
|
}
|
|
@@ -182,10 +182,10 @@ export const updateBatch = mutation({
|
|
|
182
182
|
returns: v.null(),
|
|
183
183
|
handler: async (ctx, args) => {
|
|
184
184
|
await Promise.all(
|
|
185
|
-
args.vectors.map((
|
|
186
|
-
ctx.db.patch(
|
|
187
|
-
model:
|
|
188
|
-
vector:
|
|
185
|
+
args.vectors.map((embedding) =>
|
|
186
|
+
ctx.db.patch(embedding.id, {
|
|
187
|
+
model: embedding.model,
|
|
188
|
+
vector: embedding.vector,
|
|
189
189
|
})
|
|
190
190
|
)
|
|
191
191
|
);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"3.2.4","results":[[":client/index.test.ts",{"duration":
|
|
1
|
+
{"version":"3.2.4","results":[[":client/index.test.ts",{"duration":75.65995979309082,"failed":false}],[":component/messages.test.ts",{"duration":43.858792304992676,"failed":false}],[":react/toUIMessages.test.ts",{"duration":4.371167182922363,"failed":false}]]}
|
package/src/react/deltas.ts
CHANGED
|
@@ -256,12 +256,14 @@ export function createStreamingMessage(
|
|
|
256
256
|
): MessageDoc {
|
|
257
257
|
const { streamId, ...rest } = message;
|
|
258
258
|
const metadata: MessageDoc = {
|
|
259
|
+
...rest,
|
|
259
260
|
_id: `${streamId}-${index}`,
|
|
260
261
|
_creationTime: Date.now(),
|
|
261
|
-
status:
|
|
262
|
+
status: (
|
|
263
|
+
{ streaming: "pending", finished: "success", aborted: "failed" } as const
|
|
264
|
+
)[message.status],
|
|
262
265
|
threadId,
|
|
263
266
|
tool: false,
|
|
264
|
-
...rest,
|
|
265
267
|
};
|
|
266
268
|
switch (part.type) {
|
|
267
269
|
case "text-delta":
|