109 lines
4.7 KiB
TypeScript
109 lines
4.7 KiB
TypeScript
import { ManagerClient } from "../mgr/client.js";
|
|
import { AgentRunError } from "../common/errors.js";
|
|
import type { BackendEvent, BackendProfile, CommandRecord, FailureKind, JsonRecord, RunRecord, RunnerRecord, TerminalStatus } from "../common/types.js";
|
|
|
|
export interface RunnerRegistrationInput {
|
|
runId: string;
|
|
attemptId: string;
|
|
backendProfile: BackendProfile;
|
|
placement: "host-process" | "kubernetes-job";
|
|
sourceCommit: string;
|
|
runnerId?: string;
|
|
jobName?: string;
|
|
podName?: string;
|
|
logPath?: string;
|
|
}
|
|
|
|
export interface PollCommandsResult {
|
|
items: CommandRecord[];
|
|
selected: CommandRecord | null;
|
|
}
|
|
|
|
export interface RunnerFailureReport {
|
|
terminalStatus: TerminalStatus;
|
|
failureKind: FailureKind;
|
|
failureMessage: string;
|
|
}
|
|
|
|
export class RunnerManagerApi {
|
|
readonly client: ManagerClient;
|
|
|
|
constructor(readonly managerUrl: string) {
|
|
this.client = new ManagerClient(managerUrl);
|
|
}
|
|
|
|
async register(input: RunnerRegistrationInput): Promise<RunnerRecord> {
|
|
const body: JsonRecord = {
|
|
runId: input.runId,
|
|
attemptId: input.attemptId,
|
|
backendProfile: input.backendProfile,
|
|
placement: input.placement,
|
|
sourceCommit: input.sourceCommit,
|
|
};
|
|
if (input.runnerId) body.id = input.runnerId;
|
|
if (input.jobName) body.jobName = input.jobName;
|
|
if (input.podName) body.podName = input.podName;
|
|
if (input.logPath) body.logPath = input.logPath;
|
|
return await this.client.post("/api/v1/runners/register", body) as RunnerRecord;
|
|
}
|
|
|
|
async claim(runId: string, runnerId: string, leaseMs: number): Promise<RunRecord> {
|
|
return await this.client.post(`/api/v1/runs/${encodeURIComponent(runId)}/claim`, { runnerId, leaseMs }) as RunRecord;
|
|
}
|
|
|
|
async heartbeat(runId: string, runnerId: string, leaseMs: number): Promise<RunRecord> {
|
|
return await this.client.patch(`/api/v1/runs/${encodeURIComponent(runId)}/lease`, { runnerId, leaseMs }) as RunRecord;
|
|
}
|
|
|
|
async pollCommands(runId: string, options: { afterSeq?: number; limit?: number; commandId?: string }): Promise<PollCommandsResult> {
|
|
const afterSeq = options.afterSeq ?? 0;
|
|
const limit = options.limit ?? 20;
|
|
const response = await this.client.get(`/api/v1/runs/${encodeURIComponent(runId)}/commands?afterSeq=${afterSeq}&limit=${limit}`) as { items?: CommandRecord[] };
|
|
const items = Array.isArray(response.items) ? response.items : [];
|
|
const selected = options.commandId ? items.find((item) => item.id === options.commandId && item.state === "pending" && item.type === "turn") ?? null : items.find((item) => item.state === "pending" && item.type === "turn") ?? null;
|
|
return { items, selected };
|
|
}
|
|
|
|
async ackCommand(commandId: string): Promise<CommandRecord> {
|
|
return await this.client.post(`/api/v1/commands/${encodeURIComponent(commandId)}/ack`, {}) as CommandRecord;
|
|
}
|
|
|
|
async appendEvent(runId: string, event: BackendEvent): Promise<JsonRecord> {
|
|
return await this.client.post(`/api/v1/runs/${encodeURIComponent(runId)}/events`, event as unknown as JsonRecord) as JsonRecord;
|
|
}
|
|
|
|
async reportStatus(runId: string, report: { terminalStatus: TerminalStatus; failureKind: FailureKind | null; failureMessage: string | null }): Promise<RunRecord> {
|
|
return await this.client.patch(`/api/v1/runs/${encodeURIComponent(runId)}/status`, report as unknown as JsonRecord) as RunRecord;
|
|
}
|
|
|
|
async reportFailure(runId: string, report: RunnerFailureReport): Promise<{ reported: boolean; run: RunRecord | null; reportError: string | null }> {
|
|
try {
|
|
await this.appendEvent(runId, { type: "error", payload: { failureKind: report.failureKind, message: report.failureMessage, source: "agentrun-runner" } });
|
|
const run = await this.reportStatus(runId, report);
|
|
return { reported: true, run, reportError: null };
|
|
} catch (error) {
|
|
return { reported: false, run: null, reportError: errorMessage(error) };
|
|
}
|
|
}
|
|
}
|
|
|
|
export function failureKindFromError(error: unknown): FailureKind {
|
|
if (error instanceof AgentRunError) return error.failureKind;
|
|
const message = errorMessage(error).toLowerCase();
|
|
if (message.includes("auth") || message.includes("unauthorized") || message.includes("forbidden")) return "provider-auth-failed";
|
|
if (message.includes("timeout")) return "backend-timeout";
|
|
if (message.includes("lease") || message.includes("claim")) return "runner-lease-conflict";
|
|
return "infra-failed";
|
|
}
|
|
|
|
export function terminalStatusForFailure(failureKind: FailureKind): TerminalStatus {
|
|
if (failureKind === "cancelled") return "cancelled";
|
|
if (failureKind === "secret-unavailable" || failureKind === "tenant-policy-denied" || failureKind === "schema-invalid") return "blocked";
|
|
return "failed";
|
|
}
|
|
|
|
export function errorMessage(error: unknown): string {
|
|
if (error instanceof Error) return error.message;
|
|
return String(error);
|
|
}
|