import type { ToolResultOutput } from "@ai-sdk/provider-utils"; import type { FlexibleSchema, ModelMessage, Tool, ToolExecutionOptions, ToolSet, } from "ai"; import { tool } from "ai"; import type { GenericActionCtx, GenericDataModel } from "convex/server"; import type { ProviderOptions } from "../validators.js"; import type { Agent } from "./index.js"; const MIGRATION_URL = "node_modules/@convex-dev/agent/MIGRATION.md"; const warnedDeprecations = new Set(); function warnDeprecation(key: string, message: string) { if (!warnedDeprecations.has(key)) { warnedDeprecations.add(key); console.warn(`[@convex-dev/agent] ${message}\n See: ${MIGRATION_URL}`); } } export type ToolCtx = GenericActionCtx & { agent?: Agent; userId?: string; threadId?: string; messageId?: string; }; /** * Function that is called to determine if the tool needs approval before it can be executed. */ export type ToolNeedsApprovalFunctionCtx< INPUT, Ctx extends ToolCtx = ToolCtx, > = ( ctx: Ctx, input: INPUT, options: { /** * The ID of the tool call. You can use it e.g. when sending tool-call related information with stream data. */ toolCallId: string; /** * Messages that were sent to the language model to initiate the response that contained the tool call. * The messages **do not** include the system prompt nor the assistant response that contained the tool call. */ messages: ModelMessage[]; /** * Additional context. * * Experimental (can break in patch releases). */ experimental_context?: unknown; }, ) => boolean | PromiseLike; export type ToolExecuteFunctionCtx< INPUT, OUTPUT, Ctx extends ToolCtx = ToolCtx, > = ( ctx: Ctx, input: INPUT, options: ToolExecutionOptions, ) => AsyncIterable | PromiseLike; type NeverOptional = 0 extends 1 & N ? Partial : [N] extends [never] ? Partial> : T; /** * Error message type for deprecated 'handler' property. * Using a string literal type causes TypeScript to show this message in errors. */ type HANDLER_REMOVED_ERROR = "⚠️ 'handler' was removed in @convex-dev/agent v0.6.0. Rename to 'execute'. See: node_modules/@convex-dev/agent/MIGRATION.md"; export type ToolOutputPropertiesCtx< INPUT, OUTPUT, Ctx extends ToolCtx = ToolCtx, > = NeverOptional< OUTPUT, { /** * An async function that is called with the arguments from the tool call and produces a result. * If `execute` is not provided, the tool will not be executed automatically. * * @param input - The input of the tool call. * @param options.abortSignal - A signal that can be used to abort the tool call. */ execute?: ToolExecuteFunctionCtx; outputSchema?: FlexibleSchema; /** * @deprecated Removed in v0.6.0. Use `execute` instead. */ handler?: HANDLER_REMOVED_ERROR; } >; /** * Error message type for deprecated 'args' property. * Using a string literal type causes TypeScript to show this message in errors. */ type ARGS_REMOVED_ERROR = "⚠️ 'args' was removed in @convex-dev/agent v0.6.0. Rename to 'inputSchema'. See: node_modules/@convex-dev/agent/MIGRATION.md"; export type ToolInputProperties = { /** * The schema of the input that the tool expects. * The language model will use this to generate the input. * It is also used to validate the output of the language model. * * You can use descriptions on the schema properties to make the input understandable for the language model. */ inputSchema: FlexibleSchema; /** * @deprecated Removed in v0.6.0. Use `inputSchema` instead. */ args?: ARGS_REMOVED_ERROR; }; /** * This is a wrapper around the ai.tool function that adds extra context to the * tool call, including the action context, userId, threadId, and messageId. * @param tool The tool. See https://sdk.vercel.ai/docs/ai-sdk-core/tools-and-tool-calling * Currently contains deprecated parameters `args` and `handler` to maintain backwards compatibility * but these will be removed in the future. Use `inputSchema` and `execute` instead, respectively. * * @returns A tool to be used with the AI SDK. */ export function createTool( def: { /** * An optional description of what the tool does. * Will be used by the language model to decide whether to use the tool. * Not used for provider-defined tools. */ description?: string; /** * An optional title of the tool. */ title?: string; /** * Additional provider-specific metadata. They are passed through * to the provider from the AI SDK and enable provider-specific * functionality that can be fully encapsulated in the provider. */ providerOptions?: ProviderOptions; } & ToolInputProperties & { /** * An optional list of input examples that show the language * model what the input should look like. */ inputExamples?: Array<{ input: NoInfer; }>; /** * Whether the tool needs approval before it can be executed. */ needsApproval?: | boolean | ToolNeedsApprovalFunctionCtx< [INPUT] extends [never] ? unknown : INPUT, Ctx >; /** * Strict mode setting for the tool. * * Providers that support strict mode will use this setting to determine * how the input should be generated. Strict mode will always produce * valid inputs, but it might limit what input schemas are supported. */ strict?: boolean; /** * Provide the context to use, e.g. when defining the tool at runtime. */ ctx?: Ctx; /** * Optional function that is called when the argument streaming starts. * Only called when the tool is used in a streaming context. */ onInputStart?: ( ctx: Ctx, options: ToolExecutionOptions, ) => void | PromiseLike; /** * Optional function that is called when an argument streaming delta is available. * Only called when the tool is used in a streaming context. */ onInputDelta?: ( ctx: Ctx, options: { inputTextDelta: string } & ToolExecutionOptions, ) => void | PromiseLike; /** * Optional function that is called when a tool call can be started, * even if the execute function is not provided. */ onInputAvailable?: ( ctx: Ctx, options: { input: [INPUT] extends [never] ? unknown : INPUT; } & ToolExecutionOptions, ) => void | PromiseLike; } & ToolOutputPropertiesCtx & { /** * Optional conversion function that maps the tool result to an output that can be used by the language model. * * If not provided, the tool result will be sent as a JSON object. */ toModelOutput?: ( ctx: Ctx, options: { /** * The ID of the tool call. You can use it e.g. when sending tool-call related information with stream data. */ toolCallId: string; /** * The input of the tool call. */ input: [INPUT] extends [never] ? unknown : INPUT; /** * The output of the tool call. */ output: 0 extends 1 & OUTPUT ? any : [OUTPUT] extends [never] ? any : NoInfer; }, ) => ToolResultOutput | PromiseLike; }, ): Tool { // Runtime backwards compat - types will show errors but runtime still works const inputSchema = def.inputSchema ?? (def as any).args; if (!inputSchema) throw new Error("To use a Convex tool, you must provide an `inputSchema`"); if ((def as any).args && !def.inputSchema) { warnDeprecation( "createTool.args", "createTool: 'args' is deprecated. Use 'inputSchema' instead.", ); } if ((def as any).handler && !def.execute) { warnDeprecation( "createTool.handler", "createTool: 'handler' is deprecated. Use 'execute' instead.", ); } const executeHandler = def.execute ?? (def as any).handler; if (!executeHandler && !def.outputSchema) throw new Error( "To use a Convex tool, you must either provide an execute" + " handler function, define an outputSchema, or both", ); const t = tool({ type: "function", __acceptsCtx: true, ctx: def.ctx, description: def.description, title: def.title, providerOptions: def.providerOptions, inputSchema, inputExamples: def.inputExamples, needsApproval(this: Tool, input, options) { const needsApproval = def.needsApproval; if (!needsApproval || typeof needsApproval === "boolean") return Boolean(needsApproval); if (!getCtx(this)) { throw new Error( "To use a Convex tool, you must either provide the ctx" + " at definition time (dynamically in an action), or use the Agent to" + " call it (which injects the ctx, userId and threadId)", ); } return needsApproval(getCtx(this), input, options); }, strict: def.strict, ...(executeHandler ? { execute( this: Tool, input: INPUT, options: ToolExecutionOptions, ) { if (!getCtx(this)) { throw new Error( "To use a Convex tool, you must either provide the ctx" + " at definition time (dynamically in an action), or use the Agent to" + " call it (which injects the ctx, userId and threadId)", ); } return executeHandler(getCtx(this), input, options); }, } : {}), outputSchema: def.outputSchema, }); if (def.onInputStart) { const origOnInputStart = def.onInputStart; t.onInputStart = function (this: Tool, options) { return origOnInputStart.call(this, getCtx(this), options); }; } if (def.onInputDelta) { const origOnInputDelta = def.onInputDelta; t.onInputDelta = function (this: Tool, options) { return origOnInputDelta.call(this, getCtx(this), options); }; } if (def.onInputAvailable) { const origOnInputAvailable = def.onInputAvailable; t.onInputAvailable = function (this: Tool, options) { return origOnInputAvailable.call(this, getCtx(this), options); }; } if (def.toModelOutput) { const origToModelOutput = def.toModelOutput; t.toModelOutput = function (this: Tool, options) { return origToModelOutput.call(this, getCtx(this), options); }; } return t; } function getCtx(tool: any): Ctx { return (tool as { ctx: Ctx }).ctx; } export function wrapTools( ctx: ToolCtx, ...toolSets: (ToolSet | undefined)[] ): ToolSet { const output = {} as ToolSet; for (const toolSet of toolSets) { if (!toolSet) { continue; } for (const [name, tool] of Object.entries(toolSet)) { if (tool && !(tool as any).__acceptsCtx) { output[name] = tool; } else { const out = { ...tool, ctx }; output[name] = out; } } } return output; }