@convex-dev/agent
Version:
A agent component for Convex.
358 lines (341 loc) • 11.7 kB
text/typescript
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<string>();
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<DataModel extends GenericDataModel = GenericDataModel> =
GenericActionCtx<DataModel> & {
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<boolean>;
export type ToolExecuteFunctionCtx<
INPUT,
OUTPUT,
Ctx extends ToolCtx = ToolCtx,
> = (
ctx: Ctx,
input: INPUT,
options: ToolExecutionOptions,
) => AsyncIterable<OUTPUT> | PromiseLike<OUTPUT>;
type NeverOptional<N, T> = 0 extends 1 & N
? Partial<T>
: [N] extends [never]
? Partial<Record<keyof T, undefined>>
: 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<INPUT, OUTPUT, Ctx>;
outputSchema?: FlexibleSchema<OUTPUT>;
/**
* @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<INPUT> = {
/**
* 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<INPUT>;
/**
* @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<INPUT, OUTPUT, Ctx extends ToolCtx = ToolCtx>(
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<INPUT> & {
/**
* An optional list of input examples that show the language
* model what the input should look like.
*/
inputExamples?: Array<{
input: NoInfer<INPUT>;
}>;
/**
* 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<void>;
/**
* 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<void>;
/**
* 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<void>;
} & ToolOutputPropertiesCtx<INPUT, OUTPUT, Ctx> & {
/**
* 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<OUTPUT>;
},
) => ToolResultOutput | PromiseLike<ToolResultOutput>;
},
): Tool<INPUT, OUTPUT> {
// 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<INPUT, OUTPUT>({
type: "function",
__acceptsCtx: true,
ctx: def.ctx,
description: def.description,
title: def.title,
providerOptions: def.providerOptions,
inputSchema,
inputExamples: def.inputExamples,
needsApproval(this: Tool<INPUT, OUTPUT>, 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, OUTPUT>,
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<INPUT, OUTPUT>, options) {
return origOnInputStart.call(this, getCtx(this), options);
};
}
if (def.onInputDelta) {
const origOnInputDelta = def.onInputDelta;
t.onInputDelta = function (this: Tool<INPUT, OUTPUT>, options) {
return origOnInputDelta.call(this, getCtx(this), options);
};
}
if (def.onInputAvailable) {
const origOnInputAvailable = def.onInputAvailable;
t.onInputAvailable = function (this: Tool<INPUT, OUTPUT>, options) {
return origOnInputAvailable.call(this, getCtx(this), options);
};
}
if (def.toModelOutput) {
const origToModelOutput = def.toModelOutput;
t.toModelOutput = function (this: Tool<INPUT, OUTPUT>, options) {
return origToModelOutput.call(this, getCtx(this), options);
};
}
return t;
}
function getCtx<Ctx extends ToolCtx>(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;
}