@convex-dev/agent 0.5.0-alpha.1 → 0.6.0-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/MIGRATION.md +153 -0
- package/README.md +32 -27
- package/dist/UIMessages.d.ts +46 -0
- package/dist/UIMessages.d.ts.map +1 -0
- package/dist/UIMessages.js +546 -0
- package/dist/UIMessages.js.map +1 -0
- package/dist/client/createTool.d.ts +126 -27
- package/dist/client/createTool.d.ts.map +1 -1
- package/dist/client/createTool.js +67 -12
- package/dist/client/createTool.js.map +1 -1
- package/dist/client/defaultComponent.d.ts +11 -0
- package/dist/client/defaultComponent.d.ts.map +1 -0
- package/dist/client/defaultComponent.js +7 -0
- package/dist/client/defaultComponent.js.map +1 -0
- package/dist/client/definePlaygroundAPI.d.ts +1335 -204
- package/dist/client/definePlaygroundAPI.d.ts.map +1 -1
- package/dist/client/definePlaygroundAPI.js +52 -28
- package/dist/client/definePlaygroundAPI.js.map +1 -1
- package/dist/client/files.d.ts +20 -7
- package/dist/client/files.d.ts.map +1 -1
- package/dist/client/files.js +68 -11
- package/dist/client/files.js.map +1 -1
- package/dist/client/index.d.ts +1116 -978
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +332 -747
- package/dist/client/index.js.map +1 -1
- package/dist/client/messages.d.ts +461 -0
- package/dist/client/messages.d.ts.map +1 -0
- package/dist/client/messages.js +106 -0
- package/dist/client/messages.js.map +1 -0
- package/dist/client/mockModel.d.ts +42 -0
- package/dist/client/mockModel.d.ts.map +1 -0
- package/dist/client/mockModel.js +182 -0
- package/dist/client/mockModel.js.map +1 -0
- package/dist/client/saveInputMessages.d.ts +20 -0
- package/dist/client/saveInputMessages.d.ts.map +1 -0
- package/dist/client/saveInputMessages.js +58 -0
- package/dist/client/saveInputMessages.js.map +1 -0
- package/dist/client/search.d.ts +350 -39
- package/dist/client/search.d.ts.map +1 -1
- package/dist/client/search.js +350 -39
- package/dist/client/search.js.map +1 -1
- package/dist/client/start.d.ts +84 -0
- package/dist/client/start.d.ts.map +1 -0
- package/dist/client/start.js +185 -0
- package/dist/client/start.js.map +1 -0
- package/dist/client/streamText.d.ts +46 -0
- package/dist/client/streamText.d.ts.map +1 -0
- package/dist/client/streamText.js +117 -0
- package/dist/client/streamText.js.map +1 -0
- package/dist/client/streaming.d.ts +3716 -32
- package/dist/client/streaming.d.ts.map +1 -1
- package/dist/client/streaming.js +161 -59
- package/dist/client/streaming.js.map +1 -1
- package/dist/client/threads.d.ts +46 -0
- package/dist/client/threads.d.ts.map +1 -0
- package/dist/client/threads.js +49 -0
- package/dist/client/threads.js.map +1 -0
- package/dist/client/types.d.ts +266 -128
- package/dist/client/types.d.ts.map +1 -1
- package/dist/client/utils.d.ts +4 -0
- package/dist/client/utils.d.ts.map +1 -0
- package/dist/client/utils.js +21 -0
- package/dist/client/utils.js.map +1 -0
- package/dist/component/_generated/api.d.ts +24 -2178
- package/dist/component/_generated/api.d.ts.map +1 -1
- package/dist/component/_generated/api.js +10 -1
- package/dist/component/_generated/api.js.map +1 -1
- package/dist/component/_generated/component.d.ts +3120 -0
- package/dist/component/_generated/component.d.ts.map +1 -0
- package/dist/component/_generated/component.js +11 -0
- package/dist/component/_generated/component.js.map +1 -0
- package/dist/component/_generated/dataModel.d.ts +4 -18
- package/dist/component/_generated/dataModel.d.ts.map +1 -0
- package/dist/component/_generated/dataModel.js +11 -0
- package/dist/component/_generated/dataModel.js.map +1 -0
- package/dist/component/_generated/server.d.ts +10 -38
- package/dist/component/_generated/server.d.ts.map +1 -1
- package/dist/component/_generated/server.js +9 -5
- package/dist/component/_generated/server.js.map +1 -1
- package/dist/component/files.d.ts +16 -10
- package/dist/component/files.d.ts.map +1 -1
- package/dist/component/files.js +10 -2
- package/dist/component/files.js.map +1 -1
- package/dist/component/messages.d.ts +2578 -366
- package/dist/component/messages.d.ts.map +1 -1
- package/dist/component/messages.js +397 -154
- package/dist/component/messages.js.map +1 -1
- package/dist/component/schema.d.ts +5697 -3584
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/schema.js +18 -41
- package/dist/component/schema.js.map +1 -1
- package/dist/component/streams.d.ts +39 -339
- package/dist/component/streams.d.ts.map +1 -1
- package/dist/component/streams.js +114 -73
- package/dist/component/streams.js.map +1 -1
- package/dist/component/threads.d.ts +13 -13
- package/dist/component/users.d.ts +7 -7
- package/dist/component/vector/index.d.ts +1 -1
- package/dist/component/vector/index.d.ts.map +1 -1
- package/dist/component/vector/index.js +1 -3
- package/dist/component/vector/index.js.map +1 -1
- package/dist/deltas.d.ts +43 -0
- package/dist/deltas.d.ts.map +1 -0
- package/dist/deltas.js +446 -0
- package/dist/deltas.js.map +1 -0
- package/dist/mapping.d.ts +38 -20
- package/dist/mapping.d.ts.map +1 -1
- package/dist/mapping.js +365 -97
- package/dist/mapping.js.map +1 -1
- package/dist/react/SmoothText.d.ts +5 -0
- package/dist/react/SmoothText.d.ts.map +1 -0
- package/dist/react/SmoothText.js +6 -0
- package/dist/react/SmoothText.js.map +1 -0
- package/dist/react/index.d.ts +5 -77
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +6 -160
- package/dist/react/index.js.map +1 -1
- package/dist/react/optimisticallySendMessage.d.ts +36 -3
- package/dist/react/optimisticallySendMessage.d.ts.map +1 -1
- package/dist/react/optimisticallySendMessage.js +35 -9
- package/dist/react/optimisticallySendMessage.js.map +1 -1
- package/dist/react/types.d.ts +4 -18
- package/dist/react/types.d.ts.map +1 -1
- package/dist/react/useDeltaStreams.d.ts +10 -0
- package/dist/react/useDeltaStreams.d.ts.map +1 -0
- package/dist/react/useDeltaStreams.js +106 -0
- package/dist/react/useDeltaStreams.js.map +1 -0
- package/dist/react/useSmoothText.d.ts +13 -12
- package/dist/react/useSmoothText.d.ts.map +1 -1
- package/dist/react/useSmoothText.js +32 -15
- package/dist/react/useSmoothText.js.map +1 -1
- package/dist/react/useStreamingUIMessages.d.ts +22 -0
- package/dist/react/useStreamingUIMessages.d.ts.map +1 -0
- package/dist/react/useStreamingUIMessages.js +92 -0
- package/dist/react/useStreamingUIMessages.js.map +1 -0
- package/dist/react/useThreadMessages.d.ts +104 -0
- package/dist/react/useThreadMessages.d.ts.map +1 -0
- package/dist/react/useThreadMessages.js +148 -0
- package/dist/react/useThreadMessages.js.map +1 -0
- package/dist/react/useUIMessages.d.ts +96 -0
- package/dist/react/useUIMessages.d.ts.map +1 -0
- package/dist/react/useUIMessages.js +108 -0
- package/dist/react/useUIMessages.js.map +1 -0
- package/dist/shared.d.ts +20 -4
- package/dist/shared.d.ts.map +1 -1
- package/dist/shared.js +45 -8
- package/dist/shared.js.map +1 -1
- package/dist/validators.d.ts +22981 -5666
- package/dist/validators.d.ts.map +1 -1
- package/dist/validators.js +245 -137
- package/dist/validators.js.map +1 -1
- package/package.json +101 -51
- package/src/UIMessages.combineUIMessages.test.ts +239 -0
- package/src/UIMessages.test.ts +273 -0
- package/src/UIMessages.ts +739 -0
- package/src/client/approval.test.ts +350 -0
- package/src/client/createTool.ts +291 -76
- package/src/client/defaultComponent.ts +17 -0
- package/src/client/definePlaygroundAPI.ts +67 -31
- package/src/client/files.ts +100 -20
- package/src/client/index.test.ts +40 -85
- package/src/client/index.ts +638 -1289
- package/src/client/messages.ts +237 -0
- package/src/client/mockModel.ts +252 -0
- package/src/client/saveInputMessages.test.ts +583 -0
- package/src/client/saveInputMessages.ts +101 -0
- package/src/client/search.test.ts +1207 -0
- package/src/client/search.ts +581 -70
- package/src/client/start.ts +327 -0
- package/src/client/streamText.ts +187 -0
- package/src/client/streaming.test.ts +186 -0
- package/src/client/streaming.ts +241 -97
- package/src/client/threads.ts +83 -0
- package/src/client/types.ts +370 -219
- package/src/client/utils.ts +27 -0
- package/src/component/_generated/api.ts +64 -0
- package/src/component/_generated/component.ts +4902 -0
- package/src/component/_generated/{server.d.ts → server.ts} +33 -21
- package/src/component/files.ts +11 -2
- package/src/component/messages.test.ts +195 -51
- package/src/component/messages.ts +500 -201
- package/src/component/schema.ts +20 -46
- package/src/component/setup.test.ts +7 -0
- package/src/component/streams.ts +184 -83
- package/src/component/users.test.ts +0 -1
- package/src/component/vector/index.ts +1 -3
- package/src/deltas.test.ts +626 -0
- package/src/deltas.ts +569 -0
- package/src/fromUIMessages.test.ts +497 -0
- package/src/mapping.test.ts +180 -6
- package/src/mapping.ts +479 -162
- package/src/react/SmoothText.tsx +9 -0
- package/src/react/index.ts +10 -230
- package/src/react/optimisticallySendMessage.ts +55 -12
- package/src/react/types.ts +6 -39
- package/src/react/useDeltaStreams.ts +160 -0
- package/src/react/useSmoothText.ts +56 -36
- package/src/react/useStreamingUIMessages.ts +143 -0
- package/src/react/useThreadMessages.ts +262 -0
- package/src/react/useUIMessages.test.ts +255 -0
- package/src/react/useUIMessages.ts +195 -0
- package/src/shared.ts +88 -12
- package/src/test.ts +18 -0
- package/src/toUIMessages.test.ts +1269 -0
- package/src/validators.test.ts +18 -19
- package/src/validators.ts +325 -185
- package/dist/client/_generated/_ignore.d.ts +0 -1
- package/dist/client/_generated/_ignore.d.ts.map +0 -1
- package/dist/client/_generated/_ignore.js +0 -3
- package/dist/client/_generated/_ignore.js.map +0 -1
- package/dist/client/listMessages.d.ts +0 -22
- package/dist/client/listMessages.d.ts.map +0 -1
- package/dist/client/listMessages.js +0 -25
- package/dist/client/listMessages.js.map +0 -1
- package/dist/package.json +0 -3
- package/dist/react/deltas.d.ts +0 -26
- package/dist/react/deltas.d.ts.map +0 -1
- package/dist/react/deltas.js +0 -384
- package/dist/react/deltas.js.map +0 -1
- package/dist/react/toUIMessages.d.ts +0 -15
- package/dist/react/toUIMessages.d.ts.map +0 -1
- package/dist/react/toUIMessages.js +0 -211
- package/dist/react/toUIMessages.js.map +0 -1
- package/src/client/listMessages.ts +0 -38
- package/src/component/_generated/api.d.ts +0 -2202
- package/src/component/_generated/api.js +0 -23
- package/src/component/_generated/server.js +0 -90
- package/src/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
- package/src/react/deltas.test.ts +0 -315
- package/src/react/deltas.ts +0 -478
- package/src/react/toUIMessages.test.ts +0 -420
- package/src/react/toUIMessages.ts +0 -253
- package/src/vitest.config.ts +0 -7
- /package/src/component/_generated/{dataModel.d.ts → dataModel.ts} +0 -0
package/src/client/streaming.ts
CHANGED
|
@@ -1,26 +1,40 @@
|
|
|
1
1
|
import {
|
|
2
|
-
type ChunkDetector,
|
|
3
2
|
smoothStream,
|
|
3
|
+
type AsyncIterableStream,
|
|
4
|
+
type ChunkDetector,
|
|
4
5
|
type StreamTextTransform,
|
|
6
|
+
type TextStreamPart,
|
|
5
7
|
type ToolSet,
|
|
8
|
+
type UIMessageChunk,
|
|
6
9
|
} from "ai";
|
|
7
|
-
import
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
import { v } from "convex/values";
|
|
11
|
+
import {
|
|
12
|
+
vMessageDoc,
|
|
13
|
+
vPaginationResult,
|
|
14
|
+
vStreamDelta,
|
|
15
|
+
vStreamMessage,
|
|
16
|
+
type ProviderOptions,
|
|
17
|
+
type StreamArgs,
|
|
18
|
+
type StreamDelta,
|
|
19
|
+
type StreamMessage,
|
|
13
20
|
} from "../validators.js";
|
|
14
|
-
import type { MessageDoc } from "../component/schema.js";
|
|
15
21
|
import type {
|
|
22
|
+
ActionCtx,
|
|
16
23
|
AgentComponent,
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
RunQueryCtx,
|
|
24
|
+
MutationCtx,
|
|
25
|
+
QueryCtx,
|
|
20
26
|
SyncStreamsReturnValue,
|
|
21
27
|
} from "./types.js";
|
|
22
|
-
|
|
23
|
-
|
|
28
|
+
|
|
29
|
+
export const vStreamMessagesReturnValue = v.object({
|
|
30
|
+
...vPaginationResult(vMessageDoc).fields,
|
|
31
|
+
streams: v.optional(
|
|
32
|
+
v.union(
|
|
33
|
+
v.object({ kind: v.literal("list"), messages: v.array(vStreamMessage) }),
|
|
34
|
+
v.object({ kind: v.literal("deltas"), deltas: v.array(vStreamDelta) }),
|
|
35
|
+
),
|
|
36
|
+
),
|
|
37
|
+
});
|
|
24
38
|
|
|
25
39
|
/**
|
|
26
40
|
* A function that handles fetching stream deltas, used with the React hooks
|
|
@@ -32,42 +46,47 @@ import type { Infer } from "convex/values";
|
|
|
32
46
|
* @returns The deltas for each stream from their existing cursor.
|
|
33
47
|
*/
|
|
34
48
|
export async function syncStreams(
|
|
35
|
-
ctx:
|
|
49
|
+
ctx: QueryCtx | MutationCtx | ActionCtx,
|
|
36
50
|
component: AgentComponent,
|
|
37
|
-
|
|
51
|
+
{
|
|
52
|
+
threadId,
|
|
53
|
+
streamArgs,
|
|
54
|
+
includeStatuses,
|
|
55
|
+
}: {
|
|
38
56
|
threadId: string;
|
|
39
|
-
streamArgs
|
|
57
|
+
streamArgs?: StreamArgs | undefined;
|
|
40
58
|
// By default, only streaming messages are included.
|
|
41
59
|
includeStatuses?: ("streaming" | "finished" | "aborted")[];
|
|
42
60
|
},
|
|
43
61
|
): Promise<SyncStreamsReturnValue | undefined> {
|
|
44
|
-
if (!
|
|
45
|
-
if (
|
|
62
|
+
if (!streamArgs) return undefined;
|
|
63
|
+
if (streamArgs.kind === "list") {
|
|
46
64
|
return {
|
|
47
65
|
kind: "list",
|
|
48
66
|
messages: await listStreams(ctx, component, {
|
|
49
|
-
threadId
|
|
50
|
-
startOrder:
|
|
51
|
-
includeStatuses
|
|
67
|
+
threadId,
|
|
68
|
+
startOrder: streamArgs.startOrder,
|
|
69
|
+
includeStatuses,
|
|
52
70
|
}),
|
|
53
71
|
};
|
|
54
72
|
} else {
|
|
55
73
|
return {
|
|
56
74
|
kind: "deltas",
|
|
57
75
|
deltas: await ctx.runQuery(component.streams.listDeltas, {
|
|
58
|
-
threadId
|
|
59
|
-
cursors:
|
|
76
|
+
threadId,
|
|
77
|
+
cursors: streamArgs.cursors,
|
|
60
78
|
}),
|
|
61
79
|
};
|
|
62
80
|
}
|
|
63
81
|
}
|
|
64
82
|
|
|
65
83
|
export async function abortStream(
|
|
66
|
-
ctx:
|
|
84
|
+
ctx: MutationCtx | ActionCtx,
|
|
67
85
|
component: AgentComponent,
|
|
68
|
-
args: {
|
|
69
|
-
|
|
70
|
-
|
|
86
|
+
args: { reason: string } & (
|
|
87
|
+
| { streamId: string }
|
|
88
|
+
| { threadId: string; order: number }
|
|
89
|
+
),
|
|
71
90
|
): Promise<boolean> {
|
|
72
91
|
if ("streamId" in args) {
|
|
73
92
|
return await ctx.runMutation(component.streams.abort, {
|
|
@@ -93,7 +112,7 @@ export async function abortStream(
|
|
|
93
112
|
* @returns The streams for the thread.
|
|
94
113
|
*/
|
|
95
114
|
export async function listStreams(
|
|
96
|
-
ctx:
|
|
115
|
+
ctx: QueryCtx | MutationCtx | ActionCtx,
|
|
97
116
|
component: AgentComponent,
|
|
98
117
|
{
|
|
99
118
|
threadId,
|
|
@@ -126,15 +145,31 @@ export type StreamingOptions = {
|
|
|
126
145
|
* Defaults to 250.
|
|
127
146
|
*/
|
|
128
147
|
throttleMs?: number;
|
|
148
|
+
/**
|
|
149
|
+
* If set to true, this will return immediately, as it would if you weren't
|
|
150
|
+
* saving the deltas. Otherwise, the call will "consume" the stream with
|
|
151
|
+
* .consumeStream(), which waits for the stream to finish before returning.
|
|
152
|
+
*
|
|
153
|
+
* When saving deltas, you're often not interactin with the stream otherwise.
|
|
154
|
+
*/
|
|
155
|
+
returnImmediately?: boolean;
|
|
129
156
|
};
|
|
130
157
|
export const DEFAULT_STREAMING_OPTIONS = {
|
|
131
158
|
// This chunks by sentences / clauses. Punctuation followed by whitespace.
|
|
132
159
|
chunking: /[\p{P}\s]/u,
|
|
133
160
|
throttleMs: 250,
|
|
161
|
+
returnImmediately: false,
|
|
134
162
|
} satisfies StreamingOptions;
|
|
135
163
|
|
|
164
|
+
/**
|
|
165
|
+
*
|
|
166
|
+
* @param options The options passed to `agent.streamText` to decide whether to
|
|
167
|
+
* save deltas while streaming.
|
|
168
|
+
* @param existing The transforms passed to `agent.streamText` to merge with.
|
|
169
|
+
* @returns The merged transforms to pass to the underlying `streamText` call.
|
|
170
|
+
*/
|
|
136
171
|
export function mergeTransforms<TOOLS extends ToolSet>(
|
|
137
|
-
options: StreamingOptions | boolean | undefined,
|
|
172
|
+
options: { chunking?: StreamingOptions["chunking"] } | boolean | undefined,
|
|
138
173
|
existing:
|
|
139
174
|
| StreamTextTransform<TOOLS>
|
|
140
175
|
| Array<StreamTextTransform<TOOLS>>
|
|
@@ -156,84 +191,138 @@ export function mergeTransforms<TOOLS extends ToolSet>(
|
|
|
156
191
|
return transforms;
|
|
157
192
|
}
|
|
158
193
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
194
|
+
/**
|
|
195
|
+
* DeltaStreamer can be used to save a stream of "parts" by writing
|
|
196
|
+
* batches of them in "deltas" to the database so clients can subscribe
|
|
197
|
+
* (using the syncStreams utility and client hooks) and re-hydrate the stream.
|
|
198
|
+
* You can optionally compress the parts, e.g. concatenating text deltas, to
|
|
199
|
+
* optimize the data in transit.
|
|
200
|
+
*/
|
|
201
|
+
export class DeltaStreamer<T> {
|
|
202
|
+
streamId: string | undefined;
|
|
203
|
+
public readonly config: {
|
|
204
|
+
throttleMs: number;
|
|
205
|
+
onAsyncAbort: (reason: string) => Promise<void>;
|
|
206
|
+
compress: ((parts: T[]) => T[]) | null;
|
|
207
|
+
};
|
|
208
|
+
#nextParts: T[] = [];
|
|
165
209
|
#latestWrite: number = 0;
|
|
166
210
|
#ongoingWrite: Promise<void> | undefined;
|
|
167
211
|
#cursor: number = 0;
|
|
168
212
|
public abortController: AbortController;
|
|
213
|
+
// When true, the stream will be finished externally (e.g., atomically via addMessages)
|
|
214
|
+
// and consumeStream should skip calling finish().
|
|
215
|
+
#finishedExternally: boolean = false;
|
|
169
216
|
|
|
170
217
|
constructor(
|
|
171
218
|
public readonly component: AgentComponent,
|
|
172
|
-
public readonly ctx:
|
|
173
|
-
|
|
219
|
+
public readonly ctx: MutationCtx | ActionCtx,
|
|
220
|
+
config: {
|
|
221
|
+
throttleMs: number | undefined;
|
|
222
|
+
onAsyncAbort: (reason: string) => Promise<void>;
|
|
223
|
+
abortSignal: AbortSignal | undefined;
|
|
224
|
+
compress: ((parts: T[]) => T[]) | null;
|
|
225
|
+
},
|
|
174
226
|
public readonly metadata: {
|
|
175
227
|
threadId: string;
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
228
|
+
userId?: string;
|
|
229
|
+
order: number;
|
|
230
|
+
stepOrder: number;
|
|
231
|
+
agentName?: string;
|
|
232
|
+
model?: string;
|
|
233
|
+
provider?: string;
|
|
234
|
+
providerOptions?: ProviderOptions;
|
|
235
|
+
format: "UIMessageChunk" | "TextStreamPart" | undefined;
|
|
184
236
|
},
|
|
185
237
|
) {
|
|
186
|
-
this.
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
...options,
|
|
192
|
-
};
|
|
238
|
+
this.config = {
|
|
239
|
+
throttleMs: config.throttleMs ?? DEFAULT_STREAMING_OPTIONS.throttleMs,
|
|
240
|
+
onAsyncAbort: config.onAsyncAbort,
|
|
241
|
+
compress: config.compress,
|
|
242
|
+
};
|
|
193
243
|
this.#nextParts = [];
|
|
194
|
-
this.#nextOrder = metadata.order ?? 0;
|
|
195
|
-
this.#nextStepOrder = (metadata.stepOrder ?? 0) + 1;
|
|
196
244
|
this.abortController = new AbortController();
|
|
197
|
-
if (
|
|
198
|
-
|
|
245
|
+
if (config.abortSignal) {
|
|
246
|
+
config.abortSignal.addEventListener("abort", async () => {
|
|
247
|
+
if (this.abortController.signal.aborted) {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
199
250
|
if (this.streamId) {
|
|
251
|
+
this.abortController.abort();
|
|
252
|
+
await this.#ongoingWrite;
|
|
200
253
|
await this.ctx.runMutation(this.component.streams.abort, {
|
|
201
254
|
streamId: this.streamId,
|
|
202
255
|
reason: "abortSignal",
|
|
203
256
|
});
|
|
204
257
|
}
|
|
205
|
-
this.abortController.abort();
|
|
206
258
|
});
|
|
207
259
|
}
|
|
208
260
|
}
|
|
209
|
-
|
|
261
|
+
|
|
262
|
+
// Avoid race conditions by only creating once
|
|
263
|
+
#creatingStreamIdPromise: Promise<string> | undefined;
|
|
264
|
+
public async getStreamId() {
|
|
265
|
+
if (!this.streamId) {
|
|
266
|
+
if (!this.#creatingStreamIdPromise) {
|
|
267
|
+
this.#creatingStreamIdPromise = this.ctx.runMutation(
|
|
268
|
+
this.component.streams.create,
|
|
269
|
+
this.metadata,
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
this.streamId = await this.#creatingStreamIdPromise;
|
|
273
|
+
}
|
|
274
|
+
return this.streamId;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
public async addParts(parts: T[]) {
|
|
210
278
|
if (this.abortController.signal.aborted) {
|
|
211
279
|
return;
|
|
212
280
|
}
|
|
213
|
-
|
|
214
|
-
this.streamId = await this.ctx.runMutation(
|
|
215
|
-
this.component.streams.create,
|
|
216
|
-
{
|
|
217
|
-
...omit(this.metadata, ["abortSignal"]),
|
|
218
|
-
order: this.#nextOrder,
|
|
219
|
-
stepOrder: this.#nextStepOrder,
|
|
220
|
-
},
|
|
221
|
-
);
|
|
222
|
-
}
|
|
281
|
+
await this.getStreamId();
|
|
223
282
|
this.#nextParts.push(...parts);
|
|
224
283
|
if (
|
|
225
284
|
!this.#ongoingWrite &&
|
|
226
|
-
Date.now() - this.#latestWrite >= this.
|
|
285
|
+
Date.now() - this.#latestWrite >= this.config.throttleMs
|
|
227
286
|
) {
|
|
228
287
|
this.#ongoingWrite = this.#sendDelta();
|
|
229
288
|
}
|
|
230
289
|
}
|
|
231
290
|
|
|
291
|
+
public async consumeStream(stream: AsyncIterableStream<T>) {
|
|
292
|
+
for await (const chunk of stream) {
|
|
293
|
+
await this.addParts([chunk]);
|
|
294
|
+
}
|
|
295
|
+
// Skip finish if it will be handled externally (atomically with message save)
|
|
296
|
+
if (!this.#finishedExternally) {
|
|
297
|
+
await this.finish();
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Mark the stream as being finished externally (e.g., atomically via addMessages).
|
|
303
|
+
* When called, consumeStream() will skip calling finish() since it will be
|
|
304
|
+
* handled elsewhere in the same mutation as message saving.
|
|
305
|
+
*/
|
|
306
|
+
public markFinishedExternally(): void {
|
|
307
|
+
this.#finishedExternally = true;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Get the stream ID, waiting for it to be created if necessary.
|
|
312
|
+
* Useful for passing to addMessages for atomic finish.
|
|
313
|
+
*/
|
|
314
|
+
public async getOrCreateStreamId(): Promise<string> {
|
|
315
|
+
return this.getStreamId();
|
|
316
|
+
}
|
|
317
|
+
|
|
232
318
|
async #sendDelta() {
|
|
233
319
|
if (this.abortController.signal.aborted) {
|
|
234
320
|
return;
|
|
235
321
|
}
|
|
236
322
|
const delta = this.#createDelta();
|
|
323
|
+
if (!delta) {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
237
326
|
this.#latestWrite = Date.now();
|
|
238
327
|
try {
|
|
239
328
|
const success = await this.ctx.runMutation(
|
|
@@ -241,16 +330,21 @@ export class DeltaStreamer {
|
|
|
241
330
|
delta,
|
|
242
331
|
);
|
|
243
332
|
if (!success) {
|
|
333
|
+
await this.config.onAsyncAbort("async abort");
|
|
244
334
|
this.abortController.abort();
|
|
335
|
+
return;
|
|
245
336
|
}
|
|
246
337
|
} catch (e) {
|
|
338
|
+
await this.config.onAsyncAbort(
|
|
339
|
+
e instanceof Error ? e.message : "unknown error",
|
|
340
|
+
);
|
|
247
341
|
this.abortController.abort();
|
|
248
342
|
throw e;
|
|
249
343
|
}
|
|
250
344
|
// Now that we've sent the delta, check if we need to send another one.
|
|
251
345
|
if (
|
|
252
346
|
this.#nextParts.length > 0 &&
|
|
253
|
-
Date.now() - this.#latestWrite >= this.
|
|
347
|
+
Date.now() - this.#latestWrite >= this.config.throttleMs
|
|
254
348
|
) {
|
|
255
349
|
// We send again immediately with the accumulated deltas.
|
|
256
350
|
this.#ongoingWrite = this.#sendDelta();
|
|
@@ -259,47 +353,97 @@ export class DeltaStreamer {
|
|
|
259
353
|
}
|
|
260
354
|
}
|
|
261
355
|
|
|
262
|
-
#createDelta(): StreamDelta {
|
|
356
|
+
#createDelta(): StreamDelta | undefined {
|
|
357
|
+
if (this.#nextParts.length === 0) {
|
|
358
|
+
return undefined;
|
|
359
|
+
}
|
|
263
360
|
const start = this.#cursor;
|
|
264
361
|
const end = start + this.#nextParts.length;
|
|
265
362
|
this.#cursor = end;
|
|
266
|
-
const parts = this
|
|
363
|
+
const parts = this.config.compress
|
|
364
|
+
? this.config.compress(this.#nextParts)
|
|
365
|
+
: this.#nextParts;
|
|
267
366
|
this.#nextParts = [];
|
|
268
367
|
if (!this.streamId) {
|
|
269
368
|
throw new Error("Creating a delta before the stream is created");
|
|
270
369
|
}
|
|
271
|
-
return {
|
|
370
|
+
return { streamId: this.streamId, start, end, parts };
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
public async finish() {
|
|
374
|
+
if (!this.streamId) {
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
await this.#ongoingWrite;
|
|
378
|
+
await this.#sendDelta();
|
|
379
|
+
await this.ctx.runMutation(this.component.streams.finish, {
|
|
272
380
|
streamId: this.streamId,
|
|
273
|
-
|
|
274
|
-
end,
|
|
275
|
-
parts,
|
|
276
|
-
};
|
|
381
|
+
});
|
|
277
382
|
}
|
|
278
383
|
|
|
279
|
-
public async
|
|
280
|
-
if (this
|
|
281
|
-
|
|
282
|
-
this.#ongoingWrite = undefined;
|
|
384
|
+
public async fail(reason: string) {
|
|
385
|
+
if (this.abortController.signal.aborted) {
|
|
386
|
+
return;
|
|
283
387
|
}
|
|
388
|
+
this.abortController.abort();
|
|
284
389
|
if (!this.streamId) {
|
|
285
|
-
|
|
390
|
+
return;
|
|
286
391
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
this
|
|
290
|
-
|
|
392
|
+
await this.#ongoingWrite;
|
|
393
|
+
await this.ctx.runMutation(this.component.streams.abort, {
|
|
394
|
+
streamId: this.streamId,
|
|
395
|
+
reason,
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Compressing parts when streaming to save bandwidth in deltas.
|
|
402
|
+
*/
|
|
403
|
+
|
|
404
|
+
export function compressUIMessageChunks(
|
|
405
|
+
parts: UIMessageChunk[],
|
|
406
|
+
): UIMessageChunk[] {
|
|
407
|
+
const compressed: UIMessageChunk[] = [];
|
|
408
|
+
for (const part of parts) {
|
|
409
|
+
const last = compressed.at(-1);
|
|
410
|
+
if (part.type === "text-delta" || part.type === "reasoning-delta") {
|
|
411
|
+
if (last?.type === part.type && part.id === last.id) {
|
|
412
|
+
last.delta += part.delta;
|
|
413
|
+
} else {
|
|
414
|
+
compressed.push(part);
|
|
415
|
+
}
|
|
291
416
|
} else {
|
|
292
|
-
|
|
417
|
+
compressed.push(part);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
return compressed;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
export function compressTextStreamParts(
|
|
424
|
+
parts: TextStreamPart<ToolSet>[],
|
|
425
|
+
): TextStreamPart<ToolSet>[] {
|
|
426
|
+
const compressed: TextStreamPart<ToolSet>[] = [];
|
|
427
|
+
for (const part of parts) {
|
|
428
|
+
const last = compressed.at(-1);
|
|
429
|
+
if (part.type === "text-delta" || part.type === "reasoning-delta") {
|
|
430
|
+
if (last?.type === part.type && part.id === last.id) {
|
|
431
|
+
last.text += part.text;
|
|
432
|
+
} else {
|
|
433
|
+
compressed.push(part);
|
|
434
|
+
}
|
|
435
|
+
} else {
|
|
436
|
+
if (part.type === "file") {
|
|
437
|
+
compressed.push({
|
|
438
|
+
type: "file",
|
|
439
|
+
file: {
|
|
440
|
+
...part.file,
|
|
441
|
+
uint8Array: undefined as unknown as Uint8Array,
|
|
442
|
+
},
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
compressed.push(part);
|
|
293
446
|
}
|
|
294
|
-
const finalDelta =
|
|
295
|
-
this.#nextParts.length > 0 ? this.#createDelta() : undefined;
|
|
296
|
-
this.#nextParts = [];
|
|
297
|
-
const streamId = this.streamId;
|
|
298
|
-
this.streamId = undefined;
|
|
299
|
-
this.#cursor = 0;
|
|
300
|
-
await this.ctx.runMutation(this.component.streams.finish, {
|
|
301
|
-
streamId,
|
|
302
|
-
finalDelta,
|
|
303
|
-
});
|
|
304
447
|
}
|
|
448
|
+
return compressed;
|
|
305
449
|
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { WithoutSystemFields } from "convex/server";
|
|
2
|
+
import type { ThreadDoc } from "../validators.js";
|
|
3
|
+
import type {
|
|
4
|
+
ActionCtx,
|
|
5
|
+
AgentComponent,
|
|
6
|
+
MutationCtx,
|
|
7
|
+
QueryCtx,
|
|
8
|
+
} from "./types.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Create a thread to store messages with an Agent.
|
|
12
|
+
* @param ctx The context from a mutation or action.
|
|
13
|
+
* @param component The Agent component, usually `components.agent`.
|
|
14
|
+
* @param args The associated thread metadata.
|
|
15
|
+
* @returns The id of the created thread.
|
|
16
|
+
*/
|
|
17
|
+
export async function createThread(
|
|
18
|
+
ctx: MutationCtx | ActionCtx,
|
|
19
|
+
component: AgentComponent,
|
|
20
|
+
args?: { userId?: string | null; title?: string; summary?: string },
|
|
21
|
+
) {
|
|
22
|
+
const { _id: threadId } = await ctx.runMutation(
|
|
23
|
+
component.threads.createThread,
|
|
24
|
+
{
|
|
25
|
+
userId: args?.userId ?? undefined,
|
|
26
|
+
title: args?.title,
|
|
27
|
+
summary: args?.summary,
|
|
28
|
+
},
|
|
29
|
+
);
|
|
30
|
+
return threadId;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Get the metadata for a thread.
|
|
35
|
+
* @param ctx A ctx object from a query, mutation, or action.
|
|
36
|
+
* @param args.threadId The thread to get the metadata for.
|
|
37
|
+
* @returns The metadata for the thread.
|
|
38
|
+
*/
|
|
39
|
+
export async function getThreadMetadata(
|
|
40
|
+
ctx: QueryCtx | MutationCtx | ActionCtx,
|
|
41
|
+
component: AgentComponent,
|
|
42
|
+
args: { threadId: string },
|
|
43
|
+
): Promise<ThreadDoc> {
|
|
44
|
+
const thread = await ctx.runQuery(component.threads.getThread, {
|
|
45
|
+
threadId: args.threadId,
|
|
46
|
+
});
|
|
47
|
+
if (!thread) {
|
|
48
|
+
throw new Error("Thread not found");
|
|
49
|
+
}
|
|
50
|
+
return thread;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function updateThreadMetadata(
|
|
54
|
+
ctx: MutationCtx | ActionCtx,
|
|
55
|
+
component: AgentComponent,
|
|
56
|
+
args: { threadId: string; patch: Partial<WithoutSystemFields<ThreadDoc>> },
|
|
57
|
+
) {
|
|
58
|
+
return ctx.runMutation(component.threads.updateThread, {
|
|
59
|
+
threadId: args.threadId,
|
|
60
|
+
patch: args.patch,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Search for threads by title, paginated.
|
|
66
|
+
* @param ctx The context passed from the query/mutation/action.
|
|
67
|
+
* @returns The threads matching the search, paginated.
|
|
68
|
+
*/
|
|
69
|
+
export async function searchThreadTitles(
|
|
70
|
+
ctx: QueryCtx | MutationCtx | ActionCtx,
|
|
71
|
+
component: AgentComponent,
|
|
72
|
+
{
|
|
73
|
+
userId,
|
|
74
|
+
query,
|
|
75
|
+
limit,
|
|
76
|
+
}: { userId?: string | undefined; query: string; limit?: number },
|
|
77
|
+
): Promise<ThreadDoc[]> {
|
|
78
|
+
return ctx.runQuery(component.threads.searchThreadTitles, {
|
|
79
|
+
userId,
|
|
80
|
+
query,
|
|
81
|
+
limit: limit ?? 10,
|
|
82
|
+
});
|
|
83
|
+
}
|