diff --git a/scripts/src/agentrun.test.ts b/scripts/src/agentrun.test.ts index f01a17cf..d3060e50 100644 --- a/scripts/src/agentrun.test.ts +++ b/scripts/src/agentrun.test.ts @@ -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> }; + 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/ --idempotency-key --reason "); + expect("renderedText" in attemptHelp ? attemptHelp.renderedText : "").toContain("agentrun get attempts --task "); + + 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/ --idempotency-key --reason --dry-run"); + expect(rootHelpText).toContain("agentrun get attempts --task --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 ", + }, + { + args: ["retry", "task/qt_failed_1", "--idempotency-key", "retry-key-1"], + message: "retry requires --reason ", + }, + { + args: ["get", "attempts"], + message: "get attempts requires --task ", + }, + { + args: ["retry", "run/run_invalid", "--idempotency-key", "retry-key-1", "--reason", "dependency repaired"], + message: "retry supports task/", + }, + { + args: ["retry"], + message: "retry requires task/", + }, + ]; + 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", () => { diff --git a/scripts/src/agentrun/entry.ts b/scripts/src/agentrun/entry.ts index d63b56b1..21a2851a 100644 --- a/scripts/src/agentrun/entry.ts +++ b/scripts/src/agentrun/entry.ts @@ -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/ --command ", "bun scripts/cli.ts agentrun ack task/ --reader-id cli", "bun scripts/cli.ts agentrun cancel task/ --reason --dry-run", + "bun scripts/cli.ts agentrun retry task/ --idempotency-key --reason --dry-run", + "bun scripts/cli.ts agentrun get attempts --task --limit 20", "bun scripts/cli.ts agentrun dispatch task/", "bun scripts/cli.ts agentrun create task --aipod Artificer --prompt-stdin --idempotency-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 [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 --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/|session/|run/|command/ --reason [--run ] [--dry-run]"; } + if (verb === "retry") { + return "Usage: bun scripts/cli.ts agentrun retry task/ --idempotency-key --reason [--dry-run] [--full] [-o json|yaml] [--raw]"; + } if (verb === "dispatch") { return "Usage: bun scripts/cli.ts agentrun dispatch task/"; } @@ -378,8 +385,8 @@ export function agentRunHelpText(args: string[]): string { return [ "Usage: bun scripts/cli.ts agentrun [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/ --node NC01 --lane nc01-v02", " bun scripts/cli.ts agentrun events run/ --after-seq 0 --limit 100", " bun scripts/cli.ts agentrun logs session/ --tail 100", + " bun scripts/cli.ts agentrun retry task/ --idempotency-key --reason --dry-run", + " bun scripts/cli.ts agentrun get attempts --task --limit 20", " bun scripts/cli.ts agentrun create task --aipod Artificer --prompt-stdin", "", "Machine/debug output:", diff --git a/scripts/src/agentrun/render.ts b/scripts/src/agentrun/render.ts index 976e2e20..79f83014 100644 --- a/scripts/src/agentrun/render.ts +++ b/scripts/src/agentrun/render.ts @@ -146,6 +146,125 @@ export function renderResourceResult(command: string, raw: Record, options: AgentRunResourceOptions, requestedTaskId: string, items: Record[]): 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, 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): Record | 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, keys: string[]): Record | 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, 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[] { + 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 { + 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"; diff --git a/scripts/src/agentrun/resource-actions.ts b/scripts/src/agentrun/resource-actions.ts index 17783399..facefa15 100644 --- a/scripts/src/agentrun/resource-actions.ts +++ b/scripts/src/agentrun/resource-actions.ts @@ -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 [--cursor ] [--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 [--limit 20] [-o wide|name|json|yaml]"; if (kind === "command") return "Usage: bun scripts/cli.ts agentrun get commands --run [--command ] [-o wide|name|json|yaml]"; if (kind === "runnerjob") return "Usage: bun scripts/cli.ts agentrun get runnerjobs --run --command [-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 { 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 { 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; 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 "); + 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 /`); } +export async function resourceRetry(config: UniDeskConfig | null, command: string, action: string | undefined, args: string[], options: AgentRunResourceOptions): Promise { + if (action === undefined || action.startsWith("-")) throw new AgentRunRestError("validation-failed", "retry requires task/"); + let ref: AgentRunResourceRef; + try { + ref = parseResourceRef(action, args, "task"); + } catch { + throw new AgentRunRestError("validation-failed", "retry requires task/"); + } + if (ref.kind !== "task") throw new AgentRunRestError("validation-failed", "retry supports task/"); + if (options.idempotencyKey === null) throw new AgentRunRestError("validation-failed", "retry requires --idempotency-key "); + if (options.reason === null) throw new AgentRunRestError("validation-failed", "retry requires --reason "); + 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 { const ref = parseResourceRef(action, args); if (ref.kind === "task") { diff --git a/scripts/src/agentrun/rest-bridge.ts b/scripts/src/agentrun/rest-bridge.ts index b0f1dfba..8a7b2129 100644 --- a/scripts/src/agentrun/rest-bridge.ts +++ b/scripts/src/agentrun/rest-bridge.ts @@ -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> { + 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> { 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(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): Record { + return boundedAgentRunRestErrorValue(payload, 0) as Record; +} + +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); + const result: Record = {}; + 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 | null; diff --git a/scripts/src/agentrun/utils.ts b/scripts/src/agentrun/utils.ts index 78b832d6..3395b39b 100644 --- a/scripts/src/agentrun/utils.ts +++ b/scripts/src/agentrun/utils.ts @@ -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;