fix: 收敛 session 输出渐进披露与 stdin 输入 (#136)
Co-authored-by: Codex <codex@pikas.tech>
This commit is contained in:
@@ -34,12 +34,12 @@ CLI 官方 TypeScript 入口固定为 `scripts/agentrun-cli.ts`。在 G14 非交
|
||||
## 初始命令族
|
||||
|
||||
```bash
|
||||
./scripts/agentrun runs create --json-file <run.json>
|
||||
./scripts/agentrun runs create --json-file <run.json>|--json-stdin
|
||||
./scripts/agentrun runs show <runId>
|
||||
./scripts/agentrun runs events <runId> --after-seq <n> --limit <n> [--summary|--tail-summary] [--tail <n>] [--summary-chars <n>] [--format json|tsv]
|
||||
./scripts/agentrun runs result <runId> [--command-id <commandId>]
|
||||
./scripts/agentrun runs cancel <runId> [--reason <text>]
|
||||
./scripts/agentrun commands create <runId> --type turn|steer|interrupt --json-file <payload.json>
|
||||
./scripts/agentrun commands create <runId> --type turn|steer|interrupt --json-file <payload.json>|--json-stdin
|
||||
./scripts/agentrun commands show <commandId> --run-id <runId>
|
||||
./scripts/agentrun commands result <commandId> --run-id <runId>
|
||||
./scripts/agentrun commands cancel <commandId> [--reason <text>]
|
||||
@@ -59,22 +59,22 @@ CLI 官方 TypeScript 入口固定为 `scripts/agentrun-cli.ts`。在 G14 非交
|
||||
./scripts/agentrun server status [--port <port>]
|
||||
./scripts/agentrun server logs [--port <port>] [--tail-bytes <bytes>] [--log-file <path>]
|
||||
./scripts/agentrun server stop [--port <port>]
|
||||
./scripts/agentrun queue submit --json-file <task.json> [--dry-run]
|
||||
./scripts/agentrun queue submit --json-file <task.json>|--json-stdin [--dry-run]
|
||||
./scripts/agentrun queue list [--queue <queue>] [--state <state>] [--cursor <cursor>] [--limit <limit>] [--full|--raw]
|
||||
./scripts/agentrun queue show <taskId> [--full|--raw]
|
||||
./scripts/agentrun queue stats [--queue <queue>]
|
||||
./scripts/agentrun queue commander [--queue <queue>] [--reader-id <reader>] [--limit <display-limit>] [--full|--raw]
|
||||
./scripts/agentrun queue read <taskId> [--reader-id <reader>] [--dry-run] [--full|--raw]
|
||||
./scripts/agentrun queue cancel <taskId> [--reason <text>] [--dry-run] [--full|--raw]
|
||||
./scripts/agentrun queue dispatch <taskId> [--json-file <dispatch.json>] [--dry-run] [--full|--raw]
|
||||
./scripts/agentrun queue dispatch <taskId> [--json-file <dispatch.json>|--json-stdin] [--dry-run] [--full|--raw]
|
||||
./scripts/agentrun queue refresh <taskId> [--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 <reader>]
|
||||
./scripts/agentrun sessions show <sessionId> [--reader-id <reader>]
|
||||
./scripts/agentrun sessions turn [sessionId] --json-file <run-base.json> --prompt-file <file> [--profile codex|deepseek|minimax-m3|dsflash-go|M3] [--runner-json-file <job.json>] [--no-runner-job]
|
||||
./scripts/agentrun sessions steer <sessionId> --prompt-file <file>
|
||||
./scripts/agentrun sessions turn [sessionId] [--json-file <run-base.json>|--json-stdin] [--prompt-file <file>|--prompt-stdin|--prompt <text>] [--profile codex|deepseek|minimax-m3|dsflash-go|M3] [--runner-json-file <job.json>|--runner-json-stdin] [--no-runner-job]
|
||||
./scripts/agentrun sessions steer <sessionId> [--prompt-file <file>|--prompt-stdin|--prompt <text>]
|
||||
./scripts/agentrun sessions cancel <sessionId> [--reason <text>]
|
||||
./scripts/agentrun sessions trace <sessionId> [--after-seq <n>] [--limit <limit>] [--run-id <runId>]
|
||||
./scripts/agentrun sessions output <sessionId> [--after-seq <n>] [--limit <limit>] [--run-id <runId>]
|
||||
./scripts/agentrun sessions trace <sessionId> [--after-seq <n>] [--limit <limit>] [--run-id <runId>] [--include-output] [--seq <n>|--event-id <id>|--item-id <id>] [--full|--raw]
|
||||
./scripts/agentrun sessions output <sessionId> [--after-seq <n>] [--limit <limit>] [--run-id <runId>] [--include-output] [--seq <n>|--event-id <id>|--item-id <id>] [--full|--raw]
|
||||
./scripts/agentrun sessions read <sessionId> [--reader-id <reader>]
|
||||
```
|
||||
|
||||
@@ -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 <n>`、`--event-id <id>`、`--item-id <id>`、`--include-output`、`--full`/`--raw` 做定点展开;这样保证默认不爆上下文,同时按 id/seq 可完整追溯。
|
||||
|
||||
## 配置与 Secret 边界
|
||||
|
||||
|
||||
+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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user