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 { 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 { return await this.client.post(`/api/v1/runs/${encodeURIComponent(runId)}/claim`, { runnerId, leaseMs }) as RunRecord; } async heartbeat(runId: string, runnerId: string, leaseMs: number): Promise { 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 { 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 { return await this.client.post(`/api/v1/commands/${encodeURIComponent(commandId)}/ack`, {}) as CommandRecord; } async appendEvent(runId: string, event: BackendEvent): Promise { 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 { 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); }