119 lines
3.8 KiB
TypeScript
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" };
|
|
}
|
|
}
|
|
}
|