@mastra/core
Version:
1 lines • 297 kB
Source Map (JSON)
{"version":3,"file":"workflow-event-processor-CkjVcesJ.cjs","names":["z","MastraError","ErrorDomain","ErrorCategory","removeUndefinedValues","getErrorFromUnknown","result","PUBSUB_SYMBOL","STREAM_FORMAT_SYMBOL","resolveObservabilityContext","TripWire","resolveObservabilityContext","TripWire","MastraBase","RegisteredLogger","EntityType","executeWithContext","ToolStream","PUBSUB_SYMBOL","STREAM_FORMAT_SYMBOL","createObservabilityContext","getErrorFromUnknown","MastraError","ErrorDomain","ErrorCategory","MastraNonRetryableError","TripWire","RequestContext","RequestContext","RequestContext","getErrorFromUnknown","MastraError","ErrorDomain","ErrorCategory","EventEmitter","RequestContext","#dispatch","#setDeliveryAttempts","#tryResolveWorkflow"],"sources":["../src/workflows/stream-utils.ts","../src/workflows/step.ts","../src/workflows/step-entry.ts","../src/workflows/utils.ts","../src/workflows/entry-executors/run-agent-entry.ts","../src/workflows/entry-executors/run-tool-entry.ts","../src/workflows/mapping-template.ts","../src/workflows/entry-executors/run-mapping-entry.ts","../src/workflows/evented/workflow-event-processor/utils.ts","../src/workflows/evented/helpers.ts","../src/events/processor.ts","../src/workflows/evented/step-executor.ts","../src/workflows/evented/types.ts","../src/workflows/evented/workflow-event-processor/loop.ts","../src/workflows/evented/workflow-event-processor/parallel.ts","../src/workflows/evented/workflow-event-processor/sleep.ts","../src/workflows/evented/workflow-event-processor/index.ts"],"sourcesContent":["export type StreamChunkWriter = {\n write: (chunk: unknown) => Promise<void>;\n};\n\nexport async function forwardAgentStreamChunk({\n writer,\n chunk,\n}: {\n writer?: StreamChunkWriter;\n chunk: unknown;\n}): Promise<void> {\n if (!writer) {\n return;\n }\n\n await writer.write(chunk);\n}\n","import type { ActorSignal } from '../auth/ee';\nimport type { MastraScorers } from '../evals';\nimport type { PubSub } from '../events';\nimport type { Mastra } from '../mastra';\nimport type { ObservabilityContext } from '../observability';\nimport type { RequestContext } from '../request-context';\nimport type { InferStandardSchemaOutput, StandardSchemaWithJSON } from '../schema';\nimport type { ToolStream } from '../tools/stream';\nimport type { DynamicArgument } from '../types';\nimport type { PUBSUB_SYMBOL, STREAM_FORMAT_SYMBOL } from './constants';\nimport type { OutputWriter, StepResult, StepMetadata } from './types';\nimport type { Workflow } from './workflow';\n\nexport type SuspendOptions = {\n resumeLabel?: string | string[];\n} & Record<string, any>;\n\n// Create a unique symbol that only exists at the type level\ndeclare const SuspendBrand: unique symbol;\n\n// Create a branded type that can ONLY be produced by suspend()\nexport type InnerOutput = void & { readonly [SuspendBrand]: never };\n\nexport type ExecuteFunctionParams<\n TState,\n TStepInput,\n TStepOutput,\n TResume,\n TSuspend,\n EngineType,\n TRequestContext extends Record<string, any> | unknown = unknown,\n> = Partial<ObservabilityContext> & {\n runId: string;\n resourceId?: string;\n workflowId: string;\n mastra: Mastra;\n requestContext: RequestContext<TRequestContext>;\n actor?: ActorSignal;\n inputData: TStepInput;\n state: TState;\n setState(state: TState): Promise<void>;\n resumeData?: TResume;\n suspendData?: TSuspend;\n retryCount: number;\n getInitData<T>(): T extends Workflow<any, any, any, any, any, any, any, any>\n ? InferStandardSchemaOutput<T['inputSchema']>\n : T;\n getStepResult<TOutput>(step: string): TOutput;\n getStepResult<TStep extends Step<string, any, any, any, any, any, EngineType>>(\n step: TStep,\n ): InferStandardSchemaOutput<TStep['outputSchema']>;\n suspend: unknown extends TSuspend\n ? (suspendPayload?: TSuspend, suspendOptions?: SuspendOptions) => InnerOutput | Promise<InnerOutput>\n : (suspendPayload: TSuspend, suspendOptions?: SuspendOptions) => InnerOutput | Promise<InnerOutput>;\n bail(result: TStepOutput): InnerOutput;\n bail<T>(\n result: T extends Workflow<any, any, any, any, any, infer TWorkflowOutput, any, any> ? TWorkflowOutput : T,\n ): InnerOutput;\n abort(): void;\n resume?: {\n steps: string[];\n resumePayload: TResume;\n };\n restart?: boolean;\n [PUBSUB_SYMBOL]: PubSub;\n [STREAM_FORMAT_SYMBOL]: 'legacy' | 'vnext' | undefined;\n engine: EngineType;\n abortSignal: AbortSignal;\n writer: ToolStream;\n outputWriter?: OutputWriter;\n validateSchemas?: boolean;\n};\n\nexport type ConditionFunctionParams<\n TState,\n TStepInput,\n TStepOutput,\n TResumeSchema,\n TSuspendSchema,\n EngineType,\n TRequestContext extends Record<string, any> | unknown = unknown,\n> = Omit<\n ExecuteFunctionParams<TState, TStepInput, TStepOutput, TResumeSchema, TSuspendSchema, EngineType, TRequestContext>,\n 'setState' | 'suspend'\n>;\n\nexport type ExecuteFunction<\n TState,\n TStepInput,\n TStepOutput,\n TResumeSchema,\n TSuspendSchema,\n EngineType,\n TRequestContext extends Record<string, any> | unknown = unknown,\n> = (\n params: ExecuteFunctionParams<\n TState,\n TStepInput,\n TStepOutput,\n TResumeSchema,\n TSuspendSchema,\n EngineType,\n TRequestContext\n >,\n) => Promise<TStepOutput | InnerOutput>;\n\nexport type ConditionFunction<\n TState,\n TStepInput,\n TStepOutput,\n TResumeSchema,\n TSuspendSchema,\n EngineType,\n TRequestContext extends Record<string, any> | unknown = unknown,\n> = (\n params: ConditionFunctionParams<\n TState,\n TStepInput,\n TStepOutput,\n TResumeSchema,\n TSuspendSchema,\n EngineType,\n TRequestContext\n >,\n) => Promise<boolean>;\n\nexport type LoopConditionFunction<\n TState,\n TStepInput,\n TStepOutput,\n TResumeSchema,\n TSuspendSchema,\n EngineType,\n TRequestContext extends Record<string, any> | unknown = unknown,\n> = (\n params: ConditionFunctionParams<\n TState,\n TStepInput,\n TStepOutput,\n TResumeSchema,\n TSuspendSchema,\n EngineType,\n TRequestContext\n > & {\n iterationCount: number;\n },\n) => Promise<boolean>;\n\n// Define a Step interface\nexport interface Step<\n TStepId extends string = string,\n TState = unknown,\n TInput = unknown,\n TOutput = unknown,\n TResume = unknown,\n TSuspend = unknown,\n TEngineType = any,\n TRequestContext extends Record<string, any> | unknown = unknown,\n> {\n id: TStepId;\n description?: string;\n inputSchema: StandardSchemaWithJSON<TInput>;\n outputSchema: StandardSchemaWithJSON<TOutput>;\n resumeSchema?: StandardSchemaWithJSON<TResume>;\n suspendSchema?: StandardSchemaWithJSON<TSuspend>;\n stateSchema?: StandardSchemaWithJSON<TState>;\n /**\n * Optional schema for validating request context values.\n * When provided, the request context will be validated against this schema before step execution.\n */\n requestContextSchema?: StandardSchemaWithJSON<TRequestContext>;\n execute: ExecuteFunction<TState, TInput, TOutput, TResume, TSuspend, TEngineType, TRequestContext>;\n scorers?: DynamicArgument<MastraScorers>;\n retries?: number;\n component?: string;\n metadata?: StepMetadata;\n}\n\nexport const getStepResult = (stepResults: Record<string, StepResult<any, any, any, any>>, step: any) => {\n let result;\n\n if (typeof step === 'string') {\n result = stepResults[step];\n } else {\n if (!step?.id) {\n return null;\n }\n\n result = stepResults[step.id];\n }\n\n return result?.status === 'success' ? result.output : null;\n};\n","import { z } from 'zod';\nimport type { Mastra } from '../mastra';\nimport { toStandardSchema } from '../schema';\nimport type { Step } from './step';\nimport type { SingleStepEntry } from './types';\nimport type { Workflow } from './workflow';\n\n/**\n * Accessors for the {@link SingleStepEntry} union.\n *\n * This module is the single place allowed to pattern-match the union's shape.\n * Everything else (both engines, handlers, utils) should go through these\n * helpers so that adding a new variant means changing exactly one file.\n *\n * Union *shape* questions (id, retries, schemas, …) live here; how each\n * declarative kind is *interpreted* at run time lives in `./entry-executors`.\n */\n\n/**\n * The id of a single step-like entry. Plain `step` entries key off the wrapped\n * step's id; declarative variants (agent / tool / mapping) carry their own `id`.\n */\nexport function getEntryId(entry: SingleStepEntry): string {\n return entry.type === 'step' ? entry.step.id : entry.id;\n}\n\n/**\n * The effective retry count for an entry, falling back to the provided\n * workflow-level default when the entry doesn't declare its own.\n *\n * - `step` — the step's own `retries`\n * - `agent` / `tool` — the declarative `options.retries`\n * - `mapping` — never declares retries; always the fallback\n */\nexport function getEntryRetries(entry: SingleStepEntry, fallback?: number): number | undefined {\n switch (entry.type) {\n case 'step':\n return entry.step.retries ?? fallback;\n case 'agent':\n case 'tool':\n return entry.options?.retries ?? fallback;\n case 'mapping':\n return fallback;\n }\n}\n\n/**\n * The `component` discriminator of the entry, if any. Only plain `step`\n * entries can carry one (notably `'WORKFLOW'` for nested workflows);\n * declarative variants have none.\n */\nexport function getEntryComponent(entry: SingleStepEntry): string | undefined {\n return entry.type === 'step' ? (entry.step as { component?: string }).component : undefined;\n}\n\n/**\n * Probes an entry for a nested workflow. Only the `type: 'step'` variant can\n * wrap a live `Workflow` (identified by its `component === 'WORKFLOW'`\n * discriminator from MastraBase); declarative variants never nest one.\n */\nexport function getEntryWorkflow(entry: SingleStepEntry): Workflow | null {\n if (entry.type !== 'step') {\n return null;\n }\n const step = entry.step as unknown as { component?: string };\n if (step && typeof step === 'object' && step.component === 'WORKFLOW') {\n return entry.step as unknown as Workflow;\n }\n return null;\n}\n\n/**\n * The human-readable description of the entry, if any. Declarative variants\n * don't carry a live description (agent descriptions live on the agent itself).\n */\nexport function getEntryDescription(entry: SingleStepEntry): string | undefined {\n return entry.type === 'step' ? entry.step.description : undefined;\n}\n\n/**\n * The validation schemas of an entry, used by the engines to validate step\n * input / suspend / resume data without materializing a live Step.\n *\n * - `step` — the step's own schemas\n * - `agent` — the fixed `{ prompt: string }` input contract (mirrors `createStepFromAgent`)\n * - `tool` — the resolved tool's schemas\n * - `mapping` — none (mappings accept and return anything)\n *\n * Never throws: when a tool can't be resolved the schemas are simply empty and\n * the run path surfaces the actionable not-found error.\n */\nexport function getEntrySchemas(\n entry: SingleStepEntry,\n mastra?: Mastra,\n): Partial<Pick<Step<string, any, any>, 'inputSchema' | 'resumeSchema' | 'suspendSchema'>> {\n switch (entry.type) {\n case 'step':\n return {\n inputSchema: entry.step.inputSchema,\n resumeSchema: entry.step.resumeSchema,\n suspendSchema: entry.step.suspendSchema,\n };\n case 'agent':\n return { inputSchema: toStandardSchema(z.object({ prompt: z.string() })) };\n case 'tool': {\n let tool: { inputSchema?: any; resumeSchema?: any; suspendSchema?: any } | undefined;\n try {\n tool = entry.tool ?? mastra?.getTool(entry.toolId);\n } catch {\n tool = undefined;\n }\n return tool\n ? { inputSchema: tool.inputSchema, resumeSchema: tool.resumeSchema, suspendSchema: tool.suspendSchema }\n : {};\n }\n case 'mapping':\n return {};\n }\n}\n","import type { StandardSchemaV1 } from '@standard-schema/spec';\nimport { ErrorCategory, ErrorDomain, getErrorFromUnknown, MastraError } from '../error';\nimport type { IMastraLogger } from '../logger';\nimport type { RequestContext } from '../request-context';\nimport type { StandardSchemaWithJSON } from '../schema';\nimport { removeUndefinedValues } from '../utils';\nimport type { ExecutionGraph } from './execution-engine';\nimport type { Step } from './step';\nimport { getEntryId } from './step-entry';\nimport type {\n ForeachConcurrencyContext,\n ForeachOptions,\n RestartExecutionParams,\n SingleStepEntry,\n StepFlowEntry,\n StepResult,\n TimeTravelContext,\n TimeTravelExecutionParams,\n WorkflowRunState,\n} from './types';\n\n/**\n * Validates data against a StandardSchema and returns the result.\n * Works with both sync and async schemas.\n */\nasync function validateWithStandardSchema<T>(\n schema: StandardSchemaWithJSON<T>,\n data: unknown,\n): Promise<{ success: true; data: T } | { success: false; issues: { path?: (string | number)[]; message: string }[] }> {\n const result = schema['~standard'].validate(data);\n const resolvedResult = result instanceof Promise ? await result : result;\n\n if ('issues' in resolvedResult && resolvedResult.issues) {\n return {\n success: false,\n issues: resolvedResult.issues.map((issue: StandardSchemaV1.Issue) => ({\n path: issue.path?.map((p: PropertyKey | StandardSchemaV1.PathSegment) =>\n typeof p === 'object' && 'key' in p ? p.key : p,\n ) as (string | number)[] | undefined,\n message: issue.message,\n })),\n };\n }\n\n return { success: true, data: resolvedResult.value as T };\n}\n\nexport async function validateStepInput({\n prevOutput,\n step,\n validateInputs,\n}: {\n prevOutput: any;\n step: Partial<Pick<Step<string, any, any>, 'inputSchema'>>;\n validateInputs: boolean;\n}) {\n let inputData = prevOutput;\n\n let validationError: Error | undefined;\n\n const inputSchema = step.inputSchema;\n if (validateInputs && inputSchema) {\n const validatedInput = await validateWithStandardSchema(inputSchema, prevOutput);\n\n if (!validatedInput.success) {\n const errorMessages = validatedInput.issues.map(e => `- ${e.path?.join('.')}: ${e.message}`).join('\\n');\n validationError = new MastraError(\n {\n id: 'WORKFLOW_STEP_INPUT_VALIDATION_FAILED',\n domain: ErrorDomain.MASTRA_WORKFLOW,\n category: ErrorCategory.USER,\n text: 'Step input validation failed: \\n' + errorMessages,\n },\n { issues: validatedInput.issues },\n );\n } else {\n const isEmptyObject =\n validatedInput.data !== null &&\n typeof validatedInput.data === 'object' &&\n !Array.isArray(validatedInput.data) &&\n Object.keys(validatedInput.data as Record<string, unknown>).length === 0;\n inputData = isEmptyObject ? prevOutput : validatedInput.data;\n }\n }\n\n return { inputData, validationError };\n}\n\nexport async function validateStepResumeData({\n resumeData,\n step,\n}: {\n resumeData?: any;\n step: Partial<Pick<Step<string, any, any>, 'resumeSchema'>>;\n}) {\n if (!resumeData) {\n return { resumeData: undefined, validationError: undefined };\n }\n\n let validationError: Error | undefined;\n\n const resumeSchema = step.resumeSchema;\n\n if (resumeSchema) {\n const validatedResumeData = await validateWithStandardSchema(resumeSchema, resumeData);\n if (!validatedResumeData.success) {\n const errorMessages = validatedResumeData.issues.map(e => `- ${e.path?.join('.')}: ${e.message}`).join('\\n');\n validationError = new MastraError({\n id: 'WORKFLOW_STEP_RESUME_DATA_VALIDATION_FAILED',\n domain: ErrorDomain.MASTRA_WORKFLOW,\n category: ErrorCategory.USER,\n text: 'Step resume data validation failed: \\n' + errorMessages,\n });\n } else {\n resumeData = validatedResumeData.data;\n }\n }\n return { resumeData, validationError };\n}\n\nexport async function validateStepSuspendData({\n suspendData,\n step,\n validateInputs,\n}: {\n suspendData?: any;\n step: Partial<Pick<Step<string, any, any>, 'suspendSchema'>>;\n validateInputs: boolean;\n}) {\n if (!suspendData) {\n return { suspendData: undefined, validationError: undefined };\n }\n\n let validationError: Error | undefined;\n\n const suspendSchema = step.suspendSchema;\n\n if (suspendSchema && validateInputs) {\n const validatedSuspendData = await validateWithStandardSchema(suspendSchema, suspendData);\n if (!validatedSuspendData.success) {\n const errorMessages = validatedSuspendData.issues.map(e => `- ${e.path?.join('.')}: ${e.message}`).join('\\n');\n validationError = new MastraError({\n id: 'WORKFLOW_STEP_SUSPEND_DATA_VALIDATION_FAILED',\n domain: ErrorDomain.MASTRA_WORKFLOW,\n category: ErrorCategory.USER,\n text: 'Step suspend data validation failed: \\n' + errorMessages,\n });\n } else {\n suspendData = validatedSuspendData.data;\n }\n }\n return { suspendData, validationError };\n}\n\nexport async function validateStepStateData({\n stateData,\n step,\n validateInputs,\n}: {\n stateData?: any;\n step: Step<string, any, any>;\n validateInputs: boolean;\n}) {\n if (!stateData) {\n return { stateData: undefined, validationError: undefined };\n }\n\n let validationError: Error | undefined;\n\n const stateSchema = step.stateSchema;\n\n if (stateSchema && validateInputs) {\n const validatedStateData = await validateWithStandardSchema(stateSchema, stateData);\n if (!validatedStateData.success) {\n const errorMessages = validatedStateData.issues.map(e => `- ${e.path?.join('.')}: ${e.message}`).join('\\n');\n validationError = new Error('Step state data validation failed: \\n' + errorMessages);\n } else {\n stateData = validatedStateData.data;\n }\n }\n return { stateData, validationError };\n}\n\nexport async function validateStepRequestContext({\n requestContext,\n step,\n validateInputs,\n}: {\n requestContext?: RequestContext;\n step: Step<string, any, any>;\n validateInputs: boolean;\n}) {\n let validationError: Error | undefined;\n\n const requestContextSchema = step.requestContextSchema;\n\n if (requestContextSchema && validateInputs) {\n // Get all values from requestContext\n const contextValues = requestContext?.all ?? {};\n const validatedRequestContext = await validateWithStandardSchema(requestContextSchema, contextValues);\n if (!validatedRequestContext.success) {\n const errorMessages = validatedRequestContext.issues.map(e => `- ${e.path?.join('.')}: ${e.message}`).join('\\n');\n validationError = new MastraError({\n id: 'WORKFLOW_STEP_REQUEST_CONTEXT_VALIDATION_FAILED',\n domain: ErrorDomain.MASTRA_WORKFLOW,\n category: ErrorCategory.USER,\n text: `Step request context validation failed for step '${step.id}': \\n` + errorMessages,\n });\n }\n }\n return { validationError };\n}\n\nexport function getResumeLabelsByStepId(\n resumeLabels: Record<string, { stepId: string; foreachIndex?: number }>,\n stepId: string,\n) {\n return Object.entries(resumeLabels)\n .filter(([_, value]) => value.stepId === stepId)\n .reduce(\n (acc, [key, value]) => {\n acc[key] = value;\n return acc;\n },\n {} as Record<string, { stepId: string; foreachIndex?: number }>,\n );\n}\n\nexport const runCountDeprecationMessage =\n \"Warning: 'runCount' is deprecated and will be removed on November 4th, 2025. Please use 'retryCount' instead.\";\n\n/**\n * Track which deprecation warnings have been shown globally to avoid spam\n */\nconst shownWarnings = new Set<string>();\n\n/**\n * Creates a Proxy that wraps execute function parameters to show deprecation warnings\n * when accessing deprecated properties.\n *\n * Currently handles:\n * - `runCount`: Deprecated in favor of `retryCount`, will be removed on November 4th, 2025\n */\nexport function createDeprecationProxy<T extends Record<string, any>>(\n params: T,\n {\n paramName,\n deprecationMessage,\n logger,\n }: {\n paramName: string;\n deprecationMessage: string;\n logger: IMastraLogger;\n },\n): T {\n return new Proxy(params, {\n get(target, prop, receiver) {\n if (prop === paramName && !shownWarnings.has(paramName)) {\n shownWarnings.add(paramName);\n if (logger) {\n logger.warn('\\x1b[33m%s\\x1b[0m', deprecationMessage);\n } else {\n console.warn('\\x1b[33m%s\\x1b[0m', deprecationMessage);\n }\n }\n return Reflect.get(target, prop, receiver);\n },\n });\n}\n\nconst SINGLE_STEP_TYPES = ['step', 'agent', 'tool', 'mapping'] as const;\n\n/**\n * Whether an entry is a \"single step-like\" entry: a plain user step or one of the\n * declarative variants (agent / tool / mapping) that resolve to exactly one step.\n */\nexport function isSingleStepEntry(entry: StepFlowEntry): entry is SingleStepEntry {\n return (SINGLE_STEP_TYPES as readonly string[]).includes(entry.type);\n}\n\n/**\n * The id of a single step-like entry. Plain `step` entries key off the wrapped\n * step's id; declarative variants (agent / tool / mapping) carry their own `id`.\n *\n * Public alias of {@link getEntryId} from `./step-entry`.\n */\nexport const getSingleStepEntryId = getEntryId;\n\nexport const getStepIds = (entry: StepFlowEntry): string[] => {\n if (isSingleStepEntry(entry)) {\n return [getSingleStepEntryId(entry)];\n }\n if (entry.type === 'foreach' || entry.type === 'loop') {\n return [getSingleStepEntryId(entry.step)];\n }\n if (entry.type === 'parallel' || entry.type === 'conditional') {\n return entry.steps.map(s => getSingleStepEntryId(s));\n }\n if (entry.type === 'sleep' || entry.type === 'sleepUntil') {\n return [entry.id];\n }\n return [];\n};\n\nexport const createTimeTravelExecutionParams = (params: {\n steps: string[];\n inputData?: any;\n resumeData?: any;\n context?: TimeTravelContext<any, any, any, any>;\n nestedStepsContext?: Record<string, TimeTravelContext<any, any, any, any>>;\n snapshot: WorkflowRunState;\n initialState?: any;\n graph: ExecutionGraph;\n perStep?: boolean;\n}) => {\n const { steps, inputData, resumeData, context, nestedStepsContext, snapshot, initialState, graph, perStep } = params;\n const firstStepId = steps[0]!;\n\n let executionPath: number[] = [];\n const stepResults: Record<string, StepResult<any, any, any, any>> = {};\n const snapshotContext = snapshot.context as Record<string, any>;\n\n for (const [index, entry] of graph.steps.entries()) {\n const currentExecPathLength = executionPath.length;\n //if there is resumeData, steps down the graph until the suspended step will have stepResult info to use\n if (currentExecPathLength > 0 && !resumeData) {\n break;\n }\n const stepIds = getStepIds(entry);\n const isTargetEntry = stepIds.includes(firstStepId);\n if (isTargetEntry) {\n const innerExecutionPath = stepIds?.length > 1 ? [stepIds?.findIndex(s => s === firstStepId)] : [];\n //parallel and loop steps will have more than one step id,\n // and if the step is one of those, we need the index for the execution path\n executionPath = [index, ...innerExecutionPath];\n }\n\n const prevStep = graph.steps[index - 1]!;\n let stepPayload = undefined;\n if (prevStep) {\n const prevStepIds = getStepIds(prevStep);\n if (prevStepIds.length > 0) {\n if (prevStepIds.length === 1) {\n stepPayload = (stepResults?.[prevStepIds[0]!] as any)?.output ?? {};\n } else {\n stepPayload = prevStepIds.reduce(\n (acc, stepId) => {\n acc[stepId] = (stepResults?.[stepId] as any)?.output ?? {};\n return acc;\n },\n {} as Record<string, any>,\n );\n }\n }\n }\n\n //the stepResult input is basically the payload of the first step\n if (index === 0 && stepIds.includes(firstStepId)) {\n stepResults.input = (context?.[firstStepId]?.payload ?? inputData ?? snapshotContext?.input) as any;\n } else if (index === 0) {\n stepResults.input =\n stepIds?.reduce((acc, stepId) => {\n if (acc) return acc;\n return context?.[stepId]?.payload ?? snapshotContext?.[stepId]?.payload;\n }, null) ??\n snapshotContext?.input ??\n {};\n }\n\n let stepOutput = undefined;\n const nextStep = graph.steps[index + 1]!;\n if (nextStep) {\n const nextStepIds = getStepIds(nextStep);\n if (\n nextStepIds.length > 0 &&\n inputData &&\n nextStepIds.includes(firstStepId) &&\n steps.length === 1 //steps being greater than 1 means it's travelling to step in a nested workflow\n //if it's a nested wokrflow step, the step being resumed in the nested workflow might not be the first step in it,\n // making the inputData the output here wrong\n ) {\n stepOutput = inputData;\n }\n }\n\n stepIds.forEach(stepId => {\n let result;\n const stepContext = context?.[stepId] ?? snapshotContext[stepId];\n // Siblings of the time-travel target inside a conditional were not selected by the\n // branch's condition, so they should be reported as skipped rather than as a fake\n // success (otherwise their empty output leaks into the conditional's aggregated result).\n const isUnselectedConditionalSibling = isTargetEntry && entry.type === 'conditional' && !steps?.includes(stepId);\n const defaultStepStatus = steps?.includes(stepId)\n ? 'running'\n : isUnselectedConditionalSibling\n ? 'skipped'\n : 'success';\n const status = ['failed', 'canceled'].includes(stepContext?.status)\n ? defaultStepStatus\n : (stepContext?.status ?? defaultStepStatus);\n const isCompleteStatus = ['success', 'failed', 'canceled'].includes(status);\n result = {\n status,\n payload: context?.[stepId]?.payload ?? stepPayload ?? snapshotContext[stepId]?.payload ?? {},\n output: isCompleteStatus\n ? (context?.[stepId]?.output ?? stepOutput ?? snapshotContext[stepId]?.output ?? {})\n : undefined,\n resumePayload: stepContext?.resumePayload,\n suspendPayload: stepContext?.suspendPayload,\n suspendOutput: stepContext?.suspendOutput,\n startedAt: stepContext?.startedAt ?? Date.now(),\n endedAt: isCompleteStatus ? (stepContext?.endedAt ?? Date.now()) : undefined,\n suspendedAt: stepContext?.suspendedAt,\n resumedAt: stepContext?.resumedAt,\n };\n const execPathLengthToUse = perStep ? executionPath.length : currentExecPathLength;\n if (\n execPathLengthToUse > 0 &&\n !steps?.includes(stepId) &&\n !context?.[stepId] &&\n (!snapshotContext[stepId] || (snapshotContext[stepId] && snapshotContext[stepId].status !== 'suspended'))\n ) {\n // if the step is after the timeTravelled step in the graph\n // and it doesn't exist in the snapshot,\n // OR it exists in snapshot and is not suspended,\n // we don't need to set stepResult for it\n // if perStep is true, and the step is a parallel step,\n // we want to construct result for only the timetraveled step and any step context is passed for\n result = undefined;\n }\n if (result) {\n const formattedResult = removeUndefinedValues(result);\n stepResults[stepId] = formattedResult as any;\n }\n });\n }\n\n if (!executionPath.length) {\n throw new Error(\n `Time travel target step not found in execution graph: '${steps?.join('.')}'. Verify the step id/path.`,\n );\n }\n\n const timeTravelData: TimeTravelExecutionParams = {\n inputData,\n executionPath,\n steps,\n stepResults,\n nestedStepResults: nestedStepsContext as any,\n state: initialState ?? snapshot.value ?? {},\n resumeData,\n stepExecutionPath: snapshot?.stepExecutionPath,\n };\n\n return timeTravelData;\n};\n\nexport const createRestartExecutionParams = ({\n snapshot,\n graph,\n}: {\n snapshot: WorkflowRunState;\n graph: ExecutionGraph;\n}) => {\n let nestedWorkflowPending = false;\n\n if (snapshot.status !== 'running' && snapshot.status !== 'waiting') {\n const hasPendingInput =\n snapshot.status === 'pending' &&\n snapshot.context &&\n Object.prototype.hasOwnProperty.call(snapshot.context, 'input');\n if (hasPendingInput) {\n //possible the server died just before the nested workflow execution started.\n //only nested workflows have input data in context when it's still pending\n nestedWorkflowPending = true;\n } else {\n throw new Error('This workflow run was not active');\n }\n }\n\n let nestedWorkflowActiveStepsPath: Record<string, number[]> = {};\n\n const firstEntry = graph.steps[0]!;\n\n if (isSingleStepEntry(firstEntry)) {\n nestedWorkflowActiveStepsPath = {\n [getSingleStepEntryId(firstEntry)]: [0],\n };\n } else if (firstEntry.type === 'foreach' || firstEntry.type === 'loop') {\n nestedWorkflowActiveStepsPath = {\n [getSingleStepEntryId(firstEntry.step)]: [0],\n };\n } else if (firstEntry.type === 'sleep' || firstEntry.type === 'sleepUntil') {\n nestedWorkflowActiveStepsPath = {\n [firstEntry.id]: [0],\n };\n } else if (firstEntry.type === 'conditional' || firstEntry.type === 'parallel') {\n nestedWorkflowActiveStepsPath = firstEntry.steps.reduce(\n (acc, step) => {\n acc[getSingleStepEntryId(step)] = [0];\n return acc;\n },\n {} as Record<string, number[]>,\n );\n }\n const restartData: RestartExecutionParams = {\n activePaths: nestedWorkflowPending ? [0] : snapshot.activePaths,\n activeStepsPath: nestedWorkflowPending ? nestedWorkflowActiveStepsPath : snapshot.activeStepsPath,\n stepResults: snapshot.context,\n state: snapshot.value,\n stepExecutionPath: snapshot?.stepExecutionPath,\n };\n\n return restartData;\n};\n\n/**\n * Re-hydrates serialized errors in step results back into proper Error instances.\n * This is useful when errors have been serialized through an event system (e.g., evented engine, Inngest)\n * and need to be converted back to Error instances with their custom properties preserved.\n *\n * @param steps - The workflow step results (context) that may contain serialized errors\n * @returns The same steps object with errors hydrated as Error instances\n */\nexport function hydrateSerializedStepErrors(steps: WorkflowRunState['context']) {\n if (steps) {\n for (const step of Object.values(steps)) {\n if (step.status === 'failed' && 'error' in step && step.error) {\n step.error = getErrorFromUnknown(step.error, { serializeStack: false });\n }\n }\n }\n return steps;\n}\n\n/**\n * Cleans a single step result object by removing internal properties.\n * This is a helper for cleanStepResult that handles one level of cleaning.\n */\nfunction cleanSingleResult(result: Record<string, unknown>): Record<string, unknown> {\n const { __state: _state, metadata, ...rest } = result;\n\n // Strip nestedRunId from metadata but keep other user-defined fields\n if (metadata && typeof metadata === 'object' && !Array.isArray(metadata)) {\n const { nestedRunId: _nestedRunId, ...userMetadata } = metadata as Record<string, unknown>;\n if (Object.keys(userMetadata).length > 0) {\n return { ...rest, metadata: userMetadata };\n }\n }\n\n return rest;\n}\n\n/**\n * Cleans step result data by removing internal properties at known structural levels.\n *\n * Removes:\n * - `__state` properties (internal workflow state for state propagation)\n * - `nestedRunId` from `metadata` objects (internal tracking for nested workflow retrieval)\n *\n * ## Why targeted cleaning instead of recursive?\n *\n * Internal properties only appear at specific, known locations:\n *\n * 1. **`__state`** - Added by step-executor.ts to every step result. For forEach,\n * suspended iterations store the full result (including __state) while completed\n * iterations only store the output value. See workflow-event-processor/index.ts:1227-1230.\n *\n * 2. **`metadata.nestedRunId`** - Added when nested workflows complete, stored at the\n * step result level. For forEach with nested workflows, each iteration result can\n * have this. See workflow-event-processor/index.ts:1449-1453.\n *\n * By only cleaning at the step result level and forEach iteration level, we avoid\n * accidentally stripping user data that happens to use `__state` as a property name\n * in their actual output values.\n *\n * @param stepResult - A step result object, or an array of iteration results (forEach)\n * @returns The cleaned step result with internal properties removed\n */\nexport function cleanStepResult(stepResult: unknown): unknown {\n if (stepResult === null || stepResult === undefined) {\n return stepResult;\n }\n\n if (typeof stepResult !== 'object') {\n return stepResult;\n }\n\n // Handle arrays (forEach iteration results) - clean each element at the result level only\n if (Array.isArray(stepResult)) {\n return stepResult.map(item => {\n if (item && typeof item === 'object' && !Array.isArray(item)) {\n return cleanSingleResult(item as Record<string, unknown>);\n }\n return item;\n });\n }\n\n const result = stepResult as Record<string, unknown>;\n const cleaned = cleanSingleResult(result);\n\n // If output is an array (forEach results), clean each iteration result\n // Iteration results can have __state (for suspended) or metadata.nestedRunId (for nested workflows)\n if (Array.isArray(cleaned.output)) {\n cleaned.output = cleaned.output.map((item: unknown) => {\n if (item && typeof item === 'object' && !Array.isArray(item)) {\n return cleanSingleResult(item as Record<string, unknown>);\n }\n return item;\n });\n }\n\n return cleaned;\n}\n\n/**\n * Resolves the effective concurrency for a foreach entry at execution time.\n *\n * Supports both a static number and a {@link ForeachConcurrencyResolver}\n * function that derives concurrency from the run's input. Invalid or\n * non-positive values fall back to 1 (sequential).\n */\nexport function resolveForeachConcurrency(\n opts: ForeachOptions | undefined,\n context: ForeachConcurrencyContext,\n): number {\n const configured = opts?.concurrency ?? 1;\n const resolved = typeof configured === 'function' ? configured(context) : configured;\n if (typeof resolved !== 'number' || !Number.isFinite(resolved) || resolved < 1) {\n return 1;\n }\n return Math.floor(resolved);\n}\n\nconst RESUME_SNAPSHOT_POLL_INTERVAL_MS = 25;\nconst RESUME_SNAPSHOT_POLL_TIMEOUT_MS = 2000;\n\nexport async function waitForSuspendedSnapshot(\n workflowsStore:\n | { loadWorkflowSnapshot: (args: { workflowName: string; runId: string }) => Promise<WorkflowRunState | null> }\n | undefined,\n workflowName: string,\n runId: string,\n): Promise<WorkflowRunState | null> {\n if (!workflowsStore) return null;\n\n const deadline = Date.now() + RESUME_SNAPSHOT_POLL_TIMEOUT_MS;\n let snapshot = (await workflowsStore.loadWorkflowSnapshot({ workflowName, runId })) ?? null;\n while ((!snapshot || snapshot.status !== 'suspended') && Date.now() < deadline) {\n await new Promise(resolve => setTimeout(resolve, RESUME_SNAPSHOT_POLL_INTERVAL_MS));\n snapshot = (await workflowsStore.loadWorkflowSnapshot({ workflowName, runId })) ?? null;\n }\n return snapshot;\n}\n","import type { ReadableStream } from 'node:stream/web';\nimport { TripWire } from '../../agent/trip-wire';\nimport type { PubSub } from '../../events';\nimport type { Mastra } from '../../mastra';\nimport { resolveObservabilityContext } from '../../observability';\nimport type { ChunkType } from '../../stream/types';\nimport { PUBSUB_SYMBOL, STREAM_FORMAT_SYMBOL } from '../constants';\nimport { forwardAgentStreamChunk } from '../stream-utils';\nimport type { AgentStepEntry } from '../types';\nimport type { EntryExecuteContext } from './types';\n\n/**\n * Runs a declarative `agent` entry: resolves the agent (inline handle, else the\n * Mastra registry), streams the prompt through it, forwards stream chunks, and\n * returns either the structured output or `{ text }`.\n *\n * `ctx` is the step execute context (the same object a plain step's `execute`\n * receives). `mastra` defaults to `ctx.mastra` when omitted.\n */\nexport async function runAgentEntry(\n entry: AgentStepEntry,\n ctx: EntryExecuteContext,\n mastra?: Mastra,\n): Promise<unknown> {\n const registry = mastra ?? (ctx?.mastra as Mastra | undefined);\n const agent = entry.agent ?? registry?.getAgentById(entry.agentId);\n if (!agent) {\n throw new Error(\n `Agent '${entry.agentId}' not found for workflow step '${entry.id}'. Register the agent on the Mastra instance or pass the agent instance directly.`,\n );\n }\n\n // `retries` / `scorers` / `metadata` are step-level concerns handled by the\n // engine (see getEntryRetries); everything else is passed to the agent run.\n const { retries: _retries, scorers: _scorers, metadata: _metadata, ...agentOptions } = (entry.options ?? {}) as any;\n\n const {\n inputData,\n runId,\n [PUBSUB_SYMBOL]: pubsub,\n [STREAM_FORMAT_SYMBOL]: streamFormat,\n requestContext,\n abortSignal,\n abort,\n writer,\n ...rest\n } = ctx;\n const observabilityContext = resolveObservabilityContext(rest);\n let streamPromise = {} as {\n promise: Promise<string>;\n resolve: (value: string) => void;\n reject: (reason?: any) => void;\n };\n\n streamPromise.promise = new Promise((resolve, reject) => {\n streamPromise.resolve = resolve;\n streamPromise.reject = reject;\n });\n // The promise is awaited later (and sometimes not at all when structured\n // output short-circuits); attach a no-op handler so an early rejection\n // can't surface as an unhandled rejection before the await.\n streamPromise.promise.catch(() => {});\n\n // Track structured output result\n let structuredResult: any = null;\n\n const toolData = {\n name: agent.name,\n args: inputData,\n };\n\n let stream: ReadableStream<any>;\n\n const handleFinish = (result: any) => {\n const resultWithObject = result as typeof result & { object?: unknown };\n if (agentOptions?.structuredOutput?.schema && resultWithObject.object) {\n structuredResult = resultWithObject.object;\n }\n streamPromise.resolve(result.text);\n void agentOptions?.onFinish?.(result);\n };\n\n if (\n (await agent.getModel({ requestContext })).specificationVersion === 'v1' &&\n typeof agent.streamLegacy === 'function'\n ) {\n const { fullStream } = await agent.streamLegacy((inputData as { prompt: string }).prompt, {\n ...agentOptions,\n requestContext,\n ...observabilityContext,\n onFinish: handleFinish,\n abortSignal,\n });\n stream = fullStream as any;\n } else {\n const modelOutput = await agent.stream((inputData as { prompt: string }).prompt, {\n ...agentOptions,\n requestContext,\n ...observabilityContext,\n onFinish: handleFinish,\n abortSignal,\n });\n\n // handleFinish (the agent's onFinish) is the sole source of truth for the\n // final text — the success side of .text is intentionally a no-op.\n // `modelOutput.text` can resolve with '' if a downstream output-processor\n // throws inside the base output's try/catch (see output.ts:970-973,978-981)\n // and it fires BEFORE handleFinish, so racing here would poison\n // streamPromise. Only the rejection channel below is wired up so genuine\n // stream errors still propagate.\n void modelOutput.text.then(\n () => {},\n (err: unknown) => streamPromise.reject(err),\n );\n stream = modelOutput.fullStream as ReadableStream<ChunkType>;\n }\n\n const tripwireChunk =\n streamFormat === 'legacy'\n ? await bridgeLegacyWatchEvents({ stream, pubsub, runId, toolData })\n : await consumeStreamForTripwire(stream, writer);\n\n // If a tripwire was detected, throw TripWire to abort the workflow step\n if (tripwireChunk) {\n throw new TripWire(\n tripwireChunk.payload?.reason || 'Agent tripwire triggered',\n {\n retry: tripwireChunk.payload?.retry,\n metadata: tripwireChunk.payload?.metadata,\n },\n tripwireChunk.payload?.processorId,\n );\n }\n\n if (abortSignal.aborted) {\n return abort();\n }\n\n // Return structured output if available, otherwise default text\n if (structuredResult !== null) {\n return structuredResult;\n }\n return {\n text: await streamPromise.promise,\n };\n}\n\n/**\n * Legacy-format watch-event bridge: instead of forwarding chunks to the step\n * writer, mirrors the agent stream onto the run's pubsub watch channel as\n * `tool-call-streaming-*` / `tool-call-delta` events (the shape v1 watchers\n * expect). Returns the tripwire chunk if one was seen, else `null`.\n */\nasync function bridgeLegacyWatchEvents({\n stream,\n pubsub,\n runId,\n toolData,\n}: {\n stream: ReadableStream<any>;\n pubsub: PubSub;\n runId: string;\n toolData: { name: string; args: unknown };\n}): Promise<any> {\n let tripwireChunk: any = null;\n await pubsub.publish(`workflow.events.v2.${runId}`, {\n type: 'watch',\n runId,\n data: { type: 'tool-call-streaming-start', ...(toolData ?? {}) },\n });\n try {\n for await (const chunk of stream) {\n if (chunk.type === 'tripwire') {\n tripwireChunk = chunk;\n break;\n }\n if (chunk.type === 'text-delta') {\n await pubsub.publish(`workflow.events.v2.${runId}`, {\n type: 'watch',\n runId,\n data: { type: 'tool-call-delta', ...(toolData ?? {}), argsTextDelta: chunk.textDelta },\n });\n }\n }\n } finally {\n // Watchers pair streaming-start with streaming-finish; publish it even\n // when iteration throws or breaks early so they never hang open. Swallow\n // publish failures here so they can't mask the original error.\n await pubsub\n .publish(`workflow.events.v2.${runId}`, {\n type: 'watch',\n runId,\n data: { type: 'tool-call-streaming-finish', ...(toolData ?? {}) },\n })\n .catch(() => {});\n }\n return tripwireChunk;\n}\n\n/**\n * Forwards every chunk to the step writer, stopping early when a tripwire\n * chunk appears. Returns the tripwire chunk if one was seen, else `null`.\n */\nasync function consumeStreamForTripwire(\n stream: ReadableStream<any>,\n writer: EntryExecuteContext['writer'],\n): Promise<any> {\n for await (const chunk of stream) {\n await forwardAgentStreamChunk({ writer, chunk });\n if (chunk.type === 'tripwire') {\n return chunk;\n }\n }\n return null;\n}\n","import type { Mastra } from '../../mastra';\nimport { resolveObservabilityContext } from '../../observability';\nimport type { ToolStepEntry } from '../types';\nimport type { EntryExecuteContext } from './types';\n\n/**\n * Runs a declarative `tool` entry: resolves the tool (inline handle, else the\n * Mastra registry) and executes it with the step context mapped into the tool\n * execution context.\n */\nexport async function runToolEntry(entry: ToolStepEntry, ctx: EntryExecuteContext, mastra?: Mastra): Promise<unknown> {\n const registry = mastra ?? (ctx?.mastra as Mastra | undefined);\n const tool = entry.tool ?? registry?.getTool(entry.toolId);\n if (!tool) {\n throw new Error(\n `Tool '${entry.toolId}' not found for workflow step '${entry.id}'. Pass the tool instance directly.`,\n );\n }\n\n const {\n inputData,\n mastra: ctxMastra,\n requestContext,\n suspend,\n resumeData,\n runId,\n workflowId,\n state,\n setState,\n abortSignal,\n ...rest\n } = ctx;\n const observabilityContext = resolveObservabilityContext(rest);\n const toolContext = {\n mastra: ctxMastra,\n requestContext,\n ...observabilityContext,\n abortSignal,\n resumeData,\n workflow: {\n runId,\n suspend,\n resumeData,\n workflowId,\n state,\n setState,\n },\n };\n\n return tool.execute(inputData, toolContext);\n}\n","/**\n * The `${scope.path}` mapping-template DSL used by `.map()` template sources.\n *\n * Definition-time syntax checks live in {@link validateTemplate}; run-time\n * resolution (path lookup + value coercion) lives in {@link resolveTemplate}.\n * This module has no knowledge of the step-entry union — it is a pure\n * string-DSL interpreter over a step's execute context.\n */\n\n/** Walks a dotted path on an object. `''` or `'.'` returns the root unchanged. */\nexport function traverseMappingPath(root: unknown, path: string, errorLabel: string): unknown {\n if (path === '' || path === '.') return root;\n const parts = path.split('.');\n let value: any = root;\n for (const part of parts) {\n if (typeof value === 'object' && value !== null) {\n value = value[part];\n } else {\n throw new Error(`Invalid path ${path} in ${errorLabel}`);\n }\n }\n return value;\n}\n\nconst TEMPLATE_PLACEHOLDER = /\\$\\{([^}]*)\\}/g;\n\nconst TEMPLATE_NAMESPACES = ['inputData', 'initData', 'state', 'requestContext', 'stepResults'] as const;\ntype TemplateScope = (typeof TEMPLATE_NAMESPACES)[number];\n\n/** Common error-message prefix so every template diagnostic points at the exact placeholder. */\nfunction describeBadPlaceholder(template: string, idx: number, rawExpr: string): string {\n return `Template placeholder #${idx} (\\${${rawExpr}}) in '${template}'`;\n}\n\n/** Split a placeholder body `scope.path.with.dots` into its leading scope and the dotted remainder. */\nfunction parseTemplatePlaceholder(rawExpr: string): { scope: string; rest: string } {\n const dot = rawExpr.indexOf('.');\n return {\n scope: dot === -1 ? rawExpr : rawExpr.slice(0, dot),\n rest: dot === -1 ? '' : rawExpr.slice(dot + 1),\n };\n}\n\n/**\n * Validates a `{ template }` source's syntax at workflow-definition time.\n * Throws if any placeholder is empty, whitespace-padded, references an unknown\n * namespace, or is a malformed `stepResults.<stepId>` / `stepResults.<stepId>.<path>` shape.\n *\n * Run-time concerns (does the step actually exist, does the path resolve, is\n * the value a primitive) stay in {@link resolveTemplate}.\n */\nexport function validateTemplate(template: string): void {\n let idx = 0;\n for (const match of template.matchAll(TEMPLATE_PLACEHOLDER)) {\n idx++;\n const rawExpr = match[1] ?? '';\n if (rawExpr.length === 0 || rawExpr !== rawExpr.trim()) {\n throw new Error(\n `${describeBadPlaceholder(template, idx, rawExpr)} has empty or whitespace-padded contents. ` +\n `Use \\${<scope>.<path>} with no surrounding whitespace.`,\n );\n }\n const { scope, rest } = parseTemplatePlaceholder(rawExpr);\n if (scope === 'stepResults') {\n const innerDot = rest.indexOf('.');\n const stepId = innerDot === -1 ? rest : rest.slice(0, innerDot);\n if (!stepId) {\n throw new Error(\n `${describeBadPlaceholder(template, idx, rawExpr)} must be of the form \\${stepResults.<stepId>} or \\${stepResults.<stepId>.<path>}.`,\n );\n }\n continue;\n }\n if (scope === 'requestContext') {\n if (!rest) {\n throw new Error(\n `${describeBadPlaceholder(template, idx, rawExpr)} requires a request-context key — use \\${requestContext.<key>}.`,\n );\n }\n continue;\n }\n if ((TEMPLATE_NAMESPACES as readonly string[]).includes(scope)) continue;\n throw new Error(\n `${describeBadPlaceholder(template, idx, rawExpr)} references unknown namespace \"${scope}\". ` +\n `Use one of: ${TEMPLATE_NAMESPACES.join(', ')}.`,\n );\n }\n}\n\n/**\n * Collects the step ids referenced by `${stepResults.<stepId>}` /\n * `${stepResults.<stepId>.<path>}` placeholders in a template. Assumes the\n * template already passed {@link validateTemplate}; malformed placeholders are\n * skipped. Used by validation to scope-check template references against the\n * preceding workflow-local steps.\n */\nexport function collectTemplateStepIds(template: string): string[] {\n const ids: string[] = [];\n for (const match of template.matchAll(TEMPLATE_PLACEHOLDER)) {\n const { scope, rest } = parseTemplatePlaceholder(match[1] ?? '');\n if (scope !== 'stepResults') continue;\n const innerDot = rest.indexOf('.');\n const stepId = innerDot === -1 ? rest : rest.slice(0, innerDot);\n if (stepId) ids.push(stepId);\n }\n return ids;\n}\n\n/**\n * Coerces a resolved placeholder value to a string. Primitives are stringified\n * the normal way; objects and arrays are JSON-encoded so downstream agents can\n * consume complex step outputs (e.g. `foreach(agent)` returns `{ text }[]`)\n * directly in a template. `null`/`undefined` render as empty. If JSON encoding\n * fails (circular references, BigInt, etc.), throws with a hint pointing at\n * the offending placeholder.\n */\nfunction stringifyTemplateValue(v: unknown, template: string, idx: number, rawExpr: string): string {\n if (v === null || v === undefined) return '';\n if (typeof v === 'object') {\n try {\n return JSON.stringify(v);\n } catch (err) {\n throw new Error(\n `${describeBadPlaceholder(template, idx, rawExpr)} resolved to a value that could not be JSON-stringified ` +\n `(${(err as Error).message}). Drill into a primitive path (e.g. \\${${rawExpr}.someField}) or reshape the value in a precedi