@convex-dev/agent 0.5.0-alpha.1 → 0.6.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/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 +129 -27
- package/dist/client/createTool.d.ts.map +1 -1
- package/dist/client/createTool.js +66 -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 +1323 -192
- 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 +1056 -965
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +242 -748
- 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 +175 -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 +346 -35
- 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 +171 -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 +93 -0
- package/dist/client/streamText.js.map +1 -0
- package/dist/client/streaming.d.ts +3705 -32
- package/dist/client/streaming.d.ts.map +1 -1
- package/dist/client/streaming.js +141 -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 +265 -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 +3119 -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 +2553 -342
- package/dist/component/messages.d.ts.map +1 -1
- package/dist/component/messages.js +387 -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 +35 -335
- 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 +16 -16
- package/dist/component/users.d.ts +4 -4
- 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 +447 -0
- package/dist/deltas.js.map +1 -0
- package/dist/mapping.d.ts +20 -20
- package/dist/mapping.d.ts.map +1 -1
- package/dist/mapping.js +313 -96
- 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 +101 -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 +98 -50
- package/src/UIMessages.combineUIMessages.test.ts +239 -0
- package/src/UIMessages.test.ts +273 -0
- package/src/UIMessages.ts +739 -0
- package/src/client/createTool.ts +293 -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 +520 -1290
- package/src/client/messages.ts +237 -0
- package/src/client/mockModel.ts +245 -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 +577 -70
- package/src/client/start.ts +310 -0
- package/src/client/streamText.ts +163 -0
- package/src/client/streaming.test.ts +186 -0
- package/src/client/streaming.ts +219 -97
- package/src/client/threads.ts +83 -0
- package/src/client/types.ts +368 -219
- package/src/client/utils.ts +27 -0
- package/src/component/_generated/api.ts +64 -0
- package/src/component/_generated/component.ts +4913 -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 +490 -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 +570 -0
- package/src/fromUIMessages.test.ts +497 -0
- package/src/mapping.test.ts +103 -6
- package/src/mapping.ts +422 -161
- 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 +154 -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
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import {
|
|
2
|
+
stepCountIs,
|
|
3
|
+
type CallSettings,
|
|
4
|
+
type GenerateObjectResult,
|
|
5
|
+
type IdGenerator,
|
|
6
|
+
type LanguageModel,
|
|
7
|
+
type ModelMessage,
|
|
8
|
+
type StepResult,
|
|
9
|
+
type StopCondition,
|
|
10
|
+
type ToolSet,
|
|
11
|
+
} from "ai";
|
|
12
|
+
import {
|
|
13
|
+
serializeNewMessagesInStep,
|
|
14
|
+
serializeObjectResult,
|
|
15
|
+
} from "../mapping.js";
|
|
16
|
+
import { embedMessages, fetchContextWithPrompt } from "./search.js";
|
|
17
|
+
import type { ActionCtx, AgentComponent, Config, Options } from "./types.js";
|
|
18
|
+
import type { Message, MessageDoc } from "../validators.js";
|
|
19
|
+
import {
|
|
20
|
+
getModelName,
|
|
21
|
+
getProviderName,
|
|
22
|
+
type ModelOrMetadata,
|
|
23
|
+
} from "../shared.js";
|
|
24
|
+
import { wrapTools, type ToolCtx } from "./createTool.js";
|
|
25
|
+
import type { Agent } from "./index.js";
|
|
26
|
+
import { assert, omit } from "convex-helpers";
|
|
27
|
+
import { saveInputMessages } from "./saveInputMessages.js";
|
|
28
|
+
import type { GenericActionCtx, GenericDataModel } from "convex/server";
|
|
29
|
+
|
|
30
|
+
export async function startGeneration<
|
|
31
|
+
T,
|
|
32
|
+
Tools extends ToolSet = ToolSet,
|
|
33
|
+
CustomCtx extends object = object,
|
|
34
|
+
>(
|
|
35
|
+
ctx: ActionCtx & CustomCtx,
|
|
36
|
+
component: AgentComponent,
|
|
37
|
+
/**
|
|
38
|
+
* These are the arguments you'll pass to the LLM call such as
|
|
39
|
+
* `generateText` or `streamText`. This function will look up the context
|
|
40
|
+
* and provide functions to save the steps, abort the generation, and more.
|
|
41
|
+
* The type of the arguments returned infers from the type of the arguments
|
|
42
|
+
* you pass here.
|
|
43
|
+
*/
|
|
44
|
+
args: T & {
|
|
45
|
+
/**
|
|
46
|
+
* If provided, this message will be used as the "prompt" for the LLM call,
|
|
47
|
+
* instead of the prompt or messages.
|
|
48
|
+
* This is useful if you want to first save a user message, then use it as
|
|
49
|
+
* the prompt for the LLM call in another call.
|
|
50
|
+
*/
|
|
51
|
+
promptMessageId?: string;
|
|
52
|
+
/**
|
|
53
|
+
* The model to use for the LLM calls. This will override the model specified
|
|
54
|
+
* in the Agent constructor.
|
|
55
|
+
*/
|
|
56
|
+
model?: LanguageModel;
|
|
57
|
+
/**
|
|
58
|
+
* The tools to use for the tool calls. This will override tools specified
|
|
59
|
+
* in the Agent constructor or createThread / continueThread.
|
|
60
|
+
*/
|
|
61
|
+
tools?: Tools;
|
|
62
|
+
/**
|
|
63
|
+
* The single prompt message to use for the LLM call. This will be the
|
|
64
|
+
* last message in the context. If it's a string, it will be a user role.
|
|
65
|
+
*/
|
|
66
|
+
prompt?: string | (ModelMessage | Message)[];
|
|
67
|
+
/**
|
|
68
|
+
* If provided alongside prompt, the ordering will be:
|
|
69
|
+
* 1. system prompt
|
|
70
|
+
* 2. search context
|
|
71
|
+
* 3. recent messages
|
|
72
|
+
* 4. these messages
|
|
73
|
+
* 5. prompt messages, including those already on the same `order` as
|
|
74
|
+
* the promptMessageId message, if provided.
|
|
75
|
+
*/
|
|
76
|
+
messages?: (ModelMessage | Message)[];
|
|
77
|
+
/**
|
|
78
|
+
* The abort signal to be passed to the LLM call. If triggered, it will
|
|
79
|
+
* mark the pending message as failed. If the generation is asynchronously
|
|
80
|
+
* aborted, it will trigger this signal when detected.
|
|
81
|
+
*/
|
|
82
|
+
abortSignal?: AbortSignal;
|
|
83
|
+
stopWhen?: StopCondition<Tools> | Array<StopCondition<Tools>>;
|
|
84
|
+
_internal?: { generateId?: IdGenerator };
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
threadId,
|
|
88
|
+
...opts
|
|
89
|
+
}: Options &
|
|
90
|
+
Config & {
|
|
91
|
+
userId?: string | null;
|
|
92
|
+
threadId?: string;
|
|
93
|
+
languageModel?: LanguageModel;
|
|
94
|
+
agentName: string;
|
|
95
|
+
agentForToolCtx?: Agent;
|
|
96
|
+
},
|
|
97
|
+
): Promise<{
|
|
98
|
+
args: T & {
|
|
99
|
+
system?: string;
|
|
100
|
+
model: LanguageModel;
|
|
101
|
+
messages: ModelMessage[];
|
|
102
|
+
prompt?: never;
|
|
103
|
+
tools?: Tools;
|
|
104
|
+
} & CallSettings;
|
|
105
|
+
order: number;
|
|
106
|
+
stepOrder: number;
|
|
107
|
+
userId: string | undefined;
|
|
108
|
+
promptMessageId: string | undefined;
|
|
109
|
+
updateModel: (model: ModelOrMetadata | undefined) => void;
|
|
110
|
+
save: <TOOLS extends ToolSet>(
|
|
111
|
+
toSave:
|
|
112
|
+
| { step: StepResult<TOOLS> }
|
|
113
|
+
| { object: GenerateObjectResult<unknown> },
|
|
114
|
+
createPendingMessage?: boolean,
|
|
115
|
+
) => Promise<void>;
|
|
116
|
+
fail: (reason: string) => Promise<void>;
|
|
117
|
+
getSavedMessages: () => MessageDoc[];
|
|
118
|
+
}> {
|
|
119
|
+
const userId =
|
|
120
|
+
opts.userId ??
|
|
121
|
+
(threadId &&
|
|
122
|
+
(await ctx.runQuery(component.threads.getThread, { threadId }))
|
|
123
|
+
?.userId) ??
|
|
124
|
+
undefined;
|
|
125
|
+
|
|
126
|
+
const context = await fetchContextWithPrompt(ctx, component, {
|
|
127
|
+
...opts,
|
|
128
|
+
userId,
|
|
129
|
+
threadId,
|
|
130
|
+
messages: args.messages,
|
|
131
|
+
prompt: args.prompt,
|
|
132
|
+
promptMessageId: args.promptMessageId,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const saveMessages = opts.storageOptions?.saveMessages ?? "promptAndOutput";
|
|
136
|
+
const { promptMessageId, pendingMessage, savedMessages } =
|
|
137
|
+
threadId && saveMessages !== "none"
|
|
138
|
+
? await saveInputMessages(ctx, component, {
|
|
139
|
+
...opts,
|
|
140
|
+
userId,
|
|
141
|
+
threadId,
|
|
142
|
+
prompt: args.prompt,
|
|
143
|
+
messages: args.messages,
|
|
144
|
+
promptMessageId: args.promptMessageId,
|
|
145
|
+
storageOptions: { saveMessages },
|
|
146
|
+
})
|
|
147
|
+
: {
|
|
148
|
+
promptMessageId: args.promptMessageId,
|
|
149
|
+
pendingMessage: undefined,
|
|
150
|
+
savedMessages: [] as MessageDoc[],
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const order = pendingMessage?.order ?? context.order;
|
|
154
|
+
const stepOrder = pendingMessage?.stepOrder ?? context.stepOrder;
|
|
155
|
+
let pendingMessageId = pendingMessage?._id;
|
|
156
|
+
|
|
157
|
+
const model = args.model ?? opts.languageModel;
|
|
158
|
+
assert(model, "model is required");
|
|
159
|
+
let activeModel: ModelOrMetadata = model;
|
|
160
|
+
|
|
161
|
+
const fail = async (reason: string) => {
|
|
162
|
+
if (pendingMessageId) {
|
|
163
|
+
await ctx.runMutation(component.messages.finalizeMessage, {
|
|
164
|
+
messageId: pendingMessageId,
|
|
165
|
+
result: { status: "failed", error: reason },
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
if (args.abortSignal) {
|
|
170
|
+
const abortSignal = args.abortSignal;
|
|
171
|
+
abortSignal.addEventListener(
|
|
172
|
+
"abort",
|
|
173
|
+
async () => {
|
|
174
|
+
await fail(abortSignal.reason?.toString() ?? "abortSignal");
|
|
175
|
+
},
|
|
176
|
+
{ once: true },
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
const toolCtx = {
|
|
180
|
+
...(ctx as GenericActionCtx<GenericDataModel> & CustomCtx),
|
|
181
|
+
userId,
|
|
182
|
+
threadId,
|
|
183
|
+
promptMessageId,
|
|
184
|
+
agent: opts.agentForToolCtx,
|
|
185
|
+
} satisfies ToolCtx;
|
|
186
|
+
const tools = wrapTools(toolCtx, args.tools) as Tools;
|
|
187
|
+
const aiArgs = {
|
|
188
|
+
...opts.callSettings,
|
|
189
|
+
providerOptions: opts.providerOptions,
|
|
190
|
+
...omit(args, ["promptMessageId", "messages", "prompt"]),
|
|
191
|
+
model,
|
|
192
|
+
messages: context.messages,
|
|
193
|
+
stopWhen:
|
|
194
|
+
args.stopWhen ?? (opts.maxSteps ? stepCountIs(opts.maxSteps) : undefined),
|
|
195
|
+
tools,
|
|
196
|
+
} as T & {
|
|
197
|
+
model: LanguageModel;
|
|
198
|
+
messages: ModelMessage[];
|
|
199
|
+
prompt?: never;
|
|
200
|
+
tools?: Tools;
|
|
201
|
+
_internal?: { generateId?: IdGenerator };
|
|
202
|
+
} & CallSettings;
|
|
203
|
+
if (pendingMessageId) {
|
|
204
|
+
if (!aiArgs._internal?.generateId) {
|
|
205
|
+
aiArgs._internal = {
|
|
206
|
+
...aiArgs._internal,
|
|
207
|
+
generateId: pendingMessageId
|
|
208
|
+
? () => pendingMessageId ?? crypto.randomUUID()
|
|
209
|
+
: undefined,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return {
|
|
214
|
+
args: aiArgs,
|
|
215
|
+
order: order ?? 0,
|
|
216
|
+
stepOrder: stepOrder ?? 0,
|
|
217
|
+
userId,
|
|
218
|
+
promptMessageId,
|
|
219
|
+
getSavedMessages: () => savedMessages,
|
|
220
|
+
updateModel: (model: ModelOrMetadata | undefined) => {
|
|
221
|
+
if (model) {
|
|
222
|
+
activeModel = model;
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
fail,
|
|
226
|
+
save: async <TOOLS extends ToolSet>(
|
|
227
|
+
toSave:
|
|
228
|
+
| { step: StepResult<TOOLS> }
|
|
229
|
+
| { object: GenerateObjectResult<unknown> },
|
|
230
|
+
createPendingMessage?: boolean,
|
|
231
|
+
) => {
|
|
232
|
+
if (threadId && saveMessages !== "none") {
|
|
233
|
+
const serialized =
|
|
234
|
+
"object" in toSave
|
|
235
|
+
? await serializeObjectResult(
|
|
236
|
+
ctx,
|
|
237
|
+
component,
|
|
238
|
+
toSave.object,
|
|
239
|
+
activeModel,
|
|
240
|
+
)
|
|
241
|
+
: await serializeNewMessagesInStep(
|
|
242
|
+
ctx,
|
|
243
|
+
component,
|
|
244
|
+
toSave.step,
|
|
245
|
+
activeModel,
|
|
246
|
+
);
|
|
247
|
+
const embeddings = await embedMessages(
|
|
248
|
+
ctx,
|
|
249
|
+
{ threadId, ...opts, userId },
|
|
250
|
+
serialized.messages.map((m) => m.message),
|
|
251
|
+
);
|
|
252
|
+
if (createPendingMessage) {
|
|
253
|
+
serialized.messages.push({
|
|
254
|
+
message: { role: "assistant", content: [] },
|
|
255
|
+
status: "pending",
|
|
256
|
+
});
|
|
257
|
+
embeddings?.vectors.push(null);
|
|
258
|
+
}
|
|
259
|
+
const saved = await ctx.runMutation(component.messages.addMessages, {
|
|
260
|
+
userId,
|
|
261
|
+
threadId,
|
|
262
|
+
agentName: opts.agentName,
|
|
263
|
+
promptMessageId,
|
|
264
|
+
pendingMessageId,
|
|
265
|
+
messages: serialized.messages,
|
|
266
|
+
embeddings,
|
|
267
|
+
failPendingSteps: false,
|
|
268
|
+
});
|
|
269
|
+
const lastMessage = saved.messages.at(-1)!;
|
|
270
|
+
if (createPendingMessage) {
|
|
271
|
+
if (lastMessage.status === "failed") {
|
|
272
|
+
pendingMessageId = undefined;
|
|
273
|
+
savedMessages.push(...saved.messages);
|
|
274
|
+
await fail(
|
|
275
|
+
lastMessage.error ??
|
|
276
|
+
"Aborting - the pending message was marked as failed",
|
|
277
|
+
);
|
|
278
|
+
} else {
|
|
279
|
+
pendingMessageId = lastMessage._id;
|
|
280
|
+
savedMessages.push(...saved.messages.slice(0, -1));
|
|
281
|
+
}
|
|
282
|
+
} else {
|
|
283
|
+
pendingMessageId = undefined;
|
|
284
|
+
savedMessages.push(...saved.messages);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
const output = "object" in toSave ? toSave.object : toSave.step;
|
|
288
|
+
if (opts.rawRequestResponseHandler) {
|
|
289
|
+
await opts.rawRequestResponseHandler(ctx, {
|
|
290
|
+
userId,
|
|
291
|
+
threadId,
|
|
292
|
+
agentName: opts.agentName,
|
|
293
|
+
request: output.request,
|
|
294
|
+
response: output.response,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
if (opts.usageHandler && output.usage) {
|
|
298
|
+
await opts.usageHandler(ctx, {
|
|
299
|
+
userId,
|
|
300
|
+
threadId,
|
|
301
|
+
agentName: opts.agentName,
|
|
302
|
+
model: getModelName(activeModel),
|
|
303
|
+
provider: getProviderName(activeModel),
|
|
304
|
+
usage: output.usage,
|
|
305
|
+
providerMetadata: output.providerMetadata,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
},
|
|
309
|
+
};
|
|
310
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
StepResult,
|
|
3
|
+
StreamTextResult,
|
|
4
|
+
ToolSet,
|
|
5
|
+
UIMessage as AIUIMessage,
|
|
6
|
+
} from "ai";
|
|
7
|
+
import { streamText as streamTextAi } from "ai";
|
|
8
|
+
import {
|
|
9
|
+
compressUIMessageChunks,
|
|
10
|
+
DeltaStreamer,
|
|
11
|
+
mergeTransforms,
|
|
12
|
+
type StreamingOptions,
|
|
13
|
+
} from "./streaming.js";
|
|
14
|
+
import type {
|
|
15
|
+
ActionCtx,
|
|
16
|
+
AgentComponent,
|
|
17
|
+
AgentPrompt,
|
|
18
|
+
GenerationOutputMetadata,
|
|
19
|
+
Options,
|
|
20
|
+
Output,
|
|
21
|
+
} from "./types.js";
|
|
22
|
+
import { startGeneration } from "./start.js";
|
|
23
|
+
import type { Agent } from "./index.js";
|
|
24
|
+
import { getModelName, getProviderName } from "../shared.js";
|
|
25
|
+
import { errorToString, willContinue } from "./utils.js";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* This behaves like {@link streamText} from the "ai" package except that
|
|
29
|
+
* it add context based on the userId and threadId and saves the input and
|
|
30
|
+
* resulting messages to the thread, if specified.
|
|
31
|
+
* Use {@link continueThread} to get a version of this function already scoped
|
|
32
|
+
* to a thread (and optionally userId).
|
|
33
|
+
*/
|
|
34
|
+
export async function streamText<
|
|
35
|
+
TOOLS extends ToolSet,
|
|
36
|
+
OUTPUT extends Output<any, any, any> = never,
|
|
37
|
+
>(
|
|
38
|
+
ctx: ActionCtx,
|
|
39
|
+
component: AgentComponent,
|
|
40
|
+
/**
|
|
41
|
+
* The arguments to the streamText function, similar to the ai sdk's
|
|
42
|
+
* {@link streamText} function, along with Agent prompt options.
|
|
43
|
+
*/
|
|
44
|
+
streamTextArgs: AgentPrompt &
|
|
45
|
+
Omit<
|
|
46
|
+
Parameters<typeof streamTextAi<TOOLS, OUTPUT>>[0],
|
|
47
|
+
"model" | "prompt" | "messages"
|
|
48
|
+
> & {
|
|
49
|
+
/**
|
|
50
|
+
* The tools to use for the tool calls. This will override tools specified
|
|
51
|
+
* in the Agent constructor or createThread / continueThread.
|
|
52
|
+
*/
|
|
53
|
+
tools?: TOOLS;
|
|
54
|
+
},
|
|
55
|
+
/**
|
|
56
|
+
* The {@link ContextOptions} and {@link StorageOptions}
|
|
57
|
+
* options to use for fetching contextual messages and saving input/output messages.
|
|
58
|
+
*/
|
|
59
|
+
options: Options & {
|
|
60
|
+
agentName: string;
|
|
61
|
+
userId?: string | null;
|
|
62
|
+
threadId?: string;
|
|
63
|
+
/**
|
|
64
|
+
* Whether to save incremental data (deltas) from streaming responses.
|
|
65
|
+
* Defaults to false.
|
|
66
|
+
* If false, it will not save any deltas to the database.
|
|
67
|
+
* If true, it will save deltas with {@link DEFAULT_STREAMING_OPTIONS}.
|
|
68
|
+
*
|
|
69
|
+
* Regardless of this option, when streaming you are able to use this
|
|
70
|
+
* `streamText` function as you would with the "ai" package's version:
|
|
71
|
+
* iterating over the text, streaming it over HTTP, etc.
|
|
72
|
+
*/
|
|
73
|
+
saveStreamDeltas?: boolean | StreamingOptions;
|
|
74
|
+
agentForToolCtx?: Agent;
|
|
75
|
+
},
|
|
76
|
+
): Promise<StreamTextResult<TOOLS, OUTPUT> & GenerationOutputMetadata> {
|
|
77
|
+
const { threadId } = options ?? {};
|
|
78
|
+
const { args, userId, order, stepOrder, promptMessageId, ...call } =
|
|
79
|
+
await startGeneration(ctx, component, streamTextArgs, options);
|
|
80
|
+
|
|
81
|
+
const steps: StepResult<TOOLS>[] = [];
|
|
82
|
+
|
|
83
|
+
const streamer =
|
|
84
|
+
threadId && options.saveStreamDeltas
|
|
85
|
+
? new DeltaStreamer(
|
|
86
|
+
component,
|
|
87
|
+
ctx,
|
|
88
|
+
{
|
|
89
|
+
throttleMs:
|
|
90
|
+
typeof options.saveStreamDeltas === "object"
|
|
91
|
+
? options.saveStreamDeltas.throttleMs
|
|
92
|
+
: undefined,
|
|
93
|
+
onAsyncAbort: call.fail,
|
|
94
|
+
compress: compressUIMessageChunks,
|
|
95
|
+
abortSignal: args.abortSignal,
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
threadId,
|
|
99
|
+
userId,
|
|
100
|
+
agentName: options?.agentName,
|
|
101
|
+
model: getModelName(args.model),
|
|
102
|
+
provider: getProviderName(args.model),
|
|
103
|
+
providerOptions: args.providerOptions,
|
|
104
|
+
format: "UIMessageChunk",
|
|
105
|
+
order,
|
|
106
|
+
stepOrder,
|
|
107
|
+
},
|
|
108
|
+
)
|
|
109
|
+
: undefined;
|
|
110
|
+
|
|
111
|
+
const result = streamTextAi({
|
|
112
|
+
...args,
|
|
113
|
+
abortSignal: streamer?.abortController.signal ?? args.abortSignal,
|
|
114
|
+
experimental_transform: mergeTransforms(
|
|
115
|
+
options?.saveStreamDeltas,
|
|
116
|
+
streamTextArgs.experimental_transform,
|
|
117
|
+
),
|
|
118
|
+
onError: async (error) => {
|
|
119
|
+
console.error("onError", error);
|
|
120
|
+
await call.fail(errorToString(error.error));
|
|
121
|
+
await streamer?.fail(errorToString(error.error));
|
|
122
|
+
return streamTextArgs.onError?.(error);
|
|
123
|
+
},
|
|
124
|
+
prepareStep: async (options) => {
|
|
125
|
+
const result = await streamTextArgs.prepareStep?.(options);
|
|
126
|
+
if (result) {
|
|
127
|
+
const model = result.model ?? options.model;
|
|
128
|
+
call.updateModel(model);
|
|
129
|
+
// streamer?.updateMetadata({
|
|
130
|
+
// model: getModelName(model),
|
|
131
|
+
// provider: getProviderName(model),
|
|
132
|
+
// providerOptions: options.messages.at(-1)?.providerOptions,
|
|
133
|
+
// });
|
|
134
|
+
return result;
|
|
135
|
+
}
|
|
136
|
+
return undefined;
|
|
137
|
+
},
|
|
138
|
+
onStepFinish: async (step) => {
|
|
139
|
+
steps.push(step);
|
|
140
|
+
const createPendingMessage = await willContinue(steps, args.stopWhen);
|
|
141
|
+
await call.save({ step }, createPendingMessage);
|
|
142
|
+
return args.onStepFinish?.(step);
|
|
143
|
+
},
|
|
144
|
+
}) as StreamTextResult<TOOLS, OUTPUT>;
|
|
145
|
+
const stream = streamer?.consumeStream(
|
|
146
|
+
result.toUIMessageStream<AIUIMessage<TOOLS>>(),
|
|
147
|
+
);
|
|
148
|
+
if (
|
|
149
|
+
(typeof options?.saveStreamDeltas === "object" &&
|
|
150
|
+
!options.saveStreamDeltas.returnImmediately) ||
|
|
151
|
+
options?.saveStreamDeltas === true
|
|
152
|
+
) {
|
|
153
|
+
await stream;
|
|
154
|
+
await result.consumeStream();
|
|
155
|
+
}
|
|
156
|
+
const metadata: GenerationOutputMetadata = {
|
|
157
|
+
promptMessageId,
|
|
158
|
+
order,
|
|
159
|
+
savedMessages: call.getSavedMessages(),
|
|
160
|
+
messageId: promptMessageId,
|
|
161
|
+
};
|
|
162
|
+
return Object.assign(result, metadata);
|
|
163
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, test } from "vitest";
|
|
2
|
+
import { createThread } from "./index.js";
|
|
3
|
+
import type { GenericSchema, SchemaDefinition } from "convex/server";
|
|
4
|
+
import { streamText } from "ai";
|
|
5
|
+
import { components, initConvexTest } from "./setup.test.js";
|
|
6
|
+
import { mockModel } from "./mockModel.js";
|
|
7
|
+
import { compressUIMessageChunks, DeltaStreamer } from "./streaming.js";
|
|
8
|
+
import { getParts } from "../deltas.js";
|
|
9
|
+
import type { TestConvex } from "convex-test";
|
|
10
|
+
|
|
11
|
+
const defaultTestOptions = {
|
|
12
|
+
throttleMs: 0,
|
|
13
|
+
abortSignal: undefined,
|
|
14
|
+
compress: null,
|
|
15
|
+
onAsyncAbort: async () => {
|
|
16
|
+
throw new Error("unexpected");
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const testMetadata = {
|
|
21
|
+
order: 0,
|
|
22
|
+
stepOrder: 0,
|
|
23
|
+
agentName: "test agent",
|
|
24
|
+
model: "test model",
|
|
25
|
+
provider: "test provider",
|
|
26
|
+
providerOptions: {},
|
|
27
|
+
format: "UIMessageChunk" as const,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
describe("DeltaStreamer", () => {
|
|
31
|
+
let t: TestConvex<SchemaDefinition<GenericSchema, boolean>>;
|
|
32
|
+
let threadId: string;
|
|
33
|
+
beforeEach(async () => {
|
|
34
|
+
t = initConvexTest();
|
|
35
|
+
await t.run(async (ctx) => {
|
|
36
|
+
threadId = await createThread(ctx, components.agent, {});
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
test("should save chunks via DeltaStreamer", async () => {
|
|
40
|
+
await t.run(async (ctx) => {
|
|
41
|
+
const streamer = new DeltaStreamer(
|
|
42
|
+
components.agent,
|
|
43
|
+
ctx,
|
|
44
|
+
{ ...defaultTestOptions },
|
|
45
|
+
{ ...testMetadata, threadId },
|
|
46
|
+
);
|
|
47
|
+
const result = streamText({
|
|
48
|
+
model: mockModel(),
|
|
49
|
+
prompt: "Test prompt",
|
|
50
|
+
});
|
|
51
|
+
await streamer.consumeStream(result.toUIMessageStream());
|
|
52
|
+
const streamId = streamer.streamId!;
|
|
53
|
+
expect(streamId).toBeDefined();
|
|
54
|
+
const deltas = await ctx.runQuery(components.agent.streams.listDeltas, {
|
|
55
|
+
threadId,
|
|
56
|
+
cursors: [{ cursor: 0, streamId }],
|
|
57
|
+
});
|
|
58
|
+
const { parts } = getParts(deltas);
|
|
59
|
+
const stream = result.toUIMessageStream();
|
|
60
|
+
for await (const part of stream) {
|
|
61
|
+
const expected = parts.shift();
|
|
62
|
+
expect(part).toEqual(expected);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
test("should save all parts when throttleMs is 0", async () => {
|
|
67
|
+
await t.run(async (ctx) => {
|
|
68
|
+
const streamer = new DeltaStreamer(
|
|
69
|
+
components.agent,
|
|
70
|
+
ctx,
|
|
71
|
+
{ ...defaultTestOptions, throttleMs: 0 },
|
|
72
|
+
{ ...testMetadata, threadId },
|
|
73
|
+
);
|
|
74
|
+
const result = streamText({
|
|
75
|
+
model: mockModel({
|
|
76
|
+
content: [
|
|
77
|
+
// The mockModel splits these into deltas based on spaces
|
|
78
|
+
{ type: "text", text: "A B C" },
|
|
79
|
+
{ type: "reasoning", text: "D E F" },
|
|
80
|
+
],
|
|
81
|
+
}),
|
|
82
|
+
prompt: "Test prompt",
|
|
83
|
+
});
|
|
84
|
+
await streamer.consumeStream(result.toUIMessageStream());
|
|
85
|
+
const streamId = streamer.streamId!;
|
|
86
|
+
expect(streamId).toBeDefined();
|
|
87
|
+
const deltas = await ctx.runQuery(components.agent.streams.listDeltas, {
|
|
88
|
+
threadId,
|
|
89
|
+
cursors: [{ cursor: 0, streamId }],
|
|
90
|
+
});
|
|
91
|
+
const { parts } = getParts(deltas);
|
|
92
|
+
const expected = [
|
|
93
|
+
{ type: "start" },
|
|
94
|
+
{ type: "start-step" },
|
|
95
|
+
{ type: "text-start" },
|
|
96
|
+
{ type: "text-delta", delta: "A" },
|
|
97
|
+
{ type: "text-delta", delta: " B" },
|
|
98
|
+
{ type: "text-delta", delta: " C" },
|
|
99
|
+
{ type: "text-end" },
|
|
100
|
+
{ type: "reasoning-start" },
|
|
101
|
+
{ type: "reasoning-delta", delta: "D" },
|
|
102
|
+
{ type: "reasoning-delta", delta: " E" },
|
|
103
|
+
{ type: "reasoning-delta", delta: " F" },
|
|
104
|
+
{ type: "reasoning-end" },
|
|
105
|
+
{ type: "finish-step" },
|
|
106
|
+
{ type: "finish" },
|
|
107
|
+
];
|
|
108
|
+
for (const expectedPart of expected) {
|
|
109
|
+
const part = parts.shift();
|
|
110
|
+
expect(part).toBeDefined();
|
|
111
|
+
expect(part).toMatchObject(expectedPart);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("should save compressed parts via DeltaStreamer", async () => {
|
|
117
|
+
await t.run(async (ctx) => {
|
|
118
|
+
const streamer = new DeltaStreamer(
|
|
119
|
+
components.agent,
|
|
120
|
+
ctx,
|
|
121
|
+
{
|
|
122
|
+
throttleMs: 1000,
|
|
123
|
+
abortSignal: undefined,
|
|
124
|
+
compress: compressUIMessageChunks,
|
|
125
|
+
onAsyncAbort: async () => {
|
|
126
|
+
throw new Error("async abort");
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
...testMetadata,
|
|
131
|
+
threadId,
|
|
132
|
+
},
|
|
133
|
+
);
|
|
134
|
+
const result = streamText({
|
|
135
|
+
model: mockModel({
|
|
136
|
+
content: [
|
|
137
|
+
// The mockModel splits these into deltas based on spaces
|
|
138
|
+
{ type: "text", text: "A B C" },
|
|
139
|
+
{ type: "text", text: "D E F" },
|
|
140
|
+
{ type: "reasoning", text: "J K L" },
|
|
141
|
+
{ type: "text", text: "M N O" },
|
|
142
|
+
],
|
|
143
|
+
}),
|
|
144
|
+
prompt: "Test prompt",
|
|
145
|
+
// experimental_transform: smoothStream({ chunking: "line" }),
|
|
146
|
+
onError: (error) => {
|
|
147
|
+
console.error(error);
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
await streamer.consumeStream(result.toUIMessageStream());
|
|
151
|
+
const streamId = streamer.streamId!;
|
|
152
|
+
expect(streamId).toBeDefined();
|
|
153
|
+
const deltas = await ctx.runQuery(components.agent.streams.listDeltas, {
|
|
154
|
+
threadId,
|
|
155
|
+
cursors: [{ cursor: 0, streamId }],
|
|
156
|
+
});
|
|
157
|
+
const { parts } = getParts(deltas);
|
|
158
|
+
const expected = [
|
|
159
|
+
{ type: "start" },
|
|
160
|
+
{ type: "start-step" },
|
|
161
|
+
{ type: "text-start" },
|
|
162
|
+
// These are collapsed into a single delta
|
|
163
|
+
{ type: "text-delta", delta: "A B C" },
|
|
164
|
+
{ type: "text-end" },
|
|
165
|
+
{ type: "text-start" },
|
|
166
|
+
{ type: "text-delta", delta: "D E F" },
|
|
167
|
+
{ type: "text-end" },
|
|
168
|
+
{ type: "reasoning-start" },
|
|
169
|
+
{ type: "reasoning-delta", delta: "J K L" },
|
|
170
|
+
{ type: "reasoning-end" },
|
|
171
|
+
{ type: "text-start" },
|
|
172
|
+
{ type: "text-delta", delta: "M N O" },
|
|
173
|
+
{ type: "text-end" },
|
|
174
|
+
{ type: "finish-step" },
|
|
175
|
+
{ type: "finish" },
|
|
176
|
+
];
|
|
177
|
+
for (const expectedPart of expected) {
|
|
178
|
+
const part = parts.shift();
|
|
179
|
+
expect(part).toBeDefined();
|
|
180
|
+
expect(part).toMatchObject(expectedPart);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
// TODO: test errors & aborted states
|
|
185
|
+
// TODO: test fetching partial stream data - syncStreams w/ cursors
|
|
186
|
+
});
|