From def417f0cf2bd5cba72c648a486a9e1f08b8e0b8 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 11 Jun 2026 08:21:27 +0000 Subject: [PATCH] fix: surface AgentRun resource failures in human output --- scripts/agentrun-cli-contract-test.ts | 21 ++++-- scripts/src/agentrun.ts | 93 +++++++++++++++++++++++++-- 2 files changed, 104 insertions(+), 10 deletions(-) diff --git a/scripts/agentrun-cli-contract-test.ts b/scripts/agentrun-cli-contract-test.ts index 963ffcbe..b1523f86 100644 --- a/scripts/agentrun-cli-contract-test.ts +++ b/scripts/agentrun-cli-contract-test.ts @@ -81,9 +81,7 @@ assertCondition( agentRunSource.includes('type AgentRunBridgeCaptureBackend = "local-backend-core-broker" | "remote-frontend-websocket"') && agentRunSource.includes('reason: "runner-environment"') && agentRunSource.includes('degradedReason: "capture-backend-unavailable"') - && agentRunSource.includes('"agentrun-cli-returned-failure"') - && agentRunSource.includes('failureKind: "bridge-execution-environment"') - && agentRunSource.includes('key === "nextActions"'), + && agentRunSource.includes('failureKind: "bridge-execution-environment"'), "AgentRun CLI bridge must use the remote frontend backend in runner/no-Docker environments and classify bridge failures separately", ); @@ -100,13 +98,23 @@ assertCondition( ); assertCondition( - agentRunSource.includes('if (verb === "dispatch") return await resourceDispatch(config, command, action, actionArgs, options);') - && agentRunSource.includes('runOfficialAgentRunCli(config, "queue", ["dispatch", ref.name') - && agentRunSource.includes('"list", "--state", taskListState(options)') + agentRunSource.includes('if (verb === "dispatch") return await resourceDispatch') + && agentRunSource.includes('bridgeActionArgs, options') + && agentRunSource.includes('runAgentRunRestCommand(config, "queue", ["dispatch"') + && agentRunSource.includes('taskListState(options)') && agentRunSource.includes('["commander", "--reader-id", options.readerId'), "AgentRun resources must wrap task dispatch and keep default get tasks on active list visibility", ); +assertCondition( + agentRunSource.includes("function renderFailureLines(value: Record): string[]") + && agentRunSource.includes('lines.push(`Failure: ${failureKind}`)') + && agentRunSource.includes('lines.push(`Message: ${failureMessage}`)') + && agentRunSource.includes("const failure = renderFailureLines(data);") + && agentRunSource.includes("const failure = renderFailureLines(value);"), + "AgentRun resource human output must expose failure kind and message without requiring JSON output", +); + assertCondition( agentRunSource.includes("const effectiveLimit = options.tail ?? options.limit;") && agentRunSource.includes("return renderEventLike(command, result, { ...options, limit: effectiveLimit }, \"Log\""), @@ -134,6 +142,7 @@ console.log(JSON.stringify({ "AgentRun CLI bridge keeps AgentRun failures distinct from bridge failures", "AgentRun resource parser supports apply -f -", "AgentRun resource task dispatch and active task list visibility", + "AgentRun resource failure output is visible in human mode", "AgentRun logs tail controls page limit", "AgentRun dry-run mutations keep resource-command follow-up", ], diff --git a/scripts/src/agentrun.ts b/scripts/src/agentrun.ts index 64961deb..7f30d1ee 100644 --- a/scripts/src/agentrun.ts +++ b/scripts/src/agentrun.ts @@ -622,7 +622,10 @@ function renderResultSummary(command: string, raw: Record, opti ]; const final = stringOrNull(data.finalResponse) ?? stringOrNull(data.output) ?? stringOrNull(data.summary) ?? stringOrNull(data.result); if (final !== null) lines.push("", truncateMultiline(final, options.fullText ? 8000 : 1600)); - else lines.push("", JSON.stringify(pickCompact(data, ["failureKind", "terminalClassification", "degradedReason", "message", "valuesPrinted"]), null, 2)); + else { + const failure = renderFailureLines(data); + lines.push("", ...(failure.length > 0 ? failure : ["No final response is available yet."])); + } return renderedCliResult(raw.ok !== false, command, lines.join("\n")); } @@ -921,13 +924,95 @@ function renderGenericDescription(ref: AgentRunResourceRef, data: unknown): stri const lines = [ `Name: ${ref.kind}/${ref.name}`, `State: ${displayValue(value.state ?? value.status ?? value.phase ?? "-")}`, - "", - "Summary:", - JSON.stringify(pickCompact(value, ["id", "name", "runId", "commandId", "runnerJobId", "sessionId", "state", "status", "phase", "reason", "failureKind", "terminalClassification", "valuesPrinted"]), null, 2), ]; + const identity = renderResourceIdentityLines(ref, value); + if (identity.length > 0) lines.push("", ...identity); + const failure = renderFailureLines(value); + if (failure.length > 0) lines.push("", ...failure); + const next = renderResourceNextLines(ref, value); + if (next.length > 0) lines.push("", "Next:", ...next.map((line) => ` ${line}`)); + if (identity.length === 0 && failure.length === 0 && next.length === 0) { + lines.push( + "", + "Summary:", + JSON.stringify(pickCompact(value, ["id", "name", "runId", "commandId", "runnerJobId", "sessionId", "state", "status", "phase", "reason", "failureKind", "valuesPrinted"]), null, 2), + ); + } return lines.join("\n"); } +function renderResourceIdentityLines(ref: AgentRunResourceRef, value: Record): string[] { + const fields: [string, unknown][] = [ + ["Run", value.runId], + ["Command", value.commandId], + ["RunnerJob", value.runnerJobId ?? (ref.kind === "runnerjob" ? value.id : null)], + ["Session", value.sessionId], + ["Namespace", value.namespace], + ["Job", value.jobName], + ["Image", value.image], + ["LogPath", value.logPath], + ["Started", value.startedAt], + ["Finished", value.finishedAt], + ]; + if (ref.kind !== "runnerjob") { + return fields + .filter(([label]) => label === "Run" || label === "Command" || label === "RunnerJob" || label === "Session") + .map(([label, item]) => formatResourceField(label, item)) + .filter((line): line is string => line !== null); + } + return fields.map(([label, item]) => formatResourceField(label, item)).filter((line): line is string => line !== null); +} + +function formatResourceField(label: string, value: unknown): string | null { + const text = stringOrNull(value); + if (text === null || text.length === 0) return null; + return `${label}: ${text}`; +} + +function renderFailureLines(value: Record): string[] { + const terminalClassification = record(value.terminalClassification); + const failureKind = stringOrNull(value.failureKind) + ?? stringOrNull(terminalClassification.failureKind) + ?? stringOrNull(value.degradedReason); + const failureMessage = stringOrNull(value.failureMessage) + ?? stringOrNull(terminalClassification.failureMessage) + ?? stringOrNull(value.message) + ?? stringOrNull(value.reason); + const category = stringOrNull(terminalClassification.category); + const timeoutKind = stringOrNull(terminalClassification.timeoutKind); + const timeoutState = stringOrNull(terminalClassification.timeoutState); + const providerInterruption = stringOrNull(terminalClassification.providerInterruption); + const lastActivity = stringOrNull(terminalClassification.lastActivityKind); + const lastActivitySeq = terminalClassification.lastActivitySeq; + const lines: string[] = []; + if (failureKind !== null) lines.push(`Failure: ${failureKind}`); + if (failureMessage !== null) lines.push(`Message: ${failureMessage}`); + if (category !== null) lines.push(`Category: ${category}`); + if (timeoutKind !== null || timeoutState !== null) lines.push(`Timeout: ${displayValue(timeoutKind)} / ${displayValue(timeoutState)}`); + if (providerInterruption !== null) lines.push(`Provider: ${providerInterruption}`); + if (lastActivity !== null) lines.push(`LastActivity: ${lastActivity}${lastActivitySeq === undefined ? "" : ` seq=${String(lastActivitySeq)}`}`); + return lines; +} + +function renderResourceNextLines(ref: AgentRunResourceRef, value: Record): string[] { + const runId = stringOrNull(value.runId); + const commandId = stringOrNull(value.commandId); + const runnerJobId = stringOrNull(value.runnerJobId) ?? (ref.kind === "runnerjob" ? ref.name : null); + const sessionId = stringOrNull(value.sessionId); + const lines = [ + runId === null ? null : `bun scripts/cli.ts agentrun events run/${runId} --after-seq 0 --limit 100`, + sessionId === null ? null : `bun scripts/cli.ts agentrun logs session/${sessionId} --tail 100`, + runId === null || commandId === null ? null : `bun scripts/cli.ts agentrun result run/${runId} --command ${commandId}`, + runId === null || commandId === null ? null : `bun scripts/cli.ts agentrun describe command/${commandId} --run ${runId} --full`, + runId === null || runnerJobId === null ? null : `bun scripts/cli.ts agentrun describe runnerjob/${runnerJobId} --run ${runId} --full`, + ]; + const unique: string[] = []; + for (const line of lines) { + if (line !== null && !unique.includes(line)) unique.push(line); + } + return unique.slice(0, 5); +} + function parseTaskManifest(raw: string, source: string): Record { const parsed = source.endsWith(".json") ? JSON.parse(raw) as unknown : Bun.YAML.parse(raw) as unknown; const input = record(parsed);