feat: add AgentRun cancel lifecycle policy (#859)

Co-authored-by: Codex <codex@noreply.local>
This commit is contained in:
Lyon
2026-06-25 08:44:30 +08:00
committed by GitHub
parent 94d985d628
commit 790e5df281
10 changed files with 278 additions and 13 deletions
+45
View File
@@ -1,3 +1,5 @@
// SPEC: PJ2026-01060305 AgentRun execution policy + PJ2026-01020108 cancel lifecycle draft-2026-06-25-p0.
// Parses AgentRun YAML lane policy, including cancel lifecycle values owned by config/agentrun.yaml.
import { rootPath } from "./config";
import {
asRecord,
@@ -113,6 +115,7 @@ export interface AgentRunLaneSpec {
readonly egressProxyUrl: string | null;
readonly noProxyExtra: readonly string[];
readonly retention: AgentRunRunnerRetentionSpec;
readonly cancelLifecycle: AgentRunCancelLifecycleSpec;
};
readonly localPostgres: {
readonly enabled: boolean;
@@ -195,6 +198,19 @@ export interface AgentRunRunnerRetentionSpec {
};
}
export type AgentRunCancelLifecycleStage = "accepted" | "persisted" | "delivered" | "aborting" | "terminalized" | "fenced" | "late-write-rejected";
export interface AgentRunCancelLifecycleSpec {
readonly deliveryMode: "manager-epoch";
readonly gracefulAbortMs: number;
readonly killEscalationMs: number;
readonly staleHeartbeatFencingMs: number;
readonly lateWriteFencing: {
readonly enabled: boolean;
};
readonly eventStages: readonly AgentRunCancelLifecycleStage[];
}
export interface AgentRunLaneTarget {
readonly configPath: string;
readonly spec: AgentRunLaneSpec;
@@ -312,6 +328,7 @@ export function agentRunLaneSummary(spec: AgentRunLaneSpec): Record<string, unkn
egressProxyUrl: spec.deployment.runner.egressProxyUrl,
noProxyExtra: spec.deployment.runner.noProxyExtra,
retention: spec.deployment.runner.retention,
cancelLifecycle: spec.deployment.runner.cancelLifecycle,
},
localPostgres: spec.deployment.localPostgres,
},
@@ -551,11 +568,39 @@ function parseDeployment(input: Record<string, unknown>, path: string): AgentRun
egressProxyUrl: optionalStringField(runner, "egressProxyUrl", `${path}.runner`) ?? null,
noProxyExtra: optionalStringArrayField(runner, "noProxyExtra", `${path}.runner`),
retention: parseRunnerRetention(recordField(runner, "retention", `${path}.runner`), `${path}.runner.retention`),
cancelLifecycle: parseCancelLifecycle(recordField(runner, "cancelLifecycle", `${path}.runner`), `${path}.runner.cancelLifecycle`),
},
localPostgres: parseLocalPostgres(localPostgres, `${path}.localPostgres`),
};
}
function parseCancelLifecycle(input: Record<string, unknown>, path: string): AgentRunCancelLifecycleSpec {
const lateWriteFencing = recordField(input, "lateWriteFencing", path);
return {
deliveryMode: enumField(input, "deliveryMode", path, ["manager-epoch"]),
gracefulAbortMs: positiveIntegerField(input, "gracefulAbortMs", path),
killEscalationMs: positiveIntegerField(input, "killEscalationMs", path),
staleHeartbeatFencingMs: positiveIntegerField(input, "staleHeartbeatFencingMs", path),
lateWriteFencing: {
enabled: booleanField(lateWriteFencing, "enabled", `${path}.lateWriteFencing`),
},
eventStages: parseCancelLifecycleStages(input.eventStages, `${path}.eventStages`),
};
}
function parseCancelLifecycleStages(input: unknown, path: string): readonly AgentRunCancelLifecycleStage[] {
const values: readonly AgentRunCancelLifecycleStage[] = ["accepted", "persisted", "delivered", "aborting", "terminalized", "fenced", "late-write-rejected"];
if (!Array.isArray(input)) throw new Error(`${path} must be an array`);
if (input.length === 0) throw new Error(`${path} must declare at least one stage`);
const result = input.map((value, index) => {
if (typeof value !== "string" || !values.includes(value as AgentRunCancelLifecycleStage)) throw new Error(`${path}[${index}] must be one of ${values.join(", ")}`);
return value as AgentRunCancelLifecycleStage;
});
const duplicates = result.filter((value, index) => result.indexOf(value) !== index);
if (duplicates.length > 0) throw new Error(`${path} must not contain duplicate stages: ${[...new Set(duplicates)].join(", ")}`);
return result;
}
function parseRunnerRetention(input: Record<string, unknown>, path: string): AgentRunRunnerRetentionSpec {
const selectors = recordField(input, "selectors", path);
const ageBasedCleanup = recordField(input, "ageBasedCleanup", path);