@mastra/core
Version:
1 lines • 1.05 MB
Source Map (JSON)
{"version":3,"file":"message-list-BM7m-E-v.cjs","names":["convertDataContentToBase64String","partAny","getDisplayTransform","getTransformedToolPayload","hasTransformedToolPayload","filterEmptyTextParts","getSignalType","getSignalTagName","isUserSignalType","toSignalDataPart","MastraError","ErrorDomain","ErrorCategory","convertDataContentToBase64String","AISDKError","getErrorMessage","gateway","imageMediaTypeSignatures","convertBase64ToUint8Array","stripID3","detectMediaType","fetchWithValidatedRedirects","withUserAgentSuffix","getRuntimeEnvironmentUserAgent","cancelResponseBody","DownloadError","readResponseWithSizeLimit","DEFAULT_MAX_DOWNLOAD_SIZE","z","convertUint8ArrayToBase64","isUrlSupported","asSchema","InvalidPromptError","safeValidateTypes","GatewayAuthenticationError","APICallError","retryWithExponentialBackoff","GatewayError","executeTool","safeParseJSON","resolve","TypeValidationError","createIdGenerator","getErrorMessage$1","lazySchema","zodSchema","validateTypes","DelayedPromise","isAbortError","UnsupportedFunctionalityError","getDisplayTransform","getTransformedToolPayload","hasTransformedToolPayload","MastraError","ErrorDomain","ErrorCategory","getTransformedToolPayload","hasTransformedToolPayload","AIV6.isToolUIPart","convertDataContentToBase64String","convertToCoreMessagesV4","deepEqual","DefaultGeneratedFileWithType","convertDataContentToBase64String","convertToDataContent","fetchWithRetry","MastraError","ErrorDomain","ErrorCategory","createSignal","isCreatedAgentSignal","mastraDBMessageToSignal","convertInputToMastraDBMessage","convertAIV5UIToModelMessages","convertAIV4CoreToAIV5ModelMessages","getTransformedToolPayload","hasTransformedToolPayload","MastraError","ErrorDomain","ErrorCategory"],"sources":["../src/agent/message-list/detection/TypeDetector.ts","../src/agent/message-list/prompt/image-utils.ts","../src/agent/message-list/utils/response-item-metadata.ts","../src/agent/message-list/utils/provider-compat.ts","../src/agent/message-list/adapters/AIV4Adapter.ts","../../_vendored/ai_v6/dist/index.js","../src/agent/message-list/utils/tool-name.ts","../src/agent/message-list/adapters/AIV5Adapter.ts","../src/agent/message-list/adapters/AIV6Adapter.ts","../src/agent/message-list/cache/stable-stringify.ts","../src/agent/message-list/cache/CacheKeyGenerator.ts","../src/agent/message-list/conversion/to-prompt.ts","../src/agent/message-list/conversion/utils.ts","../src/agent/message-list/utils/stamp-part.ts","../src/agent/message-list/conversion/input-converter.ts","../src/agent/message-list/conversion/output-converter.ts","../src/agent/message-list/conversion/step-content.ts","../src/agent/message-list/merge/MessageMerger.ts","../src/stream/aisdk/v5/compat/media.ts","../src/agent/message-list/prompt/convert-file.ts","../src/agent/message-list/prompt/attachments-to-parts.ts","../src/agent/message-list/prompt/convert-to-mastra-v1.ts","../src/agent/message-list/prompt/download-assets.ts","../src/agent/message-list/state/serialization.ts","../src/agent/message-list/state/MessageStateManager.ts","../src/agent/message-list/message-list.ts","../src/agent/message-list/utils/convert-messages.ts"],"sourcesContent":["import type { Message as AIV4Message, UIMessage as UIMessageV4 } from '@internal/ai-sdk-v4';\n\nimport type { MastraDBMessage, MastraMessageV1 } from '../state/types';\nimport type { AIV5Type, AIV6Type, AIV7Type, CoreMessageV4 } from '../types';\n\n/**\n * Type representing all possible message input formats\n */\nexport type MessageInput =\n | AIV7Type.UIMessage\n | AIV7Type.ModelMessage\n | AIV6Type.UIMessage\n | AIV6Type.ModelMessage\n | AIV5Type.UIMessage\n | AIV5Type.ModelMessage\n | (UIMessageV4 & { metadata?: Record<string, unknown> })\n | AIV4Message\n | CoreMessageV4\n | MastraMessageV1\n | MastraDBMessage;\n\n/**\n * TypeDetector - Centralized type detection for different message formats\n *\n * This class provides consistent type detection across all message formats,\n * which is critical for:\n * - Determining which conversion path to use\n * - Validating incoming message formats\n * - Providing better TypeScript type narrowing\n *\n * The detection order is important because some formats share similar properties.\n */\nexport class TypeDetector {\n /**\n * Check if a message is a MastraDBMessage (format 2)\n */\n static isMastraDBMessage(msg: MessageInput): msg is MastraDBMessage {\n return Boolean(\n 'content' in msg &&\n msg.content &&\n !Array.isArray(msg.content) &&\n typeof msg.content !== 'string' &&\n 'format' in msg.content &&\n msg.content.format === 2,\n );\n }\n\n /**\n * Check if a message is a MastraMessageV1 (legacy format)\n */\n static isMastraMessageV1(msg: MessageInput): msg is MastraMessageV1 {\n return !TypeDetector.isMastraDBMessage(msg) && ('threadId' in msg || 'resourceId' in msg);\n }\n\n /**\n * Check if a message is either Mastra format (V1 or V2/DB)\n */\n static isMastraMessage(msg: MessageInput): msg is MastraDBMessage | MastraMessageV1 {\n return TypeDetector.isMastraDBMessage(msg) || TypeDetector.isMastraMessageV1(msg);\n }\n\n /**\n * Check if a message is an AIV4 UIMessage\n */\n static isAIV4UIMessage(msg: MessageInput): msg is UIMessageV4 {\n return (\n !TypeDetector.isMastraMessage(msg) &&\n !TypeDetector.isAIV4CoreMessage(msg) &&\n 'parts' in msg &&\n !TypeDetector.hasAIV5UIMessageCharacteristics(msg)\n );\n }\n\n /**\n * Check if a message is an AIV6 UIMessage.\n *\n * At runtime, the v5 and v6 UI shapes overlap heavily. We only treat a\n * message as distinctly v6 if it uses v6-only parts or tool states.\n */\n static isAIV6UIMessage(msg: MessageInput): msg is AIV6Type.UIMessage {\n return (\n !TypeDetector.isMastraMessage(msg) &&\n !TypeDetector.isAIV4CoreMessage(msg) &&\n 'parts' in msg &&\n TypeDetector.hasAIV6UIMessageCharacteristics(\n msg as AIV7Type.UIMessage | AIV6Type.UIMessage | AIV5Type.UIMessage | UIMessageV4 | AIV4Message,\n )\n );\n }\n\n /**\n * Check if a message is an AIV5 UIMessage\n */\n static isAIV5UIMessage(msg: MessageInput): msg is AIV5Type.UIMessage {\n return (\n !TypeDetector.isMastraMessage(msg) &&\n !TypeDetector.isAIV6UIMessage(msg) &&\n !TypeDetector.isAIV5CoreMessage(msg) &&\n 'parts' in msg &&\n TypeDetector.hasAIV5UIMessageCharacteristics(msg)\n );\n }\n\n /**\n * Check if a message is an AIV4 CoreMessage\n */\n static isAIV4CoreMessage(msg: MessageInput): msg is CoreMessageV4 {\n // V4 CoreMessage has role and content like V5/V6, but content can be an\n // array of parts with v4-specific field names.\n return (\n !TypeDetector.isMastraMessage(msg) &&\n !('parts' in msg) &&\n 'content' in msg &&\n !TypeDetector.hasAIV5CoreMessageCharacteristics(msg)\n );\n }\n\n /**\n * Check if a message is an AIV6 ModelMessage (CoreMessage equivalent).\n */\n static isAIV6CoreMessage(msg: MessageInput): msg is AIV6Type.ModelMessage {\n return (\n !TypeDetector.isMastraMessage(msg) &&\n !('parts' in msg) &&\n 'content' in msg &&\n TypeDetector.hasAIV6CoreMessageCharacteristics(\n msg as CoreMessageV4 | AIV5Type.ModelMessage | AIV6Type.ModelMessage | AIV7Type.ModelMessage | AIV4Message,\n )\n );\n }\n\n /**\n * Check if a message is an AIV5 ModelMessage (CoreMessage equivalent)\n */\n static isAIV5CoreMessage(msg: MessageInput): msg is AIV5Type.ModelMessage {\n return (\n !TypeDetector.isMastraMessage(msg) &&\n !TypeDetector.isAIV6CoreMessage(msg) &&\n !('parts' in msg) &&\n 'content' in msg &&\n TypeDetector.hasAIV5CoreMessageCharacteristics(msg)\n );\n }\n\n /**\n * Check if a message has AIV6-only UI characteristics.\n */\n static hasAIV6UIMessageCharacteristics(\n msg: AIV7Type.UIMessage | AIV6Type.UIMessage | AIV5Type.UIMessage | UIMessageV4 | AIV4Message,\n ): msg is AIV6Type.UIMessage {\n if (!('parts' in msg) || !msg.parts) return false;\n\n for (const part of msg.parts) {\n if (part.type === 'source-document') return true;\n if (part.type === 'dynamic-tool') return true;\n\n if (\n 'toolCallId' in part &&\n 'state' in part &&\n (part.state === 'approval-requested' || part.state === 'approval-responded' || part.state === 'output-denied')\n ) {\n return true;\n }\n }\n\n return false;\n }\n\n /**\n * Check if a message has AIV5 UIMessage characteristics\n *\n * V5 UIMessages have specific part types and field names that differ from V4.\n */\n static hasAIV5UIMessageCharacteristics(\n msg: AIV7Type.UIMessage | AIV6Type.UIMessage | AIV5Type.UIMessage | UIMessageV4 | AIV4Message,\n ): msg is AIV5Type.UIMessage {\n // AI SDK v4 has separate arrays of tool invocations, reasoning, and\n // attachments that do not preserve overall part ordering, so their\n // presence is a quick early signal that this is not a v5/v6 UI message.\n if (\n 'toolInvocations' in msg ||\n 'reasoning' in msg ||\n 'experimental_attachments' in msg ||\n 'data' in msg ||\n 'annotations' in msg\n // Don't check `content` here. That would fully narrow to v5 and is more\n // likely to misclassify a loosely constructed v5/v6 UI message.\n )\n return false;\n\n if (!msg.parts) return false; // likely an AIV4 Message\n\n for (const part of msg.parts) {\n if ('metadata' in part) return true;\n\n // Tool parts are the cleanest discriminator:\n // - v4 uses `tool-invocation`\n // - v5/v6 use `tool-${toolName}` / `dynamic-tool`\n if ('toolInvocation' in part) return false;\n if ('toolCallId' in part) return true;\n if (part.type === 'source') return false;\n if (part.type === 'source-url') return true;\n\n if (part.type === 'reasoning') {\n if ('state' in part || 'text' in part) return true; // v5/v6\n if ('reasoning' in part || 'details' in part) return false; // v4\n }\n\n if (part.type === 'file' && 'mediaType' in part) return true;\n }\n\n return false; // default to v4 for backwards compatibility\n }\n\n /**\n * Check if a message has AIV6-only core characteristics.\n */\n static hasAIV6CoreMessageCharacteristics(\n msg: CoreMessageV4 | AIV5Type.ModelMessage | AIV6Type.ModelMessage | AIV7Type.ModelMessage | AIV4Message,\n ): msg is AIV6Type.ModelMessage {\n if ('parts' in msg || typeof msg.content === 'string') return false;\n\n return msg.content.some(part => part.type === 'tool-approval-request' || part.type === 'tool-approval-response');\n }\n\n /**\n * Check if a message has AIV5 CoreMessage characteristics\n *\n * V5 ModelMessages use different field names from v4\n * (for example `output` vs `result`, `input` vs `args`,\n * `mediaType` vs `mimeType`).\n */\n static hasAIV5CoreMessageCharacteristics(\n msg:\n | CoreMessageV4\n | AIV6Type.ModelMessage\n | AIV5Type.ModelMessage\n | AIV7Type.ModelMessage\n // This is here because the AIV4 Message type can omit parts entirely.\n | AIV4Message,\n ): msg is AIV5Type.ModelMessage {\n if ('experimental_providerMetadata' in msg) return false;\n // String content is identical in v4/v5/v6, so treat it as v5-compatible.\n if (typeof msg.content === 'string') return true;\n\n for (const part of msg.content) {\n if (part.type === 'tool-result' && 'output' in part) return true;\n if (part.type === 'tool-call' && 'input' in part) return true;\n if (part.type === 'tool-result' && 'result' in part) return false;\n if (part.type === 'tool-call' && 'args' in part) return false;\n if ('mediaType' in part) return true;\n if ('mimeType' in part) return false;\n if ('experimental_providerMetadata' in part) return false;\n if (part.type === 'reasoning' && 'signature' in part) return false;\n if (part.type === 'redacted-reasoning') return false;\n }\n\n // If no distinguishing features are found, the message shape is still\n // compatible with the v5 model format.\n return true;\n }\n\n /**\n * Get the normalized role for a message\n * Maps `tool` to `assistant` because tool messages are displayed as part of\n * the assistant conversation.\n */\n static getRole(message: MessageInput): MastraDBMessage['role'] {\n if (message.role === 'assistant' || message.role === 'tool') return 'assistant';\n if (message.role === 'user') return 'user';\n if (message.role === 'system') return 'system';\n throw new Error(\n `BUG: add handling for message role ${message.role} in message ${JSON.stringify(message, null, 2)}`,\n );\n }\n}\n","import { convertDataContentToBase64String } from './data-content';\n\n/**\n * Image content can be a string (URL or data URI), a URL object, or binary data\n */\nexport type ImageContent = string | URL | Uint8Array | ArrayBuffer | Buffer;\n\n/**\n * Represents the parsed components of a data URI\n */\nexport interface DataUriParts {\n mimeType?: string;\n base64Content: string;\n isDataUri: boolean;\n}\n\n/**\n * Parses a data URI string into its components.\n * Format: data:[<mediatype>][;base64],<data>\n *\n * @param dataUri - The data URI string to parse\n * @returns Parsed components including MIME type and base64 content\n */\nexport function parseDataUri(dataUri: string): DataUriParts {\n if (!dataUri.startsWith('data:')) {\n return {\n isDataUri: false,\n base64Content: dataUri,\n };\n }\n\n const base64Index = dataUri.indexOf(',');\n if (base64Index === -1) {\n // Malformed data URI, return as-is\n return {\n isDataUri: true,\n base64Content: dataUri,\n };\n }\n\n const header = dataUri.substring(5, base64Index); // Skip 'data:' prefix\n const base64Content = dataUri.substring(base64Index + 1);\n\n // Extract MIME type from header (before ';base64' or ';')\n const semicolonIndex = header.indexOf(';');\n const mimeType = semicolonIndex !== -1 ? header.substring(0, semicolonIndex) : header;\n\n return {\n isDataUri: true,\n mimeType: mimeType || undefined,\n base64Content,\n };\n}\n\n/**\n * Creates a data URI from base64 content and MIME type.\n *\n * @param base64Content - The base64 encoded content\n * @param mimeType - The MIME type (defaults to 'application/octet-stream')\n * @returns A properly formatted data URI\n */\nexport function createDataUri(base64Content: string, mimeType: string = 'application/octet-stream'): string {\n // If it's already a data URI, return as-is\n if (base64Content.startsWith('data:')) {\n return base64Content;\n }\n return `data:${mimeType};base64,${base64Content}`;\n}\n\n/**\n * Converts various image data formats to a string representation.\n * - Strings are returned as-is (could be URLs or data URIs)\n * - URL objects are converted to strings\n * - Binary data (Uint8Array, ArrayBuffer, Buffer) is converted to base64\n *\n * @param image - The image data in various formats\n * @param fallbackMimeType - MIME type to use when creating data URIs from binary data\n * @returns String representation of the image (URL, data URI, or base64)\n */\nexport function imageContentToString(image: ImageContent, fallbackMimeType?: string): string {\n if (typeof image === 'string') {\n return image;\n }\n\n if (image instanceof URL) {\n return image.toString();\n }\n\n if (image instanceof Uint8Array || image instanceof ArrayBuffer || (globalThis.Buffer && Buffer.isBuffer(image))) {\n // Convert binary data to base64\n const base64 = convertDataContentToBase64String(image);\n // If it's not already a data URI, create one\n if (fallbackMimeType && !base64.startsWith('data:')) {\n return `data:${fallbackMimeType};base64,${base64}`;\n }\n return base64;\n }\n\n // Fallback for unknown types - try to convert to string\n return String(image);\n}\n\n/**\n * Converts various image data formats to a data URI string.\n *\n * @param image - The image data in various formats\n * @param mimeType - MIME type for the data URI (defaults to 'image/png')\n * @returns Data URI string\n */\nexport function imageContentToDataUri(image: ImageContent, mimeType: string = 'image/png'): string {\n const imageStr = imageContentToString(image, mimeType);\n\n // If it's already a data URI, return as-is\n if (imageStr.startsWith('data:')) {\n return imageStr;\n }\n\n // If it's an HTTP(S) URL, return as-is (can't convert to data URI)\n if (imageStr.startsWith('http://') || imageStr.startsWith('https://')) {\n return imageStr;\n }\n\n // Otherwise, assume it's base64 and create a data URI\n return `data:${mimeType};base64,${imageStr}`;\n}\n\n/**\n * Gets a stable cache key component for image content.\n * Used for generating hash keys for caching purposes.\n *\n * @param image - The image data in various formats\n * @returns A string or number suitable for cache key generation\n */\nexport function getImageCacheKey(image: ImageContent): string | number {\n if (image instanceof URL) {\n return image.toString();\n }\n\n if (typeof image === 'string') {\n return image.length;\n }\n\n if (image instanceof Uint8Array) {\n return image.byteLength;\n }\n\n if (image instanceof ArrayBuffer) {\n return image.byteLength;\n }\n\n return image;\n}\n\n/**\n * Checks if a string is a valid URL (including protocol-relative URLs).\n *\n * @param str - The string to check\n * @returns true if the string is a valid URL\n */\nexport function isValidUrl(str: string): boolean {\n try {\n new URL(str);\n return true;\n } catch {\n // Try as protocol-relative URL\n if (str.startsWith('//')) {\n try {\n new URL(`https:${str}`);\n return true;\n } catch {\n return false;\n }\n }\n return false;\n }\n}\n\n/**\n * Categorizes a string as a URL, data URI, or raw data (base64/other).\n * Also extracts MIME type from data URIs when present.\n *\n * @param data - The string data to categorize\n * @param fallbackMimeType - Optional fallback MIME type\n * @returns Categorized data with type and extracted MIME type\n */\nexport function categorizeFileData(\n data: string,\n fallbackMimeType?: string,\n): {\n type: 'url' | 'dataUri' | 'raw' | 'providerFileId';\n mimeType?: string;\n data: string;\n} {\n // Parse as data URI first to extract MIME type\n const parsed = parseDataUri(data);\n const mimeType = parsed.isDataUri && parsed.mimeType ? parsed.mimeType : fallbackMimeType;\n\n // Check if it's a data URI\n if (parsed.isDataUri) {\n return {\n type: 'dataUri',\n mimeType,\n data,\n };\n }\n\n // Check if it's an OpenAI Files API file ID — pass through as-is so\n // @ai-sdk/openai can forward it as { file_id: \"file-...\" } to the API.\n // Distinct from 'url': the value is NOT parseable by `new URL()`, so call\n // sites that construct URLs must handle it explicitly. Collision risk with\n // raw base64 is negligible (standard base64 has no '-').\n if (data.startsWith('file-')) {\n return {\n type: 'providerFileId',\n mimeType,\n data,\n };\n }\n\n // Check if it's a URL\n if (isValidUrl(data)) {\n return {\n type: 'url',\n mimeType,\n data,\n };\n }\n\n // Otherwise it's raw data (likely base64 or other string data)\n return {\n type: 'raw',\n mimeType,\n data,\n };\n}\n\n/**\n * Resolve a stored file part's media type and payload across the AI SDK v4 and v5 shapes.\n *\n * Stored \"v2\" file parts are typed as the AI SDK v4 UI shape (`mimeType`/`data`), but\n * v5-shaped file parts (`mediaType`/`url`, renamed in the v5 Media Type Standardization)\n * reach the same read sites. Reading only the v4 fields leaves a v5 part with both values\n * `undefined`, which downstream becomes `contentType: undefined` (making `attachmentsToParts`\n * throw) or collapses distinct parts onto a single cache key. Read whichever shape is present.\n *\n * Returns the RAW resolved values (undefined-preserving); call sites that build a\n * `contentType` should apply their own `'application/octet-stream'` fallback. Mirrors #17366.\n */\nexport function resolveFilePartMediaTypeAndData(part: unknown): { mediaType: string | undefined; data: unknown } {\n // Narrow the boundary: the stored union only describes v4, so widen it here to read\n // either shape without an `as any` cast.\n const filePart = part as { mimeType?: string; data?: unknown; mediaType?: string; url?: unknown };\n return {\n mediaType: filePart.mimeType ?? filePart.mediaType,\n data: filePart.data ?? filePart.url,\n };\n}\n\n/**\n * Classifies a string as a URL, data URI, or raw data.\n *\n * @param data - The string to classify\n * @returns Object with classification and extracted metadata\n */\nexport function classifyFileData(data: string): {\n type: 'url' | 'dataUri' | 'base64' | 'other';\n mimeType?: string;\n} {\n // Check if it's a data URI\n const parsed = parseDataUri(data);\n if (parsed.isDataUri) {\n return {\n type: 'dataUri',\n mimeType: parsed.mimeType,\n };\n }\n\n // Check if it's a URL\n if (isValidUrl(data)) {\n return { type: 'url' };\n }\n\n // Check if it looks like base64 (simple heuristic)\n if (/^[A-Za-z0-9+/\\-_]+=*$/.test(data) && data.length > 20) {\n return { type: 'base64' };\n }\n\n return { type: 'other' };\n}\n","const RESPONSE_ITEM_ID_PROVIDERS = ['openai', 'azure'] as const;\n\nexport type ResponseItemIdProvider = (typeof RESPONSE_ITEM_ID_PROVIDERS)[number];\n\nfunction formatResponseProviderItemKey(provider: ResponseItemIdProvider, itemId: string): string {\n // Keep the provider namespace in the key so matching Azure/OpenAI item IDs\n // cannot merge across provider-specific response streams.\n return `${provider}:${itemId}`;\n}\n\nexport function getResponseProviderItemId(\n providerMetadata: Record<string, unknown> | undefined,\n): { provider: ResponseItemIdProvider; itemId: string } | undefined {\n return getResponseProviderItemIds(providerMetadata)[0];\n}\n\nexport function getResponseProviderItemKey(providerMetadata: Record<string, unknown> | undefined): string | undefined {\n const item = getResponseProviderItemId(providerMetadata);\n return item ? formatResponseProviderItemKey(item.provider, item.itemId) : undefined;\n}\n\nexport function getResponseProviderItemIds(\n providerMetadata: Record<string, unknown> | undefined,\n): Array<{ provider: ResponseItemIdProvider; itemId: string }> {\n if (!providerMetadata) return [];\n\n const azureMetadata = providerMetadata.azure as Record<string, unknown> | undefined;\n const azureItemId = azureMetadata?.itemId;\n const openaiMetadata = providerMetadata.openai as Record<string, unknown> | undefined;\n const openaiItemId = openaiMetadata?.itemId;\n if (typeof azureItemId === 'string' && azureItemId === openaiItemId) {\n return [{ provider: 'azure', itemId: azureItemId }];\n }\n\n // AI SDK Responses metadata is expected to use exactly one provider namespace\n // per part. If a future proxy adds both, keep this deterministic.\n return RESPONSE_ITEM_ID_PROVIDERS.flatMap(provider => {\n const metadata = providerMetadata[provider] as Record<string, unknown> | undefined;\n const itemId = metadata?.itemId;\n return typeof itemId === 'string' ? [{ provider, itemId }] : [];\n });\n}\n\nexport function getResponseProviderItemKeys(providerMetadata: Record<string, unknown> | undefined): string[] {\n return getResponseProviderItemIds(providerMetadata).map(({ provider, itemId }) =>\n formatResponseProviderItemKey(provider, itemId),\n );\n}\n","import type { CoreMessage as CoreMessageV4 } from '@internal/ai-sdk-v4';\nimport type { ModelMessage, ToolResultPart } from '@internal/ai-sdk-v5';\n\nimport type { IMastraLogger } from '../../../logger';\nimport type { MastraDBMessage } from '../state/types';\nimport { getResponseProviderItemId } from './response-item-metadata';\nimport type { ResponseItemIdProvider } from './response-item-metadata';\n\n/**\n * Tool result with input field (Anthropic requirement)\n */\nexport type ToolResultWithInput = ToolResultPart & {\n input: Record<string, unknown>;\n};\n\n// ============================================================================\n// Gemini Compatibility\n// ============================================================================\n\n/**\n * Ensures message array is compatible with Gemini API requirements.\n *\n * Gemini API requires:\n * 1. The first non-system message must be from the user role\n * 2. Cannot have only system messages - at least one user/assistant is required\n *\n * @param messages - Array of model messages to validate and fix\n * @param logger - Optional logger for warnings\n * @returns Modified messages array that satisfies Gemini requirements\n *\n * @see https://github.com/mastra-ai/mastra/issues/7287 - Tool call ordering\n * @see https://github.com/mastra-ai/mastra/issues/8053 - Single turn validation\n * @see https://github.com/mastra-ai/mastra/issues/13045 - Empty thread support\n */\nexport function ensureGeminiCompatibleMessages<T extends ModelMessage | CoreMessageV4>(\n messages: T[],\n logger?: IMastraLogger,\n): T[] {\n const result = [...messages];\n\n // Ensure first non-system message is user\n const firstNonSystemIndex = result.findIndex(m => m.role !== 'system');\n\n if (firstNonSystemIndex === -1) {\n // Only system messages or empty — warn and pass through unchanged.\n // Providers that support system-only prompts (Anthropic, OpenAI) will work natively.\n // Providers that don't (Gemini) will return their own error.\n if (result.length > 0) {\n logger?.warn(\n 'No user or assistant messages in the request. Some providers (e.g. Gemini) require at least one user message to generate a response.',\n );\n }\n } else if (result[firstNonSystemIndex]?.role === 'assistant') {\n // First non-system is assistant, insert user message before it\n result.splice(firstNonSystemIndex, 0, {\n role: 'user',\n content: '.',\n } as T);\n }\n\n return result;\n}\n\n// ============================================================================\n// Anthropic Compatibility\n// ============================================================================\n\n/**\n * Ensures model messages are compatible with Anthropic API requirements.\n *\n * Anthropic API requires tool-result parts to include an 'input' field\n * that matches the original tool call arguments.\n *\n * @param messages - Array of model messages to transform\n * @param dbMessages - MastraDB messages to look up tool call args from\n * @returns Messages with tool-result parts enriched with input field\n *\n * @see https://github.com/mastra-ai/mastra/issues/11376 - Anthropic models fail with empty object tool input\n */\nexport function ensureAnthropicCompatibleMessages(\n messages: ModelMessage[],\n dbMessages: MastraDBMessage[],\n): ModelMessage[] {\n return messages.map(msg => enrichToolResultsWithInput(msg, dbMessages));\n}\n\n/**\n * Tool call ids in the assistant message at `index` that already have a matching tool_result,\n * either inline in the same message or in the tool message immediately after it — the only\n * two positions providers accept.\n */\nfunction collectPairedToolCallIds(messages: ModelMessage[], index: number): Set<string> {\n const current = messages[index]!;\n if (!Array.isArray(current.content)) return new Set();\n\n const useIds = new Set<string>();\n const resultIds = new Set<string>();\n for (const part of current.content) {\n if (part.type === 'tool-call') useIds.add(part.toolCallId);\n else if (part.type === 'tool-result') resultIds.add(part.toolCallId);\n }\n\n const next = messages[index + 1];\n if (next && next.role === 'tool' && Array.isArray(next.content)) {\n for (const part of next.content) {\n if (part.type === 'tool-result') resultIds.add(part.toolCallId);\n }\n }\n\n return new Set([...useIds].filter(id => resultIds.has(id)));\n}\n\n/**\n * Removes orphan tool_use / tool_result blocks. Anthropic requires every tool_result\n * to be in the message immediately after its matching tool_use, and every tool_use\n * to have a matching tool_result in the next message. Recall windows can slice\n * through a parallel tool-call group and leave behind half a pair.\n */\nexport function sanitizeOrphanedToolPairs(messages: ModelMessage[]): ModelMessage[] {\n const filteredContents = messages.map(m => (Array.isArray(m.content) ? [...m.content] : null));\n\n for (let i = 0; i < messages.length; i++) {\n const current = messages[i]!;\n\n if (current.role === 'assistant' && Array.isArray(current.content)) {\n const validPairs = collectPairedToolCallIds(messages, i);\n const next = messages[i + 1];\n\n filteredContents[i] = filteredContents[i]!.filter(p => {\n if (p.type !== 'tool-call') return true;\n const tc = p as { toolCallId: string; providerExecuted?: boolean };\n // Provider-executed tools may be deferred (e.g. Anthropic web_search): the tool_use\n // can appear without a matching tool_result until the provider resumes on the next call.\n return tc.providerExecuted === true || validPairs.has(tc.toolCallId);\n });\n\n if (next && next.role === 'tool' && Array.isArray(next.content)) {\n filteredContents[i + 1] = filteredContents[i + 1]!.filter(\n p => p.type !== 'tool-result' || validPairs.has((p as { toolCallId: string }).toolCallId),\n );\n }\n } else if (current.role === 'tool' && Array.isArray(current.content)) {\n const prev = messages[i - 1];\n if (!prev || prev.role !== 'assistant' || !Array.isArray(prev.content)) {\n filteredContents[i] = filteredContents[i]!.filter(p => p.type !== 'tool-result');\n }\n }\n }\n\n const result: ModelMessage[] = [];\n for (let i = 0; i < messages.length; i++) {\n const original = messages[i]!;\n const filtered = filteredContents[i];\n if (filtered == null) {\n result.push(original);\n continue;\n }\n if (filtered.length === 0) continue;\n if (Array.isArray(original.content) && filtered.length === original.content.length) {\n result.push(original);\n continue;\n }\n result.push({ ...original, content: filtered } as ModelMessage);\n }\n\n return result;\n}\n\n/**\n * Keeps result-less tool calls in the prompt by pairing each one with a placeholder result.\n *\n * Used when the caller opted to keep suspended tool calls visible to the agent\n * (`filterIncompleteToolCalls: false`). Providers reject a tool_use with no matching\n * tool_result, so dropping the call is not the only option — synthesizing the missing\n * half keeps the pending call in context while satisfying the pairing requirement.\n *\n * Provider-executed calls are left alone: they may be legitimately deferred to the next\n * request, and giving them a result would resolve a call the provider intends to resume.\n *\n * @see https://github.com/mastra-ai/mastra/issues/20610\n */\nexport function pairOrphanedToolCalls(messages: ModelMessage[]): ModelMessage[] {\n const paired: ModelMessage[] = [];\n\n for (let i = 0; i < messages.length; i++) {\n const current = messages[i]!;\n paired.push(current);\n\n if (current.role !== 'assistant' || !Array.isArray(current.content)) continue;\n\n const pairedIds = collectPairedToolCallIds(messages, i);\n const placeholders: ToolResultPart[] = [];\n for (const part of current.content) {\n if (part.type !== 'tool-call') continue;\n const tc = part as { toolCallId: string; toolName: string; providerExecuted?: boolean };\n if (tc.providerExecuted === true || pairedIds.has(tc.toolCallId)) continue;\n placeholders.push({\n type: 'tool-result',\n toolCallId: tc.toolCallId,\n toolName: tc.toolName,\n output: { type: 'json', value: { status: 'pending' } },\n });\n }\n\n if (placeholders.length === 0) continue;\n\n const next = messages[i + 1];\n if (next && next.role === 'tool' && Array.isArray(next.content)) {\n paired.push({ ...next, content: [...next.content, ...placeholders] });\n i++;\n } else {\n paired.push({ role: 'tool', content: placeholders });\n }\n }\n\n // Every tool call is paired by now, so this only clears tool_results whose call is gone.\n return sanitizeOrphanedToolPairs(paired);\n}\n\n/**\n * Enriches a single message's tool-result parts with input field\n */\nfunction enrichToolResultsWithInput(message: ModelMessage, dbMessages: MastraDBMessage[]): ModelMessage {\n if (message.role !== 'tool' || !Array.isArray(message.content)) {\n return message;\n }\n\n return {\n ...message,\n content: message.content.map(part => {\n if (part.type === 'tool-result') {\n return {\n ...part,\n input: findToolCallArgs(dbMessages, part.toolCallId),\n } as ToolResultWithInput;\n }\n return part;\n }),\n } as ModelMessage;\n}\n\n// ============================================================================\n// OpenAI-compatible Responses Compatibility\n// ============================================================================\n\n/**\n * Checks if a message part has an OpenAI reasoning itemId.\n *\n * OpenAI Responses reasoning items are tracked via `providerMetadata.openai.itemId`.\n * Each reasoning item has a unique itemId that must be preserved for proper deduplication.\n *\n * @param part - A message part to check\n * @returns true if the part has an OpenAI itemId\n *\n * @see https://github.com/mastra-ai/mastra/issues/9005 - OpenAI reasoning items filtering\n */\nexport function hasOpenAIReasoningItemId(part: unknown): boolean {\n return Boolean(getOpenAIReasoningItemId(part));\n}\n\n/**\n * Checks if a message part has an OpenAI-compatible Responses itemId.\n *\n * Provider-neutral Responses item IDs are tracked via provider metadata or\n * provider options fields such as `openai.itemId` or `azure.itemId`.\n */\nexport function hasResponseProviderItemId(part: unknown): boolean {\n return Boolean(getResponseProviderItemIdFromPart(part));\n}\n\n/**\n * Extracts an OpenAI itemId from a message part if present.\n *\n * This only inspects `providerMetadata.openai.itemId`; use\n * `getResponseProviderItemIdFromPart` for provider-aware Azure/OpenAI lookups.\n *\n * @param part - A message part to extract from\n * @returns The itemId string or undefined if not present\n */\nexport function getOpenAIReasoningItemId(part: unknown): string | undefined {\n if (!part || typeof part !== 'object') return undefined;\n const partAny = part as Record<string, unknown>;\n const providerMetadata = partAny.providerMetadata as Record<string, unknown> | undefined;\n const openaiMetadata = providerMetadata?.openai as Record<string, unknown> | undefined;\n return typeof openaiMetadata?.itemId === 'string' ? openaiMetadata.itemId : undefined;\n}\n\nexport function getResponseProviderItemIdFromPart(\n part: unknown,\n): { provider: ResponseItemIdProvider; itemId: string } | undefined {\n if (!part || typeof part !== 'object') return undefined;\n const partAny = part as Record<string, unknown>;\n\n return (\n getResponseProviderItemId(partAny.providerMetadata as Record<string, unknown> | undefined) ||\n getResponseProviderItemId(partAny.providerOptions as Record<string, unknown> | undefined)\n );\n}\n\n// ============================================================================\n// Tool Call Args Lookup\n// ============================================================================\n\n/**\n * Finds the tool call args for a given toolCallId by searching through messages.\n * This is used to reconstruct the input field when converting tool-result parts to StaticToolResult.\n *\n * Searches through messages in reverse order (most recent first) for better performance.\n * Checks both content.parts (v2 format) and toolInvocations (legacy AIV4 format).\n *\n * @param messages - Array of MastraDB messages to search through\n * @param toolCallId - The ID of the tool call to find args for\n * @returns The args object from the matching tool call, or an empty object if not found\n */\nexport function findToolCallArgs(messages: MastraDBMessage[], toolCallId: string): Record<string, unknown> {\n // Search through all messages in reverse order (most recent first) for better performance\n for (let i = messages.length - 1; i >= 0; i--) {\n const msg = messages[i];\n if (!msg || msg.role !== 'assistant') {\n continue;\n }\n\n // Check both content.parts (v2 format) and toolInvocations (legacy format)\n if (msg.content.parts) {\n // Look for tool-invocation with matching toolCallId (can be in 'call' or 'result' state)\n const toolCallPart = msg.content.parts.find(\n p => p.type === 'tool-invocation' && p.toolInvocation.toolCallId === toolCallId,\n );\n\n if (toolCallPart && toolCallPart.type === 'tool-invocation') {\n const args = toolCallPart.toolInvocation.args || {};\n if (typeof args === 'object' && Object.keys(args).length > 0) {\n return args;\n }\n }\n }\n\n // Also check toolInvocations array (AIV4 format)\n if (msg.content.toolInvocations) {\n const toolInvocation = msg.content.toolInvocations.find(inv => inv.toolCallId === toolCallId);\n\n if (toolInvocation) {\n const args = toolInvocation.args || {};\n if (typeof args === 'object' && Object.keys(args).length > 0) {\n return args;\n }\n }\n }\n }\n\n // If not found in DB messages, return empty object\n return {};\n}\n","import type {\n UIMessage as UIMessageV4,\n CoreMessage as CoreMessageV4,\n ToolInvocation as ToolInvocationV4,\n} from '@internal/ai-sdk-v4';\n\nimport { MastraError, ErrorDomain, ErrorCategory } from '../../../error';\nimport { getTransformedToolPayload, hasTransformedToolPayload } from '../../../tools/payload-transform';\nimport { TypeDetector } from '../detection/TypeDetector';\nimport { convertDataContentToBase64String } from '../prompt/data-content';\nimport type { ImageContent } from '../prompt/image-utils';\nimport {\n categorizeFileData,\n createDataUri,\n imageContentToString,\n resolveFilePartMediaTypeAndData,\n} from '../prompt/image-utils';\nimport type {\n MastraDBMessage,\n MastraMessageContentV2,\n MastraMessagePart,\n UIMessageV4Part,\n MessageSource,\n UIMessageWithMetadata,\n} from '../state/types';\nimport { findToolCallArgs } from '../utils/provider-compat';\n\nfunction getDisplayTransform(\n providerMetadata: unknown,\n phase: 'input-available' | 'output-available' | 'error',\n fallback: unknown,\n enabled = true,\n) {\n if (!enabled) {\n return fallback;\n }\n const transform = getTransformedToolPayload(providerMetadata, 'display', phase);\n return hasTransformedToolPayload(transform) ? transform.transformed : fallback;\n}\n\nfunction transformV4ToolInvocationForDisplay(\n invocation: NonNullable<MastraMessageContentV2['toolInvocations']>[number],\n providerMetadata: unknown,\n enabled: boolean,\n) {\n return {\n ...invocation,\n args: getDisplayTransform(providerMetadata, 'input-available', invocation.args, enabled),\n ...(invocation.state === 'result'\n ? {\n result: getDisplayTransform(\n providerMetadata,\n 'output-available',\n getDisplayTransform(providerMetadata, 'error', invocation.result, enabled),\n enabled,\n ),\n }\n : {}),\n };\n}\n\n/**\n * Cast Mastra parts (including data-* extensions) to the V4 UI parts type.\n * Data-* parts (e.g. data-tool-call-suspended) are not natively typed in AI SDK V4,\n * but must be preserved so features like HITL workflow resumption work after a page refresh.\n */\nfunction preserveExtendedParts(parts: MastraMessagePart[]): UIMessageV4Part[] {\n return parts as UIMessageV4Part[];\n}\n\n/**\n * Filter out empty text parts from message parts array.\n * Empty text blocks are not allowed by Anthropic's API and cause request failures.\n * This can happen during streaming when text-start/text-end events occur without actual content.\n * However, if the only part is an empty text part, it is preserved as a legitimate placeholder\n * (e.g. empty assistant messages between tool results and user messages).\n */\nfunction filterEmptyTextParts(parts: MastraMessagePart[]): MastraMessagePart[] {\n const hasNonEmptyParts = parts.some(part => !(part.type === 'text' && part.text === ''));\n if (!hasNonEmptyParts) return parts;\n return parts.filter(part => {\n if (part.type === 'text') {\n return part.text !== '';\n }\n return true;\n });\n}\n\nfunction getSignalType(message: MastraDBMessage): string | undefined {\n const signal = message.content.metadata?.signal;\n if (signal && typeof signal === 'object' && !Array.isArray(signal)) {\n const type = (signal as Record<string, unknown>).type;\n return typeof type === 'string' ? type : message.type;\n }\n\n return message.type;\n}\n\nfunction getSignalTagName(message: MastraDBMessage): string | undefined {\n const signal = message.content.metadata?.signal;\n if (signal && typeof signal === 'object' && !Array.isArray(signal)) {\n const tagName = (signal as Record<string, unknown>).tagName;\n if (typeof tagName === 'string') return tagName;\n }\n\n const type = getSignalType(message);\n if (type === 'user') return 'user';\n if (type === 'reactive') return message.type;\n return type;\n}\n\nfunction isUserSignalType(type: string | undefined): boolean {\n return type === 'user' || type === 'user-message';\n}\n\nfunction toSignalDataPart(message: MastraDBMessage, contents: string): MastraMessagePart {\n const signal =\n message.content.metadata?.signal && typeof message.content.metadata.signal === 'object'\n ? (message.content.metadata.signal as Record<string, unknown>)\n : {};\n const metadata =\n signal.metadata && typeof signal.metadata === 'object' && !Array.isArray(signal.metadata)\n ? (signal.metadata as Record<string, unknown>)\n : {};\n const attributes =\n signal.attributes && typeof signal.attributes === 'object' && !Array.isArray(signal.attributes)\n ? (signal.attributes as Record<string, unknown>)\n : {};\n\n const type = getSignalType(message) ?? 'signal';\n const tagName = getSignalTagName(message) ?? type;\n return {\n type: type === 'user' ? 'data-user-message' : 'data-signal',\n data: {\n id: typeof signal.id === 'string' ? signal.id : message.id,\n type,\n tagName,\n contents: 'contents' in signal ? signal.contents : contents,\n createdAt: typeof signal.createdAt === 'string' ? signal.createdAt : message.createdAt.toISOString(),\n ...(typeof signal.acceptedAt === 'string' ? { acceptedAt: signal.acceptedAt } : {}),\n ...(Object.keys(attributes).length ? { attributes } : {}),\n ...(Object.keys(metadata).length ? { metadata } : {}),\n },\n } as MastraMessagePart;\n}\n\n// Re-export for backward compatibility\nexport type { UIMessageWithMetadata };\n\nexport interface AIV4AdapterContext {\n memoryInfo: { threadId?: string; resourceId?: string } | null;\n newMessageId(): string;\n generateCreatedAt(messageSource: MessageSource, start?: unknown): Date;\n /** Messages array for looking up tool call args */\n dbMessages?: MastraDBMessage[];\n}\n\n/**\n * AIV4Adapter - Handles conversions between MastraDBMessage and AI SDK V4 formats\n *\n * This adapter centralizes all AI SDK V4 (UIMessage and CoreMessage) conversion logic.\n */\nexport class AIV4Adapter {\n /**\n * Convert MastraDBMessage to AI SDK V4 UIMessage\n */\n static toUIMessage(m: MastraDBMessage, options?: { transformToolPayloads?: boolean }): UIMessageWithMetadata {\n const transformToolPayloads = options?.transformToolPayloads ?? true;\n const experimentalAttachments: UIMessageWithMetadata['experimental_attachments'] = m.content\n .experimental_attachments\n ? [...m.content.experimental_attachments]\n : [];\n const contentString =\n typeof m.content.content === `string` && m.content.content !== ''\n ? m.content.content\n : (m.content.parts ?? []).reduce((prev, part) => {\n if (part.type === `text`) {\n // return only the last text part like AI SDK does\n return part.text;\n }\n return prev;\n }, '');\n\n const parts: MastraMessageContentV2['parts'] = [];\n const sourceParts = m.content.parts ?? [];\n\n if (sourceParts.length) {\n for (const part of sourceParts) {\n if (part.type === `file`) {\n // Stored file parts can arrive in either the v4 (`mimeType`/`data`) or v5\n // (`mediaType`/`url`) shape; resolve both so a v5 part isn't read as `undefined`.\n const { mediaType: fileMimeType, data: fileData } = resolveFilePartMediaTypeAndData(part);\n // Normalize fileData to ensure it's a valid URL or data URI\n let normalizedUrl: string;\n if (typeof fileData === 'string') {\n const categorized = categorizeFileData(fileData, fileMimeType);\n if (categorized.type === 'raw') {\n // Raw base64 - convert to data URI\n normalizedUrl = createDataUri(fileData, fileMimeType || 'application/octet-stream');\n } else {\n // Already a URL, data URI, or provider file ID (e.g. OpenAI \"file-...\").\n // Provider file IDs are not parseable URLs; attachmentsToParts handles\n // them explicitly before constructing a URL.\n normalizedUrl = fileData;\n }\n } else {\n // Non-string payload (shouldn't happen for stored file parts, but handle it):\n // coerce to a string so `normalizedUrl` stays typed `string`.\n normalizedUrl = imageContentToString(fileData as ImageContent, fileMimeType);\n }\n\n experimentalAttachments.push({\n contentType: fileMimeType ?? 'application/octet-stream',\n url: normalizedUrl,\n });\n } else if (\n part.type === 'tool-invocation' &&\n (part.toolInvocation.state === 'call' || part.toolInvocation.state === 'partial-call')\n ) {\n // Filter out tool invocations with call or partial-call states\n continue;\n } else if (part.type === 'tool-invocation') {\n // Handle tool invocations with step number logic\n const isDeniedApproval = part.toolInvocation.state === 'output-denied';\n const toolInvocation = {\n ...part.toolInvocation,\n // v4 has no denied state and AI SDK v4's convertToCoreMessages requires every\n // tool invocation to carry a result. Downgrade a declined approval to a normal\n // result whose value is the decline reason so the conversion accepts it.\n ...(isDeniedApproval ? { state: 'result' as const } : {}),\n args: getDisplayTransform(\n part.providerMetadata,\n 'input-available',\n part.toolInvocation.args,\n transformToolPayloads,\n ),\n ...(part.toolInvocation.state === 'result'\n ? {\n result: getDisplayTransform(\n part.providerMetadata,\n 'output-available',\n getDisplayTransform(\n part.providerMetadata,\n 'error',\n part.toolInvocation.result,\n transformToolPayloads,\n ),\n transformToolPayloads,\n ),\n }\n : isDeniedApproval\n ? { result: part.toolInvocation.approval?.reason ?? 'Tool call was not approved by the user' }\n : {}),\n };\n\n // Find the step number for this tool invocation\n let currentStep = -1;\n let toolStep = -1;\n for (const innerPart of sourceParts) {\n if (innerPart.type === `step-start`) currentStep++;\n if (\n innerPart.type === `tool-invocation` &&\n innerPart.toolInvocation.toolCallId === part.toolInvocation.toolCallId\n ) {\n toolStep = currentStep;\n break;\n }\n }\n\n if (toolStep >= 0) {\n const preparedInvocation = {\n step: toolStep,\n ...toolInvocation,\n };\n parts.push({\n type: 'tool-invocation',\n toolInvocation: preparedInvocation,\n });\n } else {\n parts.push({\n type: 'tool-invocation',\n toolInvocation,\n });\n }\n } else {\n parts.push(part);\n }\n }\n }\n\n if (parts.length === 0 && experimentalAttachments.length > 0) {\n // make sure we have atleast one part so this message doesn't get removed when converting to core message\n parts.push({ type: 'text', text: '' });\n }\n\n const signalType = m.role === 'signal' ? getSignalType(m) : undefined;\n const isUserMessageSignal = isUserSignalType(signalType);\n const v4Parts = preserveExtendedParts(\n m.role === 'signal' && !isUserMessageSignal ? [toSignalDataPart(m, m.content.content || contentString)] : parts,\n );\n\n if (m.role === `user`) {\n const uiMess