UNPKG

@mastra/core

Version:

Mastra is a framework for building AI-powered applications and agents with a modern TypeScript stack.

1,056 lines (1,045 loc) 38.6 kB
'use strict'; var crypto = require('crypto'); var v4 = require('zod/v4'); // src/background-tasks/manager.ts // src/background-tasks/workflow-id.ts var BACKGROUND_TASK_WORKFLOW_ID = "__background-task"; // src/background-tasks/manager.ts var TOPIC_DISPATCH = "background-tasks"; var TOPIC_RESULT = "background-tasks-result"; var WORKER_GROUP = "background-task-workers"; var BackgroundTaskManager = class { pubsub; config; #mastra; // Per-task contexts — keyed by task ID, holds closures from the caller's stream. /** @internal — read by the workflow-engine step bodies in workflow.ts */ taskContexts = /* @__PURE__ */ new Map(); // Static executors keyed by tool name. Populated by `Mastra` for every // registered tool, and by `BackgroundTaskWorker.#wireStaticTools` on // standalone worker processes. Used as the fallback for cross-process // dispatch where the producer's per-task closure (taskContexts) is not // visible — a remote worker resolves the tool by name instead. staticExecutors = /* @__PURE__ */ new Map(); // Track active AbortControllers for running tasks (for cancellation + timeout) /** @internal — read by the workflow-engine step bodies in workflow.ts */ activeAbortControllers = /* @__PURE__ */ new Map(); // Pubsub callbacks (kept for unsubscribe) workerCallback; resultCallback; shuttingDown = false; // Cleanup interval handle cleanupInterval; // Tracks the in-flight `init(pubsub)` so consumers can await readiness. // Mastra fires init as fire-and-forget in `#ensureBackgroundTaskManager`, // so without this any caller that hits `enqueue`/`resume`/`cancel` // before init completes races against worker subscription + workflow // registration. Public methods that depend on init await this promise // before doing work. initPromise; constructor(config = { enabled: false }) { this.config = { globalConcurrency: config.globalConcurrency ?? 10, perAgentConcurrency: config.perAgentConcurrency ?? 5, backpressure: config.backpressure ?? "queue", defaultTimeoutMs: config.defaultTimeoutMs ?? 3e5, ...config }; } __registerMastra(mastra) { this.#mastra = mastra; } async getStorage() { const storage = this.#mastra?.getStorage(); if (!storage) { throw new Error("Storage is not initialized"); } const bgStore = await storage.getStore("backgroundTasks"); if (!bgStore) { throw new Error("Background tasks storage is not available"); } return bgStore; } async init(pubsub) { if (this.initPromise) return this.initPromise; this.initPromise = this.#doInit(pubsub); return this.initPromise; } async #doInit(pubsub) { this.pubsub = pubsub; this.workerCallback = async (event, ack) => { if (event.type === "task.dispatch") { await this.handleDispatch(event); } else if (event.type === "task.resume") { await this.handleResume(event); } else if (event.type === "task.cancel") { this.handleCancel(event); } await ack?.(); }; this.resultCallback = async (event, ack) => { if (event.type === "task.completed" || event.type === "task.failed") { await this.handleResult(event); } await ack?.(); }; if (this.#mastra) { const { buildBackgroundTaskWorkflow } = await import('./workflow-NQFS7R77.cjs'); const workflow = buildBackgroundTaskWorkflow(this); if (!this.#mastra.__hasInternalWorkflow(BACKGROUND_TASK_WORKFLOW_ID)) { this.#mastra.__registerInternalWorkflow( workflow ); } } await this.pubsub.subscribe(TOPIC_DISPATCH, this.workerCallback, { group: WORKER_GROUP }); await this.pubsub.subscribe(TOPIC_RESULT, this.resultCallback); await this.recoverStaleTasks(); const cleanupConfig = this.config.cleanup; if (cleanupConfig) { const intervalMs = cleanupConfig.cleanupIntervalMs ?? 6e4; this.cleanupInterval = setInterval(() => { void this.cleanup(); }, intervalMs); } } // --- Per-task context registration --- /** * Register per-task hooks (executor, stream emitter, result injector). * Called internally by createBackgroundTask or directly for advanced usage. */ registerTaskContext(taskId, context) { this.taskContexts.set(taskId, context); } /** * Remove per-task hooks. Called after task reaches terminal state. */ deregisterTaskContext(taskId) { this.taskContexts.delete(taskId); } /** * Register a tool executor by tool name. Used for cross-process dispatch: * when a worker in a different process picks up a `task.dispatch` event, * it has no per-task closure (`taskContexts`) for that taskId, but it can * resolve the executor by tool name via this registry. */ registerStaticExecutor(toolName, executor) { if (this.staticExecutors.has(toolName)) { this.#mastra?.getLogger?.()?.debug?.(`Overwriting existing static executor for tool "${toolName}"`); } this.staticExecutors.set(toolName, executor); } /** * Symmetric to `registerStaticExecutor`. Called when a tool is removed * from `Mastra`. */ unregisterStaticExecutor(toolName) { this.staticExecutors.delete(toolName); } /** * Look up an executor by tool name. Read by the workflow-step body in * `workflow.ts:runAttemptStep` as a fallback when no per-task `TaskContext` * is registered (cross-process path). */ getStaticExecutor(toolName) { return this.staticExecutors.get(toolName); } // --- Core operations --- /** * Enqueue a task for background execution. * Prefer `createBackgroundTask()` which returns a self-contained handle. */ async enqueue(payload, context) { if (this.shuttingDown) { throw new Error("BackgroundTaskManager is shutting down, cannot enqueue new tasks"); } if (this.initPromise) await this.initPromise; const task = { id: this.#mastra?.generateId() ?? crypto.randomUUID(), status: "pending", toolName: payload.toolName, toolCallId: payload.toolCallId, args: payload.args, agentId: payload.agentId, threadId: payload.threadId, resourceId: payload.resourceId, runId: payload.runId, retryCount: 0, maxRetries: payload.maxRetries ?? this.config.defaultRetries?.maxRetries ?? 0, timeoutMs: payload.timeoutMs ?? this.config.defaultTimeoutMs, createdAt: /* @__PURE__ */ new Date() }; if (context) { this.registerTaskContext(task.id, context); } const storage = await this.getStorage(); await storage.createTask(task); const canRun = await this.checkConcurrency(task.agentId); if (canRun) { await this.dispatch(task); return { task }; } switch (this.config.backpressure) { case "reject": this.deregisterTaskContext(task.id); await storage.deleteTask(task.id); throw new Error(`Concurrency limit reached, cannot enqueue task for tool "${task.toolName}"`); case "fallback-sync": this.deregisterTaskContext(task.id); await storage.deleteTask(task.id); return { task, fallbackToSync: true }; case "queue": default: return { task }; } } async cancel(taskId) { if (this.initPromise) await this.initPromise; const storage = await this.getStorage(); const task = await storage.getTask(taskId); if (!task) { throw new Error(`Task not found: ${taskId}`); } if (task.status === "completed" || task.status === "failed" || task.status === "cancelled" || task.status === "timed_out") { return; } if (task.status === "pending") { await storage.updateTask(taskId, { status: "cancelled", completedAt: /* @__PURE__ */ new Date() }); const cancelledTask = await storage.getTask(taskId); if (cancelledTask) await this.publishLifecycleEvent("task.cancelled", cancelledTask); this.deregisterTaskContext(taskId); return; } if (task.status === "suspended") { await storage.updateTask(taskId, { status: "cancelled", completedAt: /* @__PURE__ */ new Date() }); if (this.#mastra) { try { const workflow = this.#mastra.__getInternalWorkflow(BACKGROUND_TASK_WORKFLOW_ID); const wrapper = await workflow.createRun({ runId: taskId }); await wrapper.cancel(); } catch (err) { this.#mastra?.getLogger?.()?.warn(`background-task workflow cancel failed for ${taskId}:`, err); } } const cancelledTask = await storage.getTask(taskId); if (cancelledTask) await this.publishLifecycleEvent("task.cancelled", cancelledTask); this.deregisterTaskContext(taskId); return; } if (task.status === "running") { await storage.updateTask(taskId, { status: "cancelled", completedAt: /* @__PURE__ */ new Date() }); const controller = this.activeAbortControllers.get(taskId); if (controller) { controller.abort(new Error("Task cancelled")); this.activeAbortControllers.delete(taskId); } if (this.#mastra) { try { const workflow = this.#mastra.__getInternalWorkflow(BACKGROUND_TASK_WORKFLOW_ID); const wrapper = await workflow.createRun({ runId: taskId }); await wrapper.cancel(); } catch (err) { this.#mastra?.getLogger?.()?.warn(`background-task workflow cancel failed for ${taskId}:`, err); } } const cancelledTask = await storage.getTask(taskId); if (cancelledTask) await this.publishLifecycleEvent("task.cancelled", cancelledTask); this.deregisterTaskContext(taskId); await this.pubsub.publish(TOPIC_DISPATCH, { type: "task.cancel", data: { taskId }, runId: taskId }); } } /** * Resume a suspended task. The tool executor must be re-registered via * `registerTaskContext(taskId, ...)` before calling this if the original * registration is gone (e.g. process restart) — the manager doesn't * rehydrate executor closures from storage. * * `resumeData` is forwarded to the tool's `execute` options on the * resumed run. */ async resume(taskId, resumeData) { if (!this.#mastra) { throw new Error("Mastra is not registered with this manager"); } if (this.initPromise) await this.initPromise; const storage = await this.getStorage(); const task = await storage.getTask(taskId); if (!task) { throw new Error(`Task not found: ${taskId}`); } if (task.status !== "suspended") { throw new Error(`Cannot resume task in status '${task.status}' (expected 'suspended')`); } const canRun = await this.checkConcurrency(task.agentId); if (!canRun) { throw new Error(`Concurrency limit reached, cannot resume task "${taskId}" \u2014 retry once a slot is available`); } await this.pubsub.publish(TOPIC_DISPATCH, { type: "task.resume", data: { taskId, resumeData }, runId: taskId }); return task; } async getTask(taskId) { const storage = await this.getStorage(); return storage.getTask(taskId); } async listTasks(filter = {}) { const storage = await this.getStorage(); return storage.listTasks(filter); } /** * Deletes old completed/failed/cancelled/timed_out task records from storage. */ async cleanup() { const completedTtlMs = this.config.cleanup?.completedTtlMs ?? 36e5; const failedTtlMs = this.config.cleanup?.failedTtlMs ?? 864e5; const now = Date.now(); const storage = await this.getStorage(); await storage.deleteTasks({ status: ["completed"], toDate: new Date(now - completedTtlMs), dateFilterBy: "completedAt" }); await storage.deleteTasks({ status: ["failed", "cancelled", "timed_out"], toDate: new Date(now - failedTtlMs), dateFilterBy: "completedAt" }); } /** * Returns a promise that resolves when the next task from the given set * reaches a terminal state. */ async waitForNextTask(taskIds, options) { const storage = await this.getStorage(); const isTerminal = (status) => status === "completed" || status === "failed" || status === "cancelled" || status === "timed_out"; for (const id of taskIds) { const task = await storage.getTask(id); if (task && isTerminal(task.status)) { return task; } } return new Promise((resolve, reject) => { const startTime = Date.now(); const timeout = options?.timeoutMs ? setTimeout(() => { clearInterval(pollInterval); if (progressInterval) clearInterval(progressInterval); reject(new Error("Timed out waiting for background task")); }, options.timeoutMs) : void 0; const progressInterval = options?.onProgress ? setInterval(() => { options.onProgress(Date.now() - startTime); }, options.progressIntervalMs ?? 3e3) : void 0; const pollInterval = setInterval(async () => { for (const id of taskIds) { const task = await storage.getTask(id); if (task && isTerminal(task.status)) { clearInterval(pollInterval); if (timeout) clearTimeout(timeout); if (progressInterval) clearInterval(progressInterval); resolve(task); return; } } }, 50); }); } /** * Returns a ReadableStream of all background task lifecycle events, * filtered by optional criteria. Intended to be piped directly to an SSE response. * * On connection, emits the current state of all non-terminal tasks as a snapshot, * then subscribes to live pubsub events for subsequent updates. * * Events include: * - `task.running` (status: 'running') — task picked up by a worker * - `task.completed` (status: 'completed') — task finished successfully * - `task.failed` (status: 'failed' or 'timed_out') — task errored or timed out * - `task.cancelled` (status: 'cancelled') — task was cancelled * - `task.suspended` (status: 'suspended') — task paused via `suspend()` from * inside its tool executor; resume with `manager.resume(taskId, data)` * - `task.resumed` (status: 'running') — suspended task resumed * * The stream stays open until the caller's AbortSignal fires (client disconnect). */ stream(options) { const manager = this; const pubsub = this.pubsub; const { agentId, runId, threadId, resourceId, abortSignal, taskId } = options ?? {}; const EVENT_STATUS_MAP = { "task.running": "running", "task.output": "running", "task.completed": "completed", "task.failed": "failed", "task.cancelled": "cancelled", "task.suspended": "suspended", "task.resumed": "running" }; const CHUNK_EVENT_MAP = { "task.running": "background-task-running", "task.output": "background-task-output", "task.completed": "background-task-completed", "task.failed": "background-task-failed", "task.cancelled": "background-task-cancelled", "task.suspended": "background-task-suspended", "task.resumed": "background-task-resumed" }; return new ReadableStream({ async start(controller) { const handler = async (event) => { const status = EVENT_STATUS_MAP[event.type]; if (!status) return; const data = event.data; if (agentId && data.agentId !== agentId) return; if (runId && data.runId !== runId) return; if (threadId && data.threadId !== threadId) return; if (resourceId && data.resourceId !== resourceId) return; if (taskId && data.taskId !== taskId) return; const payload = { taskId: data.taskId, toolName: data.toolName, toolCallId: data.toolCallId, agentId: data.agentId, runId: data.runId }; switch (event.type) { case "task.running": payload.startedAt = data.startedAt; payload.args = data.args; break; case "task.completed": payload.completedAt = data.completedAt; payload.result = data.result; break; case "task.failed": payload.completedAt = data.completedAt; payload.error = data.error; break; case "task.cancelled": payload.completedAt = data.completedAt; break; case "task.output": payload.payload = data.chunk; break; case "task.suspended": payload.suspendPayload = data.suspendPayload; payload.suspendedAt = data.suspendedAt; payload.args = data.args; break; case "task.resumed": payload.startedAt = data.startedAt; payload.args = data.args; break; } try { controller.enqueue({ type: CHUNK_EVENT_MAP[event.type], payload }); } catch { } }; void pubsub.subscribe(TOPIC_RESULT, handler); abortSignal?.addEventListener("abort", () => { void pubsub.unsubscribe(TOPIC_RESULT, handler); try { controller.close(); } catch { } }); try { const storage = await manager.getStorage(); if (taskId) { const task = await storage.getTask(taskId); if (task && task.status === "running") { controller.enqueue({ type: "background-task-running", payload: { taskId: task.id, toolName: task.toolName, toolCallId: task.toolCallId, agentId: task.agentId, runId: task.runId, startedAt: task.startedAt, args: task.args } }); } } else { const { tasks: existing } = await storage.listTasks({ agentId, runId, threadId, resourceId, status: ["running"] }); for (const task of existing) { if (abortSignal?.aborted) break; try { controller.enqueue({ type: "background-task-running", payload: { taskId: task.id, toolName: task.toolName, toolCallId: task.toolCallId, agentId: task.agentId, runId: task.runId, startedAt: task.startedAt, args: task.args } }); } catch { break; } } } } catch { } } }); } async shutdown() { this.shuttingDown = true; if (this.cleanupInterval) { clearInterval(this.cleanupInterval); this.cleanupInterval = void 0; } if (this.workerCallback) { await this.pubsub.unsubscribe(TOPIC_DISPATCH, this.workerCallback); } if (this.resultCallback) { await this.pubsub.unsubscribe(TOPIC_RESULT, this.resultCallback); } this.taskContexts.clear(); await this.pubsub.flush(); } // --- Internal --- async dispatch(task) { await this.pubsub.publish(TOPIC_DISPATCH, { type: "task.dispatch", data: { taskId: task.id, toolName: task.toolName, toolCallId: task.toolCallId, args: task.args, agentId: task.agentId, threadId: task.threadId, resourceId: task.resourceId, timeoutMs: task.timeoutMs, maxRetries: task.maxRetries, runId: task.runId }, runId: task.id }); } /** * Handles a task.dispatch event. Returns true if the message was nacked (for retry). */ async handleDispatch(event) { const { taskId } = event.data; const deliveryAttempt = event.deliveryAttempt ?? 1; let nacked = false; const storage = await this.getStorage(); const task = await storage.getTask(taskId); if (!task || task.status === "cancelled") { this.deregisterTaskContext(taskId); return false; } await storage.updateTask(taskId, { status: "running", startedAt: /* @__PURE__ */ new Date(), retryCount: deliveryAttempt - 1 }); const runningTask = await storage.getTask(taskId); if (runningTask) await this.publishLifecycleEvent("task.running", runningTask); if (this.#mastra) { if (runningTask) void this.runLocalExecutionHook(runningTask); const workflow = this.#mastra.__getInternalWorkflow(BACKGROUND_TASK_WORKFLOW_ID); const run = await workflow.createRun({ runId: taskId }); void run.start({ inputData: { taskId } }).then((result) => { if (result.status !== "suspended") { void workflow.deleteWorkflowRunById(taskId); } }).catch((err) => { this.#mastra?.getLogger?.()?.error(`background-task workflow start failed for ${taskId}:`, err); }).finally(() => { void this.drainPending(); }); } return nacked; } /** * Handles a task.resume event. Mirrors the workflow branch of handleDispatch * but resumes an existing run from its suspended snapshot instead of starting * a fresh one. Concurrency gating, suspended-status validation, and the * `task.resumed` lifecycle publish all happen here so a different process * than the one that suspended the task can drive the resume. */ async handleResume(event) { const { taskId, resumeData } = event.data; const storage = await this.getStorage(); const task = await storage.getTask(taskId); if (!task || task.status !== "suspended") { return; } await storage.updateTask(taskId, { status: "running", startedAt: /* @__PURE__ */ new Date(), suspendPayload: void 0, suspendedAt: void 0 }); const resumedTask = await storage.getTask(taskId); if (resumedTask) { await this.publishLifecycleEvent("task.resumed", resumedTask); } if (!this.#mastra) return; const workflow = this.#mastra.__getInternalWorkflow(BACKGROUND_TASK_WORKFLOW_ID); const run = await workflow.createRun({ runId: taskId }); void run.resume({ resumeData }).then((result) => { if (result.status !== "suspended") { void workflow.deleteWorkflowRunById(taskId); } }).catch((err) => { this.#mastra?.getLogger?.()?.error(`background-task workflow resume failed for ${taskId}:`, err); }).finally(() => { void this.drainPending(); }); } /** * Run per-task hooks (onChunk, onResult, onComplete/onFailed) locally in the * worker path, before publishing the terminal lifecycle event. Ensures * memory / stream state is consistent by the time any pubsub subscriber is * notified. After running, the task context is deregistered so * `handleResult` (which also fires from pubsub) becomes a no-op for this * task in the same process. * * In distributed deployments where the worker runs in a different process * from the dispatcher, `this.taskContexts` won't contain an entry for * `task.id` — this method is a no-op there, and `handleResult` in the * dispatching process runs the hooks instead. */ /** * Terminal-state hooks only. Called when a task reaches `'completed'` or * `'failed'`. Suspend is non-terminal — see `runLocalSuspendHooks` for that * path. * * @internal — also called by the workflow-engine step bodies in workflow.ts */ async runLocalCompletionHooks(task, status, extras) { const ctx = this.taskContexts.get(task.id); if (!ctx) return; try { if (status === "completed") { ctx.onChunk?.({ type: "background-task-completed", payload: { taskId: task.id, toolName: task.toolName, toolCallId: task.toolCallId, runId: task.runId, result: extras.result, completedAt: task.completedAt, agentId: task.agentId } }); await ctx.onResult?.({ runId: task.runId, taskId: task.id, toolCallId: task.toolCallId, toolName: task.toolName, agentId: task.agentId, threadId: task.threadId, resourceId: task.resourceId, result: extras.result, status: "completed", completedAt: task.completedAt, startedAt: task.startedAt }); await ctx.onComplete?.(task); } else { ctx.onChunk?.({ type: "background-task-failed", payload: { taskId: task.id, toolName: task.toolName, toolCallId: task.toolCallId, runId: task.runId, error: extras.error ?? { message: "Unknown error" }, completedAt: task.completedAt, agentId: task.agentId } }); await ctx.onResult?.({ runId: task.runId, taskId: task.id, toolCallId: task.toolCallId, toolName: task.toolName, agentId: task.agentId, threadId: task.threadId, resourceId: task.resourceId, error: extras.error, status: "failed", completedAt: task.completedAt, startedAt: task.startedAt }); await ctx.onFailed?.(task); } } finally { this.deregisterTaskContext(task.id); } } /** * Per-task suspend hooks. Fires `ctx.onResult({ status: 'suspended', ... })` * so the message list / memory pick up the suspension as the tool's * current invocation state. Does NOT deregister the task context — resume * needs the executor closure intact. * * @internal — called by the workflow-engine step bodies in workflow.ts */ async runLocalSuspendHooks(task) { const ctx = this.taskContexts.get(task.id); if (!ctx) return; await ctx.onExecution?.({ runId: task.runId, taskId: task.id, toolCallId: task.toolCallId, toolName: task.toolName, agentId: task.agentId, threadId: task.threadId, resourceId: task.resourceId, startedAt: task.startedAt, suspendedAt: task.suspendedAt }); } /** @internal — also called by the workflow-engine step bodies in workflow.ts */ async runLocalExecutionHook(task) { const ctx = this.taskContexts.get(task.id); if (!ctx) return; try { await ctx.onExecution?.({ runId: task.runId, taskId: task.id, toolCallId: task.toolCallId, toolName: task.toolName, agentId: task.agentId, threadId: task.threadId, resourceId: task.resourceId, startedAt: task.startedAt }); } catch { } } async handleResult(event) { const { taskId, toolName, toolCallId, threadId, resourceId, runId } = event.data; const storage = await this.getStorage(); const task = await storage.getTask(taskId); if (task?.completedAt) { const ctx = this.taskContexts.get(taskId); if (event.type === "task.completed") { ctx?.onChunk?.({ type: "background-task-completed", payload: { taskId, toolName, toolCallId, runId, result: event.data.result, completedAt: task.completedAt, agentId: task.agentId } }); await ctx?.onResult?.({ runId, taskId, toolCallId, toolName, agentId: event.data.agentId, threadId, resourceId, result: event.data.result, status: "completed", completedAt: task.completedAt, startedAt: task.startedAt }); if (task) { await Promise.all([ctx?.onComplete?.(task), this.config.onTaskComplete?.(task)]); } } if (event.type === "task.failed") { ctx?.onChunk?.({ type: "background-task-failed", payload: { taskId, toolName, toolCallId, runId, error: event.data.error, completedAt: task.completedAt, agentId: task.agentId } }); await ctx?.onResult?.({ runId, taskId, toolCallId, toolName, agentId: event.data.agentId, threadId, resourceId, error: event.data.error, status: "failed", completedAt: task.completedAt, startedAt: task.startedAt }); if (task) { await Promise.all([ctx?.onFailed?.(task), this.config.onTaskFailed?.(task)]); } } this.deregisterTaskContext(taskId); } } handleCancel(event) { const { taskId } = event.data; const controller = this.activeAbortControllers.get(taskId); if (controller) { controller.abort(new Error("Task cancelled")); this.activeAbortControllers.delete(taskId); } this.deregisterTaskContext(taskId); } /** @internal — also called by the workflow-engine step bodies in workflow.ts */ async publishLifecycleEvent(type, task) { await this.pubsub.publish(TOPIC_RESULT, { type, data: { taskId: task.id, toolName: task.toolName, toolCallId: task.toolCallId, runId: task.runId, agentId: task.agentId, threadId: task.threadId, resourceId: task.resourceId, args: task.args, result: task.result, error: task.error, chunk: task.chunk, completedAt: task.completedAt, startedAt: task.startedAt, suspendPayload: task.suspendPayload, suspendedAt: task.suspendedAt }, runId: task.id }); } async checkConcurrency(agentId) { const storage = await this.getStorage(); const globalRunning = await storage.getRunningCount(); if (globalRunning >= this.config.globalConcurrency) { return false; } const agentRunning = await storage.getRunningCountByAgent(agentId); if (agentRunning >= this.config.perAgentConcurrency) { return false; } return true; } async drainPending() { const storage = await this.getStorage(); const { tasks: pending } = await storage.listTasks({ status: "pending", orderBy: "createdAt", orderDirection: "asc" }); for (const task of pending) { if (await this.checkConcurrency(task.agentId)) { await this.dispatch(task); } } } /** * Recovers tasks left in 'running' or 'pending' state from a previous process. */ async recoverStaleTasks() { try { const storage = await this.getStorage(); const { tasks: staleTasks } = await storage.listTasks({ status: "running" }); for (const task of staleTasks) { if (task.maxRetries > 0) { await storage.updateTask(task.id, { status: "pending", startedAt: void 0 }); } else { await storage.updateTask(task.id, { status: "failed", error: { message: "Worker process terminated before task completed" }, completedAt: /* @__PURE__ */ new Date() }); } } const { tasks: pendingTasks } = await storage.listTasks({ status: "pending", orderBy: "createdAt", orderDirection: "asc" }); for (const task of pendingTasks) { if (await this.checkConcurrency(task.agentId)) { await this.dispatch(task); } } } catch (error) { const logger = this.#mastra?.getLogger(); if (logger) { logger.error("Failed to recover stale background tasks", error); } } } }; // src/background-tasks/create.ts function createBackgroundTask(manager, options) { const { context, ...payload } = options; let taskId; return { get task() { if (!taskId) throw new Error("Task has not been dispatched yet"); return { id: taskId }; }, async dispatch() { const result = await manager.enqueue(payload, context); taskId = result.task.id; return result; }, async checkIfSuspended(args) { const result = await manager.listTasks({ toolCallId: args.toolCallId, runId: args.runId, agentId: args.agentId, threadId: args.threadId, resourceId: args.resourceId, toolName: args.toolName, status: "suspended" }); if (result.total > 0) { const task = result.tasks[0]; if (task) { taskId = task.id; return true; } } return false; }, async resume(resumeData) { if (!taskId) throw new Error("Task has not been dispatched yet"); return manager.resume(taskId, resumeData); }, async cancel() { if (!taskId) throw new Error("Task has not been dispatched yet"); return manager.cancel(taskId); }, async waitForCompletion(waitOptions) { if (!taskId) throw new Error("Task has not been dispatched yet"); return manager.waitForNextTask([taskId], waitOptions); } }; } // src/background-tasks/resolve-config.ts function resolveBackgroundConfig({ llmBgOverrides, toolName, toolConfig, agentConfig, managerConfig }) { const llmOverride = llmBgOverrides; if (agentConfig?.disabled) { return { runInBackground: false, timeoutMs: managerConfig?.defaultTimeoutMs ?? 3e5, maxRetries: managerConfig?.defaultRetries?.maxRetries ?? 0 }; } const agentToolConfig = resolveAgentToolConfig(toolName, agentConfig); const baseEnabled = agentToolConfig?.enabled ?? toolConfig?.enabled ?? false; const enabled = baseEnabled ? llmOverride?.enabled ?? true : false; const timeoutMs = llmOverride?.timeoutMs ?? agentToolConfig?.timeoutMs ?? toolConfig?.timeoutMs ?? managerConfig?.defaultTimeoutMs ?? 3e5; const maxRetries = llmOverride?.maxRetries ?? toolConfig?.maxRetries ?? managerConfig?.defaultRetries?.maxRetries ?? 0; return { runInBackground: enabled, timeoutMs, maxRetries }; } function resolveAgentToolConfig(toolName, agentConfig) { if (!agentConfig?.tools) return void 0; if (agentConfig.tools === "all") { return { enabled: true }; } if (toolName.startsWith("agent-")) { toolName = toolName.substring("agent-".length); } else if (toolName.startsWith("workflow-")) { toolName = toolName.substring("workflow-".length); } const entry = agentConfig.tools[toolName]; if (entry === void 0) return void 0; if (typeof entry === "boolean") return { enabled: entry }; return entry; } var backgroundOverrideJsonSchema = { type: "object", description: "Optional: override background execution behavior for this specific call. Set enabled=false to force foreground, enabled=true to force background. Omit entirely to use the default configuration.", properties: { enabled: { type: "boolean", description: "Force background (true) or foreground (false) execution for this call." }, timeoutMs: { type: "number", description: "Override timeout in milliseconds for this call." }, maxRetries: { type: "number", description: "Override maximum retry attempts for this call." } }, additionalProperties: false }; var backgroundOverrideZodSchema = v4.z.object({ enabled: v4.z.boolean().optional().describe("Force background (true) or foreground (false) execution for this call."), timeoutMs: v4.z.number().optional().describe("Override timeout in milliseconds for this call."), maxRetries: v4.z.number().optional().describe("Override maximum retry attempts for this call.") }).optional().describe( "Optional: override background execution behavior for this specific call. Set enabled=false to force foreground, enabled=true to force background. Omit entirely to use the default configuration." ); // src/background-tasks/system-prompt.ts function generateBackgroundTaskSystemPrompt(tools, agentConfig) { const eligibleTools = []; const enableAll = agentConfig?.tools === "all"; for (const [toolName, tool] of Object.entries(tools)) { const bgEnabledFromAgentConfig = agentConfig?.tools === "all" ? false : typeof agentConfig?.tools?.[toolName] === "boolean" ? agentConfig.tools[toolName] : agentConfig?.tools?.[toolName]?.enabled ?? false; eligibleTools.push({ toolName, toolConfig: tool.background, defaultBackground: enableAll ? true : bgEnabledFromAgentConfig ?? tool.background?.enabled ?? false }); } if (eligibleTools.length === 0) { return void 0; } const toolLines = eligibleTools.map((t) => `- ${t.toolName} (default: ${t.defaultBackground ? "background" : "foreground"})`).join("\n"); return `You have the ability to run certain tools in the background while continuing the conversation. The following tools support background execution: ${toolLines} For any of these tools, you can include a "_background" field in the tool arguments to override the default: "_background": { "enabled": true/false, "timeoutMs": number, "maxRetries": number } All fields in "_background" are optional. Only include what you want to override. Guidelines: - Use background execution when the user doesn't need the result immediately, or when you're launching multiple independent tasks. - Use foreground execution when the user is directly waiting for the result and the conversation can't continue without it. - If you don't include "_background", the tool's default configuration is used. - When a tool runs in the background, you'll receive a placeholder result with a task ID. You can reference this in your response to the user.`; } exports.BACKGROUND_TASK_WORKFLOW_ID = BACKGROUND_TASK_WORKFLOW_ID; exports.BackgroundTaskManager = BackgroundTaskManager; exports.backgroundOverrideJsonSchema = backgroundOverrideJsonSchema; exports.backgroundOverrideZodSchema = backgroundOverrideZodSchema; exports.createBackgroundTask = createBackgroundTask; exports.generateBackgroundTaskSystemPrompt = generateBackgroundTaskSystemPrompt; exports.resolveBackgroundConfig = resolveBackgroundConfig; //# sourceMappingURL=chunk-M5AKMHS2.cjs.map //# sourceMappingURL=chunk-M5AKMHS2.cjs.map