UNPKG

@mastra/core

Version:
1,337 lines (1,336 loc) 151 kB
import { n as LogLevel, t as ConsoleLogger } from "./logger-B_aQzjbm.js"; import { t as InMemoryServerCache } from "./inmemory-DHuA6Z20.js"; import { i as MastraError, n as ErrorDomain, t as ErrorCategory } from "./error-MjDSls8S.js"; import { t as EventEmitterPubSub } from "./event-emitter-C12mi0dL.js"; import { c as noOpMetricsContext, o as NoOpObservability, s as noOpLoggerContext } from "./observability-Cz-X7NF_.js"; import { c as defaultGateways } from "./llm-DntEbB3j.js"; import { o as BackgroundTaskManager } from "./background-tasks-6lJjk3_t.js"; import { _ as readPositiveIntEnv } from "./utils-CCbB2dG1.js"; import { Gt as createOnScorerHook, I as createRunScope, Kn as isDurableAgentLike, L as AgentChannels, an as createWorkflow, c as __registerMastraCtor, ft as augmentWithInit, in as createStep } from "./agent-Dj30gJa3.js"; import { r as normalizeToolPayloadTransformPolicy } from "./payload-transform-C4k4-WlM.js"; import { n as getGatewayId } from "./gateway-helpers-DusR3xFY.js"; import { t as Schedules } from "./schedules-vpzpiSqm.js"; import { t as computeNextFireAt } from "./cron-B2j814dd.js"; import { DualLogger, noopLogger } from "./logger/index.js"; import { t as WorkflowEventProcessor } from "./workflow-event-processor-BbED1LMn.js"; import { a as registerHook, r as deregisterHook } from "./hooks-s8qUTtbg.js"; import { H as BackgroundTasksInMemory, l as WorkflowsInMemory, s as InMemoryStore } from "./storage-BS3ic0Sd.js"; import { r as InMemoryDB } from "./source-xpSE-8BU.js"; import { h as agentThreadStreamRuntime, r as dispatchDueNotifications } from "./storage-B1u4gxRl.js"; import { t as createDurableAgent } from "./create-durable-agent-CmEJXplU.js"; import { t as DatasetsManager } from "./manager-CosKYh4m.js"; import { LicenseClient } from "./license/index.js"; import { initContextStorage } from "./observability/context-storage.js"; import { r as isToolLoopAgentLike, t as toolLoopAgentToMastraAgent } from "./tool-loop-agent-bRjdvkk2.js"; import { i as OrchestrationWorker, n as BackgroundTaskWorker, r as SchedulerWorker } from "./worker-Cld7uOHj.js"; import { normalizeWorkflowBuilderDefinition } from "./workflows/builder/index.js"; import { m as toJsonSchemaOrUndefined, s as collectNestedWorkflowIds, t as assertValidStoredWorkflow, u as rehydrateWorkflow } from "./validate-BWdo4oEz.js"; import { randomUUID } from "crypto"; import { z } from "zod/v4"; //#region src/notifications/workflow.ts const NOTIFICATION_DISPATCH_WORKFLOW_ID = "__mastra_notification_dispatcher"; /** * Schedule row id for the lazily-created dispatcher schedule. Deliberately * NOT `wf_`-prefixed: `registerDeclarativeSchedules` orphan-cleanup deletes * `wf_`-prefixed rows that are no longer declared in code, and this row is * created imperatively (like heartbeat rows) on first deferred notification. */ const NOTIFICATION_DISPATCH_SCHEDULE_ROW_ID = "__mastra_notification_dispatch"; const NOTIFICATION_DISPATCH_DEFAULT_CRON = "*/1 * * * *"; function parseNotificationDispatchNow(input) { const now = input ? new Date(input) : /* @__PURE__ */ new Date(); if (Number.isNaN(now.getTime())) throw new Error(`Invalid notification dispatch time: ${input}`); return now; } /** * Builds the imperative schedule row that drives the notification dispatcher. * Created lazily by `Mastra.__ensureNotificationDispatchReady()` on the first * deferred notification, rather than declared on the workflow, so idle apps * never start the scheduler. */ function buildNotificationDispatchSchedule({ cron = NOTIFICATION_DISPATCH_DEFAULT_CRON, batchSize = 100 } = {}) { const now = Date.now(); return { id: NOTIFICATION_DISPATCH_SCHEDULE_ROW_ID, target: { type: "workflow", workflowId: NOTIFICATION_DISPATCH_WORKFLOW_ID, inputData: { limit: batchSize } }, cron, status: "active", nextFireAt: computeNextFireAt(cron, { after: now }), createdAt: now, updatedAt: now, metadata: { internal: true, feature: "notifications" } }; } function createNotificationDispatchWorkflow({ batchSize = 100 } = {}) { const dispatchStep = createStep({ id: "dispatch-due-notifications", inputSchema: z.object({ now: z.string().optional(), limit: z.number().optional() }), outputSchema: z.object({ delivered: z.number(), failed: z.number() }), execute: async ({ inputData, mastra }) => { const storage = await mastra.getStorage()?.getStore("notifications"); if (!storage) return { delivered: 0, failed: 0 }; const result = await dispatchDueNotifications({ mastra, storage, now: parseNotificationDispatchNow(inputData.now), limit: inputData.limit ?? batchSize }); return { delivered: result.delivered.length, failed: result.failed.length }; } }); return createWorkflow({ id: NOTIFICATION_DISPATCH_WORKFLOW_ID, inputSchema: z.object({ now: z.string().optional(), limit: z.number().optional() }), outputSchema: z.object({ delivered: z.number(), failed: z.number() }) }).then(dispatchStep).commit(); } //#endregion //#region src/mastra/index.ts /** * Creates an error for when a null/undefined value is passed to an add* method. * This commonly occurs when config is spread ({ ...config }) and the original * object had getters or non-enumerable properties. */ function createUndefinedPrimitiveError(type, value, key) { const typeLabel = type === "mcp-server" ? "MCP server" : type; return new MastraError({ id: `MASTRA_ADD_${type.toUpperCase().replace("-", "_")}_UNDEFINED`, domain: ErrorDomain.MASTRA, category: ErrorCategory.USER, text: `Cannot add ${typeLabel}: ${typeLabel} is ${value === null ? "null" : "undefined"}. This may occur if config was spread ({ ...config }) and the original object had getters or non-enumerable properties.`, details: { status: 400, ...key && { key } } }); } /** * Stable JSON-shape comparison for two `Schedule.target` values. Uses * JSON.stringify because targets are plain JSON-serializable objects (the * storage layer round-trips them through the same encoding). Covers the * `inputData` / `initialState` / `requestContext` payload fields that we * want to detect changes on across redeploys. */ function targetsEqual(a, b) { if (a === b) return true; if (!a) return false; return JSON.stringify(a) === JSON.stringify(b); } /** * Reads the declarative schedule configs off a workflow. Supports both the * new `getScheduleConfigs(): WorkflowScheduleConfig[]` accessor on the evented * engine and a legacy `getScheduleConfig(): WorkflowScheduleConfig | undefined` * fallback used in tests that inject a fake getter. */ function collectWorkflowScheduleConfigs(workflow) { const w = workflow; if (typeof w.getScheduleConfigs === "function") return w.getScheduleConfigs() ?? []; if (typeof w.getScheduleConfig === "function") { const cfg = w.getScheduleConfig(); if (!cfg) return []; return Array.isArray(cfg) ? cfg : [cfg]; } return []; } /** * Builds the storage row id for a declarative schedule. Workflow and schedule * ids are URL-encoded so delimiters in user-supplied ids cannot collide * across workflows (e.g. `foo__bar` single vs `foo` array-entry `bar`). */ function declarativeScheduleRowId(workflowId, scheduleId) { const encodedWorkflow = encodeURIComponent(workflowId); if (scheduleId === void 0) return `wf_${encodedWorkflow}`; return `wf_${encodedWorkflow}__${encodeURIComponent(scheduleId)}`; } /** * Determines whether a stored schedule row id belongs to one of the registered * workflows. Returns the owning workflow id when the row id either equals * `wf_<encoded(workflowId)>` (single-schedule form) or starts with * `wf_<encoded(workflowId)>__` (array form). Returns undefined when no * registered workflow owns the row. */ function ownerWorkflowIdForRow(rowId, byWorkflow) { for (const workflowId of byWorkflow.keys()) { const prefix = `wf_${encodeURIComponent(workflowId)}`; if (rowId === prefix || rowId.startsWith(`${prefix}__`)) return workflowId; } } /** * Decodes the owning workflow id directly from a `wf_<encoded>` / * `wf_<encoded>__<...>` row id without needing the workflow to be in the * current registry. Used to identify rows whose workflow has been deleted * from code so we can clean them up on startup. */ function ownerWorkflowIdFromRowId(rowId) { if (!rowId.startsWith("wf_")) return void 0; const rest = rowId.slice(3); const sep = rest.indexOf("__"); const encoded = sep === -1 ? rest : rest.slice(0, sep); if (!encoded) return void 0; try { return decodeURIComponent(encoded); } catch { return; } } /** See {@link targetsEqual}. Same approach for free-form metadata. */ function metadataEqual(a, b) { const aNorm = a ?? void 0; const bNorm = b ?? void 0; if (aNorm === bNorm) return true; if (!aNorm || !bNorm) return false; return JSON.stringify(aNorm) === JSON.stringify(bNorm); } /** * The central orchestrator for Mastra applications, managing agents, workflows, storage, logging, observability, and more. * * The `Mastra` class serves as the main entry point and registry for all components in a Mastra application. * It coordinates the interaction between agents, workflows, storage systems, and other services. * @template TAgents - Record of agent instances keyed by their names * @template TWorkflows - Record of modern workflow instances * @template TVectors - Record of vector store instances for semantic search and RAG * @template TTTS - Record of text-to-speech provider instances * @template TLogger - Logger implementation type for application logging * @template TVNextNetworks - Record of next-generation agent network instances * @template TMCPServers - Record of Model Context Protocol server instances * @template TScorers - Record of evaluation scorer instances for measuring AI performance * * @example * ```typescript * const mastra = new Mastra({ * agents: { * weatherAgent: new Agent({ * id: 'weather-agent', * name: 'Weather Agent', * instructions: 'You provide weather information', * model: 'openai/gpt-5', * tools: [getWeatherTool] * }) * }, * workflows: { dataWorkflow }, * storage: new LibSQLStore({ id: 'mastra-storage', url: ':memory:' }), * logger: new PinoLogger({ name: 'MyApp' }) * }); * ``` */ var Mastra = class Mastra { #vectors; #agents; #logger; #loggerExplicit = false; #workflows; #harnesses = {}; #hiddenWorkflowKeys = /* @__PURE__ */ new Set(); #observability; #observabilityExplicit = false; #onScorerHook; #tts; #deployer; #serverMiddleware = []; #storage; #storageExplicit = false; #storageFallbackWarningPending = false; #recoveryConfig = { durableAgents: "off" }; #scorers; #tools; #processors; #processorConfigurations = /* @__PURE__ */ new Map(); #memory; #workspace; #workspaces = {}; #server; #serverExplicit = false; #studio; #studioExplicit = false; #serverAdapter; #mcpServers; #bundler; #idGenerator; #pubsub; #backgroundTaskConfig; #backgroundTaskManager; #schedulerConfig; #notificationDispatchConfig; /** * Tracks whether any registered workflow has declared a `schedule` config. * Used as a fast short-circuit so users without scheduled workflows pay * zero cost beyond a boolean check. */ #hasScheduledWorkflow = false; #gateways; #channels; #schedules; #schedulesConfig; #environment; #toolPayloadTransform; #workers = []; #workerFilter; /** * Set when the user (or `MASTRA_WORKERS=false`) explicitly disabled all event * processing in this instance via `workers: false`. Gates lazy scheduler / * agent-schedule worker injection so runtime triggers (e.g. * `schedules.create()`) don't resurrect workers the user opted out of. */ #workersDisabled = false; /** * Tracks whether `startWorkers()` has already run. Used to decide whether * lazy scheduler injection (e.g. from `mastra.schedules.create()` after boot) * needs to also `init`/`start` the worker, or whether the normal * `startWorkers()` path will pick it up. */ #workersStarted = false; /** * Set when something has signalled that the scheduler is needed at runtime * (e.g. an agent schedule was registered via `__ensureScheduleRuntimeReady()`). * Causes `#shouldEnableScheduler()` to return `true` even when there are no * declarative scheduled workflows, unless the user explicitly set * `scheduler: { enabled: false }`. */ #schedulerRequested = false; /** * Set once `__ensureNotificationDispatchReady()` has upserted the dispatcher * schedule row and requested the scheduler. Makes repeated deferred * notification creates free after the first one. */ #notificationDispatchReady = false; /** * In-flight promise for `#ensureSchedulingWorkersStarted()`. Serializes * concurrent startup requests so two callers can't both pass the * worker-existence checks and double-subscribe to the scheduling topics. */ #schedulingWorkersStartPromise; /** * In-flight promise for `__ensureExecutionWorkersStarted()`. Serializes * concurrent lazy startups triggered by background-task dispatches so two * first dispatches on a cold instance can't both init/start the same * workers. */ #executionWorkersStartPromise; /** * Fast path for `__ensureExecutionWorkersStarted()`. Set once the execution * workers + push wiring are confirmed running; reset by `stopWorkers()`. * Kept separate from `#workersStarted`, which partial `startWorkers(name)` * calls also set without starting the workflow consumer. */ #executionWorkersStarted = false; #workflowEventProcessor; #pushSubscription; #userEventSubscriptions = []; #events = {}; #internalMastraWorkflows = {}; #runScopedWorkflowTimestamps = /* @__PURE__ */ new Map(); #runScopes = /* @__PURE__ */ new Map(); #runScopeRefcounts = /* @__PURE__ */ new Map(); static INTERNAL_WORKFLOW_TTL_MS = readPositiveIntEnv("MASTRA_SUSPENDED_RUN_TTL_MS", 1800 * 1e3); #runTracingContexts = /* @__PURE__ */ new Map(); #serverCache; #storedAgentsCache = /* @__PURE__ */ new Map(); #storedScorersCache = /* @__PURE__ */ new Map(); #promptBlocks = {}; #editor; #datasets; #versions; #pubsubProxy; get pubsub() { if (!this.#pubsubProxy) { const raw = this.#pubsub; const self = this; this.#pubsubProxy = new Proxy(raw, { get(target, prop, _receiver) { if (prop === "publish") return function publish(topic, event) { if (topic === "workflows" || topic === "workflows-finish") { const data = event.data; const wfId = data?.workflowId; const rId = data?.runId; if ((() => { if (wfId && rId && self.__hasInternalWorkflow(wfId, rId)) return true; let parent = data?.parentWorkflow; let depth = 0; while (parent && depth < 16) { const pwfId = parent.workflowId; const prId = parent.runId; if (pwfId && prId && self.__hasInternalWorkflow(pwfId, prId)) return true; parent = parent.parentWorkflow; depth++; } if (rId && rId.startsWith("sched_wf_")) return true; if (rId && rId.startsWith(`sched___mastra_notification_dispatch_`)) return true; return false; })()) return target.publish(topic, event, { localOnly: true }); } else if (topic.startsWith("workflow.events.v2.")) return target.publish(topic, event, { localOnly: true }); return target.publish(topic, event); }; const val = Reflect.get(target, prop, target); if (typeof val === "function") return val.bind(target); return val; } }); } return this.#pubsubProxy; } get agentThreadStreamRuntime() { return agentThreadStreamRuntime; } get workers() { return this.#workers; } getWorker(name) { return this.#workers.find((w) => w.name === name); } get backgroundTaskManager() { return this.#backgroundTaskManager; } /** * Returns the workflow scheduler owned by the SchedulerWorker, * or undefined if the scheduler is not enabled / not yet started. * * The scheduler is created when `startWorkers()` initializes the * SchedulerWorker (guarded by `#shouldEnableScheduler()`). * * This is runtime plumbing (the cron tick loop). To create, list, pause, * resume, or delete schedules use `mastra.schedules` instead. * * @internal */ get scheduler() { return this.#findSchedulerWorker()?.scheduler; } get datasets() { if (!this.#datasets) this.#datasets = new DatasetsManager(this); return this.#datasets; } /** * Gets the currently configured ID generator function. * * @example * ```typescript * const mastra = new Mastra({ * idGenerator: context => * context?.idType === 'message' && context.threadId * ? `msg-${context.threadId}-${Date.now()}` * : `custom-${Date.now()}` * }); * const generator = mastra.getIdGenerator(); * console.log(generator?.({ idType: 'message', threadId: 'thread-123' })); // \"msg-thread-123-1234567890\" * ``` */ getIdGenerator() { return this.#idGenerator; } /** * Gets the currently configured editor instance. * The editor is responsible for handling agent instantiation and configuration. * * @example * ```typescript * const mastra = new Mastra({ * editor: new MastraEditor({ logger }) * }); * const editor = mastra.getEditor(); * ``` */ getEditor() { return this.#editor; } /** * Gets a registered channel provider by its key. * * @example * ```typescript * import { SlackProvider } from '@mastra/slack'; * const slack = mastra.getChannelProvider<SlackProvider>('slack'); * ``` */ getChannelProvider(key) { return this.#channels?.[key]; } /** * Gets all registered channel providers. */ getChannelProviders() { return this.#channels; } /** * Shorthand getter for platform channels. * Usage: `mastra.channels.slack.connect(agentId)` */ get channels() { return this.#channels ?? {}; } /** * Canonical entrypoint for schedules — recurring agent or workflow runs * persisted as schedule rows discriminated by `target.type` (`'agent'` or * `'workflow'`). Use to create, list, update, pause/resume, manually fire, * or inspect trigger history for schedules across any agent or workflow. * * Lazily constructed. Operates against `getStorage()?.getStore('schedules')`. * * @example * ```ts * const schedule = await mastra.schedules.create({ * agentId: 'pinger', * name: 'morning-checkin', * cron: '0 9 * * *', * prompt: 'good morning, anything to report?', * threadId: 't1', * resourceId: 'u1', * }); * await mastra.schedules.list({ agentId: 'pinger' }); * ``` */ get schedules() { this.#schedules ??= new Schedules(this); return this.#schedules; } /** * Returns the schedule lifecycle hook bundle configured via * `new Mastra({ schedules: { ... } })`, if any. A single bundle runs for * every agent-schedule fire; hooks branch per agent via the `agentId` on * each context. Internal: consumed by the {@link AgentScheduleWorker} to * invoke `prepare`, `onFinish`, `onError`, and `onAbort` around * schedule-driven runs. * * @internal */ __getScheduleHooks() { return this.#schedulesConfig; } /** * Returns the global version overrides configured on this Mastra instance. * These are used as defaults when resolving sub-agent versions during delegation. */ getVersionOverrides() { return this.#versions; } /** * Returns the deployment environment name configured on this Mastra instance, * falling back to `process.env.NODE_ENV` when unset, or `undefined` if neither * is provided. * * Observability automatically reads this and attaches it to all signals so * consumers can filter by environment without passing * `tracingOptions.metadata.environment` on each call. */ getEnvironment() { return this.#environment; } getToolPayloadTransform() { return this.#toolPayloadTransform; } /** * Gets the stored agents cache * @internal */ getStoredAgentCache() { return this.#storedAgentsCache; } /** * Gets the stored scorers cache * @internal */ getStoredScorerCache() { return this.#storedScorersCache; } /** * Generates a unique identifier using the configured generator or defaults to `crypto.randomUUID()`. * * This method is used internally by Mastra for creating unique IDs for various entities * like workflow runs, agent conversations, and other resources that need unique identification. * * @param context - Optional context information about what type of ID is being generated * and where it's being requested from. This allows custom ID generators * to create deterministic IDs based on context. * * @throws {MastraError} When the custom ID generator returns an empty string * * @example * ```typescript * const mastra = new Mastra(); * const id = mastra.generateId(); * console.log(id); // "550e8400-e29b-41d4-a716-446655440000" * * // With context for deterministic IDs * const messageId = mastra.generateId({ * idType: 'message', * source: 'agent', * threadId: 'thread-123' * }); * ``` */ generateId(context) { if (this.#idGenerator) { const id = this.#idGenerator(context); if (!id) { const error = new MastraError({ id: "MASTRA_ID_GENERATOR_RETURNED_EMPTY_STRING", domain: ErrorDomain.MASTRA, category: ErrorCategory.USER, text: "ID generator returned an empty string, which is not allowed" }); this.#logger?.trackException(error); throw error; } return id; } return randomUUID(); } /** * Sets a custom ID generator function for creating unique identifiers. * * The ID generator function will be used by `generateId()` instead of the default * `crypto.randomUUID()`. This is useful for creating application-specific ID formats * or integrating with existing ID generation systems. The function receives * optional context about what is requesting the ID. * * @example * ```typescript * const mastra = new Mastra(); * mastra.setIdGenerator(context => * context?.idType === 'run' && context.entityId * ? `run-${context.entityId}-${Date.now()}` * : `custom-${Date.now()}` * ); * const id = mastra.generateId({ idType: 'run', entityId: 'agent-123' }); * console.log(id); // "run-agent-123-1234567890" * ``` */ setIdGenerator(idGenerator) { this.#idGenerator = idGenerator; } /** * Sets the server configuration for this Mastra instance. * * @param server - The server configuration object * * @example * ```typescript * mastra.setServer({ ...mastra.getServer(), auth: new MastraAuthWorkos() }); * ``` */ setServer(server) { this.#server = server; } /** * Sets the studio configuration for this Mastra instance. * * The studio configuration controls authentication and authorization for Studio UI, * separate from the server configuration. This enables dual auth patterns where * Studio users (e.g., internal team) use different auth than API consumers. * * @param studio - The studio configuration object * * @example * ```typescript * // Set studio auth separately from server auth * mastra.setStudio({ * auth: new MastraAuthStudio(), * rbac: new MastraRBACStudio({ roleMapping: { admin: ['*'] } }), * }); * ``` */ setStudio(studio) { this.#studio = studio; } /** * Registers an exporter on the default observability instance. * * If the current observability is a no-op (user didn't configure any), it is * first replaced with the provided entrypoint and the instance is registered * as default. If a real observability entrypoint already exists, the exporter * is added directly to the existing default instance. * * @param exporter - The exporter to register (e.g. a MastraPlatformExporter) * @param instance - An ObservabilityInstance pre-configured with the exporter, used as default when bootstrapping * @param entrypoint - A real ObservabilityEntrypoint to bootstrap if the current one is a no-op */ registerExporter(exporter, instance, entrypoint) { if (this.#observability instanceof NoOpObservability) { this.#observability = entrypoint; this.#observability.setLogger({ logger: this.#logger }); this.#observability.setMastraContext({ mastra: this }); this.#observability.registerInstance("default", instance, true); } const defaultInstance = this.#observability.getDefaultInstance(); if (defaultInstance?.registerExporter) defaultInstance.registerExporter(exporter); } /** * Creates a new Mastra instance with the provided configuration. * * The constructor initializes all the components specified in the config, sets up * internal systems like logging and observability, and registers components with each other. * * @example * ```typescript * const mastra = new Mastra({ * agents: { * assistant: new Agent({ * id: 'assistant', * name: 'Assistant', * instructions: 'You are a helpful assistant', * model: 'openai/gpt-5' * }) * }, * storage: new PostgresStore({ * connectionString: process.env.DATABASE_URL * }), * logger: new PinoLogger({ name: 'MyApp' }), * observability: new Observability({ * configs: { default: { serviceName: 'mastra', exporters: [new MastraStorageExporter()] } }, * }), * }); * ``` */ constructor(config) { initContextStorage(); this.#serverCache = config?.cache ?? new InMemoryServerCache(); this.#recoveryConfig = { durableAgents: config?.recovery?.durableAgents ?? "off" }; this.#editor = config?.editor; this.#versions = config?.versions; this.#environment = config?.environment ?? process.env.NODE_ENV; this.#toolPayloadTransform = normalizeToolPayloadTransformPolicy(config?.transform ?? config?.toolPayloadProjection); if (config?.pubsub) this.#pubsub = config.pubsub; else this.#pubsub = new EventEmitterPubSub(); this.#events = {}; for (const topic in config?.events ?? {}) if (!Array.isArray(config?.events?.[topic])) this.#events[topic] = [config?.events?.[topic]]; else this.#events[topic] = config?.events?.[topic] ?? []; const rawWorkersEnv = process.env.MASTRA_WORKERS; let workersOption; if (rawWorkersEnv === "false") workersOption = false; else { workersOption = config?.workers; if (rawWorkersEnv && rawWorkersEnv !== "false") { const names = rawWorkersEnv.split(",").map((s) => s.trim()).filter(Boolean); if (names.length > 0) this.#workerFilter = new Set(names); } } if (workersOption === false) this.#workersDisabled = true; else { const pubsubModes = this.#pubsub.supportedModes ?? ["pull"]; const defaultWorkers = []; if (pubsubModes.includes("pull")) defaultWorkers.push(new OrchestrationWorker()); if (config?.backgroundTasks?.enabled) defaultWorkers.push(new BackgroundTaskWorker(config.backgroundTasks)); const customWorkers = workersOption ?? []; const customNames = /* @__PURE__ */ new Set(); for (const w of customWorkers) { if (customNames.has(w.name)) throw new Error(`Duplicate worker name "${w.name}" in the 'workers' option`); customNames.add(w.name); } this.#workers = [...defaultWorkers.filter((w) => !customNames.has(w.name)), ...customWorkers]; for (const w of this.#workers) w.__registerMastra(this); } let logger; if (config?.logger === false) { logger = noopLogger; this.#loggerExplicit = true; } else if (config?.logger) { logger = config.logger; this.#loggerExplicit = true; } else logger = new ConsoleLogger({ name: "Mastra", level: process.env.NODE_ENV === "production" && process.env.MASTRA_DEV !== "true" ? LogLevel.WARN : LogLevel.INFO }); this.#logger = logger; this.#idGenerator = config?.idGenerator; let storage; if (config?.storage) { storage = config.storage; this.#storageExplicit = true; } else { storage = new InMemoryStore(); this.#storageFallbackWarningPending = true; queueMicrotask(() => { if (!this.#storageFallbackWarningPending) return; this.#storageFallbackWarningPending = false; this.#logger?.warn("No `storage` configured on Mastra — falling back to an in-memory store. In-memory storage is not durable: all data is lost on restart, and it is not safe for production. Configure a persistent storage adapter (e.g. @mastra/libsql, @mastra/pg, @mastra/cloudflare)."); }); } storage = augmentWithInit(storage); if (storage.stores) { if (!storage.stores.workflows || !storage.stores.backgroundTasks) { const fallbackDb = new InMemoryDB(); if (!storage.stores.workflows) storage.stores.workflows = new WorkflowsInMemory({ db: fallbackDb }); if (!storage.stores.backgroundTasks) storage.stores.backgroundTasks = new BackgroundTasksInMemory({ db: fallbackDb }); } } if (config?.observability) { this.#observabilityExplicit = true; if (typeof config.observability.getDefaultInstance === "function") { this.#observability = config.observability; this.#observability.setLogger({ logger: this.#logger }); } else { this.#logger?.warn("Observability configuration error: Expected an Observability instance, but received a config object. Import and instantiate: import { Observability, MastraStorageExporter } from \"@mastra/observability\"; then pass: observability: new Observability({ configs: { default: { serviceName: \"mastra\", exporters: [new MastraStorageExporter()] } } }). Observability has been disabled."); this.#observability = new NoOpObservability(); } } else this.#observability = new NoOpObservability(); const dualLogger = new DualLogger(this.#logger, () => this.loggerVNext); this.#logger = dualLogger; this.#storage = storage; storage?.__registerMastra?.(this); if (this.#editor && typeof this.#editor.registerWithMastra === "function") this.#editor.registerWithMastra(this); if (process.env.MASTRA_LICENSE_KEY || process.env.MASTRA_EE_LICENSE) LicenseClient.getInstance(this.#logger).validate().catch(() => {}); this.#backgroundTaskConfig = config?.backgroundTasks; const bgWorkerFiltered = this.#workerFilter && !this.#workerFilter.has("backgroundTasks"); this.#ensureBackgroundTaskManager(workersOption === false || bgWorkerFiltered ? "producer" : void 0); this.#schedulerConfig = config?.scheduler; this.#notificationDispatchConfig = config?.notifications?.dispatch; this.#schedulesConfig = config?.schedules; this.#vectors = {}; this.#mcpServers = {}; this.#tts = {}; this.#agents = {}; this.#scorers = {}; this.#tools = {}; this.#processors = {}; this.#memory = {}; this.#workflows = {}; this.#gateways = {}; if (config?.tools) Object.entries(config.tools).forEach(([key, tool]) => { if (tool != null) this.addTool(tool, key); }); if (config?.processors) Object.entries(config.processors).forEach(([key, processor]) => { if (processor != null) this.addProcessor(processor, key); }); if (config?.memory) Object.entries(config.memory).forEach(([key, memory]) => { if (memory != null) this.addMemory(memory, key); }); if (config?.vectors) Object.entries(config.vectors).forEach(([key, vector]) => { if (vector != null) this.addVector(vector, key); }); if (config?.workspace) { this.#workspace = config.workspace; this.addWorkspace(config.workspace, void 0, { source: "mastra" }); } if (config?.scorers) Object.entries(config.scorers).forEach(([key, scorer]) => { if (scorer != null) this.addScorer(scorer, key, { source: "code" }); }); if (this.#notificationDispatchConfig?.enabled !== false) { const workflow = createNotificationDispatchWorkflow(this.#notificationDispatchConfig); this.addWorkflow(workflow, workflow.id); this.#hiddenWorkflowKeys.add(workflow.id); } if (config?.workflows) Object.entries(config.workflows).forEach(([key, workflow]) => { if (workflow != null) this.addWorkflow(workflow, key); }); if (config?.gateways) Object.entries(config.gateways).forEach(([key, gateway]) => { if (gateway != null) this.addGateway(gateway, key); }); for (const gateway of defaultGateways) { const key = getGatewayId(gateway); if (!Object.values(this.#gateways).some((existingGateway) => existingGateway != null && getGatewayId(existingGateway) === key)) this.#gateways[key] = gateway; } if (config?.mcpServers) Object.entries(config.mcpServers).forEach(([key, server]) => { if (server != null) this.addMCPServer(server, key); }); if (config?.tts) Object.entries(config.tts).forEach(([key, tts]) => { if (tts != null) this.#tts[key] = tts; }); if (config?.server) { this.#server = config.server; this.#serverExplicit = true; } if (config?.studio) { this.#studio = config.studio; this.#studioExplicit = true; } if (config?.channels) { this.#channels = config.channels; const channelRoutes = []; for (const [, channel] of Object.entries(config.channels)) { if (channel == null) continue; if (channel.__attach) channel.__attach(this); const routes = channel.getRoutes(); channelRoutes.push(...routes); } if (channelRoutes.length > 0) { const existingRoutes = this.#server?.apiRoutes ?? []; this.#server = { ...this.#server, apiRoutes: [...existingRoutes, ...channelRoutes] }; } } if (config?.agents) Object.entries(config.agents).forEach(([key, agent]) => { if (agent != null) this.addAgent(agent, key); }); const agentControllerEntries = { ...config?.harnesses ?? {}, ...config?.agentControllers ?? {} }; for (const [key, agentController] of Object.entries(agentControllerEntries)) { this.#harnesses[key] = agentController; agentController.__registerMastra(this); const controllerChannels = agentController.getChannels(); if (controllerChannels) { controllerChannels.__setLogger(this.#logger); const channelRoutes = controllerChannels.getWebhookRoutes(); if (channelRoutes.length > 0) this.#server = { ...this.#server, apiRoutes: [...this.#server?.apiRoutes ?? [], ...channelRoutes] }; controllerChannels.initialize(this).catch((err) => { this.#logger?.error(`Failed to initialize channels for agent controller ${key}:`, err); }); } } if (!config?.__ephemeral) { this.#onScorerHook = createOnScorerHook(this); registerHook("onScorerRun", this.#onScorerHook); } this.#observability.setMastraContext({ mastra: this }); this.setLogger({ logger }); if (this.#channels) Promise.resolve().then(async () => { for (const [key, channel] of Object.entries(this.#channels ?? {})) if (channel.initialize) try { await channel.initialize(); } catch (err) { console.error(`[Mastra] Failed to initialize channel "${key}":`, err); } }); } #ensureBackgroundTaskManager(modeOverride) { if (!this.#backgroundTaskConfig?.enabled || !this.#storage || this.#backgroundTaskManager) return; const effectiveMode = modeOverride ?? (this.#workersDisabled || this.#workerFilter && !this.#workerFilter.has("backgroundTasks") ? "producer" : void 0); const bgManager = new BackgroundTaskManager(effectiveMode ? { ...this.#backgroundTaskConfig, mode: effectiveMode } : this.#backgroundTaskConfig); bgManager.__registerMastra(this); this.#backgroundTaskManager = bgManager; const tools = this.#tools; if (tools) for (const [name, tool] of Object.entries(tools)) this.#registerToolWithBackgroundManager(name, tool); bgManager.init(this.#pubsub).catch((error) => { this.#logger?.error("Failed to initialize background task manager", error); }); } /** * Build a `ToolExecutor` adapter for a Mastra-registered tool and stash it * on the background task manager's static registry. Skipped if the tool has * no `execute` (declarative-only tools, e.g. MCP descriptors). */ #registerToolWithBackgroundManager(name, tool) { if (!this.#backgroundTaskManager) return; if (typeof tool.execute !== "function") return; const execute = tool.execute.bind(tool); this.#backgroundTaskManager.registerStaticExecutor(name, { execute: async (args, options) => { return execute(args, { toolCallId: "", messages: [], abortSignal: options?.abortSignal }); } }); } /** * Returns the flat list of declarative schedules sourced from currently * registered workflows. Single-schedule workflows yield one entry keyed by * `wf_<encoded(workflowId)>`. Array-form workflows yield one entry per array * entry keyed by `wf_<encoded(workflowId)>__<encoded(scheduleId)>` so the * prefix uniquely identifies "all rows owned by this workflow's declarative * config" even when ids contain `__` or other delimiter-like characters. */ #collectDeclarativeSchedules() { const out = []; const workflows = this.#workflows; for (const workflow of Object.values(workflows ?? {})) { const configs = collectWorkflowScheduleConfigs(workflow); if (configs.length === 0) continue; const isArrayForm = configs.length > 1 || configs.length === 1 && configs[0].id !== void 0; for (const cfg of configs) { const scheduleId = isArrayForm ? declarativeScheduleRowId(workflow.id, cfg.id) : declarativeScheduleRowId(workflow.id); out.push({ scheduleId, workflowId: workflow.id, cfg }); } } return out; } #shouldEnableScheduler() { if (this.#workersDisabled) return false; if (this.#schedulerConfig?.enabled === false) return false; if (this.#schedulerConfig?.enabled === true) return true; return this.#hasScheduledWorkflow || this.#schedulerRequested; } /** * Find the SchedulerWorker from the workers list (if present). */ #findSchedulerWorker() { return this.#workers.find((w) => w.name === "scheduler"); } /** * Find the AgentScheduleWorker from the workers list (if present). */ #findAgentScheduleWorker() { return this.#workers.find((w) => w.name === "agent-schedule"); } /** * Sync code-declared schedule configs to the database. Called by * SchedulerWorker during init and by addWorkflow() for late registrations. * * @internal — public so SchedulerWorker can call it, not part of the user API. */ async registerDeclarativeSchedules(schedulesStore) { const declared = this.#collectDeclarativeSchedules(); const declaredIds = new Set(declared.map((d) => d.scheduleId)); const declaredIdsByWorkflow = /* @__PURE__ */ new Map(); const workflows = this.#workflows; for (const workflow of Object.values(workflows ?? {})) declaredIdsByWorkflow.set(workflow.id, /* @__PURE__ */ new Set()); for (const { workflowId, scheduleId } of declared) { if (!declaredIdsByWorkflow.has(workflowId)) declaredIdsByWorkflow.set(workflowId, /* @__PURE__ */ new Set()); declaredIdsByWorkflow.get(workflowId).add(scheduleId); } for (const { scheduleId, workflowId, cfg } of declared) try { const existing = await schedulesStore.getSchedule(scheduleId); const now = Date.now(); const target = { type: "workflow", workflowId, inputData: cfg.inputData, initialState: cfg.initialState, requestContext: cfg.requestContext }; if (!existing) { await schedulesStore.createSchedule({ id: scheduleId, target, cron: cfg.cron, timezone: cfg.timezone, status: "active", nextFireAt: computeNextFireAt(cfg.cron, { timezone: cfg.timezone, after: now }), createdAt: now, updatedAt: now, metadata: cfg.metadata }); continue; } const patch = {}; const cronChanged = existing.cron !== cfg.cron; const timezoneChanged = (existing.timezone ?? void 0) !== (cfg.timezone ?? void 0); if (cronChanged) patch.cron = cfg.cron; if (timezoneChanged) patch.timezone = cfg.timezone; if (!targetsEqual(existing.target, target)) patch.target = target; if (!metadataEqual(existing.metadata, cfg.metadata)) patch.metadata = cfg.metadata; if (cronChanged || timezoneChanged) patch.nextFireAt = computeNextFireAt(cfg.cron, { timezone: cfg.timezone, after: now }); if (Object.keys(patch).length > 0) await schedulesStore.updateSchedule(scheduleId, patch); } catch (error) { this.#logger?.error("Failed to register declarative schedule", { scheduleId, workflowId, error }); } const allRows = await schedulesStore.listSchedules(); for (const row of allRows) { if (declaredIds.has(row.id)) continue; if (!row.id.startsWith("wf_")) continue; const ownerWorkflowId = ownerWorkflowIdForRow(row.id, declaredIdsByWorkflow) ?? ownerWorkflowIdFromRowId(row.id); if (!ownerWorkflowId) continue; try { await schedulesStore.deleteSchedule(row.id); } catch (error) { this.#logger?.error("Failed to delete orphaned declarative schedule", { scheduleId: row.id, workflowId: ownerWorkflowId, error }); } } } /** * Auto-enables the background task manager when an agent with sub-agents is * registered. Sub-agent delegation runs in the background by default so the * parent stream stays responsive; that requires the manager to be available. * No-op when the user explicitly opted out via `backgroundTasks.enabled: false`. * * Eligible agents: any agent whose `agents` field is either a static record * with at least one entry OR a dynamic (function-based) resolver. Function * resolvers are evaluated per request, so we can't inspect their contents * here — but if the caller bothered to wire one up, we enable defensively * so those resolved sub-agents also dispatch in the background. */ #maybeEnableBackgroundTasksForAgent(agent) { if (this.#backgroundTaskManager) return; if (this.#backgroundTaskConfig?.enabled === false) return; if (!agent.__hasSubAgentsConfigured?.()) return; this.#backgroundTaskConfig = { ...this.#backgroundTaskConfig ?? {}, enabled: true }; this.#ensureBackgroundTaskManager(); } getAgent(name, version) { const agent = this.#agents?.[name]; if (!agent) { const error = new MastraError({ id: "MASTRA_GET_AGENT_BY_NAME_NOT_FOUND", domain: ErrorDomain.MASTRA, category: ErrorCategory.USER, text: `Agent with name ${String(name)} not found`, details: { status: 404, agentName: String(name), agents: Object.keys(this.#agents ?? {}).join(", ") } }); this.#logger?.trackException(error); throw error; } if (!version) return this.#agents[name]; return this.resolveVersionedAgent(agent, version); } /** * Returns the `AgentChannels` instances for all registered agents and * agent controllers. Keys are agent / agent controller registration keys. * A controller's channels — also attached to its mode agents — are * reported once, under the controller's key. */ getChannels() { const result = {}; const controllerEntries = []; const controllerOwned = /* @__PURE__ */ new Set(); for (const [controllerKey, controller] of Object.entries(this.#harnesses ?? {})) { const controllerChannels = controller.getChannels(); if (controllerChannels) { controllerEntries.push([controllerKey, controllerChannels]); controllerOwned.add(controllerChannels); } } for (const [agentKey, agent] of Object.entries(this.#agents ?? {})) { const agentChannels = agent.getChannels(); if (agentChannels instanceof AgentChannels && !controllerOwned.has(agentChannels)) result[agentKey] = agentChannels; } for (const [controllerKey, controllerChannels] of controllerEntries) { if (result[controllerKey]) this.#logger?.warn(`Channels key collision: an agent and an agent controller are both registered under '${controllerKey}'; reporting the controller's channels.`); result[controllerKey] = controllerChannels; } return result; } getAgentById(id, version) { let agent = Object.values(this.#agents).find((a) => a.id === id); if (!agent) try { agent = this.getAgent(id); } catch {} if (!agent) { const error = new MastraError({ id: "MASTRA_GET_AGENT_BY_AGENT_ID_NOT_FOUND", domain: ErrorDomain.MASTRA, category: ErrorCategory.USER, text: `Agent with id ${String(id)} not found`, details: { status: 404, agentId: String(id), agents: Object.keys(this.#agents ?? {}).join(", ") } }); this.#logger?.trackException(error); throw error; } if (!version) return agent; return this.resolveVersionedAgent(agent, version); } /** * Resolve a versioned variant of an agent by applying stored overrides from the editor. * * Requires the editor package to be configured — throws * `MASTRA_EDITOR_REQUIRED_FOR_VERSIONED_AGENT_LOOKUP` if it is not. * * @param agent - The code-defined agent to resolve a version for. * @param version - Selects a version by ID or publication status. * @returns A forked agent instance with the stored overrides applied. */ async resolveVersionedAgent(agent, version) { const editor = this.getEditor(); if (!editor) { const error = new MastraError({ id: "MASTRA_EDITOR_REQUIRED_FOR_VERSIONED_AGENT_LOOKUP", domain: ErrorDomain.MASTRA, category: ErrorCategory.USER, text: "Versioned agent lookup requires the editor package to be configured", details: { status: 400, agentId: agent.id, ...version && "versionId" in version ? { versionId: version.versionId } : {}, ...version && "status" in version && version.status ? { versionStatus: version.status } : {} } }); this.#logger?.trackException(error); throw error; } return editor.agent.applyStoredOverrides(agent, "versionId" in version ? version : { status: version.status ?? "published" }); } /** * Returns all registered agents as a record keyed by their names. * * This method provides access to the complete registry of agents, allowing you to * iterate over them, check what agents are available, or perform bulk operations. * * @example * ```typescript * const mastra = new Mastra({ * agents: { * weatherAgent: new Agent({ id: 'weather-agent', name: 'weather', model: 'openai/gpt-4o' }), * supportAgent: new Agent({ id: 'support-agent', name: 'support', model: 'openai/gpt-4o' }) * } * }); * * const allAgents = mastra.listAgents(); * console.log(Object.keys(allAgents)); // ['weatherAgent', 'supportAgent'] * ``` */ listAgents() { return this.#agents; } /** * Get an AgentController hosted on this Mastra instance by its registration * key (the key it was registered under in `new Mastra({ agentControllers })`). * Returns `undefined` when none is registered under that key. Server route * handlers use this to create and drive sessions over HTTP. * * @example * ```typescript * const code = new AgentController({ id: 'code-controller', modes }); * const mastra = new Mastra({ agentControllers: { code } }); * * mastra.getAgentController('code'); // → the AgentController (by key) * ``` */ getAgentController(key) { return this.#harnesses[key]; } /** * Get an AgentController hosted on this Mastra instance by its unique `id` * (the `id` passed to the `AgentController` constructor). Falls back to a * registration-key lookup when none matches by id, mirroring * {@link getAgentById}. Returns `undefined` when none is found. * * @example * ```typescript * const code = new AgentController({ id: 'code-controller', modes }); * const mastra = new Mastra({ agentControllers: { code } }); * * mastra.getAgentControllerById('code-controller'); // → by id * ``` */ getAgentControllerById(id) { return Object.values(this.#harnesses).find((controller) => controller.id === id) ?? this.#harnesses[id]; } /** * List all AgentControllers hosted on this Mastra instance, keyed by their * registration key. */ listAgentControllers() { return this.#harnesses; } /** * Get a Harness hosted on this Mastra instance by its registration key. * * @deprecated Use {@link Mastra.getAgentController} instead. */ getHarness(key) { return this.getAgentController(key); } /** * Get a Harness hosted on this Mastra instance by its unique `id`. * * @deprecated Use {@link Mastra.getAgentControllerById} instead. */ getHarnessById(id) { return this.getAgentControllerById(id); } /** * List all Harnesses hosted on this Mastra instance, keyed by their * registration key. * * @deprecated Use {@link Mastra.listAgentControllers} instead. */ listHarnesses() { return this.listAgentControllers(); } /** * Adds a new agent to the Mastra instance. * * This method allows dynamic registration of agents after the Mastra instance * has been created. The agent will be initialized with the current logger. * * @throws {MastraError} When an agent with the same key already exists * * @example * ```typescript * const mastra = new Mastra(); * const newAgent = new Agent({ * id: 'chat-agent', * name: 'Chat Assistant', * model: 'openai/gpt-4o' * }); * mastra.addAgent(newAgent); // Uses agent.id as key * // or * mastra.addAgent(newAgent, 'customKey'); // Uses custom key * * // Durable agents (e.g., InngestAgent) are also supported: * const durableAgent = createInngestAgent({ agent: newAgent, inngest }); * mastra.addAgent(durableAgent); // Auto-registers required workflows * ``` */ addAgent(agent, key, options) { if (!agent) throw createUndefinedPrimitiveError("agent", agent, key); if (!(isDurableAgentLike(agent) && agent.agent !== agent) && agent.durable) { const durableOption = agent.durable; const opts = durableOption === true ? {} : { ...durableOption }; agent = createDurableAgent({ agent, ...opts }); } if (isDurableAgentLike(agent)) { const durableAgent = agent; const underlyingAgent = durableAgent.agent; const agentKey = key || durableAgent.id; const agents = this.#agents; if (agents[agentKey]) { this.getLogger().debug(`Agent with key ${agentKey} already exists. Skipping addition.`); return; } durableAgent.__setMastra?.(this); if (options?.source) { durableAgent.source = options.source; underlyingAgent.source = options.source; } underlyingAgent.__setLogger(this.#logger); underlyingAgent.__registerMastra(this); underlyingAgent.__registerPrimitives({ logger: this.getLogger(), storage: this.getStorage(), agents, tts: this.#tts, vectors: this.#vectors }); agents[agentKey] = durableAgent; const durableWorkflows = durableAgent.getDurab