diff --git a/docs/reference/spec-v01-cli.md b/docs/reference/spec-v01-cli.md index 662d34b..31e414b 100644 --- a/docs/reference/spec-v01-cli.md +++ b/docs/reference/spec-v01-cli.md @@ -34,12 +34,12 @@ CLI 官方 TypeScript 入口固定为 `scripts/agentrun-cli.ts`。在 G14 非交 ## 初始命令族 ```bash -./scripts/agentrun runs create --json-file +./scripts/agentrun runs create --json-file |--json-stdin ./scripts/agentrun runs show ./scripts/agentrun runs events --after-seq --limit [--summary|--tail-summary] [--tail ] [--summary-chars ] [--format json|tsv] ./scripts/agentrun runs result [--command-id ] ./scripts/agentrun runs cancel [--reason ] -./scripts/agentrun commands create --type turn|steer|interrupt --json-file +./scripts/agentrun commands create --type turn|steer|interrupt --json-file |--json-stdin ./scripts/agentrun commands show --run-id ./scripts/agentrun commands result --run-id ./scripts/agentrun commands cancel [--reason ] @@ -59,22 +59,22 @@ CLI 官方 TypeScript 入口固定为 `scripts/agentrun-cli.ts`。在 G14 非交 ./scripts/agentrun server status [--port ] ./scripts/agentrun server logs [--port ] [--tail-bytes ] [--log-file ] ./scripts/agentrun server stop [--port ] -./scripts/agentrun queue submit --json-file [--dry-run] +./scripts/agentrun queue submit --json-file |--json-stdin [--dry-run] ./scripts/agentrun queue list [--queue ] [--state ] [--cursor ] [--limit ] [--full|--raw] ./scripts/agentrun queue show [--full|--raw] ./scripts/agentrun queue stats [--queue ] ./scripts/agentrun queue commander [--queue ] [--reader-id ] [--limit ] [--full|--raw] ./scripts/agentrun queue read [--reader-id ] [--dry-run] [--full|--raw] ./scripts/agentrun queue cancel [--reason ] [--dry-run] [--full|--raw] -./scripts/agentrun queue dispatch [--json-file ] [--dry-run] [--full|--raw] +./scripts/agentrun queue dispatch [--json-file |--json-stdin] [--dry-run] [--full|--raw] ./scripts/agentrun queue refresh [--dry-run] [--full|--raw] ./scripts/agentrun sessions ps [--state default|running|unread|terminal|idle|all] [--profile codex|deepseek|minimax-m3|dsflash-go|M3] [--reader-id ] ./scripts/agentrun sessions show [--reader-id ] -./scripts/agentrun sessions turn [sessionId] --json-file --prompt-file [--profile codex|deepseek|minimax-m3|dsflash-go|M3] [--runner-json-file ] [--no-runner-job] -./scripts/agentrun sessions steer --prompt-file +./scripts/agentrun sessions turn [sessionId] [--json-file |--json-stdin] [--prompt-file |--prompt-stdin|--prompt ] [--profile codex|deepseek|minimax-m3|dsflash-go|M3] [--runner-json-file |--runner-json-stdin] [--no-runner-job] +./scripts/agentrun sessions steer [--prompt-file |--prompt-stdin|--prompt ] ./scripts/agentrun sessions cancel [--reason ] -./scripts/agentrun sessions trace [--after-seq ] [--limit ] [--run-id ] -./scripts/agentrun sessions output [--after-seq ] [--limit ] [--run-id ] +./scripts/agentrun sessions trace [--after-seq ] [--limit ] [--run-id ] [--include-output] [--seq |--event-id |--item-id ] [--full|--raw] +./scripts/agentrun sessions output [--after-seq ] [--limit ] [--run-id ] [--include-output] [--seq |--event-id |--item-id ] [--full|--raw] ./scripts/agentrun sessions read [--reader-id ] ``` @@ -100,10 +100,12 @@ CLI 官方 TypeScript 入口固定为 `scripts/agentrun-cli.ts`。在 G14 非交 - `queue refresh` 只根据 Queue task 中保存的 Core run/command 引用回写 Queue attempt 状态,不读取 Core trace 反推 commander 或统计;带 `--dry-run` 时不得写回状态。 - `queue list/show/commander` 默认返回低噪声 summary,只显示 task/attempt/session ids、state、read cursor、stats 相关字段和 drill-down 命令;需要完整 task payload、resource bundle 或 metadata 时显式使用 `--full|--raw`。 - `queue show` 不得返回或代理完整 output/trace;输出和 trace 只能通过返回的 `sessionPath` 对应 `sessions ...` 命令查询。 +- 需要提交较长 Queue task、dispatch body、run base 或 command payload 时,CLI 必须支持 `--json-stdin`,避免为了 heredoc/stdin 内容先写临时 dump 文件再传 `--json-file`;`sessions turn` 的 runner job override 也必须支持 `--runner-json-stdin`。所有 stdin JSON 仍必须解析为 object,并在 dry-run 中只展示有界 body 摘要、bytes 和 keys。 - `sessions ps` 默认只显示 running 和 unread session;`--state all` 才显示历史 read session,避免旧 session 噪声淹没当前进度。 - `sessions turn` 是异步 subagent 的受控 CLI 入口:短返回 run、command、runnerJob 和后续 poll/read/steer/cancel 命令,不等待模型完成。`--profile M3` 是 `minimax-m3` 的 CLI alias;profile 仍写入 canonical `backendProfile`,不得 fallback。 - `sessions steer` 对当前 active run 创建 `type=steer` command;`sessions cancel` 通过 Session control 取消 active command 或 run;`sessions read` 写入 reader cursor,使 terminal session 从默认 ps 中消失。 - `sessions output` 与 `sessions trace` 是输出和 trace 的唯一 CLI 查询入口;不得新增 `queue output` 或 `queue trace` 兼容命令。 +- `sessions output` 与 `sessions trace` 默认必须按渐进披露输出低噪声 JSON:只展示 `assistant_message` 与 `tool_call`/`error` 摘要,`command_output`、`backend_status`、raw event、runnerTrace 和大 stdout/stderr 只进入 `suppressedEvents` 计数与 bytes,不得默认展开正文。需要查看工具输出、backend_status 或原始 event 时,必须通过默认摘要中的 `detailCommands`,或显式使用 `--seq `、`--event-id `、`--item-id `、`--include-output`、`--full`/`--raw` 做定点展开;这样保证默认不爆上下文,同时按 id/seq 可完整追溯。 ## 配置与 Secret 边界 diff --git a/scripts/src/cli.ts b/scripts/src/cli.ts index b77fcef..243260b 100644 --- a/scripts/src/cli.ts +++ b/scripts/src/cli.ts @@ -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 or --item-id --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 { async function sessionEvents(args: ParsedArgs, sessionId: string, kind: "trace" | "output"): Promise { 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 { 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 "); + return queueMutationDryRunPlan("queue-submit", null, "/api/v1/queue/tasks", body, "POST", `./scripts/agentrun queue submit ${jsonInputHelp(args, "")}`); } return client(args).post("/api/v1/queue/tasks", body); } @@ -899,7 +1025,7 @@ async function dispatchQueueTask(args: ParsedArgs, taskId: string): Promise`, task); + return queueMutationDryRunPlan("queue-dispatch", taskId, `/api/v1/queue/tasks/${encodeURIComponent(taskId)}/dispatch`, body, "POST", `./scripts/agentrun queue dispatch ${taskId} ${jsonInputHelp(args, "")}`, 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 { } async function jsonFile(args: ParsedArgs): Promise { + 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 or --json-stdin", { httpStatus: 2 }); + return parseJsonObject(await readFile(file, "utf8"), "json file"); } async function optionalJsonFile(args: ParsedArgs): Promise { + 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 { + 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 { @@ -1433,7 +1568,7 @@ function cancelBody(args: ParsedArgs): JsonRecord { function help(args: ParsedArgs, group?: string): JsonRecord { const commands = [ - "runs create --json-file ", + "runs create --json-file |--json-stdin", "runs show ", "runs events --after-seq --limit [--summary|--tail-summary] [--tail ] [--summary-chars ] [--format json|tsv]", "runs result [--command-id ]", @@ -1443,13 +1578,13 @@ function help(args: ParsedArgs, group?: string): JsonRecord { "sessions storage ", "sessions storage --delete", "sessions show [--reader-id ]", - "sessions turn [sessionId] --json-file --prompt-file [--profile codex|deepseek|minimax-m3|dsflash-go||M3] [--runner-json-file ]", - "sessions steer --prompt-file ", + "sessions turn [sessionId] [--json-file |--json-stdin] [--prompt-file |--prompt-stdin|--prompt ] [--profile codex|deepseek|minimax-m3|dsflash-go||M3] [--runner-json-file |--runner-json-stdin]", + "sessions steer [--prompt-file |--prompt-stdin|--prompt ]", "sessions cancel [--reason ] [--full|--raw]", - "sessions trace [--after-seq ] [--limit ] [--run-id ] [--summary-chars ] [--full|--raw]", - "sessions output [--after-seq ] [--limit ] [--run-id ] [--summary-chars ] [--full|--raw]", + "sessions trace [--after-seq ] [--limit ] [--run-id ] [--summary-chars ] [--include-output] [--seq |--event-id |--item-id ] [--full|--raw]", + "sessions output [--after-seq ] [--limit ] [--run-id ] [--summary-chars ] [--include-output] [--seq |--event-id |--item-id ] [--full|--raw]", "sessions read [--reader-id ] [--full|--raw]", - "commands create --type turn|steer|interrupt --json-file ", + "commands create --type turn|steer|interrupt --json-file |--json-stdin", "commands show --run-id ", "commands result --run-id ", "commands cancel [--reason ]", @@ -1458,14 +1593,14 @@ function help(args: ParsedArgs, group?: string): JsonRecord { "runner job --dry-run --run-id --command-id --image ", "runner jobs --run-id [--command-id ]", "runner job-status [runnerJobId] --run-id ", - "queue submit --json-file [--idempotency-key ] [--dry-run]", + "queue submit --json-file |--json-stdin [--idempotency-key ] [--dry-run]", "queue list [--queue ] [--state ] [--cursor ] [--limit ] [--updated-after ] [--full|--raw]", "queue show [--full|--raw]", "queue stats [--queue ]", "queue commander [--queue ] [--reader-id ] [--limit ] [--full|--raw]", "queue read [--reader-id ] [--dry-run] [--full|--raw]", "queue cancel [--reason ] [--dry-run] [--full|--raw]", - "queue dispatch [--json-file ] [--idempotency-key ] [--image ] [--namespace ] [--dry-run] [--full|--raw]", + "queue dispatch [--json-file |--json-stdin] [--idempotency-key ] [--image ] [--namespace ] [--dry-run] [--full|--raw]", "queue refresh [--dry-run] [--full|--raw]", "secrets codex render --dry-run [--profile codex|deepseek|minimax-m3|dsflash-go|] [--codex-home ] [--model-catalog-file ] [--namespace agentrun-v01] [--secret-name ]", "provider-profiles list", diff --git a/src/selftest/cases/15-cli-events-summary.ts b/src/selftest/cases/15-cli-events-summary.ts index 202d460..7355848 100644 --- a/src/selftest/cases/15-cli-events-summary.ts +++ b/src/selftest/cases/15-cli-events-summary.ts @@ -34,30 +34,52 @@ export default function selfTest(_context: SelfTestContext): SelfTestResult { const sessionSummary = summarizeSessionEventPage({ sessionId: "sess_noise", runId: "run_noise", - items: [{ - seq: 266, - type: "backend_status", - payload: { - phase: "turn/cancelled", - status: "cancelled", - commandId: "cmd_noise", - runnerTrace: { raw: hugeRunnerTrace }, - rawEvent: { nested: hugeRunnerTrace }, + items: [ + { id: "evt_tool", seq: 265, type: "tool_call", payload: { itemId: "tool_noise", method: "commandExecution", status: "completed", command: "printf hidden", outputSummary: "tool completed" } }, + { id: "evt_output", seq: 266, type: "command_output", payload: { itemId: "tool_noise", text: hugeRunnerTrace, outputSummary: "large stdout", outputBytes: 100_000, outputTruncated: true } }, + { + id: "evt_status", + seq: 267, + type: "backend_status", + payload: { + phase: "turn/cancelled", + status: "cancelled", + commandId: "cmd_noise", + runnerTrace: { raw: hugeRunnerTrace }, + rawEvent: { nested: hugeRunnerTrace }, + }, }, - }], - count: 1, + ], + count: 3, cursor: "266", - }, { kind: "trace", sessionId: "sess_noise", afterSeq: 250, limit: 100, runId: null, tail: null, summaryChars: 80 }); + }, { kind: "trace", sessionId: "sess_noise", afterSeq: 250, limit: 100, runId: null, tail: null, summaryChars: 80, includeOutput: false }); const sessionItems = sessionSummary.items as JsonRecord[]; assert.equal(sessionSummary.action, "session-trace-summary"); - assert.equal(sessionSummary.nextAfterSeq, 266); - assert.equal(sessionItems[0]?.hasRunnerTrace, true); - assert.ok(Number(sessionItems[0]?.runnerTraceBytes) > 10_000); + assert.equal(sessionSummary.sourceCount, 3); + assert.equal(sessionSummary.displayedCount, 1); + assert.equal(sessionSummary.nextAfterSeq, 267); + assert.equal((sessionSummary.suppressedEvents as JsonRecord).count, 2); + assert.equal(((sessionSummary.suppressedEvents as JsonRecord).byType as JsonRecord).command_output, 1); + assert.equal(((sessionSummary.suppressedEvents as JsonRecord).byType as JsonRecord).backend_status, 1); + assert.deepEqual(sessionItems.map((item) => item.type), ["tool_call"]); + assert.equal(((sessionItems[0]?.detailCommands as JsonRecord).item as string).includes("--item-id tool_noise"), true); + assert.equal(((sessionItems[0]?.detailCommands as JsonRecord).seq as string).includes("--seq 265"), true); + assert.ok(Number((sessionSummary.suppressedEvents as JsonRecord).outputBytes) > 0); assert.equal(JSON.stringify(sessionSummary).includes("runner trace line"), false); assert.equal(String((sessionSummary.drillDownCommands as JsonRecord).full).includes("--full"), true); assert.equal(String((sessionSummary.drillDownCommands as JsonRecord).raw).includes("--raw"), true); assertNoSecretLeak(sessionSummary); + const sessionOutputIncluded = summarizeSessionEventPage({ + sessionId: "sess_noise", + runId: "run_noise", + items: [{ id: "evt_output", seq: 266, type: "command_output", payload: { itemId: "tool_noise", text: hugeRunnerTrace, outputSummary: "large stdout", outputBytes: 100_000, outputTruncated: true } }], + count: 1, + cursor: "266", + }, { kind: "output", sessionId: "sess_noise", afterSeq: 250, limit: 100, runId: "run_noise", tail: null, summaryChars: 80, includeOutput: true }); + assert.equal(((sessionOutputIncluded.items as JsonRecord[])[0]?.detailCommands as JsonRecord).seq, "./scripts/agentrun sessions output sess_noise --seq 266 --run-id run_noise --full"); + assert.equal(JSON.stringify(sessionOutputIncluded).includes("runner trace line"), false); + const queueSummary = summarizeQueueDispatchResult({ action: "queue-dispatch", mutation: true,