fix: 收敛 session 输出渐进披露与 stdin 输入 (#136)
Co-authored-by: Codex <codex@pikas.tech>
This commit is contained in:
+159
-24
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user