import { WORKFLOW_ENGINE, normalizeRunSpec, } from './contracts.mjs'; const EXECUTION_REQUEST_VERSION = 'workflow-execution-request-v1'; const EXECUTION_DECISION_VERSION = 'workflow-execution-decision-v1'; const MAX_IDEMPOTENCY_KEY_CHARACTERS = 200; function clampTimeoutMs(value, fallback = 30_000) { const number = Number(value); if (!Number.isFinite(number)) return fallback; return Math.min(60_000, Math.max(500, Math.floor(number))); } function normalizeIdempotencyKey(value) { return String(value ?? '').trim().slice(0, MAX_IDEMPOTENCY_KEY_CHARACTERS); } export function normalizeWorkflowExecutionRequest(input = {}) { const source = input && typeof input === 'object' && !Array.isArray(input) ? input : {}; const spec = normalizeRunSpec(source.spec ?? source.runSpec ?? source); return { version: EXECUTION_REQUEST_VERSION, spec, idempotencyKey: normalizeIdempotencyKey(source.idempotencyKey), authorization: { executionAllowed: source.authorization?.executionAllowed === true, actorId: String(source.authorization?.actorId ?? '').trim() || null, }, controls: { timeoutMs: clampTimeoutMs(source.controls?.timeoutMs), fallbackAllowed: source.controls?.fallbackAllowed !== false, cancellationAllowed: source.controls?.cancellationAllowed !== false, }, }; } function disabledHandoffError(decision) { const error = new Error( `Workflow execution handoff is disabled; ${decision.effectiveEngine} remains the executor`, ); error.code = 'WORKFLOW_EXECUTION_HANDOFF_DISABLED'; error.status = 409; error.decision = decision; return error; } /** * Framework-neutral control-plane adapter. Routing, authorization and the * environment/admin gates are evaluated before a remote engine can be invoked. */ export function createWorkflowExecutionAdapter({ configService, engineRegistry, } = {}) { if (!configService?.selectEngine) { throw new Error('Workflow execution adapter requires orchestrator config service'); } if (!engineRegistry?.has) { throw new Error('Workflow execution adapter requires workflow engine registry'); } async function plan(input) { const request = normalizeWorkflowExecutionRequest(input); const selection = await configService.selectEngine({ runId: request.spec.runId, requestId: request.spec.requestId, userId: request.spec.subject.userId, workflowName: request.spec.workflow.name, }); const candidateEngine = selection.candidateEngine ?? selection.engine ?? WORKFLOW_ENGINE.NATIVE; const nonNativeCandidate = candidateEngine !== WORKFLOW_ENGINE.NATIVE; const selectedEngine = selection.engine ?? WORKFLOW_ENGINE.NATIVE; const nonNativeSelected = selectedEngine !== WORKFLOW_ENGINE.NATIVE; const gates = { implementation: true, routingSelected: nonNativeSelected, engineRegistered: engineRegistry.has(selectedEngine), idempotencyKeyPresent: Boolean(request.idempotencyKey), executionAuthorized: request.authorization.executionAllowed, killSwitchOpen: selection.reason !== 'kill_switch', }; const handoffAllowed = nonNativeSelected && Object.values(gates).every(Boolean); return { version: EXECUTION_DECISION_VERSION, runId: request.spec.runId, requestId: request.spec.requestId, mode: selection.mode, candidateEngine, effectiveEngine: handoffAllowed ? selectedEngine : WORKFLOW_ENGINE.NATIVE, fallbackEngine: selection.fallbackEngine ?? WORKFLOW_ENGINE.NATIVE, dryRun: nonNativeCandidate && !handoffAllowed, handoffAllowed, reason: handoffAllowed ? selection.reason ?? 'execution_handoff_selected' : nonNativeCandidate ? selection.reason ?? 'execution_gate_disabled' : selection.reason ?? 'native_selected', candidateReason: selection.candidateReason ?? null, configVersion: selection.configVersion, gates, controls: request.controls, }; } return { plan, async start(input) { const request = normalizeWorkflowExecutionRequest(input); const decision = await plan(input); if (!decision.handoffAllowed) { if (decision.candidateEngine !== WORKFLOW_ENGINE.NATIVE) { throw disabledHandoffError(decision); } return { dispatched: false, reason: 'native_execution_owned_by_portal', decision, }; } const engine = engineRegistry.get(decision.effectiveEngine); try { const state = await engine.start(request.spec); return { dispatched: true, engine: decision.effectiveEngine, state, decision, }; } catch (error) { error.code = error.code ?? 'WORKFLOW_EXECUTION_HANDOFF_FAILED'; error.decision = decision; error.fallbackRequested = request.controls.fallbackAllowed; throw error; } }, async resume({ engineId = WORKFLOW_ENGINE.LANGGRAPH, runId, input = null } = {}) { const engine = engineRegistry.get(engineId); if (!engine || engineId === WORKFLOW_ENGINE.NATIVE) { throw disabledHandoffError({ effectiveEngine: WORKFLOW_ENGINE.NATIVE, reason: 'execution_gate_disabled', }); } return engine.resume(runId, input); }, async cancel({ engineId = WORKFLOW_ENGINE.LANGGRAPH, runId, reason = null } = {}) { const engine = engineRegistry.get(engineId); if (!engine || engineId === WORKFLOW_ENGINE.NATIVE) { throw disabledHandoffError({ effectiveEngine: WORKFLOW_ENGINE.NATIVE, reason: 'execution_gate_disabled', }); } return engine.cancel(runId, reason); }, }; } export const workflowExecutionAdapterInternals = { EXECUTION_DECISION_VERSION, EXECUTION_REQUEST_VERSION, MAX_IDEMPOTENCY_KEY_CHARACTERS, clampTimeoutMs, disabledHandoffError, normalizeIdempotencyKey, };