fix: 增加 runs events 低噪声 summary
This commit is contained in:
+177
-3
@@ -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>]",
|
||||
|
||||
Reference in New Issue
Block a user