import { flattenStaticPipeline, type PlayStaticPipeline, } from '../plays/static-pipeline'; import type { PlayCallExecution } from './play-call-execution'; /** * `ctx.runPlay` is function composition. It has exactly one execution * placement: inline in the caller's run. Do not add a scheduler fallback * here: children that need their own run lifecycle must be top-level plays. */ export type ChildExecutionStrategy = 'inline'; export type ChildExecutionDecision = { strategy: ChildExecutionStrategy; reason: 'scalar_child' | 'explicit_inline'; }; export type CtxRunPlayInlineOnlyReason = | 'missing_static_contract' | 'dataset_child' | 'event_wait_child' | 'suspending_child' | 'explicit_timeout' | 'explicit_child_workflow'; /** Stable machine-readable prefix for every rejected ctx.runPlay shape. */ export const CTX_RUN_PLAY_INLINE_ONLY = 'CTX_RUN_PLAY_INLINE_ONLY'; /** * One public error for every unsupported child contract. The checker and * runtime deliberately call this same helper so a dynamic child fails with * the same actionable error as a statically known child. */ export function ctxRunPlayInlineOnlyMessage( childPlayName: string, reason: CtxRunPlayInlineOnlyReason, ): string { const contractPath = reason === 'explicit_timeout' ? 'ctx.runPlay.options.timeoutMs' : reason === 'explicit_child_workflow' ? 'ctx.runPlay.options.execution' : 'ctx.runPlay'; const detail: Record = { missing_static_contract: 'its static contract could not be resolved', dataset_child: 'it uses ctx.dataset(), ctx.csv(), or a Runtime Sheet', event_wait_child: 'it waits for an event', suspending_child: 'it attempted to suspend the parent execution', explicit_timeout: 'it sets an explicit timeout', explicit_child_workflow: 'it requests execution: "child-workflow"', }; const nextStep: Record = { dataset_child: 'Move the per-record enrichment into a scalar core play, call that core with ctx.runPlay(), and keep ctx.dataset()/ctx.csv() in the router. If this child must process a batch, trigger it as a top-level play and let it own the follow-up actions.', event_wait_child: 'Trigger it as a top-level play. Event waits need their own run lifecycle and cannot be awaited through ctx.runPlay().', suspending_child: 'Trigger it as a top-level play. Work that can suspend needs its own run lifecycle and cannot be awaited through ctx.runPlay().', missing_static_contract: 'Use a published play with a resolvable static contract, or move lifecycle-owning work into a top-level play.', explicit_timeout: 'Remove the child timeout if the child is a scalar lookup, or trigger the child as a top-level play when it needs independent timing.', explicit_child_workflow: 'Remove execution: "child-workflow" for a scalar lookup, or trigger the child as a top-level play when it needs its own lifecycle.', }; return ( `${CTX_RUN_PLAY_INLINE_ONLY}: [play_authoring_run_play_option_invalid path=${contractPath}] ctx.runPlay("${childPlayName}") can only call a scalar, per-record play; ` + `${detail[reason]}. ${nextStep[reason]}` ); } /** * Resolve an authored child contract. Any shape that would need a second run, * durable scheduler state, or a suspension is rejected before invocation. */ export function resolveChildExecutionStrategy(input: { pipeline: PlayStaticPipeline | null | undefined; timeoutMs?: number | null; hasExplicitTimeout?: boolean; /** `child-workflow` remains accepted as parsed legacy source solely to emit * the canonical migration error rather than silently scheduling it. */ execution?: PlayCallExecution | 'child-workflow' | null; childPlayName?: string | null; }): ChildExecutionDecision { const childPlayName = input.childPlayName?.trim() || 'child'; if (input.execution === 'child-workflow') { throw new Error( ctxRunPlayInlineOnlyMessage(childPlayName, 'explicit_child_workflow'), ); } if (!input.pipeline) { throw new Error( ctxRunPlayInlineOnlyMessage(childPlayName, 'missing_static_contract'), ); } if (input.hasExplicitTimeout === true || input.timeoutMs != null) { throw new Error( ctxRunPlayInlineOnlyMessage(childPlayName, 'explicit_timeout'), ); } const steps = flattenStaticPipeline(input.pipeline); if ( input.pipeline.tableNamespace || input.pipeline.sheetContract || steps.some((step) => step.type === 'dataset' || step.type === 'csv') ) { throw new Error( ctxRunPlayInlineOnlyMessage(childPlayName, 'dataset_child'), ); } if ( steps.some( (step) => step.type === 'tool' && (step.isEventWait || step.toolId === 'test_wait_for_event'), ) ) { throw new Error( ctxRunPlayInlineOnlyMessage(childPlayName, 'event_wait_child'), ); } return { strategy: 'inline', reason: input.execution === 'inline' ? 'explicit_inline' : 'scalar_child', }; }