fix: 收敛 session detail 按 id 精确拉取 (#137)
Co-authored-by: Codex <codex@pikas.tech>
This commit is contained in:
@@ -73,8 +73,8 @@ CLI 官方 TypeScript 入口固定为 `scripts/agentrun-cli.ts`。在 G14 非交
|
||||
./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>] [--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 trace <sessionId> [--after-seq <n>] [--limit <limit>] [--run-id <runId>] [--include-output] [--seq <n>|--event-id <id>|--item-id <id>] [--detail-scan-pages <n>] [--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>] [--detail-scan-pages <n>] [--full|--raw]
|
||||
./scripts/agentrun sessions read <sessionId> [--reader-id <reader>]
|
||||
```
|
||||
|
||||
@@ -105,7 +105,7 @@ CLI 官方 TypeScript 入口固定为 `scripts/agentrun-cli.ts`。在 G14 非交
|
||||
- `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 可完整追溯。
|
||||
- `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` 做定点展开;默认摘要生成的 `detailCommands` 必须带上能定位该 event 的最小 `--after-seq`/`--limit` hint,避免按 id 拉详情时重新扫描长 trace。这样保证默认不爆上下文,同时按 id/seq 可完整追溯。
|
||||
|
||||
## 配置与 Secret 边界
|
||||
|
||||
|
||||
+57
-7
@@ -355,14 +355,16 @@ function withSessionDetailCommands(items: JsonRecord[], kind: "trace" | "output"
|
||||
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`;
|
||||
const pageHint = seq === null ? "" : ` --after-seq ${Math.max(0, seq - 1)} --limit 1`;
|
||||
const runFlag = runId ? ` --run-id ${runId}` : "";
|
||||
const detail = seq === null ? null : `./scripts/agentrun sessions ${kind} ${sessionId}${pageHint} --seq ${seq}${runFlag} --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` } : {}),
|
||||
...(itemId ? { item: `./scripts/agentrun sessions ${kind} ${sessionId}${pageHint} --item-id ${itemId}${runFlag} --full` } : {}),
|
||||
...(eventId ? { event: `./scripts/agentrun sessions ${kind} ${sessionId}${pageHint} --event-id ${eventId}${runFlag} --full` } : {}),
|
||||
},
|
||||
} : {}),
|
||||
};
|
||||
@@ -390,6 +392,13 @@ function sessionEventDetailResult(page: JsonValue, options: { kind: "trace" | "o
|
||||
},
|
||||
});
|
||||
}
|
||||
return sessionEventDetailResultFromMatches(page, matches, options);
|
||||
}
|
||||
|
||||
function sessionEventDetailResultFromMatches(page: JsonValue, matches: JsonRecord[], options: { kind: "trace" | "output"; sessionId: string; runId: string | null; summaryChars: number; filter: SessionEventDetailFilter; pagesScanned?: number; eventsScanned?: number }): 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 runId = stringValue(record.runId) ?? options.runId;
|
||||
return {
|
||||
action: `session-${options.kind}-event-detail`,
|
||||
@@ -398,6 +407,8 @@ function sessionEventDetailResult(page: JsonValue, options: { kind: "trace" | "o
|
||||
filter: options.filter as unknown as JsonRecord,
|
||||
sourceCount: events.length,
|
||||
count: matches.length,
|
||||
...(options.pagesScanned === undefined ? {} : { pagesScanned: options.pagesScanned }),
|
||||
...(options.eventsScanned === undefined ? {} : { eventsScanned: options.eventsScanned }),
|
||||
valuesPrinted: false,
|
||||
items: matches.map((event) => ({
|
||||
summary: summarizeRunEvent(event, options.summaryChars),
|
||||
@@ -829,15 +840,19 @@ async function listSessions(args: ParsedArgs): Promise<JsonValue> {
|
||||
async function sessionEvents(args: ParsedArgs, sessionId: string, kind: "trace" | "output"): Promise<JsonValue> {
|
||||
const params = new URLSearchParams();
|
||||
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 hasExplicitAfterSeq = optionalFlag(args, "after-seq") !== null;
|
||||
const afterSeq = requestedSeq !== null && !hasExplicitAfterSeq ? 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");
|
||||
const detailFilter = sessionEventDetailFilter(args, requestedSeq);
|
||||
if (detailFilter && requestedSeq === null && !hasExplicitAfterSeq && (detailFilter.eventId || detailFilter.itemId)) {
|
||||
return scanSessionEventDetail(args, { kind, sessionId, runId, afterSeq, limit, summaryChars: integerFlag(args, "summary-chars", 1_200, { min: 1, max: 8_000 }), filter: detailFilter });
|
||||
}
|
||||
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, {
|
||||
@@ -852,6 +867,41 @@ async function sessionEvents(args: ParsedArgs, sessionId: string, kind: "trace"
|
||||
});
|
||||
}
|
||||
|
||||
async function scanSessionEventDetail(args: ParsedArgs, options: { kind: "trace" | "output"; sessionId: string; runId: string | null; afterSeq: number; limit: number; summaryChars: number; filter: SessionEventDetailFilter }): Promise<JsonRecord> {
|
||||
const maxPages = integerFlag(args, "detail-scan-pages", 20, { min: 1, max: 100 });
|
||||
let afterSeq = options.afterSeq;
|
||||
let pagesScanned = 0;
|
||||
let eventsScanned = 0;
|
||||
while (pagesScanned < maxPages) {
|
||||
const params = new URLSearchParams();
|
||||
params.set("afterSeq", String(afterSeq));
|
||||
params.set("limit", String(options.limit));
|
||||
if (options.runId) params.set("runId", options.runId);
|
||||
const query = params.toString();
|
||||
const page = await client(args).get(`/api/v1/sessions/${encodeURIComponent(options.sessionId)}/${options.kind}${query ? `?${query}` : ""}`);
|
||||
const events = eventPageItems(page);
|
||||
pagesScanned += 1;
|
||||
eventsScanned += events.length;
|
||||
const matches = events.filter((event) => matchesSessionEventFilter(event, options.filter));
|
||||
if (matches.length > 0) return sessionEventDetailResultFromMatches(page, matches, { kind: options.kind, sessionId: options.sessionId, runId: options.runId, summaryChars: options.summaryChars, filter: options.filter, pagesScanned, eventsScanned });
|
||||
const lastSeq = events.length > 0 ? numberValue(events[events.length - 1]?.seq) : null;
|
||||
const cursorSeq = numberValue(jsonRecordValue(page)?.cursor);
|
||||
const nextSeq = cursorSeq ?? lastSeq;
|
||||
if (nextSeq === null || nextSeq <= afterSeq) break;
|
||||
afterSeq = nextSeq;
|
||||
}
|
||||
throw new AgentRunError("schema-invalid", "no session event matched --event-id/--item-id in scanned pages", {
|
||||
httpStatus: 2,
|
||||
details: {
|
||||
filter: options.filter as unknown as JsonRecord,
|
||||
pagesScanned,
|
||||
eventsScanned,
|
||||
nextAfterSeq: afterSeq,
|
||||
hint: "use the detailCommands from the summary, pass --seq, or add --after-seq/--limit near the event if the trace is longer than the scan window",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function sessionCreate(args: ParsedArgs, positionalSessionId: string | null): Promise<JsonRecord> {
|
||||
const sessionId = positionalSessionId ?? optionalFlag(args, "session-id") ?? newSessionId();
|
||||
const profile = normalizeProfile(optionalFlag(args, "profile") ?? optionalFlag(args, "backend-profile") ?? "codex");
|
||||
@@ -1581,8 +1631,8 @@ function help(args: ParsedArgs, group?: string): JsonRecord {
|
||||
"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>] [--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 trace <sessionId> [--after-seq <n>] [--limit <n>] [--run-id <runId>] [--summary-chars <n>] [--include-output] [--seq <n>|--event-id <id>|--item-id <id>] [--detail-scan-pages <n>] [--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>] [--detail-scan-pages <n>] [--full|--raw]",
|
||||
"sessions read <sessionId> [--reader-id <reader>] [--full|--raw]",
|
||||
"commands create <runId> --type turn|steer|interrupt --json-file <payload.json>|--json-stdin",
|
||||
"commands show <commandId> --run-id <runId>",
|
||||
|
||||
@@ -62,6 +62,7 @@ export default function selfTest(_context: SelfTestContext): SelfTestResult {
|
||||
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("--after-seq 264 --limit 1"), true);
|
||||
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);
|
||||
@@ -77,7 +78,7 @@ export default function selfTest(_context: SelfTestContext): SelfTestResult {
|
||||
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(((sessionOutputIncluded.items as JsonRecord[])[0]?.detailCommands as JsonRecord).seq, "./scripts/agentrun sessions output sess_noise --after-seq 265 --limit 1 --seq 266 --run-id run_noise --full");
|
||||
assert.equal(JSON.stringify(sessionOutputIncluded).includes("runner trace line"), false);
|
||||
|
||||
const queueSummary = summarizeQueueDispatchResult({
|
||||
|
||||
Reference in New Issue
Block a user