UNPKG

@mastra/core

Version:

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

1,063 lines (1,058 loc) 30.9 kB
import { Agent } from '../../chunk-H2UWWO4L.js'; import { Tool } from '../../chunk-JSUPD5IG.js'; import { MastraBase } from '../../chunk-CLJQYXNM.js'; import { RegisteredLogger } from '../../chunk-2BVZNKLX.js'; import { RuntimeContext } from '../../chunk-SGGPJWRQ.js'; import { randomUUID } from 'crypto'; import EventEmitter from 'events'; import { z } from 'zod'; // src/workflows/vNext/execution-engine.ts var ExecutionEngine = class extends MastraBase { mastra; constructor({ mastra }) { super({ name: "ExecutionEngine", component: RegisteredLogger.WORKFLOW }); this.mastra = mastra; } __registerMastra(mastra) { this.mastra = mastra; } }; // src/workflows/vNext/default.ts function fmtReturnValue(stepResults, lastOutput, error) { const base = { status: lastOutput.status, steps: stepResults }; if (lastOutput.status === "success") { base.result = lastOutput.output; } else if (lastOutput.status === "failed") { base.error = error instanceof Error ? error : lastOutput.error ?? new Error("Unknown error: " + error); } else if (lastOutput.status === "suspended") { const suspendedStepIds = Object.entries(stepResults).flatMap(([stepId, stepResult]) => { if (stepResult?.status === "suspended") { const nestedPath = stepResult?.payload?.__workflow_meta?.path; return nestedPath ? [[stepId, ...nestedPath]] : [[stepId]]; } return []; }); base.suspended = suspendedStepIds; } return base; } var DefaultExecutionEngine = class extends ExecutionEngine { /** * Executes a workflow run with the provided execution graph and input * @param graph The execution graph to execute * @param input The input data for the workflow * @returns A promise that resolves to the workflow output */ async execute(params) { const { workflowId, runId, graph, input, resume, retryConfig } = params; const { attempts = 0, delay = 0 } = retryConfig ?? {}; const steps = graph.steps; if (steps.length === 0) { throw new Error("Workflow must have at least one step"); } await this.mastra?.getStorage()?.init(); let startIdx = 0; if (resume?.resumePath) { startIdx = resume.resumePath[0]; resume.resumePath.shift(); } const stepResults = resume?.stepResults || { input }; let lastOutput; for (let i = startIdx; i < steps.length; i++) { const entry = steps[i]; try { lastOutput = await this.executeEntry({ workflowId, runId, entry, prevStep: steps[i - 1], stepResults, resume, executionContext: { executionPath: [i], suspendedPaths: {}, retryConfig: { attempts, delay } }, emitter: params.emitter, runtimeContext: params.runtimeContext }); if (lastOutput.status !== "success") { if (entry.type === "step") { params.emitter.emit("watch", { type: "watch", payload: { workflowState: { status: lastOutput.status, steps: stepResults, result: null, error: lastOutput.error } }, eventTimestamp: Date.now() }); } return fmtReturnValue(stepResults, lastOutput); } } catch (e) { this.logger.error("Error executing step: " + (e?.stack ?? e)); if (entry.type === "step") { params.emitter.emit("watch", { type: "watch", payload: { workflowState: { status: "failed", steps: stepResults, result: null, error: e } }, eventTimestamp: Date.now() }); } return fmtReturnValue(stepResults, lastOutput, e); } } params.emitter.emit("watch", { type: "watch", payload: { workflowState: { status: lastOutput.status, steps: stepResults, result: lastOutput.output, error: lastOutput.error } }, eventTimestamp: Date.now() }); return fmtReturnValue(stepResults, lastOutput); } getStepOutput(stepResults, step) { if (!step) { return stepResults.input; } else if (step.type === "step") { return stepResults[step.step.id]?.output; } else if (step.type === "parallel" || step.type === "conditional") { return step.steps.reduce( (acc, entry) => { if (entry.type === "step") { acc[entry.step.id] = stepResults[entry.step.id]?.output; } else if (entry.type === "parallel" || entry.type === "conditional") { const parallelResult = this.getStepOutput(stepResults, entry)?.output; acc = { ...acc, ...parallelResult }; } else if (entry.type === "loop") { acc[entry.step.id] = stepResults[entry.step.id]?.output; } else if (entry.type === "foreach") { acc[entry.step.id] = stepResults[entry.step.id]?.output; } return acc; }, {} ); } else if (step.type === "loop") { return stepResults[step.step.id]?.output; } else if (step.type === "foreach") { return stepResults[step.step.id]?.output; } } async executeStep({ step, stepResults, executionContext, resume, prevOutput, emitter, runtimeContext }) { let execResults; const retries = step.retries ?? executionContext.retryConfig.attempts ?? 0; for (let i = 0; i < retries + 1; i++) { try { let suspended; const result = await step.execute({ mastra: this.mastra, runtimeContext, inputData: prevOutput, resumeData: resume?.steps[0] === step.id ? resume?.resumePayload : void 0, getInitData: () => stepResults?.input, getStepResult: (step2) => { const result2 = stepResults[step2.id]; if (result2?.status === "success") { return result2.output; } return null; }, suspend: async (suspendPayload) => { executionContext.suspendedPaths[step.id] = executionContext.executionPath; suspended = { payload: suspendPayload }; }, resume: { steps: resume?.steps?.slice(1) || [], resumePayload: resume?.resumePayload, // @ts-ignore runId: stepResults[step.id]?.payload?.__workflow_meta?.runId }, emitter }); if (suspended) { execResults = { status: "suspended", payload: suspended.payload }; } else { execResults = { status: "success", output: result }; } break; } catch (e) { this.logger.error("Error executing step: " + (e?.stack ?? e)); execResults = { status: "failed", error: e instanceof Error ? e : new Error("Unknown error: " + e) }; } } return execResults; } async executeParallel({ workflowId, runId, entry, prevStep, stepResults, resume, executionContext, emitter, runtimeContext }) { let execResults; const results = await Promise.all( entry.steps.map( (step, i) => this.executeEntry({ workflowId, runId, entry: step, prevStep, stepResults, resume, executionContext: { executionPath: [...executionContext.executionPath, i], suspendedPaths: executionContext.suspendedPaths, retryConfig: executionContext.retryConfig }, emitter, runtimeContext }) ) ); const hasFailed = results.find((result) => result.status === "failed"); const hasSuspended = results.find((result) => result.status === "suspended"); if (hasFailed) { execResults = { status: "failed", error: hasFailed.error }; } else if (hasSuspended) { execResults = { status: "suspended", payload: hasSuspended.payload }; } else { execResults = { status: "success", output: results.reduce((acc, result, index) => { if (result.status === "success") { acc[entry.steps[index].step.id] = result.output; } return acc; }, {}) }; } return execResults; } async executeConditional({ workflowId, runId, entry, prevOutput, prevStep, stepResults, resume, executionContext, emitter, runtimeContext }) { let execResults; const truthyIndexes = (await Promise.all( entry.conditions.map(async (cond, index) => { try { const result = await cond({ mastra: this.mastra, runtimeContext, inputData: prevOutput, getInitData: () => stepResults?.input, getStepResult: (step) => { if (!step?.id) { return null; } const result2 = stepResults[step.id]; if (result2?.status === "success") { return result2.output; } return null; }, // TODO: this function shouldn't have suspend probably? suspend: async (_suspendPayload) => { }, emitter }); return result ? index : null; } catch (e) { this.logger.error("Error evaluating condition: " + (e?.stack ?? e)); return null; } }) )).filter((index) => index !== null); const stepsToRun = entry.steps.filter((_, index) => truthyIndexes.includes(index)); const results = await Promise.all( stepsToRun.map( (step, index) => this.executeEntry({ workflowId, runId, entry: step, prevStep, stepResults, resume, executionContext: { executionPath: [...executionContext.executionPath, index], suspendedPaths: executionContext.suspendedPaths, retryConfig: executionContext.retryConfig }, emitter, runtimeContext }) ) ); const hasFailed = results.find((result) => result.status === "failed"); const hasSuspended = results.find((result) => result.status === "suspended"); if (hasFailed) { execResults = { status: "failed", error: hasFailed.error }; } else if (hasSuspended) { execResults = { status: "suspended", payload: hasSuspended.payload }; } else { execResults = { status: "success", output: results.reduce((acc, result, index) => { if (result.status === "success") { acc[stepsToRun[index].step.id] = result.output; } return acc; }, {}) }; } return execResults; } async executeLoop({ entry, prevOutput, stepResults, resume, executionContext, emitter, runtimeContext }) { const { step, condition } = entry; let isTrue = true; let result = { status: "success", output: prevOutput }; do { result = await this.executeStep({ step, stepResults, executionContext, resume, prevOutput: result.output, emitter, runtimeContext }); if (result.status !== "success") { return result; } isTrue = await condition({ mastra: this.mastra, runtimeContext, inputData: result.output, getInitData: () => stepResults?.input, getStepResult: (step2) => { if (!step2?.id) { return null; } const result2 = stepResults[step2.id]; return result2?.status === "success" ? result2.output : null; }, suspend: async (_suspendPayload) => { }, emitter }); } while (entry.loopType === "dowhile" ? isTrue : !isTrue); return result; } async executeForeach({ entry, prevOutput, stepResults, resume, executionContext, emitter, runtimeContext }) { const { step, opts } = entry; const results = []; const concurrency = opts.concurrency; for (let i = 0; i < prevOutput.length; i += concurrency) { const items = prevOutput.slice(i, i + concurrency); const itemsResults = await Promise.all( items.map((item) => { return this.executeStep({ step, stepResults, executionContext, resume, prevOutput: item, emitter, runtimeContext }); }) ); for (const result of itemsResults) { if (result.status !== "success") { return result; } results.push(result?.output); } } return { status: "success", output: results }; } async executeEntry({ workflowId, runId, entry, prevStep, stepResults, resume, executionContext, emitter, runtimeContext }) { const prevOutput = this.getStepOutput(stepResults, prevStep); let execResults; if (entry.type === "step" || entry.type === "loop" || entry.type === "foreach") { emitter.emit("watch", { type: "watch", payload: { currentStep: { id: entry.step.id, status: "running" }, workflowState: { status: "running", steps: { ...stepResults, [entry.step.id]: { status: "running" } }, result: null, error: null } }, eventTimestamp: Date.now() }); } if (entry.type === "step") { const { step } = entry; execResults = await this.executeStep({ step, stepResults, executionContext, resume, prevOutput, emitter, runtimeContext }); } else if (resume?.resumePath?.length && (entry.type === "parallel" || entry.type === "conditional")) { const idx = resume.resumePath.shift(); return this.executeEntry({ workflowId, runId, entry: entry.steps[idx], prevStep, stepResults, resume, executionContext: { executionPath: [...executionContext.executionPath, idx], suspendedPaths: executionContext.suspendedPaths, retryConfig: executionContext.retryConfig }, emitter, runtimeContext }); } else if (entry.type === "parallel") { execResults = await this.executeParallel({ workflowId, runId, entry, prevStep, stepResults, resume, executionContext, emitter, runtimeContext }); } else if (entry.type === "conditional") { execResults = await this.executeConditional({ workflowId, runId, entry, prevStep, prevOutput, stepResults, resume, executionContext, emitter, runtimeContext }); } else if (entry.type === "loop") { execResults = await this.executeLoop({ workflowId, runId, entry, prevStep, prevOutput, stepResults, resume, executionContext, emitter, runtimeContext }); } else if (entry.type === "foreach") { execResults = await this.executeForeach({ workflowId, runId, entry, prevStep, prevOutput, stepResults, resume, executionContext, emitter, runtimeContext }); } if (entry.type === "step" || entry.type === "loop" || entry.type === "foreach") { stepResults[entry.step.id] = execResults; } await this.mastra?.getStorage()?.persistWorkflowSnapshot({ workflowName: workflowId, runId, snapshot: { runId, value: {}, context: stepResults, activePaths: [], suspendedPaths: executionContext.suspendedPaths, // @ts-ignore timestamp: Date.now() } }); if (entry.type === "step" || entry.type === "loop" || entry.type === "foreach") { emitter.emit("watch", { type: "watch", payload: { currentStep: { id: entry.step.id, status: execResults.status, output: execResults.output }, workflowState: { status: "running", steps: stepResults, result: null, error: null } }, eventTimestamp: Date.now() }); } return execResults; } }; // src/workflows/vNext/workflow.ts function createStep(params) { if (params instanceof Agent) { return { id: params.name, // @ts-ignore inputSchema: z.object({ prompt: z.string() // resourceId: z.string().optional(), // threadId: z.string().optional(), }), // @ts-ignore outputSchema: z.object({ text: z.string() }), execute: async ({ inputData }) => { const result = await params.generate(inputData.prompt, { // resourceId: inputData.resourceId, // threadId: inputData.threadId, }); return { text: result.text }; } }; } if (params instanceof Tool) { if (!params.inputSchema || !params.outputSchema) { throw new Error("Tool must have input and output schemas defined"); } return { // TODO: tool probably should have strong id type // @ts-ignore id: params.id, inputSchema: params.inputSchema, outputSchema: params.outputSchema, execute: async ({ inputData, mastra }) => { return await params.execute({ context: inputData, mastra }); } }; } return { id: params.id, description: params.description, inputSchema: params.inputSchema, outputSchema: params.outputSchema, resumeSchema: params.resumeSchema, suspendSchema: params.suspendSchema, execute: params.execute }; } function cloneStep(step, opts) { return { id: opts.id, description: step.description, inputSchema: step.inputSchema, outputSchema: step.outputSchema, execute: step.execute }; } function createWorkflow(params) { return new NewWorkflow(params); } function cloneWorkflow(workflow, opts) { const wf = new NewWorkflow({ id: opts.id, inputSchema: workflow.inputSchema, outputSchema: workflow.outputSchema, steps: workflow.stepDefs, mastra: workflow.mastra }); wf.setStepFlow(workflow.stepGraph); wf.commit(); return wf; } var NewWorkflow = class extends MastraBase { id; description; inputSchema; outputSchema; steps; stepDefs; stepFlow; executionEngine; executionGraph; retryConfig; #mastra; #runs = /* @__PURE__ */ new Map(); constructor({ mastra, id, inputSchema, outputSchema, description, executionEngine, retryConfig, steps }) { super({ name: id, component: RegisteredLogger.WORKFLOW }); this.id = id; this.description = description; this.inputSchema = inputSchema; this.outputSchema = outputSchema; this.retryConfig = retryConfig ?? { attempts: 0, delay: 0 }; this.executionGraph = this.buildExecutionGraph(); this.stepFlow = []; this.#mastra = mastra; this.steps = {}; this.stepDefs = steps; if (!executionEngine) { this.executionEngine = new DefaultExecutionEngine({ mastra: this.#mastra }); } else { this.executionEngine = executionEngine; } this.#runs = /* @__PURE__ */ new Map(); } get mastra() { return this.#mastra; } __registerMastra(mastra) { this.#mastra = mastra; this.executionEngine.__registerMastra(mastra); } __registerPrimitives(p) { if (p.telemetry) { this.__setTelemetry(p.telemetry); } if (p.logger) { this.__setLogger(p.logger); } } setStepFlow(stepFlow) { this.stepFlow = stepFlow; } /** * Adds a step to the workflow * @param step The step to add to the workflow * @returns The workflow instance for chaining */ then(step) { this.stepFlow.push({ type: "step", step }); this.steps[step.id] = step; return this; } map(mappingConfig) { const mappingStep = createStep({ id: `mapping_${randomUUID()}`, inputSchema: z.object({}), outputSchema: z.object({}), execute: async ({ getStepResult, getInitData, runtimeContext }) => { const result = {}; for (const [key, mapping] of Object.entries(mappingConfig)) { const m = mapping; if (m.value !== void 0) { result[key] = m.value; continue; } if (m.runtimeContextPath) { result[key] = runtimeContext.get(m.runtimeContextPath); continue; } const stepResult = m.initData ? getInitData() : getStepResult(m.step); if (m.path === ".") { result[key] = stepResult; continue; } const pathParts = m.path.split("."); let value = stepResult; for (const part of pathParts) { if (typeof value === "object" && value !== null) { value = value[part]; } else { throw new Error(`Invalid path ${m.path} in step ${m.step.id}`); } } result[key] = value; } return result; } }); this.stepFlow.push({ type: "step", step: mappingStep }); return this; } // TODO: make typing better here parallel(steps) { this.stepFlow.push({ type: "parallel", steps: steps.map((step) => ({ type: "step", step })) }); steps.forEach((step) => { this.steps[step.id] = step; }); return this; } // TODO: make typing better here branch(steps) { this.stepFlow.push({ type: "conditional", steps: steps.map(([_cond, step]) => ({ type: "step", step })), conditions: steps.map(([cond]) => cond), serializedConditions: steps.map(([cond, _step]) => ({ id: `${_step.id}-condition`, fn: cond.toString() })) }); steps.forEach(([_, step]) => { this.steps[step.id] = step; }); return this; } dowhile(step, condition) { this.stepFlow.push({ type: "loop", step, condition, loopType: "dowhile", serializedCondition: { id: `${step.id}-condition`, fn: condition.toString() } }); this.steps[step.id] = step; return this; } dountil(step, condition) { this.stepFlow.push({ type: "loop", step, condition, loopType: "dountil", serializedCondition: { id: `${step.id}-condition`, fn: condition.toString() } }); this.steps[step.id] = step; return this; } foreach(step, opts) { this.stepFlow.push({ type: "foreach", step, opts: opts ?? { concurrency: 1 } }); this.steps[step.id] = step; return this; } /** * Builds the execution graph for this workflow * @returns The execution graph that can be used to execute the workflow */ buildExecutionGraph() { return { id: randomUUID(), steps: this.stepFlow }; } /** * Finalizes the workflow definition and prepares it for execution * This method should be called after all steps have been added to the workflow * @returns A built workflow instance ready for execution */ commit() { this.executionGraph = this.buildExecutionGraph(); return this; } get stepGraph() { return this.stepFlow; } /** * Creates a new workflow run instance * @param options Optional configuration for the run * @returns A Run instance that can be used to execute the workflow */ createRun(options) { const runIdToUse = options?.runId || randomUUID(); const run = this.#runs.get(runIdToUse) ?? new Run({ workflowId: this.id, runId: runIdToUse, executionEngine: this.executionEngine, executionGraph: this.executionGraph, mastra: this.#mastra, retryConfig: this.retryConfig, cleanup: () => this.#runs.delete(runIdToUse) }); this.#runs.set(runIdToUse, run); return run; } async execute({ inputData, resumeData, suspend, resume, emitter, mastra }) { this.__registerMastra(mastra); const run = resume?.steps?.length ? this.createRun({ runId: resume.runId }) : this.createRun(); const unwatch = run.watch((event) => { emitter.emit("nested-watch", { event, workflowId: this.id, runId: run.runId, isResume: !!resume?.steps?.length }); }); const res = resume?.steps?.length ? await run.resume({ resumeData, step: resume.steps }) : await run.start({ inputData }); unwatch(); const suspendedSteps = Object.entries(res.steps).filter(([_stepName, stepResult]) => { const stepRes = stepResult; return stepRes?.status === "suspended"; }); if (suspendedSteps?.length) { for (const [stepName, stepResult] of suspendedSteps) { const suspendPath = [stepName, ...stepResult?.payload?.__workflow_meta?.path ?? []]; await suspend({ ...stepResult?.payload, __workflow_meta: { runId: run.runId, path: suspendPath } }); } } if (res.status === "failed") { throw res.error; } return res.status === "success" ? res.result : void 0; } async getWorkflowRuns() { const storage = this.#mastra?.getStorage(); if (!storage) { this.logger.debug("Cannot get workflow runs. Mastra engine is not initialized"); return { runs: [], total: 0 }; } return storage.getWorkflowRuns({ workflowName: this.id }); } async getWorkflowRun(runId) { const runs = await this.getWorkflowRuns(); return runs?.runs.find((r) => r.runId === runId) || this.#runs.get(runId); } }; var Run = class { emitter; /** * Unique identifier for this workflow */ workflowId; /** * Unique identifier for this run */ runId; /** * Internal state of the workflow run */ state = {}; /** * The execution engine for this run */ executionEngine; /** * The execution graph for this run */ executionGraph; /** * The storage for this run */ #mastra; cleanup; retryConfig; constructor(params) { this.workflowId = params.workflowId; this.runId = params.runId; this.executionEngine = params.executionEngine; this.executionGraph = params.executionGraph; this.#mastra = params.mastra; this.emitter = new EventEmitter(); this.retryConfig = params.retryConfig; this.cleanup = params.cleanup; } /** * Starts the workflow execution with the provided input * @param input The input data for the workflow * @returns A promise that resolves to the workflow output */ async start({ inputData, runtimeContext }) { const result = await this.executionEngine.execute({ workflowId: this.workflowId, runId: this.runId, graph: this.executionGraph, input: inputData, emitter: this.emitter, retryConfig: this.retryConfig, runtimeContext: runtimeContext ?? new RuntimeContext() }); this.cleanup?.(); return result; } watch(cb) { const watchCb = (event) => { this.updateState(event.payload); cb({ type: event.type, payload: this.getState(), eventTimestamp: event.eventTimestamp }); }; this.emitter.on("watch", watchCb); const nestedWatchCb = ({ event, workflowId }) => { try { const { type, payload, eventTimestamp } = event; const prefixedSteps = Object.fromEntries( Object.entries(payload?.workflowState?.steps ?? {}).map(([stepId, step]) => [ `${this.workflowId}.${stepId}`, step ]) ); const newPayload = { currentStep: { ...payload?.currentStep, id: `${workflowId}.${payload?.currentStep?.id}` }, workflowState: { steps: prefixedSteps } }; this.updateState(newPayload); cb({ type, payload: this.getState(), eventTimestamp }); } catch (e) { console.error(e); } }; this.emitter.on("nested-watch", nestedWatchCb); return () => { this.emitter.off("watch", watchCb); this.emitter.off("nested-watch", nestedWatchCb); }; } async resume(params) { const steps = (Array.isArray(params.step) ? params.step : [params.step]).map( (step) => typeof step === "string" ? step : step?.id ); const snapshot = await this.#mastra?.storage?.loadWorkflowSnapshot({ workflowName: this.workflowId, runId: this.runId }); return this.executionEngine.execute({ workflowId: this.workflowId, runId: this.runId, graph: this.executionGraph, input: params.resumeData, resume: { steps, stepResults: snapshot?.context, resumePayload: params.resumeData, // @ts-ignore resumePath: snapshot?.suspendedPaths?.[steps?.[0]] }, emitter: this.emitter, runtimeContext: params.runtimeContext ?? new RuntimeContext() }); } /** * Returns the current state of the workflow run * @returns The current state of the workflow run */ getState() { return this.state; } updateState(state) { if (state.currentStep) { this.state.currentStep = state.currentStep; } else if (state.workflowState?.status !== "running") { delete this.state.currentStep; } if (state.workflowState) { this.state.workflowState = deepMerge(this.state.workflowState ?? {}, state.workflowState ?? {}); } } }; function deepMerge(a, b) { if (!a || typeof a !== "object") return b; if (!b || typeof b !== "object") return a; const result = { ...a }; for (const key in b) { if (b[key] === void 0) continue; if (b[key] !== null && typeof b[key] === "object") { const aVal = result[key]; const bVal = b[key]; if (Array.isArray(bVal)) { result[key] = Array.isArray(aVal) ? [...aVal, ...bVal].filter((item) => item !== void 0) : bVal.filter((item) => item !== void 0); } else if (typeof aVal === "object" && aVal !== null) { result[key] = deepMerge(aVal, bVal); } else { result[key] = bVal; } } else { result[key] = b[key]; } } return result; } export { DefaultExecutionEngine, ExecutionEngine, NewWorkflow, Run, cloneStep, cloneWorkflow, createStep, createWorkflow };