From fde849937855b2f8de3bbe1a4f2c1a204f0a44e3 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 11 Jun 2026 11:00:23 +0000 Subject: [PATCH] fix: summarize AgentRun command output by signal --- scripts/agentrun-cli-contract-test.ts | 26 +++ scripts/src/agentrun.ts | 257 +++++++++++++++++++++++++- 2 files changed, 278 insertions(+), 5 deletions(-) diff --git a/scripts/agentrun-cli-contract-test.ts b/scripts/agentrun-cli-contract-test.ts index dd2569d9..40a79048 100644 --- a/scripts/agentrun-cli-contract-test.ts +++ b/scripts/agentrun-cli-contract-test.ts @@ -133,6 +133,30 @@ assertCondition( "AgentRun logs/events must render payload error, backend phase, and command id summaries by default", ); +assertCondition( + agentRunSource.includes("function commandOutputSummary(payload: Record): string | null") + && agentRunSource.includes("function summarizeStructuredCliOutput(value: Record): string | null") + && agentRunSource.includes("function parseJsonRecordFromText(raw: string): Record | null") + && agentRunSource.includes('if (type === "command_output")') + && agentRunSource.includes("push(commandOutputSummary(payload))") + && agentRunSource.includes('if (type !== "command_output")'), + "AgentRun command_output summaries must use a dedicated low-noise renderer instead of raw payload JSON", +); + +assertCondition( + agentRunSource.includes("isLowSignalJsonField") + && agentRunSource.includes('key === "generatedAt" || key === "cli" || key === "version" || key === "valuesRedacted" || key === "secretMaterialStored"') + && agentRunSource.includes('firstPathText(value, ["action", "operation", "command", "kind"])') + && agentRunSource.includes('"traceId", "lastTraceId", "trace.traceId", "trace.id", "body.traceId", "body.trace.id", "providerTrace.traceId", "runnerTrace.traceId"') + && agentRunSource.includes('"sessionId", "session.sessionId", "body.sessionId", "workspace.selectedAgentSessionId", "workspace.selectedConversation.sessionId"') + && agentRunSource.includes('"conversationId", "session.conversationId", "body.conversationId", "workspace.selectedConversationId", "workspace.selectedConversation.conversationId"') + && agentRunSource.includes('"providerProfile", "backendProfile", "profile", "session.providerProfile", "workspace.providerProfile"') + && agentRunSource.includes('"pipelineRun", "pipelineRunName", "pipelineRun.name", "pipeline.runName", "pipeline.name"') + && agentRunSource.includes("function summarizePartialJsonCommandOutput(raw: string): string | null") + && agentRunSource.includes("if (failure !== null || isFailureLikeStatus(status))"), + "AgentRun command_output summaries must prefer business fields and suppress metadata-only JSON headers", +); + assertCondition( agentRunSource.includes("function rerunWithoutDryRun(command: string): string") && agentRunSource.includes("options.dryRun ? [rerunWithoutDryRun(command)] : undefined"), @@ -157,6 +181,8 @@ console.log(JSON.stringify({ "AgentRun resource failure output is visible in human mode", "AgentRun logs tail is enforced by the render-only client", "AgentRun logs/events expose payload error and backend phase summaries", + "AgentRun command_output summaries use a dedicated low-noise renderer", + "AgentRun command_output summaries prefer business fields over metadata headers", "AgentRun dry-run mutations keep resource-command follow-up", ], })); diff --git a/scripts/src/agentrun.ts b/scripts/src/agentrun.ts index e6169e0b..7d4b383c 100644 --- a/scripts/src/agentrun.ts +++ b/scripts/src/agentrun.ts @@ -950,8 +950,7 @@ function agentRunEventSummary(item: Record, payload: Record, payload: Record): string | null { + const raw = commandOutputText(payload); + if (raw === null) return null; + const structured = parseJsonRecordFromText(raw); + if (structured !== null) { + const summary = summarizeStructuredCliOutput(structured); + if (summary !== null) return summary; + } + return summarizePartialJsonCommandOutput(raw) ?? summarizeTextCommandOutput(raw); +} + +function commandOutputText(payload: Record): string | null { + const summary = record(payload.summary); + const summaryText = stringOrNull(summary.text); + const payloadText = stringOrNull(payload.text); + if (summary.textTruncated === true && payloadText !== null) return payloadText; + return summaryText + ?? payloadText + ?? stringOrNull(payload.outputSummary) + ?? eventText(payload.summary); +} + +function parseJsonRecordFromText(raw: string): Record | null { + const trimmed = raw.trim(); + const direct = tryParseJsonRecord(trimmed); + if (direct !== null) return direct; + + const firstBrace = trimmed.indexOf("{"); + const lastBrace = trimmed.lastIndexOf("}"); + if (firstBrace >= 0 && lastBrace > firstBrace) { + const sliced = tryParseJsonRecord(trimmed.slice(firstBrace, lastBrace + 1)); + if (sliced !== null) return sliced; + } + + const line = trimmed + .split(/\r?\n/u) + .map((item) => item.trim()) + .reverse() + .find((item) => item.startsWith("{") && item.endsWith("}")); + return line === undefined ? null : tryParseJsonRecord(line); +} + +function tryParseJsonRecord(raw: string): Record | null { + try { + const parsed = JSON.parse(raw) as unknown; + return isRecord(parsed) ? parsed : null; + } catch { + return null; + } +} + +function summarizeStructuredCliOutput(value: Record): string | null { + const parts: string[] = []; + const action = firstPathText(value, ["action", "operation", "command", "kind"]); + const status = firstPathText(value, ["status", "state", "phase", "body.status"]); + const failure = firstPathText(value, ["failureKind", "error.failureKind", "body.failureKind"]); + const message = firstPathText(value, ["error.message", "error.additionalDetails", "message", "reason", "body.message", "body.error", "result.message"]); + const primary = [ + action, + status, + labeledSummaryValue("http", firstPathText(value, ["httpStatus", "request.httpStatus"])), + ].filter((item): item is string => item !== null); + if (primary.length > 0) pushSummaryPart(parts, primary.join(" ")); + + pushLabeledSummaryPart(parts, "failure", failure); + if (failure !== null || isFailureLikeStatus(status)) pushLabeledSummaryPart(parts, "message", message); + pushLabeledSummaryPart(parts, "trace", firstPathText(value, ["traceId", "lastTraceId", "trace.traceId", "trace.id", "body.traceId", "body.trace.id", "providerTrace.traceId", "runnerTrace.traceId"]), true); + pushLabeledSummaryPart(parts, "session", firstPathText(value, ["sessionId", "session.sessionId", "body.sessionId", "workspace.selectedAgentSessionId", "workspace.selectedConversation.sessionId"]), true); + pushLabeledSummaryPart(parts, "conversation", firstPathText(value, ["conversationId", "session.conversationId", "body.conversationId", "workspace.selectedConversationId", "workspace.selectedConversation.conversationId"]), true); + pushLabeledSummaryPart(parts, "thread", firstPathText(value, ["threadId", "session.threadId", "body.threadId", "workspace.selectedConversation.threadId"]), true); + pushLabeledSummaryPart(parts, "provider", firstPathText(value, ["providerProfile", "backendProfile", "profile", "session.providerProfile", "workspace.providerProfile"])); + pushLabeledSummaryPart(parts, "route", commandOutputRouteSummary(value)); + pushLabeledSummaryPart(parts, "pipeline", firstPathText(value, ["pipelineRun", "pipelineRunName", "pipelineRun.name", "pipeline.runName", "pipeline.name"]), true); + pushLabeledSummaryPart(parts, "run", firstPathText(value, ["runId", "agentRun.runId"]), true); + pushLabeledSummaryPart(parts, "cmd", firstPathText(value, ["commandId", "agentRun.commandId", "providerTrace.commandId", "runnerTrace.commandId"]), true); + pushLabeledSummaryPart(parts, "job", firstPathText(value, ["jobName", "agentRun.jobName", "runnerJobId"]), true); + pushLabeledSummaryPart(parts, "lane", firstPathText(value, ["runtimeEndpoint.lane", "lane"])); + pushLabeledSummaryPart(parts, "ns", firstPathText(value, ["runtimeEndpoint.namespace", "namespace"])); + pushLabeledSummaryPart(parts, "actor", firstPathText(value, ["actor.username", "body.actor.username", "actor.displayName", "body.actor.displayName"])); + pushLabeledSummaryPart(parts, "auth", firstPathText(value, ["authMethod", "auth.authMethod", "body.authMethod"])); + pushLabeledSummaryPart(parts, "reply", firstPathText(value, ["reply.content", "result.reply.content", "body.reply.content", "answer", "content", "body.content"])); + if (failure === null && !isFailureLikeStatus(status)) pushLabeledSummaryPart(parts, "message", message); + + const itemsCount = arrayRecords(value.items).length; + if (itemsCount > 0) pushSummaryPart(parts, `items=${itemsCount}`); + const eventsCount = arrayRecords(value.events).length; + if (eventsCount > 0) pushSummaryPart(parts, `events=${eventsCount}`); + if (value.sessionUsable === false) pushSummaryPart(parts, "sessionUsable=false"); + if (record(value.summary).textTruncated === true || value.textTruncated === true || value.outputTruncated === true) pushSummaryPart(parts, "truncated=true"); + + return parts.length > 0 ? parts.slice(0, 10).join("; ") : null; +} + +function commandOutputRouteSummary(value: Record): string | null { + const route = record(value.route); + const request = record(value.request); + const method = stringOrNull(route.method) ?? stringOrNull(request.method); + const path = stringOrNull(route.path) ?? stringOrNull(request.path); + if (method !== null && path !== null) return `${method} ${path}`; + return stringOrNull(request.url) ?? stringOrNull(value.baseUrl); +} + +function firstPathText(value: Record, paths: string[]): string | null { + for (const path of paths) { + const text = summaryScalar(pathValue(value, path)); + if (text !== null) return text; + } + return null; +} + +function pathValue(value: Record, path: string): unknown { + let current: unknown = value; + for (const key of path.split(".")) { + if (!isRecord(current)) return undefined; + current = current[key]; + } + return current; +} + +function summaryScalar(value: unknown): string | null { + if (typeof value === "string") { + const trimmed = value.trim(); + return trimmed.length > 0 ? trimmed : null; + } + if (typeof value === "number" && Number.isFinite(value)) return String(value); + if (typeof value === "boolean") return String(value); + return null; +} + +function isFailureLikeStatus(value: string | null): boolean { + return value !== null && /(?:fail|error|timeout|cancel|interrupt|denied|rejected)/iu.test(value); +} + +function pushLabeledSummaryPart(parts: string[], label: string, value: string | null, shorten = false): void { + const text = labeledSummaryValue(label, value, shorten); + if (text !== null) pushSummaryPart(parts, text); +} + +function labeledSummaryValue(label: string, value: string | null, shorten = false): string | null { + if (value === null) return null; + const rendered = shorten ? shortId(value) : truncateOneLine(value, 80); + return `${label}=${rendered}`; +} + +function pushSummaryPart(parts: string[], value: string): void { + const text = truncateOneLine(value, 160); + if (text.length > 0 && !parts.includes(text)) parts.push(text); +} + +function summarizeTextCommandOutput(raw: string): string | null { + const lines = raw + .split(/\r?\n/u) + .map((line) => line.trim()) + .filter((line) => line.length > 0 && !isLowSignalCommandOutputLine(line)); + const signal = lines.find((line) => /(?:trace|session|conversation|thread|status|failure|error|route|http|pipeline|provider|commit|succeeded|failed|created)/iu.test(line)) + ?? lines[0] + ?? null; + if (signal === null) return null; + return truncateOneLine(cleanCommandOutputLine(signal), 220); +} + +function summarizePartialJsonCommandOutput(raw: string): string | null { + const fields = new Map(); + for (const line of raw.split(/\r?\n/u)) { + const field = jsonLineField(line); + if (field === null || isLowSignalJsonField(field.key)) continue; + if (!fields.has(field.key) && field.value.length > 0) fields.set(field.key, field.value); + } + + const parts: string[] = []; + const action = fields.get("action") ?? fields.get("operation") ?? fields.get("command") ?? fields.get("kind") ?? null; + const status = fields.get("status") ?? fields.get("state") ?? fields.get("phase") ?? null; + const http = fields.get("httpStatus") ?? null; + const failure = fields.get("failureKind") ?? null; + const message = fields.get("message") ?? fields.get("reason") ?? fields.get("error") ?? null; + const primary = [action, status, labeledSummaryValue("http", http)].filter((item): item is string => item !== null); + if (primary.length > 0) pushSummaryPart(parts, primary.join(" ")); + + pushLabeledSummaryPart(parts, "failure", failure); + if (failure !== null || isFailureLikeStatus(status)) pushLabeledSummaryPart(parts, "message", message); + pushLabeledSummaryPart(parts, "trace", fields.get("traceId") ?? fields.get("lastTraceId") ?? null, true); + pushLabeledSummaryPart(parts, "session", fields.get("sessionId") ?? fields.get("selectedAgentSessionId") ?? null, true); + pushLabeledSummaryPart(parts, "conversation", fields.get("conversationId") ?? fields.get("selectedConversationId") ?? null, true); + pushLabeledSummaryPart(parts, "thread", fields.get("threadId") ?? null, true); + pushLabeledSummaryPart(parts, "provider", fields.get("providerProfile") ?? fields.get("backendProfile") ?? fields.get("profile") ?? null); + pushLabeledSummaryPart(parts, "route", partialRouteSummary(fields)); + pushLabeledSummaryPart(parts, "pipeline", fields.get("pipelineRun") ?? fields.get("pipelineRunName") ?? null, true); + pushLabeledSummaryPart(parts, "run", fields.get("runId") ?? null, true); + pushLabeledSummaryPart(parts, "cmd", fields.get("commandId") ?? null, true); + pushLabeledSummaryPart(parts, "job", fields.get("jobName") ?? fields.get("runnerJobId") ?? null, true); + pushLabeledSummaryPart(parts, "lane", fields.get("lane") ?? null); + pushLabeledSummaryPart(parts, "ns", fields.get("namespace") ?? null); + pushLabeledSummaryPart(parts, "actor", fields.get("username") ?? fields.get("displayName") ?? null); + pushLabeledSummaryPart(parts, "auth", fields.get("authMethod") ?? null); + if (failure === null && !isFailureLikeStatus(status)) pushLabeledSummaryPart(parts, "message", message); + + return parts.length > 0 ? parts.slice(0, 10).join("; ") : null; +} + +function partialRouteSummary(fields: Map): string | null { + const method = fields.get("method") ?? null; + const path = fields.get("path") ?? null; + if (method !== null && path !== null) return `${method} ${path}`; + return fields.get("url") ?? fields.get("baseUrl") ?? null; +} + +function jsonLineField(line: string): { key: string; value: string } | null { + const match = line.trim().replace(/,$/u, "").match(/^"([^"]+)":\s*(.+)$/u); + if (match === null) return null; + return { key: match[1], value: cleanJsonScalar(match[2]) }; +} + +function cleanJsonScalar(raw: string): string { + const trimmed = raw.trim(); + if (trimmed === "null" || trimmed === "{" || trimmed === "[" || trimmed === "}" || trimmed === "]") return ""; + if (trimmed.startsWith('"') && trimmed.endsWith('"')) { + try { + const parsed = JSON.parse(trimmed) as unknown; + return typeof parsed === "string" ? parsed.trim() : String(parsed); + } catch { + return trimmed.slice(1, -1).trim(); + } + } + return trimmed; +} + +function cleanCommandOutputLine(line: string): string { + const trimmed = line.replace(/,$/u, "").trim(); + const jsonField = trimmed.match(/^"([^"]+)":\s*(.+)$/u); + if (jsonField === null) return trimmed; + const key = jsonField[1]; + const value = jsonField[2].replace(/^"|"$/gu, "").trim(); + return `${key}=${value}`; +} + +function isLowSignalCommandOutputLine(line: string): boolean { + const trimmed = line.replace(/,$/u, "").trim(); + if (trimmed === "{" || trimmed === "}" || trimmed === "}," || trimmed === "];" || trimmed === "[") return true; + const field = jsonLineField(trimmed); + return field !== null && isLowSignalJsonField(field.key); +} + +function isLowSignalJsonField(key: string): boolean { + return key === "generatedAt" || key === "cli" || key === "version" || key === "valuesRedacted" || key === "secretMaterialStored"; +} + function eventText(value: unknown): string | null { const direct = stringOrNull(value); if (direct !== null && direct.trim().length > 0) return direct.trim();