@mastra/core
Version:
690 lines (689 loc) • 27.3 kB
JavaScript
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