feat: 补充 runner Job dry-run 骨架
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user