@convex-dev/agent 0.6.0-alpha.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MIGRATION.md +153 -0
- package/package.json +4 -2
- package/src/UIMessages.ts +135 -0
- package/src/client/approval.test.ts +350 -0
- package/src/client/createTool.ts +50 -52
- package/src/client/index.ts +162 -1
- package/src/client/mockModel.ts +9 -2
- package/src/client/search.test.ts +4 -5
- package/src/client/search.ts +47 -2
- package/src/client/start.ts +41 -24
- package/src/client/streamText.ts +27 -3
- package/src/client/streaming.integration.test.ts +1206 -0
- package/src/client/streaming.ts +41 -12
- 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 +143 -1
- package/src/mapping.ts +115 -6
- package/src/react/useDeltaStreams.ts +6 -0
- package/dist/UIMessages.d.ts +0 -46
- package/dist/UIMessages.d.ts.map +0 -1
- package/dist/UIMessages.js +0 -546
- package/dist/UIMessages.js.map +0 -1
- package/dist/client/createTool.d.ts +0 -170
- package/dist/client/createTool.d.ts.map +0 -1
- package/dist/client/createTool.js +0 -115
- package/dist/client/createTool.js.map +0 -1
- package/dist/client/defaultComponent.d.ts +0 -11
- package/dist/client/defaultComponent.d.ts.map +0 -1
- package/dist/client/defaultComponent.js +0 -7
- package/dist/client/defaultComponent.js.map +0 -1
- package/dist/client/definePlaygroundAPI.d.ts +0 -1725
- package/dist/client/definePlaygroundAPI.d.ts.map +0 -1
- package/dist/client/definePlaygroundAPI.js +0 -271
- package/dist/client/definePlaygroundAPI.js.map +0 -1
- package/dist/client/files.d.ts +0 -69
- package/dist/client/files.d.ts.map +0 -1
- package/dist/client/files.js +0 -181
- package/dist/client/files.js.map +0 -1
- package/dist/client/index.d.ts +0 -2044
- package/dist/client/index.d.ts.map +0 -1
- package/dist/client/index.js +0 -804
- package/dist/client/index.js.map +0 -1
- package/dist/client/messages.d.ts +0 -461
- package/dist/client/messages.d.ts.map +0 -1
- package/dist/client/messages.js +0 -106
- package/dist/client/messages.js.map +0 -1
- package/dist/client/mockModel.d.ts +0 -42
- package/dist/client/mockModel.d.ts.map +0 -1
- package/dist/client/mockModel.js +0 -175
- package/dist/client/mockModel.js.map +0 -1
- package/dist/client/saveInputMessages.d.ts +0 -20
- package/dist/client/saveInputMessages.d.ts.map +0 -1
- package/dist/client/saveInputMessages.js +0 -58
- package/dist/client/saveInputMessages.js.map +0 -1
- package/dist/client/search.d.ts +0 -493
- package/dist/client/search.d.ts.map +0 -1
- package/dist/client/search.js +0 -425
- package/dist/client/search.js.map +0 -1
- package/dist/client/start.d.ts +0 -84
- package/dist/client/start.d.ts.map +0 -1
- package/dist/client/start.js +0 -171
- package/dist/client/start.js.map +0 -1
- package/dist/client/streamText.d.ts +0 -46
- package/dist/client/streamText.d.ts.map +0 -1
- package/dist/client/streamText.js +0 -93
- package/dist/client/streamText.js.map +0 -1
- package/dist/client/streaming.d.ts +0 -3767
- package/dist/client/streaming.d.ts.map +0 -1
- package/dist/client/streaming.js +0 -294
- package/dist/client/streaming.js.map +0 -1
- package/dist/client/threads.d.ts +0 -46
- package/dist/client/threads.d.ts.map +0 -1
- package/dist/client/threads.js +0 -49
- package/dist/client/threads.js.map +0 -1
- package/dist/client/types.d.ts +0 -460
- package/dist/client/types.d.ts.map +0 -1
- package/dist/client/types.js +0 -2
- package/dist/client/types.js.map +0 -1
- package/dist/client/utils.d.ts +0 -4
- package/dist/client/utils.d.ts.map +0 -1
- package/dist/client/utils.js +0 -21
- package/dist/client/utils.js.map +0 -1
- package/dist/component/_generated/api.d.ts +0 -48
- package/dist/component/_generated/api.d.ts.map +0 -1
- package/dist/component/_generated/api.js +0 -31
- package/dist/component/_generated/api.js.map +0 -1
- package/dist/component/_generated/component.d.ts +0 -3119
- package/dist/component/_generated/component.d.ts.map +0 -1
- package/dist/component/_generated/component.js +0 -11
- package/dist/component/_generated/component.js.map +0 -1
- package/dist/component/_generated/dataModel.d.ts +0 -46
- package/dist/component/_generated/dataModel.d.ts.map +0 -1
- package/dist/component/_generated/dataModel.js +0 -11
- package/dist/component/_generated/dataModel.js.map +0 -1
- package/dist/component/_generated/server.d.ts +0 -121
- package/dist/component/_generated/server.d.ts.map +0 -1
- package/dist/component/_generated/server.js +0 -78
- package/dist/component/_generated/server.js.map +0 -1
- package/dist/component/apiKeys.d.ts +0 -11
- package/dist/component/apiKeys.d.ts.map +0 -1
- package/dist/component/apiKeys.js +0 -69
- package/dist/component/apiKeys.js.map +0 -1
- package/dist/component/convex.config.d.ts +0 -3
- package/dist/component/convex.config.d.ts.map +0 -1
- package/dist/component/convex.config.js +0 -3
- package/dist/component/convex.config.js.map +0 -1
- package/dist/component/files.d.ts +0 -97
- package/dist/component/files.d.ts.map +0 -1
- package/dist/component/files.js +0 -190
- package/dist/component/files.js.map +0 -1
- package/dist/component/messages.d.ts +0 -3850
- package/dist/component/messages.d.ts.map +0 -1
- package/dist/component/messages.js +0 -747
- package/dist/component/messages.js.map +0 -1
- package/dist/component/schema.d.ts +0 -8026
- package/dist/component/schema.d.ts.map +0 -1
- package/dist/component/schema.js +0 -147
- package/dist/component/schema.js.map +0 -1
- package/dist/component/streams.d.ts +0 -128
- package/dist/component/streams.d.ts.map +0 -1
- package/dist/component/streams.js +0 -413
- package/dist/component/streams.js.map +0 -1
- package/dist/component/threads.d.ts +0 -115
- package/dist/component/threads.d.ts.map +0 -1
- package/dist/component/threads.js +0 -208
- package/dist/component/threads.js.map +0 -1
- package/dist/component/users.d.ts +0 -52
- package/dist/component/users.d.ts.map +0 -1
- package/dist/component/users.js +0 -229
- package/dist/component/users.js.map +0 -1
- package/dist/component/vector/index.d.ts +0 -61
- package/dist/component/vector/index.d.ts.map +0 -1
- package/dist/component/vector/index.js +0 -146
- package/dist/component/vector/index.js.map +0 -1
- package/dist/component/vector/tables.d.ts +0 -58
- package/dist/component/vector/tables.d.ts.map +0 -1
- package/dist/component/vector/tables.js +0 -56
- package/dist/component/vector/tables.js.map +0 -1
- package/dist/deltas.d.ts +0 -43
- package/dist/deltas.d.ts.map +0 -1
- package/dist/deltas.js +0 -447
- package/dist/deltas.js.map +0 -1
- package/dist/mapping.d.ts +0 -54
- package/dist/mapping.d.ts.map +0 -1
- package/dist/mapping.js +0 -626
- package/dist/mapping.js.map +0 -1
- package/dist/react/SmoothText.d.ts +0 -5
- package/dist/react/SmoothText.d.ts.map +0 -1
- package/dist/react/SmoothText.js +0 -6
- package/dist/react/SmoothText.js.map +0 -1
- package/dist/react/index.d.ts +0 -25
- package/dist/react/index.d.ts.map +0 -1
- package/dist/react/index.js +0 -70
- package/dist/react/index.js.map +0 -1
- package/dist/react/optimisticallySendMessage.d.ts +0 -42
- package/dist/react/optimisticallySendMessage.d.ts.map +0 -1
- package/dist/react/optimisticallySendMessage.js +0 -74
- package/dist/react/optimisticallySendMessage.js.map +0 -1
- package/dist/react/types.d.ts +0 -12
- package/dist/react/types.d.ts.map +0 -1
- package/dist/react/types.js +0 -2
- package/dist/react/types.js.map +0 -1
- package/dist/react/useDeltaStreams.d.ts +0 -10
- package/dist/react/useDeltaStreams.d.ts.map +0 -1
- package/dist/react/useDeltaStreams.js +0 -101
- package/dist/react/useDeltaStreams.js.map +0 -1
- package/dist/react/useSmoothText.d.ts +0 -27
- package/dist/react/useSmoothText.d.ts.map +0 -1
- package/dist/react/useSmoothText.js +0 -68
- package/dist/react/useSmoothText.js.map +0 -1
- package/dist/react/useStreamingUIMessages.d.ts +0 -22
- package/dist/react/useStreamingUIMessages.d.ts.map +0 -1
- package/dist/react/useStreamingUIMessages.js +0 -92
- package/dist/react/useStreamingUIMessages.js.map +0 -1
- package/dist/react/useThreadMessages.d.ts +0 -104
- package/dist/react/useThreadMessages.d.ts.map +0 -1
- package/dist/react/useThreadMessages.js +0 -148
- package/dist/react/useThreadMessages.js.map +0 -1
- package/dist/react/useUIMessages.d.ts +0 -96
- package/dist/react/useUIMessages.d.ts.map +0 -1
- package/dist/react/useUIMessages.js +0 -108
- package/dist/react/useUIMessages.js.map +0 -1
- package/dist/shared.d.ts +0 -26
- package/dist/shared.d.ts.map +0 -1
- package/dist/shared.js +0 -67
- package/dist/shared.js.map +0 -1
- package/dist/validators.d.ts +0 -24516
- package/dist/validators.d.ts.map +0 -1
- package/dist/validators.js +0 -475
- package/dist/validators.js.map +0 -1
package/src/client/createTool.ts
CHANGED
|
@@ -11,7 +11,7 @@ import type { GenericActionCtx, GenericDataModel } from "convex/server";
|
|
|
11
11
|
import type { ProviderOptions } from "../validators.js";
|
|
12
12
|
import type { Agent } from "./index.js";
|
|
13
13
|
|
|
14
|
-
const MIGRATION_URL = "
|
|
14
|
+
const MIGRATION_URL = "node_modules/@convex-dev/agent/MIGRATION.md";
|
|
15
15
|
const warnedDeprecations = new Set<string>();
|
|
16
16
|
function warnDeprecation(key: string, message: string) {
|
|
17
17
|
if (!warnedDeprecations.has(key)) {
|
|
@@ -72,60 +72,57 @@ type NeverOptional<N, T> = 0 extends 1 & N
|
|
|
72
72
|
? Partial<Record<keyof T, undefined>>
|
|
73
73
|
: T;
|
|
74
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Error message type for deprecated 'handler' property.
|
|
77
|
+
* Using a string literal type causes TypeScript to show this message in errors.
|
|
78
|
+
*/
|
|
79
|
+
type HANDLER_REMOVED_ERROR =
|
|
80
|
+
"⚠️ 'handler' was removed in @convex-dev/agent v0.6.0. Rename to 'execute'. See: node_modules/@convex-dev/agent/MIGRATION.md";
|
|
81
|
+
|
|
75
82
|
export type ToolOutputPropertiesCtx<
|
|
76
83
|
INPUT,
|
|
77
84
|
OUTPUT,
|
|
78
85
|
Ctx extends ToolCtx = ToolCtx,
|
|
79
86
|
> = NeverOptional<
|
|
80
87
|
OUTPUT,
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
outputSchema?: FlexibleSchema<OUTPUT>;
|
|
97
|
-
execute?: never;
|
|
98
|
-
}
|
|
99
|
-
| {
|
|
100
|
-
outputSchema: FlexibleSchema<OUTPUT>;
|
|
101
|
-
execute?: never;
|
|
102
|
-
handler?: never;
|
|
103
|
-
}
|
|
88
|
+
{
|
|
89
|
+
/**
|
|
90
|
+
* An async function that is called with the arguments from the tool call and produces a result.
|
|
91
|
+
* If `execute` is not provided, the tool will not be executed automatically.
|
|
92
|
+
*
|
|
93
|
+
* @param input - The input of the tool call.
|
|
94
|
+
* @param options.abortSignal - A signal that can be used to abort the tool call.
|
|
95
|
+
*/
|
|
96
|
+
execute?: ToolExecuteFunctionCtx<INPUT, OUTPUT, Ctx>;
|
|
97
|
+
outputSchema?: FlexibleSchema<OUTPUT>;
|
|
98
|
+
/**
|
|
99
|
+
* @deprecated Removed in v0.6.0. Use `execute` instead.
|
|
100
|
+
*/
|
|
101
|
+
handler?: HANDLER_REMOVED_ERROR;
|
|
102
|
+
}
|
|
104
103
|
>;
|
|
105
104
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
inputSchema?: never;
|
|
128
|
-
};
|
|
105
|
+
/**
|
|
106
|
+
* Error message type for deprecated 'args' property.
|
|
107
|
+
* Using a string literal type causes TypeScript to show this message in errors.
|
|
108
|
+
*/
|
|
109
|
+
type ARGS_REMOVED_ERROR =
|
|
110
|
+
"⚠️ 'args' was removed in @convex-dev/agent v0.6.0. Rename to 'inputSchema'. See: node_modules/@convex-dev/agent/MIGRATION.md";
|
|
111
|
+
|
|
112
|
+
export type ToolInputProperties<INPUT> = {
|
|
113
|
+
/**
|
|
114
|
+
* The schema of the input that the tool expects.
|
|
115
|
+
* The language model will use this to generate the input.
|
|
116
|
+
* It is also used to validate the output of the language model.
|
|
117
|
+
*
|
|
118
|
+
* You can use descriptions on the schema properties to make the input understandable for the language model.
|
|
119
|
+
*/
|
|
120
|
+
inputSchema: FlexibleSchema<INPUT>;
|
|
121
|
+
/**
|
|
122
|
+
* @deprecated Removed in v0.6.0. Use `inputSchema` instead.
|
|
123
|
+
*/
|
|
124
|
+
args?: ARGS_REMOVED_ERROR;
|
|
125
|
+
};
|
|
129
126
|
|
|
130
127
|
/**
|
|
131
128
|
* This is a wrapper around the ai.tool function that adds extra context to the
|
|
@@ -238,24 +235,25 @@ export function createTool<INPUT, OUTPUT, Ctx extends ToolCtx = ToolCtx>(
|
|
|
238
235
|
) => ToolResultOutput | PromiseLike<ToolResultOutput>;
|
|
239
236
|
},
|
|
240
237
|
): Tool<INPUT, OUTPUT> {
|
|
241
|
-
|
|
238
|
+
// Runtime backwards compat - types will show errors but runtime still works
|
|
239
|
+
const inputSchema = def.inputSchema ?? (def as any).args;
|
|
242
240
|
if (!inputSchema)
|
|
243
|
-
throw new Error("To use a Convex tool, you must provide an `inputSchema`
|
|
241
|
+
throw new Error("To use a Convex tool, you must provide an `inputSchema`");
|
|
244
242
|
|
|
245
|
-
if (def.args && !def.inputSchema) {
|
|
243
|
+
if ((def as any).args && !def.inputSchema) {
|
|
246
244
|
warnDeprecation(
|
|
247
245
|
"createTool.args",
|
|
248
246
|
"createTool: 'args' is deprecated. Use 'inputSchema' instead.",
|
|
249
247
|
);
|
|
250
248
|
}
|
|
251
|
-
if (def.handler && !def.execute) {
|
|
249
|
+
if ((def as any).handler && !def.execute) {
|
|
252
250
|
warnDeprecation(
|
|
253
251
|
"createTool.handler",
|
|
254
252
|
"createTool: 'handler' is deprecated. Use 'execute' instead.",
|
|
255
253
|
);
|
|
256
254
|
}
|
|
257
255
|
|
|
258
|
-
const executeHandler = def.execute ?? def.handler;
|
|
256
|
+
const executeHandler = def.execute ?? (def as any).handler;
|
|
259
257
|
if (!executeHandler && !def.outputSchema)
|
|
260
258
|
throw new Error(
|
|
261
259
|
"To use a Convex tool, you must either provide an execute" +
|
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,167 @@ 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, existingResponseMessage } =
|
|
1074
|
+
await this.findApprovalContext(ctx, {
|
|
1075
|
+
threadId: args.threadId,
|
|
1076
|
+
approvalId: args.approvalId,
|
|
1077
|
+
});
|
|
1078
|
+
|
|
1079
|
+
const newPart = {
|
|
1080
|
+
type: "tool-approval-response" as const,
|
|
1081
|
+
approvalId: args.approvalId,
|
|
1082
|
+
approved: args.approved,
|
|
1083
|
+
reason: args.reason,
|
|
1084
|
+
};
|
|
1085
|
+
|
|
1086
|
+
// Merge into an existing approval-response message for this step
|
|
1087
|
+
// so the AI SDK sees a single tool message per step.
|
|
1088
|
+
if (existingResponseMessage) {
|
|
1089
|
+
const existingContent = existingResponseMessage.message?.content;
|
|
1090
|
+
const mergedContent = Array.isArray(existingContent)
|
|
1091
|
+
? [...(existingContent as any[]), newPart]
|
|
1092
|
+
: [newPart];
|
|
1093
|
+
await this.updateMessage(ctx, {
|
|
1094
|
+
messageId: existingResponseMessage._id,
|
|
1095
|
+
patch: {
|
|
1096
|
+
message: { role: "tool", content: mergedContent },
|
|
1097
|
+
status: "success",
|
|
1098
|
+
},
|
|
1099
|
+
});
|
|
1100
|
+
return { messageId: existingResponseMessage._id };
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
const { messageId } = await this.saveMessage(ctx, {
|
|
1104
|
+
threadId: args.threadId,
|
|
1105
|
+
promptMessageId,
|
|
1106
|
+
skipEmbeddings: true,
|
|
1107
|
+
message: {
|
|
1108
|
+
role: "tool",
|
|
1109
|
+
content: [newPart],
|
|
1110
|
+
},
|
|
1111
|
+
});
|
|
1112
|
+
return { messageId };
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
private async findApprovalContext(
|
|
1116
|
+
ctx: MutationCtx,
|
|
1117
|
+
args: { threadId: string; approvalId: string },
|
|
1118
|
+
): Promise<{
|
|
1119
|
+
promptMessageId: string;
|
|
1120
|
+
existingResponseMessage: MessageDoc | undefined;
|
|
1121
|
+
}> {
|
|
1122
|
+
// NOTE: This pagination returns messages in descending order (newest first).
|
|
1123
|
+
// The "already handled" check (tool-approval-response) relies on seeing
|
|
1124
|
+
// responses before their corresponding requests. If the pagination order
|
|
1125
|
+
// changes, this logic will need to be updated.
|
|
1126
|
+
let cursor: string | null = null;
|
|
1127
|
+
let existingResponseMessage: MessageDoc | undefined;
|
|
1128
|
+
do {
|
|
1129
|
+
const page = await this.listMessages(ctx, {
|
|
1130
|
+
threadId: args.threadId,
|
|
1131
|
+
paginationOpts: { cursor, numItems: 100 },
|
|
1132
|
+
});
|
|
1133
|
+
for (const message of page.page) {
|
|
1134
|
+
const content = message.message?.content;
|
|
1135
|
+
if (!Array.isArray(content)) continue;
|
|
1136
|
+
// Check if this assistant message starts a different approval step.
|
|
1137
|
+
// If so, any response message we've seen so far belongs to a newer
|
|
1138
|
+
// step — reset it so we don't merge across step boundaries.
|
|
1139
|
+
if (
|
|
1140
|
+
message.message?.role === "assistant" &&
|
|
1141
|
+
content.some(
|
|
1142
|
+
(p: any) =>
|
|
1143
|
+
p.type === "tool-approval-request" &&
|
|
1144
|
+
p.approvalId !== args.approvalId,
|
|
1145
|
+
)
|
|
1146
|
+
) {
|
|
1147
|
+
existingResponseMessage = undefined;
|
|
1148
|
+
}
|
|
1149
|
+
for (const part of content) {
|
|
1150
|
+
const typedPart = part as { type?: unknown; approvalId?: unknown };
|
|
1151
|
+
if (
|
|
1152
|
+
typedPart.type === "tool-approval-response" &&
|
|
1153
|
+
typedPart.approvalId === args.approvalId
|
|
1154
|
+
) {
|
|
1155
|
+
throw new Error(`Approval ${args.approvalId} was already handled`);
|
|
1156
|
+
}
|
|
1157
|
+
// Track the most recent tool-approval-response message for merging
|
|
1158
|
+
if (
|
|
1159
|
+
typedPart.type === "tool-approval-response" &&
|
|
1160
|
+
!existingResponseMessage
|
|
1161
|
+
) {
|
|
1162
|
+
existingResponseMessage = message;
|
|
1163
|
+
}
|
|
1164
|
+
if (
|
|
1165
|
+
typedPart.type === "tool-approval-request" &&
|
|
1166
|
+
typedPart.approvalId === args.approvalId
|
|
1167
|
+
) {
|
|
1168
|
+
return { promptMessageId: message._id, existingResponseMessage };
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
cursor = page.isDone ? null : page.continueCursor;
|
|
1173
|
+
} while (cursor !== null);
|
|
1174
|
+
|
|
1175
|
+
throw new Error(
|
|
1176
|
+
`Approval request ${args.approvalId} was not found in thread ${args.threadId}`,
|
|
1177
|
+
);
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1019
1180
|
/**
|
|
1020
1181
|
* Explicitly save a "step" created by the AI SDK.
|
|
1021
1182
|
* @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 = {
|
|
@@ -292,7 +292,7 @@ describe("search.ts", () => {
|
|
|
292
292
|
expect(result[1]._id).toBe("2");
|
|
293
293
|
});
|
|
294
294
|
|
|
295
|
-
it("should
|
|
295
|
+
it("should keep tool calls with approval request but NO approval response (auto-deny handles them)", () => {
|
|
296
296
|
const messages: MessageDoc[] = [
|
|
297
297
|
{
|
|
298
298
|
_id: "1",
|
|
@@ -321,19 +321,18 @@ describe("search.ts", () => {
|
|
|
321
321
|
|
|
322
322
|
const result = filterOutOrphanedToolMessages(messages);
|
|
323
323
|
expect(result).toHaveLength(1);
|
|
324
|
-
// The assistant message should
|
|
324
|
+
// The assistant message should keep the tool-call (auto-deny resolves it downstream)
|
|
325
325
|
const assistantContent = result[0].message?.content;
|
|
326
326
|
expect(Array.isArray(assistantContent)).toBe(true);
|
|
327
327
|
if (Array.isArray(assistantContent)) {
|
|
328
|
-
|
|
329
|
-
expect(assistantContent).toHaveLength(2);
|
|
328
|
+
expect(assistantContent).toHaveLength(3);
|
|
330
329
|
expect(assistantContent.find((p) => p.type === "text")).toBeDefined();
|
|
331
330
|
expect(
|
|
332
331
|
assistantContent.find((p) => p.type === "tool-approval-request"),
|
|
333
332
|
).toBeDefined();
|
|
334
333
|
expect(
|
|
335
334
|
assistantContent.find((p) => p.type === "tool-call"),
|
|
336
|
-
).
|
|
335
|
+
).toBeDefined();
|
|
337
336
|
}
|
|
338
337
|
});
|
|
339
338
|
|
package/src/client/search.ts
CHANGED
|
@@ -29,7 +29,33 @@ import type {
|
|
|
29
29
|
ActionCtx,
|
|
30
30
|
} from "./types.js";
|
|
31
31
|
import { inlineMessagesFiles } from "./files.js";
|
|
32
|
-
import {
|
|
32
|
+
import {
|
|
33
|
+
autoDenyUnresolvedApprovals,
|
|
34
|
+
docsToModelMessages,
|
|
35
|
+
toModelMessage,
|
|
36
|
+
} from "../mapping.js";
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Filter out approval parts from messages before sending to AI providers.
|
|
40
|
+
* Providers like Anthropic don't understand tool-approval-request and
|
|
41
|
+
* tool-approval-response parts and will error if they are included.
|
|
42
|
+
*/
|
|
43
|
+
function filterApprovalPartsForProvider(messages: ModelMessage[]): ModelMessage[] {
|
|
44
|
+
return messages.map((message) => {
|
|
45
|
+
if (!Array.isArray(message.content)) {
|
|
46
|
+
return message;
|
|
47
|
+
}
|
|
48
|
+
const filteredContent = message.content.filter(
|
|
49
|
+
(part) =>
|
|
50
|
+
(part as { type: string }).type !== "tool-approval-request" &&
|
|
51
|
+
(part as { type: string }).type !== "tool-approval-response",
|
|
52
|
+
);
|
|
53
|
+
return {
|
|
54
|
+
...message,
|
|
55
|
+
content: filteredContent,
|
|
56
|
+
} as typeof message;
|
|
57
|
+
});
|
|
58
|
+
}
|
|
33
59
|
|
|
34
60
|
const DEFAULT_VECTOR_SCORE_THRESHOLD = 0.0;
|
|
35
61
|
// 10k characters should be more than enough for most cases, and stays under
|
|
@@ -230,7 +256,11 @@ export async function fetchRecentAndSearchMessages(
|
|
|
230
256
|
searchMessages = filterOutOrphanedToolMessages(
|
|
231
257
|
sorted(searchResults.filter((m) => !included?.has(m._id))),
|
|
232
258
|
);
|
|
259
|
+
searchMessages = filterApprovalPartsForProvider(searchMessages);
|
|
233
260
|
}
|
|
261
|
+
|
|
262
|
+
// filter out approval parts before returning
|
|
263
|
+
searchMessages = filterApprovalPartsForProvider(searchMessages);
|
|
234
264
|
// Ensure we don't include tool messages without a corresponding tool call
|
|
235
265
|
return { recentMessages, searchMessages };
|
|
236
266
|
}
|
|
@@ -285,6 +315,12 @@ export function filterOutOrphanedToolMessages(docs: MessageDoc[]) {
|
|
|
285
315
|
return approvalId !== undefined && approvalResponseIds.has(approvalId);
|
|
286
316
|
};
|
|
287
317
|
|
|
318
|
+
// Helper: check if tool call has a pending approval request
|
|
319
|
+
// (auto-deny handles these downstream, so they must survive the filter)
|
|
320
|
+
const hasApprovalRequest = (toolCallId: string) => {
|
|
321
|
+
return approvalRequestsByToolCallId.has(toolCallId);
|
|
322
|
+
};
|
|
323
|
+
|
|
288
324
|
for (const doc of docs) {
|
|
289
325
|
if (
|
|
290
326
|
doc.message?.role === "assistant" &&
|
|
@@ -294,7 +330,8 @@ export function filterOutOrphanedToolMessages(docs: MessageDoc[]) {
|
|
|
294
330
|
(p) =>
|
|
295
331
|
p.type !== "tool-call" ||
|
|
296
332
|
toolResultIds.has(p.toolCallId) ||
|
|
297
|
-
hasApprovalResponse(p.toolCallId)
|
|
333
|
+
hasApprovalResponse(p.toolCallId) ||
|
|
334
|
+
hasApprovalRequest(p.toolCallId),
|
|
298
335
|
);
|
|
299
336
|
if (content.length) {
|
|
300
337
|
result.push({
|
|
@@ -657,11 +694,19 @@ export async function fetchContextWithPrompt(
|
|
|
657
694
|
})
|
|
658
695
|
: allMessages;
|
|
659
696
|
|
|
697
|
+
// Post-process: auto-deny unresolved approvals so the AI SDK sees a
|
|
698
|
+
// complete history. Applied after contextHandler so custom handlers
|
|
699
|
+
// don't need to handle this.
|
|
700
|
+
processedMessages = autoDenyUnresolvedApprovals(processedMessages);
|
|
701
|
+
|
|
660
702
|
// Process messages to inline localhost files (if not, file urls pointing to localhost will be sent to LLM providers)
|
|
661
703
|
if (process.env.CONVEX_CLOUD_URL?.startsWith("http://127.0.0.1")) {
|
|
662
704
|
processedMessages = await inlineMessagesFiles(processedMessages);
|
|
663
705
|
}
|
|
664
706
|
|
|
707
|
+
// Filter out approval parts before sending to AI providers
|
|
708
|
+
processedMessages = filterApprovalPartsForProvider(processedMessages);
|
|
709
|
+
|
|
665
710
|
return {
|
|
666
711
|
messages: processedMessages,
|
|
667
712
|
order: promptMessage?.order,
|
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,
|