@mastra/core
Version:
1 lines • 72.1 kB
Source Map (JSON)
{"version":3,"file":"source-BqLo7mT8.cjs","names":["VersionedStorageDomain","normalizePerPage","deepEqual","calculatePagination","OWNED_FIELDS_BY_GROUP","ownershipFromEditorConfig","FilesystemVersionedHelpers","SOURCE_CONTROL_AGENTS_DIR","normalizePerPage","calculatePagination","getSourceAgentFilePath"],"sources":["../src/storage/domains/agents/base.ts","../src/storage/domains/agents/inmemory.ts","../src/storage/domains/inmemory-db.ts","../src/storage/domains/agents/filesystem.ts","../src/storage/domains/agents/source.ts"],"sourcesContent":["import type {\n StorageAgentType,\n StorageAgentSnapshotType,\n StorageResolvedAgentType,\n StorageCreateAgentInput,\n StorageUpdateAgentInput,\n StorageListAgentsInput,\n StorageListAgentsOutput,\n StorageListAgentsResolvedOutput,\n} from '../../types';\nimport { VersionedStorageDomain } from '../versioned';\nimport type { VersionBase, CreateVersionInputBase, ListVersionsInputBase, ListVersionsOutputBase } from '../versioned';\n\n// ============================================================================\n// Agent Version Types\n// ============================================================================\n\n/**\n * Represents a stored version of an agent configuration.\n * The config fields are top-level on the version row (no nested snapshot object).\n */\nexport interface AgentVersion extends StorageAgentSnapshotType, VersionBase {\n /** ID of the agent this version belongs to */\n agentId: string;\n}\n\n/**\n * Input for creating a new agent version.\n * Config fields are top-level (no nested snapshot object).\n */\nexport interface CreateVersionInput extends StorageAgentSnapshotType, CreateVersionInputBase {\n /** ID of the agent this version belongs to */\n agentId: string;\n}\n\n/**\n * Sort direction for version listings.\n */\nexport type VersionSortDirection = 'ASC' | 'DESC';\n\n/**\n * Fields that can be used for ordering version listings.\n */\nexport type VersionOrderBy = 'versionNumber' | 'createdAt';\n\n/**\n * Input for listing agent versions with pagination and sorting.\n */\nexport interface ListVersionsInput extends ListVersionsInputBase {\n /** ID of the agent to list versions for */\n agentId: string;\n}\n\n/**\n * Output for listing agent versions with pagination info.\n */\nexport interface ListVersionsOutput extends ListVersionsOutputBase<AgentVersion> {}\n\n// ============================================================================\n// AgentsStorage Base Class\n// ============================================================================\n\nexport abstract class AgentsStorage extends VersionedStorageDomain<\n StorageAgentType,\n StorageAgentSnapshotType,\n StorageResolvedAgentType,\n AgentVersion,\n CreateVersionInput,\n ListVersionsInput,\n ListVersionsOutput,\n { agent: StorageCreateAgentInput },\n StorageUpdateAgentInput,\n StorageListAgentsInput | undefined,\n StorageListAgentsOutput,\n StorageListAgentsResolvedOutput\n> {\n protected readonly listKey = 'agents';\n protected readonly versionMetadataFields = [\n 'id',\n 'agentId',\n 'versionNumber',\n 'changedFields',\n 'changeMessage',\n 'createdAt',\n ] satisfies (keyof AgentVersion)[];\n\n constructor() {\n super({\n component: 'STORAGE',\n name: 'AGENTS',\n });\n }\n}\n","import { deepEqual } from '../../../utils';\nimport { normalizePerPage, calculatePagination } from '../../base';\nimport type {\n StorageAgentType,\n StorageCreateAgentInput,\n StorageUpdateAgentInput,\n StorageListAgentsInput,\n StorageListAgentsOutput,\n ThreadOrderBy,\n ThreadSortDirection,\n} from '../../types';\nimport type { InMemoryDB } from '../inmemory-db';\nimport type {\n AgentVersion,\n CreateVersionInput,\n ListVersionsInput,\n ListVersionsOutput,\n VersionOrderBy,\n VersionSortDirection,\n} from './base';\nimport { AgentsStorage } from './base';\n\nexport class InMemoryAgentsStorage extends AgentsStorage {\n private db: InMemoryDB;\n\n constructor({ db }: { db: InMemoryDB }) {\n super();\n this.db = db;\n }\n\n async dangerouslyClearAll(): Promise<void> {\n this.db.agents.clear();\n this.db.agentVersions.clear();\n }\n\n // ==========================================================================\n // Agent CRUD Methods\n // ==========================================================================\n\n async getById(id: string): Promise<StorageAgentType | null> {\n const agent = this.db.agents.get(id);\n return agent ? this.deepCopyAgent(agent) : null;\n }\n\n async create(input: { agent: StorageCreateAgentInput }): Promise<StorageAgentType> {\n const { agent } = input;\n\n if (this.db.agents.has(agent.id)) {\n throw new Error(`Agent with id ${agent.id} already exists`);\n }\n\n const now = new Date();\n // Default visibility to 'private' when an authorId is set; leave undefined for legacy unowned rows.\n const visibility = agent.visibility ?? (agent.authorId ? 'private' : undefined);\n const newAgent: StorageAgentType = {\n id: agent.id,\n status: 'draft',\n activeVersionId: undefined,\n authorId: agent.authorId,\n visibility,\n metadata: agent.metadata,\n favoriteCount: 0,\n createdAt: now,\n updatedAt: now,\n };\n\n this.db.agents.set(agent.id, newAgent);\n\n // Extract config fields from the flat input (everything except agent-record fields)\n const { id: _id, authorId: _authorId, visibility: _visibility, metadata: _metadata, ...snapshotConfig } = agent;\n\n // Create version 1 from the config\n const versionId = crypto.randomUUID();\n await this.createVersion({\n id: versionId,\n agentId: agent.id,\n versionNumber: 1,\n ...snapshotConfig,\n changedFields: Object.keys(snapshotConfig),\n changeMessage: 'Initial version',\n });\n\n // Return the thin agent record (activeVersionId remains null)\n return this.deepCopyAgent(newAgent);\n }\n\n async update(input: StorageUpdateAgentInput): Promise<StorageAgentType> {\n const { id, ...updates } = input;\n\n const existingAgent = this.db.agents.get(id);\n if (!existingAgent) {\n throw new Error(`Agent with id ${id} not found`);\n }\n\n const { authorId, visibility, activeVersionId, metadata, status } = updates;\n\n const updatedAgent: StorageAgentType = {\n ...existingAgent,\n ...(authorId !== undefined && { authorId }),\n ...(visibility !== undefined && { visibility }),\n ...(activeVersionId !== undefined && { activeVersionId }),\n ...(metadata !== undefined && {\n metadata: { ...existingAgent.metadata, ...metadata },\n }),\n ...(status !== undefined && { status }),\n updatedAt: new Date(),\n };\n\n this.db.agents.set(id, updatedAgent);\n return this.deepCopyAgent(updatedAgent);\n }\n\n async delete(id: string): Promise<void> {\n // Idempotent delete - no-op if agent doesn't exist\n this.db.agents.delete(id);\n // Also delete all versions for this agent\n await this.deleteVersionsByParentId(id);\n }\n\n async list(args?: StorageListAgentsInput): Promise<StorageListAgentsOutput> {\n const {\n page = 0,\n perPage: perPageInput,\n orderBy,\n authorId,\n visibility,\n metadata,\n status,\n entityIds,\n pinFavoritedFor,\n favoritedOnly,\n } = args || {};\n const { field, direction } = this.parseOrderBy(orderBy);\n\n // Normalize perPage for query (false → MAX_SAFE_INTEGER, 0 → 0, undefined → 100)\n const perPage = normalizePerPage(perPageInput, 100);\n\n if (page < 0) {\n throw new Error('page must be >= 0');\n }\n\n // Prevent unreasonably large page values\n const maxOffset = Number.MAX_SAFE_INTEGER / 2;\n if (page * perPage > maxOffset) {\n throw new Error('page value too large');\n }\n\n // Get all agents and apply filters\n let agents = Array.from(this.db.agents.values());\n\n // Restrict to a set of IDs (used by ?favoritedOnly=true).\n // An empty array means \"no candidates\" -> empty result.\n if (entityIds !== undefined) {\n if (entityIds.length === 0) {\n return {\n agents: [],\n total: 0,\n page,\n perPage: perPageInput === false ? false : perPage,\n hasMore: false,\n };\n }\n const idSet = new Set(entityIds);\n agents = agents.filter(agent => idSet.has(agent.id));\n }\n\n // Filter by status\n if (status) {\n agents = agents.filter(agent => agent.status === status);\n }\n\n // Filter by authorId if provided\n if (authorId !== undefined) {\n agents = agents.filter(agent => agent.authorId === authorId);\n }\n\n // Filter by visibility if provided\n if (visibility !== undefined) {\n agents = agents.filter(agent => agent.visibility === visibility);\n }\n\n // Filter by metadata if provided (AND logic - all key-value pairs must match)\n if (metadata && Object.keys(metadata).length > 0) {\n agents = agents.filter(agent => {\n if (!agent.metadata) return false;\n return Object.entries(metadata).every(([key, value]) => deepEqual(agent.metadata![key], value));\n });\n }\n\n // Optional favorited-first ordering / favorites-only filter.\n const favoritedIds = pinFavoritedFor ? this.collectFavoritedIdsFor(pinFavoritedFor) : undefined;\n if (favoritedOnly) {\n if (favoritedIds) {\n agents = agents.filter(agent => favoritedIds.has(agent.id));\n } else {\n // Defensive: favoritedOnly with no userId can never match a real row.\n agents = [];\n }\n }\n\n const sortedAgents = this.sortAgents(agents, field, direction, favoritedIds);\n\n // Deep clone agents to avoid mutation\n const clonedAgents = sortedAgents.map(agent => this.deepCopyAgent(agent));\n\n const { offset, perPage: perPageForResponse } = calculatePagination(page, perPageInput, perPage);\n\n return {\n agents: clonedAgents.slice(offset, offset + perPage),\n total: clonedAgents.length,\n page,\n perPage: perPageForResponse,\n hasMore: offset + perPage < clonedAgents.length,\n };\n }\n\n // ==========================================================================\n // Agent Version Methods\n // ==========================================================================\n\n async createVersion(input: CreateVersionInput): Promise<AgentVersion> {\n // Check if version with this ID already exists (versions are immutable)\n if (this.db.agentVersions.has(input.id)) {\n throw new Error(`Version with id ${input.id} already exists`);\n }\n\n // Check for duplicate (agentId, versionNumber) pair\n for (const version of this.db.agentVersions.values()) {\n if (version.agentId === input.agentId && version.versionNumber === input.versionNumber) {\n throw new Error(`Version number ${input.versionNumber} already exists for agent ${input.agentId}`);\n }\n }\n\n const version: AgentVersion = {\n ...input,\n createdAt: new Date(),\n };\n\n // Deep clone before storing to prevent external mutation\n this.db.agentVersions.set(input.id, this.deepCopyVersion(version));\n return this.deepCopyVersion(version);\n }\n\n async getVersion(id: string): Promise<AgentVersion | null> {\n const version = this.db.agentVersions.get(id);\n return version ? this.deepCopyVersion(version) : null;\n }\n\n async getVersionByNumber(agentId: string, versionNumber: number): Promise<AgentVersion | null> {\n for (const version of this.db.agentVersions.values()) {\n if (version.agentId === agentId && version.versionNumber === versionNumber) {\n return this.deepCopyVersion(version);\n }\n }\n return null;\n }\n\n async getLatestVersion(agentId: string): Promise<AgentVersion | null> {\n let latest: AgentVersion | null = null;\n for (const version of this.db.agentVersions.values()) {\n if (version.agentId === agentId) {\n if (!latest || version.versionNumber > latest.versionNumber) {\n latest = version;\n }\n }\n }\n return latest ? this.deepCopyVersion(latest) : null;\n }\n\n async listVersions(input: ListVersionsInput): Promise<ListVersionsOutput> {\n const { agentId, page = 0, perPage: perPageInput, orderBy } = input;\n const { field, direction } = this.parseVersionOrderBy(orderBy);\n\n // Normalize perPage for query (false -> MAX_SAFE_INTEGER, 0 -> 0, undefined -> 20)\n const perPage = normalizePerPage(perPageInput, 20);\n\n if (page < 0) {\n throw new Error('page must be >= 0');\n }\n\n // Prevent unreasonably large page values\n const maxOffset = Number.MAX_SAFE_INTEGER / 2;\n if (page * perPage > maxOffset) {\n throw new Error('page value too large');\n }\n\n // Filter versions by agentId\n let versions = Array.from(this.db.agentVersions.values()).filter(v => v.agentId === agentId);\n\n // Sort versions\n versions = this.sortVersions(versions, field, direction);\n\n // Deep clone versions to avoid mutation\n const clonedVersions = versions.map(v => this.deepCopyVersion(v));\n\n const total = clonedVersions.length;\n const { offset, perPage: perPageForResponse } = calculatePagination(page, perPageInput, perPage);\n const paginatedVersions = clonedVersions.slice(offset, offset + perPage);\n\n return {\n versions: paginatedVersions,\n total,\n page,\n perPage: perPageForResponse,\n hasMore: offset + perPage < total,\n };\n }\n\n async deleteVersion(id: string): Promise<void> {\n // Idempotent delete - no-op if version doesn't exist\n this.db.agentVersions.delete(id);\n }\n\n async deleteVersionsByParentId(entityId: string): Promise<void> {\n const idsToDelete: string[] = [];\n for (const [id, version] of this.db.agentVersions.entries()) {\n if (version.agentId === entityId) {\n idsToDelete.push(id);\n }\n }\n\n for (const id of idsToDelete) {\n this.db.agentVersions.delete(id);\n }\n }\n\n async countVersions(agentId: string): Promise<number> {\n let count = 0;\n for (const version of this.db.agentVersions.values()) {\n if (version.agentId === agentId) {\n count++;\n }\n }\n return count;\n }\n\n // ==========================================================================\n // Private Helper Methods\n // ==========================================================================\n\n /**\n * Deep copy a thin agent record to prevent external mutation of stored data\n */\n private deepCopyAgent(agent: StorageAgentType): StorageAgentType {\n return {\n ...agent,\n metadata: agent.metadata ? { ...agent.metadata } : agent.metadata,\n };\n }\n\n /**\n * Deep copy a version to prevent external mutation of stored data\n */\n private deepCopyVersion(version: AgentVersion): AgentVersion {\n return structuredClone(version);\n }\n\n private sortAgents(\n agents: StorageAgentType[],\n field: ThreadOrderBy,\n direction: ThreadSortDirection,\n favoritedIds?: Set<string>,\n ): StorageAgentType[] {\n return agents.sort((a, b) => {\n // Compound sort: favorited first, then existing orderBy, then id ASC for stable pagination.\n if (favoritedIds) {\n const aFav = favoritedIds.has(a.id) ? 1 : 0;\n const bFav = favoritedIds.has(b.id) ? 1 : 0;\n if (aFav !== bFav) return bFav - aFav;\n }\n\n const aValue = new Date(a[field]).getTime();\n const bValue = new Date(b[field]).getTime();\n if (aValue !== bValue) {\n return direction === 'ASC' ? aValue - bValue : bValue - aValue;\n }\n\n // Stable tie-break for same `createdAt`/`updatedAt`.\n return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;\n });\n }\n\n /**\n * Collect the set of agent IDs favorited by the given user. Returns an empty\n * Set when the favorites domain is not wired or the user has no favorites.\n */\n private collectFavoritedIdsFor(userId: string): Set<string> {\n const favorited = new Set<string>();\n for (const row of this.db.favorites.values()) {\n if (row.userId === userId && row.entityType === 'agent') {\n favorited.add(row.entityId);\n }\n }\n return favorited;\n }\n\n private sortVersions(\n versions: AgentVersion[],\n field: VersionOrderBy,\n direction: VersionSortDirection,\n ): AgentVersion[] {\n return versions.sort((a, b) => {\n let aVal: number;\n let bVal: number;\n\n if (field === 'createdAt') {\n aVal = a.createdAt.getTime();\n bVal = b.createdAt.getTime();\n } else {\n // versionNumber\n aVal = a.versionNumber;\n bVal = b.versionNumber;\n }\n\n return direction === 'ASC' ? aVal - bVal : bVal - aVal;\n });\n }\n}\n","import type { BackgroundTask } from '../../background-tasks/types';\nimport type { ScoreRowData } from '../../evals/types';\nimport type { StorageThreadType } from '../../memory/types';\nimport type {\n StorageAgentType,\n StorageMCPClientType,\n StorageMCPServerType,\n StorageMessageType,\n StoragePromptBlockType,\n StorageResourceType,\n StorageScorerDefinitionType,\n StorageFavoriteType,\n StorageWorkspaceType,\n StorageSkillType,\n StorageToolProviderConnection,\n StorageWorkflowRun,\n ObservationalMemoryRecord,\n DatasetRecord,\n DatasetItemRow,\n DatasetVersion,\n Experiment,\n ExperimentResult,\n} from '../types';\nimport type { AgentVersion } from './agents';\nimport type { MCPClientVersion } from './mcp-clients';\nimport type { MCPServerVersion } from './mcp-servers';\nimport type { TraceEntry } from './observability';\nimport type { FeedbackRecord } from './observability/feedback';\nimport type { LogRecord } from './observability/logs';\nimport type { MetricRecord } from './observability/metrics';\nimport type { ScoreRecord } from './observability/scores';\nimport type { PromptBlockVersion } from './prompt-blocks';\nimport type { Schedule, ScheduleTrigger } from './schedules/base';\nimport type { ScorerDefinitionVersion } from './scorer-definitions';\nimport type { SkillVersion } from './skills';\nimport type { WorkflowDefinition } from './workflow-definitions';\nimport type { WorkspaceVersion } from './workspaces';\n\n/**\n * InMemoryDB is a thin database layer for in-memory storage.\n * It holds all the Maps that store data, similar to how a real database\n * connection (pg-promise client, libsql client) is shared across domains.\n *\n * Each domain receives a reference to this db and operates on the relevant Maps.\n */\nexport class InMemoryDB {\n readonly threads = new Map<string, StorageThreadType>();\n readonly messages = new Map<string, StorageMessageType>();\n readonly resources = new Map<string, StorageResourceType>();\n readonly workflows = new Map<string, StorageWorkflowRun>();\n readonly workflowDefinitions = new Map<string, WorkflowDefinition>();\n readonly scores = new Map<string, ScoreRowData>();\n readonly traces = new Map<string, TraceEntry>();\n readonly metricRecords: MetricRecord[] = [];\n readonly logRecords: LogRecord[] = [];\n readonly scoreRecords: ScoreRecord[] = [];\n readonly feedbackRecords: FeedbackRecord[] = [];\n observabilityNextCursorId = 1;\n readonly traceCursorIds = new Map<string, number>();\n readonly branchCursorIds = new Map<string, number>();\n readonly metricCursorIds = new Map<MetricRecord, number>();\n readonly logCursorIds = new Map<LogRecord, number>();\n readonly scoreCursorIds = new Map<ScoreRecord, number>();\n readonly feedbackCursorIds = new Map<FeedbackRecord, number>();\n readonly agents = new Map<string, StorageAgentType>();\n readonly agentVersions = new Map<string, AgentVersion>();\n readonly promptBlocks = new Map<string, StoragePromptBlockType>();\n readonly promptBlockVersions = new Map<string, PromptBlockVersion>();\n readonly scorerDefinitions = new Map<string, StorageScorerDefinitionType>();\n readonly scorerDefinitionVersions = new Map<string, ScorerDefinitionVersion>();\n readonly mcpClients = new Map<string, StorageMCPClientType>();\n readonly mcpClientVersions = new Map<string, MCPClientVersion>();\n readonly mcpServers = new Map<string, StorageMCPServerType>();\n readonly mcpServerVersions = new Map<string, MCPServerVersion>();\n readonly workspaces = new Map<string, StorageWorkspaceType>();\n readonly workspaceVersions = new Map<string, WorkspaceVersion>();\n readonly skills = new Map<string, StorageSkillType>();\n readonly skillVersions = new Map<string, SkillVersion>();\n /**\n * Favorites keyed by `${userId}\\u0000${entityType}\\u0000${entityId}`. The\n * favorites domain owns reads/writes; this Map lives on InMemoryDB so the\n * favorites domain can also mutate `agents` / `skills` `favoriteCount` atomically\n * within the same synchronous block.\n */\n readonly favorites = new Map<string, StorageFavoriteType>();\n /** Observational memory records, keyed by resourceId, each holding array of records (generations) */\n readonly observationalMemory = new Map<string, ObservationalMemoryRecord[]>();\n\n // Dataset domain maps\n readonly datasets = new Map<string, DatasetRecord>();\n readonly datasetItems = new Map<string, DatasetItemRow[]>();\n readonly datasetVersions = new Map<string, DatasetVersion>();\n\n // Experiment domain maps\n readonly experiments = new Map<string, Experiment>();\n readonly experimentResults = new Map<string, ExperimentResult>();\n\n // Background tasks domain\n readonly backgroundTasks = new Map<string, BackgroundTask>();\n\n // Schedules domain\n readonly schedules = new Map<string, Schedule>();\n readonly scheduleTriggers: ScheduleTrigger[] = [];\n\n /**\n * Tool provider connections keyed by `${authorId}\\u0000${providerId}\\u0000${connectionId}`.\n */\n readonly toolProviderConnections = new Map<string, StorageToolProviderConnection>();\n\n /**\n * Clears all data from all collections.\n * Useful for testing.\n */\n clear(): void {\n this.threads.clear();\n this.messages.clear();\n this.resources.clear();\n this.workflows.clear();\n this.workflowDefinitions.clear();\n this.scores.clear();\n this.traces.clear();\n this.metricRecords.length = 0;\n this.logRecords.length = 0;\n this.scoreRecords.length = 0;\n this.feedbackRecords.length = 0;\n this.observabilityNextCursorId = 1;\n this.traceCursorIds.clear();\n this.branchCursorIds.clear();\n this.metricCursorIds.clear();\n this.logCursorIds.clear();\n this.scoreCursorIds.clear();\n this.feedbackCursorIds.clear();\n this.agents.clear();\n this.agentVersions.clear();\n this.promptBlocks.clear();\n this.promptBlockVersions.clear();\n this.scorerDefinitions.clear();\n this.scorerDefinitionVersions.clear();\n this.mcpClients.clear();\n this.mcpClientVersions.clear();\n this.mcpServers.clear();\n this.mcpServerVersions.clear();\n this.workspaces.clear();\n this.workspaceVersions.clear();\n this.skills.clear();\n this.skillVersions.clear();\n this.favorites.clear();\n this.observationalMemory.clear();\n this.datasets.clear();\n this.datasetItems.clear();\n this.datasetVersions.clear();\n this.experiments.clear();\n this.experimentResults.clear();\n this.backgroundTasks.clear();\n this.schedules.clear();\n this.scheduleTriggers.length = 0;\n this.toolProviderConnections.clear();\n }\n}\n","import type { StorageMastraRef } from '../../base';\nimport type { FilesystemDB } from '../../filesystem-db';\nimport { FilesystemVersionedHelpers } from '../../filesystem-versioned';\nimport type {\n StorageAgentType,\n StorageCreateAgentInput,\n StorageUpdateAgentInput,\n StorageListAgentsInput,\n StorageListAgentsOutput,\n} from '../../types';\nimport type { AgentVersion, CreateVersionInput, ListVersionsInput, ListVersionsOutput } from './base';\nimport { AgentsStorage } from './base';\n\n/**\n * Fields persisted for filesystem-stored agents.\n * Only fields that `applyStoredOverrides` actually uses plus the\n * minimum required by the storage schema (`name`, `model`).\n */\nconst PERSISTED_SNAPSHOT_FIELDS = new Set([\n 'name',\n 'instructions',\n 'model',\n 'tools',\n 'integrationTools',\n 'toolProviders',\n 'mcpClients',\n 'requestContextSchema',\n]);\n\n/**\n * Fields always excluded from per-entity (code-mode) JSON files regardless\n * of editor config. `model`/`name` are not editable from Studio for\n * code-defined agents, so they should not appear in the committed override\n * JSON — they would otherwise look like settable fields in code review and\n * could drift from the source-of-truth declaration in code.\n */\nconst CODE_MODE_EXCLUDED_FIELDS = new Set(['model', 'name']);\n\n/**\n * Fields that depend on per-agent editor ownership.\n * When the agent's editor config does not own a given field (e.g.\n * descriptions-only mode does not own raw instructions), it should be\n * omitted from the on-disk per-entity JSON entirely.\n */\nconst OWNED_FIELDS_BY_GROUP = {\n instructions: ['instructions'],\n tools: ['tools', 'integrationTools', 'mcpClients'],\n} as const;\n\nfunction ownershipFromEditorConfig(editorConfig: unknown): {\n ownsInstructions: boolean;\n ownsTools: boolean;\n} {\n if (editorConfig === false) {\n return { ownsInstructions: false, ownsTools: false };\n }\n if (editorConfig === undefined || editorConfig === null) {\n // Code agents without explicit editor config behave as fully editable.\n return { ownsInstructions: true, ownsTools: true };\n }\n if (typeof editorConfig !== 'object') {\n return { ownsInstructions: false, ownsTools: false };\n }\n const cfg = editorConfig as { instructions?: unknown; tools?: unknown };\n const ownsInstructions = cfg.instructions === true;\n const toolsCfg = cfg.tools;\n const ownsTools =\n toolsCfg === true ||\n (typeof toolsCfg === 'object' && toolsCfg !== null && (toolsCfg as { description?: unknown }).description === true);\n return { ownsInstructions, ownsTools };\n}\n\nfunction stripUnusedFields<T extends Record<string, unknown>>(obj: T): T {\n const result = {} as Record<string, unknown>;\n for (const [key, value] of Object.entries(obj)) {\n if (PERSISTED_SNAPSHOT_FIELDS.has(key)) {\n result[key] = value;\n }\n }\n return result as T;\n}\n\nfunction isAgentNotFoundError(error: unknown, entityId: string): boolean {\n if (!error || typeof error !== 'object') return false;\n\n const maybeError = error as { id?: unknown; message?: unknown; details?: { status?: unknown; agentId?: unknown } };\n return (\n maybeError.id === 'MASTRA_GET_AGENT_BY_AGENT_ID_NOT_FOUND' ||\n (maybeError.details?.status === 404 && maybeError.details?.agentId === entityId) ||\n maybeError.message === `Agent with id ${entityId} not found`\n );\n}\n\nexport class FilesystemAgentsStorage extends AgentsStorage {\n private helpers: FilesystemVersionedHelpers<StorageAgentType, AgentVersion>;\n private storageMastra?: StorageMastraRef;\n\n constructor({ db }: { db: FilesystemDB }) {\n super();\n const getCodeAgent = (entityId: string) => {\n try {\n const agent = this.storageMastra?.getAgentById?.(entityId);\n return agent?.source === 'code' ? agent : undefined;\n } catch (error) {\n if (isAgentNotFoundError(error, entityId)) {\n return undefined;\n }\n throw error;\n }\n };\n const isCodeAgent = (entityId: string): boolean => Boolean(getCodeAgent(entityId));\n const editorConfigFor = (entityId: string): unknown => getCodeAgent(entityId)?.__getEditorConfig?.();\n this.helpers = new FilesystemVersionedHelpers({\n db,\n entitiesFile: 'agents.json',\n parentIdField: 'agentId',\n name: 'FilesystemAgentsStorage',\n versionMetadataFields: ['id', 'agentId', 'versionNumber', 'changedFields', 'changeMessage', 'createdAt'],\n perEntityFilesDir: 'agents',\n // Per-entity layout is used only for code-mode agents — i.e. agents\n // that are declared in code (`source === 'code'`). For db-mode and\n // user-created stored agents we keep the shared `agents.json` layout.\n shouldPersistToPerEntityFile: entity => isCodeAgent(entity.id),\n perEntitySnapshotFilter: (snapshot, entity) => {\n const { ownsInstructions, ownsTools } = ownershipFromEditorConfig(editorConfigFor(entity.id));\n const excludedByOwnership = new Set<string>();\n if (!ownsInstructions) {\n for (const field of OWNED_FIELDS_BY_GROUP.instructions) excludedByOwnership.add(field);\n }\n if (!ownsTools) {\n for (const field of OWNED_FIELDS_BY_GROUP.tools) excludedByOwnership.add(field);\n }\n const result: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(snapshot)) {\n if (CODE_MODE_EXCLUDED_FIELDS.has(key)) continue;\n if (excludedByOwnership.has(key)) continue;\n result[key] = value;\n }\n return result;\n },\n });\n }\n\n __registerMastra(mastra: StorageMastraRef): void {\n this.storageMastra = mastra;\n }\n\n override async init(): Promise<void> {\n await this.helpers.db.init();\n }\n\n async dangerouslyClearAll(): Promise<void> {\n await this.helpers.dangerouslyClearAll();\n }\n\n async getById(id: string): Promise<StorageAgentType | null> {\n return this.helpers.getById(id);\n }\n\n async create(input: { agent: StorageCreateAgentInput }): Promise<StorageAgentType> {\n const { agent } = input;\n const now = new Date();\n // Default visibility to 'private' when an authorId is set; leave undefined for legacy unowned rows.\n const visibility = agent.visibility ?? (agent.authorId ? 'private' : undefined);\n const entity: StorageAgentType = {\n id: agent.id,\n status: 'draft',\n activeVersionId: undefined,\n authorId: agent.authorId,\n visibility,\n metadata: agent.metadata,\n createdAt: now,\n updatedAt: now,\n };\n\n await this.helpers.createEntity(agent.id, entity);\n\n const { id: _id, authorId: _authorId, visibility: _visibility, metadata: _metadata, ...snapshotConfig } = agent;\n const filtered = stripUnusedFields(snapshotConfig);\n const versionId = crypto.randomUUID();\n await this.createVersion({\n id: versionId,\n agentId: agent.id,\n versionNumber: 1,\n ...filtered,\n changedFields: Object.keys(filtered),\n changeMessage: 'Initial version',\n } as CreateVersionInput);\n\n return structuredClone(entity);\n }\n\n async update(input: StorageUpdateAgentInput): Promise<StorageAgentType> {\n const { id, ...updates } = input;\n // Strip snapshot config fields that don't belong on the entity record\n const entityUpdates: Record<string, unknown> = {};\n const entityFields = new Set(['authorId', 'visibility', 'metadata', 'activeVersionId', 'status']);\n for (const [key, value] of Object.entries(updates)) {\n if (entityFields.has(key)) {\n entityUpdates[key] = value;\n }\n }\n return this.helpers.updateEntity(id, entityUpdates);\n }\n\n async delete(id: string): Promise<void> {\n await this.helpers.deleteEntity(id);\n }\n\n async list(args?: StorageListAgentsInput): Promise<StorageListAgentsOutput> {\n const { page, perPage, orderBy, authorId, visibility, metadata, status } = args || {};\n const result = await this.helpers.listEntities({\n page,\n perPage,\n orderBy,\n listKey: 'agents',\n filters: { authorId, visibility, metadata, status },\n });\n return result as unknown as StorageListAgentsOutput;\n }\n\n async createVersion(input: CreateVersionInput): Promise<AgentVersion> {\n const { id, agentId, versionNumber, changedFields, changeMessage, ...snapshotFields } = input;\n const filtered = stripUnusedFields(snapshotFields as Record<string, unknown>);\n return this.helpers.createVersion({\n id,\n agentId,\n versionNumber,\n changedFields,\n changeMessage,\n ...filtered,\n } as AgentVersion);\n }\n\n async getVersion(id: string): Promise<AgentVersion | null> {\n return this.helpers.getVersion(id);\n }\n\n async getVersionByNumber(agentId: string, versionNumber: number): Promise<AgentVersion | null> {\n return this.helpers.getVersionByNumber(agentId, versionNumber);\n }\n\n async getLatestVersion(agentId: string): Promise<AgentVersion | null> {\n return this.helpers.getLatestVersion(agentId);\n }\n\n async listVersions(input: ListVersionsInput): Promise<ListVersionsOutput> {\n const result = await this.helpers.listVersions(input, 'agentId');\n return result as ListVersionsOutput;\n }\n\n async deleteVersion(id: string): Promise<void> {\n await this.helpers.deleteVersion(id);\n }\n\n async deleteVersionsByParentId(entityId: string): Promise<void> {\n await this.helpers.deleteVersionsByParentId(entityId);\n }\n\n async countVersions(agentId: string): Promise<number> {\n return this.helpers.countVersions(agentId);\n }\n}\n","import { calculatePagination, normalizePerPage } from '../../base';\nimport type { StorageMastraRef } from '../../base';\nimport { SOURCE_CONTROL_AGENTS_DIR, getSourceAgentFilePath } from '../../source-control';\nimport type { SourceFileHistoryEntry, SourceControlProvider, SourceWriteResult } from '../../source-control';\nimport type {\n StorageAgentType,\n StorageCreateAgentInput,\n StorageListAgentsInput,\n StorageListAgentsOutput,\n StorageUpdateAgentInput,\n} from '../../types';\nimport { InMemoryDB } from '../inmemory-db';\nimport type {\n AgentVersion,\n CreateVersionInput,\n ListVersionsInput,\n ListVersionsOutput,\n VersionOrderBy,\n VersionSortDirection,\n} from './base';\nimport { AgentsStorage } from './base';\nimport { InMemoryAgentsStorage } from './inmemory';\n\nconst SOURCE_VERSION_PREFIX = 'source:';\n\nconst COMMON_EXCLUDED_FIELDS = new Set([\n 'id',\n 'model',\n 'scorers',\n 'skills',\n 'workflows',\n 'agents',\n 'integrationTools',\n 'toolProviders',\n 'inputProcessors',\n 'outputProcessors',\n 'memory',\n 'mcpClients',\n 'workspace',\n 'browser',\n 'defaultOptions',\n]);\nconst CODE_SOURCE_EXCLUDED_FIELDS = new Set(['name']);\n\nconst OWNED_FIELDS_BY_GROUP = {\n instructions: ['instructions'],\n tools: ['tools'],\n} as const;\n\nexport interface SourceAgentsSourceControlConfig {\n provider: SourceControlProvider;\n agentIds?: string[];\n}\n\nfunction ownershipFromEditorConfig(editorConfig: unknown): {\n ownsInstructions: boolean;\n ownsTools: boolean;\n} {\n if (editorConfig === false) {\n return { ownsInstructions: false, ownsTools: false };\n }\n if (editorConfig === undefined || editorConfig === null) {\n return { ownsInstructions: true, ownsTools: true };\n }\n if (typeof editorConfig !== 'object') {\n return { ownsInstructions: false, ownsTools: false };\n }\n const cfg = editorConfig as { instructions?: unknown; tools?: unknown };\n const ownsInstructions = cfg.instructions === true;\n const toolsCfg = cfg.tools;\n const ownsTools =\n toolsCfg === true ||\n (typeof toolsCfg === 'object' && toolsCfg !== null && (toolsCfg as { description?: unknown }).description === true);\n return { ownsInstructions, ownsTools };\n}\n\nfunction snapshotFromVersion(version: AgentVersion): Record<string, unknown> {\n const { id, agentId, versionNumber, changedFields, changeMessage, createdAt, ...snapshot } = version;\n void id;\n void agentId;\n void versionNumber;\n void changedFields;\n void changeMessage;\n void createdAt;\n return snapshot;\n}\n\nfunction filterSourceSnapshot(\n snapshot: Record<string, unknown>,\n editorConfig: unknown,\n isCodeDefinedAgent: boolean,\n): Record<string, unknown> {\n const excludedByOwnership = new Set<string>();\n if (isCodeDefinedAgent) {\n const { ownsInstructions, ownsTools } = ownershipFromEditorConfig(editorConfig);\n if (!ownsInstructions) {\n for (const field of OWNED_FIELDS_BY_GROUP.instructions) excludedByOwnership.add(field);\n }\n if (!ownsTools) {\n for (const field of OWNED_FIELDS_BY_GROUP.tools) excludedByOwnership.add(field);\n }\n }\n\n const result: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(snapshot)) {\n if (COMMON_EXCLUDED_FIELDS.has(key)) continue;\n if (isCodeDefinedAgent && CODE_SOURCE_EXCLUDED_FIELDS.has(key)) continue;\n if (excludedByOwnership.has(key)) continue;\n if (value === undefined) continue;\n result[key] = value;\n }\n return result;\n}\n\nfunction parseJsonObject(content: string): Record<string, unknown> | null {\n try {\n const parsed = JSON.parse(content) as unknown;\n return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? (parsed as Record<string, unknown>) : null;\n } catch {\n return null;\n }\n}\n\nfunction stableStringify(value: unknown): string {\n if (Array.isArray(value)) {\n return `[${value.map(stableStringify).join(',')}]`;\n }\n if (value && typeof value === 'object') {\n const entries = Object.entries(value as Record<string, unknown>)\n .filter(([, entryValue]) => entryValue !== undefined)\n .sort(([a], [b]) => a.localeCompare(b));\n return `{${entries.map(([key, entryValue]) => `${JSON.stringify(key)}:${stableStringify(entryValue)}`).join(',')}}`;\n }\n return JSON.stringify(value);\n}\n\nfunction agentIdFromSourcePath(path: string): string | undefined {\n const prefix = `${SOURCE_CONTROL_AGENTS_DIR}/`;\n if (!path.startsWith(prefix) || !path.endsWith('.json')) return undefined;\n\n const filename = path.slice(prefix.length, -'.json'.length);\n if (!filename || filename.includes('/')) return undefined;\n\n try {\n return decodeURIComponent(filename);\n } catch {\n return filename;\n }\n}\n\nexport class SourceAgentsSourceControl extends AgentsStorage {\n private readonly provider: SourceControlProvider;\n private readonly knownAgentIds: Set<string>;\n private readonly db = new InMemoryDB();\n private readonly memory = new InMemoryAgentsStorage({ db: this.db });\n private storageMastra?: StorageMastraRef;\n private readonly providerVersions = new Map<string, AgentVersion>();\n private readonly loadedHistory = new Set<string>();\n private readonly hydratedAgents = new Set<string>();\n private readonly activeRefs = new Map<string, string>();\n private providerAgentIdsDiscovered = false;\n\n constructor({ provider, agentIds = [] }: SourceAgentsSourceControlConfig) {\n super();\n this.provider = provider;\n this.knownAgentIds = new Set(agentIds);\n }\n\n __registerMastra(mastra: StorageMastraRef): void {\n this.storageMastra = mastra;\n }\n\n override async init(): Promise<void> {\n const capabilities = await this.provider.getCapabilities();\n if (!capabilities.canRead) {\n throw new Error(capabilities.reason ?? `Source provider ${this.provider.displayName} cannot read files`);\n }\n this.refreshKnownAgentIds();\n await this.discoverProviderAgentIds();\n await Promise.all([...this.knownAgentIds].map(agentId => this.hydrateAgent(agentId)));\n }\n\n async dangerouslyClearAll(): Promise<void> {\n this.hydratedAgents.clear();\n this.loadedHistory.clear();\n this.providerVersions.clear();\n this.activeRefs.clear();\n this.providerAgentIdsDiscovered = false;\n await this.memory.dangerouslyClearAll();\n }\n\n async useProviderRef(agentId: string, ref: string): Promise<void> {\n this.activeRefs.set(agentId, ref);\n this.hydratedAgents.delete(agentId);\n this.loadedHistory.delete(agentId);\n for (const [versionId, version] of this.providerVersions.entries()) {\n if (version.agentId === agentId) {\n this.providerVersions.delete(versionId);\n }\n }\n await this.memory.delete(agentId);\n await this.hydrateAgent(agentId);\n }\n\n async getById(id: string): Promise<StorageAgentType | null> {\n await this.hydrateAgent(id);\n return this.memory.getById(id);\n }\n\n async create(input: { agent: StorageCreateAgentInput }): Promise<StorageAgentType> {\n await this.hydrateAgent(input.agent.id);\n const existing = await this.memory.getById(input.agent.id);\n if (existing) {\n throw new Error(`Agent with id ${input.agent.id} already exists`);\n }\n\n await this.persistSnapshot(input.agent.id, { ...input.agent }, 'Initial version');\n const created = await this.memory.create(input);\n this.knownAgentIds.add(input.agent.id);\n return created;\n }\n\n async update(input: StorageUpdateAgentInput): Promise<StorageAgentType> {\n await this.hydrateAgent(input.id);\n return this.memory.update(input);\n }\n\n async delete(id: string): Promise<void> {\n this.knownAgentIds.delete(id);\n this.hydratedAgents.delete(id);\n this.loadedHistory.delete(id);\n for (const versionId of this.providerVersions.keys()) {\n if (this.providerVersions.get(versionId)?.agentId === id) {\n this.providerVersions.delete(versionId);\n }\n }\n await this.memory.delete(id);\n }\n\n async list(args?: StorageListAgentsInput): Promise<StorageListAgentsOutput> {\n this.refreshKnownAgentIds();\n await this.discoverProviderAgentIds();\n await Promise.all([...this.knownAgentIds].map(agentId => this.hydrateAgent(agentId)));\n return this.memory.list(args);\n }\n\n async createVersion(input: CreateVersionInput): Promise<AgentVersion> {\n await this.hydrateAgent(input.agentId);\n const existingVersion = await this.memory.getVersion(input.id);\n if (existingVersion) {\n throw new Error(`Version with id ${input.id} already exists`);\n }\n const existingVersionNumber = await this.memory.getVersionByNumber(input.agentId, input.versionNumber);\n if (existingVersionNumber) {\n throw new Error(`Version number ${input.versionNumber} already exists for agent ${input.agentId}`);\n }\n\n const snapshot = snapshotFromVersion({ ...input, createdAt: new Date() } as AgentVersion);\n const result = await this.persistSnapshot(input.agentId, snapshot, input.changeMessage);\n const version = await this.memory.createVersion(input);\n this.rememberProviderVersion(input.agentId, version, result);\n return version;\n }\n\n async getVersion(id: string): Promise<AgentVersion | null> {\n const providerVersion = this.providerVersions.get(id);\n if (providerVersion) {\n return structuredClone(providerVersion);\n }\n return this.memory.getVersion(id);\n }\n\n async getVersionByNumber(agentId: string, versionNumber: number): Promise<AgentVersion | null> {\n await this.loadHistory(agentId);\n const providerVersion = [...this.providerVersions.values()].find(\n version => version.agentId === agentId && version.versionNumber === versionNumber,\n );\n if (providerVersion) {\n return structuredClone(providerVersion);\n }\n return this.memory.getVersionByNumber(agentId, versionNumber);\n }\n\n async getLatestVersion(agentId: string): Promise<AgentVersion | null> {\n await this.loadHistory(agentId);\n const providerLatest = [...this.providerVersions.values()]\n .filter(version => version.agentId === agentId)\n .sort((a, b) => b.versionNumber - a.versionNumber)[0];\n if (providerLatest) {\n return structuredClone(providerLatest);\n }\n return this.memory.getLatestVersion(agentId);\n }\n\n async listVersions(input: ListVersionsInput): Promise<ListVersionsOutput> {\n await this.loadHistory(input.agentId);\n const providerVersions = [...this.providerVersions.values()].filter(version => version.agentId === input.agentId);\n if (providerVersions.length === 0) {\n return this.memory.listVersions(input);\n }\n\n const { page = 0, perPage: perPageInput, orderBy } = input;\n const { field, direction } = this.parseVersionOrderBy(orderBy);\n const perPage = normalizePerPage(perPageInput, 20);\n const sortedVersions = this.sortVersions(providerVersions, field, direction).map(version =>\n structuredClone(version),\n );\n const total = sortedVersions.length;\n const { offset, perPage: perPageForResponse } = calculatePagination(page, perPageInput, perPage);\n\n return {\n versions: sortedVersions.slice(offset, offset + perPage),\n total,\n page,\n perPage: perPageForResponse,\n hasMore: offset + perPage < total,\n };\n }\n\n async deleteVersion(id: string): Promise<void> {\n this.providerVersions.delete(id);\n await this.memory.deleteVersion(id);\n }\n\n async deleteVersionsByParentId(entityId: string): Promise<void> {\n for (const [versionId, version] of this.providerVersions.entries()) {\n if (version.agentId === entityId) {\n this.providerVersions.delete(versionId);\n }\n }\n await this.memory.deleteVersionsByParentId(entityId);\n }\n\n async countVersions(entityId: string): Promise<number> {\n await this.loadHistory(entityId);\n const providerCount = [...this.providerVersions.values()].filter(version => version.agentId === entityId).length;\n return providerCount || this.memory.countVersions(entityId);\n }\n\n private refreshKnownAgentIds(): void {\n const agents = this.storageMastra?.listAgents?.();\n if (!agents) return;\n for (const agent of Object.values(agents)) {\n if (agent.source === 'code') {\n this.knownAgentIds.add(agent.id);\n }\n }\n }\n\n private async discoverProviderAgentIds(): Promise<void> {\n if (this.providerAgentIdsDiscovered || !this.provider.listFiles) return;\n\n const files = await this.provider.listFiles({ path: SOURCE_CONTROL_AGENTS_DIR });\n for (const file of files) {\n const agentId = agentIdFromSourcePath(file.path);\n if (agentId) {\n this.knownAgentIds.add(agentId);\n }\n }\n this.providerAgentIdsDiscovered = true;\n }\n\n private async hydrateAgent(agentId: string): Promise<void> {\n if (this.hydratedAgents.has(agentId)) return;\n\n const ref = this.activeRefs.get(agentId);\n const file = await this.provider.readFile({ path: getSourceAgentFilePath(agentId), ref });\n if (!file) {\n this.hydratedAgents.add(agentId);\n return;\n }\n\n const snapshot = parseJsonObject(file.content);\n if (!snapshot) {\n this.hydratedAgents.add(agentId);\n return;\n }\n\n this.knownAgentIds.add(agentId);\n this.hydratedAgents.add(agentId);\n const now = new Date();\n const versionId = `hydrated-${agentId}-v1`;\n this.db.agents.set(agentId, {\n id: agentId,\n status: 'published',\n activeVersionId: versionId,\n favoriteCount: 0,\n createdAt: now,\n updatedAt: now,\n });\n this.db.agentVersions.set(versionId, {\n id: versionId,\n agentId,\n versionNumber: 1,\n ...snapshot,\n createdAt: now,\n } as AgentVersion);\n }\n\n private getCodeDefinedAgent(agentId: string): { source?: string; __getEditorConfig?: () => unknown } | undefined {\n try {\n const agent = this.storageMastra?.getAgentById?.(agentId) as\n | { source?: string; __getEditorConfig?: () => unknown }\n | undefined;\n return agent?.source === 'code' ? agent : undefined;\n } catch {\n return undefined;\n }\n }\n\n private async persistSnapshot(\n agentId: string,\n snapshot: Record<string, unknown>,\n message?: string,\n ): Promise<SourceWriteResult> {\n const capabilities = await this.provider.getCapabilities();\n if (!capabilities.canWrite) {\n throw new Error(capabilities.reason ?? `Source provider ${this.provider.displayName} cannot write files`);\n }\n const agent = this.getCodeDefinedAgent(agentId);\n const filtered = filterSourceSnapshot(snapshot, agent?.__getEditorConfig?.(), Boolean(agent));\n return this.provider.writeFile({\n path: getSourceAgentFilePath(agentId),\n ref: this.activeRefs.get(agentId),\n content: `${stableStringify(filtered)}\\n`,\n message,\n });\n }\n\n private async loadHistory(agentId: string): Promise<void> {\n if (this.loadedHistory.has(agentId)) return;\n\n const capabilities = await this.provider.getCapabilities();\n if (!capabilities.canListHistory) {\n this.loadedHistory.add(agentId);\n return;\n }\n\n const activeRef = this.activeRefs.get(agentId);\n const entries = await this.provider.listFileHistory({ path: getSourceAgentFilePath(agentId), ref: activeRef });\n const orderedEntries = [...entries].reverse();\n const versions = new Map<string, AgentVersion>();\n let versionNumber = 0;\n for (const entry of orderedEntries) {\n const file = await this.provider.readFile({ path: getSourceAgentFilePath(agentId), ref: entry.ref ?? entry.id });\n if (!file) continue;\n const snapshot = parseJsonObject(file.content);\n if (!snapshot) continue;\n versionNumber += 1;\n const version = this.versionFromHistoryEntry(agentId, entry, versionNumber, snapshot);\n versions.set(version.id, version);\n }\n for (const [versionId, version] of versions) {\n this.providerVersions.set(versionId, version);\n }\n this.loadedHistory.add(agentId);\n }\n\n private rememberProviderVersion(agentId: string, version: AgentVersion, result: SourceWriteResult): void {\n const versionId = result.commitSha ? `${SOURCE_VERSION_PREFIX}${result.commitSha}:${agentId}` : version.id;\n this.providerVersions.set(versionId, {\n ...structuredClone(version),\n id: versionId,\n ag