UNPKG

@mastra/core

Version:
583 lines (582 loc) 21.7 kB
const require_rolldown_runtime = require("./rolldown-runtime-uwYp4b74.cjs"); const require_error = require("./error-B-e62x-A.cjs"); const require_cron = require("./cron-CrmzJKFJ.cjs"); let crypto = require("crypto"); let zod_v4 = require("zod/v4"); let _sindresorhus_slugify = require("@sindresorhus/slugify"); _sindresorhus_slugify = require_rolldown_runtime.__toESM(_sindresorhus_slugify, 1); //#region src/schedules/types.ts /** Stable schedule id prefix for agent schedules. */ const AGENT_SCHEDULE_PREFIX = "agent_"; /** * Stable schedule id prefix for imperative workflow schedules created via * `mastra.schedules.create({ workflowId, ... })`. Intentionally distinct from * the `wf_` prefix used by declarative `createWorkflow({ schedule })` rows — * the boot-time declarative sync sweeps `wf_` rows against the in-code * config and must never delete imperative rows. */ const WORKFLOW_SCHEDULE_PREFIX = "schedule_"; /** Shared zod for {@link AgentSignalAttributes} (XML tag attribute values). */ const ScheduleAttributesSchema = zod_v4.z.record(zod_v4.z.string(), zod_v4.z.union([ zod_v4.z.string(), zod_v4.z.number(), zod_v4.z.boolean(), zod_v4.z.null() ])); /** Serializable stream options applied to a woken run. See {@link ScheduleStreamOptions}. */ const ScheduleStreamOptionsSchema = zod_v4.z.object({ requestContext: zod_v4.z.record(zod_v4.z.string(), zod_v4.z.unknown()).optional() }); /** Options applied when the target thread is actively streaming. */ const ScheduleIfActiveSchema = zod_v4.z.object({ behavior: zod_v4.z.enum([ "deliver", "persist", "discard" ]).optional(), attributes: ScheduleAttributesSchema.optional() }); /** Options applied when the target thread is idle. */ const ScheduleIfIdleSchema = zod_v4.z.object({ behavior: zod_v4.z.enum([ "wake", "persist", "discard" ]).optional(), attributes: ScheduleAttributesSchema.optional(), streamOptions: ScheduleStreamOptionsSchema.optional() }); /** * Input payload persisted in `Schedule.target.inputData` for the built-in * agent-schedule fire. The scheduler tick rehydrates this on every fire. */ const ScheduleInputSchema = zod_v4.z.object({ scheduleId: zod_v4.z.string(), agentId: zod_v4.z.string(), prompt: zod_v4.z.string(), threadId: zod_v4.z.string().optional(), resourceId: zod_v4.z.string().optional(), signalType: zod_v4.z.enum([ "user", "state", "reactive", "notification", "user-message", "system-reminder" ]).optional(), /** * XML tag name the signal renders as. Defaults to `schedule`, so a fire * surfaces to the agent as `<schedule>…</schedule>`. Override to render a * different tag. */ tagName: zod_v4.z.string().optional(), /** Attributes rendered onto the signal's XML tag. */ attributes: ScheduleAttributesSchema.optional(), /** * Provider options merged into the schedule signal payload on every fire. * Stored as a plain JSON object (`MastraProviderMetadata` is JSON-safe) and * applied regardless of `ifActive` / `ifIdle`. */ providerOptions: zod_v4.z.record(zod_v4.z.string(), zod_v4.z.unknown()).optional(), ifActive: ScheduleIfActiveSchema.optional(), ifIdle: ScheduleIfIdleSchema.optional() }); const ScheduleOutputSchema = zod_v4.z.object({ status: zod_v4.z.enum([ "fired", "signal-accepted", "skipped-thread-blocked", "thread-missing", "agent-missing", "invalid-input" ]), reason: zod_v4.z.string().optional() }); //#endregion //#region src/schedules/schedules.ts /** PubSub topic consumed by the workflow event processor. */ const TOPIC_WORKFLOWS = "workflows"; /** * Slugify the caller-facing portion of a schedule id into a canonical * `<prefix><slug>` shape. The slug part is lowercased and stripped of * characters that are unsafe in storage keys / URLs; the prefix is added only * if missing so a caller can pass either `nightly-summary` or * `agent_nightly-summary` and get the same canonical id. Returns an empty * string when nothing slug-able remains. */ function canonicalizeScheduleId(rawId, prefix) { const trimmed = rawId.trim(); const slug = (0, _sindresorhus_slugify.default)(trimmed.startsWith(prefix) ? trimmed.slice(prefix.length) : trimmed); if (!slug) return ""; return `${prefix}${slug}`; } /** * Normalize a caller-supplied schedule id for `create`. Throws * `SCHEDULES_INVALID_ID` when the id is empty after normalization so callers * cannot create an unaddressable schedule. */ function normalizeScheduleId(rawId, prefix) { const canonical = canonicalizeScheduleId(rawId, prefix); if (!canonical) throw new require_error.MastraError({ id: "SCHEDULES_INVALID_ID", domain: require_error.ErrorDomain.AGENT, category: require_error.ErrorCategory.USER, text: `schedules.create: id "${rawId}" is empty after normalization. Provide an id with at least one alphanumeric character.` }); return canonical; } /** * Unified service for cron schedules. Schedules are persisted as `Schedule` * rows whose `target` discriminates what fires: `type: 'agent'` rows run an * agent (via signal or `agent.generate`), `type: 'workflow'` rows start a * workflow run. This class is a typed projection over `SchedulesStorage` * that knows how to build targets and surface flat * {@link AgentSchedule} / {@link WorkflowSchedule} views. * * Use via `mastra.schedules` (the canonical CRUD surface). */ var Schedules = class { #mastra; constructor(mastra) { this.#mastra = mastra; } async #getStore() { const store = await this.#mastra.getStorage()?.getStore("schedules"); if (!store) throw new require_error.MastraError({ id: "SCHEDULES_NO_SCHEDULES_STORAGE", domain: require_error.ErrorDomain.AGENT, category: require_error.ErrorCategory.USER, text: "Schedules require a storage adapter that implements the schedules domain." }); return store; } /** * Resolve a caller-supplied id to a stored row. An id is first looked up * verbatim (covering `agent_`, `schedule_`, `wf_`, and legacy `hb_` ids); * when that misses, a bare caller id is canonicalized to `agent_<slug>` to * match what agent-schedule `create` persisted. */ async #load(id) { const store = await this.#getStore(); const trimmed = id.trim(); const exact = trimmed ? await store.getSchedule(trimmed) : null; if (exact) return exact; const canonical = canonicalizeScheduleId(trimmed, AGENT_SCHEDULE_PREFIX); if (!canonical || canonical === trimmed) return null; return store.getSchedule(canonical); } async create(input) { if ("workflowId" in input && input.workflowId) return this.#createWorkflowSchedule(input); return this.#createAgentSchedule(input); } async #createAgentSchedule(input) { require_cron.validateCron(input.cron, input.timezone); if (!input.agentId) throw new require_error.MastraError({ id: "SCHEDULES_MISSING_TARGET_ID", domain: require_error.ErrorDomain.AGENT, category: require_error.ErrorCategory.USER, text: "schedules.create requires `agentId` or `workflowId`." }); if (input.threadId && !input.resourceId) throw new require_error.MastraError({ id: "SCHEDULES_MISSING_RESOURCE_ID", domain: require_error.ErrorDomain.AGENT, category: require_error.ErrorCategory.USER, text: "schedules.create requires `resourceId` when `threadId` is set." }); if (!input.threadId) { const offenders = []; if (input.signalType !== void 0) offenders.push("signalType"); if (input.ifActive !== void 0) offenders.push("ifActive"); if (input.ifIdle !== void 0) offenders.push("ifIdle"); if (input.resourceId !== void 0) offenders.push("resourceId"); if (offenders.length > 0) throw new require_error.MastraError({ id: "SCHEDULES_THREADLESS_OPTIONS", domain: require_error.ErrorDomain.AGENT, category: require_error.ErrorCategory.USER, text: `schedules.create: ${offenders.join(", ")} require a threadId.` }); } const store = await this.#getStore(); await this.#mastra.__ensureScheduleRuntimeReady(); const id = input.id !== void 0 ? normalizeScheduleId(input.id, AGENT_SCHEDULE_PREFIX) : `${AGENT_SCHEDULE_PREFIX}${(0, crypto.randomUUID)()}`; await this.#assertIdAvailable(store, id, input.id !== void 0); const now = Date.now(); const nextFireAt = require_cron.computeNextFireAt(input.cron, { timezone: input.timezone, after: now }); const schedule = { id, target: { type: "agent", agentId: input.agentId, prompt: input.prompt, ...input.name !== void 0 ? { name: input.name } : {}, ...input.threadId ? { threadId: input.threadId } : {}, ...input.resourceId ? { resourceId: input.resourceId } : {}, ...input.signalType ? { signalType: input.signalType } : {}, ...input.tagName ? { tagName: input.tagName } : {}, ...input.attributes ? { attributes: input.attributes } : {}, ...input.providerOptions ? { providerOptions: input.providerOptions } : {}, ...input.ifActive ? { ifActive: input.ifActive } : {}, ...input.ifIdle ? { ifIdle: input.ifIdle } : {} }, cron: input.cron, timezone: input.timezone, status: input.status ?? "active", nextFireAt, createdAt: now, updatedAt: now, ownerType: "agent", ownerId: input.agentId, ...input.metadata ? { metadata: input.metadata } : {} }; return toAgentSchedule(await store.createSchedule(schedule)); } async #createWorkflowSchedule(input) { require_cron.validateCron(input.cron, input.timezone); const store = await this.#getStore(); await this.#mastra.__ensureScheduleRuntimeReady(); const id = input.id !== void 0 ? normalizeScheduleId(input.id, WORKFLOW_SCHEDULE_PREFIX) : `${WORKFLOW_SCHEDULE_PREFIX}${(0, crypto.randomUUID)()}`; await this.#assertIdAvailable(store, id, input.id !== void 0); const now = Date.now(); const nextFireAt = require_cron.computeNextFireAt(input.cron, { timezone: input.timezone, after: now }); const schedule = { id, target: { type: "workflow", workflowId: input.workflowId, ...input.inputData !== void 0 ? { inputData: input.inputData } : {}, ...input.initialState !== void 0 ? { initialState: input.initialState } : {}, ...input.requestContext !== void 0 ? { requestContext: input.requestContext } : {} }, cron: input.cron, timezone: input.timezone, status: input.status ?? "active", nextFireAt, createdAt: now, updatedAt: now, ...input.metadata ? { metadata: input.metadata } : {} }; return toWorkflowSchedule(await store.createSchedule(schedule)); } async #assertIdAvailable(store, id, callerProvided) { if (!callerProvided) return; if (await store.getSchedule(id)) throw new require_error.MastraError({ id: "SCHEDULES_ID_EXISTS", domain: require_error.ErrorDomain.AGENT, category: require_error.ErrorCategory.USER, text: `schedules.create: a schedule with id "${id}" already exists. Use update() to modify it or choose a different id.` }); } async get(id) { const schedule = await this.#load(id); if (!schedule) return null; return toScheduleView(schedule); } async list(filter) { const views = (await (await this.#getStore()).listSchedules({ ...filter?.agentId ? { ownerType: "agent", ownerId: filter.agentId } : {}, ...filter?.workflowId ? { workflowId: filter.workflowId } : {}, ...filter?.status ? { status: filter.status } : {} })).map(toScheduleView).filter((s) => s !== null).filter((s) => filter?.agentId ? s.agentId !== void 0 : true); if (!(filter?.threadId !== void 0 || filter?.resourceId !== void 0 || filter?.name !== void 0)) return views; return views.filter((s) => { if (s.agentId === void 0) return false; if (filter?.threadId !== void 0 && s.threadId !== filter.threadId) return false; if (filter?.resourceId !== void 0 && s.resourceId !== filter.resourceId) return false; if (filter?.name !== void 0 && s.name !== filter.name) return false; return true; }); } async update(id, patch) { const store = await this.#getStore(); const existing = await this.#load(id); if (!existing) throw new require_error.MastraError({ id: "SCHEDULES_NOT_FOUND", domain: require_error.ErrorDomain.AGENT, category: require_error.ErrorCategory.USER, text: `Schedule "${id}" not found.` }); const nextCron = patch.cron ?? existing.cron; const nextTimezone = patch.timezone !== void 0 ? patch.timezone : existing.timezone; if (patch.cron !== void 0 || patch.timezone !== void 0) require_cron.validateCron(nextCron, nextTimezone); const nextTarget = existing.target.type === "agent" ? this.#patchAgentTarget(existing.target, patch) : this.#patchWorkflowTarget(existing.target, patch); const resuming = patch.status === "active" && existing.status === "paused"; const nextFireAt = patch.cron !== void 0 || patch.timezone !== void 0 || resuming ? require_cron.computeNextFireAt(nextCron, { timezone: nextTimezone, after: Date.now() }) : void 0; return toScheduleView(await store.updateSchedule(existing.id, { ...patch.cron !== void 0 ? { cron: patch.cron } : {}, ...patch.timezone !== void 0 ? { timezone: patch.timezone } : {}, target: nextTarget, ...nextFireAt !== void 0 ? { nextFireAt } : {}, ...patch.metadata !== void 0 ? { metadata: patch.metadata } : {}, ...patch.status !== void 0 ? { status: patch.status } : {} })); } #patchAgentTarget(existingTarget, patch) { if (!existingTarget.threadId) { const offenders = []; if (patch.signalType !== void 0) offenders.push("signalType"); if (patch.ifActive !== void 0) offenders.push("ifActive"); if (patch.ifIdle !== void 0) offenders.push("ifIdle"); if (offenders.length > 0) throw new require_error.MastraError({ id: "SCHEDULES_THREADLESS_OPTIONS", domain: require_error.ErrorDomain.AGENT, category: require_error.ErrorCategory.USER, text: `schedules.update: ${offenders.join(", ")} require a threadId.` }); } return { ...existingTarget, ...patch.prompt !== void 0 ? { prompt: patch.prompt } : {}, ...patch.name !== void 0 ? { name: patch.name } : {}, ...patch.signalType !== void 0 ? { signalType: patch.signalType } : {}, ...patch.tagName !== void 0 ? { tagName: patch.tagName } : {}, ...patch.attributes !== void 0 ? { attributes: patch.attributes } : {}, ...patch.providerOptions !== void 0 ? { providerOptions: patch.providerOptions } : {}, ...patch.ifActive !== void 0 ? { ifActive: patch.ifActive } : {}, ...patch.ifIdle !== void 0 ? { ifIdle: patch.ifIdle } : {} }; } #patchWorkflowTarget(existingTarget, patch) { const offenders = [ "prompt", "name", "signalType", "tagName", "attributes", "providerOptions", "ifActive", "ifIdle" ].filter((key) => patch[key] !== void 0); if (offenders.length > 0) throw new require_error.MastraError({ id: "SCHEDULES_INVALID_WORKFLOW_PATCH", domain: require_error.ErrorDomain.AGENT, category: require_error.ErrorCategory.USER, text: `schedules.update: ${offenders.join(", ")} only apply to agent schedules.` }); const wfPatch = patch; return { ...existingTarget, ...wfPatch.inputData !== void 0 ? { inputData: wfPatch.inputData } : {}, ...wfPatch.initialState !== void 0 ? { initialState: wfPatch.initialState } : {}, ...wfPatch.requestContext !== void 0 ? { requestContext: wfPatch.requestContext } : {} }; } async delete(id) { const store = await this.#getStore(); const existing = await this.#load(id); if (!existing) return; await store.deleteSchedule(existing.id); } async pause(id) { const store = await this.#getStore(); const existing = await this.#load(id); if (!existing) throw new require_error.MastraError({ id: "SCHEDULES_NOT_FOUND", domain: require_error.ErrorDomain.AGENT, category: require_error.ErrorCategory.USER, text: `Schedule "${id}" not found.` }); if (existing.status === "paused") return toScheduleView(existing); return toScheduleView(await store.updateSchedule(existing.id, { status: "paused" })); } async resume(id) { const store = await this.#getStore(); const existing = await this.#load(id); if (!existing) throw new require_error.MastraError({ id: "SCHEDULES_NOT_FOUND", domain: require_error.ErrorDomain.AGENT, category: require_error.ErrorCategory.USER, text: `Schedule "${id}" not found.` }); if (existing.status === "active") return toScheduleView(existing); const nextFireAt = require_cron.computeNextFireAt(existing.cron, { timezone: existing.timezone, after: Date.now() }); return toScheduleView(await store.updateSchedule(existing.id, { status: "active", nextFireAt })); } async run(id) { const existing = await this.#load(id); if (!existing) throw new require_error.MastraError({ id: "SCHEDULES_NOT_FOUND", domain: require_error.ErrorDomain.AGENT, category: require_error.ErrorCategory.USER, text: `Schedule "${id}" not found.` }); const now = Date.now(); if (existing.target.type === "agent") { const claimId = `manual_${existing.id}_${now}`; await this.#mastra.pubsub.publish("agent-schedules", { type: "agent-schedule.fire", runId: claimId, data: { scheduleId: existing.id, claimId, scheduledFireAt: now, target: existing.target, triggerKind: "manual" } }); return { scheduleId: existing.id, claimId, scheduledFireAt: now }; } const { workflowId, inputData, initialState, requestContext } = existing.target; const claimId = `sched_${existing.id}_${now}`; await this.#mastra.pubsub.publish(TOPIC_WORKFLOWS, { type: "workflow.start", runId: claimId, data: { workflowId, runId: claimId, prevResult: { status: "success", output: inputData ?? {} }, requestContext: requestContext ?? {}, initialState: initialState ?? {} } }); const store = await this.#getStore(); try { await store.recordTrigger({ scheduleId: existing.id, runId: claimId, scheduledFireAt: now, actualFireAt: now, outcome: "published", triggerKind: "manual" }); } catch {} return { scheduleId: existing.id, claimId, scheduledFireAt: now }; } }; /** * Project a `Schedule` row to a flat {@link AgentSchedule} view. Returns * `null` when the schedule is not an agent schedule * (`target.type !== 'agent'`), allowing callers to filter mixed result sets * in one pass. */ function toAgentSchedule(schedule) { if (schedule.target?.type !== "agent") return null; const target = schedule.target; return { id: schedule.id, agentId: target.agentId, ...target.name !== void 0 ? { name: target.name } : {}, ...target.threadId ? { threadId: target.threadId } : {}, ...target.resourceId ? { resourceId: target.resourceId } : {}, prompt: target.prompt, cron: schedule.cron, ...schedule.timezone ? { timezone: schedule.timezone } : {}, status: schedule.status, nextFireAt: schedule.nextFireAt, ...schedule.lastFireAt !== void 0 ? { lastFireAt: schedule.lastFireAt } : {}, ...schedule.lastRunId ? { lastRunId: schedule.lastRunId } : {}, ...target.signalType ? { signalType: target.signalType } : {}, ...target.tagName ? { tagName: target.tagName } : {}, ...target.attributes ? { attributes: target.attributes } : {}, ...target.providerOptions ? { providerOptions: target.providerOptions } : {}, ...target.ifActive ? { ifActive: target.ifActive } : {}, ...target.ifIdle ? { ifIdle: target.ifIdle } : {}, ...schedule.metadata ? { metadata: schedule.metadata } : {}, createdAt: schedule.createdAt, updatedAt: schedule.updatedAt }; } /** * Project a `Schedule` row to a flat {@link WorkflowSchedule} view. Returns * `null` when the schedule is not a workflow schedule. */ function toWorkflowSchedule(schedule) { if (schedule.target?.type !== "workflow") return null; const target = schedule.target; return { id: schedule.id, workflowId: target.workflowId, cron: schedule.cron, ...schedule.timezone ? { timezone: schedule.timezone } : {}, status: schedule.status, nextFireAt: schedule.nextFireAt, ...schedule.lastFireAt !== void 0 ? { lastFireAt: schedule.lastFireAt } : {}, ...schedule.lastRunId ? { lastRunId: schedule.lastRunId } : {}, ...target.inputData !== void 0 ? { inputData: target.inputData } : {}, ...target.initialState !== void 0 ? { initialState: target.initialState } : {}, ...target.requestContext !== void 0 ? { requestContext: target.requestContext } : {}, ...schedule.metadata ? { metadata: schedule.metadata } : {}, createdAt: schedule.createdAt, updatedAt: schedule.updatedAt }; } /** Project a `Schedule` row to whichever flat view matches its target type. */ function toScheduleView(schedule) { return toAgentSchedule(schedule) ?? toWorkflowSchedule(schedule); } //#endregion Object.defineProperty(exports, "AGENT_SCHEDULE_PREFIX", { enumerable: true, get: function() { return AGENT_SCHEDULE_PREFIX; } }); Object.defineProperty(exports, "ScheduleInputSchema", { enumerable: true, get: function() { return ScheduleInputSchema; } }); Object.defineProperty(exports, "ScheduleOutputSchema", { enumerable: true, get: function() { return ScheduleOutputSchema; } }); Object.defineProperty(exports, "Schedules", { enumerable: true, get: function() { return Schedules; } }); Object.defineProperty(exports, "WORKFLOW_SCHEDULE_PREFIX", { enumerable: true, get: function() { return WORKFLOW_SCHEDULE_PREFIX; } }); Object.defineProperty(exports, "toAgentSchedule", { enumerable: true, get: function() { return toAgentSchedule; } }); Object.defineProperty(exports, "toScheduleView", { enumerable: true, get: function() { return toScheduleView; } }); Object.defineProperty(exports, "toWorkflowSchedule", { enumerable: true, get: function() { return toWorkflowSchedule; } }); //# sourceMappingURL=schedules-D_aayWxz.cjs.map