Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,8 @@ Phase presets serialize as a versioned `schemaVersion: 1` catalog. Legacy arrays

Agent execution stays adapter-neutral. The `AgentAdapterContract` describes required launch/send/capture/health/readiness/report/substrate behavior for Pi, Codex, and Claude Code compatible workers without coupling the domain layer to any one CLI. Readiness requires a nonce-equivalent proof, worker reports require `taskId`, `status`, `evidenceRefs`, and `summary`, and health can be supported or best-effort but not absent.

The runtime state schema is separately versioned as `RuntimeState.schemaVersion: 1`. It defines additive boundaries for RunObjective, PolicySelection, TaskGraph refs, WorkerState, EvidenceRef, EvaluationResult, RewardRecord, and IntegrationCandidate data; unknown newer versions fail closed, and RunContract-facing artifact refs expose only objective, policy-selection, and evaluation artifacts.

The domain boundary is explicit: `Decomposer` creates the concrete graph, `Scheduler` computes ready tasks, `WorkerRuntime` dispatches and heartbeats work, `Verifier` validates evidence refs, and `GateEngine` decides transitions.

## Workflow Map
Expand Down
121 changes: 121 additions & 0 deletions src/domain/runtime-state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
export const RUNTIME_STATE_SCHEMA_VERSION = 1;

export type RuntimeStatus = "draft" | "active" | "blocked" | "failed" | "sealed";
export type RuntimeTaskStatus = "pending" | "ready" | "claimed" | "verifying" | "completed" | "blocked" | "failed" | "repair_required";
export type RuntimeWorkerStatus = "ready" | "busy" | "unhealthy" | "completed-retained" | "safe-to-close" | "stale-registry";
export type RuntimeArtifactKind = "run-objective" | "policy-selection" | "task-graph" | "worker-state" | "evidence" | "evaluation" | "reward" | "integration-candidate" | "final-report";
export type EvaluationVerdict = "pass" | "fail" | "repair" | "blocked";
export type IntegrationCandidateStatus = "pending" | "accepted" | "rejected" | "repair_required";

export interface RuntimeArtifactRef { kind: RuntimeArtifactKind; artifactId: string; path: string }
export interface RunObjectiveSchema { id: string; goal: string; successCriteria: string[]; constraints: string[] }
export interface PolicySelectionSchema { id: string; selectedPolicyId: string; rationale: string; artifactRefs: RuntimeArtifactRef[] }
export interface RuntimeTaskSchema { id: string; title: string; status: RuntimeTaskStatus; dependsOn: string[]; evidenceRefs: RuntimeArtifactRef[]; evaluationRefs: RuntimeArtifactRef[] }
export interface RuntimeWorkerSchema { id: string; adapterId: string; status: RuntimeWorkerStatus; readinessNonce?: string; lastHeartbeatAt?: string }
export interface EvaluationResultSchema { id: string; taskId?: string; verdict: EvaluationVerdict; evidenceRefs: RuntimeArtifactRef[] }
export interface RewardRecordSchema { id: string; objectiveId: string; score: number; evidenceRefs: RuntimeArtifactRef[] }
export interface IntegrationCandidateSchema { id: string; taskIds: string[]; status: IntegrationCandidateStatus; evidenceRefs: RuntimeArtifactRef[] }

export interface RuntimeState {
schemaVersion: typeof RUNTIME_STATE_SCHEMA_VERSION;
runId: string;
status: RuntimeStatus;
createdAt: string;
updatedAt: string;
objective?: RunObjectiveSchema;
policySelection?: PolicySelectionSchema;
tasks: RuntimeTaskSchema[];
workers: RuntimeWorkerSchema[];
evaluations: EvaluationResultSchema[];
rewards: RewardRecordSchema[];
integrationCandidates: IntegrationCandidateSchema[];
artifactRefs: RuntimeArtifactRef[];
}

export type RuntimeStateParseResult = { ok: true; state: RuntimeState } | { ok: false; reason: "malformed" | "unsupported-newer-schema"; issues: string[] };

const runtimeStatuses = new Set<RuntimeStatus>(["draft", "active", "blocked", "failed", "sealed"]);
const taskStatuses = new Set<RuntimeTaskStatus>(["pending", "ready", "claimed", "verifying", "completed", "blocked", "failed", "repair_required"]);
const workerStatuses = new Set<RuntimeWorkerStatus>(["ready", "busy", "unhealthy", "completed-retained", "safe-to-close", "stale-registry"]);
const artifactKinds = new Set<RuntimeArtifactKind>(["run-objective", "policy-selection", "task-graph", "worker-state", "evidence", "evaluation", "reward", "integration-candidate", "final-report"]);
const evaluationVerdicts = new Set<EvaluationVerdict>(["pass", "fail", "repair", "blocked"]);
const integrationStatuses = new Set<IntegrationCandidateStatus>(["pending", "accepted", "rejected", "repair_required"]);

export function createRuntimeState(input: { runId: string; now: string }): RuntimeState {
requireText(input.runId, "runId");
if (!isTimestamp(input.now)) throw new Error("now must be a timestamp");
return { schemaVersion: 1, runId: input.runId, status: "draft", createdAt: input.now, updatedAt: input.now, tasks: [], workers: [], evaluations: [], rewards: [], integrationCandidates: [], artifactRefs: [] };
}

export function parseRuntimeState(input: unknown): RuntimeStateParseResult {
if (!isRecord(input)) return { ok: false, reason: "malformed", issues: ["runtime state must be an object"] };
if (typeof input.schemaVersion !== "number" || !Number.isInteger(input.schemaVersion) || input.schemaVersion < 1) return { ok: false, reason: "malformed", issues: ["schemaVersion must be a positive integer"] };
if (input.schemaVersion > RUNTIME_STATE_SCHEMA_VERSION) return { ok: false, reason: "unsupported-newer-schema", issues: [`unsupported runtime state schemaVersion ${input.schemaVersion}`] };
const issues: string[] = [];
needText(input.runId, "runId", issues);
needEnum(input.status, runtimeStatuses, "status", issues);
needTimestamp(input.createdAt, "createdAt", issues);
needTimestamp(input.updatedAt, "updatedAt", issues);
if (input.objective !== undefined) validateObjective(input.objective, "objective", issues);
if (input.policySelection !== undefined) validatePolicy(input.policySelection, "policySelection", issues);
eachRecord(input.tasks, "tasks", issues, (item, label) => validateTask(item, label, issues));
eachRecord(input.workers, "workers", issues, (item, label) => validateWorker(item, label, issues));
eachRecord(input.evaluations, "evaluations", issues, (item, label) => validateEvaluation(item, label, issues));
eachRecord(input.rewards, "rewards", issues, (item, label) => validateReward(item, label, issues));
eachRecord(input.integrationCandidates, "integrationCandidates", issues, (item, label) => validateIntegration(item, label, issues));
eachRecord(input.artifactRefs, "artifactRefs", issues, (item, label) => validateArtifactRef(item, label, issues));
return issues.length ? { ok: false, reason: "malformed", issues } : { ok: true, state: input as unknown as RuntimeState };
}

export function buildRunContractRuntimeRefs(state: RuntimeState): RuntimeArtifactRef[] {
return state.artifactRefs.filter((ref) => ref.kind === "run-objective" || ref.kind === "policy-selection" || ref.kind === "evaluation");
}

function validateObjective(value: unknown, label: string, issues: string[]): void {
if (!isRecord(value)) { issues.push(`${label} must be an object`); return; }
needText(value.id, `${label}.id`, issues); needText(value.goal, `${label}.goal`, issues); needStringArray(value.successCriteria, `${label}.successCriteria`, issues); needStringArray(value.constraints, `${label}.constraints`, issues);
}
function validatePolicy(value: unknown, label: string, issues: string[]): void {
if (!isRecord(value)) { issues.push(`${label} must be an object`); return; }
needText(value.id, `${label}.id`, issues); needText(value.selectedPolicyId, `${label}.selectedPolicyId`, issues); needText(value.rationale, `${label}.rationale`, issues); eachRecord(value.artifactRefs, `${label}.artifactRefs`, issues, (item, itemLabel) => validateArtifactRef(item, itemLabel, issues));
}
function validateTask(value: Record<string, unknown>, label: string, issues: string[]): void {
needText(value.id, `${label}.id`, issues); needText(value.title, `${label}.title`, issues); needEnum(value.status, taskStatuses, `${label}.status`, issues); needStringArray(value.dependsOn, `${label}.dependsOn`, issues); eachRecord(value.evidenceRefs, `${label}.evidenceRefs`, issues, (item, itemLabel) => validateArtifactRef(item, itemLabel, issues)); eachRecord(value.evaluationRefs, `${label}.evaluationRefs`, issues, (item, itemLabel) => validateArtifactRef(item, itemLabel, issues));
}
function validateWorker(value: Record<string, unknown>, label: string, issues: string[]): void {
needText(value.id, `${label}.id`, issues); needText(value.adapterId, `${label}.adapterId`, issues); needEnum(value.status, workerStatuses, `${label}.status`, issues); if (value.readinessNonce !== undefined) needText(value.readinessNonce, `${label}.readinessNonce`, issues); if (value.lastHeartbeatAt !== undefined) needTimestamp(value.lastHeartbeatAt, `${label}.lastHeartbeatAt`, issues);
}
function validateEvaluation(value: Record<string, unknown>, label: string, issues: string[]): void {
needText(value.id, `${label}.id`, issues); if (value.taskId !== undefined) needText(value.taskId, `${label}.taskId`, issues); needEnum(value.verdict, evaluationVerdicts, `${label}.verdict`, issues); eachRecord(value.evidenceRefs, `${label}.evidenceRefs`, issues, (item, itemLabel) => validateArtifactRef(item, itemLabel, issues));
}
function validateReward(value: Record<string, unknown>, label: string, issues: string[]): void {
needText(value.id, `${label}.id`, issues); needText(value.objectiveId, `${label}.objectiveId`, issues); if (typeof value.score !== "number" || !Number.isFinite(value.score)) issues.push(`${label}.score must be finite`); eachRecord(value.evidenceRefs, `${label}.evidenceRefs`, issues, (item, itemLabel) => validateArtifactRef(item, itemLabel, issues));
}
function validateIntegration(value: Record<string, unknown>, label: string, issues: string[]): void {
needText(value.id, `${label}.id`, issues); needStringArray(value.taskIds, `${label}.taskIds`, issues); needEnum(value.status, integrationStatuses, `${label}.status`, issues); eachRecord(value.evidenceRefs, `${label}.evidenceRefs`, issues, (item, itemLabel) => validateArtifactRef(item, itemLabel, issues));
}
function validateArtifactRef(value: Record<string, unknown>, label: string, issues: string[]): void {
needEnum(value.kind, artifactKinds, `${label}.kind`, issues); needText(value.artifactId, `${label}.artifactId`, issues); needText(value.path, `${label}.path`, issues);
}

function eachRecord(value: unknown, label: string, issues: string[], visit: (record: Record<string, unknown>, label: string) => void): void {
if (!Array.isArray(value)) { issues.push(`${label} must be an array`); return; }
value.forEach((item, index) => isRecord(item) ? visit(item, `${label}[${index}]`) : issues.push(`${label}[${index}] must be an object`));
}
function needStringArray(value: unknown, label: string, issues: string[]): void {
if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || !item.trim())) issues.push(`${label} must be a string array`);
}
function requireText(value: unknown, label: string): void {
if (typeof value !== "string" || !value.trim()) throw new Error(`${label} is required`);
}
function needText(value: unknown, label: string, issues: string[]): void {
if (typeof value !== "string" || !value.trim()) issues.push(`${label} is required`);
}
function needEnum<T extends string>(value: unknown, values: Set<T>, label: string, issues: string[]): void {
if (typeof value !== "string" || !values.has(value as T)) issues.push(`${label} is invalid`);
}
function needTimestamp(value: unknown, label: string, issues: string[]): void {
if (typeof value !== "string" || !isTimestamp(value)) issues.push(`${label} must be a timestamp`);
}
function isTimestamp(value: string): boolean { return Number.isFinite(Date.parse(value)); }
function isRecord(value: unknown): value is Record<string, unknown> { return typeof value === "object" && value !== null && !Array.isArray(value); }
55 changes: 55 additions & 0 deletions test/runtime-state.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import * as assert from "node:assert/strict";
import { test } from "node:test";
import { buildRunContractRuntimeRefs, createRuntimeState, parseRuntimeState, type RuntimeArtifactRef, type RuntimeState } from "../src/domain/runtime-state.js";

const now = "2026-01-01T00:00:00.000Z";

function state(): RuntimeState {
return createRuntimeState({ runId: "run-1", now });
}

test("runtime state schema initializes versioned additive state", () => {
assert.deepEqual(state(), { schemaVersion: 1, runId: "run-1", status: "draft", createdAt: now, updatedAt: now, tasks: [], workers: [], evaluations: [], rewards: [], integrationCandidates: [], artifactRefs: [] });
assert.equal(parseRuntimeState(state()).ok, true);
});

test("runtime state parser fails closed on unknown newer schemas and malformed refs", () => {
const newer = parseRuntimeState({ ...state(), schemaVersion: 99 });
assert.equal(newer.ok, false);
assert.equal(newer.reason, "unsupported-newer-schema");

const malformed = parseRuntimeState({ ...state(), updatedAt: "not-a-date", artifactRefs: [{ kind: "evidence", artifactId: "", path: "verify.md" }] });
assert.equal(malformed.ok, false);
assert.equal(malformed.reason, "malformed");
assert.ok(malformed.issues.includes("updatedAt must be a timestamp"));
assert.ok(malformed.issues.includes("artifactRefs[0].artifactId is required"));
const zero = parseRuntimeState({ ...state(), schemaVersion: 0 });
assert.equal(zero.ok, false);
assert.equal(zero.reason, "malformed");
assert.throws(() => createRuntimeState({ runId: "", now }), /runId is required/);
const nested = parseRuntimeState({ ...state(), status: "done", tasks: [{ id: "t", title: "T", status: "bad", dependsOn: [], evidenceRefs: [{ kind: "mystery", artifactId: "e", path: "e.json" }], evaluationRefs: [] }], workers: [{ id: "w", adapterId: "a", status: "lost", readinessNonce: 123 }], evaluations: [{ id: "e", taskId: {}, verdict: "pass", evidenceRefs: [] }] });
assert.equal(nested.ok, false);
assert.ok(nested.issues.includes("status is invalid"));
assert.ok(nested.issues.includes("tasks[0].status is invalid"));
assert.ok(nested.issues.includes("tasks[0].evidenceRefs[0].kind is invalid"));
assert.ok(nested.issues.includes("workers[0].status is invalid"));
assert.ok(nested.issues.includes("workers[0].readinessNonce is required"));
assert.ok(nested.issues.includes("evaluations[0].taskId is required"));
});

test("run contract runtime refs expose objective, policy, and evaluation artifacts only", () => {
const refs: RuntimeArtifactRef[] = [
{ kind: "run-objective", artifactId: "objective", path: "runtime/objective.json" },
{ kind: "policy-selection", artifactId: "policy", path: "runtime/policy.json" },
{ kind: "task-graph", artifactId: "graph", path: "runtime/task-graph.json" },
{ kind: "evaluation", artifactId: "evaluation", path: "runtime/evaluation.json" },
];
assert.deepEqual(buildRunContractRuntimeRefs({ ...state(), artifactRefs: refs }), [refs[0], refs[1], refs[3]]);
});

test("runtime state can represent successful and repair-required examples", () => {
const success: RuntimeState = { ...state(), status: "sealed", objective: { id: "objective", goal: "ship MVP", successCriteria: ["tests pass"], constraints: ["no destructive cleanup"] }, tasks: [{ id: "build", title: "Build slice", status: "completed", dependsOn: [], evidenceRefs: [{ kind: "evidence", artifactId: "verify", path: "verify.md" }], evaluationRefs: [] }] };
const repair: RuntimeState = { ...state(), status: "blocked", tasks: [{ id: "build", title: "Build slice", status: "repair_required", dependsOn: [], evidenceRefs: [], evaluationRefs: [{ kind: "evaluation", artifactId: "eval", path: "evaluation.json" }] }], integrationCandidates: [{ id: "candidate", taskIds: ["build"], status: "repair_required", evidenceRefs: [] }] };
assert.equal(parseRuntimeState(success).ok, true);
assert.equal(parseRuntimeState(repair).ok, true);
});
Loading