UNPKG

@mastra/core

Version:
530 lines (524 loc) 21.3 kB
'use strict'; var chunkXSOONORA_cjs = require('./chunk-XSOONORA.cjs'); var v4 = require('zod/v4'); var crypto = require('crypto'); var slugify = require('@sindresorhus/slugify'); var croner = require('croner'); function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; } var slugify__default = /*#__PURE__*/_interopDefault(slugify); var AGENT_SCHEDULE_PREFIX = "agent_"; var WORKFLOW_SCHEDULE_PREFIX = "schedule_"; var ScheduleAttributesSchema = v4.z.record(v4.z.string(), v4.z.union([v4.z.string(), v4.z.number(), v4.z.boolean(), v4.z.null()])); var ScheduleStreamOptionsSchema = v4.z.object({ requestContext: v4.z.record(v4.z.string(), v4.z.unknown()).optional() }); var ScheduleIfActiveSchema = v4.z.object({ behavior: v4.z.enum(["deliver", "persist", "discard"]).optional(), attributes: ScheduleAttributesSchema.optional() }); var ScheduleIfIdleSchema = v4.z.object({ behavior: v4.z.enum(["wake", "persist", "discard"]).optional(), attributes: ScheduleAttributesSchema.optional(), streamOptions: ScheduleStreamOptionsSchema.optional() }); var ScheduleInputSchema = v4.z.object({ scheduleId: v4.z.string(), agentId: v4.z.string(), prompt: v4.z.string(), threadId: v4.z.string().optional(), resourceId: v4.z.string().optional(), signalType: 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: 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: v4.z.record(v4.z.string(), v4.z.unknown()).optional(), ifActive: ScheduleIfActiveSchema.optional(), ifIdle: ScheduleIfIdleSchema.optional() }); var ScheduleOutputSchema = v4.z.object({ status: v4.z.enum([ "fired", "signal-accepted", "skipped-thread-blocked", "thread-missing", "agent-missing", "invalid-input" ]), reason: v4.z.string().optional() }); function validateCron(cron, timezone) { if (typeof cron !== "string" || cron.trim() === "") { throw new Error( `Invalid cron expression: expected a non-empty cron string (e.g. "0 * * * *"), but received ${cron === void 0 ? "undefined" : JSON.stringify(cron)}.` ); } let job; try { job = new croner.Cron(cron); } catch (error) { const reason = error instanceof Error ? error.message : String(error); throw new Error(`Invalid cron expression "${cron}": ${reason}`); } if (timezone !== void 0) { try { new croner.Cron(cron, { timezone }).nextRun(); } catch (error) { const reason = error instanceof Error ? error.message : String(error); throw new Error(`Invalid timezone "${timezone}": ${reason}`); } } else { job.nextRun(); } } function computeNextFireAt(cron, options) { const job = new croner.Cron(cron, { timezone: options?.timezone }); const reference = options?.after !== void 0 ? new Date(options.after) : /* @__PURE__ */ new Date(); const next = job.nextRun(reference); if (!next) { throw new Error(`Cron expression "${cron}" has no future occurrence after ${reference.toISOString()}`); } return next.getTime(); } // src/schedules/schedules.ts var TOPIC_WORKFLOWS = "workflows"; function canonicalizeScheduleId(rawId, prefix) { const trimmed = rawId.trim(); const withoutPrefix = trimmed.startsWith(prefix) ? trimmed.slice(prefix.length) : trimmed; const slug = slugify__default.default(withoutPrefix); if (!slug) return ""; return `${prefix}${slug}`; } function normalizeScheduleId(rawId, prefix) { const canonical = canonicalizeScheduleId(rawId, prefix); if (!canonical) { throw new chunkXSOONORA_cjs.MastraError({ id: "SCHEDULES_INVALID_ID", domain: chunkXSOONORA_cjs.ErrorDomain.AGENT, category: chunkXSOONORA_cjs.ErrorCategory.USER, text: `schedules.create: id "${rawId}" is empty after normalization. Provide an id with at least one alphanumeric character.` }); } return canonical; } var Schedules = class { #mastra; constructor(mastra) { this.#mastra = mastra; } async #getStore() { const storage = this.#mastra.getStorage(); const store = await storage?.getStore("schedules"); if (!store) { throw new chunkXSOONORA_cjs.MastraError({ id: "SCHEDULES_NO_SCHEDULES_STORAGE", domain: chunkXSOONORA_cjs.ErrorDomain.AGENT, category: chunkXSOONORA_cjs.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) { validateCron(input.cron, input.timezone); if (!input.agentId) { throw new chunkXSOONORA_cjs.MastraError({ id: "SCHEDULES_MISSING_TARGET_ID", domain: chunkXSOONORA_cjs.ErrorDomain.AGENT, category: chunkXSOONORA_cjs.ErrorCategory.USER, text: "schedules.create requires `agentId` or `workflowId`." }); } if (input.threadId && !input.resourceId) { throw new chunkXSOONORA_cjs.MastraError({ id: "SCHEDULES_MISSING_RESOURCE_ID", domain: chunkXSOONORA_cjs.ErrorDomain.AGENT, category: chunkXSOONORA_cjs.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 chunkXSOONORA_cjs.MastraError({ id: "SCHEDULES_THREADLESS_OPTIONS", domain: chunkXSOONORA_cjs.ErrorDomain.AGENT, category: chunkXSOONORA_cjs.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}${crypto.randomUUID()}`; await this.#assertIdAvailable(store, id, input.id !== void 0); const now = Date.now(); const nextFireAt = computeNextFireAt(input.cron, { timezone: input.timezone, after: now }); const 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 } : {} }; const schedule = { id, target, 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 } : {} }; const created = await store.createSchedule(schedule); return toAgentSchedule(created); } async #createWorkflowSchedule(input) { 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}${crypto.randomUUID()}`; await this.#assertIdAvailable(store, id, input.id !== void 0); const now = Date.now(); const nextFireAt = computeNextFireAt(input.cron, { timezone: input.timezone, after: now }); const 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 } : {} }; const schedule = { id, target, cron: input.cron, timezone: input.timezone, status: input.status ?? "active", nextFireAt, createdAt: now, updatedAt: now, ...input.metadata ? { metadata: input.metadata } : {} }; const created = await store.createSchedule(schedule); return toWorkflowSchedule(created); } async #assertIdAvailable(store, id, callerProvided) { if (!callerProvided) return; const existing = await store.getSchedule(id); if (existing) { throw new chunkXSOONORA_cjs.MastraError({ id: "SCHEDULES_ID_EXISTS", domain: chunkXSOONORA_cjs.ErrorDomain.AGENT, category: chunkXSOONORA_cjs.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 store = await this.#getStore(); const schedules = await store.listSchedules({ ...filter?.agentId ? { ownerType: "agent", ownerId: filter.agentId } : {}, ...filter?.workflowId ? { workflowId: filter.workflowId } : {}, ...filter?.status ? { status: filter.status } : {} }); const views = schedules.map(toScheduleView).filter((s) => s !== null).filter((s) => filter?.agentId ? s.agentId !== void 0 : true); const agentOnly = filter?.threadId !== void 0 || filter?.resourceId !== void 0 || filter?.name !== void 0; if (!agentOnly) 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 chunkXSOONORA_cjs.MastraError({ id: "SCHEDULES_NOT_FOUND", domain: chunkXSOONORA_cjs.ErrorDomain.AGENT, category: chunkXSOONORA_cjs.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) { 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 ? computeNextFireAt(nextCron, { timezone: nextTimezone, after: Date.now() }) : void 0; const updated = 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 } : {} }); return toScheduleView(updated); } #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 chunkXSOONORA_cjs.MastraError({ id: "SCHEDULES_THREADLESS_OPTIONS", domain: chunkXSOONORA_cjs.ErrorDomain.AGENT, category: chunkXSOONORA_cjs.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 agentOnly = [ "prompt", "name", "signalType", "tagName", "attributes", "providerOptions", "ifActive", "ifIdle" ]; const offenders = agentOnly.filter((key) => patch[key] !== void 0); if (offenders.length > 0) { throw new chunkXSOONORA_cjs.MastraError({ id: "SCHEDULES_INVALID_WORKFLOW_PATCH", domain: chunkXSOONORA_cjs.ErrorDomain.AGENT, category: chunkXSOONORA_cjs.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 chunkXSOONORA_cjs.MastraError({ id: "SCHEDULES_NOT_FOUND", domain: chunkXSOONORA_cjs.ErrorDomain.AGENT, category: chunkXSOONORA_cjs.ErrorCategory.USER, text: `Schedule "${id}" not found.` }); } if (existing.status === "paused") return toScheduleView(existing); const updated = await store.updateSchedule(existing.id, { status: "paused" }); return toScheduleView(updated); } async resume(id) { const store = await this.#getStore(); const existing = await this.#load(id); if (!existing) { throw new chunkXSOONORA_cjs.MastraError({ id: "SCHEDULES_NOT_FOUND", domain: chunkXSOONORA_cjs.ErrorDomain.AGENT, category: chunkXSOONORA_cjs.ErrorCategory.USER, text: `Schedule "${id}" not found.` }); } if (existing.status === "active") return toScheduleView(existing); const nextFireAt = computeNextFireAt(existing.cron, { timezone: existing.timezone, after: Date.now() }); const updated = await store.updateSchedule(existing.id, { status: "active", nextFireAt }); return toScheduleView(updated); } async run(id) { const existing = await this.#load(id); if (!existing) { throw new chunkXSOONORA_cjs.MastraError({ id: "SCHEDULES_NOT_FOUND", domain: chunkXSOONORA_cjs.ErrorDomain.AGENT, category: chunkXSOONORA_cjs.ErrorCategory.USER, text: `Schedule "${id}" not found.` }); } const now = Date.now(); if (existing.target.type === "agent") { const claimId2 = `manual_${existing.id}_${now}`; await this.#mastra.pubsub.publish("agent-schedules", { type: "agent-schedule.fire", runId: claimId2, data: { scheduleId: existing.id, claimId: claimId2, scheduledFireAt: now, target: existing.target, triggerKind: "manual" } }); return { scheduleId: existing.id, claimId: claimId2, 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 }; } }; 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 }; } 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 }; } function toScheduleView(schedule) { return toAgentSchedule(schedule) ?? toWorkflowSchedule(schedule); } exports.AGENT_SCHEDULE_PREFIX = AGENT_SCHEDULE_PREFIX; exports.ScheduleInputSchema = ScheduleInputSchema; exports.ScheduleOutputSchema = ScheduleOutputSchema; exports.Schedules = Schedules; exports.WORKFLOW_SCHEDULE_PREFIX = WORKFLOW_SCHEDULE_PREFIX; exports.computeNextFireAt = computeNextFireAt; exports.toAgentSchedule = toAgentSchedule; exports.toScheduleView = toScheduleView; exports.toWorkflowSchedule = toWorkflowSchedule; exports.validateCron = validateCron; //# sourceMappingURL=chunk-5Z776ZCD.cjs.map //# sourceMappingURL=chunk-5Z776ZCD.cjs.map