@convex-dev/agent 0.6.0-alpha.0 → 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/dist/client/createTool.d.ts +18 -21
- package/dist/client/createTool.d.ts.map +1 -1
- package/dist/client/createTool.js +3 -2
- package/dist/client/createTool.js.map +1 -1
- package/dist/client/definePlaygroundAPI.d.ts +48 -48
- package/dist/client/index.d.ts +109 -62
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +92 -1
- package/dist/client/index.js.map +1 -1
- package/dist/client/messages.d.ts +12 -12
- package/dist/client/mockModel.d.ts.map +1 -1
- package/dist/client/mockModel.js +9 -2
- package/dist/client/mockModel.js.map +1 -1
- package/dist/client/search.d.ts +12 -12
- package/dist/client/search.d.ts.map +1 -1
- package/dist/client/search.js +3 -3
- package/dist/client/search.js.map +1 -1
- package/dist/client/start.d.ts +1 -1
- package/dist/client/start.d.ts.map +1 -1
- package/dist/client/start.js +28 -14
- package/dist/client/start.js.map +1 -1
- package/dist/client/streamText.d.ts.map +1 -1
- package/dist/client/streamText.js +27 -3
- package/dist/client/streamText.js.map +1 -1
- package/dist/client/streaming.d.ts +105 -94
- package/dist/client/streaming.d.ts.map +1 -1
- package/dist/client/streaming.js +28 -8
- package/dist/client/streaming.js.map +1 -1
- package/dist/client/types.d.ts +13 -12
- package/dist/client/types.d.ts.map +1 -1
- package/dist/component/_generated/component.d.ts +1 -0
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/messages.d.ts +143 -142
- package/dist/component/messages.d.ts.map +1 -1
- package/dist/component/messages.js +13 -3
- package/dist/component/messages.js.map +1 -1
- package/dist/component/streams.d.ts +5 -5
- package/dist/component/threads.d.ts +19 -19
- package/dist/component/users.d.ts +3 -3
- package/dist/deltas.d.ts.map +1 -1
- package/dist/deltas.js +0 -1
- package/dist/deltas.js.map +1 -1
- package/dist/mapping.d.ts +19 -1
- package/dist/mapping.d.ts.map +1 -1
- package/dist/mapping.js +57 -6
- package/dist/mapping.js.map +1 -1
- package/dist/react/useDeltaStreams.d.ts.map +1 -1
- package/dist/react/useDeltaStreams.js +5 -0
- package/dist/react/useDeltaStreams.js.map +1 -1
- package/package.json +4 -2
- package/src/client/approval.test.ts +350 -0
- package/src/client/createTool.ts +50 -52
- package/src/client/index.ts +120 -1
- package/src/client/mockModel.ts +9 -2
- package/src/client/search.ts +7 -3
- package/src/client/start.ts +41 -24
- package/src/client/streamText.ts +27 -3
- package/src/client/streaming.ts +33 -11
- package/src/client/types.ts +14 -12
- package/src/component/_generated/component.ts +53 -64
- package/src/component/messages.ts +12 -2
- package/src/deltas.ts +0 -1
- package/src/mapping.test.ts +77 -0
- package/src/mapping.ts +62 -6
- package/src/react/useDeltaStreams.ts +6 -0
package/src/client/index.ts
CHANGED
|
@@ -19,7 +19,7 @@ import type {
|
|
|
19
19
|
} from "ai";
|
|
20
20
|
import { generateObject, generateText, stepCountIs, streamObject } from "ai";
|
|
21
21
|
|
|
22
|
-
const MIGRATION_URL = "
|
|
22
|
+
const MIGRATION_URL = "node_modules/@convex-dev/agent/MIGRATION.md";
|
|
23
23
|
const warnedDeprecations = new Set<string>();
|
|
24
24
|
function warnDeprecation(key: string, message: string) {
|
|
25
25
|
if (!warnedDeprecations.has(key)) {
|
|
@@ -1016,6 +1016,125 @@ export class Agent<
|
|
|
1016
1016
|
);
|
|
1017
1017
|
}
|
|
1018
1018
|
|
|
1019
|
+
/**
|
|
1020
|
+
* Approve a tool call that requires human approval.
|
|
1021
|
+
* Saves a `tool-approval-response` message to the thread.
|
|
1022
|
+
* After calling this, call `agent.streamText` or `agent.generateText`
|
|
1023
|
+
* with `promptMessageId` set to the returned `messageId` to continue
|
|
1024
|
+
* generation — the AI SDK will automatically execute the approved tool.
|
|
1025
|
+
*
|
|
1026
|
+
* The approval response is attached to the same generation order as the
|
|
1027
|
+
* original approval request, preserving tool_call/tool_result adjacency in
|
|
1028
|
+
* the continuation context even if newer thread messages exist.
|
|
1029
|
+
*
|
|
1030
|
+
* @param ctx A ctx object from a mutation.
|
|
1031
|
+
* @param args.threadId The thread containing the tool call.
|
|
1032
|
+
* @param args.approvalId The approval ID from the tool-approval-request part.
|
|
1033
|
+
* @param args.reason Optional reason for approval.
|
|
1034
|
+
* @returns The messageId of the saved approval response message.
|
|
1035
|
+
*/
|
|
1036
|
+
async approveToolCall(
|
|
1037
|
+
ctx: MutationCtx,
|
|
1038
|
+
args: { threadId: string; approvalId: string; reason?: string },
|
|
1039
|
+
): Promise<{ messageId: string }> {
|
|
1040
|
+
return this.respondToToolCallApproval(ctx, { ...args, approved: true });
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
/**
|
|
1044
|
+
* Deny a tool call that requires human approval.
|
|
1045
|
+
* Saves a `tool-approval-response` message to the thread.
|
|
1046
|
+
* After calling this, call `agent.streamText` or `agent.generateText`
|
|
1047
|
+
* with `promptMessageId` set to the returned `messageId` to continue
|
|
1048
|
+
* generation — the AI SDK will automatically create an `execution-denied`
|
|
1049
|
+
* result and let the model respond accordingly.
|
|
1050
|
+
*
|
|
1051
|
+
* @param ctx A ctx object from a mutation.
|
|
1052
|
+
* @param args.threadId The thread containing the tool call.
|
|
1053
|
+
* @param args.approvalId The approval ID from the tool-approval-request part.
|
|
1054
|
+
* @param args.reason Optional reason for denial.
|
|
1055
|
+
* @returns The messageId of the saved denial response message.
|
|
1056
|
+
*/
|
|
1057
|
+
async denyToolCall(
|
|
1058
|
+
ctx: MutationCtx,
|
|
1059
|
+
args: { threadId: string; approvalId: string; reason?: string },
|
|
1060
|
+
): Promise<{ messageId: string }> {
|
|
1061
|
+
return this.respondToToolCallApproval(ctx, { ...args, approved: false });
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
private async respondToToolCallApproval(
|
|
1065
|
+
ctx: MutationCtx,
|
|
1066
|
+
args: {
|
|
1067
|
+
threadId: string;
|
|
1068
|
+
approvalId: string;
|
|
1069
|
+
approved: boolean;
|
|
1070
|
+
reason?: string;
|
|
1071
|
+
},
|
|
1072
|
+
): Promise<{ messageId: string }> {
|
|
1073
|
+
const promptMessageId = await this.getApprovalRequestMessageId(ctx, {
|
|
1074
|
+
threadId: args.threadId,
|
|
1075
|
+
approvalId: args.approvalId,
|
|
1076
|
+
});
|
|
1077
|
+
|
|
1078
|
+
const { messageId } = await this.saveMessage(ctx, {
|
|
1079
|
+
threadId: args.threadId,
|
|
1080
|
+
promptMessageId,
|
|
1081
|
+
skipEmbeddings: true,
|
|
1082
|
+
message: {
|
|
1083
|
+
role: "tool",
|
|
1084
|
+
content: [
|
|
1085
|
+
{
|
|
1086
|
+
type: "tool-approval-response",
|
|
1087
|
+
approvalId: args.approvalId,
|
|
1088
|
+
approved: args.approved,
|
|
1089
|
+
reason: args.reason,
|
|
1090
|
+
},
|
|
1091
|
+
],
|
|
1092
|
+
},
|
|
1093
|
+
});
|
|
1094
|
+
return { messageId };
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
private async getApprovalRequestMessageId(
|
|
1098
|
+
ctx: MutationCtx,
|
|
1099
|
+
args: { threadId: string; approvalId: string },
|
|
1100
|
+
): Promise<string> {
|
|
1101
|
+
// NOTE: This pagination returns messages in descending order (newest first).
|
|
1102
|
+
// The "already handled" check (tool-approval-response) relies on seeing
|
|
1103
|
+
// responses before their corresponding requests. If the pagination order
|
|
1104
|
+
// changes, this logic will need to be updated.
|
|
1105
|
+
let cursor: string | null = null;
|
|
1106
|
+
do {
|
|
1107
|
+
const page = await this.listMessages(ctx, {
|
|
1108
|
+
threadId: args.threadId,
|
|
1109
|
+
paginationOpts: { cursor, numItems: 100 },
|
|
1110
|
+
});
|
|
1111
|
+
for (const message of page.page) {
|
|
1112
|
+
const content = message.message?.content;
|
|
1113
|
+
if (!Array.isArray(content)) continue;
|
|
1114
|
+
for (const part of content) {
|
|
1115
|
+
const typedPart = part as { type?: unknown; approvalId?: unknown };
|
|
1116
|
+
if (
|
|
1117
|
+
typedPart.type === "tool-approval-response" &&
|
|
1118
|
+
typedPart.approvalId === args.approvalId
|
|
1119
|
+
) {
|
|
1120
|
+
throw new Error(`Approval ${args.approvalId} was already handled`);
|
|
1121
|
+
}
|
|
1122
|
+
if (
|
|
1123
|
+
typedPart.type === "tool-approval-request" &&
|
|
1124
|
+
typedPart.approvalId === args.approvalId
|
|
1125
|
+
) {
|
|
1126
|
+
return message._id;
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
cursor = page.isDone ? null : page.continueCursor;
|
|
1131
|
+
} while (cursor !== null);
|
|
1132
|
+
|
|
1133
|
+
throw new Error(
|
|
1134
|
+
`Approval request ${args.approvalId} was not found in thread ${args.threadId}`,
|
|
1135
|
+
);
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1019
1138
|
/**
|
|
1020
1139
|
* Explicitly save a "step" created by the AI SDK.
|
|
1021
1140
|
* @param ctx The ctx argument to a mutation or action.
|
package/src/client/mockModel.ts
CHANGED
|
@@ -16,8 +16,15 @@ const DEFAULT_USAGE = {
|
|
|
16
16
|
outputTokens: 10,
|
|
17
17
|
inputTokens: 3,
|
|
18
18
|
totalTokens: 13,
|
|
19
|
-
inputTokenDetails:
|
|
20
|
-
|
|
19
|
+
inputTokenDetails: {
|
|
20
|
+
noCacheTokens: 3,
|
|
21
|
+
cacheReadTokens: 0,
|
|
22
|
+
cacheWriteTokens: 0,
|
|
23
|
+
},
|
|
24
|
+
outputTokenDetails: {
|
|
25
|
+
textTokens: 10,
|
|
26
|
+
reasoningTokens: 0,
|
|
27
|
+
},
|
|
21
28
|
};
|
|
22
29
|
|
|
23
30
|
export type MockModelArgs = {
|
package/src/client/search.ts
CHANGED
|
@@ -29,7 +29,11 @@ import type {
|
|
|
29
29
|
ActionCtx,
|
|
30
30
|
} from "./types.js";
|
|
31
31
|
import { inlineMessagesFiles } from "./files.js";
|
|
32
|
-
import {
|
|
32
|
+
import {
|
|
33
|
+
docsToModelMessages,
|
|
34
|
+
mergeApprovalResponseMessages,
|
|
35
|
+
toModelMessage,
|
|
36
|
+
} from "../mapping.js";
|
|
33
37
|
|
|
34
38
|
const DEFAULT_VECTOR_SCORE_THRESHOLD = 0.0;
|
|
35
39
|
// 10k characters should be more than enough for most cases, and stays under
|
|
@@ -637,13 +641,13 @@ export async function fetchContextWithPrompt(
|
|
|
637
641
|
const inputPrompt = promptArray.map(toModelMessage);
|
|
638
642
|
const existingResponses = docsToModelMessages(existingResponseDocs);
|
|
639
643
|
|
|
640
|
-
const allMessages = [
|
|
644
|
+
const allMessages = mergeApprovalResponseMessages([
|
|
641
645
|
...search,
|
|
642
646
|
...recent,
|
|
643
647
|
...inputMessages,
|
|
644
648
|
...inputPrompt,
|
|
645
649
|
...existingResponses,
|
|
646
|
-
];
|
|
650
|
+
]);
|
|
647
651
|
let processedMessages = args.contextHandler
|
|
648
652
|
? await args.contextHandler(ctx, {
|
|
649
653
|
allMessages,
|
package/src/client/start.ts
CHANGED
|
@@ -112,6 +112,7 @@ export async function startGeneration<
|
|
|
112
112
|
| { step: StepResult<TOOLS> }
|
|
113
113
|
| { object: GenerateObjectResult<unknown> },
|
|
114
114
|
createPendingMessage?: boolean,
|
|
115
|
+
finishStreamId?: string,
|
|
115
116
|
) => Promise<void>;
|
|
116
117
|
fail: (reason: string) => Promise<void>;
|
|
117
118
|
getSavedMessages: () => MessageDoc[];
|
|
@@ -200,16 +201,18 @@ export async function startGeneration<
|
|
|
200
201
|
tools?: Tools;
|
|
201
202
|
_internal?: { generateId?: IdGenerator };
|
|
202
203
|
} & CallSettings;
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
204
|
+
// NOTE: We intentionally do NOT override _internal.generateId here.
|
|
205
|
+
// The AI SDK uses generateId() for many internal IDs (approval IDs,
|
|
206
|
+
// tool execution IDs, message IDs, etc.) and they must be unique.
|
|
207
|
+
// The pending message is linked via the explicit `pendingMessageId`
|
|
208
|
+
// parameter passed to addMessages in the save closure.
|
|
209
|
+
// Track how many response messages we've already saved across steps.
|
|
210
|
+
// step.response.messages is cumulative — each step appends to it.
|
|
211
|
+
// We need to know which messages are new in each step to serialize
|
|
212
|
+
// only the new ones (important for tool approval flows where the SDK
|
|
213
|
+
// may add extra messages like approval tool-results).
|
|
214
|
+
let previousResponseMessageCount = 0;
|
|
215
|
+
|
|
213
216
|
return {
|
|
214
217
|
args: aiArgs,
|
|
215
218
|
order: order ?? 0,
|
|
@@ -228,22 +231,35 @@ export async function startGeneration<
|
|
|
228
231
|
| { step: StepResult<TOOLS> }
|
|
229
232
|
| { object: GenerateObjectResult<unknown> },
|
|
230
233
|
createPendingMessage?: boolean,
|
|
234
|
+
/**
|
|
235
|
+
* If provided, finish this stream atomically with the message save.
|
|
236
|
+
* This prevents UI flickering from separate mutations (issue #181).
|
|
237
|
+
*/
|
|
238
|
+
finishStreamId?: string,
|
|
231
239
|
) => {
|
|
232
240
|
if (threadId && saveMessages !== "none") {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
241
|
+
let serialized;
|
|
242
|
+
if ("object" in toSave) {
|
|
243
|
+
serialized = await serializeObjectResult(
|
|
244
|
+
ctx,
|
|
245
|
+
component,
|
|
246
|
+
toSave.object,
|
|
247
|
+
activeModel,
|
|
248
|
+
);
|
|
249
|
+
} else {
|
|
250
|
+
const allResponseMessages = toSave.step.response.messages;
|
|
251
|
+
const newResponseMessages = allResponseMessages.slice(
|
|
252
|
+
previousResponseMessageCount,
|
|
253
|
+
);
|
|
254
|
+
previousResponseMessageCount = allResponseMessages.length;
|
|
255
|
+
serialized = await serializeNewMessagesInStep(
|
|
256
|
+
ctx,
|
|
257
|
+
component,
|
|
258
|
+
toSave.step,
|
|
259
|
+
activeModel,
|
|
260
|
+
newResponseMessages,
|
|
261
|
+
);
|
|
262
|
+
}
|
|
247
263
|
const embeddings = await embedMessages(
|
|
248
264
|
ctx,
|
|
249
265
|
{ threadId, ...opts, userId },
|
|
@@ -265,6 +281,7 @@ export async function startGeneration<
|
|
|
265
281
|
messages: serialized.messages,
|
|
266
282
|
embeddings,
|
|
267
283
|
failPendingSteps: false,
|
|
284
|
+
finishStreamId,
|
|
268
285
|
});
|
|
269
286
|
const lastMessage = saved.messages.at(-1)!;
|
|
270
287
|
if (createPendingMessage) {
|
package/src/client/streamText.ts
CHANGED
|
@@ -80,6 +80,9 @@ export async function streamText<
|
|
|
80
80
|
|
|
81
81
|
const steps: StepResult<TOOLS>[] = [];
|
|
82
82
|
|
|
83
|
+
// Track the final step for atomic save with stream finish (issue #181)
|
|
84
|
+
let pendingFinalStep: StepResult<TOOLS> | undefined;
|
|
85
|
+
|
|
83
86
|
const streamer =
|
|
84
87
|
threadId && options.saveStreamDeltas
|
|
85
88
|
? new DeltaStreamer(
|
|
@@ -138,7 +141,14 @@ export async function streamText<
|
|
|
138
141
|
onStepFinish: async (step) => {
|
|
139
142
|
steps.push(step);
|
|
140
143
|
const createPendingMessage = await willContinue(steps, args.stopWhen);
|
|
141
|
-
|
|
144
|
+
if (!createPendingMessage && streamer) {
|
|
145
|
+
// This is the final step with streaming enabled.
|
|
146
|
+
// Defer saving until stream consumption completes for atomic finish (issue #181).
|
|
147
|
+
streamer.markFinishedExternally();
|
|
148
|
+
pendingFinalStep = step;
|
|
149
|
+
} else {
|
|
150
|
+
await call.save({ step }, createPendingMessage);
|
|
151
|
+
}
|
|
142
152
|
return args.onStepFinish?.(step);
|
|
143
153
|
},
|
|
144
154
|
}) as StreamTextResult<TOOLS, OUTPUT>;
|
|
@@ -150,8 +160,22 @@ export async function streamText<
|
|
|
150
160
|
!options.saveStreamDeltas.returnImmediately) ||
|
|
151
161
|
options?.saveStreamDeltas === true
|
|
152
162
|
) {
|
|
153
|
-
|
|
154
|
-
|
|
163
|
+
try {
|
|
164
|
+
await stream;
|
|
165
|
+
await result.consumeStream();
|
|
166
|
+
} catch (e) {
|
|
167
|
+
// If the stream errored (e.g. onStepFinish threw), the DeltaStreamer's
|
|
168
|
+
// finish() was never called, leaving the streaming message stuck in
|
|
169
|
+
// "streaming" state. Clean it up by marking it as aborted.
|
|
170
|
+
await streamer?.fail(e instanceof Error ? e.message : String(e));
|
|
171
|
+
throw e;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// If we deferred the final step save, do it now with atomic stream finish.
|
|
176
|
+
if (pendingFinalStep && streamer) {
|
|
177
|
+
const finishStreamId = await streamer.getOrCreateStreamId();
|
|
178
|
+
await call.save({ step: pendingFinalStep }, false, finishStreamId);
|
|
155
179
|
}
|
|
156
180
|
const metadata: GenerationOutputMetadata = {
|
|
157
181
|
promptMessageId,
|
package/src/client/streaming.ts
CHANGED
|
@@ -210,6 +210,9 @@ export class DeltaStreamer<T> {
|
|
|
210
210
|
#ongoingWrite: Promise<void> | undefined;
|
|
211
211
|
#cursor: number = 0;
|
|
212
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;
|
|
213
216
|
|
|
214
217
|
constructor(
|
|
215
218
|
public readonly component: AgentComponent,
|
|
@@ -259,17 +262,16 @@ export class DeltaStreamer<T> {
|
|
|
259
262
|
// Avoid race conditions by only creating once
|
|
260
263
|
#creatingStreamIdPromise: Promise<string> | undefined;
|
|
261
264
|
public async getStreamId() {
|
|
262
|
-
if (this.streamId) {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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;
|
|
267
273
|
}
|
|
268
|
-
|
|
269
|
-
this.component.streams.create,
|
|
270
|
-
this.metadata,
|
|
271
|
-
);
|
|
272
|
-
this.streamId = await this.#creatingStreamIdPromise;
|
|
274
|
+
return this.streamId;
|
|
273
275
|
}
|
|
274
276
|
|
|
275
277
|
public async addParts(parts: T[]) {
|
|
@@ -290,7 +292,27 @@ export class DeltaStreamer<T> {
|
|
|
290
292
|
for await (const chunk of stream) {
|
|
291
293
|
await this.addParts([chunk]);
|
|
292
294
|
}
|
|
293
|
-
|
|
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();
|
|
294
316
|
}
|
|
295
317
|
|
|
296
318
|
async #sendDelta() {
|
package/src/client/types.ts
CHANGED
|
@@ -46,6 +46,14 @@ import type {
|
|
|
46
46
|
import type { StreamingOptions } from "./streaming.js";
|
|
47
47
|
import type { ComponentApi } from "../component/_generated/component.js";
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Type-level check that ensures models are from AI SDK v6.
|
|
51
|
+
* If a v5 model (LanguageModelV2) is passed, TypeScript will show the error message string.
|
|
52
|
+
*/
|
|
53
|
+
type AssertAISDKv6<T> = T extends { specificationVersion: "v3" }
|
|
54
|
+
? T
|
|
55
|
+
: "⚠️ @convex-dev/agent v0.6.0 requires AI SDK v6. Update your dependencies: npm install ai@^6.0.35 @ai-sdk/openai@^3.0.10 (or other provider). See: node_modules/@convex-dev/agent/MIGRATION.md";
|
|
56
|
+
|
|
49
57
|
export type AgentPrompt = {
|
|
50
58
|
/**
|
|
51
59
|
* System message to include in the prompt. Overwrites Agent instructions.
|
|
@@ -91,23 +99,17 @@ export type AgentPrompt = {
|
|
|
91
99
|
export type Config = {
|
|
92
100
|
/**
|
|
93
101
|
* The LLM model to use for generating / streaming text and objects.
|
|
94
|
-
*
|
|
102
|
+
* Requires AI SDK v6 (@ai-sdk/* packages v3.x).
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
95
105
|
* import { openai } from "@ai-sdk/openai"
|
|
96
106
|
* const myAgent = new Agent(components.agent, {
|
|
97
107
|
* languageModel: openai.chat("gpt-4o-mini"),
|
|
108
|
+
* })
|
|
98
109
|
*/
|
|
99
|
-
languageModel?: LanguageModel
|
|
110
|
+
languageModel?: AssertAISDKv6<LanguageModel>;
|
|
100
111
|
/**
|
|
101
|
-
*
|
|
102
|
-
* If specified, it will use this for generating vector embeddings
|
|
103
|
-
* of chats, and can opt-in to doing vector search for automatic context
|
|
104
|
-
* on generateText, etc.
|
|
105
|
-
* e.g.
|
|
106
|
-
* import { openai } from "@ai-sdk/openai"
|
|
107
|
-
* const myAgent = new Agent(components.agent, {
|
|
108
|
-
* ...
|
|
109
|
-
* textEmbeddingModel: openai.embedding("text-embedding-3-small")
|
|
110
|
-
* @deprecated — Use embeddingModel instead.
|
|
112
|
+
* @deprecated Use `embeddingModel` instead.
|
|
111
113
|
*/
|
|
112
114
|
textEmbeddingModel?: EmbeddingModel;
|
|
113
115
|
/**
|
|
@@ -154,6 +154,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
154
154
|
vectors: Array<Array<number> | null>;
|
|
155
155
|
};
|
|
156
156
|
failPendingSteps?: boolean;
|
|
157
|
+
finishStreamId?: string;
|
|
157
158
|
hideFromUserIdSearch?: boolean;
|
|
158
159
|
messages: Array<{
|
|
159
160
|
error?: string;
|
|
@@ -1420,22 +1421,19 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
1420
1421
|
toolName: string;
|
|
1421
1422
|
type: "tool-call";
|
|
1422
1423
|
}
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
toolName: string;
|
|
1437
|
-
type: "tool-call";
|
|
1438
|
-
}
|
|
1424
|
+
| {
|
|
1425
|
+
args: any;
|
|
1426
|
+
input?: any;
|
|
1427
|
+
providerExecuted?: boolean;
|
|
1428
|
+
providerMetadata?: Record<
|
|
1429
|
+
string,
|
|
1430
|
+
Record<string, any>
|
|
1431
|
+
>;
|
|
1432
|
+
providerOptions?: Record<string, Record<string, any>>;
|
|
1433
|
+
toolCallId: string;
|
|
1434
|
+
toolName: string;
|
|
1435
|
+
type: "tool-call";
|
|
1436
|
+
}
|
|
1439
1437
|
| {
|
|
1440
1438
|
args?: any;
|
|
1441
1439
|
experimental_content?: Array<
|
|
@@ -2533,22 +2531,19 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
2533
2531
|
toolName: string;
|
|
2534
2532
|
type: "tool-call";
|
|
2535
2533
|
}
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
toolName: string;
|
|
2550
|
-
type: "tool-call";
|
|
2551
|
-
}
|
|
2534
|
+
| {
|
|
2535
|
+
args: any;
|
|
2536
|
+
input?: any;
|
|
2537
|
+
providerExecuted?: boolean;
|
|
2538
|
+
providerMetadata?: Record<
|
|
2539
|
+
string,
|
|
2540
|
+
Record<string, any>
|
|
2541
|
+
>;
|
|
2542
|
+
providerOptions?: Record<string, Record<string, any>>;
|
|
2543
|
+
toolCallId: string;
|
|
2544
|
+
toolName: string;
|
|
2545
|
+
type: "tool-call";
|
|
2546
|
+
}
|
|
2552
2547
|
| {
|
|
2553
2548
|
args?: any;
|
|
2554
2549
|
experimental_content?: Array<
|
|
@@ -3054,22 +3049,19 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
3054
3049
|
toolName: string;
|
|
3055
3050
|
type: "tool-call";
|
|
3056
3051
|
}
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
toolName: string;
|
|
3071
|
-
type: "tool-call";
|
|
3072
|
-
}
|
|
3052
|
+
| {
|
|
3053
|
+
args: any;
|
|
3054
|
+
input?: any;
|
|
3055
|
+
providerExecuted?: boolean;
|
|
3056
|
+
providerMetadata?: Record<
|
|
3057
|
+
string,
|
|
3058
|
+
Record<string, any>
|
|
3059
|
+
>;
|
|
3060
|
+
providerOptions?: Record<string, Record<string, any>>;
|
|
3061
|
+
toolCallId: string;
|
|
3062
|
+
toolName: string;
|
|
3063
|
+
type: "tool-call";
|
|
3064
|
+
}
|
|
3073
3065
|
| {
|
|
3074
3066
|
args?: any;
|
|
3075
3067
|
experimental_content?: Array<
|
|
@@ -4063,22 +4055,19 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
4063
4055
|
toolName: string;
|
|
4064
4056
|
type: "tool-call";
|
|
4065
4057
|
}
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
toolName: string;
|
|
4080
|
-
type: "tool-call";
|
|
4081
|
-
}
|
|
4058
|
+
| {
|
|
4059
|
+
args: any;
|
|
4060
|
+
input?: any;
|
|
4061
|
+
providerExecuted?: boolean;
|
|
4062
|
+
providerMetadata?: Record<
|
|
4063
|
+
string,
|
|
4064
|
+
Record<string, any>
|
|
4065
|
+
>;
|
|
4066
|
+
providerOptions?: Record<string, Record<string, any>>;
|
|
4067
|
+
toolCallId: string;
|
|
4068
|
+
toolName: string;
|
|
4069
|
+
type: "tool-call";
|
|
4070
|
+
}
|
|
4082
4071
|
| {
|
|
4083
4072
|
args?: any;
|
|
4084
4073
|
experimental_content?: Array<
|
|
@@ -39,7 +39,7 @@ import {
|
|
|
39
39
|
vVectorId,
|
|
40
40
|
} from "./vector/tables.js";
|
|
41
41
|
import { changeRefcount } from "./files.js";
|
|
42
|
-
import { getStreamingMessagesWithMetadata } from "./streams.js";
|
|
42
|
+
import { getStreamingMessagesWithMetadata, finishHandler } from "./streams.js";
|
|
43
43
|
import { partial } from "convex-helpers/validators";
|
|
44
44
|
|
|
45
45
|
function publicMessage(message: Doc<"messages">): MessageDoc {
|
|
@@ -141,6 +141,9 @@ const addMessagesArgs = {
|
|
|
141
141
|
// if set to true, these messages will not show up in text or vector search
|
|
142
142
|
// results for the userId
|
|
143
143
|
hideFromUserIdSearch: v.optional(v.boolean()),
|
|
144
|
+
// If provided, finish this stream atomically with the message save.
|
|
145
|
+
// This prevents UI flickering from separate mutations (issue #181).
|
|
146
|
+
finishStreamId: v.optional(v.id("streamingMessages")),
|
|
144
147
|
};
|
|
145
148
|
export const addMessages = mutation({
|
|
146
149
|
args: addMessagesArgs,
|
|
@@ -161,6 +164,8 @@ async function addMessagesHandler(
|
|
|
161
164
|
const {
|
|
162
165
|
embeddings,
|
|
163
166
|
failPendingSteps,
|
|
167
|
+
// Destructured separately to exclude from `...rest` (used in addMessages args, not message fields)
|
|
168
|
+
finishStreamId,
|
|
164
169
|
messages,
|
|
165
170
|
promptMessageId,
|
|
166
171
|
pendingMessageId,
|
|
@@ -274,7 +279,7 @@ async function addMessagesHandler(
|
|
|
274
279
|
order: pendingMessage.order,
|
|
275
280
|
stepOrder: pendingMessage.stepOrder,
|
|
276
281
|
});
|
|
277
|
-
toReturn.push(pendingMessage);
|
|
282
|
+
toReturn.push((await ctx.db.get(pendingMessage._id))!);
|
|
278
283
|
continue;
|
|
279
284
|
}
|
|
280
285
|
if (message.message.role === "user") {
|
|
@@ -303,6 +308,11 @@ async function addMessagesHandler(
|
|
|
303
308
|
// TODO: delete the associated stream data for the order/stepOrder
|
|
304
309
|
toReturn.push((await ctx.db.get(messageId))!);
|
|
305
310
|
}
|
|
311
|
+
// Atomically finish the stream if requested, preventing UI flickering
|
|
312
|
+
// from separate mutations for message save and stream finish (issue #181).
|
|
313
|
+
if (finishStreamId) {
|
|
314
|
+
await finishHandler(ctx, { streamId: finishStreamId });
|
|
315
|
+
}
|
|
306
316
|
return { messages: toReturn.map(publicMessage) };
|
|
307
317
|
}
|
|
308
318
|
|
package/src/deltas.ts
CHANGED
|
@@ -124,7 +124,6 @@ export async function deriveUIMessagesFromDeltas(
|
|
|
124
124
|
blankUIMessage(streamMessage, threadId),
|
|
125
125
|
parts,
|
|
126
126
|
);
|
|
127
|
-
// TODO: this fails on partial tool calls
|
|
128
127
|
messages.push(uiMessage);
|
|
129
128
|
} else {
|
|
130
129
|
const [uiMessages] = deriveUIMessagesFromTextStreamParts(
|