@mastra/core
Version:
Mastra is a framework for building AI-powered applications and agents with a modern TypeScript stack.
1,048 lines (1,038 loc) • 38.3 kB
JavaScript
import { randomUUID } from 'crypto';
import { z } from '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-EJC4LNUU.js');
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() ?? 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 = z.object({
enabled: z.boolean().optional().describe("Force background (true) or foreground (false) execution for this call."),
timeoutMs: z.number().optional().describe("Override timeout in milliseconds for this call."),
maxRetries: 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.`;
}
export { BACKGROUND_TASK_WORKFLOW_ID, BackgroundTaskManager, backgroundOverrideJsonSchema, backgroundOverrideZodSchema, createBackgroundTask, generateBackgroundTaskSystemPrompt, resolveBackgroundConfig };
//# sourceMappingURL=chunk-QOKJTCIS.js.map
//# sourceMappingURL=chunk-QOKJTCIS.js.map