import type { z } from 'zod' import type { ToolContext, ToolDefinition, ToolPermission, ToolResult, } from '../types/tool/index.js' import type { ToolPresentation } from '../types/tool/presentation.js' import { toErrorMessage } from '../utils/error.js' export interface DefineToolOptions { name: string description: string inputSchema: S modelInputSchema?: Record enforceModelInput?: boolean validationErrorHint?: string category: ToolDefinition['category'] permissions: ToolPermission[] /** Whether this exact call only observes state; conservative for unknown inputs. */ readOnly: boolean | ((input: z.infer) => boolean) destructive: boolean | ((input: z.infer) => boolean) concurrencySafe: boolean /** Batch ordering boundary; see {@link ToolDefinition.executionBarrier}. */ executionBarrier?: boolean tier?: string /** * How this tool's call and result should be shown; see * {@link ToolPresentation}. * * Here rather than only on `ToolDefinition` for the reason `maxRetries` * is: this builder is the sanctioned way to author a tool, and a field * the executor reads that the builder cannot set is a field only * hand-written definitions can use. */ presentCall?: ToolPresentation>['presentCall'] presentResult?: ToolPresentation>['presentResult'] /** Per-execution deadline; see {@link ToolDefinition.timeoutMs}. */ timeoutMs?: number /** * In-loop retry budget for a FAILED execution; see * {@link ToolDefinition.maxRetries}. * * The executor has always read this field, and this builder — the * sanctioned way to author a tool — had no way to set it, so the * documented "the tool author opts in, per tool" was reachable only by * hand-writing the interface. */ maxRetries?: number /** Return shape shown to the model; see {@link ToolDefinition.outputSchema}. */ outputSchema?: Record /** Settle the turn with this tool's output; see {@link ToolDefinition.terminal}. */ terminal?: boolean /** * The argument holding a shell command line; see * {@link ToolDefinition.commandArgument}. * * Here as well as on the definition for the reason `maxRetries` and * `presentCall` are: this builder is the sanctioned way to author a tool, * and a field a host reads that the builder cannot set is a field only * hand-written definitions can use. */ commandArgument?: string /** The argument holding a filesystem path; see {@link ToolDefinition.pathArgument}. */ pathArgument?: string /** The argument asking to leave the sandbox; see {@link ToolDefinition.sandboxEscapeArgument}. */ sandboxEscapeArgument?: string execute(input: z.infer, context: ToolContext): Promise } export function defineTool( options: DefineToolOptions, ): ToolDefinition> { type TInput = z.infer return { name: options.name, description: options.description, inputSchema: options.inputSchema, modelInputSchema: options.modelInputSchema, enforceModelInput: options.enforceModelInput, validationErrorHint: options.validationErrorHint, tier: options.tier, ...(options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}), ...(options.maxRetries !== undefined ? { maxRetries: options.maxRetries } : {}), ...(options.commandArgument !== undefined ? { commandArgument: options.commandArgument } : {}), ...(options.pathArgument !== undefined ? { pathArgument: options.pathArgument } : {}), ...(options.sandboxEscapeArgument !== undefined ? { sandboxEscapeArgument: options.sandboxEscapeArgument } : {}), ...(options.presentCall ? { presentCall: options.presentCall } : {}), ...(options.presentResult ? { presentResult: options.presentResult } : {}), ...(options.outputSchema !== undefined ? { outputSchema: options.outputSchema } : {}), ...(options.terminal !== undefined ? { terminal: options.terminal } : {}), ...(options.executionBarrier !== undefined ? { executionBarrier: options.executionBarrier } : {}), category: options.category, permissions: options.permissions, isReadOnly: typeof options.readOnly === 'function' ? options.readOnly : () => options.readOnly as boolean, isDestructive: typeof options.destructive === 'function' ? options.destructive : () => options.destructive as boolean, isConcurrencySafe: () => options.concurrencySafe, async execute(input: TInput, context: ToolContext): Promise { try { return await options.execute(input, context) } catch (err) { const message = toErrorMessage(err) return { success: false, output: '', error: `${options.name} failed: ${message}`, } } }, } }