UNPKG

@mastra/core

Version:
556 lines (555 loc) 14.8 kB
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