@mastra/core
Version:
556 lines (555 loc) • 14.8 kB
JavaScript
const require_request_context = require("./request-context-ByoZMp-j.cjs");
const require_pull_transport = require("./pull-transport-BlEVfOcc.cjs");
//#region src/schedules/worker.ts
/** PubSub topic on which the scheduler publishes `agent-schedule.fire` events. */
const TOPIC_AGENT_SCHEDULES = "agent-schedules";
const DEFAULT_GROUP = "mastra-agent-schedules";
/**
* Consumes `agent-schedule.fire` events published by the scheduler and runs
* the configured agent — either by `sendSignal` (threaded) or
* `agent.generate` (threadless). Mirrors `OrchestrationWorker`'s
* subscribe-on-start / unsubscribe-on-stop lifecycle.
*
* Records the schedule trigger after dispatching so the trigger row
* carries the agent's runId (not just the scheduler's claim id),
* letting the UI link triggers to real agent runs.
*/
var AgentScheduleWorker = class extends require_pull_transport.MastraWorker {
name = "agent-schedule";
#config;
#transport;
#pushCb;
#running = false;
constructor(config = {}) {
super();
this.#config = config;
}
async init(deps) {
await super.init(deps);
if (!deps.mastra) throw new Error("AgentScheduleWorker requires Mastra instance");
}
async start() {
if (this.#running) return;
if (!this.deps) throw new Error("AgentScheduleWorker: call init() before start()");
if (!(this.deps.pubsub.supportedModes ?? ["pull"]).includes("pull")) {
const cb = (event, ack, nack) => {
this.#handleEvent(event, ack, nack);
};
this.#pushCb = cb;
await this.deps.pubsub.subscribe(TOPIC_AGENT_SCHEDULES, cb);
this.#running = true;
return;
}
const group = this.#config.group ?? DEFAULT_GROUP;
this.#transport = new require_pull_transport.PullTransport({
pubsub: this.deps.pubsub,
group,
topic: TOPIC_AGENT_SCHEDULES,
logger: this.deps.logger
});
await this.#transport.start({ route: (event, ack, nack) => this.#handleEvent(event, ack, nack) });
this.#running = true;
}
async stop() {
if (!this.#running) return;
try {
if (this.#transport) {
await this.#transport.stop();
this.#transport = void 0;
}
if (this.#pushCb && this.deps) {
await this.deps.pubsub.unsubscribe(TOPIC_AGENT_SCHEDULES, this.#pushCb);
this.#pushCb = void 0;
}
} finally {
this.#running = false;
}
}
get isRunning() {
return this.#running;
}
async #handleEvent(event, ack, nack) {
if (event.type !== "agent-schedule.fire") {
await ack?.();
return;
}
const mastra = this.mastra;
const payload = event.data;
try {
await this.#dispatch(mastra, payload);
await ack?.();
} catch (err) {
this.deps?.logger?.error("AgentScheduleWorker: error processing agent-schedule.fire", {
scheduleId: payload?.scheduleId,
claimId: payload?.claimId,
error: err
});
await nack?.();
}
}
async #dispatch(mastra, data) {
const { scheduleId, claimId, scheduledFireAt, target } = data;
const actualFireAt = Date.now();
const result = await executeAgentSchedule(mastra, scheduleId, target, {
triggerKind: data.triggerKind ?? "schedule-fire",
firedAt: new Date(actualFireAt),
logger: this.deps?.logger
});
await this.#recordTrigger({
scheduleId,
claimId,
scheduledFireAt,
actualFireAt,
outcome: result.outcome,
runId: result.runId,
error: result.reason,
triggerKind: data.triggerKind ?? "schedule-fire"
});
}
async #recordTrigger(args) {
const store = await this.deps?.storage.getStore("schedules");
if (!store) return;
try {
await store.recordTrigger({
scheduleId: args.scheduleId,
runId: args.runId ?? args.claimId,
scheduledFireAt: args.scheduledFireAt,
actualFireAt: args.actualFireAt,
outcome: args.outcome,
error: args.error,
triggerKind: args.triggerKind
});
} catch (err) {
this.deps?.logger?.error("AgentScheduleWorker: failed to record trigger", {
scheduleId: args.scheduleId,
claimId: args.claimId,
error: err
});
}
}
};
/**
* Best-effort delete of the schedule row. Self-clean is best-effort —
* an explicit `schedules.delete()` may have raced us. Swallow errors.
*/
async function selfClean(mastra, scheduleId) {
try {
const store = await mastra.getStorage()?.getStore("schedules");
if (!store) return;
await store.deleteSchedule(scheduleId);
} catch (error) {
mastra.getLogger?.()?.debug?.("agent-schedule self-clean failed", {
scheduleId,
error
});
}
}
/**
* Resolves the agent, runs the user `prepare` hook (if any), applies
* idle filters, and either `sendSignal`s into the target
* thread or runs `agent.generate`. The returned `runId` is the agent
* run id from the SDK call (when a run was actually started), suitable
* for trigger-row linkability.
*
* `outcome` is the final outcome for the schedule trigger row and is
* also the one that drove the `onFinish`/`onError`/`onAbort` hook
* selection. `status` is retained for back-compat with existing tests.
*/
async function executeAgentSchedule(mastra, scheduleId, target, ctx = {}) {
const { agentId } = target;
const trigger = {
kind: ctx.triggerKind === "manual" ? "manual" : "cron",
firedAt: ctx.firedAt ?? /* @__PURE__ */ new Date()
};
const log = ctx.logger ?? mastra.getLogger?.();
const agent = (() => {
try {
return mastra.getAgentById(agentId);
} catch {
return null;
}
})();
if (!agent) {
await selfClean(mastra, scheduleId);
return {
status: "agent-missing",
outcome: "failed",
reason: `agent "${agentId}" no longer registered`
};
}
const hooks = mastra.__getScheduleHooks?.() ?? void 0;
const scheduleRef = hooks ? await loadScheduleRef(mastra, scheduleId, target, log) : scheduleRefFromTarget(scheduleId, target);
const rowDefaults = buildEffectiveFromTarget(target);
let prepared;
if (hooks?.prepare) try {
const prepareCtx = {
mastra,
agentId,
schedule: scheduleRef,
trigger
};
prepared = await hooks.prepare(prepareCtx);
} catch (err) {
await safeHookCall(log, () => hooks.onError?.({
mastra,
agentId,
schedule: scheduleRef,
trigger,
phase: "prepare",
error: err instanceof Error ? err : new Error(String(err)),
effective: rowDefaults
}));
return {
status: "invalid-input",
outcome: "failed",
reason: err instanceof Error ? err.message : String(err)
};
}
if (prepared === null) {
await safeHookCall(log, () => hooks?.onFinish?.({
mastra,
agentId,
schedule: scheduleRef,
trigger,
outcome: "skipped",
effective: rowDefaults
}));
return {
status: "fired",
outcome: "skipped"
};
}
const effective = mergeEffective(rowDefaults, prepared);
const scheduleRunMeta = {
scheduleId,
...effective.threadId ? { threadId: effective.threadId } : {}
};
if (effective.threadId) {
if (!effective.resourceId) {
const reason = "resourceId required when threadId is set";
await safeHookCall(log, () => hooks?.onError?.({
mastra,
agentId,
schedule: scheduleRef,
trigger,
phase: "run",
error: /* @__PURE__ */ new Error(reason),
effective
}));
return {
status: "invalid-input",
outcome: "failed",
reason
};
}
const memory = await agent.getMemory();
if (memory) {
if (!await memory.getThreadById({ threadId: effective.threadId })) {
await selfClean(mastra, scheduleId);
const reason = `thread "${effective.threadId}" not found`;
await safeHookCall(log, () => hooks?.onError?.({
mastra,
agentId,
schedule: scheduleRef,
trigger,
phase: "run",
error: new Error(reason),
effective
}));
return {
status: "thread-missing",
outcome: "failed",
reason
};
}
}
let signalResult;
try {
const signalType = effective.signalType ?? "notification";
const signalBase = {
tagName: effective.tagName ?? "schedule",
contents: effective.prompt,
...effective.attributes ? { attributes: effective.attributes } : {},
providerOptions: mergeProviderOptions(effective.providerOptions, scheduleRunMeta)
};
const signal = signalType === "state" ? {
...signalBase,
type: "state"
} : {
...signalBase,
type: signalType
};
signalResult = agent.sendSignal(signal, {
resourceId: effective.resourceId,
threadId: effective.threadId,
...effective.ifActive ? { ifActive: effective.ifActive } : {},
...effective.ifIdle ? { ifIdle: buildIfIdleOptions(effective.ifIdle) } : {}
});
} catch (err) {
const error = err instanceof Error ? err : new Error(String(err));
await safeHookCall(log, () => hooks?.onError?.({
mastra,
agentId,
schedule: scheduleRef,
trigger,
phase: "run",
error,
effective
}));
return {
status: "invalid-input",
outcome: "failed",
reason: error.message
};
}
let settled;
try {
settled = await signalResult.accepted;
} catch (err) {
const error = err instanceof Error ? err : new Error(String(err));
await safeHookCall(log, () => hooks?.onError?.({
mastra,
agentId,
schedule: scheduleRef,
trigger,
phase: "run",
error,
effective
}));
return {
status: "invalid-input",
outcome: "failed",
reason: error.message
};
}
const action = settled.action;
const runId = "runId" in settled ? settled.runId : void 0;
if (action === "deliver") {
await safeHookCall(log, () => hooks?.onFinish?.({
mastra,
agentId,
schedule: scheduleRef,
trigger,
outcome: "delivered",
runId,
joinedExistingRun: true,
effective
}));
return {
status: "signal-accepted",
outcome: "delivered",
runId
};
}
if (action === "persist") {
if (signalResult.persisted) try {
await signalResult.persisted;
} catch {}
await safeHookCall(log, () => hooks?.onFinish?.({
mastra,
agentId,
schedule: scheduleRef,
trigger,
outcome: "persisted",
runId,
effective
}));
return {
status: "signal-accepted",
outcome: "persisted",
runId
};
}
if (action === "discard") {
await safeHookCall(log, () => hooks?.onFinish?.({
mastra,
agentId,
schedule: scheduleRef,
trigger,
outcome: "discarded",
runId,
effective
}));
return {
status: "signal-accepted",
outcome: "discarded",
runId
};
}
if (action === "blocked") {
await safeHookCall(log, () => hooks?.onFinish?.({
mastra,
agentId,
schedule: scheduleRef,
trigger,
outcome: "skipped",
runId,
effective
}));
return {
status: "skipped-thread-blocked",
outcome: "skipped",
runId
};
}
await safeHookCall(log, () => hooks?.onFinish?.({
mastra,
agentId,
schedule: scheduleRef,
trigger,
outcome: "succeeded",
runId,
effective
}));
return {
status: "signal-accepted",
outcome: "succeeded",
runId
};
}
try {
const result = await agent.generate(effective.prompt, { providerOptions: mergeProviderOptions(effective.providerOptions, scheduleRunMeta) });
const runId = extractRunId(result);
await safeHookCall(log, () => hooks?.onFinish?.({
mastra,
agentId,
schedule: scheduleRef,
trigger,
outcome: "succeeded",
runId,
result: extractRunSnapshot(result),
effective
}));
return {
status: "fired",
outcome: "succeeded",
runId
};
} catch (err) {
const error = err instanceof Error ? err : new Error(String(err));
if (isAbortError(error)) {
await safeHookCall(log, () => hooks?.onAbort?.({
mastra,
agentId,
schedule: scheduleRef,
trigger,
runId: extractRunId(error) ?? scheduleId,
effective
}));
return {
status: "fired",
outcome: "aborted"
};
}
await safeHookCall(log, () => hooks?.onError?.({
mastra,
agentId,
schedule: scheduleRef,
trigger,
phase: "run",
error,
effective
}));
return {
status: "invalid-input",
outcome: "failed",
reason: error.message
};
}
}
function buildEffectiveFromTarget(target) {
return {
threadId: target.threadId,
resourceId: target.resourceId,
prompt: target.prompt,
signalType: target.signalType,
tagName: target.tagName,
attributes: target.attributes,
providerOptions: target.providerOptions,
ifActive: target.ifActive,
ifIdle: target.ifIdle
};
}
/**
* Maps the stored, JSON-safe `ifIdle` config onto the signal API's
* `AgentSignalIfIdleOptions`, rehydrating the plain `streamOptions.requestContext`
* object into a live `RequestContext` before the wake signal runs.
*/
function buildIfIdleOptions(ifIdle) {
const requestContext = ifIdle.streamOptions?.requestContext;
return {
...ifIdle.behavior ? { behavior: ifIdle.behavior } : {},
...ifIdle.attributes ? { attributes: ifIdle.attributes } : {},
...requestContext ? { streamOptions: { requestContext: new require_request_context.RequestContext(Object.entries(requestContext)) } } : {}
};
}
function mergeEffective(base, overrides) {
if (!overrides) return base;
return {
...base,
...overrides
};
}
function mergeProviderOptions(fromHook, scheduleRunMeta) {
const base = fromHook ?? {};
const baseMastra = base.mastra ?? {};
return {
...base,
mastra: {
...baseMastra,
schedule: scheduleRunMeta
}
};
}
async function loadScheduleRef(mastra, scheduleId, target, logger) {
try {
const schedule = await mastra.schedules.get(scheduleId);
if (schedule && schedule.agentId !== void 0) return {
...schedule,
agentId: schedule.agentId
};
} catch (err) {
logger?.debug?.("AgentScheduleWorker: failed to load schedule row for hook context", {
scheduleId,
error: err
});
}
return scheduleRefFromTarget(scheduleId, target);
}
/** Minimal hook-context projection built from the fire event's target. */
function scheduleRefFromTarget(scheduleId, target) {
return {
id: scheduleId,
agentId: target.agentId,
...target.name !== void 0 ? { name: target.name } : {}
};
}
async function safeHookCall(logger, fn) {
try {
await fn();
} catch (err) {
logger?.error?.("AgentScheduleWorker: hook threw, ignoring", { error: err });
}
}
function isAbortError(err) {
if (!err || typeof err !== "object") return false;
return err.name === "AbortError";
}
function extractRunId(value) {
if (value && typeof value === "object" && "runId" in value) {
const runId = value.runId;
if (typeof runId === "string") return runId;
}
}
function extractRunSnapshot(value) {
if (!value || typeof value !== "object") return void 0;
const v = value;
const snapshot = {};
if (typeof v.text === "string") snapshot.text = v.text;
if (v.usage && typeof v.usage === "object") snapshot.usage = v.usage;
if (typeof v.finishReason === "string") snapshot.finishReason = v.finishReason;
return Object.keys(snapshot).length > 0 ? snapshot : void 0;
}
//#endregion
exports.AgentScheduleWorker = AgentScheduleWorker;
//# sourceMappingURL=worker-D6tELOAF.cjs.map