pi-firecode 0.8.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -15
- package/activity.ts +92 -0
- package/busy.ts +188 -0
- package/config.example.jsonc +38 -47
- package/config.ts +198 -172
- package/deliver.ts +100 -13
- package/flame.ts +149 -0
- package/format.ts +84 -31
- package/header.ts +208 -82
- package/index.ts +17 -11
- package/master/actions.ts +293 -0
- package/master/activity-list.ts +281 -0
- package/master/event-card.ts +18 -37
- package/master/event-format.ts +71 -40
- package/master/guard.ts +46 -0
- package/master/index.ts +88 -1036
- package/master/list-view.ts +129 -0
- package/master/outbox.ts +188 -0
- package/master/prompts/master.zh.md +17 -12
- package/master/prompts/worker.zh.md +1 -1
- package/master/role.ts +6 -1
- package/master/run.ts +315 -0
- package/master/runtime.ts +259 -0
- package/master/spawn.ts +79 -38
- package/master/state.ts +30 -6
- package/master/worker-view.ts +517 -0
- package/package.json +1 -1
- package/provider/claude-sub.ts +35 -13
- package/provider/openai-native/src/compact-client.ts +2 -14
- package/provider/openai-native/src/options.ts +4 -2
- package/review/advisor.ts +0 -2
- package/review/card.ts +36 -23
- package/review/checkpoint.ts +2 -8
- package/review/evidence.ts +42 -44
- package/review/index.ts +271 -525
- package/review/occupancy.ts +25 -0
- package/review/outcome.ts +31 -7
- package/review/prompt.ts +7 -8
- package/review/prompts/review.en.md +7 -6
- package/review/prompts/review.zh.md +7 -6
- package/review/reviewer.ts +2 -5
- package/review/session.ts +11 -30
- package/review/state.ts +32 -60
- package/review/ui.ts +30 -469
- package/round-recorder.ts +18 -0
- package/session/herdr-display.ts +1 -1
- package/session/presets.ts +130 -87
- package/session/quota.ts +123 -0
- package/session/stats.ts +4 -1
- package/statusbar/index.ts +242 -104
- package/statusbar/render.ts +95 -163
- package/theme.ts +2 -41
- package/tools/actions.ts +53 -0
- package/tools/assistant-view.ts +64 -0
- package/tools/click-anchor.ts +56 -0
- package/tools/group-view.ts +420 -0
- package/tools/grouping.ts +146 -221
- package/tools/host.ts +369 -0
- package/tools/index.ts +108 -105
- package/tools/line.ts +95 -58
- package/tools/machine.ts +78 -0
- package/tools/parts.ts +2 -2
- package/tools/round.ts +95 -0
- package/tools/turn-clock.ts +41 -0
- package/tools/turn-summary.ts +109 -0
- package/truncated-write.ts +18 -0
- package/watcher/card.ts +28 -22
- package/watcher/index.ts +16 -44
- package/watcher/observer.ts +3 -3
- package/watcher/prompts/watch.zh.md +1 -1
- package/watcher/transcript.ts +4 -12
- package/flame-frames.ts +0 -460
- package/review/progress.ts +0 -324
- package/session/bark.ts +0 -157
- package/session/working-flame.ts +0 -116
- package/statusbar/quota-cache.ts +0 -60
- package/statusbar/quota-parse.ts +0 -85
- package/statusbar/quota.ts +0 -203
- package/statusbar/tps.ts +0 -87
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 审查占用频道:review 是唯一发布者,herdr 集成、输入框外壳与观察员订阅;频道名与 payload 只在这里定义。
|
|
3
|
+
* 消费者按 active 的 true/false 做计数配对,所以进度变化不能靠重发 true:持有时带活的 progress 访问器,
|
|
4
|
+
* 外壳每次绘制调用它。progress 是进程内求值函数,频道不可序列化转发。
|
|
5
|
+
*/
|
|
6
|
+
export const OCCUPANCY_CHANNEL = "herdr:blocked";
|
|
7
|
+
/** 持有时的展示标签:herdr 侧边栏与 Master 的 state_labels 判定都读它。 */
|
|
8
|
+
export const OCCUPANCY_LABEL = "对抗审查进行中";
|
|
9
|
+
|
|
10
|
+
/** 审查此刻在哪一步:排队等回合结束、审查者在审、顾问介入、执行模型修复、总结回合。 */
|
|
11
|
+
export type ReviewStage = "queued" | "reviewing" | "advisor" | "fixing" | "summarizing";
|
|
12
|
+
|
|
13
|
+
export interface ReviewProgress {
|
|
14
|
+
stage: ReviewStage;
|
|
15
|
+
/** 当前轮次;排队时为 0。 */
|
|
16
|
+
round: number;
|
|
17
|
+
/** 本轮通过 / 阻断 / 审查者总数;只有 reviewing 有意义。 */
|
|
18
|
+
passed: number;
|
|
19
|
+
blocked: number;
|
|
20
|
+
total: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type OccupancyPayload =
|
|
24
|
+
| { active: true; label: string; progress: () => ReviewProgress | undefined }
|
|
25
|
+
| { active: false };
|
package/review/outcome.ts
CHANGED
|
@@ -2,9 +2,6 @@ import { readFileSync } from "node:fs";
|
|
|
2
2
|
import { CHECKPOINT_TYPE, isValidCheckpoint } from "./checkpoint.js";
|
|
3
3
|
import type { ReviewState } from "./state.js";
|
|
4
4
|
|
|
5
|
-
/** 审查活跃期在 herdr:blocked 频道发布的展示标签。 */
|
|
6
|
-
export const REVIEW_OCCUPANCY_LABEL = "对抗审查进行中";
|
|
7
|
-
|
|
8
5
|
export type ReviewOutcome =
|
|
9
6
|
| { status: "passed"; runId: string; rounds: number }
|
|
10
7
|
| { status: "stopped"; runId: string; rounds: number; advisorAdvice?: string }
|
|
@@ -13,7 +10,7 @@ export type ReviewOutcome =
|
|
|
13
10
|
| { status: "none"; runId?: string }
|
|
14
11
|
| { status: "error"; message: string };
|
|
15
12
|
|
|
16
|
-
/** 只读 Worker session,解析最近一条 fire-review checkpoint
|
|
13
|
+
/** 只读 Worker session,解析最近一条 fire-review checkpoint 的判定;会话结束时的一次性兜底读取用它。 */
|
|
17
14
|
export function readReviewOutcome(sessionPath: string): ReviewOutcome {
|
|
18
15
|
let content: string;
|
|
19
16
|
try {
|
|
@@ -41,20 +38,47 @@ export function readReviewOutcome(sessionPath: string): ReviewOutcome {
|
|
|
41
38
|
else damage ??= "fire-review checkpoint 格式无效";
|
|
42
39
|
}
|
|
43
40
|
if (!latest) return damage ? { status: "error", message: damage } : { status: "none" };
|
|
41
|
+
return outcomeOf(latest);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** 会话事件里刚追加的一条记录若是有效 checkpoint,给出它的判定;订阅方据此增量跟进,不重读整份 JSONL。 */
|
|
45
|
+
export function outcomeOfEntry(entry: unknown): ReviewOutcome | undefined {
|
|
46
|
+
const state = checkpointOf(entry);
|
|
47
|
+
return state && outcomeOf(state);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** 审查进行中的轮次与审查者进度;不是审查相的 checkpoint 或不是 checkpoint 都给 undefined。 */
|
|
51
|
+
export interface ReviewProgress {
|
|
52
|
+
round: number;
|
|
53
|
+
settled: number;
|
|
54
|
+
total: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function reviewProgressOf(entry: unknown): ReviewProgress | undefined {
|
|
58
|
+
const active = checkpointOf(entry)?.active;
|
|
59
|
+
return active ? { round: active.round, settled: active.settledCount, total: active.reviewers.length } : undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function checkpointOf(entry: unknown): ReviewState | undefined {
|
|
63
|
+
return isCheckpointEntry(entry) && isValidCheckpoint(entry.data) ? entry.data as ReviewState : undefined;
|
|
64
|
+
}
|
|
44
65
|
|
|
66
|
+
function outcomeOf(latest: ReviewState): ReviewOutcome {
|
|
45
67
|
if (latest.phase === "idle") return { status: "none", runId: latest.runId };
|
|
46
68
|
if (latest.phase !== "settled") return { status: "in_progress", runId: latest.runId };
|
|
47
69
|
const rounds = latest.history.length;
|
|
48
|
-
const
|
|
70
|
+
const last = latest.history.at(-1);
|
|
71
|
+
const result = last?.result;
|
|
49
72
|
if (result === "passed") return { status: "passed", runId: latest.runId, rounds };
|
|
50
73
|
// stopped(顾问叫停)与 failed(maxRounds 用尽)都是质量裁决终止;
|
|
51
74
|
// error / cancelled / timed_out 是基础设施故障或人为中断,不弱化成“停止”。
|
|
52
75
|
if (result === "stopped" || result === "failed") {
|
|
53
76
|
// 顾问叫停时把裁决带给读取方:Master 拿到停止原因才能调整方向。
|
|
54
|
-
const advice =
|
|
77
|
+
const advice = last?.advisor?.advice;
|
|
55
78
|
return { status: "stopped", runId: latest.runId, rounds, ...(advice ? { advisorAdvice: advice } : {}) };
|
|
56
79
|
}
|
|
57
|
-
|
|
80
|
+
// 轮记录的 details 已写明故障形态(超时/供应商报错);枚举名只是它缺失时的兜底。
|
|
81
|
+
return { status: "failed", runId: latest.runId, rounds, reason: last?.details?.trim() || result || "unknown" };
|
|
58
82
|
}
|
|
59
83
|
|
|
60
84
|
function isCheckpointEntry(value: unknown): value is { data: unknown } {
|
package/review/prompt.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { readFileSync } from "node:fs";
|
|
|
6
6
|
import { dirname, join } from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import type { Language } from "../config.js";
|
|
9
|
+
import { wrapEnvelope } from "../deliver.js";
|
|
9
10
|
import type { AdvisorResult, ReviewState, SummaryKind } from "./state.js";
|
|
10
11
|
|
|
11
12
|
const PROMPTS_DIR = join(dirname(fileURLToPath(import.meta.url)), "prompts");
|
|
@@ -67,7 +68,7 @@ function reviewReminder(language: Language) {
|
|
|
67
68
|
}
|
|
68
69
|
|
|
69
70
|
/** 往轮 FAIL 发现清单(两相收敛的闭环输入):第 2 轮起注入。 */
|
|
70
|
-
|
|
71
|
+
function priorRoundsSection(
|
|
71
72
|
history: ReviewState["history"],
|
|
72
73
|
round: number,
|
|
73
74
|
language: Language,
|
|
@@ -141,12 +142,9 @@ export function buildFixFeedback(input: FixFeedbackInput): string {
|
|
|
141
142
|
: "顾问建议";
|
|
142
143
|
parts.push("", `${label}${input.language === "en" ? ":" : ":"}`, input.advisor.advice);
|
|
143
144
|
}
|
|
144
|
-
return
|
|
145
|
+
return wrapEnvelope("firecode_review", parts.join("\n"));
|
|
145
146
|
}
|
|
146
147
|
|
|
147
|
-
export function reviewEnvelope(content: string): string {
|
|
148
|
-
return `<firecode_review>\n${content}\n</firecode_review>`;
|
|
149
|
-
}
|
|
150
148
|
|
|
151
149
|
function narrowInstruction(language: Language, narrowed: boolean) {
|
|
152
150
|
if (!narrowed) return language === "en" ? FIX_INSTRUCTION_EN : FIX_INSTRUCTION_ZH;
|
|
@@ -175,14 +173,15 @@ export interface SummaryPromptInput {
|
|
|
175
173
|
|
|
176
174
|
/** 质量裁决终态后投给执行模型的总结回合提示:人话收尾,带反循环禁令。 */
|
|
177
175
|
export function buildSummaryPrompt(input: SummaryPromptInput): string {
|
|
178
|
-
const
|
|
179
|
-
|
|
176
|
+
const omitted = input.material.length - SUMMARY_MATERIAL_LIMIT;
|
|
177
|
+
const material = omitted > 0
|
|
178
|
+
? `${input.material.slice(0, SUMMARY_MATERIAL_LIMIT)}\n${input.language === "en" ? `[material truncated: ${omitted} characters omitted]` : `[材料截断:省略 ${omitted} 字]`}`
|
|
180
179
|
: input.material;
|
|
181
180
|
const body = summaryInstruction(input.language, input.kind, input.rounds);
|
|
182
181
|
const content = material.trim()
|
|
183
182
|
? `${body}\n\n${summaryMaterialLabel(input.language, input.kind)}\n${material}`
|
|
184
183
|
: body;
|
|
185
|
-
return
|
|
184
|
+
return wrapEnvelope("firecode_review", content);
|
|
186
185
|
}
|
|
187
186
|
|
|
188
187
|
function summaryMaterialLabel(language: Language, kind: SummaryKind): string {
|
|
@@ -22,6 +22,8 @@ Requirement anchor: the first user message is the original request; later user m
|
|
|
22
22
|
Blocking candidates (High/Medium):
|
|
23
23
|
- Logic defects: wrong assumptions, missed edge cases, missing error handling, races
|
|
24
24
|
- Fake or insufficient tests: new logic uncovered, weak assertions, hardcoded bypass of real logic
|
|
25
|
+
- Acceptance-test integrity: the implementation commit modified acceptance tests (assertions, cases, special-cased inputs); acceptance tests are implementation-coupled or tautological and verify the implementation rather than the requirement; the red run did not fail for "not yet implemented"
|
|
26
|
+
- Engineering principles (Medium): a second source for the same fact, rule, or config; fallbacks, dead code, or stale notes kept for old paths; patching the symptom while the root cause stands; a new layer or abstraction that does not absorb existing duplication; new or touched tests that fail the better-test value gate
|
|
25
27
|
- Key delivery claims you verified to be false
|
|
26
28
|
- Regression risk: changes break existing behavior
|
|
27
29
|
- The original or corrected current scope is unmet
|
|
@@ -31,12 +33,11 @@ Non-blocking (always to suggestions, never FAIL): unrelated changes mixed into d
|
|
|
31
33
|
|
|
32
34
|
## Evidence
|
|
33
35
|
|
|
34
|
-
- Only two kinds of facts count: project files you actually read, and output of
|
|
35
|
-
-
|
|
36
|
-
-
|
|
37
|
-
-
|
|
38
|
-
-
|
|
39
|
-
- If you use bash, only run safe verification; never modify files, install dependencies, delete files, or run git reset/clean/checkout/commit/rebase.
|
|
36
|
+
- Only two kinds of facts count: project files you actually read, and output of verification commands you actually ran. Session evidence is a lead; key judgments return to these two.
|
|
37
|
+
- "evidence truncated" / "truncated, N chars" markers in the session record are omissions made when assembling evidence, not messages that were left unfinished; before judging a deliverable incomplete, read the original from the session file the marker points to.
|
|
38
|
+
- Attribution: the checkout may carry parallel work or pre-existing uncommitted changes; attribute by the tool trail in the session record (the paths this session actually edited). File scope-violation, unrelated-change, and command-failure findings only against changes attributable to this session; anything else goes to suggestions at most, failures with a rerun hint.
|
|
39
|
+
- PASS rests on reading the source relevant to this change and checking its logic; run verification through the project's existing entry points at the narrowest scope covering the change, widening only when the affected scope cannot be judged.
|
|
40
|
+
- bash is for verification only; never modify files, install dependencies, delete files, or run git reset/clean/checkout/commit/rebase.
|
|
40
41
|
|
|
41
42
|
## Two-phase convergence (no lowering the bar)
|
|
42
43
|
|
|
@@ -22,6 +22,8 @@
|
|
|
22
22
|
阻塞项(高/中候选):
|
|
23
23
|
- 逻辑缺陷:错误假设、边界条件遗漏、错误处理缺失、竞态
|
|
24
24
|
- 虚假或不充分的测试:未覆盖新逻辑、断言过弱、硬编码绕过真实逻辑
|
|
25
|
+
- 验收测试失信:实现提交改动了验收测试(断言、用例、特判输入);验收测试与实现耦合或同义反复,验的不是需求;红的原因不是"尚未实现"
|
|
26
|
+
- 工程原则(中):同一事实、规则或配置出现第二个来源;为兼容旧路径保留的回退、死代码或过时说明;在症状处打补丁而根因未动;新增中间层或抽象却没有收口既有重复;新增或触及的测试不过 better-test 的价值门
|
|
25
27
|
- 关键交付声明经你核实不成立
|
|
26
28
|
- 回归风险:变更破坏既有行为
|
|
27
29
|
- 原始需求或修正后的当前范围未被满足
|
|
@@ -31,12 +33,11 @@
|
|
|
31
33
|
|
|
32
34
|
## 证据规则
|
|
33
35
|
|
|
34
|
-
-
|
|
35
|
-
-
|
|
36
|
-
-
|
|
37
|
-
-
|
|
38
|
-
-
|
|
39
|
-
- 若使用 bash,只做安全验证;不得修改文件、安装依赖、删除文件,或执行 git reset/clean/checkout/commit/rebase。
|
|
36
|
+
- 事实证据只有两类:你实际读取的当前项目文件、你实际运行的验证命令输出。会话证据是线索,关键判断回到这两类证据。
|
|
37
|
+
- 会话记录里的「证据截断」「截断,原文 N 字」标记是证据组装为控制篇幅做的省略,不是消息本身没写完;判断交付是否完整前,按标记给出的会话文件路径用 read 核对原文。
|
|
38
|
+
- 归因:checkout 可能含并行工作或既有未提交改动,以会话记录中的工具轨迹(本会话实际编辑的路径)归因。范围越界、混入无关变更与命令失败只对归因到本会话的改动立案;归因不到的至多写入建议区,失败附复跑提示。
|
|
39
|
+
- PASS 建立在实读与本次变更相关的源码并核对逻辑之上;验证命令走项目现有入口,跑覆盖变更的最窄范围,影响范围无法判断时再扩大。
|
|
40
|
+
- bash 只做验证;不得修改文件、安装依赖、删除文件,或执行 git reset/clean/checkout/commit/rebase。
|
|
40
41
|
|
|
41
42
|
## 两相收敛(不降低质量标准)
|
|
42
43
|
|
package/review/reviewer.ts
CHANGED
|
@@ -19,8 +19,6 @@ export interface RunReviewerOptions {
|
|
|
19
19
|
language: Language;
|
|
20
20
|
signal?: AbortSignal;
|
|
21
21
|
runSession: ReviewSessionRunner;
|
|
22
|
-
/** 结构化会话事件:驱动活动条的实时进度。 */
|
|
23
|
-
onEvent?: (event: Record<string, unknown>) => void;
|
|
24
22
|
}
|
|
25
23
|
|
|
26
24
|
export type ParseOutcome = {
|
|
@@ -40,7 +38,6 @@ export async function runReviewer(options: RunReviewerOptions): Promise<Reviewer
|
|
|
40
38
|
cwd: options.cwd,
|
|
41
39
|
timeoutMs: options.config.timeoutMs,
|
|
42
40
|
signal: options.signal,
|
|
43
|
-
onEvent: options.onEvent,
|
|
44
41
|
});
|
|
45
42
|
const parsed =
|
|
46
43
|
result.kind === "output"
|
|
@@ -193,7 +190,7 @@ const FINDING_FIELDS = [
|
|
|
193
190
|
const SUGGESTIONS_HEADING = /^##\s+(?:建议(非阻塞)|Suggestions \(non-blocking\))\s*$/iu;
|
|
194
191
|
|
|
195
192
|
/** PASS 证据锚点闸门:摘要行在前,首个证据行必须同时含文件段(带扩展名)与命令段。 */
|
|
196
|
-
|
|
193
|
+
function passIssue(body: string): string | undefined {
|
|
197
194
|
const lines = body
|
|
198
195
|
.split(/\r?\n/)
|
|
199
196
|
.map((line) => line.trim())
|
|
@@ -211,7 +208,7 @@ export function passIssue(body: string): string | undefined {
|
|
|
211
208
|
* FAIL 发现闸门:至少一条带「问题」的发现,且不能全落在「建议(非阻塞)」区。
|
|
212
209
|
* 空 FAIL 或一段散文都不能驱动执行模型改代码——格式非法的票一律作废为基础设施错误。
|
|
213
210
|
*/
|
|
214
|
-
|
|
211
|
+
function failIssue(body: string, language: Language): string | undefined {
|
|
215
212
|
const noFinding =
|
|
216
213
|
language === "en"
|
|
217
214
|
? "FAIL has no blocking finding: a `## Finding` section is required"
|
package/review/session.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type { Model } from "@earendil-works/pi-ai";
|
|
2
1
|
import type { AgentSessionEvent } from "@earendil-works/pi-coding-agent";
|
|
3
2
|
import type { ThinkingLevelValue } from "../config.js";
|
|
4
3
|
import type { InProcessSessionPool } from "../master/spawn.js";
|
|
5
4
|
import type { PromptLayers } from "./prompt.js";
|
|
5
|
+
import { textOf } from "../format.js";
|
|
6
6
|
|
|
7
7
|
export type ReviewSessionResult =
|
|
8
8
|
| { kind: "output"; text: string }
|
|
@@ -13,7 +13,6 @@ export type ReviewSessionResult =
|
|
|
13
13
|
|
|
14
14
|
export interface ReviewSessionOptions {
|
|
15
15
|
pool: InProcessSessionPool;
|
|
16
|
-
resolveModel(id: string): Promise<Model<any>>;
|
|
17
16
|
role: "reviewer" | "advisor";
|
|
18
17
|
model: string;
|
|
19
18
|
thinking: ThinkingLevelValue;
|
|
@@ -22,28 +21,24 @@ export interface ReviewSessionOptions {
|
|
|
22
21
|
cwd: string;
|
|
23
22
|
timeoutMs: number;
|
|
24
23
|
signal?: AbortSignal;
|
|
25
|
-
onEvent?: (event: AgentSessionEvent) => void;
|
|
26
24
|
}
|
|
27
25
|
|
|
28
26
|
export type ReviewSessionRunner = (
|
|
29
|
-
options: Omit<ReviewSessionOptions, "pool"
|
|
27
|
+
options: Omit<ReviewSessionOptions, "pool">,
|
|
30
28
|
) => Promise<ReviewSessionResult>;
|
|
31
29
|
|
|
32
|
-
export function createReviewSessionRunner(
|
|
33
|
-
pool
|
|
34
|
-
resolveModel: ReviewSessionOptions["resolveModel"],
|
|
35
|
-
): ReviewSessionRunner {
|
|
36
|
-
return (options) => runReviewSession({ ...options, pool, resolveModel });
|
|
30
|
+
export function createReviewSessionRunner(pool: InProcessSessionPool): ReviewSessionRunner {
|
|
31
|
+
return (options) => runReviewSession({ ...options, pool });
|
|
37
32
|
}
|
|
38
33
|
|
|
39
|
-
|
|
34
|
+
async function runReviewSession(options: ReviewSessionOptions): Promise<ReviewSessionResult> {
|
|
40
35
|
if (options.signal?.aborted) return { kind: "aborted" };
|
|
41
36
|
let spawned: Awaited<ReturnType<InProcessSessionPool["spawn"]>>;
|
|
42
37
|
try {
|
|
43
38
|
spawned = await options.pool.spawn({
|
|
44
39
|
cwd: options.cwd,
|
|
45
40
|
role: options.role,
|
|
46
|
-
model: await options.resolveModel(options.model),
|
|
41
|
+
model: await options.pool.resolveModel(options.model),
|
|
47
42
|
thinking: options.thinking,
|
|
48
43
|
tools: [...new Set(options.tools)].filter((tool) => tool !== "write" && tool !== "edit"),
|
|
49
44
|
systemPrompt: { mode: "replace", text: clean(options.prompt.system) },
|
|
@@ -57,7 +52,7 @@ export async function runReviewSession(options: ReviewSessionOptions): Promise<R
|
|
|
57
52
|
: { kind: "error", message: errorText(error) };
|
|
58
53
|
}
|
|
59
54
|
if (options.signal?.aborted) {
|
|
60
|
-
spawned.dispose();
|
|
55
|
+
await spawned.dispose();
|
|
61
56
|
return { kind: "aborted" };
|
|
62
57
|
}
|
|
63
58
|
|
|
@@ -66,12 +61,11 @@ export async function runReviewSession(options: ReviewSessionOptions): Promise<R
|
|
|
66
61
|
const unsubscribe = spawned.session.subscribe((event) => {
|
|
67
62
|
const assistant = assistantMessage(event);
|
|
68
63
|
if (assistant) {
|
|
69
|
-
finalText =
|
|
64
|
+
finalText = textOf(assistant.content);
|
|
70
65
|
finalError = assistant.stopReason === "error"
|
|
71
66
|
? assistant.errorMessage || "model error"
|
|
72
67
|
: undefined;
|
|
73
68
|
}
|
|
74
|
-
options.onEvent?.(event);
|
|
75
69
|
});
|
|
76
70
|
let interrupted: "aborted" | "timeout" | undefined;
|
|
77
71
|
let wake!: () => void;
|
|
@@ -84,17 +78,15 @@ export async function runReviewSession(options: ReviewSessionOptions): Promise<R
|
|
|
84
78
|
finalError = errorText(error);
|
|
85
79
|
});
|
|
86
80
|
await Promise.race([run, interruption]);
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
return { kind: interrupted };
|
|
90
|
-
}
|
|
81
|
+
// 中断只靠 finally 的 dispose 收尾:pi 的 abort 在模型流卡死时永不返回,等它会拖住整个关闭链。
|
|
82
|
+
if (interrupted) return { kind: interrupted };
|
|
91
83
|
if (finalError) return { kind: "error", message: finalError };
|
|
92
84
|
return finalText?.trim() ? { kind: "output", text: finalText } : { kind: "empty" };
|
|
93
85
|
} finally {
|
|
94
86
|
clearTimeout(timeout);
|
|
95
87
|
options.signal?.removeEventListener("abort", onAbort);
|
|
96
88
|
unsubscribe();
|
|
97
|
-
spawned.dispose();
|
|
89
|
+
await spawned.dispose();
|
|
98
90
|
}
|
|
99
91
|
}
|
|
100
92
|
|
|
@@ -109,17 +101,6 @@ function assistantMessage(event: AgentSessionEvent): {
|
|
|
109
101
|
return [...event.messages].reverse().find((message) => message.role === "assistant");
|
|
110
102
|
}
|
|
111
103
|
|
|
112
|
-
function messageText(content: unknown): string {
|
|
113
|
-
if (typeof content === "string") return content;
|
|
114
|
-
if (!Array.isArray(content)) return "";
|
|
115
|
-
return content
|
|
116
|
-
.filter((part): part is { type: "text"; text: string } =>
|
|
117
|
-
typeof part === "object" && part !== null && (part as { type?: unknown }).type === "text"
|
|
118
|
-
&& typeof (part as { text?: unknown }).text === "string")
|
|
119
|
-
.map((part) => part.text)
|
|
120
|
-
.join("\n");
|
|
121
|
-
}
|
|
122
|
-
|
|
123
104
|
function clean(text: string): string {
|
|
124
105
|
return text.replaceAll("\0", "");
|
|
125
106
|
}
|
package/review/state.ts
CHANGED
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
* 事故终态(取消 / 超时 / 基础设施错误)直接 settled,不烧总结回合。
|
|
11
11
|
* 不变量:同一时刻至多一个活动轮;round 单调递增;history 只追加不改写。
|
|
12
12
|
*/
|
|
13
|
+
import type { ModelAtom } from "../config.js";
|
|
14
|
+
|
|
13
15
|
export type Phase =
|
|
14
16
|
| "idle"
|
|
15
17
|
| "queued"
|
|
@@ -25,10 +27,8 @@ export type AdvisorVerdict = "continue" | "stop" | "narrow";
|
|
|
25
27
|
export type StopReason = "advisor" | "max_rounds" | "user" | "shutdown" | "timeout";
|
|
26
28
|
|
|
27
29
|
/** 单个审查者的输出(output 契约解析结果,纯数据)。 */
|
|
28
|
-
export interface ReviewerResult {
|
|
30
|
+
export interface ReviewerResult extends ModelAtom {
|
|
29
31
|
index: number;
|
|
30
|
-
model: string;
|
|
31
|
-
thinking: string;
|
|
32
32
|
status: Exclude<ReviewerStatus, "running">;
|
|
33
33
|
/** 短摘要:PASS 一行收敛摘要 / FAIL 发现一句话。 */
|
|
34
34
|
summary: string;
|
|
@@ -37,10 +37,8 @@ export interface ReviewerResult {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
/** 审查中某个审查者的进行状态;settled 后携带完整结果。 */
|
|
40
|
-
export interface ActiveReviewer {
|
|
40
|
+
export interface ActiveReviewer extends ModelAtom {
|
|
41
41
|
index: number;
|
|
42
|
-
model: string;
|
|
43
|
-
thinking: string;
|
|
44
42
|
status: ReviewerStatus;
|
|
45
43
|
result: ReviewerResult | null;
|
|
46
44
|
}
|
|
@@ -125,7 +123,7 @@ export interface ReviewLimits {
|
|
|
125
123
|
advisorAfterFailures: number;
|
|
126
124
|
advisorModel: string;
|
|
127
125
|
/** 本轮审查者(model/thinking),beginRound 时填入 active。 */
|
|
128
|
-
reviewers:
|
|
126
|
+
reviewers: ModelAtom[];
|
|
129
127
|
language?: "zh" | "en";
|
|
130
128
|
}
|
|
131
129
|
|
|
@@ -242,7 +240,7 @@ function onStart(
|
|
|
242
240
|
if (state.phase === "reviewing" || state.phase === "needs_fix" || state.phase === "awaiting_fix")
|
|
243
241
|
return { state, effects: [] };
|
|
244
242
|
const focus = event.focus.trim();
|
|
245
|
-
//
|
|
243
|
+
// 排队相不发卡:输入框边框与审查活动行已各有一份排队提示,记录里只留开始/结果卡。
|
|
246
244
|
if (event.busy)
|
|
247
245
|
return {
|
|
248
246
|
state: {
|
|
@@ -490,7 +488,7 @@ function settleRound(
|
|
|
490
488
|
return {
|
|
491
489
|
state: { ...base, phase: "needs_fix", pending, consecutiveFailures },
|
|
492
490
|
// 顾问可能裁定 stop,反馈永远不会投递:此时不能提前宣布「已交回修复」;
|
|
493
|
-
//
|
|
491
|
+
// 卡里也不写「顾问介入中」——持久记录会过时,实况由审查活动行与输入框边框承担。
|
|
494
492
|
effects: [failCard, { kind: "advance" }],
|
|
495
493
|
};
|
|
496
494
|
return {
|
|
@@ -518,7 +516,6 @@ function onAdvisorSettled(
|
|
|
518
516
|
if (state.phase !== "needs_fix" || !state.pending) return { state, effects: [] };
|
|
519
517
|
const pending = state.pending;
|
|
520
518
|
const advisor = event.result;
|
|
521
|
-
const displayDetails = aggregateDetails(pending.reviewers, limits.language ?? "zh");
|
|
522
519
|
if (advisor.verdict === "stop") {
|
|
523
520
|
const round = roundRecord(pending.round, "stopped", advisor.advice, pending.reviewers, advisor, state.roundStartedAt, now);
|
|
524
521
|
return {
|
|
@@ -718,20 +715,14 @@ function aggregate(
|
|
|
718
715
|
archiveDetails: string;
|
|
719
716
|
summary: string;
|
|
720
717
|
} {
|
|
721
|
-
const
|
|
722
|
-
const
|
|
718
|
+
const verdicts = reviewers.filter((item) => item.status === "passed" || item.status === "failed");
|
|
719
|
+
const absent = reviewers.filter((item) => item.status === "error");
|
|
723
720
|
const archiveDetails = aggregateDetails(reviewers, language);
|
|
724
|
-
|
|
725
|
-
//
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
result: "error",
|
|
730
|
-
displayDetails: aggregateDetails(fatalErrors, language),
|
|
731
|
-
feedbackDetails: "",
|
|
732
|
-
archiveDetails,
|
|
733
|
-
summary: "",
|
|
734
|
-
};
|
|
721
|
+
// 有裁决就成轮:缺席者(会话故障或输出契约违例)只在结论里点名,不阻止形成质量结论;
|
|
722
|
+
// 全员缺席才是基础设施不可用。否则一个供应商额度耗尽就会让整条审查通道停摆。
|
|
723
|
+
if (verdicts.length === 0)
|
|
724
|
+
return { result: "error", displayDetails: archiveDetails, feedbackDetails: "", archiveDetails, summary: "" };
|
|
725
|
+
const failed = verdicts.filter((item) => item.status === "failed");
|
|
735
726
|
if (failed.length > 0)
|
|
736
727
|
return {
|
|
737
728
|
result: "failed",
|
|
@@ -740,65 +731,46 @@ function aggregate(
|
|
|
740
731
|
archiveDetails,
|
|
741
732
|
summary: "",
|
|
742
733
|
};
|
|
743
|
-
const passed = reviewers.filter((item) => item.status === "passed");
|
|
744
|
-
if (passed.length === 0)
|
|
745
|
-
return {
|
|
746
|
-
result: "error",
|
|
747
|
-
displayDetails: aggregateDetails(errors, language),
|
|
748
|
-
feedbackDetails: "",
|
|
749
|
-
archiveDetails,
|
|
750
|
-
summary: "",
|
|
751
|
-
};
|
|
752
734
|
return {
|
|
753
735
|
result: "passed",
|
|
754
736
|
displayDetails: archiveDetails,
|
|
755
737
|
feedbackDetails: "",
|
|
756
738
|
archiveDetails,
|
|
757
|
-
summary: aggregatePassSummary(
|
|
739
|
+
summary: aggregatePassSummary(verdicts, absent, language),
|
|
758
740
|
};
|
|
759
741
|
}
|
|
760
742
|
|
|
761
|
-
function isFormatError(details: string) {
|
|
762
|
-
return details.startsWith("审查输出格式无效") || details.startsWith("review output format invalid");
|
|
763
|
-
}
|
|
764
|
-
|
|
765
743
|
function aggregateDetails(reviewers: ReviewerResult[], language: "zh" | "en"): string {
|
|
766
744
|
return reviewers
|
|
767
745
|
.map((item) => `${modelLabel(item, language)}\n${item.details.trim()}`)
|
|
768
746
|
.join("\n\n");
|
|
769
747
|
}
|
|
770
748
|
|
|
771
|
-
function aggregatePassSummary(
|
|
772
|
-
const
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
].join("\n");
|
|
784
|
-
}
|
|
785
|
-
const parts = reviewers.map((item) => ({
|
|
786
|
-
body: item.summary,
|
|
787
|
-
suggestions: splitSuggestions(item.details).suggestions,
|
|
788
|
-
}));
|
|
789
|
-
const lines = parts.map((part, index) =>
|
|
790
|
-
`• ${shortModel(reviewers[index]?.model ?? "")}${language === "en" ? ": " : ":"}${passBody(part.body) || fallback}`,
|
|
791
|
-
);
|
|
792
|
-
const suggestions = [...new Set(parts.flatMap((part) => part.suggestions))];
|
|
749
|
+
function aggregatePassSummary(passed: ReviewerResult[], absent: ReviewerResult[], language: "zh" | "en") {
|
|
750
|
+
const en = language === "en";
|
|
751
|
+
const fallback = en ? "Review passed." : "审查通过。";
|
|
752
|
+
const lines = passed.length === 1
|
|
753
|
+
? [passBody(passed[0]?.summary ?? "") || fallback]
|
|
754
|
+
: passed.map((item) => `• ${shortModel(item.model)}${en ? ": " : ":"}${passBody(item.summary) || fallback}`);
|
|
755
|
+
if (absent.length > 0)
|
|
756
|
+
lines.push(
|
|
757
|
+
(en ? "No verdict from " : "未形成裁决:")
|
|
758
|
+
+ absent.map((item) => `${shortModel(item.model)}${en ? ` (${firstLine(item.details)})` : `(${firstLine(item.details)})`}`).join(en ? ", " : "、"),
|
|
759
|
+
);
|
|
760
|
+
const suggestions = [...new Set(passed.flatMap((item) => splitSuggestions(item.details).suggestions))];
|
|
793
761
|
if (suggestions.length === 0) return lines.join("\n");
|
|
794
762
|
return [
|
|
795
763
|
...lines,
|
|
796
764
|
"",
|
|
797
|
-
|
|
765
|
+
en ? "## Suggestions (non-blocking)" : "## 建议(非阻塞)",
|
|
798
766
|
...suggestions.map((item) => `- ${item}`),
|
|
799
767
|
].join("\n");
|
|
800
768
|
}
|
|
801
769
|
|
|
770
|
+
function firstLine(text: string) {
|
|
771
|
+
return text.trim().split(/\r?\n/u, 1)[0]?.split(/[::]/u, 1)[0]?.trim() ?? "";
|
|
772
|
+
}
|
|
773
|
+
|
|
802
774
|
function splitSuggestions(summary: string) {
|
|
803
775
|
const lines = summary.split(/\r?\n/u);
|
|
804
776
|
const index = lines.findIndex((line) => /^##\s*(?:建议(非阻塞)|Suggestions \(non-blocking\))/iu.test(line.trim()));
|