fix: 收敛 session 输出渐进披露与 stdin 输入 (#136)

Co-authored-by: Codex <codex@pikas.tech>
This commit is contained in:
Lyon
2026-06-10 09:34:22 +08:00
committed by GitHub
parent a4b83abe46
commit ecbe1368ba
3 changed files with 206 additions and 47 deletions
+159 -24
View File
@@ -189,6 +189,13 @@ interface SessionEventSummaryOptions {
runId: string | null;
tail: number | null;
summaryChars: number;
includeOutput: boolean;
}
interface SessionEventDetailFilter {
seq: number | null;
eventId: string | null;
itemId: string | null;
}
export function summarizeRunEventPage(page: JsonValue, options: RunEventSummaryOptions): JsonRecord {
@@ -216,9 +223,12 @@ export function summarizeSessionEventPage(page: JsonValue, options: SessionEvent
if (!record) throw new AgentRunError("schema-invalid", "sessions event response must be an object", { httpStatus: 2 });
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;
const summarized = selected.map((event) => summarizeRunEvent(event, options.summaryChars));
const visible = options.includeOutput ? summarized : summarized.filter(isDefaultVisibleSessionEvent);
const suppressed = summarizeSuppressedSessionEvents(summarized, visible);
const runId = stringValue(record.runId) ?? options.runId;
const items = withSessionDetailCommands(visible, options.kind, options.sessionId, runId);
const lastSeq = summarized.length > 0 ? summarized[summarized.length - 1]?.seq ?? null : null;
return {
action: `session-${options.kind}-summary`,
sessionId: stringValue(record.sessionId) ?? options.sessionId,
@@ -227,13 +237,22 @@ export function summarizeSessionEventPage(page: JsonValue, options: SessionEvent
limit: options.limit,
tail: options.tail,
sourceCount: events.length,
displayedCount: items.length,
count: items.length,
cursor: stringValue(record.cursor),
lastSeq,
nextAfterSeq: lastSeq,
suppressedEvents: suppressed,
valuesPrinted: false,
items,
drillDownCommands: sessionEventDrillDownCommands(options.kind, options.sessionId, { afterSeq: options.afterSeq, limit: options.limit, runId }),
progressiveDisclosure: {
default: "assistant messages plus tool summaries; command_output/backend_status chunks are counted, not displayed",
includeOutputFlag: "--include-output",
detailFlags: "--seq <seq> or --item-id <itemId> --full",
fullFlag: "--full",
rawFlag: "--raw",
},
};
}
@@ -276,16 +295,19 @@ export function summarizeQueueDispatchResult(result: JsonValue, taskId: string):
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 type = stringValue(event.type) ?? "unknown";
const command = type === "assistant_message" ? null : boundedSummaryString(stringValue(payload.command), Math.min(summaryChars, 240));
const text = type === "assistant_message" ? boundedSummaryString(stringValue(payload.text), summaryChars) : null;
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 summary = text ?? outputSummary ?? command ?? message ?? "";
const summaryTruncated = [command, text, outputSummary, message].some((value) => value?.endsWith("...") === true);
const runnerTrace = findNestedValue(payload, "runnerTrace");
return {
eventId: stringValue(event.id),
seq: numberValue(event.seq) ?? 0,
type: stringValue(event.type) ?? "unknown",
type,
itemId: stringValue(payload.itemId) ?? stringValue(payload.id),
method: stringValue(payload.method),
status: stringValue(payload.status) ?? stringValue(payload.terminalStatus) ?? stringValue(payload.phase),
phase: stringValue(payload.phase),
@@ -306,6 +328,95 @@ function summarizeRunEvent(event: JsonRecord, summaryChars: number): JsonRecord
};
}
function isDefaultVisibleSessionEvent(item: JsonRecord): boolean {
const type = stringValue(item.type);
return type === "assistant_message" || type === "tool_call" || type === "error";
}
function summarizeSuppressedSessionEvents(all: JsonRecord[], visible: JsonRecord[]): JsonRecord {
const visibleSeqs = new Set(visible.map((item) => item.seq));
const suppressed = all.filter((item) => !visibleSeqs.has(item.seq));
const byType: JsonRecord = {};
for (const item of suppressed) {
const type = stringValue(item.type) ?? "unknown";
byType[type] = Number(byType[type] ?? 0) + 1;
}
return {
count: suppressed.length,
byType,
outputBytes: suppressed.reduce((total, item) => total + Number(item.outputBytes ?? 0), 0),
outputTruncated: suppressed.some((item) => item.outputTruncated === true),
valuesPrinted: false,
};
}
function withSessionDetailCommands(items: JsonRecord[], kind: "trace" | "output", sessionId: string, runId: string | null): JsonRecord[] {
return items.map((item) => {
const seq = numberValue(item.seq);
const itemId = stringValue(item.itemId);
const eventId = stringValue(item.eventId);
const detail = seq === null ? null : `./scripts/agentrun sessions ${kind} ${sessionId} --seq ${seq}${runId ? ` --run-id ${runId}` : ""} --full`;
return {
...item,
...(detail || itemId || eventId ? {
detailCommands: {
...(detail ? { seq: detail } : {}),
...(itemId ? { item: `./scripts/agentrun sessions ${kind} ${sessionId} --item-id ${itemId}${runId ? ` --run-id ${runId}` : ""} --full` } : {}),
...(eventId ? { event: `./scripts/agentrun sessions ${kind} ${sessionId} --event-id ${eventId}${runId ? ` --run-id ${runId}` : ""} --full` } : {}),
},
} : {}),
};
});
}
function sessionEventDetailFilter(args: ParsedArgs, seq: number | null): SessionEventDetailFilter | null {
const eventId = optionalFlag(args, "event-id");
const itemId = optionalFlag(args, "item-id");
if (seq === null && !eventId && !itemId) return null;
return { seq, eventId, itemId };
}
function sessionEventDetailResult(page: JsonValue, options: { kind: "trace" | "output"; sessionId: string; runId: string | null; summaryChars: number; filter: SessionEventDetailFilter }): JsonRecord {
const record = jsonRecordValue(page);
if (!record) throw new AgentRunError("schema-invalid", "sessions event response must be an object", { httpStatus: 2 });
const events = eventPageItems(page);
const matches = events.filter((event) => matchesSessionEventFilter(event, options.filter));
if (matches.length === 0) {
throw new AgentRunError("schema-invalid", "no session event matched --seq/--event-id/--item-id in the fetched page", {
httpStatus: 2,
details: {
filter: options.filter as unknown as JsonRecord,
hint: "increase --limit or adjust --after-seq when looking up --event-id/--item-id outside the current page",
},
});
}
const runId = stringValue(record.runId) ?? options.runId;
return {
action: `session-${options.kind}-event-detail`,
sessionId: stringValue(record.sessionId) ?? options.sessionId,
runId,
filter: options.filter as unknown as JsonRecord,
sourceCount: events.length,
count: matches.length,
valuesPrinted: false,
items: matches.map((event) => ({
summary: summarizeRunEvent(event, options.summaryChars),
event: redactJson(event) as JsonRecord,
eventBytes: jsonByteLength(event),
valuesPrinted: false,
})),
};
}
function matchesSessionEventFilter(event: JsonRecord, filter: SessionEventDetailFilter): boolean {
const payload = jsonRecordValue(event.payload) ?? {};
const nestedItem = jsonRecordValue(payload.item);
if (filter.seq !== null && numberValue(event.seq) === filter.seq) return true;
if (filter.eventId && stringValue(event.id) === filter.eventId) return true;
if (filter.itemId && (stringValue(payload.itemId) === filter.itemId || stringValue(payload.id) === filter.itemId || stringValue(nestedItem?.id) === filter.itemId)) return true;
return false;
}
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) => [
@@ -357,6 +468,17 @@ function integerFlag(args: ParsedArgs, name: string, fallback: number, options:
return value;
}
function optionalIntegerFlag(args: ParsedArgs, name: string, options: { min: number; max?: number }): number | null {
const raw = optionalFlag(args, name);
if (raw === null) return null;
const value = 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;
@@ -706,14 +828,17 @@ async function listSessions(args: ParsedArgs): Promise<JsonValue> {
async function sessionEvents(args: ParsedArgs, sessionId: string, kind: "trace" | "output"): Promise<JsonValue> {
const params = new URLSearchParams();
const afterSeq = integerFlag(args, "after-seq", 0, { min: 0 });
const limit = integerFlag(args, "limit", 100, { min: 1, max: 500 });
const requestedSeq = optionalIntegerFlag(args, "seq", { min: 0 });
const afterSeq = requestedSeq !== null && optionalFlag(args, "after-seq") === null ? Math.max(0, requestedSeq - 1) : integerFlag(args, "after-seq", 0, { min: 0 });
const limit = requestedSeq !== null && optionalFlag(args, "limit") === null ? 1 : integerFlag(args, "limit", 100, { min: 1, max: 500 });
const runId = optionalFlag(args, "run-id");
params.set("afterSeq", String(afterSeq));
params.set("limit", String(limit));
if (runId) params.set("runId", runId);
const query = params.toString();
const page = await client(args).get(`/api/v1/sessions/${encodeURIComponent(sessionId)}/${kind}${query ? `?${query}` : ""}`);
const detailFilter = sessionEventDetailFilter(args, requestedSeq);
if (detailFilter) return sessionEventDetailResult(page, { kind, sessionId, runId, summaryChars: integerFlag(args, "summary-chars", 1_200, { min: 1, max: 8_000 }), filter: detailFilter });
if (wantsExpandedOutput(args)) return page;
return summarizeSessionEventPage(page, {
kind,
@@ -723,6 +848,7 @@ async function sessionEvents(args: ParsedArgs, sessionId: string, kind: "trace"
runId,
tail: tailFlag(args, limit),
summaryChars: integerFlag(args, "summary-chars", 900, { min: 1, max: 4_000 }),
includeOutput: args.flags.get("include-output") === true,
});
}
@@ -848,7 +974,7 @@ async function submitQueueTask(args: ParsedArgs): Promise<JsonValue> {
const idempotencyKey = optionalFlag(args, "idempotency-key");
if (idempotencyKey) body.idempotencyKey = idempotencyKey;
if (args.flags.get("dry-run") === true) {
return queueMutationDryRunPlan("queue-submit", null, "/api/v1/queue/tasks", body, "POST", "./scripts/agentrun queue submit --json-file <task.json>");
return queueMutationDryRunPlan("queue-submit", null, "/api/v1/queue/tasks", body, "POST", `./scripts/agentrun queue submit ${jsonInputHelp(args, "<task.json>")}`);
}
return client(args).post("/api/v1/queue/tasks", body);
}
@@ -899,7 +1025,7 @@ async function dispatchQueueTask(args: ParsedArgs, taskId: string): Promise<Json
const body = await queueDispatchBody(args);
if (args.flags.get("dry-run") === true) {
const task = await client(args).get(`/api/v1/queue/tasks/${encodeURIComponent(taskId)}`);
return queueMutationDryRunPlan("queue-dispatch", taskId, `/api/v1/queue/tasks/${encodeURIComponent(taskId)}/dispatch`, body, "POST", `./scripts/agentrun queue dispatch ${taskId} --json-file <dispatch.json>`, task);
return queueMutationDryRunPlan("queue-dispatch", taskId, `/api/v1/queue/tasks/${encodeURIComponent(taskId)}/dispatch`, body, "POST", `./scripts/agentrun queue dispatch ${taskId} ${jsonInputHelp(args, "<dispatch.json>")}`, task);
}
const result = await client(args).post(`/api/v1/queue/tasks/${encodeURIComponent(taskId)}/dispatch`, body);
if (wantsExpandedOutput(args)) return result;
@@ -1297,25 +1423,34 @@ async function sleep(ms: number): Promise<void> {
}
async function jsonFile(args: ParsedArgs): Promise<JsonRecord> {
if (args.flags.get("json-stdin") === true) return parseJsonObject(await readStdinText(), "stdin json");
const file = optionalFlag(args, "json-file");
if (!file) throw new AgentRunError("schema-invalid", "--json-file is required", { httpStatus: 2 });
const value = JSON.parse(await readFile(file, "utf8")) as unknown;
if (typeof value === "object" && value !== null && !Array.isArray(value)) return value as JsonRecord;
throw new AgentRunError("schema-invalid", "json file must contain an object", { httpStatus: 2 });
if (!file) throw new AgentRunError("schema-invalid", "JSON input is required; use --json-file <file> or --json-stdin", { httpStatus: 2 });
return parseJsonObject(await readFile(file, "utf8"), "json file");
}
async function optionalJsonFile(args: ParsedArgs): Promise<JsonRecord> {
if (args.flags.get("json-stdin") === true) return jsonFile(args);
const file = optionalFlag(args, "json-file");
if (!file) return {};
return jsonFile(args);
}
async function optionalRunnerJsonFile(args: ParsedArgs): Promise<JsonRecord> {
if (args.flags.get("runner-json-stdin") === true) return parseJsonObject(await readStdinText(), "runner stdin json");
const file = optionalFlag(args, "runner-json-file");
if (!file) return {};
const value = JSON.parse(await readFile(file, "utf8")) as unknown;
return parseJsonObject(await readFile(file, "utf8"), "runner json file");
}
function parseJsonObject(text: string, source: string): JsonRecord {
const value = JSON.parse(text) as unknown;
if (typeof value === "object" && value !== null && !Array.isArray(value)) return value as JsonRecord;
throw new AgentRunError("schema-invalid", "runner json file must contain an object", { httpStatus: 2 });
throw new AgentRunError("schema-invalid", `${source} must contain an object`, { httpStatus: 2 });
}
function jsonInputHelp(args: ParsedArgs, filePlaceholder: string): string {
return args.flags.get("json-stdin") === true ? "--json-stdin" : `--json-file ${filePlaceholder}`;
}
async function readPrompt(args: ParsedArgs): Promise<string> {
@@ -1433,7 +1568,7 @@ function cancelBody(args: ParsedArgs): JsonRecord {
function help(args: ParsedArgs, group?: string): JsonRecord {
const commands = [
"runs create --json-file <run.json>",
"runs create --json-file <run.json>|--json-stdin",
"runs show <runId>",
"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>]",
@@ -1443,13 +1578,13 @@ function help(args: ParsedArgs, group?: string): JsonRecord {
"sessions storage <sessionId>",
"sessions storage <sessionId> --delete",
"sessions show <sessionId> [--reader-id <reader>]",
"sessions turn [sessionId] --json-file <run-base.json> --prompt-file <file> [--profile codex|deepseek|minimax-m3|dsflash-go|<dynamic-profile>|M3] [--runner-json-file <job.json>]",
"sessions steer <sessionId> --prompt-file <file>",
"sessions turn [sessionId] [--json-file <run-base.json>|--json-stdin] [--prompt-file <file>|--prompt-stdin|--prompt <text>] [--profile codex|deepseek|minimax-m3|dsflash-go|<dynamic-profile>|M3] [--runner-json-file <job.json>|--runner-json-stdin]",
"sessions steer <sessionId> [--prompt-file <file>|--prompt-stdin|--prompt <text>]",
"sessions cancel <sessionId> [--reason <text>] [--full|--raw]",
"sessions trace <sessionId> [--after-seq <n>] [--limit <n>] [--run-id <runId>] [--summary-chars <n>] [--full|--raw]",
"sessions output <sessionId> [--after-seq <n>] [--limit <n>] [--run-id <runId>] [--summary-chars <n>] [--full|--raw]",
"sessions trace <sessionId> [--after-seq <n>] [--limit <n>] [--run-id <runId>] [--summary-chars <n>] [--include-output] [--seq <n>|--event-id <id>|--item-id <id>] [--full|--raw]",
"sessions output <sessionId> [--after-seq <n>] [--limit <n>] [--run-id <runId>] [--summary-chars <n>] [--include-output] [--seq <n>|--event-id <id>|--item-id <id>] [--full|--raw]",
"sessions read <sessionId> [--reader-id <reader>] [--full|--raw]",
"commands create <runId> --type turn|steer|interrupt --json-file <payload.json>",
"commands create <runId> --type turn|steer|interrupt --json-file <payload.json>|--json-stdin",
"commands show <commandId> --run-id <runId>",
"commands result <commandId> --run-id <runId>",
"commands cancel <commandId> [--reason <text>]",
@@ -1458,14 +1593,14 @@ function help(args: ParsedArgs, group?: string): JsonRecord {
"runner job --dry-run --run-id <runId> --command-id <commandId> --image <image>",
"runner jobs --run-id <runId> [--command-id <commandId>]",
"runner job-status [runnerJobId] --run-id <runId>",
"queue submit --json-file <task.json> [--idempotency-key <key>] [--dry-run]",
"queue submit --json-file <task.json>|--json-stdin [--idempotency-key <key>] [--dry-run]",
"queue list [--queue <queue>] [--state <state>] [--cursor <cursor>] [--limit <limit>] [--updated-after <version>] [--full|--raw]",
"queue show <taskId> [--full|--raw]",
"queue stats [--queue <queue>]",
"queue commander [--queue <queue>] [--reader-id <reader>] [--limit <display-limit>] [--full|--raw]",
"queue read <taskId> [--reader-id <reader>] [--dry-run] [--full|--raw]",
"queue cancel <taskId> [--reason <text>] [--dry-run] [--full|--raw]",
"queue dispatch <taskId> [--json-file <dispatch.json>] [--idempotency-key <key>] [--image <image>] [--namespace <namespace>] [--dry-run] [--full|--raw]",
"queue dispatch <taskId> [--json-file <dispatch.json>|--json-stdin] [--idempotency-key <key>] [--image <image>] [--namespace <namespace>] [--dry-run] [--full|--raw]",
"queue refresh <taskId> [--dry-run] [--full|--raw]",
"secrets codex render --dry-run [--profile codex|deepseek|minimax-m3|dsflash-go|<dynamic-profile>] [--codex-home <dir>] [--model-catalog-file <file>] [--namespace agentrun-v01] [--secret-name <name>]",
"provider-profiles list",