@mastra/core
Version:
1 lines • 96.5 kB
Source Map (JSON)
{"version":3,"file":"utils-Bw7FoAI3.cjs","names":["RequestContext","MastraBase","isVercelTool","isProviderDefinedTool","z","isZodObject","safeExtendZodObject","backgroundOverrideZodSchema","backgroundOverrideJsonSchema","executeWithContext","wrapMastra","noopObserve","ToolStream","createObservabilityContext","validateToolInput","validateToolSuspendData","validateToolOutput","getOrCreateSpan","EntityType","MastraFGAPermissions","MastraError","ErrorDomain","ErrorCategory","safeStringify","OpenAIReasoningSchemaCompatLayer","OpenAISchemaCompatLayer","GoogleSchemaCompatLayer","AnthropicSchemaCompatLayer","DeepSeekSchemaCompatLayer","MetaSchemaCompatLayer","getNeedsApprovalFn","z","Tool","isVercelTool","MastraError","ErrorDomain","ErrorCategory"],"sources":["../src/tools/tool-builder/builder.ts","../src/utils.ts"],"sourcesContent":["import type { Schema } from '@internal/ai-v6';\nimport type { ProviderDefinedTool, ToolExecutionOptions } from '@internal/external-types';\nimport {\n OpenAIReasoningSchemaCompatLayer,\n OpenAISchemaCompatLayer,\n GoogleSchemaCompatLayer,\n AnthropicSchemaCompatLayer,\n DeepSeekSchemaCompatLayer,\n MetaSchemaCompatLayer,\n applyCompatLayer,\n convertZodSchemaToAISDKSchema,\n jsonSchema,\n} from '@mastra/schema-compat';\nimport type { JSONSchema7Definition } from 'json-schema';\nimport { z } from 'zod/v4';\nimport { MastraFGAPermissions } from '../../auth/ee';\nimport { backgroundOverrideJsonSchema, backgroundOverrideZodSchema } from '../../background-tasks';\nimport { MastraBase } from '../../base';\nimport { ErrorCategory, MastraError, ErrorDomain } from '../../error';\nimport type { Mastra } from '../../mastra';\nimport { SpanType, wrapMastra, EntityType, getOrCreateSpan, createObservabilityContext } from '../../observability';\nimport type { AnySpan } from '../../observability';\nimport { executeWithContext } from '../../observability/utils';\nimport { RequestContext } from '../../request-context';\nimport { isStandardSchemaWithJSON, toStandardSchema, standardSchemaToJSONSchema } from '../../schema';\nimport type { StandardSchemaWithJSON } from '../../schema';\nimport { getNeedsApprovalFn, isVercelTool, isProviderDefinedTool } from '../../tools/toolchecks';\nimport type { ToolOptions } from '../../utils';\nimport { safeStringify } from '../../utils';\nimport { isZodObject, safeExtendZodObject } from '../../utils/zod-utils';\n\nimport type { SuspendOptions } from '../../workflows';\nimport { ToolStream } from '../stream';\nimport type {\n CoreTool,\n McpMetadata,\n MastraToolInvocationOptions,\n NeedsApprovalFn,\n ToolAction,\n VercelTool,\n VercelToolV5,\n} from '../types';\nimport { noopObserve } from '../types';\nimport { validateToolInput, validateToolOutput, validateToolSuspendData } from '../validation';\n\n/**\n * Merge two RequestContexts so non-serializable values survive the evented\n * workflow engine's toJSON/reconstruct cycle.\n *\n * The evented engine serialises the RequestContext via `toJSON()` when\n * publishing workflow events. Values that fail `JSON.stringify` (functions,\n * objects with circular references — e.g. the `controller` context) are silently\n * dropped. The reconstructed RC handed to steps is therefore *degraded*.\n *\n * Tools, however, also hold a reference to the *original* RC captured during\n * tool conversion (the \"closure\" RC). By merging both — exec first, then\n * closure on top — keys that survived serialisation are preserved while\n * non-serializable keys from the closure (like `controller`) are restored.\n */\n/**\n * Detect RequestContext-like objects structurally. We cannot use `instanceof`\n * here because duplicate copies of @mastra/core may be loaded in the same\n * process (bundlers, monorepos) and the prototype identity is not guaranteed.\n */\nfunction isRequestContextLike(value: unknown): value is RequestContext {\n if (!value || typeof value !== 'object') return false;\n const rc = value as RequestContext;\n return (\n typeof rc.get === 'function' &&\n typeof rc.set === 'function' &&\n typeof rc.entries === 'function' &&\n typeof rc.size === 'function'\n );\n}\n\nfunction mergeRequestContexts(\n closureRC: RequestContext | undefined,\n execRC: RequestContext | undefined,\n): RequestContext {\n if (closureRC && closureRC === execRC) return closureRC;\n if (!closureRC && !execRC) return new RequestContext();\n if (!closureRC) return isRequestContextLike(execRC) ? execRC : new RequestContext();\n if (!execRC || !isRequestContextLike(execRC) || execRC.size() === 0) return closureRC;\n\n const merged = new RequestContext();\n // Start with the evented engine's serialised snapshot\n for (const [key, value] of execRC.entries()) {\n merged.set(key, value);\n }\n // Overlay closure values — restores non-serializable keys and ensures the\n // authoritative (non-degraded) copy wins for keys present in both.\n for (const [key, value] of closureRC.entries()) {\n merged.set(key, value);\n }\n return merged;\n}\n\n/**\n * Types that can be converted to Mastra tools.\n * Includes provider-defined tools from external packages via ProviderDefinedTool.\n */\nexport type ToolToConvert = VercelTool | ToolAction<any, any, any> | VercelToolV5 | ProviderDefinedTool;\nexport type LogType = 'tool' | 'toolset' | 'client-tool';\n\ninterface LogOptions {\n agentName?: string;\n toolName: string;\n type?: 'tool' | 'toolset' | 'client-tool';\n}\n\ninterface LogMessageOptions {\n start: string;\n error: string;\n logData: Record<string, unknown>;\n}\n\n/**\n * Detect Zod v4 schemas. Zod v3 stores the type name as `_def.typeName`\n * (e.g. \"ZodObject\"); Zod v4 stores it as `_def.type` (e.g. \"object\"). We\n * cannot use `instanceof` here because both Zod versions may be loaded in the\n * same process and the prototype identity is not guaranteed.\n */\nfunction isZodV4Schema(schema: unknown): boolean {\n const def = (schema as any)?._def;\n return !!def && typeof def.type === 'string' && !def.typeName;\n}\n\n/**\n * Build a Standard Schema that:\n * - exposes the spliced JSON Schema (with `_background`/`suspendedToolRunId`/\n * `resumeData` properties added) so provider compat layers see the override\n * fields when serializing the tool to an LLM, and\n * - delegates runtime `validate` to the *original* schema so Zod v3\n * `.transform()` / `.default()` / `.refine()` and other Standard Schema\n * parsing behavior still run before `execute()` sees the args.\n *\n * Injected override keys (`_background`, `suspendedToolRunId`, `resumeData`)\n * are stripped from the input before delegating, then merged back into the\n * validated value so the inner `execute()` still receives them — matching the\n * Zod v4 `.extend()` path's behavior.\n *\n * If the original schema has no `~standard.validate` (e.g. a raw JSON Schema\n * with no Standard Schema wrapper), fall back to validating against the\n * spliced JSON Schema directly.\n */\nfunction buildJsonOverrideSchema(\n originalSchema: unknown,\n splicedJsonSchema: JSONSchema7Definition,\n injectedKeys: readonly string[],\n): StandardSchemaWithJSON {\n const fallback = toStandardSchema(splicedJsonSchema as any);\n const original = originalSchema as { '~standard'?: { validate?: (v: unknown) => any } } | undefined;\n const originalValidate = original?.['~standard']?.validate?.bind(original['~standard']);\n\n // Standard Schema for *just* the injected override fields, so we can validate\n // malformed override payloads (e.g. `_background: { enabled: \"yes\" }`) before\n // merging them into the result. Matches the Zod v4 `.extend()` path's\n // behavior, which validates these fields as part of the object.\n // See https://github.com/mastra-ai/mastra/pull/16915#discussion_r3282600679\n const splicedProperties =\n splicedJsonSchema && typeof splicedJsonSchema === 'object' && 'properties' in splicedJsonSchema\n ? ((splicedJsonSchema.properties ?? {}) as Record<string, JSONSchema7Definition>)\n : {};\n const injectedProperties: Record<string, JSONSchema7Definition> = {};\n for (const key of injectedKeys) {\n if (splicedProperties[key] !== undefined) injectedProperties[key] = splicedProperties[key];\n }\n const injectedValidator = toStandardSchema({\n type: 'object',\n properties: injectedProperties,\n additionalProperties: false,\n } as any);\n\n const stripInjected = (input: unknown) => {\n if (!input || typeof input !== 'object' || Array.isArray(input)) return { stripped: input, injected: {} };\n const injected: Record<string, unknown> = {};\n const stripped: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(input as Record<string, unknown>)) {\n if (injectedKeys.includes(k)) injected[k] = v;\n else stripped[k] = v;\n }\n return { stripped, injected };\n };\n\n const validate = (input: unknown) => {\n const { stripped, injected } = stripInjected(input);\n\n const baseResult = originalValidate\n ? (originalValidate(stripped) as\n | { value: unknown }\n | { issues: readonly unknown[] }\n | Promise<{ value: unknown } | { issues: readonly unknown[] }>)\n : fallback['~standard'].validate(stripped);\n\n const injectedResult = injectedValidator['~standard'].validate(injected);\n\n const combine = (\n base: { value: unknown } | { issues: readonly unknown[] },\n inj: { value: unknown } | { issues: readonly unknown[] },\n ) => {\n const baseIssues = 'issues' in base ? (base.issues ?? []) : [];\n const injIssues = 'issues' in inj ? (inj.issues ?? []) : [];\n if (baseIssues.length || injIssues.length) {\n return { issues: [...baseIssues, ...injIssues] };\n }\n const baseValue = (base as { value: unknown }).value;\n const injValue = (inj as { value: unknown }).value;\n if (baseValue && typeof baseValue === 'object' && !Array.isArray(baseValue)) {\n const injMerged =\n injValue && typeof injValue === 'object' && !Array.isArray(injValue)\n ? (injValue as Record<string, unknown>)\n : injected;\n return { value: { ...(baseValue as Record<string, unknown>), ...injMerged } };\n }\n return base;\n };\n\n const baseIsPromise = baseResult && typeof (baseResult as Promise<unknown>).then === 'function';\n const injIsPromise = injectedResult && typeof (injectedResult as Promise<unknown>).then === 'function';\n if (baseIsPromise || injIsPromise) {\n return Promise.all([baseResult, injectedResult]).then(([b, i]) =>\n combine(\n b as { value: unknown } | { issues: readonly unknown[] },\n i as { value: unknown } | { issues: readonly unknown[] },\n ),\n );\n }\n return combine(\n baseResult as { value: unknown } | { issues: readonly unknown[] },\n injectedResult as { value: unknown } | { issues: readonly unknown[] },\n );\n };\n\n return {\n '~standard': {\n version: 1,\n vendor: 'mastra-json-override',\n validate,\n jsonSchema: fallback['~standard'].jsonSchema,\n },\n } as StandardSchemaWithJSON;\n}\n\nexport class CoreToolBuilder extends MastraBase {\n private originalTool: ToolToConvert;\n private options: ToolOptions;\n private logType?: LogType;\n\n constructor(input: {\n originalTool: ToolToConvert;\n options: ToolOptions;\n logType?: LogType;\n autoResumeSuspendedTools?: boolean;\n backgroundTaskEnabled?: boolean;\n }) {\n super({ name: 'CoreToolBuilder' });\n this.originalTool = input.originalTool;\n this.options = input.options;\n this.logType = input.logType;\n\n // Only inject the `_background` override schema for tools that are actually\n // eligible for background execution — otherwise every user tool's input\n // schema would be mutated with a v4 Zod field, which breaks v3-authored\n // tools (keyValidator._parse crashes in schema-compat validation).\n const isBackgroundEligible = !!input.backgroundTaskEnabled;\n const isResumableTool =\n input.autoResumeSuspendedTools ||\n (this.originalTool as unknown as ToolAction<any, any>).id?.startsWith('agent-') ||\n (this.originalTool as unknown as ToolAction<any, any>).id?.startsWith('workflow-');\n\n if (!isVercelTool(this.originalTool) && !isProviderDefinedTool(this.originalTool)) {\n if (isBackgroundEligible || isResumableTool) {\n let schema = this.originalTool.inputSchema;\n if (typeof schema === 'function') {\n schema = schema();\n }\n if (!schema) {\n schema = z.object({});\n }\n\n // Preferred path: when the user's input schema is a Zod v4 ZodObject\n // (the common case for tools authored with `zod` / `zod/v4`), keep using\n // `.extend()`. This preserves the exact JSON Schema shape that existing\n // provider compat layers + LLM recordings expect.\n //\n // Fallback path: for everything else (Zod v3 ZodObject, raw JSON Schema,\n // `JsonSchemaWrapper`, etc.) splice the override fields directly into a\n // JSON Schema. Mixing a Zod v4 wrapper (`backgroundOverrideZodSchema`,\n // `.nullable()`, `.optional()`) into a Zod v3 ZodObject's `.shape` is\n // what triggered the original crash:\n // `TypeError: keyValidator._parse is not a function`.\n if (isZodObject(schema) && isZodV4Schema(schema)) {\n let nextSchema: z.ZodObject<any> = schema as z.ZodObject<any>;\n if (isBackgroundEligible) {\n nextSchema = safeExtendZodObject(nextSchema, {\n _background: backgroundOverrideZodSchema,\n });\n }\n if (isResumableTool) {\n nextSchema = safeExtendZodObject(nextSchema, {\n suspendedToolRunId: z.string().describe('The runId of the suspended tool').nullable().optional(),\n resumeData: z\n .any()\n .describe('The resumeData object created from the resumeSchema of suspended tool')\n .optional(),\n });\n }\n this.originalTool.inputSchema = nextSchema;\n } else {\n // Normalize to Standard Schema, extract JSON Schema, splice overrides.\n const standardSchema = isStandardSchemaWithJSON(schema) ? schema : toStandardSchema(schema);\n const jsonSchema = standardSchemaToJSONSchema(standardSchema, { io: 'input' });\n\n if (jsonSchema && typeof jsonSchema === 'object' && jsonSchema.type === 'object') {\n const properties: Record<string, JSONSchema7Definition> = { ...(jsonSchema.properties ?? {}) };\n const injectedKeys: string[] = [];\n\n if (isBackgroundEligible) {\n properties._background = backgroundOverrideJsonSchema;\n injectedKeys.push('_background');\n }\n if (isResumableTool) {\n // Match the pre-PR JSON Schema shape so existing provider compat\n // layers + LLM recordings collapse it identically.\n properties.suspendedToolRunId = {\n type: ['string', 'null'],\n description: 'The runId of the suspended tool',\n };\n properties.resumeData = {\n description: 'The resumeData object created from the resumeSchema of suspended tool',\n };\n injectedKeys.push('suspendedToolRunId', 'resumeData');\n }\n\n // Preserve the original schema's runtime validator (Zod v3\n // `.transform()` / `.default()` / `.refine()` etc.) while exposing\n // the spliced JSON Schema for provider serialization. See\n // https://github.com/mastra-ai/mastra/pull/16915#discussion_r3282520408\n this.originalTool.inputSchema = buildJsonOverrideSchema(\n schema,\n { ...jsonSchema, properties },\n injectedKeys,\n );\n }\n }\n }\n }\n }\n\n // Helper to get parameters based on tool type\n private getParameters = () => {\n if (isVercelTool(this.originalTool)) {\n // Handle both 'parameters' (v4) and 'inputSchema' (v5) properties\n // Also handle case where the schema is a function that returns a schema\n let schema =\n this.originalTool.parameters ??\n ('inputSchema' in this.originalTool ? (this.originalTool as any).inputSchema : undefined) ??\n z.object({});\n\n // If schema is a function, call it to get the actual schema\n if (typeof schema === 'function') {\n schema = schema();\n }\n\n return schema;\n }\n\n // For Mastra tools, inputSchema might also be a function\n let schema = this.originalTool.inputSchema;\n\n if (isStandardSchemaWithJSON(schema)) {\n return schema;\n }\n\n // If schema is a function, call it to get the actual schema\n if (typeof schema === 'function') {\n schema = schema();\n }\n\n return schema;\n };\n\n private getOutputSchema = () => {\n if ('outputSchema' in this.originalTool) {\n let schema = this.originalTool.outputSchema;\n\n if (isStandardSchemaWithJSON(schema)) {\n return schema;\n }\n\n // If schema is a function, call it to get the actual schema\n if (typeof schema === 'function') {\n schema = schema();\n }\n\n return schema;\n }\n\n return null;\n };\n\n private getResumeSchema = () => {\n if ('resumeSchema' in this.originalTool) {\n let schema = this.originalTool.resumeSchema;\n\n // If schema is a function, call it to get the actual schema\n if (typeof schema === 'function') {\n schema = schema();\n }\n\n return schema;\n }\n return null;\n };\n\n private getSuspendSchema = () => {\n if ('suspendSchema' in this.originalTool) {\n let schema = this.originalTool.suspendSchema;\n\n // If schema is a function, call it to get the actual schema\n if (typeof schema === 'function') {\n schema = schema();\n }\n\n return schema;\n }\n return null;\n };\n\n // For provider-defined tools, we need to include all required properties\n // AI SDK v5 uses type: 'provider-defined', AI SDK v6 uses type: 'provider'\n private buildProviderTool(tool: ToolToConvert): (CoreTool & { id: `${string}.${string}` }) | undefined {\n if (\n 'type' in tool &&\n (tool.type === 'provider-defined' || tool.type === 'provider') &&\n 'id' in tool &&\n typeof tool.id === 'string' &&\n tool.id.includes('.')\n ) {\n // Get schema directly from provider-defined tool (v4 uses parameters, v5 uses inputSchema)\n let parameters: unknown =\n 'parameters' in tool ? tool.parameters : 'inputSchema' in tool ? (tool as any).inputSchema : undefined;\n\n // If schema is a function, call it to get the actual schema\n if (typeof parameters === 'function') {\n parameters = parameters();\n }\n\n // Get output schema directly from provider-defined tool\n let outputSchema: unknown = 'outputSchema' in tool ? (tool as any).outputSchema : undefined;\n\n // If schema is a function, call it to get the actual schema\n if (typeof outputSchema === 'function') {\n outputSchema = outputSchema();\n }\n\n // Convert parameters to AI SDK Schema format\n let processedParameters;\n if (parameters !== undefined && parameters !== null) {\n if (typeof parameters === 'object' && 'jsonSchema' in parameters) {\n // Already in AI SDK Schema format\n processedParameters = parameters;\n } else if (isStandardSchemaWithJSON(parameters)) {\n // StandardSchemaWithJSON - extract the JSON schema and wrap it\n // Use input since parameters represent tool input\n const jsonSchema = standardSchemaToJSONSchema(parameters, { io: 'input' });\n processedParameters = { jsonSchema };\n } else {\n // Assume Zod schema - convert to AI SDK Schema\n processedParameters = convertZodSchemaToAISDKSchema(parameters as any);\n }\n } else {\n // No schema provided - create default empty object schema for AI SDK v1 compatibility\n // OpenAI requires at minimum type: \"object\" even for tools without parameters\n processedParameters = {\n jsonSchema: {\n type: 'object',\n properties: {},\n additionalProperties: false,\n },\n };\n }\n\n // Convert output schema to AI SDK Schema format if present\n let processedOutputSchema;\n if (outputSchema !== undefined && outputSchema !== null) {\n if (typeof outputSchema === 'object' && 'jsonSchema' in outputSchema) {\n // Already in AI SDK Schema format\n processedOutputSchema = outputSchema;\n } else if (isStandardSchemaWithJSON(outputSchema)) {\n // StandardSchemaWithJSON - extract the JSON schema and wrap it\n const jsonSchema = standardSchemaToJSONSchema(outputSchema);\n processedOutputSchema = { jsonSchema };\n } else {\n // Assume Zod schema - convert to AI SDK Schema\n processedOutputSchema = convertZodSchemaToAISDKSchema(outputSchema as any);\n }\n }\n\n return {\n ...(processedOutputSchema ? { outputSchema: processedOutputSchema } : {}),\n type: 'provider-defined' as const,\n id: tool.id as `${string}.${string}`,\n // V5 SDK factories set a hardcoded `name` (e.g. \"web_search\" for\n // anthropic.web_search_20250305). Preserve it so that when this tool\n // is later used with a V6 provider, the bidirectional toolNameMapping\n // resolves the correct model-facing name instead of the versioned ID.\n ...('name' in tool && typeof tool.name === 'string' ? { name: tool.name } : {}),\n args: ('args' in this.originalTool ? this.originalTool.args : {}) as Record<string, unknown>,\n description: tool.description,\n parameters: processedParameters,\n execute: this.originalTool.execute\n ? this.createExecute(\n this.originalTool,\n { ...this.options, description: this.originalTool.description },\n this.logType,\n )\n : undefined,\n toModelOutput: 'toModelOutput' in this.originalTool ? this.originalTool.toModelOutput : undefined,\n transform: 'transform' in this.originalTool ? this.originalTool.transform : undefined,\n inputExamples: 'inputExamples' in this.originalTool ? this.originalTool.inputExamples : undefined,\n } as unknown as (CoreTool & { id: `${string}.${string}` }) | undefined;\n }\n\n return undefined;\n }\n\n private createLogMessageOptions({ agentName, toolName, type }: LogOptions): LogMessageOptions {\n const toolType = type === 'toolset' ? 'toolset' : 'tool';\n return {\n start: `Executing ${toolType}`,\n error: `Failed ${toolType} execution`,\n logData: { agent: agentName, tool: toolName },\n };\n }\n\n private createExecute(tool: ToolToConvert, options: ToolOptions, logType?: 'tool' | 'toolset' | 'client-tool') {\n // don't add memory, mastra, or tracing context to logging (tracingContext may contain sensitive observability credentials)\n const {\n logger,\n mastra: _mastra,\n memory: _memory,\n requestContext,\n model,\n tracingContext: _tracingContext,\n tracingPolicy: _tracingPolicy,\n ...rest\n } = options;\n const logModelObject = {\n modelId: model?.modelId,\n provider: model?.provider,\n specificationVersion: model?.specificationVersion,\n };\n\n const { start, logData } = this.createLogMessageOptions({\n agentName: options.agentName,\n toolName: options.name,\n type: logType,\n });\n\n // Extract MCP metadata once with proper typing to avoid repeated unsafe casts\n const mcpMeta =\n !isVercelTool(tool) && 'mcpMetadata' in tool ? (tool as { mcpMetadata?: McpMetadata }).mcpMetadata : undefined;\n\n const execFunction = async (args: unknown, execOptions: MastraToolInvocationOptions, toolSpan?: AnySpan) => {\n try {\n let result;\n let suspendData = null;\n\n if (isVercelTool(tool)) {\n // Handle Vercel tools (AI SDK tools)\n result = await executeWithContext({\n span: toolSpan,\n fn: async () => tool?.execute?.(args, execOptions as ToolExecutionOptions),\n });\n } else {\n // Handle Mastra tools - wrap mastra instance with tracing context for context propagation\n\n /**\n * MASTRA INSTANCE TYPES IN TOOL EXECUTION:\n *\n * Full Mastra & MastraPrimitives (has getAgent, getWorkflow, etc.):\n * - Auto-generated workflow tools from agent.listWorkflows()\n * - These get this.#mastra directly and can be wrapped\n *\n * MastraPrimitives only (limited interface):\n * - Memory tools (from memory.listTools())\n * - Assigned tools (agent.tools)\n * - Toolset tools (from toolsets)\n * - Client tools (passed as tools in generate/stream options)\n * - These get mastraProxy and have limited functionality\n *\n * TODO: Consider providing full Mastra instance to more tool types for enhanced functionality\n */\n // Wrap mastra with tracing context - wrapMastra will handle whether it's a full instance or primitives\n const wrappedMastra = options.mastra ? wrapMastra(options.mastra, { currentSpan: toolSpan }) : options.mastra;\n\n const resumeSchema = this.getResumeSchema();\n // Pass raw args as first parameter, context as second\n // Properly structure context based on execution source\n const baseContext = {\n threadId: options.threadId,\n resourceId: options.resourceId,\n mastra: wrappedMastra,\n memory: options.memory,\n runId: options.runId,\n requestContext: mergeRequestContexts(options.requestContext, execOptions.requestContext),\n actor: execOptions.actor,\n // Workspace for file operations and command execution\n // Execution-time workspace (from prepareStep/processInputStep) takes precedence over build-time workspace\n workspace: execOptions.workspace ?? options.workspace,\n // Browser for web automation (lazily initialized on first use)\n browser: options.browser,\n observe: execOptions.observe ?? noopObserve,\n writer: new ToolStream(\n {\n prefix: 'tool',\n callId: execOptions.toolCallId,\n name: options.name,\n runId: options.runId!,\n },\n options.outputWriter || execOptions.outputWriter,\n ),\n ...createObservabilityContext({ currentSpan: toolSpan }),\n abortSignal: execOptions.abortSignal,\n suspend: (args: any, suspendOptions?: SuspendOptions) => {\n suspendData = args;\n const newSuspendOptions = {\n ...(suspendOptions ?? {}),\n resumeSchema:\n suspendOptions?.resumeSchema ??\n (resumeSchema\n ? JSON.stringify(standardSchemaToJSONSchema(toStandardSchema(resumeSchema), { io: 'input' }))\n : undefined),\n };\n return execOptions.suspend?.(args, newSuspendOptions);\n },\n resumeData: execOptions.resumeData,\n };\n\n // Check if this is agent execution\n // Agent execution takes precedence over workflow execution because agents may\n // use workflows internally for their agentic loop\n // Note: AI SDK v4 doesn't pass toolCallId/messages, so we also check for agentName and threadId\n const isAgentExecution =\n (execOptions.toolCallId && execOptions.messages) ||\n (options.agentName && options.threadId && !options.workflowId);\n\n // Check if this is workflow execution (has workflow properties in options)\n // Only consider it workflow execution if it's NOT agent execution\n const isWorkflowExecution = !isAgentExecution && (options.workflow || options.workflowId);\n\n let toolContext;\n if (isAgentExecution) {\n // Nest agent-specific properties under 'agent' key\n // Do NOT include workflow context even if workflow properties exist\n // (agents use workflows internally but tools should see agent context)\n const { suspend, resumeData, threadId, resourceId, ...restBaseContext } = baseContext;\n toolContext = {\n ...restBaseContext,\n agent: {\n agentId: options.agentId || '',\n toolCallId: execOptions.toolCallId || '',\n messages: execOptions.messages || [],\n suspend,\n resumeData,\n threadId,\n resourceId,\n outputWriter: options.outputWriter || execOptions.outputWriter,\n flushMessages: execOptions.flushMessages,\n },\n };\n } else if (isWorkflowExecution) {\n // Nest workflow-specific properties under 'workflow' key\n const { suspend, resumeData, ...restBaseContext } = baseContext;\n toolContext = {\n ...restBaseContext,\n workflow: options.workflow || {\n runId: options.runId,\n workflowId: options.workflowId,\n state: options.state,\n setState: options.setState,\n suspend,\n resumeData,\n },\n };\n } else if (execOptions.mcp) {\n // MCP execution context\n toolContext = {\n ...baseContext,\n mcp: execOptions.mcp,\n };\n } else {\n // Direct execution or unknown context\n toolContext = baseContext;\n }\n\n const resumeData = execOptions.resumeData;\n\n if (resumeData) {\n const resumeValidation = validateToolInput(resumeSchema, resumeData, options.name);\n if (resumeValidation.error) {\n logger?.warn(resumeValidation.error.message);\n toolSpan?.end({ output: resumeValidation.error, attributes: { success: false } });\n return resumeValidation.error as any;\n }\n }\n\n result = await executeWithContext({ span: toolSpan, fn: async () => tool?.execute?.(args, toolContext) });\n }\n\n if (suspendData) {\n const suspendSchema = this.getSuspendSchema();\n const suspendValidation = validateToolSuspendData(suspendSchema, suspendData, options.name);\n if (suspendValidation.error) {\n logger?.warn(suspendValidation.error.message);\n toolSpan?.end({ output: suspendValidation.error, attributes: { success: false } });\n return suspendValidation.error as any;\n }\n }\n\n // Skip validation if suspend was called without a result\n const shouldSkipValidation = typeof result === 'undefined' && !!suspendData;\n if (shouldSkipValidation) {\n toolSpan?.end({ output: result, attributes: { success: true } });\n return result;\n }\n\n // Validate output for Vercel/AI SDK tools which don't have built-in validation\n // Mastra tools handle their own validation in Tool.execute() which properly\n // applies Zod transforms (e.g., .transform(), .pipe()) to the output\n if (isVercelTool(tool)) {\n const outputSchema = this.getOutputSchema();\n const outputValidation = validateToolOutput(outputSchema, result, options.name, false);\n if (outputValidation.error) {\n logger?.warn(outputValidation.error.message);\n toolSpan?.end({ output: outputValidation.error, attributes: { success: false } });\n return outputValidation.error;\n }\n result = outputValidation.data;\n }\n\n // Return result (validated for Vercel tools, already validated for Mastra tools)\n toolSpan?.end({ output: result, attributes: { success: true } });\n return result;\n } catch (error) {\n toolSpan?.error({ error: error as Error, attributes: { success: false } });\n throw error;\n }\n };\n\n return async (args: unknown, execOptions?: MastraToolInvocationOptions) => {\n let logger = options.logger || this.logger;\n\n // Create tool span early so validation failures are always observable.\n // Prefer execution-time tracingContext (passed at runtime for VNext methods)\n // Fall back to build-time context for Legacy methods (AI SDK v4 doesn't support passing custom options)\n const tracingContext = execOptions?.tracingContext || options.tracingContext;\n const toolRequestContext = execOptions?.requestContext ?? options.requestContext;\n const toolSpan = getOrCreateSpan({\n type: mcpMeta ? SpanType.MCP_TOOL_CALL : SpanType.TOOL_CALL,\n name: mcpMeta ? `mcp_tool: '${options.name}' on '${mcpMeta.serverName}'` : `tool: '${options.name}'`,\n input: args,\n entityType: EntityType.TOOL,\n entityId: options.name,\n entityName: options.name,\n attributes: mcpMeta\n ? {\n mcpServer: mcpMeta.serverName,\n serverVersion: mcpMeta.serverVersion,\n toolDescription: options.description,\n toolCallId: execOptions?.toolCallId,\n }\n : {\n toolDescription: options.description,\n toolType: logType || 'tool',\n toolCallId: execOptions?.toolCallId,\n },\n tracingPolicy: options.tracingPolicy,\n tracingContext: tracingContext,\n requestContext: toolRequestContext,\n mastra: options.mastra && 'observability' in options.mastra ? (options.mastra as Mastra) : undefined,\n });\n\n const fgaProvider = (options.mastra as any)?.getServer?.()?.fga;\n const user = toolRequestContext?.get('user');\n if (fgaProvider) {\n const { getAgentToolFGAResourceId, getMCPToolFGAResourceId, getStandaloneToolFGAResourceId, requireFGA } =\n await import('../../auth/ee/fga-check');\n const toolResourceId = mcpMeta?.serverName\n ? getMCPToolFGAResourceId(mcpMeta.serverName, options.name)\n : options.agentId\n ? getAgentToolFGAResourceId(options.agentId, options.name)\n : getStandaloneToolFGAResourceId(options.name);\n await requireFGA({\n fgaProvider,\n user,\n resource: { type: 'tool', id: toolResourceId },\n permission: MastraFGAPermissions.TOOLS_EXECUTE,\n requestContext: toolRequestContext,\n actor: execOptions?.actor,\n context: {\n resourceId: options.resourceId,\n },\n metadata: {\n toolName: options.name,\n agentId: options.agentId,\n agentName: options.agentName,\n runId: options.runId,\n threadId: options.threadId,\n executionResourceId: options.resourceId,\n mcpMetadata: mcpMeta,\n },\n });\n }\n\n try {\n logger.debug(start, { ...logData, ...rest, model: logModelObject, args });\n\n // When a tool is being resumed (resumeData present in execOptions), skip input\n // validation. The original args were already validated during the initial\n // execution, and during resume the tool's execute function checks resumeData\n // and returns early without using the input args.\n const isResuming = !!execOptions?.resumeData;\n\n // Validate input parameters if schema exists\n // Use the processed schema for validation if available, otherwise fall back to original\n const parameters = this.getParameters();\n if (!isResuming) {\n const { data, error } = validateToolInput(parameters, args, options.name);\n //suspendedToolRunId is only required when resumeData is provided\n const suspendedToolRunIdErrToIgnore =\n error?.message?.includes('suspendedToolRunId: Required') && !(args as Record<string, unknown>)?.resumeData;\n if (error && !suspendedToolRunIdErrToIgnore) {\n logger.warn('Tool input validation failed', { ...logData, validationError: error.message });\n toolSpan?.end({ output: error, attributes: { success: false } });\n return error;\n }\n // Use validated/transformed data\n args = data;\n }\n\n // there is a small delay in stream output so we add an immediate to ensure the stream is ready\n return await new Promise((resolve, reject) => {\n setImmediate(async () => {\n try {\n const result = await execFunction(args, execOptions!, toolSpan);\n resolve(result);\n } catch (err) {\n reject(err);\n }\n });\n });\n } catch (err) {\n const mastraError = new MastraError(\n {\n id: 'TOOL_EXECUTION_FAILED',\n domain: ErrorDomain.TOOL,\n category: ErrorCategory.USER,\n details: {\n errorMessage: String(err),\n argsJson: safeStringify(args),\n model: model?.modelId ?? '',\n },\n },\n err,\n );\n toolSpan?.error({ error: mastraError, attributes: { success: false } });\n logger.trackException(mastraError, { ...logData, ...rest, model: logModelObject, args });\n throw mastraError;\n }\n };\n }\n\n buildV5() {\n const builtTool = this.build();\n\n if (!builtTool.parameters) {\n throw new Error('Tool parameters are required');\n }\n\n const base = {\n ...builtTool,\n inputSchema: builtTool.parameters,\n onInputStart: 'onInputStart' in this.originalTool ? this.originalTool.onInputStart : undefined,\n onInputDelta: 'onInputDelta' in this.originalTool ? this.originalTool.onInputDelta : undefined,\n onInputAvailable: 'onInputAvailable' in this.originalTool ? this.originalTool.onInputAvailable : undefined,\n onOutput: 'onOutput' in this.originalTool ? this.originalTool.onOutput : undefined,\n };\n\n // For provider-defined tools, exclude execute and add name as per v5 spec\n if (builtTool.type === 'provider-defined') {\n const { execute, parameters, ...rest } = base;\n // Prefer the preserved provider name (e.g. \"web_search\" from V5 SDK\n // factories) over the ID-derived name (e.g. \"web_search_20250305\").\n const name =\n ('name' in builtTool && typeof builtTool.name === 'string' ? builtTool.name : null) ||\n builtTool.id.split('.')[1] ||\n builtTool.id;\n return {\n ...rest,\n type: builtTool.type,\n id: builtTool.id,\n name,\n args: builtTool.args,\n } as VercelToolV5;\n }\n\n return base as VercelToolV5;\n }\n\n build(): CoreTool {\n const providerTool = this.buildProviderTool(this.originalTool);\n if (providerTool) {\n return providerTool;\n }\n const model = this.options.model;\n\n const schemaCompatLayers = [];\n\n if (model) {\n // Respect the model's own capability flag; do not disable it based solely on specificationVersion.\n const supportsStructuredOutputs =\n 'supportsStructuredOutputs' in model ? (model.supportsStructuredOutputs ?? false) : false;\n\n const modelInfo = {\n modelId: model.modelId,\n supportsStructuredOutputs,\n provider: model.provider,\n };\n\n schemaCompatLayers.push(\n new OpenAIReasoningSchemaCompatLayer(modelInfo),\n new OpenAISchemaCompatLayer(modelInfo),\n new GoogleSchemaCompatLayer(modelInfo),\n new AnthropicSchemaCompatLayer(modelInfo),\n new DeepSeekSchemaCompatLayer(modelInfo),\n new MetaSchemaCompatLayer(modelInfo),\n );\n }\n\n const originalSchema = this.getParameters();\n let processedInputSchema: Schema | undefined;\n\n if (originalSchema) {\n if (isStandardSchemaWithJSON(originalSchema)) {\n // Find the first applicable compatibility layer\n const applicableLayer = schemaCompatLayers.find(layer => layer.shouldApply());\n\n let schemaToUse: StandardSchemaWithJSON;\n if (applicableLayer) {\n schemaToUse = applicableLayer.processToCompatSchema(originalSchema as any);\n } else {\n schemaToUse = toStandardSchema(originalSchema);\n }\n\n processedInputSchema = jsonSchema(\n standardSchemaToJSONSchema(schemaToUse, {\n io: 'input',\n }),\n {\n validate: (value: unknown) => {\n const result = schemaToUse['~standard'].validate(value);\n // standard-schema validate may return a Promise\n if (result instanceof Promise) {\n return result.then(r => {\n if ('issues' in r && r.issues) {\n return {\n success: false as const,\n error: new Error(r.issues.map((i: any) => i.message).join(', ')),\n };\n }\n return { success: true as const, value: (r as { value: unknown }).value };\n });\n }\n // standard-schema returns { value } on success or { issues } on failure,\n // but AI SDK expects { success: boolean, value/error }\n if ('issues' in result && result.issues) {\n return {\n success: false as const,\n error: new Error(result.issues.map((i: any) => i.message).join(', ')),\n };\n }\n return { success: true as const, value: (result as { value: unknown }).value };\n },\n },\n );\n } else {\n processedInputSchema = applyCompatLayer({\n schema: originalSchema,\n compatLayers: schemaCompatLayers,\n mode: 'aiSdkSchema',\n });\n }\n }\n\n const outputSchema = this.getOutputSchema();\n let processedOutputSchema;\n\n if (outputSchema) {\n if (isStandardSchemaWithJSON(outputSchema)) {\n processedOutputSchema = standardSchemaToJSONSchema(outputSchema, { io: 'output' });\n } else {\n processedOutputSchema = applyCompatLayer({\n schema: outputSchema,\n compatLayers: [],\n mode: 'aiSdkSchema',\n });\n }\n }\n\n // Map AI SDK's needsApproval to our requireApproval\n // needsApproval can be boolean or a function that takes input and returns boolean\n let requireApproval = false;\n let needsApprovalFn: NeedsApprovalFn | undefined;\n\n if (typeof this.options.requireApproval === 'function') {\n requireApproval = true;\n needsApprovalFn = this.options.requireApproval;\n } else if (typeof this.options.requireApproval === 'boolean') {\n requireApproval = this.options.requireApproval;\n needsApprovalFn = undefined;\n }\n\n if (isVercelTool(this.originalTool) && 'needsApproval' in this.originalTool) {\n const needsApproval = (this.originalTool as any).needsApproval;\n if (typeof needsApproval === 'boolean') {\n requireApproval = needsApproval;\n needsApprovalFn = undefined;\n } else if (typeof needsApproval === 'function') {\n // Store the function to evaluate it per-call\n needsApprovalFn = needsApproval;\n // Set requireApproval to true so the tool-call-step knows to check the function\n requireApproval = true;\n }\n }\n\n // Preserve a needsApprovalFn that was attached directly to the tool instance\n // (e.g. MCP tools wrap a server-level `requireToolApproval` function and set\n // `needsApprovalFn` on the tool while keeping `requireApproval` as a boolean).\n // The branches above only derive needsApprovalFn from options/AI SDK shapes, so\n // without this it would be dropped during conversion and conditional approval\n // would silently fall back to the boolean flag.\n const instanceNeedsApprovalFn = getNeedsApprovalFn(this.originalTool);\n if (!needsApprovalFn && instanceNeedsApprovalFn) {\n needsApprovalFn = instanceNeedsApprovalFn;\n // Ensure the tool-call-step knows to evaluate the function per call.\n requireApproval = true;\n }\n\n const definition = {\n type: 'function' as const,\n description: this.originalTool.description,\n requireApproval,\n needsApprovalFn,\n hasSuspendSchema: !!this.getSuspendSchema(),\n execute: this.originalTool.execute\n ? this.createExecute(\n this.originalTool,\n { ...this.options, description: this.originalTool.description },\n this.logType,\n )\n : undefined,\n };\n\n return {\n ...definition,\n id: 'id' in this.originalTool ? this.originalTool.id : undefined,\n parameters: processedInputSchema ?? z.object({}),\n outputSchema: processedOutputSchema,\n strict: 'strict' in this.originalTool ? this.originalTool.strict : undefined,\n providerOptions: 'providerOptions' in this.originalTool ? this.originalTool.providerOptions : undefined,\n mcp: 'mcp' in this.originalTool ? this.originalTool.mcp : undefined,\n toModelOutput: 'toModelOutput' in this.originalTool ? this.originalTool.toModelOutput : undefined,\n transform: 'transform' in this.originalTool ? this.originalTool.transform : undefined,\n inputExamples: 'inputExamples' in this.originalTool ? this.originalTool.inputExamples : undefined,\n onInputStart: 'onInputStart' in this.originalTool ? this.originalTool.onInputStart : undefined,\n onInputDelta: 'onInputDelta' in this.originalTool ? this.originalTool.onInputDelta : undefined,\n onInputAvailable: 'onInputAvailable' in this.originalTool ? this.originalTool.onInputAvailable : undefined,\n onOutput: 'onOutput' in this.originalTool ? this.originalTool.onOutput : undefined,\n // Preserve tool-level background config so the agentic loop can pick it up\n // from the converted CoreTool at dispatch time.\n backgroundConfig: this.options.backgroundConfig,\n } as unknown as CoreTool;\n }\n}\n","import { createHash } from 'node:crypto';\nimport type { CoreMessage } from '@internal/ai-sdk-v4';\nimport { jsonSchemaToZod } from '@mastra/schema-compat/json-to-zod';\nimport { z } from 'zod/v4';\nimport type { MastraPrimitives } from './action';\nimport type { ToolsInput } from './agent';\nimport type { ToolBackgroundConfig } from './background-tasks';\nimport type { MastraBrowser } from './browser/browser';\nimport { ErrorCategory, ErrorDomain, MastraError } from './error';\nimport type { MastraLanguageModel, MastraLegacyLanguageModel } from './llm/model/shared.types';\nimport type { IMastraLogger } from './logger';\nimport type { Mastra } from './mastra';\nimport type { AiMessageType, MastraMemory } from './memory';\nimport type { ObservabilityContext, TracingPolicy } from './observability';\nimport type { RequestContext } from './request-context';\nimport type { CoreTool, VercelTool, VercelToolV5 } from './tools';\nimport { Tool } from './tools/tool';\nimport { CoreToolBuilder } from './tools/tool-builder/builder';\nimport type { ToolToConvert } from './tools/tool-builder/builder';\nimport { isVercelTool } from './tools/toolchecks';\nimport type { OutputWriter } from './workflows/types';\nimport type { Workspace } from './workspace/workspace';\n\n// Re-export Zod utilities for external use (isZodType is defined locally below)\nexport { getZodTypeName, getZodDef, isZodArray, isZodObject } from './utils/zod-utils';\nexport { fetchWithRetry } from './utils/fetchWithRetry';\nexport type { FetchWithRetryOptions } from './utils/fetchWithRetry';\n\nexport { boundedStringify, ensureSerializable, isBoundedSerializable, safeStringify } from './utils/safe-stringify';\nexport { deepEqual } from './utils/deep-equal';\n\nexport const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));\n\n/**\n * Read a positive-integer environment variable (e.g. a TTL in ms). Unset, empty,\n * non-numeric, fractional, or non-positive values fall back to `fallback`.\n */\nexport function readPositiveIntEnv(name: string, fallback: number): number {\n const raw = process.env[name];\n if (!raw) return fallback;\n const parsed = Number(raw);\n return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;\n}\n\n/**\n * Checks if a value is a plain object (not an array, function, Date, RegExp, etc.)\n */\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n if (value === null || typeof value !== 'object') return false;\n const proto = Object.getPrototypeOf(value);\n return proto === Object.prototype || proto === null;\n}\n\n/**\n * Deep merges two objects, recursively merging nested plain objects.\n * Arrays, functions, and other non-plain objects are replaced (not merged).\n */\nexport fun