pi-firecode 1.0.0 → 1.0.2
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 +59 -41
- package/README.zh-CN.md +78 -0
- package/config.example.jsonc +21 -22
- package/dist/config.example.jsonc +90 -0
- package/dist/index.js +11443 -0
- package/dist/master/prompts/master.en.md +22 -0
- package/{master → dist/master}/prompts/master.zh.md +3 -3
- package/dist/master/prompts/worker.en.md +1 -0
- package/dist/watcher/prompts/watch.en.md +43 -0
- package/package.json +10 -27
- package/activity.ts +0 -92
- package/busy.ts +0 -188
- package/config.ts +0 -500
- package/deliver.ts +0 -119
- package/flame.ts +0 -149
- package/format.ts +0 -133
- package/header.ts +0 -218
- package/herdr-client.ts +0 -60
- package/index.ts +0 -68
- package/jsonc.ts +0 -34
- package/master/actions.ts +0 -293
- package/master/activity-list.ts +0 -281
- package/master/event-card.ts +0 -87
- package/master/event-format.ts +0 -83
- package/master/guard.ts +0 -46
- package/master/index.ts +0 -252
- package/master/list-view.ts +0 -129
- package/master/outbox.ts +0 -188
- package/master/prompt.ts +0 -26
- package/master/role.ts +0 -18
- package/master/run.ts +0 -315
- package/master/runtime.ts +0 -259
- package/master/spawn.ts +0 -234
- package/master/state.ts +0 -225
- package/master/worker-view.ts +0 -517
- package/provider/claude-sub.ts +0 -151
- package/provider/openai-native/index.ts +0 -8
- package/provider/openai-native/src/compact-client.ts +0 -350
- package/provider/openai-native/src/config.ts +0 -297
- package/provider/openai-native/src/extension.ts +0 -93
- package/provider/openai-native/src/native-compaction.ts +0 -151
- package/provider/openai-native/src/native-details.ts +0 -157
- package/provider/openai-native/src/native-replay.ts +0 -253
- package/provider/openai-native/src/native-runtime.ts +0 -144
- package/provider/openai-native/src/options.ts +0 -87
- package/provider/openai-native/src/request-pipeline.ts +0 -21
- package/provider/openai-native/src/responses-input.ts +0 -433
- package/review/advisor.ts +0 -110
- package/review/card.ts +0 -398
- package/review/checkpoint.ts +0 -378
- package/review/evidence.ts +0 -230
- package/review/index.ts +0 -992
- package/review/occupancy.ts +0 -25
- package/review/outcome.ts +0 -98
- package/review/prompt.ts +0 -209
- package/review/reviewer.ts +0 -353
- package/review/session.ts +0 -110
- package/review/state.ts +0 -877
- package/review/ui.ts +0 -89
- package/round-recorder.ts +0 -18
- package/session/herdr-display.ts +0 -87
- package/session/presets.ts +0 -300
- package/session/quota.ts +0 -123
- package/session/rename.ts +0 -46
- package/session/stats.ts +0 -313
- package/statusbar/index.ts +0 -256
- package/statusbar/render.ts +0 -113
- package/theme.ts +0 -45
- package/tools/actions.ts +0 -53
- package/tools/assistant-view.ts +0 -64
- package/tools/click-anchor.ts +0 -56
- package/tools/group-view.ts +0 -420
- package/tools/grouping.ts +0 -158
- package/tools/host.ts +0 -369
- package/tools/index.ts +0 -186
- package/tools/line.ts +0 -231
- package/tools/machine.ts +0 -78
- package/tools/parts.ts +0 -143
- package/tools/round.ts +0 -95
- package/tools/timing.ts +0 -22
- package/tools/turn-clock.ts +0 -41
- package/tools/turn-summary.ts +0 -109
- package/truncated-write.ts +0 -18
- package/watcher/card.ts +0 -91
- package/watcher/index.ts +0 -176
- package/watcher/observer.ts +0 -75
- package/watcher/transcript.ts +0 -56
- /package/{master → dist/master}/prompts/worker.zh.md +0 -0
- /package/{review → dist/review}/prompts/advisor.en.md +0 -0
- /package/{review → dist/review}/prompts/advisor.zh.md +0 -0
- /package/{review → dist/review}/prompts/review.en.md +0 -0
- /package/{review → dist/review}/prompts/review.zh.md +0 -0
- /package/{watcher → dist/watcher}/prompts/watch.zh.md +0 -0
package/review/checkpoint.ts
DELETED
|
@@ -1,378 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* /fire-review 的持久化:custom entry(不进 LLM 上下文)存 reducer 状态快照。
|
|
3
|
-
*
|
|
4
|
-
* 零外部依赖:schema 用纯函数一次性整体校验(结构不符直接丢弃,不做字段级兼容),
|
|
5
|
-
* 不引入 typebox——firecode 单目录自封闭,运行时无 node_modules,不能在扩展里声明生产依赖。
|
|
6
|
-
*
|
|
7
|
-
* 写入分两档:
|
|
8
|
-
* - beginCheckpoint:新审查首次写入,无条件替换旧终态;
|
|
9
|
-
* - writeCheckpoint:后续写入带 Run ID CAS——期望值由调用方(controller)记住的
|
|
10
|
-
* 上一次写入值提供,出现不是自己写的 Run ID 才算冲突(CheckpointConflictError)。
|
|
11
|
-
*/
|
|
12
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
13
|
-
import type {
|
|
14
|
-
ActiveCheck,
|
|
15
|
-
ActiveReviewer,
|
|
16
|
-
AdvisorResult,
|
|
17
|
-
PendingRound,
|
|
18
|
-
RepairState,
|
|
19
|
-
ReviewRound,
|
|
20
|
-
ReviewState,
|
|
21
|
-
ReviewerResult,
|
|
22
|
-
SummaryState,
|
|
23
|
-
} from "./state.js";
|
|
24
|
-
|
|
25
|
-
export const CHECKPOINT_TYPE = "firecode-review-checkpoint";
|
|
26
|
-
const VERSION = 5;
|
|
27
|
-
|
|
28
|
-
/** 写入凭证:同一场审查内 Run ID 不变,靠单调递增的 seq 识别陈旧写者。 */
|
|
29
|
-
export interface CheckpointStamp {
|
|
30
|
-
runId: string;
|
|
31
|
-
seq: number;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export class CheckpointConflictError extends Error {
|
|
35
|
-
constructor() {
|
|
36
|
-
super("fire-review checkpoint Run ID 已失效");
|
|
37
|
-
this.name = "CheckpointConflictError";
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** 只读会话访问(checkpoint 读/写前的 CAS 读取)。 */
|
|
42
|
-
export interface CheckpointReadContext {
|
|
43
|
-
sessionManager: { getBranch(): unknown[] };
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const PHASES = new Set([
|
|
47
|
-
"idle",
|
|
48
|
-
"queued",
|
|
49
|
-
"reviewing",
|
|
50
|
-
"needs_fix",
|
|
51
|
-
"awaiting_fix",
|
|
52
|
-
"summarizing",
|
|
53
|
-
"settled",
|
|
54
|
-
]);
|
|
55
|
-
const ROUND_RESULTS = new Set([
|
|
56
|
-
"passed",
|
|
57
|
-
"failed",
|
|
58
|
-
"error",
|
|
59
|
-
"stopped",
|
|
60
|
-
"cancelled",
|
|
61
|
-
"timed_out",
|
|
62
|
-
]);
|
|
63
|
-
const REVIEWER_STATUSES = new Set(["running", "passed", "failed", "error"]);
|
|
64
|
-
const ADVISOR_VERDICTS = new Set(["continue", "stop", "narrow"]);
|
|
65
|
-
const STOP_REASONS = new Set([
|
|
66
|
-
"advisor",
|
|
67
|
-
"max_rounds",
|
|
68
|
-
"user",
|
|
69
|
-
"shutdown",
|
|
70
|
-
"timeout",
|
|
71
|
-
]);
|
|
72
|
-
const REPAIR_STATUSES = new Set(["pending", "awaiting_start", "running", "completed"]);
|
|
73
|
-
const SUMMARY_KINDS = new Set(["passed", "max_rounds", "advisor_stop"]);
|
|
74
|
-
const SUMMARY_STATUSES = new Set(["pending", "awaiting_start", "running"]);
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* 键白名单由领域类型派生:satisfies 要求逐字段列全,
|
|
78
|
-
* state.ts 增删字段而此处未同步时编译失败——手写校验漂移的唯一防线。
|
|
79
|
-
*/
|
|
80
|
-
function keysOf<T extends Record<string, true>>(map: T): string[] {
|
|
81
|
-
return Object.keys(map);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const REVIEWER_RESULT_KEYS = keysOf({
|
|
85
|
-
index: true,
|
|
86
|
-
model: true,
|
|
87
|
-
thinking: true,
|
|
88
|
-
status: true,
|
|
89
|
-
summary: true,
|
|
90
|
-
details: true,
|
|
91
|
-
} satisfies Record<keyof ReviewerResult, true>);
|
|
92
|
-
|
|
93
|
-
const ACTIVE_REVIEWER_KEYS = keysOf({
|
|
94
|
-
index: true,
|
|
95
|
-
model: true,
|
|
96
|
-
thinking: true,
|
|
97
|
-
status: true,
|
|
98
|
-
result: true,
|
|
99
|
-
} satisfies Record<keyof ActiveReviewer, true>);
|
|
100
|
-
|
|
101
|
-
const ACTIVE_CHECK_KEYS = keysOf({
|
|
102
|
-
round: true,
|
|
103
|
-
reviewers: true,
|
|
104
|
-
settledCount: true,
|
|
105
|
-
} satisfies Record<keyof ActiveCheck, true>);
|
|
106
|
-
|
|
107
|
-
const ADVISOR_KEYS = keysOf({
|
|
108
|
-
verdict: true,
|
|
109
|
-
advice: true,
|
|
110
|
-
} satisfies Record<keyof AdvisorResult, true>);
|
|
111
|
-
|
|
112
|
-
const ROUND_KEYS = keysOf({
|
|
113
|
-
round: true,
|
|
114
|
-
result: true,
|
|
115
|
-
details: true,
|
|
116
|
-
reviewers: true,
|
|
117
|
-
advisor: true,
|
|
118
|
-
reason: true,
|
|
119
|
-
elapsedMs: true,
|
|
120
|
-
} satisfies Record<keyof ReviewRound, true>);
|
|
121
|
-
|
|
122
|
-
const PENDING_ROUND_KEYS = keysOf({
|
|
123
|
-
round: true,
|
|
124
|
-
reviewers: true,
|
|
125
|
-
details: true,
|
|
126
|
-
} satisfies Record<keyof PendingRound, true>);
|
|
127
|
-
|
|
128
|
-
const REPAIR_KEYS = keysOf({
|
|
129
|
-
details: true,
|
|
130
|
-
advisor: true,
|
|
131
|
-
status: true,
|
|
132
|
-
} satisfies Record<keyof RepairState, true>);
|
|
133
|
-
|
|
134
|
-
const SUMMARY_KEYS = keysOf({
|
|
135
|
-
kind: true,
|
|
136
|
-
status: true,
|
|
137
|
-
} satisfies Record<keyof SummaryState, true>);
|
|
138
|
-
|
|
139
|
-
const CHECKPOINT_KEYS = keysOf({
|
|
140
|
-
version: true,
|
|
141
|
-
seq: true,
|
|
142
|
-
runId: true,
|
|
143
|
-
phase: true,
|
|
144
|
-
round: true,
|
|
145
|
-
focus: true,
|
|
146
|
-
history: true,
|
|
147
|
-
active: true,
|
|
148
|
-
pending: true,
|
|
149
|
-
repair: true,
|
|
150
|
-
summary: true,
|
|
151
|
-
consecutiveFailures: true,
|
|
152
|
-
startedAt: true,
|
|
153
|
-
roundStartedAt: true,
|
|
154
|
-
updatedAt: true,
|
|
155
|
-
} satisfies Record<keyof ReviewState | "version" | "seq", true>);
|
|
156
|
-
|
|
157
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
158
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function isString(value: unknown): value is string {
|
|
162
|
-
return typeof value === "string";
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function isNonNegativeInt(value: unknown): value is number {
|
|
166
|
-
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function oneOf(value: unknown, allowed: ReadonlySet<string>): value is string {
|
|
170
|
-
return isString(value) && allowed.has(value);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function hasOnlyKeys(record: Record<string, unknown>, keys: readonly string[]): boolean {
|
|
174
|
-
return Object.keys(record).every((key) => keys.includes(key));
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function isValidReviewerResult(value: unknown): boolean {
|
|
178
|
-
if (!isRecord(value)) return false;
|
|
179
|
-
if (!isNonNegativeInt(value.index) || !hasOnlyKeys(value, REVIEWER_RESULT_KEYS))
|
|
180
|
-
return false;
|
|
181
|
-
return (
|
|
182
|
-
isString(value.model) &&
|
|
183
|
-
isString(value.thinking) &&
|
|
184
|
-
oneOf(value.status, REVIEWER_STATUSES) &&
|
|
185
|
-
value.status !== "running" &&
|
|
186
|
-
isString(value.summary) &&
|
|
187
|
-
isString(value.details)
|
|
188
|
-
);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function isValidActiveReviewer(value: unknown): boolean {
|
|
192
|
-
if (!isRecord(value) || !isNonNegativeInt(value.index)) return false;
|
|
193
|
-
if (!hasOnlyKeys(value, ACTIVE_REVIEWER_KEYS)) return false;
|
|
194
|
-
return (
|
|
195
|
-
isString(value.model) &&
|
|
196
|
-
isString(value.thinking) &&
|
|
197
|
-
oneOf(value.status, REVIEWER_STATUSES) &&
|
|
198
|
-
(value.result === null || isValidReviewerResult(value.result))
|
|
199
|
-
);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
function isValidActiveCheck(value: unknown): boolean {
|
|
203
|
-
if (!isRecord(value)) return false;
|
|
204
|
-
if (!hasOnlyKeys(value, ACTIVE_CHECK_KEYS)) return false;
|
|
205
|
-
const round = value.round;
|
|
206
|
-
const settledCount = value.settledCount;
|
|
207
|
-
if (!isNonNegativeInt(round) || !isNonNegativeInt(settledCount)) return false;
|
|
208
|
-
return (
|
|
209
|
-
round >= 1 &&
|
|
210
|
-
Array.isArray(value.reviewers) &&
|
|
211
|
-
value.reviewers.every(isValidActiveReviewer) &&
|
|
212
|
-
value.reviewers.length > 0 &&
|
|
213
|
-
settledCount <= value.reviewers.length
|
|
214
|
-
);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function isValidAdvisor(value: unknown): boolean {
|
|
218
|
-
if (!isRecord(value) || !hasOnlyKeys(value, ADVISOR_KEYS)) return false;
|
|
219
|
-
return oneOf(value.verdict, ADVISOR_VERDICTS) && isString(value.advice);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
function isValidRound(value: unknown): boolean {
|
|
223
|
-
if (!isRecord(value)) return false;
|
|
224
|
-
if (!hasOnlyKeys(value, ROUND_KEYS)) return false;
|
|
225
|
-
return (
|
|
226
|
-
isNonNegativeInt(value.round) &&
|
|
227
|
-
oneOf(value.result, ROUND_RESULTS) &&
|
|
228
|
-
isString(value.details) &&
|
|
229
|
-
Array.isArray(value.reviewers) &&
|
|
230
|
-
value.reviewers.every(isValidReviewerResult) &&
|
|
231
|
-
(value.advisor === undefined || isValidAdvisor(value.advisor)) &&
|
|
232
|
-
(value.reason === undefined || oneOf(value.reason, STOP_REASONS)) &&
|
|
233
|
-
isNonNegativeInt(value.elapsedMs)
|
|
234
|
-
);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
function isValidPendingRound(value: unknown): boolean {
|
|
238
|
-
if (!isRecord(value)) return false;
|
|
239
|
-
if (!hasOnlyKeys(value, PENDING_ROUND_KEYS)) return false;
|
|
240
|
-
const round = value.round;
|
|
241
|
-
return (
|
|
242
|
-
isNonNegativeInt(round) &&
|
|
243
|
-
round >= 1 &&
|
|
244
|
-
Array.isArray(value.reviewers) &&
|
|
245
|
-
value.reviewers.every(isValidReviewerResult) &&
|
|
246
|
-
isString(value.details)
|
|
247
|
-
);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function isValidRepair(value: unknown): boolean {
|
|
251
|
-
return (
|
|
252
|
-
isRecord(value) &&
|
|
253
|
-
hasOnlyKeys(value, REPAIR_KEYS) &&
|
|
254
|
-
isString(value.details) &&
|
|
255
|
-
(value.advisor === null || isValidAdvisor(value.advisor)) &&
|
|
256
|
-
oneOf(value.status, REPAIR_STATUSES)
|
|
257
|
-
);
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
function isValidSummary(value: unknown): boolean {
|
|
261
|
-
return (
|
|
262
|
-
isRecord(value) &&
|
|
263
|
-
hasOnlyKeys(value, SUMMARY_KEYS) &&
|
|
264
|
-
oneOf(value.kind, SUMMARY_KINDS) &&
|
|
265
|
-
oneOf(value.status, SUMMARY_STATUSES)
|
|
266
|
-
);
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
/** 一次性整体校验 checkpoint;结构或版本不符返回 false(调用方直接丢弃)。 */
|
|
270
|
-
export function isValidCheckpoint(value: unknown): boolean {
|
|
271
|
-
if (!isRecord(value)) return false;
|
|
272
|
-
if (!hasOnlyKeys(value, CHECKPOINT_KEYS)) return false;
|
|
273
|
-
if (!isNonNegativeInt(value.seq) || value.seq < 1) return false;
|
|
274
|
-
return (
|
|
275
|
-
value.version === VERSION &&
|
|
276
|
-
isString(value.runId) &&
|
|
277
|
-
oneOf(value.phase, PHASES) &&
|
|
278
|
-
isNonNegativeInt(value.round) &&
|
|
279
|
-
isString(value.focus) &&
|
|
280
|
-
Array.isArray(value.history) &&
|
|
281
|
-
value.history.every(isValidRound) &&
|
|
282
|
-
(value.active === null || isValidActiveCheck(value.active)) &&
|
|
283
|
-
(value.pending === null || isValidPendingRound(value.pending)) &&
|
|
284
|
-
(value.repair === null || isValidRepair(value.repair)) &&
|
|
285
|
-
(value.summary === null || isValidSummary(value.summary)) &&
|
|
286
|
-
isNonNegativeInt(value.consecutiveFailures) &&
|
|
287
|
-
isNonNegativeInt(value.startedAt) &&
|
|
288
|
-
isNonNegativeInt(value.roundStartedAt) &&
|
|
289
|
-
isNonNegativeInt(value.updatedAt)
|
|
290
|
-
);
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
function toCheckpoint(state: ReviewState) {
|
|
294
|
-
return {
|
|
295
|
-
version: VERSION,
|
|
296
|
-
runId: state.runId,
|
|
297
|
-
phase: state.phase,
|
|
298
|
-
round: state.round,
|
|
299
|
-
focus: state.focus,
|
|
300
|
-
history: state.history,
|
|
301
|
-
active: state.active,
|
|
302
|
-
pending: state.pending,
|
|
303
|
-
repair: state.repair,
|
|
304
|
-
summary: state.summary,
|
|
305
|
-
consecutiveFailures: state.consecutiveFailures,
|
|
306
|
-
startedAt: state.startedAt,
|
|
307
|
-
roundStartedAt: state.roundStartedAt,
|
|
308
|
-
updatedAt: state.updatedAt,
|
|
309
|
-
};
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
/** 读当前分支上最近一个 checkpoint;无、损坏或版本不匹配都返回 undefined。 */
|
|
313
|
-
export function readCheckpoint(ctx: CheckpointReadContext): ReviewState | undefined {
|
|
314
|
-
const entry = latestEntry(ctx);
|
|
315
|
-
return entry ? (entry as unknown as ReviewState) : undefined;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
function latestEntry(ctx: CheckpointReadContext): Record<string, unknown> | undefined {
|
|
319
|
-
const entries = ctx.sessionManager.getBranch();
|
|
320
|
-
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
321
|
-
const entry = entries[index];
|
|
322
|
-
if (!isRecord(entry) || entry.type !== "custom") continue;
|
|
323
|
-
if (entry.customType !== CHECKPOINT_TYPE) continue;
|
|
324
|
-
if (!isValidCheckpoint(entry.data)) return undefined;
|
|
325
|
-
return entry.data as Record<string, unknown>;
|
|
326
|
-
}
|
|
327
|
-
return undefined;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
/** 读最近一条 checkpoint 的写入凭证;无则 null。 */
|
|
331
|
-
export function readStamp(ctx: CheckpointReadContext): CheckpointStamp | null {
|
|
332
|
-
const entry = latestEntry(ctx);
|
|
333
|
-
if (!entry) return null;
|
|
334
|
-
const data = entry as { runId: string; seq: number };
|
|
335
|
-
return { runId: data.runId, seq: data.seq };
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
function appendCheckpoint(
|
|
339
|
-
pi: ExtensionAPI,
|
|
340
|
-
state: ReviewState,
|
|
341
|
-
seq: number,
|
|
342
|
-
): CheckpointStamp {
|
|
343
|
-
const data = { ...toCheckpoint(state), seq };
|
|
344
|
-
if (!isValidCheckpoint(data)) throw new Error("fire-review checkpoint 状态非法");
|
|
345
|
-
pi.appendEntry(CHECKPOINT_TYPE, data);
|
|
346
|
-
return { runId: state.runId, seq };
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
/** 新审查首次写入:无条件替换旧终态(不校验旧 Run ID)。 */
|
|
350
|
-
/** 新审查首写:无条件替换旧终态,返回本次写入凭证。 */
|
|
351
|
-
export function beginCheckpoint(
|
|
352
|
-
pi: ExtensionAPI,
|
|
353
|
-
_ctx: CheckpointReadContext,
|
|
354
|
-
state: ReviewState,
|
|
355
|
-
): CheckpointStamp {
|
|
356
|
-
return appendCheckpoint(pi, state, 1);
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
/**
|
|
360
|
-
* 后续写入时当前持久化凭证必须与 controller 记住的上一次完全一致。
|
|
361
|
-
* 只比 Run ID 不够——同一场审查被两个运行时同时恢复时 Run ID 相同,
|
|
362
|
-
* 靠 seq 才能识别出陈旧写者并拒绝静默覆盖。
|
|
363
|
-
*/
|
|
364
|
-
export function writeCheckpoint(
|
|
365
|
-
pi: ExtensionAPI,
|
|
366
|
-
ctx: CheckpointReadContext,
|
|
367
|
-
state: ReviewState,
|
|
368
|
-
expected: CheckpointStamp,
|
|
369
|
-
): CheckpointStamp {
|
|
370
|
-
const current = readStamp(ctx);
|
|
371
|
-
if (
|
|
372
|
-
!current ||
|
|
373
|
-
current.runId !== expected.runId ||
|
|
374
|
-
current.seq !== expected.seq
|
|
375
|
-
)
|
|
376
|
-
throw new CheckpointConflictError();
|
|
377
|
-
return appendCheckpoint(pi, state, expected.seq + 1);
|
|
378
|
-
}
|
package/review/evidence.ts
DELETED
|
@@ -1,230 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 会话证据组装:取会话分支 entries 转纯文本,按 token 预算裁剪。
|
|
3
|
-
*
|
|
4
|
-
* 根因规避:首条用户消息是原始需求锚点,固定保留,不参与预算竞争;
|
|
5
|
-
* 预算只裁剪中间的旧消息,从最新往前保留最近工作,审查者既知道要什么也知道做了什么。
|
|
6
|
-
* assistant 的工具调用轨迹(工具名 + path/command)随消息渲染:它是范围归因的一手证据,
|
|
7
|
-
* 审查者靠它判断本会话实际编辑了什么,共享 checkout 上无法归因的 diff 不得立案。
|
|
8
|
-
* isError 的调用标注失败:尝试过不等于编辑成功,失败调用不得成为归因依据;
|
|
9
|
-
* 无结果的调用不标——把压缩/中止场景的真实编辑误标未完成,会反向把归因弱化成假 PASS。
|
|
10
|
-
* toolResult 正文仍跳过(输出体积大且非一手证据——审查者应自行重跑验证命令)。
|
|
11
|
-
*/
|
|
12
|
-
import type { Language } from "../config.js";
|
|
13
|
-
import { textOf } from "../format.js";
|
|
14
|
-
|
|
15
|
-
export const DEFAULT_EVIDENCE_TOKENS = 24_000;
|
|
16
|
-
/** 单条消息渲染上限,防单条超长消息撑爆预算。 */
|
|
17
|
-
const MESSAGE_MAX_CHARS = 3_000;
|
|
18
|
-
|
|
19
|
-
export interface Evidence {
|
|
20
|
-
text: string;
|
|
21
|
-
/** 被预算裁剪掉的中间消息条数。 */
|
|
22
|
-
omitted: number;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
interface Render {
|
|
26
|
-
language: Language;
|
|
27
|
-
/** 会话文件:截断标记据此告诉审查者完整原文在哪里。内存会话没有。 */
|
|
28
|
-
sessionFile?: string;
|
|
29
|
-
failedCalls: ReadonlySet<string>;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function buildEvidence(
|
|
33
|
-
entries: readonly unknown[],
|
|
34
|
-
language: Language,
|
|
35
|
-
{ budgetTokens = DEFAULT_EVIDENCE_TOKENS, sessionFile }: { budgetTokens?: number; sessionFile?: string } = {},
|
|
36
|
-
): Evidence {
|
|
37
|
-
const render: Render = { language, ...(sessionFile ? { sessionFile } : {}), failedCalls: collectFailedCalls(entries) };
|
|
38
|
-
const blocks = entries.flatMap((entry) => renderEntry(entry, render));
|
|
39
|
-
if (blocks.length === 0) return { text: "", omitted: 0 };
|
|
40
|
-
// 锚点必须是首条用户消息(原始需求):它之前可能排着其他扩展的可显示消息,
|
|
41
|
-
// 盲取第一块会把真正的需求锚点让进预算竞争、在长会话里被裁掉。
|
|
42
|
-
const anchorIndex = Math.max(
|
|
43
|
-
0,
|
|
44
|
-
blocks.findIndex((block) => block.role === "user"),
|
|
45
|
-
);
|
|
46
|
-
const anchor = blocks[anchorIndex];
|
|
47
|
-
const rest = blocks.filter((_, index) => index !== anchorIndex);
|
|
48
|
-
const recent: string[] = [];
|
|
49
|
-
let used = estimateTokens(anchor.text);
|
|
50
|
-
let omitted = 0;
|
|
51
|
-
for (let index = rest.length - 1; index >= 0; index -= 1) {
|
|
52
|
-
const block = rest[index];
|
|
53
|
-
const cost = estimateTokens(block.text);
|
|
54
|
-
if (used + cost > budgetTokens) {
|
|
55
|
-
omitted += 1;
|
|
56
|
-
continue;
|
|
57
|
-
}
|
|
58
|
-
recent.push(block.text);
|
|
59
|
-
used += cost;
|
|
60
|
-
}
|
|
61
|
-
recent.reverse();
|
|
62
|
-
const text =
|
|
63
|
-
recent.length === 0
|
|
64
|
-
? anchor.text
|
|
65
|
-
: `${anchor.text}\n\n${omitted > 0 ? gapLabel(omitted, language) : ""}${recent.join("\n\n")}`;
|
|
66
|
-
return { text, omitted };
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function gapLabel(omitted: number, language: Language) {
|
|
70
|
-
return language === "en"
|
|
71
|
-
? `[${omitted} intermediate message(s) omitted under the evidence budget]\n\n`
|
|
72
|
-
: `[证据预算省略了 ${omitted} 条中间消息]\n\n`;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
type EvidenceBlock = { text: string; role?: "user" };
|
|
76
|
-
|
|
77
|
-
/** isError 的 toolResult 集合:失败调用不得在轨迹里冒充实际编辑。 */
|
|
78
|
-
function collectFailedCalls(entries: readonly unknown[]): ReadonlySet<string> {
|
|
79
|
-
const failed = new Set<string>();
|
|
80
|
-
for (const entry of entries) {
|
|
81
|
-
if (!isRecord(entry) || entry.type !== "message") continue;
|
|
82
|
-
const message = asRecord(entry.message);
|
|
83
|
-
if (message?.role !== "toolResult" || message.isError !== true) continue;
|
|
84
|
-
if (typeof message.toolCallId === "string") failed.add(message.toolCallId);
|
|
85
|
-
}
|
|
86
|
-
return failed;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function renderEntry(entry: unknown, render: Render): EvidenceBlock[] {
|
|
90
|
-
const { language } = render;
|
|
91
|
-
if (!isRecord(entry)) return [];
|
|
92
|
-
switch (entry.type) {
|
|
93
|
-
case "message": {
|
|
94
|
-
const message = asRecord(entry.message);
|
|
95
|
-
if (!message) return [];
|
|
96
|
-
if (message.role === "user")
|
|
97
|
-
return [
|
|
98
|
-
{
|
|
99
|
-
text: `## ${userLabel(language)}\n${clip(textOf(message.content), render)}`,
|
|
100
|
-
role: "user" as const,
|
|
101
|
-
},
|
|
102
|
-
];
|
|
103
|
-
if (message.role === "assistant")
|
|
104
|
-
return [{ text: `## ${assistantLabel(language)}\n${assistantBody(message.content, render)}` }];
|
|
105
|
-
return [];
|
|
106
|
-
}
|
|
107
|
-
case "custom_message": {
|
|
108
|
-
if (entry.display !== true) return [];
|
|
109
|
-
return [{ text: `## ${customLabel(language, String(entry.customType ?? ""))}\n${clip(textOf(entry.content), render)}` }];
|
|
110
|
-
}
|
|
111
|
-
case "compaction":
|
|
112
|
-
return typeof entry.summary === "string" && entry.summary
|
|
113
|
-
? [{ text: `## ${summaryLabel(language)}\n${clip(entry.summary, render)}` }]
|
|
114
|
-
: [];
|
|
115
|
-
case "branch_summary":
|
|
116
|
-
return typeof entry.summary === "string" && entry.summary
|
|
117
|
-
? [{ text: `## ${branchSummaryLabel(language)}\n${clip(entry.summary, render)}` }]
|
|
118
|
-
: [];
|
|
119
|
-
default:
|
|
120
|
-
return [];
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function userLabel(language: Language) {
|
|
125
|
-
return language === "en" ? "User" : "用户";
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function assistantLabel(language: Language) {
|
|
129
|
-
return language === "en" ? "Assistant" : "助手";
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function customLabel(language: Language, customType: string) {
|
|
133
|
-
return language === "en" ? `Message (${customType})` : `消息(${customType})`;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function summaryLabel(language: Language) {
|
|
137
|
-
return language === "en" ? "History summary (compacted)" : "历史摘要(已压缩)";
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function branchSummaryLabel(language: Language) {
|
|
141
|
-
return language === "en" ? "Branch summary" : "分支摘要";
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/** assistant 正文 = 文本段 + 工具调用轨迹;纯工具回合也因此留下编辑记录。 */
|
|
145
|
-
function assistantBody(content: unknown, render: Render): string {
|
|
146
|
-
if (typeof content === "string") return clip(content, render);
|
|
147
|
-
if (!Array.isArray(content)) return "";
|
|
148
|
-
const trail = content
|
|
149
|
-
.map((part) => toolCallLine(asRecord(part), render))
|
|
150
|
-
.filter(Boolean)
|
|
151
|
-
.join("\n");
|
|
152
|
-
const body = clip(textOf(content), render);
|
|
153
|
-
return [body, trail].filter(Boolean).join("\n");
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function toolCallLine(part: Record<string, unknown> | undefined, render: Render): string {
|
|
157
|
-
const { language, failedCalls } = render;
|
|
158
|
-
if (part?.type !== "toolCall" || typeof part.name !== "string" || !part.name) return "";
|
|
159
|
-
const args = asRecord(part.arguments);
|
|
160
|
-
const target =
|
|
161
|
-
typeof args?.path === "string"
|
|
162
|
-
? args.path
|
|
163
|
-
: typeof args?.command === "string"
|
|
164
|
-
? args.command
|
|
165
|
-
: "";
|
|
166
|
-
const failed =
|
|
167
|
-
typeof part.id === "string" && failedCalls.has(part.id)
|
|
168
|
-
? language === "en"
|
|
169
|
-
? " (failed)"
|
|
170
|
-
: "(失败)"
|
|
171
|
-
: "";
|
|
172
|
-
return `${`[${part.name}] ${clipLine(target, render)}`.trimEnd()}${failed}`;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/** 单行轨迹上限:防超长 bash 命令撑大证据块;路径不受影响。 */
|
|
176
|
-
const TOOL_LINE_MAX_CHARS = 200;
|
|
177
|
-
|
|
178
|
-
function clipLine(text: string, { language, sessionFile }: Render) {
|
|
179
|
-
const single = text.replace(/\s+/gu, " ").trim();
|
|
180
|
-
if (single.length <= TOOL_LINE_MAX_CHARS) return single;
|
|
181
|
-
const where = sessionFile ?? (language === "en" ? "the session file" : "会话文件");
|
|
182
|
-
const note = language === "en"
|
|
183
|
-
? `…[truncated, ${single.length} chars; full original in ${where}]`
|
|
184
|
-
: `…[截断,原文 ${single.length} 字,完整原文在 ${where}]`;
|
|
185
|
-
return `${single.slice(0, TOOL_LINE_MAX_CHARS)}${note}`;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* 截断处必须写明是证据组装的截断:裸省略号会被审查者当成回复本身没写完(长交付物曾因此连判 FAIL)。
|
|
190
|
-
* 完整原文在会话文件里,审查者能用 read 自行核对。
|
|
191
|
-
*/
|
|
192
|
-
function clip(text: string, { language, sessionFile }: Render) {
|
|
193
|
-
if (text.length <= MESSAGE_MAX_CHARS) return text.trim();
|
|
194
|
-
const where = sessionFile ?? (language === "en" ? "the session file" : "会话文件");
|
|
195
|
-
const note = language === "en"
|
|
196
|
-
? `[evidence truncated: this message has ${text.length} characters, only the first ${MESSAGE_MAX_CHARS} are shown; the full original is in ${where} — read it if you need to verify]`
|
|
197
|
-
: `[证据截断:本条消息原文 ${text.length} 字,此处只给出前 ${MESSAGE_MAX_CHARS} 字;完整原文在 ${where},需要核对时用 read 查看]`;
|
|
198
|
-
return `${text.slice(0, MESSAGE_MAX_CHARS).trim()}\n${note}`;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* 粗略 token 估计:CJK 每字 1 token,其余按 4 字符/token。
|
|
203
|
-
* 用于预算裁剪的相对量级,不追求精确。
|
|
204
|
-
*/
|
|
205
|
-
export function estimateTokens(text: string): number {
|
|
206
|
-
let cjk = 0;
|
|
207
|
-
let other = 0;
|
|
208
|
-
for (const char of text) {
|
|
209
|
-
if (isCjk(char)) cjk += 1;
|
|
210
|
-
else other += 1;
|
|
211
|
-
}
|
|
212
|
-
return Math.ceil(cjk + other / 4);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function isCjk(char: string) {
|
|
216
|
-
const code = char.codePointAt(0) ?? 0;
|
|
217
|
-
return (
|
|
218
|
-
(code >= 0x4e00 && code <= 0x9fff) ||
|
|
219
|
-
(code >= 0x3400 && code <= 0x4dbf) ||
|
|
220
|
-
(code >= 0xf900 && code <= 0xfaff)
|
|
221
|
-
);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
225
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
229
|
-
return isRecord(value) ? value : undefined;
|
|
230
|
-
}
|