UNPKG

@mastra/core

Version:

The core foundation of the Mastra framework, providing essential components and interfaces for building AI-powered applications.

1,515 lines (1,508 loc) • 75.7 kB
'use strict'; var chunk5FAJ6HUC_cjs = require('./chunk-5FAJ6HUC.cjs'); var chunkPL7PVTGF_cjs = require('./chunk-PL7PVTGF.cjs'); var api = require('@opentelemetry/api'); var zod = require('zod'); var radash = require('radash'); var EventEmitter = require('events'); var sift = require('sift'); var xstate = require('xstate'); function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; } var EventEmitter__default = /*#__PURE__*/_interopDefault(EventEmitter); var sift__default = /*#__PURE__*/_interopDefault(sift); // src/workflows/step.ts var Step = class { id; description; inputSchema; outputSchema; payload; execute; retryConfig; mastra; constructor({ id, description, execute, payload, outputSchema, inputSchema, retryConfig }) { this.id = id; this.description = description ?? ""; this.inputSchema = inputSchema; this.payload = payload; this.outputSchema = outputSchema; this.execute = execute; this.retryConfig = retryConfig; } }; function createStep(opts) { return new Step(opts); } // src/workflows/types.ts var WhenConditionReturnValue = /* @__PURE__ */ ((WhenConditionReturnValue2) => { WhenConditionReturnValue2["CONTINUE"] = "continue"; WhenConditionReturnValue2["CONTINUE_FAILED"] = "continue_failed"; WhenConditionReturnValue2["ABORT"] = "abort"; WhenConditionReturnValue2["LIMBO"] = "limbo"; return WhenConditionReturnValue2; })(WhenConditionReturnValue || {}); function isErrorEvent(stateEvent) { return stateEvent.type.startsWith("xstate.error.actor."); } function isTransitionEvent(stateEvent) { return stateEvent.type.startsWith("xstate.done.actor."); } function isVariableReference(value) { return typeof value === "object" && "step" in value && "path" in value; } function getStepResult(result) { if (result?.status === "success") return result.output; return void 0; } function getSuspendedPaths({ value, path, suspendedPaths }) { if (typeof value === "string") { if (value === "suspended") { suspendedPaths.add(path); } } else { Object.keys(value).forEach( (key) => getSuspendedPaths({ value: value[key], path: path ? `${path}.${key}` : key, suspendedPaths }) ); } } function isFinalState(status) { return ["completed", "failed"].includes(status); } function isLimboState(status) { return status === "limbo"; } function recursivelyCheckForFinalState({ value, suspendedPaths, path }) { if (typeof value === "string") { return isFinalState(value) || isLimboState(value) || suspendedPaths.has(path); } return Object.keys(value).every( (key) => recursivelyCheckForFinalState({ value: value[key], suspendedPaths, path: path ? `${path}.${key}` : key }) ); } function getActivePathsAndStatus(value) { const paths = []; const traverse = (current, path = []) => { for (const [key, value2] of Object.entries(current)) { const currentPath = [...path, key]; if (typeof value2 === "string") { paths.push({ stepPath: currentPath, stepId: key, status: value2 }); } else if (typeof value2 === "object" && value2 !== null) { traverse(value2, currentPath); } } }; traverse(value); return paths; } function mergeChildValue(startStepId, parent, child) { const traverse = (current) => { const obj = {}; for (const [key, value] of Object.entries(current)) { if (key === startStepId) { obj[key] = { ...child }; } else if (typeof value === "string") { obj[key] = value; } else if (typeof value === "object" && value !== null) { obj[key] = traverse(value); } } return obj; }; return traverse(parent); } var updateStepInHierarchy = (value, targetStepId) => { const result = {}; for (const key of Object.keys(value)) { const currentValue = value[key]; if (key === targetStepId) { result[key] = "pending"; } else if (typeof currentValue === "object" && currentValue !== null) { result[key] = updateStepInHierarchy(currentValue, targetStepId); } else { result[key] = currentValue; } } return result; }; function getResultActivePaths(state) { return getActivePathsAndStatus(state.value).reduce((acc, curr) => { const entry = { status: curr.status }; if (curr.status === "suspended") { entry.suspendPayload = state.context.steps[curr.stepId].suspendPayload; } acc.set(curr.stepId, entry); return acc; }, /* @__PURE__ */ new Map()); } function isWorkflow(step) { return !!step?.name; } function resolveVariables({ runId, logger, variables, context }) { const resolvedData = {}; for (const [key, variable] of Object.entries(variables)) { const sourceData = variable.step === "trigger" ? context.triggerData : getStepResult(context.steps[variable.step.id ?? variable.step.name]); logger.debug( `Got source data for ${key} variable from ${variable.step === "trigger" ? "trigger" : variable.step.id ?? variable.step.name}`, { sourceData, path: variable.path, runId } ); if (!sourceData && variable.step !== "trigger") { resolvedData[key] = void 0; continue; } const value = variable.path === "" || variable.path === "." ? sourceData : radash.get(sourceData, variable.path); logger.debug(`Resolved variable ${key}`, { value, runId }); resolvedData[key] = value; } return resolvedData; } function workflowToStep(workflow, { mastra }) { workflow.setNested(true); return { id: workflow.name, workflow, execute: async ({ context, suspend, emit, runId, mastra: mastra2 }) => { if (mastra2) { workflow.__registerMastra(mastra2); workflow.__registerPrimitives({ logger: mastra2.getLogger(), telemetry: mastra2.getTelemetry() }); } const run = context.isResume ? workflow.createRun({ runId: context.isResume.runId }) : workflow.createRun(); const unwatch = run.watch((state) => { emit("state-update", workflow.name, state.value, { ...context, ...{ [workflow.name]: state.context } }); }); const awaitedResult = context.isResume && context.isResume.stepId.includes(".") ? await run.resume({ stepId: context.isResume.stepId.split(".").slice(1).join("."), context: context.inputData }) : await run.start({ triggerData: context.inputData }); unwatch(); if (!awaitedResult) { throw new Error("Workflow run failed"); } if (awaitedResult.activePaths?.size > 0) { const suspendedStep = [...awaitedResult.activePaths.entries()].find(([stepId, { status }]) => { return status === "suspended"; }); if (suspendedStep) { await suspend(suspendedStep[1].suspendPayload, { ...awaitedResult, runId: run.runId }); } } return { ...awaitedResult, runId: run.runId }; } }; } var Machine = class extends EventEmitter__default.default { logger; #mastra; #workflowInstance; #executionSpan; #stepGraph; #machine; #runId; #startStepId; name; #actor = null; #steps = {}; #retryConfig; constructor({ logger, mastra, workflowInstance, executionSpan, name, runId, steps, stepGraph, retryConfig, startStepId }) { super(); this.#mastra = mastra; this.#workflowInstance = workflowInstance; this.#executionSpan = executionSpan; this.logger = logger; this.#runId = runId; this.#startStepId = startStepId; this.name = name; this.#stepGraph = stepGraph; this.#steps = steps; this.#retryConfig = retryConfig; this.initializeMachine(); } get startStepId() { return this.#startStepId; } async execute({ stepId, input, snapshot, resumeData } = {}) { if (snapshot) { this.logger.debug(`Workflow snapshot received`, { runId: this.#runId, snapshot }); } const origSteps = input.steps; const isResumedInitialStep = this.#stepGraph?.initial[0]?.step?.id === stepId; if (isResumedInitialStep) { snapshot = void 0; input.steps = {}; } this.logger.debug(`Machine input prepared`, { runId: this.#runId, input }); const actorSnapshot = snapshot ? { ...snapshot, context: { ...input, inputData: { ...snapshot?.context?.inputData || {}, ...resumeData }, // ts-ignore is needed here because our snapshot types don't really match xstate snapshot types right now. We should fix this in general. // @ts-ignore isResume: { runId: snapshot?.context?.steps[stepId.split(".")?.[0]]?.output?.runId || this.#runId, stepId } } } : void 0; this.logger.debug(`Creating actor with configuration`, { input, actorSnapshot, runId: this.#runId, machineStates: this.#machine.config.states }); this.#actor = xstate.createActor(this.#machine, { inspect: (inspectionEvent) => { this.logger.debug("XState inspection event", { type: inspectionEvent.type, event: inspectionEvent.event, runId: this.#runId }); }, input: { ...input, inputData: { ...snapshot?.context?.inputData || {}, ...resumeData } }, snapshot: actorSnapshot }); this.#actor.start(); if (stepId) { this.#actor.send({ type: "RESET_TO_PENDING", stepId }); } this.logger.debug("Actor started", { runId: this.#runId }); return new Promise((resolve, reject) => { if (!this.#actor) { this.logger.error("Actor not initialized", { runId: this.#runId }); const e = new Error("Actor not initialized"); this.#executionSpan?.recordException(e); this.#executionSpan?.end(); reject(e); return; } const suspendedPaths = /* @__PURE__ */ new Set(); this.#actor.subscribe(async (state) => { this.emit("state-update", this.#startStepId, state.value, state.context); getSuspendedPaths({ value: state.value, path: "", suspendedPaths }); const allStatesValue = state.value; const allStatesComplete = recursivelyCheckForFinalState({ value: allStatesValue, suspendedPaths, path: "" }); this.logger.debug("State completion check", { allStatesComplete, suspendedPaths: Array.from(suspendedPaths), runId: this.#runId }); if (!allStatesComplete) { this.logger.debug("Not all states complete", { allStatesComplete, suspendedPaths: Array.from(suspendedPaths), runId: this.#runId }); return; } try { this.logger.debug("All states complete", { runId: this.#runId }); await this.#workflowInstance.persistWorkflowSnapshot(); this.#cleanup(); this.#executionSpan?.end(); resolve({ results: isResumedInitialStep ? { ...origSteps, ...state.context.steps } : state.context.steps, activePaths: getResultActivePaths( state ) }); } catch (error) { this.logger.debug("Failed to persist final snapshot", { error }); this.#cleanup(); this.#executionSpan?.end(); resolve({ results: isResumedInitialStep ? { ...origSteps, ...state.context.steps } : state.context.steps, activePaths: getResultActivePaths( state ) }); } }); }); } #cleanup() { if (this.#actor) { this.#actor.stop(); this.#actor = null; } this.removeAllListeners(); } #makeDelayMap() { const delayMap = {}; Object.keys(this.#steps).forEach((stepId) => { delayMap[stepId] = this.#steps[stepId]?.retryConfig?.delay || this.#retryConfig?.delay || 1e3; }); return delayMap; } #getDefaultActions() { return { updateStepResult: xstate.assign({ steps: ({ context, event }) => { if (!isTransitionEvent(event)) return context.steps; const { stepId, result } = event.output; return { ...context.steps, [stepId]: { status: "success", output: result } }; } }), setStepError: xstate.assign({ steps: ({ context, event }, params) => { if (!isErrorEvent(event)) return context.steps; const { stepId } = params; if (!stepId) return context.steps; return { ...context.steps, [stepId]: { status: "failed", error: event.error.message } }; } }), notifyStepCompletion: async (_, params) => { const { stepId } = params; this.logger.debug(`Step ${stepId} completed`); }, snapshotStep: xstate.assign({ _snapshot: ({}, params) => { const { stepId } = params; return { stepId }; } }), persistSnapshot: async ({ context }) => { if (context._snapshot) { await this.#workflowInstance.persistWorkflowSnapshot(); } return; }, decrementAttemptCount: xstate.assign({ attempts: ({ context, event }, params) => { if (!isTransitionEvent(event)) return context.attempts; const { stepId } = params; const attemptCount = context.attempts[stepId]; if (attemptCount === void 0) return context.attempts; return { ...context.attempts, [stepId]: attemptCount - 1 }; } }) }; } #getDefaultActors() { return { resolverFunction: xstate.fromPromise(async ({ input }) => { const { stepNode, context } = input; const attemptCount = context.attempts[stepNode.step.id]; const resolvedData = this.#resolveVariables({ stepConfig: stepNode.config, context, stepId: stepNode.step.id }); this.logger.debug(`Resolved variables for ${stepNode.step.id}`, { resolvedData, runId: this.#runId }); const logger = this.logger; let mastraProxy = void 0; if (this.#mastra) { mastraProxy = chunk5FAJ6HUC_cjs.createMastraProxy({ mastra: this.#mastra, logger }); } let result = void 0; try { result = await stepNode.config.handler({ context: { ...context, inputData: { ...context?.inputData || {}, ...resolvedData }, getStepResult: (stepId) => { const resolvedStepId = typeof stepId === "string" ? stepId : stepId.id; if (resolvedStepId === "trigger") { return context.triggerData; } const result2 = context.steps[resolvedStepId]; if (result2 && result2.status === "success") { return result2.output; } return void 0; } }, emit: (event, ...args) => { this.emit(event, ...args); }, suspend: async (payload, softSuspend) => { await this.#workflowInstance.suspend(stepNode.step.id, this); if (this.#actor) { context.steps[stepNode.step.id] = { status: "suspended", suspendPayload: payload, output: softSuspend }; this.logger.debug(`Sending SUSPENDED event for step ${stepNode.step.id}`); this.#actor?.send({ type: "SUSPENDED", suspendPayload: payload, stepId: stepNode.step.id, softSuspend }); } else { this.logger.debug(`Actor not available for step ${stepNode.step.id}`); } }, runId: this.#runId, mastra: mastraProxy }); } catch (error) { this.logger.debug(`Step ${stepNode.step.id} failed`, { stepId: stepNode.step.id, error, runId: this.#runId }); this.logger.debug(`Attempt count for step ${stepNode.step.id}`, { attemptCount, attempts: context.attempts, runId: this.#runId, stepId: stepNode.step.id }); if (!attemptCount || attemptCount < 0) { return { type: "STEP_FAILED", error: error instanceof Error ? error.message : `Step:${stepNode.step.id} failed with error: ${error}`, stepId: stepNode.step.id }; } return { type: "STEP_WAITING", stepId: stepNode.step.id }; } this.logger.debug(`Step ${stepNode.step.id} result`, { stepId: stepNode.step.id, result, runId: this.#runId }); return { type: "STEP_SUCCESS", result, stepId: stepNode.step.id }; }), conditionCheck: xstate.fromPromise(async ({ input }) => { const { context, stepNode } = input; const stepConfig = stepNode.config; this.logger.debug(`Checking conditions for step ${stepNode.step.id}`, { stepId: stepNode.step.id, runId: this.#runId }); if (!stepConfig?.when) { return { type: "CONDITIONS_MET" }; } this.logger.debug(`Checking conditions for step ${stepNode.step.id}`, { stepId: stepNode.step.id, runId: this.#runId }); if (typeof stepConfig?.when === "function") { let conditionMet = await stepConfig.when({ context: { ...context, getStepResult: (stepId) => { const resolvedStepId = typeof stepId === "string" ? stepId : stepId.id; if (resolvedStepId === "trigger") { return context.triggerData; } const result = context.steps[resolvedStepId]; if (result && result.status === "success") { return result.output; } return void 0; } }, mastra: this.#mastra }); if (conditionMet === "abort" /* ABORT */) { conditionMet = false; } else if (conditionMet === "continue_failed" /* CONTINUE_FAILED */) { return { type: "CONDITIONS_SKIP_TO_COMPLETED" }; } else if (conditionMet === "limbo" /* LIMBO */) { return { type: "CONDITIONS_LIMBO" }; } else if (conditionMet) { this.logger.debug(`Condition met for step ${stepNode.step.id}`, { stepId: stepNode.step.id, runId: this.#runId }); return { type: "CONDITIONS_MET" }; } return this.#workflowInstance.hasSubscribers(stepNode.step.id) ? { type: "CONDITIONS_SKIPPED" } : { type: "CONDITIONS_LIMBO" }; } else { const conditionMet = this.#evaluateCondition(stepConfig.when, context); if (!conditionMet) { return { type: "CONDITION_FAILED", error: `Step:${stepNode.step.id} condition check failed` }; } } return { type: "CONDITIONS_MET" }; }), spawnSubscriberFunction: xstate.fromPromise( async ({ input }) => { const { parentStepId, context } = input; const result = await this.#workflowInstance.runMachine(parentStepId, context); return Promise.resolve({ steps: result.reduce((acc, r) => { return { ...acc, ...r?.results }; }, {}) }); } ) }; } #resolveVariables({ stepConfig, context, stepId }) { this.logger.debug(`Resolving variables for step ${stepId}`, { stepId, runId: this.#runId }); const resolvedData = {}; for (const [key, variable] of Object.entries(stepConfig.data)) { const sourceData = variable.step === "trigger" ? context.triggerData : getStepResult(context.steps[variable.step.id]); this.logger.debug( `Got source data for ${key} variable from ${variable.step === "trigger" ? "trigger" : variable.step.id}`, { sourceData, path: variable.path, runId: this.#runId } ); if (!sourceData && variable.step !== "trigger") { resolvedData[key] = void 0; continue; } const value = variable.path === "" || variable.path === "." ? sourceData : radash.get(sourceData, variable.path); this.logger.debug(`Resolved variable ${key}`, { value, runId: this.#runId }); resolvedData[key] = value; } return resolvedData; } initializeMachine() { const machine = xstate.setup({ types: {}, delays: this.#makeDelayMap(), actions: this.#getDefaultActions(), actors: this.#getDefaultActors() }).createMachine({ id: this.name, type: "parallel", context: ({ input }) => ({ ...input }), states: this.#buildStateHierarchy(this.#stepGraph) }); this.#machine = machine; return machine; } #buildStateHierarchy(stepGraph) { const states = {}; stepGraph.initial.forEach((stepNode) => { const nextSteps = [...stepGraph[stepNode.step.id] || []]; states[stepNode.step.id] = { ...this.#buildBaseState(stepNode, nextSteps) }; }); return states; } #buildBaseState(stepNode, nextSteps = []) { const nextStep = nextSteps.shift(); return { initial: "pending", on: { RESET_TO_PENDING: { target: ".pending" // Note the dot to target child state } }, states: { pending: { entry: () => { this.logger.debug(`Step ${stepNode.step.id} pending`, { stepId: stepNode.step.id, runId: this.#runId }); }, exit: () => { this.logger.debug(`Step ${stepNode.step.id} finished pending`, { stepId: stepNode.step.id, runId: this.#runId }); }, invoke: { src: "conditionCheck", input: ({ context }) => { return { context, stepNode }; }, onDone: [ { guard: ({ event }) => { return event.output.type === "SUSPENDED"; }, target: "suspended", actions: [ xstate.assign({ steps: ({ context, event }) => { if (event.output.type !== "SUSPENDED") return context.steps; if (event.output.softSuspend) { return { ...context.steps, [stepNode.step.id]: { status: "suspended", ...context.steps?.[stepNode.step.id] || {}, output: event.output.softSuspend } }; } return { ...context.steps, [stepNode.step.id]: { status: "suspended", ...context.steps?.[stepNode.step.id] || {} } }; }, attempts: ({ context, event }) => { if (event.output.type !== "SUSPENDED") return context.attempts; return { ...context.attempts, [stepNode.step.id]: stepNode.step.retryConfig?.attempts || 0 }; } }) ] }, { guard: ({ event }) => { return event.output.type === "WAITING"; }, target: "waiting", actions: [ { type: "decrementAttemptCount", params: { stepId: stepNode.step.id } }, xstate.assign({ steps: ({ context, event }) => { if (event.output.type !== "WAITING") return context.steps; return { ...context.steps, [stepNode.step.id]: { status: "waiting" } }; } }) ] }, { guard: ({ event }) => { return event.output.type === "CONDITIONS_MET"; }, target: "executing" }, { guard: ({ event }) => { return event.output.type === "CONDITIONS_SKIP_TO_COMPLETED"; }, target: "completed" }, { guard: ({ event }) => { return event.output.type === "CONDITIONS_SKIPPED"; }, actions: xstate.assign({ steps: ({ context }) => { const newStep = { ...context.steps, [stepNode.step.id]: { status: "skipped" } }; this.logger.debug(`Step ${stepNode.step.id} skipped`, { stepId: stepNode.step.id, runId: this.#runId }); return newStep; } }), target: "runningSubscribers" }, { guard: ({ event }) => { return event.output.type === "CONDITIONS_LIMBO"; }, target: "limbo", actions: xstate.assign({ steps: ({ context }) => { const newStep = { ...context.steps, [stepNode.step.id]: { status: "skipped" } }; this.logger.debug(`Step ${stepNode.step.id} skipped`, { stepId: stepNode.step.id, runId: this.#runId }); return newStep; } }) }, { guard: ({ event }) => { return event.output.type === "CONDITION_FAILED"; }, target: "failed", actions: xstate.assign({ steps: ({ context, event }) => { if (event.output.type !== "CONDITION_FAILED") return context.steps; this.logger.debug(`Workflow condition check failed`, { error: event.output.error, stepId: stepNode.step.id }); return { ...context.steps, [stepNode.step.id]: { status: "failed", error: event.output.error } }; } }) } ] } }, waiting: { entry: () => { this.logger.debug(`Step ${stepNode.step.id} waiting`, { stepId: stepNode.step.id, timestamp: (/* @__PURE__ */ new Date()).toISOString(), runId: this.#runId }); }, exit: () => { this.logger.debug(`Step ${stepNode.step.id} finished waiting`, { stepId: stepNode.step.id, timestamp: (/* @__PURE__ */ new Date()).toISOString(), runId: this.#runId }); }, after: { [stepNode.step.id]: { target: "pending" } } }, limbo: { // no target, will stay in limbo indefinitely entry: () => { this.logger.debug(`Step ${stepNode.step.id} limbo`, { stepId: stepNode.step.id, timestamp: (/* @__PURE__ */ new Date()).toISOString(), runId: this.#runId }); }, exit: () => { this.logger.debug(`Step ${stepNode.step.id} finished limbo`, { stepId: stepNode.step.id, timestamp: (/* @__PURE__ */ new Date()).toISOString(), runId: this.#runId }); } }, suspended: { type: "final", entry: [ () => { this.logger.debug(`Step ${stepNode.step.id} suspended`, { stepId: stepNode.step.id, runId: this.#runId }); }, xstate.assign({ steps: ({ context, event }) => { return { ...context.steps, [stepNode.step.id]: { ...context?.steps?.[stepNode.step.id] || {}, status: "suspended", suspendPayload: event.type === "SUSPENDED" ? event.suspendPayload : void 0, output: event.type === "SUSPENDED" ? event.softSuspend : void 0 } }; } }) ] }, executing: { entry: () => { this.logger.debug(`Step ${stepNode.step.id} executing`, { stepId: stepNode.step.id, runId: this.#runId }); }, on: { SUSPENDED: { target: "suspended", actions: [ xstate.assign({ steps: ({ context, event }) => { return { ...context.steps, [stepNode.step.id]: { status: "suspended", suspendPayload: event.type === "SUSPENDED" ? event.suspendPayload : void 0, output: event.type === "SUSPENDED" ? event.softSuspend : void 0 } }; } }) ] } }, invoke: { src: "resolverFunction", input: ({ context }) => ({ context, stepNode }), onDone: [ { guard: ({ event }) => { return event.output.type === "STEP_FAILED"; }, target: "failed", actions: xstate.assign({ steps: ({ context, event }) => { if (event.output.type !== "STEP_FAILED") return context.steps; const newStep = { ...context.steps, [stepNode.step.id]: { status: "failed", error: event.output.error } }; this.logger.debug(`Step ${stepNode.step.id} failed`, { error: event.output.error, stepId: stepNode.step.id }); return newStep; } }) }, { guard: ({ event }) => { return event.output.type === "STEP_SUCCESS"; }, actions: [ ({ event }) => { this.logger.debug(`Step ${stepNode.step.id} finished executing`, { stepId: stepNode.step.id, output: event.output, runId: this.#runId }); }, { type: "updateStepResult", params: { stepId: stepNode.step.id } }, { type: "spawnSubscribers", params: { stepId: stepNode.step.id } } ], target: "runningSubscribers" }, { guard: ({ event }) => { return event.output.type === "STEP_WAITING"; }, target: "waiting", actions: [ { type: "decrementAttemptCount", params: { stepId: stepNode.step.id } }, xstate.assign({ steps: ({ context, event }) => { if (event.output.type !== "STEP_WAITING") return context.steps; return { ...context.steps, [stepNode.step.id]: { status: "waiting" } }; } }) ] } ], onError: { target: "failed", actions: [{ type: "setStepError", params: { stepId: stepNode.step.id } }] } } }, runningSubscribers: { entry: () => { this.logger.debug(`Step ${stepNode.step.id} running subscribers`, { stepId: stepNode.step.id, runId: this.#runId }); }, exit: () => { this.logger.debug(`Step ${stepNode.step.id} finished running subscribers`, { stepId: stepNode.step.id, runId: this.#runId }); }, invoke: { src: "spawnSubscriberFunction", input: ({ context }) => ({ parentStepId: stepNode.step.id, context }), onDone: { target: nextStep ? nextStep.step.id : "completed", actions: [ xstate.assign({ steps: ({ context, event }) => ({ ...context.steps, ...event.output.steps }) }), () => this.logger.debug(`Subscriber execution completed`, { stepId: stepNode.step.id }) ] }, onError: { target: nextStep ? nextStep.step.id : "completed", actions: ({ event }) => { this.logger.debug(`Subscriber execution failed`, { error: event.error, stepId: stepNode.step.id }); } } } }, completed: { type: "final", entry: [ { type: "notifyStepCompletion", params: { stepId: stepNode.step.id } }, { type: "snapshotStep", params: { stepId: stepNode.step.id } }, { type: "persistSnapshot" } ] }, failed: { type: "final", entry: [ { type: "notifyStepCompletion", params: { stepId: stepNode.step.id } }, { type: "snapshotStep", params: { stepId: stepNode.step.id } }, { type: "persistSnapshot" } ] }, // build chain of next steps recursively ...nextStep ? { [nextStep.step.id]: { ...this.#buildBaseState(nextStep, nextSteps) } } : {} } }; } #evaluateCondition(condition, context) { let andBranchResult = true; let baseResult = true; let orBranchResult = true; const simpleCondition = Object.entries(condition).find(([key]) => key.includes(".")); if (simpleCondition) { const [key, queryValue] = simpleCondition; const [stepId, ...pathParts] = key.split("."); const path = pathParts.join("."); const sourceData = stepId === "trigger" ? context.triggerData : getStepResult(context.steps[stepId]); this.logger.debug(`Got condition data from step ${stepId}`, { stepId, sourceData, runId: this.#runId }); if (!sourceData) { return false; } let value = radash.get(sourceData, path); if (stepId !== "trigger" && path === "status" && !value) { value = "success"; } if (typeof queryValue === "object" && queryValue !== null) { baseResult = sift__default.default(queryValue)(value); } else { baseResult = value === queryValue; } } if ("ref" in condition) { const { ref, query } = condition; const sourceData = ref.step === "trigger" ? context.triggerData : getStepResult(context.steps[ref.step.id]); this.logger.debug(`Got condition data from ${ref.step === "trigger" ? "trigger" : ref.step.id}`, { sourceData, runId: this.#runId }); if (!sourceData) { return false; } let value = radash.get(sourceData, ref.path); if (ref.step !== "trigger" && ref.path === "status" && !value) { value = "success"; } baseResult = sift__default.default(query)(value); } if ("and" in condition) { andBranchResult = condition.and.every((cond) => this.#evaluateCondition(cond, context)); this.logger.debug(`Evaluated AND condition`, { andBranchResult, runId: this.#runId }); } if ("or" in condition) { orBranchResult = condition.or.some((cond) => this.#evaluateCondition(cond, context)); this.logger.debug(`Evaluated OR condition`, { orBranchResult, runId: this.#runId }); } if ("not" in condition) { baseResult = !this.#evaluateCondition(condition.not, context); this.logger.debug(`Evaluated NOT condition`, { baseResult, runId: this.#runId }); } const finalResult = baseResult && andBranchResult && orBranchResult; this.logger.debug(`Evaluated condition`, { finalResult, runId: this.#runId }); return finalResult; } getSnapshot() { const snapshot = this.#actor?.getSnapshot(); return snapshot; } }; // src/workflows/workflow-instance.ts var WorkflowInstance = class { name; #mastra; #machines = {}; logger; #steps = {}; #stepGraph; #stepSubscriberGraph = {}; #retryConfig; events; #runId; #state = null; #executionSpan; #onStepTransition = /* @__PURE__ */ new Set(); #onFinish; #resultMapping; // indexed by stepId #suspendedMachines = {}; // {step1&&step2: {step1: true, step2: true}} #compoundDependencies = {}; constructor({ name, logger, steps, runId, retryConfig, mastra, stepGraph, stepSubscriberGraph, onFinish, onStepTransition, resultMapping, events }) { this.name = name; this.logger = logger; this.#steps = steps; this.#stepGraph = stepGraph; this.#stepSubscriberGraph = stepSubscriberGraph; this.#retryConfig = retryConfig; this.#mastra = mastra; this.#runId = runId ?? crypto.randomUUID(); this.#onFinish = onFinish; this.#resultMapping = resultMapping; this.events = events; onStepTransition?.forEach((handler) => this.#onStepTransition.add(handler)); this.#initializeCompoundDependencies(); } setState(state) { this.#state = state; } get runId() { return this.#runId; } get executionSpan() { return this.#executionSpan; } watch(onTransition) { this.#onStepTransition.add(onTransition); return () => { this.#onStepTransition.delete(onTransition); }; } async start({ triggerData } = {}) { const results = await this.execute({ triggerData }); if (this.#onFinish) { this.#onFinish(); } return { ...results, runId: this.runId }; } isCompoundDependencyMet(stepKey) { if (!this.#isCompoundKey(stepKey)) return true; const dependencies = this.#compoundDependencies[stepKey]; return dependencies ? Object.values(dependencies).every((status) => status === true) : true; } async execute({ triggerData, snapshot, stepId, resumeData } = {}) { this.#executionSpan = this.#mastra?.getTelemetry()?.tracer.startSpan(`workflow.${this.name}.execute`, { attributes: { componentName: this.name, runId: this.runId } }); let machineInput = { // Maintain the original step results and their output steps: {}, triggerData: triggerData || {}, attempts: Object.keys(this.#steps).reduce( (acc, stepKey) => { acc[stepKey] = this.#steps[stepKey]?.retryConfig?.attempts || this.#retryConfig?.attempts || 0; return acc; }, {} ) }; let stepGraph = this.#stepGraph; let startStepId = "trigger"; if (snapshot) { const runState = snapshot; if (stepId && runState?.suspendedSteps?.[stepId]) { startStepId = runState.suspendedSteps[stepId]; stepGraph = this.#stepSubscriberGraph[startStepId] ?? this.#stepGraph; machineInput = runState.context; } } const defaultMachine = new Machine({ logger: this.logger, mastra: this.#mastra, workflowInstance: this, name: this.name, runId: this.runId, steps: this.#steps, stepGraph, executionSpan: this.#executionSpan, startStepId, retryConfig: this.#retryConfig }); this.#machines[startStepId] = defaultMachine; const stateUpdateHandler = (startStepId2, state, context) => { if (startStepId2 === "trigger") { this.#state = state; } else { this.#state = mergeChildValue(startStepId2, this.#state, state); } const now = Date.now(); if (this.#onStepTransition) { this.#onStepTransition.forEach((onTransition) => { void onTransition({ runId: this.#runId, value: this.#state, context, activePaths: getActivePathsAndStatus(this.#state), timestamp: now }); }); } }; defaultMachine.on("state-update", stateUpdateHandler); const { results, activePaths } = await defaultMachine.execute({ snapshot, stepId, input: machineInput, resumeData }); await this.persistWorkflowSnapshot(); const result = { results, activePaths }; if (this.#resultMapping) { result.result = resolveVariables({ runId: this.#runId, logger: this.logger, variables: this.#resultMapping, context: { steps: results, triggerData} }); } return result; } hasSubscribers(stepId) { return Object.keys(this.#stepSubscriberGraph).some((key) => key.split("&&").includes(stepId)); } async runMachine(parentStepId, input) { const stepStatus = input.steps[parentStepId]?.status; const subscriberKeys = Object.keys(this.#stepSubscriberGraph).filter((key) => key.split("&&").includes(parentStepId)); subscriberKeys.forEach((key) => { if (["success", "failure", "skipped"].includes(stepStatus) && this.#isCompoundKey(key)) { this.#compoundDependencies[key][parentStepId] = true; } }); const stateUpdateHandler = (startStepId, state, context) => { if (startStepId === "trigger") { this.#state = state; } else { this.#state = mergeChildValue(startStepId, this.#state, state); } const now = Date.now(); if (this.#onStepTransition) { this.#onStepTransition.forEach((onTransition) => { void onTransition({ runId: this.#runId, value: this.#state, context, activePaths: getActivePathsAndStatus(this.#state), timestamp: now }); }); } }; const results = await Promise.all( subscriberKeys.map(async (key) => { if (!this.#stepSubscriberGraph[key] || !this.isCompoundDependencyMet(key)) { return; } this.#initializeCompoundDependencies(); const machine = new Machine({ logger: this.logger, mastra: this.#mastra, workflowInstance: this, name: parentStepId === "trigger" ? this.name : `${this.name}-${parentStepId}`, runId: this.runId, steps: this.#steps, stepGraph: this.#stepSubscriberGraph[key], executionSpan: this.#executionSpan, startStepId: parentStepId }); machine.on("state-update", stateUpdateHandler); this.#machines[parentStepId] = machine; return machine.execute({ input }); }) ); return results; } async suspend(stepId, machine) { this.#suspendedMachines[stepId] = machine; } /** * Persists the workflow state to the database */ async persistWorkflowSnapshot() { const existingSnapshot = await this.#mastra?.storage?.loadWorkflowSnapshot({ workflowName: this.name, runId: this.#runId }); const machineSnapshots = {}; for (const [stepId, machine] of Object.entries(this.#machines)) { const machineSnapshot = machine?.getSnapshot(); if (machineSnapshot) { machineSnapshots[stepId] = { ...machineSnapshot }; } } let snapshot = machineSnapshots["trigger"]; delete machineSnapshots["trigger"]; const suspendedSteps = Object.entries(this.#suspendedMachines).reduce( (acc, [stepId, machine]) => { acc[stepId] = machine.startStepId; return acc; }, {} ); if (!snapshot && existingSnapshot) { existingSnapshot.childStates = { ...existingSnapshot.childStates, ...machineSnapshots }; existingSnapshot.suspendedSteps = { ...existingSnapshot.suspendedSteps, ...suspendedSteps }; await this.#mastra?.storage?.persistWorkflowSnapshot({ workflowName: this.name, runId: this.#runId, snapshot: existingSnapshot }); return; } else if (snapshot && !existingSnapshot) { snapshot.suspendedSteps = suspendedSteps; snapshot.childStates = { ...machineSnapshots }; await this.#mastra?.storage?.persistWorkflowSnapshot({ workflowName: this.name, runId: this.#runId, snapshot }); return; } else if (!snapshot) { this.logger.debug("Snapshot cannot be persisted. No snapshot received.", { runId: this.#runId }); return; } snapshot.suspendedSteps = { ...existingSnapshot.suspendedSteps, ...suspendedSteps }; if (!existingSnapshot || snapshot === existingSnapshot) { await this.#mastra?.storage?.persistWorkflowSnapshot({ workflowName: this.name, runId: this.#runId, snapshot }); return; } if (existingSnapshot?.childStates) { snapshot.childStates = { ...existingSnapshot.childStates, ...machineSnapshots }; } else { snapshot.childStates = machineSnapshots; } await this.#mastra?.storage?.persistWorkflowSnapshot({ workflowName: this.name, runId: this.#runId, snapshot }); } async getState() { const storedSnapshot = await this.#mastra?.storage?.loadWorkflowSnapshot({ workflowName: this.name, runId: this.runId }); const prevSnapshot = storedSnapshot ? { trigger: storedSnapshot, ...Object.entries(storedSnapshot?.childStates ?? {}).reduce( (acc, [stepId, snapshot2]) => ({ ...acc, [stepId]: snapshot2 }), {} ) } : {}; const currentSnapshot = Object.entries(this.#machines).reduce( (acc, [stepId, machine]) => { const snapshot2 = machine.getSnapshot(); if (!snapshot2) { return acc; } return { ...acc, [stepId]: snapshot2 }; }, {} ); Object.assign(prevSnapshot, currentSnapshot); const trigger = prevSnapshot.trigger; delete prevSnapshot.trigger; const snapshot = { ...trigger}; const m = getActivePathsAndStatus(prevSnapshot.value); return { runId: this.runId, value: snapshot.value, context: snapshot.context, activePaths: m, timestamp: Date.now() }; } async resumeWithEvent(eventName, data) { const event = this.events?.[eventName]; if (!event) { throw new Error(`Event ${eventName} not found`); } const results = await this.resume({ stepId: `__${eventName}_event`, context: { resumedEvent: data } }); return results; } async resume({ stepId, context: resumeContext }) { await new Promise((resolve) => setTimeout(resolve, 0)); return this._resume({ stepId, context: resumeContext }); } async #loadWorkflowSnapshot(runId) { if (!this.#mastra?.storage) { this.logger.debug("Snapshot cannot be loaded. Mastra engine is not initialized", { runId }); return; } await this.persistWorkflowSnapshot(); return this.#mastra.getStorage()?.loadWorkflowSnapshot({ runId, workflowName: this.name }); } async _resume({ stepId, context: resumeContext }) { const snapshot = await this.#loadWorkflowSnapshot(this.runId); if (!snapshot) { throw new Error(`No snapshot found for workflow run ${this.runId}`); } const stepParts = stepId.split("."); const stepPath = stepParts.join("."); if (stepParts.length > 1) { stepId = stepParts[0] ?? stepId; } let parsedSnapshot; try { parsedSnapshot = typeof snapshot === "string" ? JSON.parse(snapshot) : snapshot; } catch (error) { this.logger.debug("Failed to parse workflow snapshot for resume", { error, runId: this.runId }); throw new Error("Failed to parse workflow snapshot"); } const startStepId = parsedSnapshot.suspendedSteps?.[stepId]; if (!startStepId) {