Merge pull request #1816 from pikasTech/fix/307-agentrun-queue-retry-cli
fix(agentrun): 添加队列任务重试资源命令
This commit is contained in:
@@ -4,7 +4,25 @@ import { parseAgentRunClientConfigYaml, resolveAgentRunAuth, runAgentRunCommand,
|
||||
import { resolveAgentRunLaneTarget } from "./agentrun-lanes";
|
||||
import { placeholderAgentRunImage, renderAgentRunGitopsFiles } from "./agentrun-manifests";
|
||||
|
||||
function renderedTextOf(value: unknown): string {
|
||||
if (typeof value !== "object" || value === null) throw new Error("expected rendered CLI result");
|
||||
const text = (value as { renderedText?: unknown }).renderedText;
|
||||
if (typeof text !== "string") throw new Error("expected renderedText string");
|
||||
return text;
|
||||
}
|
||||
|
||||
function renderedContentTypeOf(value: unknown): string {
|
||||
if (typeof value !== "object" || value === null) throw new Error("expected rendered CLI result");
|
||||
const contentType = (value as { contentType?: unknown }).contentType;
|
||||
if (typeof contentType !== "string") throw new Error("expected contentType string");
|
||||
return contentType;
|
||||
}
|
||||
|
||||
const agentRunClientYaml = [
|
||||
"version: 1",
|
||||
"kind: AgentRunConfig",
|
||||
"metadata:",
|
||||
" name: agentrun",
|
||||
"manager:",
|
||||
" baseUrl: https://agentrun.127-0-0-1.nip.io/",
|
||||
" timeoutMs: 15000",
|
||||
@@ -32,9 +50,27 @@ const agentRunClientYaml = [
|
||||
"client:",
|
||||
" role: render-only",
|
||||
" transport: direct-http",
|
||||
" sessionPolicy:",
|
||||
" tenantId: unidesk",
|
||||
" projectId: test",
|
||||
" providerId: NC01",
|
||||
" backendProfile: codex",
|
||||
" workspaceRef:",
|
||||
" kind: opaque",
|
||||
" executionPolicy:",
|
||||
" sandbox: workspace-write",
|
||||
" approval: never",
|
||||
" timeoutMs: 900000",
|
||||
" network: enabled",
|
||||
" secretScope:",
|
||||
" allowCredentialEcho: false",
|
||||
].join("\n");
|
||||
|
||||
const agentRunMinimalClientYaml = [
|
||||
"version: 1",
|
||||
"kind: AgentRunConfig",
|
||||
"metadata:",
|
||||
" name: agentrun",
|
||||
"manager:",
|
||||
" baseUrl: http://agentrun.example.local:8080",
|
||||
" timeoutMs: 15000",
|
||||
@@ -46,6 +82,20 @@ const agentRunMinimalClientYaml = [
|
||||
"client:",
|
||||
" role: render-only",
|
||||
" transport: direct-http",
|
||||
" sessionPolicy:",
|
||||
" tenantId: unidesk",
|
||||
" projectId: test",
|
||||
" providerId: NC01",
|
||||
" backendProfile: codex",
|
||||
" workspaceRef:",
|
||||
" kind: opaque",
|
||||
" executionPolicy:",
|
||||
" sandbox: workspace-write",
|
||||
" approval: never",
|
||||
" timeoutMs: 900000",
|
||||
" network: enabled",
|
||||
" secretScope:",
|
||||
" allowCredentialEcho: false",
|
||||
].join("\n");
|
||||
|
||||
describe("AgentRun resource bridge argv", () => {
|
||||
@@ -114,7 +164,7 @@ describe("AgentRun render-only REST client config", () => {
|
||||
|
||||
describe("AgentRun default transport contract", () => {
|
||||
test("server-facing compatibility groups use REST dispatcher, not official SSH CLI wrapper", () => {
|
||||
const source = readFileSync(new URL("./agentrun.ts", import.meta.url), "utf8");
|
||||
const source = readFileSync(new URL("./agentrun/entry.ts", import.meta.url), "utf8");
|
||||
const commandRouter = source.slice(source.indexOf("export async function runAgentRunCommand"), source.indexOf("function isAgentRunRestCompatGroup"));
|
||||
expect(commandRouter).toContain("runAgentRunRestCompatCommand");
|
||||
expect(commandRouter).not.toContain("runOfficialAgentRunCli");
|
||||
@@ -163,6 +213,252 @@ describe("AgentRun default transport contract", () => {
|
||||
rmSync(tempConfigPath, { force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("retry dry-run is server-validated and attempts use the formal resource API", async () => {
|
||||
const taskId = "qt_failed_1";
|
||||
const attempt = {
|
||||
attemptId: "qat_retry_1",
|
||||
taskId,
|
||||
retryIndex: 1,
|
||||
state: "reserved",
|
||||
idempotencyKey: "retry-key-1",
|
||||
reason: "dependency repaired",
|
||||
payloadHash: "sha256:payload",
|
||||
previousAttemptId: "qat_initial_0",
|
||||
runId: "run_retry_1",
|
||||
commandId: "cmd_retry_1",
|
||||
runnerJobId: "rjob_retry_1",
|
||||
sessionId: "ses_retry_1",
|
||||
sessionPath: "/api/v1/sessions/ses_retry_1",
|
||||
failureKind: "infra-failed",
|
||||
failureMessage: "kubectl create runner job failed before materialization",
|
||||
createdAt: "2026-07-12T00:00:00.000Z",
|
||||
updatedAt: "2026-07-12T00:00:01.000Z",
|
||||
activatedAt: "2026-07-12T00:00:01.000Z",
|
||||
terminalAt: null,
|
||||
};
|
||||
let retryRequests = 0;
|
||||
let attemptRequests = 0;
|
||||
const server = Bun.serve({
|
||||
port: 0,
|
||||
async fetch(request) {
|
||||
const url = new URL(request.url);
|
||||
expect(request.headers.get("authorization")).toBe("Bearer secret-value");
|
||||
if (url.pathname === `/api/v1/queue/tasks/${taskId}/retry`) {
|
||||
retryRequests += 1;
|
||||
expect(request.method).toBe("POST");
|
||||
expect(await request.json()).toEqual({
|
||||
idempotencyKey: "retry-key-1",
|
||||
reason: "dependency repaired",
|
||||
dryRun: true,
|
||||
});
|
||||
return Response.json({
|
||||
ok: true,
|
||||
data: {
|
||||
action: "queue-retry",
|
||||
mutation: false,
|
||||
idempotentReplay: false,
|
||||
task: { id: taskId, state: "failed", queue: "commander", payloadHash: "sha256:payload" },
|
||||
attempt: null,
|
||||
previousAttempt: { ...attempt, attemptId: "qat_initial_0", retryIndex: 0, state: "failed", previousAttemptId: null },
|
||||
run: null,
|
||||
command: null,
|
||||
runnerJob: null,
|
||||
allowedTransition: { from: "failed", to: "running", retryIndex: 1, previousAttemptId: "qat_initial_0", payloadHash: "sha256:payload", valuesPrinted: false },
|
||||
pollActions: [],
|
||||
},
|
||||
});
|
||||
}
|
||||
if (url.pathname === `/api/v1/queue/tasks/${taskId}/attempts`) {
|
||||
attemptRequests += 1;
|
||||
expect(request.method).toBe("GET");
|
||||
expect(url.searchParams.get("cursor")).toBe("1");
|
||||
expect(url.searchParams.get("limit")).toBe("10");
|
||||
return Response.json({ ok: true, data: { taskId, items: [attempt], count: 1, cursor: null } });
|
||||
}
|
||||
return new Response("not found", { status: 404 });
|
||||
},
|
||||
});
|
||||
const previousConfig = process.env.AGENTRUN_CLIENT_CONFIG;
|
||||
const previousKey = process.env.HWLAB_API_KEY;
|
||||
process.env.HWLAB_API_KEY = "secret-value";
|
||||
const tempConfigPath = `/tmp/unidesk-agentrun-retry-test-${process.pid}-${Date.now()}.yaml`;
|
||||
try {
|
||||
process.env.AGENTRUN_CLIENT_CONFIG = tempConfigPath;
|
||||
await Bun.write(tempConfigPath, agentRunMinimalClientYaml.replace("http://agentrun.example.local:8080", server.url.href.replace(/\/$/u, "")));
|
||||
const retryResult = await runAgentRunCommand(null, ["retry", `task/${taskId}`, "--idempotency-key", "retry-key-1", "--reason", "dependency repaired", "--dry-run"]);
|
||||
expect(retryResult.ok).toBe(true);
|
||||
expect("renderedText" in retryResult).toBe(true);
|
||||
if ("renderedText" in retryResult) {
|
||||
expect(retryResult.renderedText).toContain(`Task retry validated: task/${taskId}`);
|
||||
expect(retryResult.renderedText).toContain("Transition: failed -> running retryIndex=1");
|
||||
expect(retryResult.renderedText).toContain("Attempt: planned");
|
||||
}
|
||||
|
||||
const attemptsResult = await runAgentRunCommand(null, ["get", "attempts", "--task", taskId, "--cursor", "1", "--limit", "10", "-o", "json"]);
|
||||
expect(attemptsResult.ok).toBe(true);
|
||||
expect("renderedText" in attemptsResult).toBe(true);
|
||||
if ("renderedText" in attemptsResult && typeof attemptsResult.renderedText === "string") {
|
||||
const payload = JSON.parse(attemptsResult.renderedText) as { kind?: string; taskId?: string; count?: number; items?: Array<Record<string, unknown>> };
|
||||
expect(payload.kind).toBe("AttemptList");
|
||||
expect(payload.taskId).toBe(taskId);
|
||||
expect(payload.count).toBe(1);
|
||||
expect(payload.items?.[0]?.name).toBe(attempt.attemptId);
|
||||
expect(payload.items?.[0]?.retryIndex).toBe(1);
|
||||
expect(payload.items?.[0]?.failureMessage).toBe(attempt.failureMessage);
|
||||
}
|
||||
|
||||
const attemptsHumanResult = await runAgentRunCommand(null, ["get", "attempts", "--task", taskId, "--cursor", "1", "--limit", "10"]);
|
||||
expect("renderedText" in attemptsHumanResult ? attemptsHumanResult.renderedText : "").toContain("infra-failed: kubectl create runner job failed before materialization");
|
||||
expect(retryRequests).toBe(1);
|
||||
expect(attemptRequests).toBe(2);
|
||||
} finally {
|
||||
server.stop(true);
|
||||
if (previousConfig === undefined) delete process.env.AGENTRUN_CLIENT_CONFIG;
|
||||
else process.env.AGENTRUN_CLIENT_CONFIG = previousConfig;
|
||||
if (previousKey === undefined) delete process.env.HWLAB_API_KEY;
|
||||
else process.env.HWLAB_API_KEY = previousKey;
|
||||
rmSync(tempConfigPath, { force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("retry and attempt resource help stays scoped to the controlled commands", async () => {
|
||||
const retryHelp = await runAgentRunCommand(null, ["retry", "--help"]);
|
||||
const attemptHelp = await runAgentRunCommand(null, ["get", "attempts", "--help"]);
|
||||
expect("renderedText" in retryHelp ? retryHelp.renderedText : "").toContain("agentrun retry task/<taskId> --idempotency-key <key> --reason <text>");
|
||||
expect("renderedText" in attemptHelp ? attemptHelp.renderedText : "").toContain("agentrun get attempts --task <taskId>");
|
||||
|
||||
const rootHelp = Bun.spawnSync(["bun", "scripts/cli.ts", "agentrun", "--help"], {
|
||||
cwd: new URL("../..", import.meta.url).pathname,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
const rootHelpText = rootHelp.stdout.toString();
|
||||
expect(rootHelp.exitCode).toBe(0);
|
||||
expect(rootHelpText).toContain("Verbs: get, describe, events, logs, result, ack, cancel, retry,");
|
||||
expect(rootHelpText).toContain("Resources: task/qt, attempt, run,");
|
||||
expect(rootHelpText).toContain("agentrun retry task/<taskId> --idempotency-key <key> --reason <text> --dry-run");
|
||||
expect(rootHelpText).toContain("agentrun get attempts --task <taskId> --limit 20");
|
||||
expect(rootHelpText.split("\n").length).toBeLessThan(40);
|
||||
});
|
||||
|
||||
test("local retry and attempt validation preserves bounded human and machine errors", async () => {
|
||||
const cases = [
|
||||
{
|
||||
args: ["retry", "task/qt_failed_1", "--reason", "dependency repaired"],
|
||||
message: "retry requires --idempotency-key <key>",
|
||||
},
|
||||
{
|
||||
args: ["retry", "task/qt_failed_1", "--idempotency-key", "retry-key-1"],
|
||||
message: "retry requires --reason <text>",
|
||||
},
|
||||
{
|
||||
args: ["get", "attempts"],
|
||||
message: "get attempts requires --task <taskId>",
|
||||
},
|
||||
{
|
||||
args: ["retry", "run/run_invalid", "--idempotency-key", "retry-key-1", "--reason", "dependency repaired"],
|
||||
message: "retry supports task/<taskId>",
|
||||
},
|
||||
{
|
||||
args: ["retry"],
|
||||
message: "retry requires task/<taskId>",
|
||||
},
|
||||
];
|
||||
for (const item of cases) {
|
||||
const human = await runAgentRunCommand(null, item.args);
|
||||
const humanText = renderedTextOf(human);
|
||||
expect(human.ok).toBe(false);
|
||||
expect(humanText).toContain("Error: validation-failed");
|
||||
expect(humanText).toContain(item.message);
|
||||
expect(humanText.length).toBeLessThan(1600);
|
||||
|
||||
for (const outputArgs of [["-o", "json"], ["--raw"]]) {
|
||||
const machine = await runAgentRunCommand(null, [...item.args, ...outputArgs]);
|
||||
const machineText = renderedTextOf(machine);
|
||||
const payload = JSON.parse(machineText) as { ok?: boolean; failureKind?: string; message?: string };
|
||||
expect(machine.ok).toBe(false);
|
||||
expect(renderedContentTypeOf(machine)).toBe("application/json");
|
||||
expect(payload.ok).toBe(false);
|
||||
expect(payload.failureKind).toBe("validation-failed");
|
||||
expect(payload.message).toBe(item.message);
|
||||
expect(machineText.length).toBeLessThan(2400);
|
||||
}
|
||||
}
|
||||
|
||||
for (const cliArgs of [
|
||||
["retry", "run/run_invalid", "--idempotency-key", "retry-key-1", "--reason", "dependency repaired", "-o", "json"],
|
||||
["get", "attempts", "--raw"],
|
||||
]) {
|
||||
const cli = Bun.spawnSync(["bun", "scripts/cli.ts", "agentrun", ...cliArgs], {
|
||||
cwd: new URL("../..", import.meta.url).pathname,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
const payload = JSON.parse(cli.stdout.toString()) as { ok?: boolean; failureKind?: string };
|
||||
expect(cli.exitCode).toBe(1);
|
||||
expect(payload.ok).toBe(false);
|
||||
expect(payload.failureKind).toBe("validation-failed");
|
||||
expect(cli.stdout.toString().length).toBeLessThan(2400);
|
||||
}
|
||||
});
|
||||
|
||||
test("server retry validation errors stay bounded in human and JSON output", async () => {
|
||||
const taskId = "qt_completed_1";
|
||||
const longMessage = `queue task ${taskId} cannot be retried from state completed: ${"x".repeat(6000)}`;
|
||||
const longDiagnostic = `server diagnostic: ${"y".repeat(9000)}`;
|
||||
let requests = 0;
|
||||
const server = Bun.serve({
|
||||
port: 0,
|
||||
async fetch(request) {
|
||||
const url = new URL(request.url);
|
||||
expect(url.pathname).toBe(`/api/v1/queue/tasks/${taskId}/retry`);
|
||||
expect(request.method).toBe("POST");
|
||||
expect(await request.json()).toEqual({ idempotencyKey: "retry-key-terminal", reason: "operator requested retry", dryRun: true });
|
||||
requests += 1;
|
||||
return Response.json({
|
||||
ok: false,
|
||||
failureKind: "schema-invalid",
|
||||
message: longMessage,
|
||||
details: { allowedStates: ["failed", "blocked"], diagnostic: longDiagnostic, valuesPrinted: false },
|
||||
valuesPrinted: false,
|
||||
}, { status: 409 });
|
||||
},
|
||||
});
|
||||
const previousConfig = process.env.AGENTRUN_CLIENT_CONFIG;
|
||||
const previousKey = process.env.HWLAB_API_KEY;
|
||||
process.env.HWLAB_API_KEY = "secret-value";
|
||||
const tempConfigPath = `/tmp/unidesk-agentrun-retry-error-test-${process.pid}-${Date.now()}.yaml`;
|
||||
try {
|
||||
process.env.AGENTRUN_CLIENT_CONFIG = tempConfigPath;
|
||||
await Bun.write(tempConfigPath, agentRunMinimalClientYaml.replace("http://agentrun.example.local:8080", server.url.href.replace(/\/$/u, "")));
|
||||
const baseArgs = ["retry", `task/${taskId}`, "--idempotency-key", "retry-key-terminal", "--reason", "operator requested retry", "--dry-run"];
|
||||
const human = await runAgentRunCommand(null, baseArgs);
|
||||
const humanText = renderedTextOf(human);
|
||||
expect(human.ok).toBe(false);
|
||||
expect(humanText).toContain(`queue task ${taskId} cannot be retried from state completed`);
|
||||
expect(humanText).not.toContain("x".repeat(500));
|
||||
expect(humanText.length).toBeLessThan(1800);
|
||||
|
||||
const json = await runAgentRunCommand(null, [...baseArgs, "-o", "json"]);
|
||||
const jsonText = renderedTextOf(json);
|
||||
const payload = JSON.parse(jsonText) as { ok?: boolean; failureKind?: string; message?: string; agentrun?: { details?: { diagnostic?: string } } };
|
||||
expect(json.ok).toBe(false);
|
||||
expect(renderedContentTypeOf(json)).toBe("application/json");
|
||||
expect(payload.failureKind).toBe("validation-failed");
|
||||
expect(payload.message?.length).toBeLessThanOrEqual(400);
|
||||
expect(payload.agentrun?.details?.diagnostic?.length).toBeLessThanOrEqual(400);
|
||||
expect(jsonText.length).toBeLessThan(5000);
|
||||
expect(requests).toBe(2);
|
||||
} finally {
|
||||
server.stop(true);
|
||||
if (previousConfig === undefined) delete process.env.AGENTRUN_CLIENT_CONFIG;
|
||||
else process.env.AGENTRUN_CLIENT_CONFIG = previousConfig;
|
||||
if (previousKey === undefined) delete process.env.HWLAB_API_KEY;
|
||||
else process.env.HWLAB_API_KEY = previousKey;
|
||||
rmSync(tempConfigPath, { force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("AgentRun runner API key SecretRef", () => {
|
||||
|
||||
@@ -49,7 +49,7 @@ import { cleanupLocalPostgres, cleanupReleasedPvs, cleanupRunners, cleanupRuns,
|
||||
|
||||
export function agentRunHelp(): unknown {
|
||||
return {
|
||||
command: "agentrun get|describe|events|logs|result|ack|cancel|dispatch|create|apply|send|explain",
|
||||
command: "agentrun get|describe|events|logs|result|ack|cancel|retry|dispatch|create|apply|send|explain",
|
||||
output: "human by default; use -o json|yaml or --raw for machine/debug output",
|
||||
usage: [
|
||||
"bun scripts/cli.ts agentrun get tasks --queue commander --limit 20",
|
||||
@@ -62,6 +62,8 @@ export function agentRunHelp(): unknown {
|
||||
"bun scripts/cli.ts agentrun result run/<runId> --command <commandId>",
|
||||
"bun scripts/cli.ts agentrun ack task/<taskId> --reader-id cli",
|
||||
"bun scripts/cli.ts agentrun cancel task/<taskId> --reason <text> --dry-run",
|
||||
"bun scripts/cli.ts agentrun retry task/<taskId> --idempotency-key <key> --reason <text> --dry-run",
|
||||
"bun scripts/cli.ts agentrun get attempts --task <taskId> --limit 20",
|
||||
"bun scripts/cli.ts agentrun dispatch task/<taskId>",
|
||||
"bun scripts/cli.ts agentrun create task --aipod Artificer --prompt-stdin --idempotency-key <key>",
|
||||
"bun scripts/cli.ts agentrun apply -f - --dry-run",
|
||||
@@ -93,9 +95,9 @@ export function agentRunHelp(): unknown {
|
||||
"bun scripts/cli.ts agentrun git-mirror status --full",
|
||||
"bun scripts/cli.ts agentrun git-mirror legacy-ops --help",
|
||||
],
|
||||
resources: ["task/qt", "run", "command/cmd", "runnerjob/rjob", "session/ses", "aipodspec/aps"],
|
||||
resources: ["task/qt", "attempt", "run", "command/cmd", "runnerjob/rjob", "session/ses", "aipodspec/aps"],
|
||||
description: "Operate AgentRun through Kubernetes-style resource verbs. Human output is compact by default; -o json|yaml returns the UniDesk render-only client schema, --raw exposes the REST envelope, and --node/--lane targets a YAML-declared runtime lane.",
|
||||
legacyCompatibility: "queue/runs/commands/runner/sessions/aipod-specs remain as compatibility groups backed by direct HTTP; new commander work should use get/describe/events/logs/result/ack/cancel/dispatch/create/apply/send. sessions turn/steer are removed; use send only.",
|
||||
legacyCompatibility: "queue/runs/commands/runner/sessions/aipod-specs remain as compatibility groups backed by direct HTTP; new commander work should use get/describe/events/logs/result/ack/cancel/retry/dispatch/create/apply/send. sessions turn/steer are removed; use send only.",
|
||||
cicdBoundary: "NC01 PaC consumer 仅由 GitHub PR merge 自动触发;默认帮助只给 status/history。legacy 与平台维护写入口只在显式 scoped help 中展示。",
|
||||
};
|
||||
}
|
||||
@@ -238,6 +240,7 @@ export function isResourceVerb(value: string | undefined): value is AgentRunReso
|
||||
|| value === "result"
|
||||
|| value === "ack"
|
||||
|| value === "cancel"
|
||||
|| value === "retry"
|
||||
|| value === "dispatch"
|
||||
|| value === "create"
|
||||
|| value === "apply"
|
||||
@@ -257,11 +260,12 @@ export function agentRunHelpText(args: string[]): string {
|
||||
return [
|
||||
"Usage: bun scripts/cli.ts agentrun get <resource> [options]",
|
||||
"",
|
||||
"Resources: tasks|sessions|runs|commands|runnerjobs|aipodspecs",
|
||||
"Resources: tasks|attempts|sessions|runs|commands|runnerjobs|aipodspecs",
|
||||
"Output: table by default; use -o wide|name|json|yaml.",
|
||||
"",
|
||||
"Examples:",
|
||||
" bun scripts/cli.ts agentrun get tasks --queue commander --limit 20",
|
||||
" bun scripts/cli.ts agentrun get attempts --task <taskId> --limit 20",
|
||||
" bun scripts/cli.ts agentrun get sessions --limit 20",
|
||||
" bun scripts/cli.ts agentrun get tasks -o json",
|
||||
].join("\n");
|
||||
@@ -293,6 +297,9 @@ export function agentRunHelpText(args: string[]): string {
|
||||
if (verb === "cancel") {
|
||||
return "Usage: bun scripts/cli.ts agentrun cancel task/<taskId>|session/<sessionId>|run/<runId>|command/<commandId> --reason <text> [--run <runId>] [--dry-run]";
|
||||
}
|
||||
if (verb === "retry") {
|
||||
return "Usage: bun scripts/cli.ts agentrun retry task/<taskId> --idempotency-key <key> --reason <text> [--dry-run] [--full] [-o json|yaml] [--raw]";
|
||||
}
|
||||
if (verb === "dispatch") {
|
||||
return "Usage: bun scripts/cli.ts agentrun dispatch task/<taskId>";
|
||||
}
|
||||
@@ -378,8 +385,8 @@ export function agentRunHelpText(args: string[]): string {
|
||||
return [
|
||||
"Usage: bun scripts/cli.ts agentrun <verb> <resource> [options]",
|
||||
"",
|
||||
"Verbs: get, describe, events, logs, result, ack, cancel, dispatch, create, apply, send, explain",
|
||||
"Resources: task/qt, run, command/cmd, runnerjob/rjob, session/ses, aipodspec/aps",
|
||||
"Verbs: get, describe, events, logs, result, ack, cancel, retry, dispatch, create, apply, send, explain",
|
||||
"Resources: task/qt, attempt, run, command/cmd, runnerjob/rjob, session/ses, aipodspec/aps",
|
||||
"",
|
||||
"Common:",
|
||||
" bun scripts/cli.ts agentrun get tasks --queue commander --limit 20",
|
||||
@@ -387,6 +394,8 @@ export function agentRunHelpText(args: string[]): string {
|
||||
" bun scripts/cli.ts agentrun describe run/<runId> --node NC01 --lane nc01-v02",
|
||||
" bun scripts/cli.ts agentrun events run/<runId> --after-seq 0 --limit 100",
|
||||
" bun scripts/cli.ts agentrun logs session/<sessionId> --tail 100",
|
||||
" bun scripts/cli.ts agentrun retry task/<taskId> --idempotency-key <key> --reason <text> --dry-run",
|
||||
" bun scripts/cli.ts agentrun get attempts --task <taskId> --limit 20",
|
||||
" bun scripts/cli.ts agentrun create task --aipod Artificer --prompt-stdin",
|
||||
"",
|
||||
"Machine/debug output:",
|
||||
|
||||
@@ -146,6 +146,125 @@ export function renderResourceResult(command: string, raw: Record<string, unknow
|
||||
return renderedCliResult(raw.ok !== false, command, renderResourceTable(items, options.output === "wide"));
|
||||
}
|
||||
|
||||
export function renderQueueAttemptList(command: string, raw: Record<string, unknown>, options: AgentRunResourceOptions, requestedTaskId: string, items: Record<string, unknown>[]): RenderedCliResult {
|
||||
if (options.raw) return renderMachine(command, raw, "json", raw.ok !== false);
|
||||
const data = record(innerData(raw));
|
||||
const taskId = stringOrNull(data.taskId) ?? requestedTaskId;
|
||||
const cursor = typeof data.cursor === "number" ? String(data.cursor) : stringOrNull(data.cursor);
|
||||
const rawItems = arrayRecords(data.items);
|
||||
if (options.full) {
|
||||
return renderMachine(command, {
|
||||
kind: "AttemptList",
|
||||
taskId,
|
||||
count: data.count ?? rawItems.length,
|
||||
cursor,
|
||||
items: rawItems,
|
||||
}, options.output === "yaml" ? "yaml" : "json", raw.ok !== false);
|
||||
}
|
||||
const payload = { kind: "AttemptList", taskId, count: items.length, cursor, items };
|
||||
if (options.output === "json" || options.output === "yaml") return renderMachine(command, payload, options.output, raw.ok !== false);
|
||||
if (options.output === "name") return renderedCliResult(raw.ok !== false, command, items.map((item) => `attempt/${String(item.name ?? "")}`).join("\n"));
|
||||
if (items.length === 0) return renderedCliResult(raw.ok !== false, command, `No attempt resources found for task/${taskId}.`);
|
||||
const lines = [
|
||||
`Attempts for task/${taskId}`,
|
||||
renderTable(["NAME", "INDEX", "STATE", "PREVIOUS", "RUN", "CMD", "RJOB", "AGE", "FAILURE", "REASON"], items.map((item) => [
|
||||
resourceName(item),
|
||||
displayValue(item.retryIndex),
|
||||
displayValue(item.state),
|
||||
shortId(stringOrDash(item.previousAttemptId)),
|
||||
shortId(stringOrDash(item.runId)),
|
||||
shortId(stringOrDash(item.commandId)),
|
||||
shortId(stringOrDash(item.runnerJobId)),
|
||||
displayValue(item.age),
|
||||
queueAttemptFailureSummary(item),
|
||||
truncateOneLine(displayValue(item.reason), 48),
|
||||
])),
|
||||
];
|
||||
if (cursor !== null) lines.push(`Next: ${nextQueueAttemptPageCommand(command, cursor, options.limit)}`);
|
||||
return renderedCliResult(raw.ok !== false, command, lines.join("\n"));
|
||||
}
|
||||
|
||||
export function renderQueueRetrySummary(command: string, raw: Record<string, unknown>, options: AgentRunResourceOptions, requestedTaskId: string): RenderedCliResult {
|
||||
if (options.raw) return renderMachine(command, raw, "json", raw.ok !== false);
|
||||
const data = record(innerData(raw));
|
||||
const task = record(data.task);
|
||||
const attempt = record(data.attempt);
|
||||
const previousAttempt = record(data.previousAttempt);
|
||||
const allowedTransition = record(data.allowedTransition);
|
||||
const taskId = stringOrNull(task.id) ?? requestedTaskId;
|
||||
if (options.full) {
|
||||
return renderMachine(command, { kind: "TaskRetry", taskId, ...data }, options.output === "yaml" ? "yaml" : "json", raw.ok !== false);
|
||||
}
|
||||
const normalizedAttempt = compactQueueRetryAttempt(attempt);
|
||||
const normalizedPreviousAttempt = compactQueueRetryAttempt(previousAttempt);
|
||||
const run = record(data.run);
|
||||
const runId = stringOrNull(run.id) ?? stringOrNull(attempt.runId);
|
||||
const commandResource = record(data.command);
|
||||
const commandId = stringOrNull(commandResource.id) ?? stringOrNull(attempt.commandId);
|
||||
const runnerJob = record(data.runnerJob);
|
||||
const runnerJobId = stringOrNull(runnerJob.id) ?? stringOrNull(attempt.runnerJobId);
|
||||
const payload = {
|
||||
kind: "TaskRetry",
|
||||
taskId,
|
||||
dryRun: options.dryRun,
|
||||
mutation: data.mutation === true,
|
||||
idempotentReplay: data.idempotentReplay === true,
|
||||
task: pickCompact(task, ["id", "state", "queue", "lane", "payloadHash", "updatedAt"]),
|
||||
attempt: normalizedAttempt,
|
||||
previousAttempt: normalizedPreviousAttempt,
|
||||
allowedTransition,
|
||||
run: compactQueueRetryIdentity(run, ["id", "status", "terminalStatus", "createdAt", "updatedAt"]),
|
||||
command: compactQueueRetryIdentity(commandResource, ["id", "type", "state", "terminalStatus", "createdAt", "updatedAt"]),
|
||||
runnerJob: compactQueueRetryIdentity(runnerJob, ["id", "state", "phase", "terminalStatus", "attemptId", "createdAt", "updatedAt"]),
|
||||
};
|
||||
if (options.output === "json" || options.output === "yaml") return renderMachine(command, payload, options.output, raw.ok !== false);
|
||||
const retryIndex = attempt.retryIndex ?? allowedTransition.retryIndex;
|
||||
const attemptId = stringOrNull(attempt.attemptId);
|
||||
const previousAttemptId = stringOrNull(previousAttempt.attemptId) ?? stringOrNull(allowedTransition.previousAttemptId);
|
||||
const lines = [
|
||||
`${options.dryRun ? "Task retry validated" : "Task retry submitted"}: task/${taskId}`,
|
||||
`OK: ${String(raw.ok !== false)}`,
|
||||
`DryRun: ${String(options.dryRun)}`,
|
||||
`Mutation: ${String(data.mutation === true)}`,
|
||||
`IdempotentReplay: ${String(data.idempotentReplay === true)}`,
|
||||
`Transition: ${displayValue(allowedTransition.from)} -> ${displayValue(allowedTransition.to)} retryIndex=${displayValue(retryIndex)}`,
|
||||
`Attempt: ${attemptId === null ? "planned" : `attempt/${shortId(attemptId)}`} state=${displayValue(attempt.state ?? allowedTransition.to)}`,
|
||||
`PreviousAttempt: ${previousAttemptId === null ? "-" : `attempt/${shortId(previousAttemptId)}`}`,
|
||||
];
|
||||
if (runId !== null) lines.push(`Run: run/${shortId(runId)}`);
|
||||
if (commandId !== null) lines.push(`Command: command/${shortId(commandId)}`);
|
||||
if (runnerJobId !== null) lines.push(`RunnerJob: runnerjob/${shortId(runnerJobId)}`);
|
||||
const next = [
|
||||
`bun scripts/cli.ts agentrun get attempts --task ${taskId}`,
|
||||
`bun scripts/cli.ts agentrun describe task/${taskId}`,
|
||||
runId === null ? null : `bun scripts/cli.ts agentrun events run/${runId} --after-seq 0 --limit 100`,
|
||||
runId === null || commandId === null ? null : `bun scripts/cli.ts agentrun describe command/${commandId} --run ${runId}`,
|
||||
].filter((value): value is string => value !== null);
|
||||
lines.push("", "Next:", ...next.map((value) => ` ${value}`));
|
||||
return renderedCliResult(raw.ok !== false, command, lines.join("\n"));
|
||||
}
|
||||
|
||||
function compactQueueRetryAttempt(value: Record<string, unknown>): Record<string, unknown> | null {
|
||||
if (Object.keys(value).length === 0) return null;
|
||||
const attempt = pickCompact(value, ["attemptId", "taskId", "retryIndex", "state", "idempotencyKey", "reason", "payloadHash", "previousAttemptId", "runId", "commandId", "runnerJobId", "sessionId", "sessionPath", "failureKind", "failureMessage", "createdAt", "updatedAt", "activatedAt", "terminalAt"]);
|
||||
attempt.reason = boundedQueueAttemptText(attempt.reason, 240);
|
||||
attempt.failureMessage = boundedQueueAttemptText(attempt.failureMessage, 320);
|
||||
attempt.sessionPath = boundedQueueAttemptText(attempt.sessionPath, 240);
|
||||
return attempt;
|
||||
}
|
||||
|
||||
function compactQueueRetryIdentity(value: Record<string, unknown>, keys: string[]): Record<string, unknown> | null {
|
||||
return Object.keys(value).length === 0 ? null : pickCompact(value, keys);
|
||||
}
|
||||
|
||||
function nextQueueAttemptPageCommand(command: string, cursor: string, limit: number): string {
|
||||
const base = command
|
||||
.replace(/\s+--cursor(?:=|\s+)\S+/gu, "")
|
||||
.replace(/\s+--limit(?:=|\s+)\S+/gu, "")
|
||||
.trim();
|
||||
return `bun scripts/cli.ts ${base} --cursor ${cursor} --limit ${limit}`;
|
||||
}
|
||||
|
||||
export function renderDescribe(command: string, raw: Record<string, unknown>, options: AgentRunResourceOptions, ref: AgentRunResourceRef, text: string): RenderedCliResult {
|
||||
if (options.raw) return renderMachine(command, raw, "json", raw.ok !== false);
|
||||
if (options.output === "json" || options.output === "yaml") {
|
||||
@@ -355,6 +474,7 @@ export function parseResourceKind(raw: string | undefined): AgentRunResourceKind
|
||||
if (raw === undefined) return null;
|
||||
const value = raw.toLowerCase().replace(/s$/u, "");
|
||||
if (value === "task" || value === "qt" || value === "queue" || value === "queuetask") return "task";
|
||||
if (value === "attempt") return "attempt";
|
||||
if (value === "run") return "run";
|
||||
if (value === "command" || value === "cmd") return "command";
|
||||
if (value === "runnerjob" || value === "runner-job" || value === "rjob" || value === "job") return "runnerjob";
|
||||
@@ -432,6 +552,46 @@ export function normalizeTaskItems(data: unknown, options: AgentRunResourceOptio
|
||||
});
|
||||
}
|
||||
|
||||
export function normalizeQueueAttemptItems(data: unknown): Record<string, unknown>[] {
|
||||
const items = arrayRecords(record(data).items ?? data);
|
||||
return items.map((item) => ({
|
||||
kind: "attempt",
|
||||
name: item.attemptId,
|
||||
attemptId: item.attemptId,
|
||||
taskId: item.taskId,
|
||||
retryIndex: item.retryIndex,
|
||||
state: item.state,
|
||||
idempotencyKey: item.idempotencyKey,
|
||||
reason: boundedQueueAttemptText(item.reason, 240),
|
||||
payloadHash: item.payloadHash,
|
||||
previousAttemptId: item.previousAttemptId,
|
||||
runId: item.runId,
|
||||
commandId: item.commandId,
|
||||
runnerJobId: item.runnerJobId,
|
||||
sessionId: item.sessionId,
|
||||
sessionPath: boundedQueueAttemptText(item.sessionPath, 240),
|
||||
failureKind: item.failureKind,
|
||||
failureMessage: boundedQueueAttemptText(item.failureMessage, 320),
|
||||
createdAt: item.createdAt,
|
||||
updatedAt: item.updatedAt,
|
||||
activatedAt: item.activatedAt,
|
||||
terminalAt: item.terminalAt,
|
||||
age: relativeAge(stringOrNull(item.updatedAt) ?? stringOrNull(item.createdAt)),
|
||||
}));
|
||||
}
|
||||
|
||||
function boundedQueueAttemptText(value: unknown, maxChars: number): string | null {
|
||||
const text = stringOrNull(value);
|
||||
return text === null ? null : truncateOneLine(text, maxChars);
|
||||
}
|
||||
|
||||
function queueAttemptFailureSummary(item: Record<string, unknown>): string {
|
||||
const kind = stringOrNull(item.failureKind);
|
||||
const message = stringOrNull(item.failureMessage);
|
||||
if (kind === null && message === null) return "-";
|
||||
return truncateOneLine(kind === null ? message ?? "-" : message === null ? kind : `${kind}: ${message}`, 72);
|
||||
}
|
||||
|
||||
export function taskListState(options: AgentRunResourceOptions): string {
|
||||
const requested = options.state?.split(",").map((item) => item.trim()).filter((item) => item.length > 0);
|
||||
return requested?.[0] ?? "running";
|
||||
|
||||
@@ -37,7 +37,7 @@ import type { AgentRunResourceOptions, AgentRunResourceRef, AgentRunResourceVerb
|
||||
import { agentRunDryRunPlan } from "./config";
|
||||
import { isHelpArg, renderAgentRunHelp } from "./entry";
|
||||
import { agentRunExplain, arrayRecords, shortId } from "./options";
|
||||
import { agentRunCancelAuthorityDisclosure, agentRunCancelCascadeScope, agentRunCancelFencingDisclosure, agentRunCancelRunnerAbortDisclosure, compactTaskDescriptionPayload, innerData, normalizeAipodSpecItems, normalizeAttemptResources, normalizeEventItems, normalizeLogItems, normalizeRunnerJobItems, normalizeSessionItems, normalizeSingleCommand, normalizeTaskItems, parseResourceKind, parseResourceRef, renderDescribe, renderEventLike, renderGenericDescription, renderMachine, renderMutationSummary, renderResourceResult, renderResultSummary, renderTaskDescription, renderedCliResult, requiredContext, rerunWithoutDryRun, resolveAgentRunCancelPolicyTarget, resourceApply, resourceCreate, resourceDispatch, resourceSessionPromptCommand, taskListState, unwrapTaskDetail } from "./render";
|
||||
import { agentRunCancelAuthorityDisclosure, agentRunCancelCascadeScope, agentRunCancelFencingDisclosure, agentRunCancelRunnerAbortDisclosure, compactTaskDescriptionPayload, innerData, normalizeAipodSpecItems, normalizeAttemptResources, normalizeEventItems, normalizeLogItems, normalizeQueueAttemptItems, normalizeRunnerJobItems, normalizeSessionItems, normalizeSingleCommand, normalizeTaskItems, parseResourceKind, parseResourceRef, renderDescribe, renderEventLike, renderGenericDescription, renderMachine, renderMutationSummary, renderQueueAttemptList, renderQueueRetrySummary, renderResourceResult, renderResultSummary, renderTaskDescription, renderedCliResult, requiredContext, rerunWithoutDryRun, resolveAgentRunCancelPolicyTarget, resourceApply, resourceCreate, resourceDispatch, resourceSessionPromptCommand, taskListState, unwrapTaskDetail } from "./render";
|
||||
import { AgentRunRestError, renderAgentRunRestError, resolveAgentRunRestTarget, runAgentRunRestCommand, withAgentRunRestTarget } from "./rest-bridge";
|
||||
import { runnerJobObservationScript, type RunnerJobObservationIdentity } from "./runner-observation";
|
||||
import { capture, captureJsonPayload, compactCapture, nonNegativeIntegerOrNull, record, stringOrNull } from "./utils";
|
||||
@@ -45,20 +45,29 @@ import { capture, captureJsonPayload, compactCapture, nonNegativeIntegerOrNull,
|
||||
export function agentRunGetKindHelp(kindRaw: string): string {
|
||||
const kind = parseResourceKind(kindRaw);
|
||||
if (kind === "task") return "Usage: bun scripts/cli.ts agentrun get tasks [--queue commander] [--state running,pending,completed,failed] [--unread] [--limit 20] [-o wide|name|json|yaml]";
|
||||
if (kind === "attempt") return "Usage: bun scripts/cli.ts agentrun get attempts --task <taskId> [--cursor <retry-index>] [--limit 20] [--full] [-o json|yaml] [--raw]";
|
||||
if (kind === "session") return "Usage: bun scripts/cli.ts agentrun get sessions [--limit 20] [-o wide|name|json|yaml]";
|
||||
if (kind === "run") return "Usage: bun scripts/cli.ts agentrun get runs --task <taskId> [--limit 20] [-o wide|name|json|yaml]";
|
||||
if (kind === "command") return "Usage: bun scripts/cli.ts agentrun get commands --run <runId> [--command <commandId>] [-o wide|name|json|yaml]";
|
||||
if (kind === "runnerjob") return "Usage: bun scripts/cli.ts agentrun get runnerjobs --run <runId> --command <commandId> [-o wide|name|json|yaml]";
|
||||
if (kind === "aipodspec") return "Usage: bun scripts/cli.ts agentrun get aipodspecs [-o wide|name|json|yaml]";
|
||||
return "Unknown resource. Supported: tasks, sessions, runs, commands, runnerjobs, aipodspecs.";
|
||||
return "Unknown resource. Supported: tasks, attempts, sessions, runs, commands, runnerjobs, aipodspecs.";
|
||||
}
|
||||
|
||||
export async function runAgentRunResourceCommand(config: UniDeskConfig | null, verb: AgentRunResourceVerb, action: string | undefined, actionArgs: string[], canonicalArgs: string[]): Promise<RenderedCliResult> {
|
||||
if (isHelpArg(action) || actionArgs.some(isHelpArg)) return renderAgentRunHelp(canonicalArgs);
|
||||
const resourceArgs = action === undefined ? actionArgs : [action, ...actionArgs];
|
||||
const options = parseResourceOptions(resourceArgs);
|
||||
const bridgeActionArgs = stripAgentRunResourceWrapperArgs(actionArgs);
|
||||
const command = `agentrun ${canonicalArgs.join(" ")}`.trim();
|
||||
let options: AgentRunResourceOptions;
|
||||
try {
|
||||
options = parseResourceOptions(resourceArgs);
|
||||
} catch (error) {
|
||||
const validationError = error instanceof AgentRunRestError
|
||||
? error
|
||||
: new AgentRunRestError("validation-failed", error instanceof Error ? error.message : String(error));
|
||||
return renderAgentRunRestError(command, validationError, resourceErrorOutputOptions(resourceArgs));
|
||||
}
|
||||
const bridgeActionArgs = stripAgentRunResourceWrapperArgs(actionArgs);
|
||||
try {
|
||||
return await withAgentRunRestTarget(resolveAgentRunRestTarget(config, options), async () => {
|
||||
if (verb === "explain") return renderedCliResult(true, command, agentRunExplain(action ?? "task", resourceArgs, options));
|
||||
@@ -69,6 +78,7 @@ export async function runAgentRunResourceCommand(config: UniDeskConfig | null, v
|
||||
if (verb === "result") return await resourceResult(config, command, action, bridgeActionArgs, options);
|
||||
if (verb === "ack") return await resourceAck(config, command, action, bridgeActionArgs, options);
|
||||
if (verb === "cancel") return await resourceCancel(config, command, action, bridgeActionArgs, options);
|
||||
if (verb === "retry") return await resourceRetry(config, command, action, bridgeActionArgs, options);
|
||||
if (verb === "dispatch") return await resourceDispatch(config, command, action, bridgeActionArgs, options);
|
||||
if (verb === "create") return await resourceCreate(config, command, action, bridgeActionArgs, options);
|
||||
if (verb === "apply") return await resourceApply(config, command, bridgeActionArgs, options);
|
||||
@@ -82,6 +92,21 @@ export async function runAgentRunResourceCommand(config: UniDeskConfig | null, v
|
||||
return renderedCliResult(false, command, `Unsupported AgentRun resource command. Try: bun scripts/cli.ts agentrun --help`);
|
||||
}
|
||||
|
||||
function resourceErrorOutputOptions(args: string[]): AgentRunResourceOptions {
|
||||
const options = parseResourceOptions([]);
|
||||
options.raw = args.includes("--raw");
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const arg = args[index] ?? "";
|
||||
const value = arg === "-o" || arg === "--output"
|
||||
? args[index + 1]
|
||||
: arg.startsWith("--output=") || arg.startsWith("-o=")
|
||||
? arg.slice(arg.indexOf("=") + 1)
|
||||
: null;
|
||||
if (value === "json" || value === "yaml") options.output = value;
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
export function stripAgentRunResourceWrapperArgs(args: string[]): string[] {
|
||||
const result: string[] = [];
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
@@ -109,6 +134,7 @@ export function parseResourceOptions(args: string[]): AgentRunResourceOptions {
|
||||
raw: false,
|
||||
debug: false,
|
||||
limit: 20,
|
||||
cursor: null,
|
||||
queue: null,
|
||||
state: null,
|
||||
unread: false,
|
||||
@@ -130,7 +156,7 @@ export function parseResourceOptions(args: string[]): AgentRunResourceOptions {
|
||||
lane: null,
|
||||
passthroughArgs: [],
|
||||
};
|
||||
const valueFlags = new Set(["-o", "--output", "--limit", "--queue", "--state", "--reader-id", "--task", "--task-id", "--run", "--run-id", "--command", "--command-id", "--session", "--session-id", "--after-seq", "--tail", "--reason", "-f", "--file", "--filename", "--aipod", "--idempotency-key", "--node", "--lane"]);
|
||||
const valueFlags = new Set(["-o", "--output", "--limit", "--cursor", "--queue", "--state", "--reader-id", "--task", "--task-id", "--run", "--run-id", "--command", "--command-id", "--session", "--session-id", "--after-seq", "--tail", "--reason", "-f", "--file", "--filename", "--aipod", "--idempotency-key", "--node", "--lane"]);
|
||||
const booleanFlags = new Set(["--full", "--raw", "--debug", "--unread", "--dry-run", "--full-text", "--prompt-stdin", "--stdin"]);
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const arg = args[index] ?? "";
|
||||
@@ -169,6 +195,7 @@ export function applyResourceOption(options: AgentRunResourceOptions, flag: stri
|
||||
else if (flag === "--full-text") options.fullText = true;
|
||||
else if (flag === "--prompt-stdin" || flag === "--stdin") options.promptStdin = true;
|
||||
else if (flag === "--limit") options.limit = parseNonNegativeInt(value, "--limit", 20, 500);
|
||||
else if (flag === "--cursor") options.cursor = parseNonNegativeInt(value, "--cursor", 0, Number.MAX_SAFE_INTEGER);
|
||||
else if (flag === "--queue") options.queue = requiredValue(value, flag);
|
||||
else if (flag === "--state") options.state = requiredValue(value, flag);
|
||||
else if (flag === "--reader-id") options.readerId = requiredValue(value, flag);
|
||||
@@ -200,7 +227,7 @@ export function requiredValue(value: string | null, flag: string): string {
|
||||
|
||||
export async function resourceGet(config: UniDeskConfig | null, command: string, action: string | undefined, args: string[], options: AgentRunResourceOptions): Promise<RenderedCliResult> {
|
||||
const kind = parseResourceKind(action);
|
||||
if (kind === null) throw new Error("get requires a resource: tasks|sessions|runs|commands|runnerjobs|aipodspecs");
|
||||
if (kind === null) throw new Error("get requires a resource: tasks|attempts|sessions|runs|commands|runnerjobs|aipodspecs");
|
||||
let result: Record<string, unknown>;
|
||||
if (kind === "task") {
|
||||
const taskListArgs = options.unread
|
||||
@@ -220,6 +247,17 @@ export async function resourceGet(config: UniDeskConfig | null, command: string,
|
||||
result = await runAgentRunRestCommand(config, "sessions", ["ps", "--limit", String(options.limit)]);
|
||||
return renderResourceResult(command, result, options, "Session", normalizeSessionItems(innerData(result)).slice(0, options.limit));
|
||||
}
|
||||
if (kind === "attempt") {
|
||||
if (options.taskId === null) throw new AgentRunRestError("validation-failed", "get attempts requires --task <taskId>");
|
||||
result = await runAgentRunRestCommand(config, "queue", [
|
||||
"attempts",
|
||||
options.taskId,
|
||||
...(options.cursor === null ? [] : ["--cursor", String(options.cursor)]),
|
||||
"--limit",
|
||||
String(options.limit),
|
||||
]);
|
||||
return renderQueueAttemptList(command, result, options, options.taskId, normalizeQueueAttemptItems(innerData(result)).slice(0, options.limit));
|
||||
}
|
||||
if (kind === "aipodspec") {
|
||||
result = await runAgentRunRestCommand(config, "aipod-specs", ["list"]);
|
||||
return renderResourceResult(command, result, options, "AipodSpec", normalizeAipodSpecItems(innerData(result)).slice(0, options.limit));
|
||||
@@ -240,6 +278,29 @@ export async function resourceGet(config: UniDeskConfig | null, command: string,
|
||||
throw new Error(`get ${kind}s requires more context; use --task, --run/--command, or describe <kind>/<id>`);
|
||||
}
|
||||
|
||||
export async function resourceRetry(config: UniDeskConfig | null, command: string, action: string | undefined, args: string[], options: AgentRunResourceOptions): Promise<RenderedCliResult> {
|
||||
if (action === undefined || action.startsWith("-")) throw new AgentRunRestError("validation-failed", "retry requires task/<taskId>");
|
||||
let ref: AgentRunResourceRef;
|
||||
try {
|
||||
ref = parseResourceRef(action, args, "task");
|
||||
} catch {
|
||||
throw new AgentRunRestError("validation-failed", "retry requires task/<taskId>");
|
||||
}
|
||||
if (ref.kind !== "task") throw new AgentRunRestError("validation-failed", "retry supports task/<taskId>");
|
||||
if (options.idempotencyKey === null) throw new AgentRunRestError("validation-failed", "retry requires --idempotency-key <key>");
|
||||
if (options.reason === null) throw new AgentRunRestError("validation-failed", "retry requires --reason <text>");
|
||||
const result = await runAgentRunRestCommand(config, "queue", [
|
||||
"retry",
|
||||
ref.name,
|
||||
"--idempotency-key",
|
||||
options.idempotencyKey,
|
||||
"--reason",
|
||||
options.reason,
|
||||
...(options.dryRun ? ["--dry-run"] : []),
|
||||
]);
|
||||
return renderQueueRetrySummary(command, result, options, ref.name);
|
||||
}
|
||||
|
||||
export async function resourceDescribe(config: UniDeskConfig | null, command: string, action: string | undefined, args: string[], options: AgentRunResourceOptions): Promise<RenderedCliResult> {
|
||||
const ref = parseResourceRef(action, args);
|
||||
if (ref.kind === "task") {
|
||||
|
||||
@@ -482,9 +482,11 @@ export async function runAgentRunQueueRest(action: string | undefined, id: strin
|
||||
if (action === "list") return await agentRunRestRequest("agentrun queue list", "GET", `/api/v1/queue/tasks${agentRunQuery(args, ["queue", "state", "cursor", "limit", "updated-after"])}`);
|
||||
if (action === "commander") return await agentRunRestRequest("agentrun queue commander", "GET", `/api/v1/queue/commander${agentRunQuery(args, ["queue", "reader-id"])}`);
|
||||
if (action === "stats") return await agentRunRestRequest("agentrun queue stats", "GET", `/api/v1/queue/stats${agentRunQuery(args, ["queue"])}`);
|
||||
if (action === "attempts" && id) return await agentRunRestRequest("agentrun queue attempts", "GET", `/api/v1/queue/tasks/${encodeURIComponent(id)}/attempts${agentRunQuery(args, ["cursor", "limit"])}`);
|
||||
if (action === "show" && id) return await agentRunRestRequest("agentrun queue show", "GET", `/api/v1/queue/tasks/${encodeURIComponent(id)}`);
|
||||
if (action === "submit") return await submitQueueTaskRest(args);
|
||||
if (action === "dispatch" && id) return await mutateQueueTaskRest("queue-dispatch", id, "dispatch", queueDispatchBodyFromArgs(args), args);
|
||||
if (action === "retry" && id) return await retryQueueTaskRest(id, args);
|
||||
if (action === "read" && id) return await mutateQueueTaskRest("queue-read", id, "read", { readerId: agentRunOption(args, "reader-id") ?? "cli" }, args);
|
||||
if (action === "cancel" && id) return await mutateQueueTaskRest("queue-cancel", id, "cancel", cancelBodyFromArgs(args), args);
|
||||
if (action === "refresh" && id) return await mutateQueueTaskRest("queue-refresh", id, "refresh", {}, args);
|
||||
@@ -603,6 +605,16 @@ export async function mutateQueueTaskRest(action: string, taskId: string, suffix
|
||||
return await agentRunRestRequest(`agentrun queue ${suffix}`, "POST", pathValue, body);
|
||||
}
|
||||
|
||||
export async function retryQueueTaskRest(taskId: string, args: string[]): Promise<Record<string, unknown>> {
|
||||
const idempotencyKey = requiredAgentRunOption(args, ["idempotency-key"], "queue retry requires --idempotency-key");
|
||||
const reason = requiredAgentRunOption(args, ["reason"], "queue retry requires --reason");
|
||||
return await agentRunRestRequest("agentrun queue retry", "POST", `/api/v1/queue/tasks/${encodeURIComponent(taskId)}/retry`, {
|
||||
idempotencyKey,
|
||||
reason,
|
||||
dryRun: agentRunHasFlag(args, "dry-run"),
|
||||
});
|
||||
}
|
||||
|
||||
export async function sessionSendRest(sessionId: string, args: string[]): Promise<Record<string, unknown>> {
|
||||
const aipod = agentRunOption(args, "aipod") ?? agentRunOption(args, "aipod-spec");
|
||||
if (aipod) return await sessionSendWithAipodRest(sessionId, aipod, args);
|
||||
@@ -1047,8 +1059,10 @@ export async function withAgentRunRestTarget<T>(target: AgentRunRestTarget | nul
|
||||
}
|
||||
|
||||
export function renderAgentRunRestError(command: string, error: AgentRunRestError, options: AgentRunResourceOptions): RenderedCliResult {
|
||||
const payload = error.toPayload(command);
|
||||
if (options.raw || options.output === "json" || options.output === "yaml") return renderMachine(command, payload, options.output === "yaml" ? "yaml" : "json", false);
|
||||
const rawPayload = error.toPayload(command);
|
||||
if (options.raw) return renderMachine(command, rawPayload, "json", false);
|
||||
const payload = boundedAgentRunRestErrorPayload(rawPayload);
|
||||
if (options.output === "json" || options.output === "yaml") return renderMachine(command, payload, options.output === "yaml" ? "yaml" : "json", false);
|
||||
const laneHint = record(record(payload.agentrun).laneAwareLookup);
|
||||
const currentEndpoint = record(laneHint.currentEndpoint);
|
||||
const nextCommands = Array.isArray(laneHint.nextCommands)
|
||||
@@ -1063,12 +1077,18 @@ export function renderAgentRunRestError(command: string, error: AgentRunRestErro
|
||||
` Config: ${String(currentEndpoint.configPath ?? "(unknown)")}`,
|
||||
` ${String(laneHint.summary ?? "The resource was not found on the currently configured AgentRun manager endpoint.")}`,
|
||||
];
|
||||
const validationHelp = `bun scripts/cli.ts agentrun ${command.split(/\s+/u)[1] ?? "--help"} --help`;
|
||||
const nextLines = nextCommands.length > 0
|
||||
? nextCommands
|
||||
: [
|
||||
"Check config/agentrun.yaml manager.baseUrl and the AgentRun API route.",
|
||||
"Verify HWLAB_API_KEY is present in env or in the configured auth.file without printing the key.",
|
||||
];
|
||||
: error.failureKind === "validation-failed"
|
||||
? [
|
||||
"Review the AgentRun validation message and correct the request; do not bypass the formal resource API.",
|
||||
validationHelp,
|
||||
]
|
||||
: [
|
||||
"Check config/agentrun.yaml manager.baseUrl and the AgentRun API route.",
|
||||
"Verify HWLAB_API_KEY is present in env or in the configured auth.file without printing the key.",
|
||||
];
|
||||
return renderedCliResult(false, command, [
|
||||
`Error: ${payload.failureKind}`,
|
||||
String(payload.message ?? ""),
|
||||
@@ -1079,6 +1099,23 @@ export function renderAgentRunRestError(command: string, error: AgentRunRestErro
|
||||
].join("\n"));
|
||||
}
|
||||
|
||||
export function boundedAgentRunRestErrorPayload(payload: Record<string, unknown>): Record<string, unknown> {
|
||||
return boundedAgentRunRestErrorValue(payload, 0) as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function boundedAgentRunRestErrorValue(value: unknown, depth: number): unknown {
|
||||
if (typeof value === "string") return truncateOneLine(value, 400);
|
||||
if (value === null || typeof value === "number" || typeof value === "boolean") return value;
|
||||
if (Array.isArray(value)) return value.slice(0, 12).map((item) => boundedAgentRunRestErrorValue(item, depth + 1));
|
||||
if (typeof value !== "object") return String(value);
|
||||
if (depth >= 5) return "[nested details omitted]";
|
||||
const entries = Object.entries(value as Record<string, unknown>);
|
||||
const result: Record<string, unknown> = {};
|
||||
for (const [key, item] of entries.slice(0, 20)) result[key] = boundedAgentRunRestErrorValue(item, depth + 1);
|
||||
if (entries.length > 20) result.omittedFieldCount = entries.length - 20;
|
||||
return result;
|
||||
}
|
||||
|
||||
export class AgentRunRestError extends Error {
|
||||
readonly failureKind: AgentRunFailureKind;
|
||||
readonly bridge: Record<string, unknown> | null;
|
||||
|
||||
@@ -48,9 +48,9 @@ export type AgentRunFailureKind = "auth-missing" | "auth-failed" | "agentrun-con
|
||||
|
||||
export type AgentRunRestCompatGroup = "queue" | "sessions" | "aipod-specs" | "aipods" | "runs" | "commands" | "runner";
|
||||
|
||||
export type AgentRunResourceVerb = "get" | "describe" | "events" | "logs" | "result" | "ack" | "cancel" | "dispatch" | "create" | "apply" | "send" | "explain";
|
||||
export type AgentRunResourceVerb = "get" | "describe" | "events" | "logs" | "result" | "ack" | "cancel" | "retry" | "dispatch" | "create" | "apply" | "send" | "explain";
|
||||
|
||||
export type AgentRunResourceKind = "task" | "run" | "command" | "runnerjob" | "session" | "aipodspec";
|
||||
export type AgentRunResourceKind = "task" | "attempt" | "run" | "command" | "runnerjob" | "session" | "aipodspec";
|
||||
|
||||
export type AgentRunOutputMode = "human" | "wide" | "name" | "json" | "yaml";
|
||||
|
||||
@@ -65,6 +65,7 @@ export interface AgentRunResourceOptions {
|
||||
raw: boolean;
|
||||
debug: boolean;
|
||||
limit: number;
|
||||
cursor: number | null;
|
||||
queue: string | null;
|
||||
state: string | null;
|
||||
unread: boolean;
|
||||
|
||||
Reference in New Issue
Block a user