Files
AgentCompany/src/main/run-attempt-policy.ts
T

119 lines
3.8 KiB
TypeScript

export const DEFAULT_MAX_CONSECUTIVE_SELF_WAKES = 5;
export interface HeartbeatAttemptInput {
kind: "heartbeat";
trigger: string;
activeRunCount: number;
maxConcurrentRuns: number;
lastStartedAtMs: number | null;
cooldownMs: number;
nowMs: number;
bypassCooldown: boolean;
}
export interface TaskFailureAttemptInput {
kind: "task_failure";
retryCount: number;
maxRetryAttempts: number;
}
export interface SelfWakeAttemptInput {
kind: "self_wake";
hasWorkToContinue: boolean;
consecutiveSelfWakes: number;
maxConsecutiveSelfWakes: number;
}
export type HeartbeatAttemptDecision =
| { action: "run" }
| { action: "skip"; reason: "max_concurrent_runs" | "cooldown"; waitMs?: number };
export type TaskFailureAttemptDecision =
| { action: "retry"; attemptNumber: number }
| { action: "escalate" };
export type SelfWakeAttemptDecision =
| { action: "run" }
| { action: "skip"; reason: "no_work" | "self_wake_exhausted" };
type RunAttemptInput =
| HeartbeatAttemptInput
| TaskFailureAttemptInput
| SelfWakeAttemptInput;
type RunAttemptDecision =
| HeartbeatAttemptDecision
| TaskFailureAttemptDecision
| SelfWakeAttemptDecision;
export function runAttemptKey(agentId: string, companyId: string): string {
return `${companyId}:${agentId}`;
}
function assertNonNegativeInteger(name: string, value: number): void {
if (!Number.isInteger(value) || value < 0) {
throw new Error(`run-attempt policy: ${name} must be a non-negative integer`);
}
}
function assertNonNegativeNumber(name: string, value: number): void {
if (!Number.isFinite(value) || value < 0) {
throw new Error(`run-attempt policy: ${name} must be a non-negative number`);
}
}
export function decideRunAttempt(input: HeartbeatAttemptInput): HeartbeatAttemptDecision;
export function decideRunAttempt(input: TaskFailureAttemptInput): TaskFailureAttemptDecision;
export function decideRunAttempt(input: SelfWakeAttemptInput): SelfWakeAttemptDecision;
export function decideRunAttempt(input: RunAttemptInput): RunAttemptDecision {
switch (input.kind) {
case "heartbeat": {
assertNonNegativeInteger("activeRunCount", input.activeRunCount);
assertNonNegativeInteger("maxConcurrentRuns", input.maxConcurrentRuns);
assertNonNegativeNumber("cooldownMs", input.cooldownMs);
assertNonNegativeNumber("nowMs", input.nowMs);
if (input.lastStartedAtMs !== null) {
assertNonNegativeNumber("lastStartedAtMs", input.lastStartedAtMs);
}
if (input.activeRunCount >= input.maxConcurrentRuns) {
return { action: "skip", reason: "max_concurrent_runs" };
}
if (!input.bypassCooldown && input.lastStartedAtMs !== null) {
const elapsedMs = input.nowMs - input.lastStartedAtMs;
if (elapsedMs < input.cooldownMs) {
return {
action: "skip",
reason: "cooldown",
waitMs: input.cooldownMs - elapsedMs,
};
}
}
return { action: "run" };
}
case "task_failure": {
assertNonNegativeInteger("retryCount", input.retryCount);
assertNonNegativeInteger("maxRetryAttempts", input.maxRetryAttempts);
if (input.retryCount < input.maxRetryAttempts) {
return { action: "retry", attemptNumber: input.retryCount + 1 };
}
return { action: "escalate" };
}
case "self_wake": {
assertNonNegativeInteger("consecutiveSelfWakes", input.consecutiveSelfWakes);
assertNonNegativeInteger("maxConsecutiveSelfWakes", input.maxConsecutiveSelfWakes);
if (!input.hasWorkToContinue) {
return { action: "skip", reason: "no_work" };
}
if (input.consecutiveSelfWakes >= input.maxConsecutiveSelfWakes) {
return { action: "skip", reason: "self_wake_exhausted" };
}
return { action: "run" };
}
}
}