/** * Deterministic FSM for Multi-Step Workflows * * Orchestrates plan -> review -> execute -> verify using rules, not LLM decisions. * Risk-based phase skipping: high=all phases, medium=skip planner, low=execute+test only. */ export type Phase = "classify"|"plan"|"plan_review"|"execute"|"code_review"|"quality_review"|"security"|"test"|"output_review"|"done"|"failed"|"paused"; export type RiskLevel = "low"|"medium"|"high"; export type Verdict = "approve"|"approve_with_notes"|"request_changes"|"reject"|"block"; export interface PhaseRecord { phase: Phase; enteredAt: string; exitedAt: string|null; verdict: Verdict|null; provider: string|null; model: string|null; retryCount: number; notes: string|null; } export interface WorkflowContext { id: string; currentPhase: Phase; risk: RiskLevel; lastVerdict: Verdict|null; retries: Record; maxRetries: number; testsPass: boolean; history: PhaseRecord[]; createdAt: string; metadata: Record; } interface Transition { from: Phase; to: Phase; condition: (ctx: WorkflowContext) => boolean; description: string; } const HIGH_KEYWORDS = ["schema","migration","deploy","delete","drop","env","database","refactor","security","auth","production","secrets","credentials","permission"]; const MED_KEYWORDS = ["endpoint","feature","service","model","api","integration","webhook","middleware","route"]; export function classifyRisk(desc: string): RiskLevel { const l = desc.toLowerCase(); if (HIGH_KEYWORDS.some(k => l.includes(k))) return "high"; if (MED_KEYWORDS.some(k => l.includes(k))) return "medium"; return "low"; } const PHASE_ORDER: Phase[] = ["classify","plan","plan_review","execute","code_review","quality_review","security","test","output_review"]; const T: Transition[] = [ {from:"classify",to:"plan",condition:c=>c.risk==="high",description:"High risk -> full planning"}, {from:"classify",to:"execute",condition:c=>c.risk==="medium",description:"Medium risk -> skip planner"}, {from:"classify",to:"execute",condition:c=>c.risk==="low",description:"Low risk -> direct execute"}, {from:"plan",to:"plan_review",condition:()=>true,description:"Plan -> review"}, {from:"plan_review",to:"execute",condition:c=>c.lastVerdict==="approve"||c.lastVerdict==="approve_with_notes",description:"Plan approved -> execute"}, {from:"plan_review",to:"plan",condition:c=>(c.lastVerdict==="reject"||c.lastVerdict==="request_changes")&&(c.retries["plan"]??0) retry"}, {from:"plan_review",to:"failed",condition:c=>(c.lastVerdict==="reject"||c.lastVerdict==="request_changes")&&(c.retries["plan"]??0)>=c.maxRetries,description:"Plan rejected max retries"}, {from:"execute",to:"code_review",condition:c=>c.risk!=="low",description:"Non-low -> code review"}, {from:"execute",to:"test",condition:c=>c.risk==="low",description:"Low -> skip reviews"}, {from:"code_review",to:"quality_review",condition:c=>c.lastVerdict==="approve"||c.lastVerdict==="approve_with_notes",description:"Code approved -> quality"}, {from:"code_review",to:"execute",condition:c=>(c.lastVerdict==="reject"||c.lastVerdict==="request_changes")&&(c.retries["execute"]??0) re-execute"}, {from:"code_review",to:"failed",condition:c=>(c.lastVerdict==="reject"||c.lastVerdict==="request_changes")&&(c.retries["execute"]??0)>=c.maxRetries,description:"Code rejected max retries"}, {from:"quality_review",to:"security",condition:c=>c.risk==="high",description:"High -> security audit"}, {from:"quality_review",to:"test",condition:c=>c.risk!=="high",description:"Non-high -> skip security"}, {from:"security",to:"failed",condition:c=>c.lastVerdict==="block",description:"Security BLOCK -> failed"}, {from:"security",to:"test",condition:c=>c.lastVerdict!=="block",description:"Security passed -> test"}, {from:"test",to:"output_review",condition:c=>c.testsPass,description:"Tests pass -> output review"}, {from:"test",to:"execute",condition:c=>!c.testsPass&&(c.retries["execute"]??0) re-execute"}, {from:"test",to:"failed",condition:c=>!c.testsPass&&(c.retries["execute"]??0)>=c.maxRetries,description:"Tests fail max retries"}, {from:"output_review",to:"done",condition:()=>true,description:"Output reviewed -> done"}, ]; export function createWorkflow(id: string, description: string, opts?: {maxRetries?: number; metadata?: Record}): WorkflowContext { const risk = classifyRisk(description); return {id, currentPhase:"classify", risk, lastVerdict:null, retries:{}, maxRetries:opts?.maxRetries??3, testsPass:false, history:[{phase:"classify",enteredAt:new Date().toISOString(),exitedAt:null,verdict:null,provider:null,model:null,retryCount:0,notes:`Risk: ${risk}`}], createdAt:new Date().toISOString(), metadata:opts?.metadata??{}}; } export function advance(ctx: WorkflowContext, result?: {verdict?:Verdict;testsPass?:boolean;provider?:string;model?:string;notes?:string}): Phase|null { if(result?.verdict!=null) ctx.lastVerdict=result.verdict; if(result?.testsPass!=null) ctx.testsPass=result.testsPass; const cur=ctx.history[ctx.history.length-1]; if(cur){cur.exitedAt=new Date().toISOString();cur.verdict=result?.verdict??null;cur.provider=result?.provider??null;cur.model=result?.model??null;cur.notes=result?.notes??null;} for(const t of T){ if(t.from===ctx.currentPhase&&t.condition(ctx)){ const fi=PHASE_ORDER.indexOf(t.from),ti=PHASE_ORDER.indexOf(t.to); if(ti>=0&&fi>=0&&ti<=fi) ctx.retries[t.to]=(ctx.retries[t.to]??0)+1; ctx.currentPhase=t.to; ctx.history.push({phase:t.to,enteredAt:new Date().toISOString(),exitedAt:null,verdict:null,provider:null,model:null,retryCount:ctx.retries[t.to]??0,notes:t.description}); return t.to; } } return null; } export function pause(ctx: WorkflowContext, reason: string): void { ctx.currentPhase="paused"; ctx.history.push({phase:"paused",enteredAt:new Date().toISOString(),exitedAt:null,verdict:null,provider:null,model:null,retryCount:0,notes:reason}); } export function resume(ctx: WorkflowContext, phase: Phase): void { const p=ctx.history[ctx.history.length-1];if(p)p.exitedAt=new Date().toISOString(); ctx.currentPhase=phase; ctx.history.push({phase,enteredAt:new Date().toISOString(),exitedAt:null,verdict:null,provider:null,model:null,retryCount:ctx.retries[phase]??0,notes:"Resumed"}); } export function isTerminated(ctx: WorkflowContext): boolean { return ctx.currentPhase==="done"||ctx.currentPhase==="failed"; } export function getPhaseSequence(ctx: WorkflowContext): Phase[] { return ctx.history.map(r=>r.phase); } export function getLLMCallCount(ctx: WorkflowContext): number { const sys: Phase[]=["classify","paused","done","failed"]; return ctx.history.filter(r=>!sys.includes(r.phase)&&r.exitedAt!=null).length; }