@mastra/core
Version:
1,337 lines (1,336 loc) • 151 kB
JavaScript
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