UNPKG

@mastra/core

Version:
1,298 lines 139 kB
import { a as RegisteredLogger } from "./logger-B_aQzjbm.js"; import { t as MastraBase } from "./base-BeUQ6mLP.js"; import { a as MastraNonRetryableError, i as MastraError, n as ErrorDomain, o as getErrorFromUnknown, t as ErrorCategory } from "./error-MjDSls8S.js"; import { a as resolveObservabilityContext, i as createObservabilityContext } from "./observability-Cz-X7NF_.js"; import { f as EntityType, t as executeWithContext } from "./utils-DxsDNzD2.js"; import "./tracing-Bm0k4FBA.js"; import { a as RequestContext } from "./request-context-p_Tq-4EM.js"; import { toStandardSchema } from "./schema/index.js"; import { n as ToolStream } from "./types-C59tsW89.js"; import { v as removeUndefinedValues } from "./utils-CCbB2dG1.js"; import { t as TripWire } from "./trip-wire-csEv7lz7.js"; import { PUBSUB_SYMBOL, STREAM_FORMAT_SYMBOL } from "./workflows/constants.js"; import { randomUUID } from "crypto"; import EventEmitter$1 from "events"; import { z } from "zod"; //#region src/workflows/stream-utils.ts async function forwardAgentStreamChunk({ writer, chunk }) { if (!writer) return; await writer.write(chunk); } //#endregion //#region src/workflows/step.ts const getStepResult = (stepResults, step) => { let result; if (typeof step === "string") result = stepResults[step]; else { if (!step?.id) return null; result = stepResults[step.id]; } return result?.status === "success" ? result.output : null; }; //#endregion //#region src/workflows/step-entry.ts /** * Accessors for the {@link SingleStepEntry} union. * * This module is the single place allowed to pattern-match the union's shape. * Everything else (both engines, handlers, utils) should go through these * helpers so that adding a new variant means changing exactly one file. * * Union *shape* questions (id, retries, schemas, …) live here; how each * declarative kind is *interpreted* at run time lives in `./entry-executors`. */ /** * The id of a single step-like entry. Plain `step` entries key off the wrapped * step's id; declarative variants (agent / tool / mapping) carry their own `id`. */ function getEntryId(entry) { return entry.type === "step" ? entry.step.id : entry.id; } /** * The effective retry count for an entry, falling back to the provided * workflow-level default when the entry doesn't declare its own. * * - `step` — the step's own `retries` * - `agent` / `tool` — the declarative `options.retries` * - `mapping` — never declares retries; always the fallback */ function getEntryRetries(entry, fallback) { switch (entry.type) { case "step": return entry.step.retries ?? fallback; case "agent": case "tool": return entry.options?.retries ?? fallback; case "mapping": return fallback; } } /** * The `component` discriminator of the entry, if any. Only plain `step` * entries can carry one (notably `'WORKFLOW'` for nested workflows); * declarative variants have none. */ function getEntryComponent(entry) { return entry.type === "step" ? entry.step.component : void 0; } /** * Probes an entry for a nested workflow. Only the `type: 'step'` variant can * wrap a live `Workflow` (identified by its `component === 'WORKFLOW'` * discriminator from MastraBase); declarative variants never nest one. */ function getEntryWorkflow(entry) { if (entry.type !== "step") return null; const step = entry.step; if (step && typeof step === "object" && step.component === "WORKFLOW") return entry.step; return null; } /** * The validation schemas of an entry, used by the engines to validate step * input / suspend / resume data without materializing a live Step. * * - `step` — the step's own schemas * - `agent` — the fixed `{ prompt: string }` input contract (mirrors `createStepFromAgent`) * - `tool` — the resolved tool's schemas * - `mapping` — none (mappings accept and return anything) * * Never throws: when a tool can't be resolved the schemas are simply empty and * the run path surfaces the actionable not-found error. */ function getEntrySchemas(entry, mastra) { switch (entry.type) { case "step": return { inputSchema: entry.step.inputSchema, resumeSchema: entry.step.resumeSchema, suspendSchema: entry.step.suspendSchema }; case "agent": return { inputSchema: toStandardSchema(z.object({ prompt: z.string() })) }; case "tool": { let tool; try { tool = entry.tool ?? mastra?.getTool(entry.toolId); } catch { tool = void 0; } return tool ? { inputSchema: tool.inputSchema, resumeSchema: tool.resumeSchema, suspendSchema: tool.suspendSchema } : {}; } case "mapping": return {}; } } //#endregion //#region src/workflows/utils.ts /** * Validates data against a StandardSchema and returns the result. * Works with both sync and async schemas. */ async function validateWithStandardSchema(schema, data) { const result = schema["~standard"].validate(data); const resolvedResult = result instanceof Promise ? await result : result; if ("issues" in resolvedResult && resolvedResult.issues) return { success: false, issues: resolvedResult.issues.map((issue) => ({ path: issue.path?.map((p) => typeof p === "object" && "key" in p ? p.key : p), message: issue.message })) }; return { success: true, data: resolvedResult.value }; } async function validateStepInput({ prevOutput, step, validateInputs }) { let inputData = prevOutput; let validationError; const inputSchema = step.inputSchema; if (validateInputs && inputSchema) { const validatedInput = await validateWithStandardSchema(inputSchema, prevOutput); if (!validatedInput.success) { const errorMessages = validatedInput.issues.map((e) => `- ${e.path?.join(".")}: ${e.message}`).join("\n"); validationError = new MastraError({ id: "WORKFLOW_STEP_INPUT_VALIDATION_FAILED", domain: ErrorDomain.MASTRA_WORKFLOW, category: ErrorCategory.USER, text: "Step input validation failed: \n" + errorMessages }, { issues: validatedInput.issues }); } else inputData = validatedInput.data !== null && typeof validatedInput.data === "object" && !Array.isArray(validatedInput.data) && Object.keys(validatedInput.data).length === 0 ? prevOutput : validatedInput.data; } return { inputData, validationError }; } async function validateStepResumeData({ resumeData, step }) { if (!resumeData) return { resumeData: void 0, validationError: void 0 }; let validationError; const resumeSchema = step.resumeSchema; if (resumeSchema) { const validatedResumeData = await validateWithStandardSchema(resumeSchema, resumeData); if (!validatedResumeData.success) { const errorMessages = validatedResumeData.issues.map((e) => `- ${e.path?.join(".")}: ${e.message}`).join("\n"); validationError = new MastraError({ id: "WORKFLOW_STEP_RESUME_DATA_VALIDATION_FAILED", domain: ErrorDomain.MASTRA_WORKFLOW, category: ErrorCategory.USER, text: "Step resume data validation failed: \n" + errorMessages }); } else resumeData = validatedResumeData.data; } return { resumeData, validationError }; } async function validateStepSuspendData({ suspendData, step, validateInputs }) { if (!suspendData) return { suspendData: void 0, validationError: void 0 }; let validationError; const suspendSchema = step.suspendSchema; if (suspendSchema && validateInputs) { const validatedSuspendData = await validateWithStandardSchema(suspendSchema, suspendData); if (!validatedSuspendData.success) { const errorMessages = validatedSuspendData.issues.map((e) => `- ${e.path?.join(".")}: ${e.message}`).join("\n"); validationError = new MastraError({ id: "WORKFLOW_STEP_SUSPEND_DATA_VALIDATION_FAILED", domain: ErrorDomain.MASTRA_WORKFLOW, category: ErrorCategory.USER, text: "Step suspend data validation failed: \n" + errorMessages }); } else suspendData = validatedSuspendData.data; } return { suspendData, validationError }; } async function validateStepStateData({ stateData, step, validateInputs }) { if (!stateData) return { stateData: void 0, validationError: void 0 }; let validationError; const stateSchema = step.stateSchema; if (stateSchema && validateInputs) { const validatedStateData = await validateWithStandardSchema(stateSchema, stateData); if (!validatedStateData.success) { const errorMessages = validatedStateData.issues.map((e) => `- ${e.path?.join(".")}: ${e.message}`).join("\n"); validationError = /* @__PURE__ */ new Error("Step state data validation failed: \n" + errorMessages); } else stateData = validatedStateData.data; } return { stateData, validationError }; } async function validateStepRequestContext({ requestContext, step, validateInputs }) { let validationError; const requestContextSchema = step.requestContextSchema; if (requestContextSchema && validateInputs) { const validatedRequestContext = await validateWithStandardSchema(requestContextSchema, requestContext?.all ?? {}); if (!validatedRequestContext.success) { const errorMessages = validatedRequestContext.issues.map((e) => `- ${e.path?.join(".")}: ${e.message}`).join("\n"); validationError = new MastraError({ id: "WORKFLOW_STEP_REQUEST_CONTEXT_VALIDATION_FAILED", domain: ErrorDomain.MASTRA_WORKFLOW, category: ErrorCategory.USER, text: `Step request context validation failed for step '${step.id}': \n` + errorMessages }); } } return { validationError }; } function getResumeLabelsByStepId(resumeLabels, stepId) { return Object.entries(resumeLabels).filter(([_, value]) => value.stepId === stepId).reduce((acc, [key, value]) => { acc[key] = value; return acc; }, {}); } const runCountDeprecationMessage = "Warning: 'runCount' is deprecated and will be removed on November 4th, 2025. Please use 'retryCount' instead."; /** * Track which deprecation warnings have been shown globally to avoid spam */ const shownWarnings = /* @__PURE__ */ new Set(); /** * Creates a Proxy that wraps execute function parameters to show deprecation warnings * when accessing deprecated properties. * * Currently handles: * - `runCount`: Deprecated in favor of `retryCount`, will be removed on November 4th, 2025 */ function createDeprecationProxy(params, { paramName, deprecationMessage, logger }) { return new Proxy(params, { get(target, prop, receiver) { if (prop === paramName && !shownWarnings.has(paramName)) { shownWarnings.add(paramName); if (logger) logger.warn("\x1B[33m%s\x1B[0m", deprecationMessage); else console.warn("\x1B[33m%s\x1B[0m", deprecationMessage); } return Reflect.get(target, prop, receiver); } }); } const SINGLE_STEP_TYPES = [ "step", "agent", "tool", "mapping" ]; /** * Whether an entry is a "single step-like" entry: a plain user step or one of the * declarative variants (agent / tool / mapping) that resolve to exactly one step. */ function isSingleStepEntry(entry) { return SINGLE_STEP_TYPES.includes(entry.type); } /** * The id of a single step-like entry. Plain `step` entries key off the wrapped * step's id; declarative variants (agent / tool / mapping) carry their own `id`. * * Public alias of {@link getEntryId} from `./step-entry`. */ const getSingleStepEntryId = getEntryId; const getStepIds = (entry) => { if (isSingleStepEntry(entry)) return [getSingleStepEntryId(entry)]; if (entry.type === "foreach" || entry.type === "loop") return [getSingleStepEntryId(entry.step)]; if (entry.type === "parallel" || entry.type === "conditional") return entry.steps.map((s) => getSingleStepEntryId(s)); if (entry.type === "sleep" || entry.type === "sleepUntil") return [entry.id]; return []; }; const createTimeTravelExecutionParams = (params) => { const { steps, inputData, resumeData, context, nestedStepsContext, snapshot, initialState, graph, perStep } = params; const firstStepId = steps[0]; let executionPath = []; const stepResults = {}; const snapshotContext = snapshot.context; for (const [index, entry] of graph.steps.entries()) { const currentExecPathLength = executionPath.length; if (currentExecPathLength > 0 && !resumeData) break; const stepIds = getStepIds(entry); const isTargetEntry = stepIds.includes(firstStepId); if (isTargetEntry) executionPath = [index, ...stepIds?.length > 1 ? [stepIds?.findIndex((s) => s === firstStepId)] : []]; const prevStep = graph.steps[index - 1]; let stepPayload = void 0; if (prevStep) { const prevStepIds = getStepIds(prevStep); if (prevStepIds.length > 0) if (prevStepIds.length === 1) stepPayload = (stepResults?.[prevStepIds[0]])?.output ?? {}; else stepPayload = prevStepIds.reduce((acc, stepId) => { acc[stepId] = (stepResults?.[stepId])?.output ?? {}; return acc; }, {}); } if (index === 0 && stepIds.includes(firstStepId)) stepResults.input = context?.[firstStepId]?.payload ?? inputData ?? snapshotContext?.input; else if (index === 0) stepResults.input = stepIds?.reduce((acc, stepId) => { if (acc) return acc; return context?.[stepId]?.payload ?? snapshotContext?.[stepId]?.payload; }, null) ?? snapshotContext?.input ?? {}; let stepOutput = void 0; const nextStep = graph.steps[index + 1]; if (nextStep) { const nextStepIds = getStepIds(nextStep); if (nextStepIds.length > 0 && inputData && nextStepIds.includes(firstStepId) && steps.length === 1) stepOutput = inputData; } stepIds.forEach((stepId) => { let result; const stepContext = context?.[stepId] ?? snapshotContext[stepId]; const isUnselectedConditionalSibling = isTargetEntry && entry.type === "conditional" && !steps?.includes(stepId); const defaultStepStatus = steps?.includes(stepId) ? "running" : isUnselectedConditionalSibling ? "skipped" : "success"; const status = ["failed", "canceled"].includes(stepContext?.status) ? defaultStepStatus : stepContext?.status ?? defaultStepStatus; const isCompleteStatus = [ "success", "failed", "canceled" ].includes(status); result = { status, payload: context?.[stepId]?.payload ?? stepPayload ?? snapshotContext[stepId]?.payload ?? {}, output: isCompleteStatus ? context?.[stepId]?.output ?? stepOutput ?? snapshotContext[stepId]?.output ?? {} : void 0, resumePayload: stepContext?.resumePayload, suspendPayload: stepContext?.suspendPayload, suspendOutput: stepContext?.suspendOutput, startedAt: stepContext?.startedAt ?? Date.now(), endedAt: isCompleteStatus ? stepContext?.endedAt ?? Date.now() : void 0, suspendedAt: stepContext?.suspendedAt, resumedAt: stepContext?.resumedAt }; if ((perStep ? executionPath.length : currentExecPathLength) > 0 && !steps?.includes(stepId) && !context?.[stepId] && (!snapshotContext[stepId] || snapshotContext[stepId] && snapshotContext[stepId].status !== "suspended")) result = void 0; if (result) { const formattedResult = removeUndefinedValues(result); stepResults[stepId] = formattedResult; } }); } if (!executionPath.length) throw new Error(`Time travel target step not found in execution graph: '${steps?.join(".")}'. Verify the step id/path.`); return { inputData, executionPath, steps, stepResults, nestedStepResults: nestedStepsContext, state: initialState ?? snapshot.value ?? {}, resumeData, stepExecutionPath: snapshot?.stepExecutionPath }; }; const createRestartExecutionParams = ({ snapshot, graph }) => { let nestedWorkflowPending = false; if (snapshot.status !== "running" && snapshot.status !== "waiting") if (snapshot.status === "pending" && snapshot.context && Object.prototype.hasOwnProperty.call(snapshot.context, "input")) nestedWorkflowPending = true; else throw new Error("This workflow run was not active"); let nestedWorkflowActiveStepsPath = {}; const firstEntry = graph.steps[0]; if (isSingleStepEntry(firstEntry)) nestedWorkflowActiveStepsPath = { [getSingleStepEntryId(firstEntry)]: [0] }; else if (firstEntry.type === "foreach" || firstEntry.type === "loop") nestedWorkflowActiveStepsPath = { [getSingleStepEntryId(firstEntry.step)]: [0] }; else if (firstEntry.type === "sleep" || firstEntry.type === "sleepUntil") nestedWorkflowActiveStepsPath = { [firstEntry.id]: [0] }; else if (firstEntry.type === "conditional" || firstEntry.type === "parallel") nestedWorkflowActiveStepsPath = firstEntry.steps.reduce((acc, step) => { acc[getSingleStepEntryId(step)] = [0]; return acc; }, {}); return { activePaths: nestedWorkflowPending ? [0] : snapshot.activePaths, activeStepsPath: nestedWorkflowPending ? nestedWorkflowActiveStepsPath : snapshot.activeStepsPath, stepResults: snapshot.context, state: snapshot.value, stepExecutionPath: snapshot?.stepExecutionPath }; }; /** * Re-hydrates serialized errors in step results back into proper Error instances. * This is useful when errors have been serialized through an event system (e.g., evented engine, Inngest) * and need to be converted back to Error instances with their custom properties preserved. * * @param steps - The workflow step results (context) that may contain serialized errors * @returns The same steps object with errors hydrated as Error instances */ function hydrateSerializedStepErrors(steps) { if (steps) { for (const step of Object.values(steps)) if (step.status === "failed" && "error" in step && step.error) step.error = getErrorFromUnknown(step.error, { serializeStack: false }); } return steps; } /** * Cleans a single step result object by removing internal properties. * This is a helper for cleanStepResult that handles one level of cleaning. */ function cleanSingleResult(result) { const { __state: _state, metadata, ...rest } = result; if (metadata && typeof metadata === "object" && !Array.isArray(metadata)) { const { nestedRunId: _nestedRunId, ...userMetadata } = metadata; if (Object.keys(userMetadata).length > 0) return { ...rest, metadata: userMetadata }; } return rest; } /** * Cleans step result data by removing internal properties at known structural levels. * * Removes: * - `__state` properties (internal workflow state for state propagation) * - `nestedRunId` from `metadata` objects (internal tracking for nested workflow retrieval) * * ## Why targeted cleaning instead of recursive? * * Internal properties only appear at specific, known locations: * * 1. **`__state`** - Added by step-executor.ts to every step result. For forEach, * suspended iterations store the full result (including __state) while completed * iterations only store the output value. See workflow-event-processor/index.ts:1227-1230. * * 2. **`metadata.nestedRunId`** - Added when nested workflows complete, stored at the * step result level. For forEach with nested workflows, each iteration result can * have this. See workflow-event-processor/index.ts:1449-1453. * * By only cleaning at the step result level and forEach iteration level, we avoid * accidentally stripping user data that happens to use `__state` as a property name * in their actual output values. * * @param stepResult - A step result object, or an array of iteration results (forEach) * @returns The cleaned step result with internal properties removed */ function cleanStepResult(stepResult) { if (stepResult === null || stepResult === void 0) return stepResult; if (typeof stepResult !== "object") return stepResult; if (Array.isArray(stepResult)) return stepResult.map((item) => { if (item && typeof item === "object" && !Array.isArray(item)) return cleanSingleResult(item); return item; }); const cleaned = cleanSingleResult(stepResult); if (Array.isArray(cleaned.output)) cleaned.output = cleaned.output.map((item) => { if (item && typeof item === "object" && !Array.isArray(item)) return cleanSingleResult(item); return item; }); return cleaned; } /** * Resolves the effective concurrency for a foreach entry at execution time. * * Supports both a static number and a {@link ForeachConcurrencyResolver} * function that derives concurrency from the run's input. Invalid or * non-positive values fall back to 1 (sequential). */ function resolveForeachConcurrency(opts, context) { const configured = opts?.concurrency ?? 1; const resolved = typeof configured === "function" ? configured(context) : configured; if (typeof resolved !== "number" || !Number.isFinite(resolved) || resolved < 1) return 1; return Math.floor(resolved); } const RESUME_SNAPSHOT_POLL_INTERVAL_MS = 25; const RESUME_SNAPSHOT_POLL_TIMEOUT_MS = 2e3; async function waitForSuspendedSnapshot(workflowsStore, workflowName, runId) { if (!workflowsStore) return null; const deadline = Date.now() + RESUME_SNAPSHOT_POLL_TIMEOUT_MS; let snapshot = await workflowsStore.loadWorkflowSnapshot({ workflowName, runId }) ?? null; while ((!snapshot || snapshot.status !== "suspended") && Date.now() < deadline) { await new Promise((resolve) => setTimeout(resolve, RESUME_SNAPSHOT_POLL_INTERVAL_MS)); snapshot = await workflowsStore.loadWorkflowSnapshot({ workflowName, runId }) ?? null; } return snapshot; } //#endregion //#region src/workflows/entry-executors/run-agent-entry.ts /** * Runs a declarative `agent` entry: resolves the agent (inline handle, else the * Mastra registry), streams the prompt through it, forwards stream chunks, and * returns either the structured output or `{ text }`. * * `ctx` is the step execute context (the same object a plain step's `execute` * receives). `mastra` defaults to `ctx.mastra` when omitted. */ async function runAgentEntry(entry, ctx, mastra) { const registry = mastra ?? ctx?.mastra; const agent = entry.agent ?? registry?.getAgentById(entry.agentId); if (!agent) throw new Error(`Agent '${entry.agentId}' not found for workflow step '${entry.id}'. Register the agent on the Mastra instance or pass the agent instance directly.`); const { retries: _retries, scorers: _scorers, metadata: _metadata, ...agentOptions } = entry.options ?? {}; const { inputData, runId, [PUBSUB_SYMBOL]: pubsub, [STREAM_FORMAT_SYMBOL]: streamFormat, requestContext, abortSignal, abort, writer, ...rest } = ctx; const observabilityContext = resolveObservabilityContext(rest); let streamPromise = {}; streamPromise.promise = new Promise((resolve, reject) => { streamPromise.resolve = resolve; streamPromise.reject = reject; }); streamPromise.promise.catch(() => {}); let structuredResult = null; const toolData = { name: agent.name, args: inputData }; let stream; const handleFinish = (result) => { const resultWithObject = result; if (agentOptions?.structuredOutput?.schema && resultWithObject.object) structuredResult = resultWithObject.object; streamPromise.resolve(result.text); agentOptions?.onFinish?.(result); }; if ((await agent.getModel({ requestContext })).specificationVersion === "v1" && typeof agent.streamLegacy === "function") { const { fullStream } = await agent.streamLegacy(inputData.prompt, { ...agentOptions, requestContext, ...observabilityContext, onFinish: handleFinish, abortSignal }); stream = fullStream; } else { const modelOutput = await agent.stream(inputData.prompt, { ...agentOptions, requestContext, ...observabilityContext, onFinish: handleFinish, abortSignal }); modelOutput.text.then(() => {}, (err) => streamPromise.reject(err)); stream = modelOutput.fullStream; } const tripwireChunk = streamFormat === "legacy" ? await bridgeLegacyWatchEvents({ stream, pubsub, runId, toolData }) : await consumeStreamForTripwire(stream, writer); if (tripwireChunk) throw new TripWire(tripwireChunk.payload?.reason || "Agent tripwire triggered", { retry: tripwireChunk.payload?.retry, metadata: tripwireChunk.payload?.metadata }, tripwireChunk.payload?.processorId); if (abortSignal.aborted) return abort(); if (structuredResult !== null) return structuredResult; return { text: await streamPromise.promise }; } /** * Legacy-format watch-event bridge: instead of forwarding chunks to the step * writer, mirrors the agent stream onto the run's pubsub watch channel as * `tool-call-streaming-*` / `tool-call-delta` events (the shape v1 watchers * expect). Returns the tripwire chunk if one was seen, else `null`. */ async function bridgeLegacyWatchEvents({ stream, pubsub, runId, toolData }) { let tripwireChunk = null; await pubsub.publish(`workflow.events.v2.${runId}`, { type: "watch", runId, data: { type: "tool-call-streaming-start", ...toolData ?? {} } }); try { for await (const chunk of stream) { if (chunk.type === "tripwire") { tripwireChunk = chunk; break; } if (chunk.type === "text-delta") await pubsub.publish(`workflow.events.v2.${runId}`, { type: "watch", runId, data: { type: "tool-call-delta", ...toolData ?? {}, argsTextDelta: chunk.textDelta } }); } } finally { await pubsub.publish(`workflow.events.v2.${runId}`, { type: "watch", runId, data: { type: "tool-call-streaming-finish", ...toolData ?? {} } }).catch(() => {}); } return tripwireChunk; } /** * Forwards every chunk to the step writer, stopping early when a tripwire * chunk appears. Returns the tripwire chunk if one was seen, else `null`. */ async function consumeStreamForTripwire(stream, writer) { for await (const chunk of stream) { await forwardAgentStreamChunk({ writer, chunk }); if (chunk.type === "tripwire") return chunk; } return null; } //#endregion //#region src/workflows/entry-executors/run-tool-entry.ts /** * Runs a declarative `tool` entry: resolves the tool (inline handle, else the * Mastra registry) and executes it with the step context mapped into the tool * execution context. */ async function runToolEntry(entry, ctx, mastra) { const registry = mastra ?? ctx?.mastra; const tool = entry.tool ?? registry?.getTool(entry.toolId); if (!tool) throw new Error(`Tool '${entry.toolId}' not found for workflow step '${entry.id}'. Pass the tool instance directly.`); const { inputData, mastra: ctxMastra, requestContext, suspend, resumeData, runId, workflowId, state, setState, abortSignal, ...rest } = ctx; const toolContext = { mastra: ctxMastra, requestContext, ...resolveObservabilityContext(rest), abortSignal, resumeData, workflow: { runId, suspend, resumeData, workflowId, state, setState } }; return tool.execute(inputData, toolContext); } //#endregion //#region src/workflows/mapping-template.ts /** * The `${scope.path}` mapping-template DSL used by `.map()` template sources. * * Definition-time syntax checks live in {@link validateTemplate}; run-time * resolution (path lookup + value coercion) lives in {@link resolveTemplate}. * This module has no knowledge of the step-entry union — it is a pure * string-DSL interpreter over a step's execute context. */ /** Walks a dotted path on an object. `''` or `'.'` returns the root unchanged. */ function traverseMappingPath(root, path, errorLabel) { if (path === "" || path === ".") return root; const parts = path.split("."); let value = root; for (const part of parts) if (typeof value === "object" && value !== null) value = value[part]; else throw new Error(`Invalid path ${path} in ${errorLabel}`); return value; } const TEMPLATE_PLACEHOLDER = /\$\{([^}]*)\}/g; const TEMPLATE_NAMESPACES = [ "inputData", "initData", "state", "requestContext", "stepResults" ]; /** Common error-message prefix so every template diagnostic points at the exact placeholder. */ function describeBadPlaceholder(template, idx, rawExpr) { return `Template placeholder #${idx} (\${${rawExpr}}) in '${template}'`; } /** Split a placeholder body `scope.path.with.dots` into its leading scope and the dotted remainder. */ function parseTemplatePlaceholder(rawExpr) { const dot = rawExpr.indexOf("."); return { scope: dot === -1 ? rawExpr : rawExpr.slice(0, dot), rest: dot === -1 ? "" : rawExpr.slice(dot + 1) }; } /** * Validates a `{ template }` source's syntax at workflow-definition time. * Throws if any placeholder is empty, whitespace-padded, references an unknown * namespace, or is a malformed `stepResults.<stepId>` / `stepResults.<stepId>.<path>` shape. * * Run-time concerns (does the step actually exist, does the path resolve, is * the value a primitive) stay in {@link resolveTemplate}. */ function validateTemplate(template) { let idx = 0; for (const match of template.matchAll(TEMPLATE_PLACEHOLDER)) { idx++; const rawExpr = match[1] ?? ""; if (rawExpr.length === 0 || rawExpr !== rawExpr.trim()) throw new Error(`${describeBadPlaceholder(template, idx, rawExpr)} has empty or whitespace-padded contents. Use \${<scope>.<path>} with no surrounding whitespace.`); const { scope, rest } = parseTemplatePlaceholder(rawExpr); if (scope === "stepResults") { const innerDot = rest.indexOf("."); if (!(innerDot === -1 ? rest : rest.slice(0, innerDot))) throw new Error(`${describeBadPlaceholder(template, idx, rawExpr)} must be of the form \${stepResults.<stepId>} or \${stepResults.<stepId>.<path>}.`); continue; } if (scope === "requestContext") { if (!rest) throw new Error(`${describeBadPlaceholder(template, idx, rawExpr)} requires a request-context key — use \${requestContext.<key>}.`); continue; } if (TEMPLATE_NAMESPACES.includes(scope)) continue; throw new Error(`${describeBadPlaceholder(template, idx, rawExpr)} references unknown namespace "${scope}". Use one of: ${TEMPLATE_NAMESPACES.join(", ")}.`); } } /** * Collects the step ids referenced by `${stepResults.<stepId>}` / * `${stepResults.<stepId>.<path>}` placeholders in a template. Assumes the * template already passed {@link validateTemplate}; malformed placeholders are * skipped. Used by validation to scope-check template references against the * preceding workflow-local steps. */ function collectTemplateStepIds(template) { const ids = []; for (const match of template.matchAll(TEMPLATE_PLACEHOLDER)) { const { scope, rest } = parseTemplatePlaceholder(match[1] ?? ""); if (scope !== "stepResults") continue; const innerDot = rest.indexOf("."); const stepId = innerDot === -1 ? rest : rest.slice(0, innerDot); if (stepId) ids.push(stepId); } return ids; } /** * Coerces a resolved placeholder value to a string. Primitives are stringified * the normal way; objects and arrays are JSON-encoded so downstream agents can * consume complex step outputs (e.g. `foreach(agent)` returns `{ text }[]`) * directly in a template. `null`/`undefined` render as empty. If JSON encoding * fails (circular references, BigInt, etc.), throws with a hint pointing at * the offending placeholder. */ function stringifyTemplateValue(v, template, idx, rawExpr) { if (v === null || v === void 0) return ""; if (typeof v === "object") try { return JSON.stringify(v); } catch (err) { throw new Error(`${describeBadPlaceholder(template, idx, rawExpr)} resolved to a value that could not be JSON-stringified (${err.message}). Drill into a primitive path (e.g. \${${rawExpr}.someField}) or reshape the value in a preceding step.`); } return String(v); } /** * Resolves `${<scope>.<path>}` placeholders against the implicit namespaces * available in a step's execute context. See the `.map()` overload signature * for the full list of accepted scopes (`inputData`, `initData`, `state`, * `requestContext`, `stepResults.<stepId>`). */ function resolveTemplate(template, ctx) { let idx = 0; return template.replace(TEMPLATE_PLACEHOLDER, (_match, rawExpr) => { idx++; return resolveTemplatePlaceholder(rawExpr, template, idx, ctx); }); } function resolveTemplatePlaceholder(rawExpr, template, idx, ctx) { const { scope, rest } = parseTemplatePlaceholder(rawExpr); const label = describeBadPlaceholder(template, idx, rawExpr); switch (scope) { case "inputData": return stringifyTemplateValue(traverseMappingPath(ctx.inputData, rest, label), template, idx, rawExpr); case "initData": return stringifyTemplateValue(traverseMappingPath(ctx.getInitData(), rest, label), template, idx, rawExpr); case "state": return stringifyTemplateValue(traverseMappingPath(ctx.state, rest, label), template, idx, rawExpr); case "requestContext": return stringifyTemplateValue(ctx.requestContext.get(rest), template, idx, rawExpr); case "stepResults": { const innerDot = rest.indexOf("."); const stepId = innerDot === -1 ? rest : rest.slice(0, innerDot); const subPath = innerDot === -1 ? "" : rest.slice(innerDot + 1); const stepResult = ctx.getStepResult(stepId); if (stepResult == null) throw new Error(`${label} references stepResults.${stepId} but step "${stepId}" has no successful output (not run yet, not registered, failed, or produced no output).`); return stringifyTemplateValue(traverseMappingPath(stepResult, subPath, label), template, idx, rawExpr); } default: throw new Error(`${label} references unknown namespace "${scope}". Use one of: ${TEMPLATE_NAMESPACES.join(", ")}.`); } } //#endregion //#region src/workflows/entry-executors/run-mapping-entry.ts /** * Runs a declarative `mapping` entry. Function configs are invoked directly; * object configs are interpreted key-by-key (`value` / `fn` / `template` / * `requestContextPath` / `step`+`path` / `initData`+`path`). */ async function runMappingEntry(entry, ctx) { const { mapConfig } = entry; if (typeof mapConfig === "function") return mapConfig(ctx); const { getStepResult, getInitData, requestContext } = ctx; const result = {}; for (const [key, mapping] of Object.entries(mapConfig)) { const m = mapping; if (m.value !== void 0) { result[key] = m.value; continue; } if (m.fn !== void 0) { result[key] = await m.fn(ctx); continue; } if (typeof m.template === "string") { result[key] = resolveTemplate(m.template, ctx); continue; } if (m.requestContextPath) { result[key] = requestContext.get(m.requestContextPath); continue; } result[key] = traverseMappingPath(m.initData ? getInitData() : getStepResult(Array.isArray(m.step) ? m.step.find((s) => { const stepRes = getStepResult(s); if (typeof stepRes === "object" && stepRes !== null) return Object.keys(stepRes).length > 0; return stepRes; }) : m.step), m.path, describeMappingSource(m)); } return result; } /** Human-readable source label for path-traversal errors. */ function describeMappingSource(m) { if (m.initData) return "initData"; const stepLabel = (s) => typeof s === "string" ? s : s?.id ?? "unknown"; if (Array.isArray(m.step)) return `step ${m.step.map(stepLabel).join("|")}`; return `step ${stepLabel(m.step)}`; } //#endregion //#region src/workflows/evented/workflow-event-processor/utils.ts function getNestedWorkflow(mastra, { workflowId, executionPath, parentWorkflow, runId }) { let workflow = null; if (parentWorkflow) { const nestedWorkflow = getNestedWorkflow(mastra, parentWorkflow); if (!nestedWorkflow) return null; workflow = nestedWorkflow; } workflow = workflow ?? (mastra.__hasInternalWorkflow(workflowId, runId) ? mastra.__getInternalWorkflow(workflowId, runId) : mastra.getWorkflow(workflowId)); let parentStep = workflow.stepGraph[executionPath[0]]; if (parentStep?.type === "parallel" || parentStep?.type === "conditional") parentStep = parentStep.steps[executionPath[1]]; if (parentStep?.type === "loop" || parentStep?.type === "foreach") return getEntryWorkflow(parentStep.step); if (parentStep && isSingleStepEntry(parentStep)) return getEntryWorkflow(parentStep); return null; } /** * Resolves the single-step entry addressed by an execution path, or null when * the path doesn't land on a single-step-like entry. For `loop` / `foreach` * the body entry is returned. */ function getStepEntry(workflow, executionPath) { let parentStep = workflow.stepGraph[executionPath[0]]; if (parentStep?.type === "parallel" || parentStep?.type === "conditional") parentStep = parentStep.steps[executionPath[1]]; if (parentStep?.type === "loop" || parentStep?.type === "foreach") return parentStep.step; if (parentStep && isSingleStepEntry(parentStep)) return parentStep; return null; } /** * Resolves the id of the entry addressed by an execution path, or null when the * path doesn't land on a single-step-like entry. For `loop` / `foreach` the id * of the body entry is returned. */ function getStepId(workflow, executionPath) { const entry = getStepEntry(workflow, executionPath); return entry ? getEntryId(entry) : null; } function isExecutableStep(step) { return isSingleStepEntry(step) || step.type === "loop" || step.type === "foreach"; } //#endregion //#region src/workflows/evented/helpers.ts /** * Helper functions for evented workflow execution. */ /** * Type guard to check if a chunk is a tripwire chunk. * @param chunk - The chunk to check * @returns True if the chunk is a TripwireChunk */ function isTripwireChunk(chunk) { return chunk !== null && typeof chunk === "object" && "type" in chunk && chunk.type === "tripwire" && "payload" in chunk; } /** * Creates a TripWire error from a tripwire chunk. * @param chunk - The tripwire chunk from the stream * @returns A TripWire error instance */ function createTripWireFromChunk(chunk) { const { payload } = chunk; return new TripWire(payload.reason || "Agent tripwire triggered", { retry: payload.retry, metadata: payload.metadata }, payload.processorId); } /** * Extracts text delta from a stream chunk, handling V1 vs V2 differences. * * V1 (AI SDK v4): Uses `chunk.textDelta` for raw text * V2 (AI SDK v5): Uses `chunk.payload.text` for normalized text * * @param chunk - The stream chunk * @param isV2Model - Whether this is a V2 model (uses normalized payload) * @returns The text delta string, or undefined if not a text-delta chunk */ function getTextDeltaFromChunk(chunk, isV2Model) { if (chunk.type !== "text-delta") return; return isV2Model ? chunk.payload?.text : chunk.textDelta; } /** * Resolves the current workflow state from multiple potential sources. * Priority order: stepResult.__state > stepResults.__state > state > empty object * * @param params - The state sources to check * @returns The resolved state object */ function resolveCurrentState(params) { const { stepResult, stepResults, state } = params; return stepResult?.__state ?? stepResults?.__state ?? state ?? {}; } //#endregion //#region src/events/processor.ts var EventProcessor = class { mastra; __registerMastra(mastra) { this.mastra = mastra; } constructor({ mastra }) { this.mastra = mastra; } }; //#endregion //#region src/workflows/evented/step-executor.ts var StepExecutor = class extends MastraBase { mastra; constructor({ mastra }) { super({ name: "StepExecutor", component: RegisteredLogger.WORKFLOW }); this.mastra = mastra; } __registerMastra(mastra) { this.mastra = mastra; const logger = mastra?.getLogger(); if (logger) this.__setLogger(logger); } /** * Creates an output writer function that publishes chunks to the workflow event stream. * @param runId - The workflow run ID * @returns An async function that writes chunks to the pubsub */ createOutputWriter(runId) { return async (chunk) => { try { if (this.mastra?.pubsub) await this.mastra.pubsub.publish(`workflow.events.v2.${runId}`, { type: "watch", runId, data: chunk }); } catch (err) { this.logger.debug("Failed to publish workflow watch event", { runId, error: err }); } }; } async execute(params) { const { entry, stepResults, runId, requestContext, retryCount = 0, perStep } = params; const stepId = getEntryId(entry); const schemas = getEntrySchemas(entry, this.mastra); const abortController = params.abortController ?? new AbortController(); let suspended; let bailed; const startedAt = Date.now(); const { inputData, validationError } = await validateStepInput({ prevOutput: typeof params.foreachIdx === "number" ? params.input?.[params.foreachIdx] : params.input, step: schemas, validateInputs: params.validateInputs ?? true }); let stepInfo = { ...stepResults[stepId], startedAt, payload: (typeof params.foreachIdx === "number" ? params.input : inputData) ?? {} }; if (params.resumeData) { stepInfo.resumePayload = params.resumeData; stepInfo.resumedAt = Date.now(); if (stepInfo.suspendPayload && "__workflow_meta" in stepInfo.suspendPayload) { const { __workflow_meta, ...userSuspendPayload } = stepInfo.suspendPayload; stepInfo.suspendPayload = userSuspendPayload; } } let suspendDataToUse = params.stepResults[stepId]?.status === "suspended" ? params.stepResults[stepId]?.suspendPayload : void 0; if (suspendDataToUse && typeof params.foreachIdx === "number") { const iterationResult = suspendDataToUse.__workflow_meta?.foreachOutput?.[params.foreachIdx]; if (iterationResult?.status === "suspended" && iterationResult.suspendPayload) suspendDataToUse = iterationResult.suspendPayload; } if (suspendDataToUse && "__workflow_meta" in suspendDataToUse) { const { __workflow_meta, ...userSuspendData } = suspendDataToUse; suspendDataToUse = userSuspendData; } let stateUpdate; const workflowStepSpan = params.tracingContext?.currentSpan?.createChildSpan({ type: "workflow_step", name: `workflow step: '${stepId}'`, entityType: EntityType.WORKFLOW_STEP, entityId: stepId, input: inputData, tracingPolicy: params.tracingPolicy, requestContext }); const stepTracingContext = workflowStepSpan ? { currentSpan: workflowStepSpan } : params.tracingContext ?? {}; try { if (validationError) throw validationError; const callId = randomUUID(); const outputWriter = this.createOutputWriter(runId); const stepOutput = await executeWithContext({ span: stepTracingContext.currentSpan, fn: () => { const executionContext = createDeprecationProxy({ workflowId: params.workflowId, runId, mastra: this.mastra, requestContext, inputData, state: params.state, setState: async (newState) => { stateUpdate = { ...stateUpdate ?? params.state, ...newState }; }, retryCount, resumeData: params.resumeData, suspendData: suspendDataToUse, getInitData: () => stepResults?.input, getStepResult: getStepResult.bind(this, stepResults), suspend: async (suspendPayload, suspendOptions) => { const { suspendData, validationError } = await validateStepSuspendData({ suspendData: suspendPayload, step: schemas, validateInputs: params.validateInputs ?? true }); if (validationError) throw validationError; const resumeLabels = {}; if (suspendOptions?.resumeLabel) { const labels = Array.isArray(suspendOptions.resumeLabel) ? suspendOptions.resumeLabel : [suspendOptions.resumeLabel]; for (const label of labels) resumeLabels[label] = { stepId, foreachIndex: params.foreachIdx }; } suspended = { payload: { ...suspendData, __workflow_meta: { runId, path: [stepId], foreachIndex: params.foreachIdx, resumeLabels: Object.keys(resumeLabels).length > 0 ? resumeLabels : void 0 } } }; }, bail: (result) => { bailed = { payload: result }; }, writer: new ToolStream({ prefix: "workflow-step", callId, name: stepId, runId }, outputWriter), abort: () => { abortController?.abort(); }, [PUBSUB_SYMBOL]: this.mastra.pubsub, [STREAM_FORMAT_SYMBOL]: params.format, engine: {}, abortSignal: abortController?.signal, ...createObservabilityContext(stepTracingContext) }, { paramName: "runCount", deprecationMessage: runCountDeprecationMessage, logger: this.logger }); switch (entry.type) { case "step": return entry.step.execute(executionContext); case "agent": return runAgentEntry(entry, executionContext, this.mastra); case "tool": return runToolEntry(entry, executionContext, this.mastra); case "mapping": return runMappingEntry(entry, executionContext); } } }); const nestedWflowStepPaused = getEntryComponent(entry) === "WORKFLOW" && perStep; const endedAt = Date.now(); const finalState = stateUpdate ?? params.state; let finalResult; if (suspended) { finalResult = { ...stepInfo, status: "suspended", suspendedAt: endedAt, ...stepOutput ? { suspendOutput: stepOutput } : {}, __state: finalState }; if (suspended.payload) finalResult.suspendPayload = suspended.payload; } else if (bailed) finalResult = { ...stepInfo, status: "bailed", endedAt, output: bailed.payload, __state: finalState }; else if (nestedWflowStepPaused) finalResult = { ...stepInfo, status: "paused", __state: finalState }; else finalResult = { ...stepInfo, status: "success", endedAt, output: stepOutput, __state: finalState }; if (finalResult.status === "success") workflowStepSpan?.end({ output: stepOutput, attributes: { status: "success" } }); else workflowStepSpan?.end({ attributes: { status: finalResult.status } }); return finalResult; } catch (error) { const endedAt = Date.now(); const errorInstance = getErrorFromUnknown(error, { serializeStack: false, fallbackMessage: "Unknown step execution error" }); workflowStepSpan?.error({ error: errorInstance }); const mastraError = new MastraError({ id: "WORKFLOW_STEP_INVOKE_FAILED", domain: ErrorDomain.MASTRA_WORKFLOW, category: ErrorCategory.USER, details: { workflowId: params.workflowId, runId: params.runId, stepId } }, errorInstance); this.logger?.trackException(mastraError); this.logger?.error(`Error executing step ${stepId}: ` + errorInstance?.stack); return { ...stepInfo, status: "failed", endedAt, error: errorInstance, ...error instanceof MastraNonRetryableError && { nonRetryable: true }, tripwire: error instanceof TripWire ? { reason: error.message, retry: error.options?.retry, metadata: error.options?.metadata, processorId: error.processorId } : void 0 }; } } async evaluateConditions(params) { const { step, stepResults, runId, requestContext, retryCount = 0 } = params; const abortController = params.abortController ?? new AbortController(); return (await Promise.all(step.conditions.map((condition) => { try { return this.evaluateCondition({ workflowId: params.workflowId, condition, runId, requestContext, inputData: params.input, state: params.state, retryCount, resumeData: params.resumeData, abortController, stepResults, iterationCount: 0 }); } catch (e) { this.mastra?.getLogger()?.error("error evaluating condition", e); return false; } }))).reduce((acc, result, idx) => { if (result) acc.push(idx); return acc; }, []); } async evaluateCondition({ workflowId, condition, runId, inputData, resumeData, stepResults, state, requestContext, abortController, retryCount = 0, iterationCount }) { const callId = randomUUID(); const outputWriter = this.createOutputWriter(runId); return condition(createDeprecationProxy({ workflowId, runId, mastra: this.mastra, requestContext, inputData, state, retryCount, resumeData, getInitData: () => stepResults?.input, getStepResult: getStepResult.bind(this, stepResults), bail: (_result) => { throw new Error("Not implemented"); }, writer: new ToolStream({ prefix: "workflow-step", callId, name: "condition", runId }, outputWriter), abort: () => { abortController?.abort(); }, [PUBSUB_SYMBOL]: this.mastra.pubsub, [STREAM_FORMAT_SYMBOL]: void 0, engine: {}, abortSignal: abortController?.signal, ...createObservabilityContext(), iterationCount }, { paramName: "runCount", deprecationMessage: runCountDeprecationMessage, logger: this.logger })); } async resolveSleep(params) { const { step, stepResults, runId, requestContext, retryCount = 0 } = params; const currentState = params.state ?? stepResults?.__state ?? {}; const abortController = params.abortController ?? new AbortController(); if (step.duration) return step.duration; if (!step.fn) return 0; try { const callId = randomUUID(); const outputWriter = this.createOutputWriter(runId); return await step.fn(createDeprecationProxy({ workflowId: params.workflowId, runId, mastra: this.mastra, requestContext, inputData: params.input, state: currentState, setState: async (newState) => { Object.assign(currentState, newState); }, retryCount, resumeData: params.resumeData, getInitData: () => stepResults?.input, getStepResult: getStepResult.bind(this, stepResults), suspend: async (_suspendPayload) => { throw new Error("Not implemented"); }, bail: (_result) => { throw new Error("Not implemented"); }, abort: () => { abortController?.abort(); }, writer: new ToolStream({ prefix: "workflow-step", callId, name: step.id, runId }, outputWriter), [PUBSUB_SYMBOL]: this.mastra.pubsub, [STREAM_FORMAT_SYMBOL]: void 0, engine: {}, abortSignal: abortController?.signal, ...createObservabilityContext() }, { paramName: "runCount", deprecationMessage: runCountDeprecationMessage, logger: this.logger })); } catch (e) { this.mastra?.getLogger()?.error("error evaluating condition", e); return 0; } } async resolveSleepUntil(params) { const { step, stepResults, runId, requestContext, retryCount = 0 } = params; const currentState = params.state ?? stepResults?.__state ?? {}; const abortController = params.abortController ?? new AbortController(); if (step.date) return step.date.getTime() - Date.now(); if (!step.fn) return 0; try { const callId = randomUUID(); const outputWriter = this.createOutputWriter(runId); return (await step.fn(createDeprecationProxy({ workflowId: param