UNPKG

@mastra/core

Version:
690 lines (689 loc) 27.3 kB
import { a as RequestContext } from "./request-context-p_Tq-4EM.js"; import { standardSchemaToJSONSchema, toStandardSchema } from "./schema/index.js"; import { a as isZodObject, i as isZodArray, r as getZodTypeName, s as unwrapZodType } from "./zod-utils-DTkc-hhd.js"; //#region src/tools/validation.ts /** * Safely validates data against a Standard Schema. * Catches internal Zod errors (like undefined union options) and provides better error messages. * * @param schema The Standard Schema to validate against * @param data The data to validate * @returns The validation result or throws with a descriptive error */ function safeValidate(schema, data) { try { const result = schema["~standard"].validate(data); if (result instanceof Promise) throw new Error("Your schema is async, which is not supported. Please use a sync schema."); if ("issues" in result && Array.isArray(result.issues) && result.issues.length > 0) return { issues: result.issues }; return result; } catch (err) { if (err instanceof TypeError && err.message.includes("Cannot read properties of undefined")) throw new Error(`Schema validation failed due to an invalid schema definition. This often happens when a union schema (z.union or z.or) has undefined options. Please check that all schema options are properly defined. Original error: ${err.message}`); throw err; } } function isValidationError(value) { return value !== null && typeof value === "object" && "error" in value && value.error === true && "validationErrors" in value; } /** * Extracts a string key from a path segment (handles both PropertyKey and PathSegment objects). */ function getPathKey(segment) { if (typeof segment === "object" && segment !== null && "key" in segment) return String(segment.key); return String(segment); } /** * Creates an empty FormattedValidationErrors object. */ function createEmptyErrors() { return { errors: [], fields: {} }; } /** * Builds a formatted errors object from standard schema validation issues. * * @param issues Array of validation issues from standard schema validation * @returns Formatted errors object with nested structure based on paths */ function buildFormattedErrors(issues) { const result = createEmptyErrors(); for (const issue of issues) if (!issue.path || issue.path.length === 0) result.errors.push(issue.message); else { let current = result; for (let i = 0; i < issue.path.length; i++) { const key = getPathKey(issue.path[i]); if (i === issue.path.length - 1) { if (!current.fields[key]) current.fields[key] = createEmptyErrors(); current.fields[key].errors.push(issue.message); } else { if (!current.fields[key]) current.fields[key] = createEmptyErrors(); current = current.fields[key]; } } } return result; } /** * Safely truncates data for error messages to avoid exposing sensitive information. * @param data The data to truncate * @param maxLength Maximum length of the truncated string (default: 200) * @returns Truncated string representation */ function truncateForLogging(data, maxLength = 200) { try { const stringified = JSON.stringify(data, null, 2); if (stringified.length <= maxLength) return stringified; return stringified.slice(0, maxLength) + "... (truncated)"; } catch { return "[Unable to serialize data]"; } } /** * Validates raw suspend data against a schema. * * @param schema The schema to validate against * @param suspendData The raw suspend data to validate * @param toolId Optional tool ID for better error messages * @returns The validated data or a validation error */ function validateToolSuspendData(schema, suspendData, toolId) { if (!schema || !("~standard" in schema)) return { data: suspendData }; const validation = safeValidate(schema, suspendData); if ("value" in validation) return { data: validation.value }; const errorMessages = validation.issues.map((e) => `- ${e.path?.map((p) => getPathKey(p)).join(".") || "root"}: ${e.message}`).join("\n"); return { error: { error: true, message: `Tool suspension data validation failed${toolId ? ` for ${toolId}` : ""}. Please fix the following errors and try again:\n${errorMessages}\n\nProvided arguments: ${truncateForLogging(suspendData)}`, validationErrors: buildFormattedErrors(validation.issues) } }; } /** * Normalizes undefined/null input to an appropriate default value based on schema type. * This handles LLMs (Claude Sonnet 4.5, Gemini 2.4, etc.) that send undefined/null * instead of {} or [] when all parameters are optional. * * @param schema The Zod schema to check * @param input The input to normalize * @returns The normalized input (original value, {}, or []) */ function normalizeNullishInput(schema, input) { if (typeof input !== "undefined" && input !== null) return input; const jsonSchema = standardSchemaToJSONSchema(schema, { io: "input" }); if (jsonSchema.type === "array") return []; if (jsonSchema.type === "object") return {}; return input; } /** * Checks if a value is a plain object (created by {} or new Object()). * This excludes class instances, built-in objects like Date/Map/URL, etc. * * @param value The value to check * @returns true if the value is a plain object */ function isPlainObject(value) { if (value === null || typeof value !== "object") return false; const proto = Object.getPrototypeOf(value); return proto === Object.prototype || proto === null; } /** * Recursively converts undefined values to null in an object. * This is needed for OpenAI compat layers which convert .optional() to .nullable() * for strict mode compliance. When fields are omitted (undefined), we convert them * to null so the schema validation passes, and the transform then converts null back * to undefined. (GitHub #11457) * * Only recurses into plain objects to preserve class instances and built-in objects * like Date, Map, URL, etc. (GitHub #11502) * * @param input The input to process * @returns The processed input with undefined values converted to null */ function convertUndefinedToNull(input) { if (input === void 0) return null; if (input === null || typeof input !== "object") return input; if (Array.isArray(input)) return input.map(convertUndefinedToNull); if (!isPlainObject(input)) return input; const result = {}; for (const [key, value] of Object.entries(input)) result[key] = convertUndefinedToNull(value); return result; } /** * Recursively strips null/undefined values from object properties. * This handles LLMs (e.g. Gemini) that send null for .optional() fields, * where Zod expects undefined, not null. By stripping nullish values, * we let Zod treat them as "not provided" which matches .optional() semantics. * (GitHub #12362) * * @param input The input to process * @returns The processed input with null/undefined values stripped from objects */ function stripNullishValues(input) { if (input === null || input === void 0) return; if (typeof input !== "object") return input; if (Array.isArray(input)) return input.map((item) => item === null ? null : stripNullishValues(item)); if (!isPlainObject(input)) return input; const result = {}; for (const [key, value] of Object.entries(input)) { if (value === null || value === void 0) continue; result[key] = stripNullishValues(value); } return result; } /** * Strip null/undefined values only at specific paths that caused validation errors. * Preserves null for .nullable() fields that are valid. */ function stripNullishValuesAtPaths(input, paths, currentPath = "") { if (input === null || input === void 0) return paths.has(currentPath) ? void 0 : input; if (typeof input !== "object") return input; if (Array.isArray(input)) return input.map((item, i) => stripNullishValuesAtPaths(item, paths, currentPath ? `${currentPath}.${i}` : String(i))); if (!isPlainObject(input)) return input; const result = {}; for (const [key, value] of Object.entries(input)) { const fieldPath = currentPath ? `${currentPath}.${key}` : key; if ((value === null || value === void 0) && paths.has(fieldPath)) continue; result[key] = stripNullishValuesAtPaths(value, paths, fieldPath); } return result; } /** * Gets the value at a path in a nested object, using the same path segment format * as Standard Schema validation issues. * * @param obj The object to traverse * @param pathSegments Array of path segments from a validation issue * @returns The value at the path, or a sentinel symbol if the path doesn't exist */ const PATH_NOT_FOUND = Symbol("PATH_NOT_FOUND"); function getValueAtPath(obj, pathSegments) { let current = obj; for (const segment of pathSegments) { if (current === null || current === void 0 || typeof current !== "object") return PATH_NOT_FOUND; const key = typeof segment === "object" && segment !== null && "key" in segment ? String(segment.key) : String(segment); current = current[key]; } return current; } /** * Coerces stringified JSON values in object properties when the schema expects * an array or object but the LLM returned a JSON string. * * Some LLMs (e.g., GLM4.7) return stringified JSON for array/object parameters: * { "args": "[\"parse_excel.py\"]" } * instead of: * { "args": ["parse_excel.py"] } * * This function walks the top-level properties of a plain object and attempts * to JSON.parse string values when the schema expects a non-string type. * (GitHub #12757) * * @param schema The Zod schema to check field types against * @param input The input to process * @returns The input with stringified JSON values coerced, or the original input */ function coerceStringifiedJsonValues(schema, input) { if (!isPlainObject(input)) return input; const unwrapped = unwrapZodType(schema); if (!isZodObject(unwrapped)) return input; const shape = unwrapped.shape; if (!shape || typeof shape !== "object") return input; let changed = false; const result = { ...input }; for (const [key, value] of Object.entries(input)) { if (typeof value !== "string") continue; const fieldSchema = shape[key]; if (!fieldSchema) continue; const baseFieldSchema = unwrapZodType(fieldSchema); if (getZodTypeName(baseFieldSchema) === "ZodString") continue; const trimmed = value.trim(); if (isZodArray(baseFieldSchema) && trimmed.startsWith("[") || isZodObject(baseFieldSchema) && trimmed.startsWith("{")) try { const parsed = JSON.parse(value); if (isZodArray(baseFieldSchema) && Array.isArray(parsed) || isZodObject(baseFieldSchema) && isPlainObject(parsed)) { result[key] = parsed; changed = true; } } catch {} } return changed ? result : input; } /** * Validates raw input data against a schema. * * @param schema The schema to validate against (or undefined to skip validation) * @param input The raw input data to validate * @param toolId Optional tool ID for better error messages * @returns The validated data or a validation error */ function validateToolInput(schema, input, toolId) { if (!schema || !("~standard" in schema)) return { data: input }; schema = toStandardSchema(schema); let normalizedInput = normalizeNullishInput(schema, input); normalizedInput = convertUndefinedToNull(normalizedInput); const validation = safeValidate(schema, normalizedInput); if ("value" in validation) return { data: validation.value }; const coercedInput = coerceStringifiedJsonValues(schema, normalizedInput); if (coercedInput !== normalizedInput) { const coercedValidation = safeValidate(schema, coercedInput); if ("value" in coercedValidation) return { data: coercedValidation.value }; } const failingNullPaths = new Set(validation.issues.filter((issue) => { if (!issue.path || issue.path.length === 0) return false; const value = getValueAtPath(normalizedInput, issue.path); return value === null || value === void 0; }).map((issue) => issue.path?.map((p) => typeof p === "object" && "key" in p ? String(p.key) : String(p)).join(".")).filter((p) => !!p)); const strippedInput = failingNullPaths.size > 0 ? stripNullishValuesAtPaths(input, failingNullPaths) : stripNullishValues(input); const normalizedStripped = normalizeNullishInput(schema, strippedInput); const retryValidation = safeValidate(schema, normalizedStripped); if ("value" in retryValidation) return { data: retryValidation.value }; const promptJsonSchema = standardSchemaToJSONSchema(schema, { io: "input" }); if (promptJsonSchema.type === "object" && promptJsonSchema.properties != null && "prompt" in promptJsonSchema.properties && normalizedInput != null && typeof normalizedInput === "object" && !Array.isArray(normalizedInput)) { const obj = normalizedInput; if (obj.prompt == null) { const alias = [ obj.query, obj.message, obj.input ].find((v) => typeof v === "string"); if (alias !== void 0) { const coercedPromptInput = { ...obj, prompt: alias }; const coercedPromptValidation = safeValidate(schema, coercedPromptInput); if ("value" in coercedPromptValidation) return { data: coercedPromptValidation.value }; } } } const errorMessages = validation.issues.map((e) => `- ${e.path?.map((p) => getPathKey(p)).join(".") || "root"}: ${e.message}`).join("\n"); return { error: { error: true, message: `Tool input validation failed${toolId ? ` for ${toolId}` : ""}. Please fix the following errors and try again:\n${errorMessages}\n\nProvided arguments: ${truncateForLogging(input)}`, validationErrors: buildFormattedErrors(validation.issues) } }; } /** * Validates tool output data against a schema. * * @param schema The schema to validate against * @param output The output data to validate * @param toolId Optional tool ID for better error messages * @returns The validated data or a validation error */ function validateToolOutput(schema, output, toolId, suspendCalled) { if (!schema || !("~standard" in schema) || suspendCalled) return { data: output }; const validation = safeValidate(schema, output); if ("value" in validation) return { data: validation.value }; const errorMessages = validation.issues.map((e) => `- ${e.path?.map((p) => getPathKey(p)).join(".") || "root"}: ${e.message}`).join("\n"); return { error: { error: true, message: `Tool output validation failed${toolId ? ` for ${toolId}` : ""}. The tool returned invalid output:\n${errorMessages}\n\nReturned output: ${truncateForLogging(output)}`, validationErrors: buildFormattedErrors(validation.issues) } }; } /** * Keys that are considered sensitive and should be redacted in error messages. */ const SENSITIVE_KEYS = [ "password", "secret", "token", "apiKey", "api_key", "auth", "credential" ]; /** * Redacts sensitive keys from an object for safe logging. * @param obj The object to redact * @returns A new object with sensitive values replaced with '[REDACTED]' */ function redactSensitiveKeys(obj) { if (obj === null || typeof obj !== "object") return obj; if (Array.isArray(obj)) return obj.map(redactSensitiveKeys); const result = {}; for (const [key, value] of Object.entries(obj)) if (SENSITIVE_KEYS.some((sensitive) => key.toLowerCase().includes(sensitive.toLowerCase()))) result[key] = "[REDACTED]"; else if (typeof value === "object" && value !== null) result[key] = redactSensitiveKeys(value); else result[key] = value; return result; } /** * Validates request context data against a schema. * This is used to validate the request context before tool execution. * * @param schema The schema to validate against (PublicSchema which accepts Zod, JSONSchema, etc.) * @param requestContext The request context to validate * @param identifier Optional identifier (tool/step ID) for better error messages * @returns The validated data or a validation error */ function validateRequestContext(schema, requestContext, identifier) { if (!schema) return { data: requestContext?.all ?? {} }; const contextValues = requestContext?.all ?? {}; const validation = toStandardSchema(schema)["~standard"].validate(contextValues); if (validation instanceof Promise) throw new Error("Your schema is async, which is not supported. Please use a sync schema."); if ("value" in validation) return { data: validation.value }; const errorMessages = validation.issues.map((e) => `- ${e.path?.map((p) => getPathKey(p)).join(".") || "root"}: ${e.message}`).join("\n"); const redactedContext = redactSensitiveKeys(contextValues); return { data: contextValues, error: { error: true, message: `Request context validation failed${identifier ? ` for ${identifier}` : ""}. Please fix the following errors and try again:\n${errorMessages}\n\nProvided request context: ${truncateForLogging(redactedContext)}`, validationErrors: buildFormattedErrors(validation.issues) } }; } //#endregion //#region src/tools/tool.ts /** * Marker to identify Mastra tools even when `instanceof` fails. * This can happen in environments like Vite SSR where the same module * may be loaded multiple times, creating different class instances. * Uses Symbol.for() so the same symbol is shared across module copies. * Follows the naming convention: <org>.<product>.<category>.<className> */ const MASTRA_TOOL_MARKER = Symbol.for("mastra.core.tool.Tool"); /** * A type-safe tool that agents and workflows can call to perform specific actions. * * @template TSchemaIn - Input schema type * @template TSchemaOut - Output schema type * @template TSuspendSchema - Suspend operation schema type * @template TResumeSchema - Resume operation schema type * @template TContext - Execution context type * * @example Basic tool with validation * ```typescript * const weatherTool = createTool({ * id: 'get-weather', * description: 'Get weather for a location', * inputSchema: z.object({ * location: z.string(), * units: z.enum(['celsius', 'fahrenheit']).optional() * }), * execute: async (inputData) => { * return await fetchWeather(inputData.location, inputData.units); * } * }); * ``` * * @example Tool requiring approval * ```typescript * const deleteFileTool = createTool({ * id: 'delete-file', * description: 'Delete a file', * requireApproval: true, * inputSchema: z.object({ filepath: z.string() }), * execute: async (inputData) => { * await fs.unlink(inputData.filepath); * return { deleted: true }; * } * }); * ``` * * @example Tool with Mastra integration * ```typescript * const saveTool = createTool({ * id: 'save-data', * description: 'Save data to storage', * inputSchema: z.object({ key: z.string(), value: z.any() }), * execute: async (inputData, context) => { * const storage = context?.mastra?.getStorage(); * await storage?.set(inputData.key, inputData.value); * return { saved: true }; * } * }); * ``` */ var Tool = class { /** Unique identifier for the tool */ id; /** Description of what the tool does */ description; /** Schema for validating input parameters */ inputSchema; /** Schema for validating output structure */ outputSchema; /** Schema for suspend operation data */ suspendSchema; /** Schema for resume operation data */ resumeSchema; /** * Schema for validating request context values. * When provided, the request context will be validated against this schema before tool execution. */ requestContextSchema; /** * Tool execution function * @param inputData - The raw, validated input data * @param context - Optional execution context with metadata * @returns Promise resolving to tool output or a ValidationError if input validation fails */ execute; /** Parent Mastra instance for accessing shared resources */ mastra; /** * Whether the tool requires explicit user approval before execution. * Accepts a boolean for static behavior, or a function evaluated per-call * for conditional approval. * @example * ```typescript * // Static * requireApproval: true * * // Conditional — only require approval for non-dry-run calls * requireApproval: async ({ isDryRun }) => !isDryRun * ``` */ requireApproval; /** * Runtime-resolved per-tool approval predicate, evaluated per call. * * This is set automatically when a tool's `requireApproval` is a function, or by the * MCP client when wrapping a server-level `requireToolApproval` function — not something * you normally set yourself (prefer the `requireApproval` option). When present it is the * authoritative per-tool approval decision and is always evaluated by the agent runtime. */ needsApprovalFn; /** * Enables strict tool input generation for providers that support it. */ strict; /** * Provider-specific options passed to the model when this tool is used. * Keys are provider names (e.g., 'anthropic', 'openai'), values are provider-specific configs. * @example * ```typescript * providerOptions: { * anthropic: { * cacheControl: { type: 'ephemeral' } * } * } * ``` */ providerOptions; /** * Optional function to transform the tool's raw output before sending it to the model. * The raw result is still available for application logic; only the model sees the transformed version. */ toModelOutput; /** * Optional target-aware transform for display and transcript payloads. */ transform; /** * Optional MCP-specific properties including annotations and metadata. * Only relevant when the tool is being used in an MCP context. * @example * ```typescript * mcp: { * annotations: { * title: 'Weather Lookup', * readOnlyHint: true, * destructiveHint: false * }, * _meta: { * version: '1.0.0', * author: 'team@example.com' * } * } * ``` */ mcp; onInputStart; onInputDelta; onInputAvailable; onOutput; /** * Examples of valid tool inputs passed through to the AI SDK. */ inputExamples; /** * Metadata identifying this tool as originating from an MCP server. * Set automatically by the MCP client when creating tools. */ mcpMetadata; /** * Background task configuration for this tool. * When enabled, the tool can be executed in the background while the agent conversation continues. */ background; /** * Creates a new Tool instance with input validation wrapper. * * @param opts - Tool configuration and execute function * @example * ```typescript * const tool = new Tool({ * id: 'my-tool', * description: 'Does something useful', * inputSchema: z.object({ name: z.string() }), * execute: async (inputData) => ({ greeting: `Hello ${inputData.name}` }) * }); * ``` */ constructor(opts) { this[MASTRA_TOOL_MARKER] = true; this.id = opts.id; this.description = opts.description; this.inputSchema = opts.inputSchema ? toStandardSchema(opts.inputSchema) : void 0; this.outputSchema = opts.outputSchema ? toStandardSchema(opts.outputSchema) : void 0; this.suspendSchema = opts.suspendSchema ? toStandardSchema(opts.suspendSchema) : void 0; this.resumeSchema = opts.resumeSchema ? toStandardSchema(opts.resumeSchema) : void 0; this.requestContextSchema = opts.requestContextSchema; this.mastra = opts.mastra; this.requireApproval = opts.requireApproval || false; this.strict = opts.strict; this.providerOptions = opts.providerOptions; this.toModelOutput = opts.toModelOutput; this.transform = opts.transform; this.inputExamples = opts.inputExamples; this.mcp = opts.mcp; this.mcpMetadata = opts.mcpMetadata; this.background = opts.background; this.onInputStart = opts.onInputStart; this.onInputDelta = opts.onInputDelta; this.onInputAvailable = opts.onInputAvailable; this.onOutput = opts.onOutput; if (opts.execute) { const originalExecute = opts.execute; this.execute = async (inputData, context) => { const isResuming = !!(context?.resumeData || context?.agent?.resumeData); let data = inputData; if (!isResuming) { const validationResult = validateToolInput(this.inputSchema, inputData, this.id); if (validationResult.error) return validationResult.error; data = validationResult.data; } const { error: requestContextError } = validateRequestContext(this.requestContextSchema, context?.requestContext, this.id); if (requestContextError) return requestContextError; let suspendData = null; const baseContext = context ? { ...context, ...context.suspend ? { suspend: (args, suspendOptions) => { suspendData = args; return context.suspend?.(args, suspendOptions); } } : {} } : {}; let organizedContext = baseContext; if (!context) organizedContext = { requestContext: new RequestContext(), mastra: void 0 }; else { const isAgentExecution = baseContext.toolCallId && baseContext.messages; const isWorkflowExecution = !isAgentExecution && (baseContext.workflow || baseContext.workflowId); if (isAgentExecution && !baseContext.agent) { const { agentId, toolCallId, messages, suspend, resumeData, threadId, resourceId, writableStream, ...rest } = baseContext; organizedContext = { ...rest, agent: { agentId: agentId || "", toolCallId, messages, suspend, resumeData, threadId, resourceId, writableStream }, requestContext: rest.requestContext || new RequestContext() }; } else if (isWorkflowExecution && !baseContext.workflow) { const { workflowId, runId, state, setState, suspend, resumeData, ...rest } = baseContext; organizedContext = { ...rest, workflow: { workflowId, runId, state, setState, suspend, resumeData }, requestContext: rest.requestContext || new RequestContext() }; } else organizedContext = { ...baseContext, agent: baseContext.agent ? { ...baseContext.agent, agentId: baseContext.agent.agentId ?? "", suspend: (args, suspendOptions) => { suspendData = args; return baseContext.agent?.suspend?.(args, suspendOptions); } } : baseContext.agent, workflow: baseContext.workflow ? { ...baseContext.workflow, suspend: (args, suspendOptions) => { suspendData = args; return baseContext.workflow?.suspend?.(args, suspendOptions); } } : baseContext.workflow, requestContext: baseContext.requestContext || new RequestContext() }; } const resumeData = organizedContext.agent?.resumeData ?? organizedContext.workflow?.resumeData ?? organizedContext?.resumeData; if (resumeData) { const resumeValidation = validateToolInput(this.resumeSchema, resumeData, this.id); if (resumeValidation.error) return resumeValidation.error; } const output = await originalExecute(data, organizedContext); if (suspendData) { const suspendValidation = validateToolSuspendData(this.suspendSchema, suspendData, this.id); if (suspendValidation.error) return suspendValidation.error; } const skiptOutputValidation = !!(typeof output === "undefined" && suspendData); const outputValidation = validateToolOutput(this.outputSchema, output, this.id, skiptOutputValidation); if (outputValidation.error) return outputValidation.error; return outputValidation.data; }; } } }; function createTool(opts) { return new Tool(opts); } //#endregion export { validateToolInput as a, isValidationError as i, Tool as n, validateToolOutput as o, createTool as r, validateToolSuspendData as s, MASTRA_TOOL_MARKER as t }; //# sourceMappingURL=tool-qGw4ZhYO.js.map