fix: 增加 runs events 低噪声 summary

This commit is contained in:
Codex
2026-06-09 22:53:42 +08:00
parent 5919a3c737
commit 3850604c2b
3 changed files with 216 additions and 5 deletions
+177 -3
View File
@@ -14,15 +14,30 @@ import type { BackendProfile, CommandRecord, JsonRecord, JsonValue, RunRecord, S
import { AgentRunError, errorToJson } from "../../src/common/errors.js";
import type { RunnerOnceOptions } from "../../src/runner/run-once.js";
import { backendProfileSpec, isBackendProfile } from "../../src/common/backend-profiles.js";
import { outputBytesFromPayload, outputTruncatedFromPayload } from "../../src/common/output.js";
import { redactText } from "../../src/common/redaction.js";
interface ParsedArgs {
positional: string[];
flags: Map<string, string | boolean>;
}
const plainTextOutputMarker = Symbol("agentrun.plainTextOutput");
interface PlainTextOutput {
[plainTextOutputMarker]: true;
text: string;
}
type CliResult = JsonValue | PlainTextOutput;
export async function runCli(argv: string[]): Promise<void> {
try {
const result = await dispatch(parseArgs(argv));
if (isPlainTextOutput(result)) {
process.stdout.write(result.text.endsWith("\n") ? result.text : `${result.text}\n`);
return;
}
print({ ok: true, data: result });
} catch (error) {
const status = error instanceof AgentRunError ? error.httpStatus : 1;
@@ -31,7 +46,7 @@ export async function runCli(argv: string[]): Promise<void> {
}
}
async function dispatch(args: ParsedArgs): Promise<JsonValue> {
async function dispatch(args: ParsedArgs): Promise<CliResult> {
const [group, command, id] = args.positional;
if (!group || group === "help" || group === "--help") return help(args);
if (args.flags.get("help") === true) return help(args, group);
@@ -74,7 +89,7 @@ async function dispatch(args: ParsedArgs): Promise<JsonValue> {
if (group === "queue" && command === "refresh" && id) return client(args).post(`/api/v1/queue/tasks/${encodeURIComponent(id)}/refresh`, {});
if (group === "runs" && command === "create") return client(args).post("/api/v1/runs", await jsonFile(args));
if (group === "runs" && command === "show" && id) return client(args).get(`/api/v1/runs/${encodeURIComponent(id)}`);
if (group === "runs" && command === "events" && id) return client(args).get(`/api/v1/runs/${encodeURIComponent(id)}/events?afterSeq=${flag(args, "after-seq", "0")}&limit=${flag(args, "limit", "100")}`);
if (group === "runs" && command === "events" && id) return runEvents(args, id);
if (group === "runs" && command === "result" && id) {
const commandId = optionalFlag(args, "command-id");
return client(args).get(`/api/v1/runs/${encodeURIComponent(id)}/result${commandId ? `?commandId=${encodeURIComponent(commandId)}` : ""}`);
@@ -136,6 +151,165 @@ async function listRunnerJobs(args: ParsedArgs): Promise<JsonValue> {
return client(args).get(`/api/v1/runs/${encodeURIComponent(runId)}/runner-jobs${commandId ? `?commandId=${encodeURIComponent(commandId)}` : ""}`);
}
async function runEvents(args: ParsedArgs, runId: string): Promise<CliResult> {
const afterSeq = integerFlag(args, "after-seq", 0, { min: 0 });
const limit = integerFlag(args, "limit", 100, { min: 1, max: 500 });
const page = await client(args).get(`/api/v1/runs/${encodeURIComponent(runId)}/events?afterSeq=${afterSeq}&limit=${limit}`);
const format = optionalFlag(args, "format") ?? "json";
if (format !== "json" && format !== "tsv") throw new AgentRunError("schema-invalid", "runs events --format must be json or tsv", { httpStatus: 2 });
const tail = tailFlag(args, limit);
const wantsSummary = args.flags.get("summary") === true || args.flags.get("tail-summary") === true || tail !== null || format === "tsv";
if (!wantsSummary) return page;
const summary = summarizeRunEventPage(page, {
runId,
afterSeq,
limit,
tail: tail ?? (args.flags.get("tail-summary") === true ? Math.min(limit, 20) : null),
summaryChars: integerFlag(args, "summary-chars", 900, { min: 1, max: 4_000 }),
});
if (format === "tsv") return plainTextOutput(renderRunEventSummaryTsv(summary));
return summary;
}
interface RunEventSummaryOptions {
runId: string;
afterSeq: number;
limit: number;
tail: number | null;
summaryChars: number;
}
export function summarizeRunEventPage(page: JsonValue, options: RunEventSummaryOptions): JsonRecord {
const events = eventPageItems(page);
const selected = options.tail === null ? events : events.slice(-options.tail);
const items = selected.map((event) => summarizeRunEvent(event, options.summaryChars));
const lastSeq = items.length > 0 ? items[items.length - 1]?.seq ?? null : null;
return {
action: "runs-events-summary",
runId: options.runId,
afterSeq: options.afterSeq,
limit: options.limit,
tail: options.tail,
sourceCount: events.length,
count: items.length,
lastSeq,
nextAfterSeq: lastSeq,
valuesPrinted: false,
items,
};
}
function summarizeRunEvent(event: JsonRecord, summaryChars: number): JsonRecord {
const payload = jsonRecordValue(event.payload) ?? {};
const command = boundedSummaryString(stringValue(payload.command), summaryChars);
const text = boundedSummaryString(stringValue(payload.text), summaryChars);
const outputSummary = boundedSummaryString(stringValue(payload.outputSummary) ?? nestedSummaryText(payload), summaryChars);
const message = boundedSummaryString(stringValue(payload.failureMessage) ?? stringValue(payload.message), summaryChars);
const summary = text ?? command ?? outputSummary ?? message ?? "";
const summaryTruncated = [command, text, outputSummary, message].some((value) => value?.endsWith("...") === true);
return {
seq: numberValue(event.seq) ?? 0,
type: stringValue(event.type) ?? "unknown",
method: stringValue(payload.method),
status: stringValue(payload.status) ?? stringValue(payload.terminalStatus) ?? stringValue(payload.phase),
command,
text,
exitCode: numberValue(payload.exitCode),
durationMs: numberValue(payload.durationMs),
outputTruncated: outputTruncatedFromPayload(payload) || summaryTruncated,
outputBytes: outputBytesFromPayload(payload),
outputSummary,
summary,
};
}
export function renderRunEventSummaryTsv(summary: JsonRecord): string {
const items = Array.isArray(summary.items) ? summary.items.filter((item): item is JsonRecord => jsonRecordValue(item) !== null) : [];
const rows = items.map((item) => [
item.seq,
item.type,
item.method,
item.status,
item.exitCode,
item.durationMs,
item.outputTruncated,
item.outputBytes,
item.summary,
].map(tsvCell).join("\t"));
return ["seq\ttype\tmethod\tstatus\texitCode\tdurationMs\toutputTruncated\toutputBytes\tsummary", ...rows].join("\n");
}
function eventPageItems(page: JsonValue): JsonRecord[] {
const record = jsonRecordValue(page);
if (!record) throw new AgentRunError("schema-invalid", "runs events response must be an object", { httpStatus: 2 });
if (!Array.isArray(record.items)) throw new AgentRunError("schema-invalid", "runs events response.items must be an array", { httpStatus: 2 });
return record.items.map((item) => {
const event = jsonRecordValue(item);
if (!event) throw new AgentRunError("schema-invalid", "runs events item must be an object", { httpStatus: 2 });
return event;
});
}
function nestedSummaryText(payload: JsonRecord): string | null {
const summary = payload.summary;
if (typeof summary === "string") return summary;
const summaryRecord = jsonRecordValue(summary);
return summaryRecord ? stringValue(summaryRecord.text) : null;
}
function boundedSummaryString(value: string | null, limit: number): string | null {
if (!value) return null;
const normalized = redactText(value).replace(/\s+/gu, " ").trim();
if (normalized.length === 0) return null;
return normalized.length > limit ? `${normalized.slice(0, Math.max(0, limit - 3))}...` : normalized;
}
function integerFlag(args: ParsedArgs, name: string, fallback: number, options: { min: number; max?: number }): number {
const raw = optionalFlag(args, name);
const value = raw === null ? fallback : Number(raw);
if (!Number.isInteger(value) || value < options.min || (options.max !== undefined && value > options.max)) {
const maxText = options.max === undefined ? "" : ` and <= ${options.max}`;
throw new AgentRunError("schema-invalid", `--${name} must be an integer >= ${options.min}${maxText}`, { httpStatus: 2 });
}
return value;
}
function tailFlag(args: ParsedArgs, limit: number): number | null {
const value = args.flags.get("tail");
if (value === undefined) return null;
if (value === true) return Math.min(limit, 20);
const parsed = Number(value);
if (!Number.isInteger(parsed) || parsed < 1 || parsed > limit) throw new AgentRunError("schema-invalid", `--tail must be an integer between 1 and --limit (${limit})`, { httpStatus: 2 });
return parsed;
}
function stringValue(value: JsonValue | undefined): string | null {
return typeof value === "string" && value.length > 0 ? value : null;
}
function numberValue(value: JsonValue | undefined): number | null {
return typeof value === "number" && Number.isFinite(value) ? value : null;
}
function jsonRecordValue(value: unknown): JsonRecord | null {
return typeof value === "object" && value !== null && !Array.isArray(value) ? value as JsonRecord : null;
}
function tsvCell(value: JsonValue | undefined): string {
if (value === null || value === undefined) return "";
return String(value).replace(/[\t\r\n]+/gu, " ");
}
function plainTextOutput(text: string): PlainTextOutput {
return { [plainTextOutputMarker]: true, text };
}
function isPlainTextOutput(value: CliResult): value is PlainTextOutput {
return typeof value === "object" && value !== null && plainTextOutputMarker in value;
}
async function listSessions(args: ParsedArgs): Promise<JsonValue> {
const params = new URLSearchParams();
const state = optionalFlag(args, "state") ?? (args.flags.get("running") === true ? "running" : args.flags.get("unread") === true ? "unread" : args.flags.get("all") === true ? "all" : null);
@@ -814,7 +988,7 @@ function help(args: ParsedArgs, group?: string): JsonRecord {
const commands = [
"runs create --json-file <run.json>",
"runs show <runId>",
"runs events <runId> --after-seq <n> --limit <n>",
"runs events <runId> --after-seq <n> --limit <n> [--summary|--tail-summary] [--tail <n>] [--summary-chars <n>] [--format json|tsv]",
"runs result <runId> [--command-id <commandId>]",
"runs cancel <runId> [--reason <text>]",
"sessions ps [--state default|running|unread|terminal|idle|all] [--profile codex|deepseek|minimax-m3|dsflash-go|<dynamic-profile>|M3] [--reader-id <reader>]",