import type { ReportFieldSchema, ReportSchemaPolicy, ReportSchemaV1 } from "./workflow-types.js"; export const WORKER_ATTEMPT_STATES = [ "pending", "running", "blocked", "ambiguous", "done", "failed", "cancelled", ] as const; export type WorkerAttemptState = (typeof WORKER_ATTEMPT_STATES)[number]; export function normalizeWorkerAttemptState(value: unknown): WorkerAttemptState { switch (value) { case "spawn-intent": return "pending"; case "exited": return "running"; case "pending": case "running": case "blocked": case "ambiguous": case "done": case "failed": case "cancelled": return value; default: throw new TypeError("worker attempt state is invalid"); } } export interface WorkerGateFacts { readonly evidence: { readonly exists: boolean }; readonly report: { readonly schemaDeclared: boolean; readonly matches: boolean; readonly policy?: ReportSchemaPolicy }; readonly acceptanceCheck: { readonly declared: boolean; readonly exitCode: number | null }; readonly human: { readonly approvalRequired: boolean; readonly approved: boolean }; readonly budget: { readonly exceeded: boolean }; readonly runtime: { readonly supervisorLost: boolean; readonly effectsUncertain: boolean; readonly certain?: boolean; }; readonly failure?: { readonly observed: boolean }; readonly termination?: { readonly observed: boolean }; readonly wait?: { readonly declared: boolean }; readonly reconcile: { readonly decisionExists: boolean; readonly uncertaintyCleared?: boolean }; readonly input: { readonly kind: "revise_contract" | "steer" | "peer_message" | null; readonly correlationMatches: boolean; }; readonly cancel: { readonly verbReceived: boolean }; } export type WorkerGateGuard = | "evidence.exists" | "report.matches(report_schema)" | "acceptance_check.exit == 0" | "human.approved" | "budget.not_exceeded" | "runtime.uncertain" | "runtime.certain" | "failure.observed" | "termination.observed" | "wait.declared" | "reconcile.decision.exists" | "reconcile.uncertainty.cleared" | "input.is_new" | "input.correlation_matches" | "cancel.verb"; export interface WorkerAttemptTransitionRule { readonly from: WorkerAttemptState; readonly to: WorkerAttemptState; readonly guards: readonly WorkerGateGuard[]; } const DONE_GUARDS: readonly WorkerGateGuard[] = [ "runtime.certain", "evidence.exists", "report.matches(report_schema)", "acceptance_check.exit == 0", "human.approved", "budget.not_exceeded", ]; const RECONCILE_GUARDS: readonly WorkerGateGuard[] = [ "reconcile.decision.exists", "reconcile.uncertainty.cleared", ]; const CANCELLED_GUARDS: readonly WorkerGateGuard[] = [ "cancel.verb", "termination.observed", "runtime.certain", ]; export const WORKER_ATTEMPT_LEGALITY_TABLE: readonly WorkerAttemptTransitionRule[] = [ { from: "pending", to: "running", guards: [] }, { from: "pending", to: "failed", guards: ["failure.observed"] }, { from: "pending", to: "ambiguous", guards: ["runtime.uncertain"] }, { from: "pending", to: "cancelled", guards: CANCELLED_GUARDS }, { from: "running", to: "blocked", guards: ["wait.declared"] }, { from: "running", to: "done", guards: DONE_GUARDS }, { from: "running", to: "failed", guards: ["failure.observed"] }, { from: "running", to: "ambiguous", guards: ["runtime.uncertain"] }, { from: "running", to: "cancelled", guards: CANCELLED_GUARDS }, { from: "blocked", to: "running", guards: ["input.is_new", "input.correlation_matches"], }, { from: "blocked", to: "failed", guards: ["failure.observed"] }, { from: "blocked", to: "ambiguous", guards: ["runtime.uncertain"] }, { from: "blocked", to: "cancelled", guards: CANCELLED_GUARDS }, { from: "ambiguous", to: "pending", guards: RECONCILE_GUARDS }, { from: "ambiguous", to: "running", guards: RECONCILE_GUARDS }, { from: "ambiguous", to: "blocked", guards: [...RECONCILE_GUARDS, "wait.declared"] }, { from: "ambiguous", to: "done", guards: [...RECONCILE_GUARDS, ...DONE_GUARDS] }, { from: "ambiguous", to: "failed", guards: [...RECONCILE_GUARDS, "failure.observed"] }, { from: "ambiguous", to: "cancelled", guards: [...RECONCILE_GUARDS, ...CANCELLED_GUARDS], }, ] as const; export type WorkerAttemptTransitionResult = | { readonly allowed: true } | { readonly allowed: false; readonly reason: "undeclared-transition" | "guard-failed"; readonly blockers: readonly WorkerGateGuard[]; }; export function evaluateWorkerAttemptTransition( from: WorkerAttemptState, to: WorkerAttemptState, facts: WorkerGateFacts, ): WorkerAttemptTransitionResult { const rule = WORKER_ATTEMPT_LEGALITY_TABLE.find( (candidate) => candidate.from === from && candidate.to === to, ); if (!rule) { return { allowed: false, reason: "undeclared-transition", blockers: [] }; } const blockers = rule.guards.filter((guard) => !guardSatisfied(guard, facts)); return blockers.length === 0 ? { allowed: true } : { allowed: false, reason: "guard-failed", blockers }; } function guardSatisfied(guard: WorkerGateGuard, facts: WorkerGateFacts): boolean { switch (guard) { case "evidence.exists": return facts.evidence.exists; case "report.matches(report_schema)": return !facts.report.schemaDeclared || facts.report.policy === "advisory" || facts.report.policy === "preferred" || facts.report.matches; case "acceptance_check.exit == 0": return !facts.acceptanceCheck.declared || facts.acceptanceCheck.exitCode === 0; case "human.approved": return !facts.human.approvalRequired || facts.human.approved; case "budget.not_exceeded": return !facts.budget.exceeded; case "runtime.uncertain": return facts.runtime.supervisorLost || facts.runtime.effectsUncertain; case "runtime.certain": return facts.runtime.certain === true || (!facts.runtime.supervisorLost && !facts.runtime.effectsUncertain); case "failure.observed": return facts.failure?.observed === true; case "termination.observed": return facts.termination?.observed === true; case "wait.declared": return facts.wait?.declared === true; case "reconcile.decision.exists": return facts.reconcile.decisionExists; case "reconcile.uncertainty.cleared": return facts.reconcile.uncertaintyCleared === true; case "input.is_new": return facts.input.kind === "revise_contract" || facts.input.kind === "steer" || facts.input.kind === "peer_message"; case "input.correlation_matches": return facts.input.correlationMatches; case "cancel.verb": return facts.cancel.verbReceived; } } export const TERMINAL_ATTEMPT_PRIORITY = ["ambiguous", "cancelled", "failed", "running"] as const; export function classifyObservedTermination(input: { readonly runtimeCertain: boolean; readonly effectsUncertain: boolean; readonly cancellationRequested: boolean; readonly terminationObserved: boolean; readonly failureObserved: boolean; }): WorkerAttemptState { if (!input.runtimeCertain || input.effectsUncertain) return "ambiguous"; if (input.cancellationRequested && input.terminationObserved) return "cancelled"; if (input.failureObserved) return "failed"; return "running"; } export function reportMatchesSchema(schema: ReportSchemaV1, report: unknown): boolean { if (!isRecord(report)) return false; for (const required of schema.required) { if (!Object.prototype.hasOwnProperty.call(report, required)) return false; } for (const [name, field] of Object.entries(schema.properties)) { if (!Object.prototype.hasOwnProperty.call(report, name)) continue; if (!matchesFieldType(field, report[name])) return false; } return true; } function matchesFieldType(field: ReportFieldSchema, value: unknown): boolean { switch (field.type) { case "string": return typeof value === "string"; case "number": return typeof value === "number" && Number.isFinite(value); case "boolean": return typeof value === "boolean"; case "object": return isRecord(value); case "array": return Array.isArray(value); case "null": return value === null; } } function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); }