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/config.ts
DELETED
|
@@ -1,500 +0,0 @@
|
|
|
1
|
-
/** FireCode 配置:只读 Pi Agent 目录下的 `extensions/firecode/config.jsonc`。 */
|
|
2
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
5
|
-
import { parseJsonc } from "./jsonc.js";
|
|
6
|
-
|
|
7
|
-
export type Language = "zh" | "en";
|
|
8
|
-
export type ThinkingLevelValue =
|
|
9
|
-
| "off"
|
|
10
|
-
| "minimal"
|
|
11
|
-
| "low"
|
|
12
|
-
| "medium"
|
|
13
|
-
| "high"
|
|
14
|
-
| "xhigh"
|
|
15
|
-
| "max";
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* 模型原子:配置里一律写作 "provider/model/thinking",解析后拆成运行时模型 id 与思考档。
|
|
19
|
-
* 全仓库指定模型与思考档的唯一形状。
|
|
20
|
-
*/
|
|
21
|
-
export interface ModelAtom {
|
|
22
|
-
model: string;
|
|
23
|
-
thinking: ThinkingLevelValue;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface Preset {
|
|
27
|
-
model?: ModelAtom;
|
|
28
|
-
tools?: string[];
|
|
29
|
-
instructions?: string;
|
|
30
|
-
/** 一键切换,如 alt+1;不填则无快捷键 */
|
|
31
|
-
key?: string;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/** /fire-review 配置:审查者 / 顾问模型 + 循环限制。见 config.jsonc 的 review 节注释。 */
|
|
35
|
-
export interface ReviewConfig {
|
|
36
|
-
advisor: ModelAtom;
|
|
37
|
-
reviewers: ModelAtom[];
|
|
38
|
-
/** 审查轮数硬上限。 */
|
|
39
|
-
maxRounds: number;
|
|
40
|
-
/** 连续几轮失败触发顾问仲裁。 */
|
|
41
|
-
advisorAfterFailures: number;
|
|
42
|
-
/** 单个审查者 / 顾问会话超时(分钟)。 */
|
|
43
|
-
timeoutMinutes: number;
|
|
44
|
-
/** 审查者只读工具白名单。 */
|
|
45
|
-
tools: string[];
|
|
46
|
-
language: Language;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export interface MasterRole extends ModelAtom {
|
|
50
|
-
role: string;
|
|
51
|
-
use: string;
|
|
52
|
-
fallback: ModelAtom[];
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export interface MasterConfig {
|
|
56
|
-
roles: MasterRole[];
|
|
57
|
-
workerExcludeExtensions: string[];
|
|
58
|
-
autoActivate: boolean;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** 观察员喂给观察会话的增量粒度:minimal 省略 reasoning 与 diff 正文。 */
|
|
62
|
-
export type WatcherContext = "minimal" | "full";
|
|
63
|
-
|
|
64
|
-
/** Watcher 观察员配置:模型原子必须显式配置,绝不回退默认模型。 */
|
|
65
|
-
export interface WatcherConfig extends ModelAtom {
|
|
66
|
-
enabled: boolean;
|
|
67
|
-
context: WatcherContext;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
export const FEATURES = [
|
|
72
|
-
"header",
|
|
73
|
-
"statusbar",
|
|
74
|
-
"tools",
|
|
75
|
-
"presets",
|
|
76
|
-
"rename",
|
|
77
|
-
"stats",
|
|
78
|
-
"claudeSub",
|
|
79
|
-
"openaiNative",
|
|
80
|
-
"review",
|
|
81
|
-
"master",
|
|
82
|
-
"watcher",
|
|
83
|
-
] as const;
|
|
84
|
-
|
|
85
|
-
export type Feature = (typeof FEATURES)[number];
|
|
86
|
-
|
|
87
|
-
/** 顶层只认这些节;openai 节由 provider/openai-native 自己解析。 */
|
|
88
|
-
const SECTIONS = ["features", "keys", "openai", "presets", "review", "master", "watcher"];
|
|
89
|
-
|
|
90
|
-
export const DEFAULT_KEYS = {
|
|
91
|
-
rename: "ctrl+r",
|
|
92
|
-
fast: "ctrl+f",
|
|
93
|
-
} as const;
|
|
94
|
-
|
|
95
|
-
export type FireCodeKeys = {
|
|
96
|
-
rename: string;
|
|
97
|
-
fast: string;
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
export interface FireCodeConfig {
|
|
101
|
-
features: Partial<Record<Feature, boolean>>;
|
|
102
|
-
keys: FireCodeKeys;
|
|
103
|
-
presets: Record<string, Preset>;
|
|
104
|
-
review: ReviewConfig;
|
|
105
|
-
master: MasterConfig;
|
|
106
|
-
watcher: WatcherConfig;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/** 一节能否启动的唯一判定:要么交出可用配置,要么给出拒绝启动的原因。 */
|
|
110
|
-
export type Section<T> = { config: T } | { error: string };
|
|
111
|
-
|
|
112
|
-
export type LoadedConfig = {
|
|
113
|
-
config: FireCodeConfig;
|
|
114
|
-
/** 需要在 session_start 全局警告的问题;关闭的功能那一节的问题不在其中。 */
|
|
115
|
-
problems: string[];
|
|
116
|
-
review: Section<ReviewConfig>;
|
|
117
|
-
master: Section<MasterConfig>;
|
|
118
|
-
watcher: Section<WatcherConfig>;
|
|
119
|
-
/** features 整节类型错误:已安全回退成全关,但那是配置坏而非用户关闭。 */
|
|
120
|
-
featuresBroken: boolean;
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
export const CONFIG_PATH = join(getAgentDir(), "extensions", "firecode", "config.jsonc");
|
|
124
|
-
|
|
125
|
-
function readFile(problems: string[]): Record<string, unknown> {
|
|
126
|
-
if (!existsSync(CONFIG_PATH)) {
|
|
127
|
-
problems.push("config.jsonc 不存在,已关闭可选功能");
|
|
128
|
-
return { features: Object.fromEntries(FEATURES.map((feature) => [feature, false])) };
|
|
129
|
-
}
|
|
130
|
-
try {
|
|
131
|
-
const parsed: unknown = parseJsonc(readFileSync(CONFIG_PATH, "utf8"));
|
|
132
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
133
|
-
problems.push("config.jsonc 顶层必须是对象");
|
|
134
|
-
return {};
|
|
135
|
-
}
|
|
136
|
-
return parsed as Record<string, unknown>;
|
|
137
|
-
} catch (error) {
|
|
138
|
-
// 统一前缀:文件级故障必须能被调用方识别并阻断功能,
|
|
139
|
-
// 不能因为消息文本不带节名就被当成无关问题过滤掉。
|
|
140
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
141
|
-
problems.push(`config.jsonc 解析失败:${message}`);
|
|
142
|
-
return {};
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
147
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function asRecord(value: unknown): Record<string, unknown> {
|
|
151
|
-
return value && typeof value === "object" && !Array.isArray(value)
|
|
152
|
-
? (value as Record<string, unknown>)
|
|
153
|
-
: {};
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/** 嵌套对象也做键白名单:拼写错误必须报出来,不能静默回退默认值。 */
|
|
157
|
-
function rejectUnknownKeys(
|
|
158
|
-
record: Record<string, unknown>,
|
|
159
|
-
allowed: readonly string[],
|
|
160
|
-
field: string,
|
|
161
|
-
problems: string[],
|
|
162
|
-
) {
|
|
163
|
-
for (const key of Object.keys(record))
|
|
164
|
-
if (!allowed.includes(key)) problems.push(`未知字段 ${field}.${key}`);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function booleanValue(value: unknown, field: string, fallback: boolean, problems: string[]): boolean {
|
|
168
|
-
if (value === undefined) return fallback;
|
|
169
|
-
if (typeof value === "boolean") return value;
|
|
170
|
-
problems.push(`${field} 必须是 true 或 false`);
|
|
171
|
-
return fallback;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function stringValue(value: unknown, field: string, problems: string[]): string | undefined {
|
|
175
|
-
if (typeof value === "string" && value) return value;
|
|
176
|
-
problems.push(`${field} 必须是非空字符串`);
|
|
177
|
-
return undefined;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function stringArray(value: unknown, field: string, problems: string[]): string[] {
|
|
181
|
-
if (value === undefined) return [];
|
|
182
|
-
if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || !item)) {
|
|
183
|
-
problems.push(`${field} 必须是非空字符串数组`);
|
|
184
|
-
return [];
|
|
185
|
-
}
|
|
186
|
-
return [...new Set(value)];
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function checkFeatures(features: Record<string, unknown>, problems: string[]): void {
|
|
190
|
-
for (const [key, value] of Object.entries(features)) {
|
|
191
|
-
if (!FEATURES.includes(key as Feature)) {
|
|
192
|
-
problems.push(`未知开关 features.${key},可用:${FEATURES.join(" / ")}`);
|
|
193
|
-
continue;
|
|
194
|
-
}
|
|
195
|
-
// 开关只能是布尔:写成字符串 "false" 时因为 `!== false` 仍会启用,
|
|
196
|
-
// 而启用 review 意味着真实的模型调用,不能静默放行。
|
|
197
|
-
if (typeof value !== "boolean")
|
|
198
|
-
problems.push(`features.${key} 必须是 true 或 false`);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
function checkKeys(keys: FireCodeKeys, presets: Record<string, Preset>, problems: string[]): void {
|
|
203
|
-
const owners = new Map<string, string>([
|
|
204
|
-
[keys.rename, "keys.rename"],
|
|
205
|
-
[keys.fast, "keys.fast"],
|
|
206
|
-
]);
|
|
207
|
-
const declared = Object.entries(keys);
|
|
208
|
-
for (let index = 0; index < declared.length; index++) {
|
|
209
|
-
for (let other = index + 1; other < declared.length; other++) {
|
|
210
|
-
if (declared[index][1] === declared[other][1]) {
|
|
211
|
-
problems.push(
|
|
212
|
-
`快捷键 ${declared[index][1]} 被 keys.${declared[index][0]} 和 keys.${declared[other][0]} 重复占用`,
|
|
213
|
-
);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
for (const [name, preset] of Object.entries(presets)) {
|
|
218
|
-
if (!preset?.key) continue;
|
|
219
|
-
const owner = owners.get(preset.key);
|
|
220
|
-
if (owner) problems.push(`快捷键 ${preset.key} 被 ${owner} 和预设 ${name} 重复占用`);
|
|
221
|
-
else owners.set(preset.key, `预设 ${name}`);
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
let cached: LoadedConfig | undefined;
|
|
226
|
-
|
|
227
|
-
export function loadConfig(): LoadedConfig {
|
|
228
|
-
if (cached) return cached;
|
|
229
|
-
|
|
230
|
-
// 文件级与开关级问题阻断所有付费功能:开关写成字符串 "false" 时 `!== false` 仍会启用。
|
|
231
|
-
const blocking: string[] = [];
|
|
232
|
-
const raw = readFile(blocking);
|
|
233
|
-
// features 省略表示沿用默认全开;只要显式写了,就必须是对象。
|
|
234
|
-
// 非对象不能回退成 {},因为 {} 在入口语义里正是「全部启用」。
|
|
235
|
-
const featuresBroken = raw.features !== undefined && !isPlainObject(raw.features);
|
|
236
|
-
if (featuresBroken) blocking.push("features 必须是对象");
|
|
237
|
-
const features: Partial<Record<Feature, boolean>> = featuresBroken
|
|
238
|
-
? Object.fromEntries(FEATURES.map((feature) => [feature, false]))
|
|
239
|
-
: asRecord(raw.features);
|
|
240
|
-
checkFeatures(features, blocking);
|
|
241
|
-
|
|
242
|
-
const problems = [...blocking];
|
|
243
|
-
for (const key of Object.keys(raw)) if (!SECTIONS.includes(key)) problems.push(`未知配置节 ${key}`);
|
|
244
|
-
const rawKeys = asRecord(raw.keys);
|
|
245
|
-
rejectUnknownKeys(rawKeys, Object.keys(DEFAULT_KEYS), "keys", problems);
|
|
246
|
-
const presets = parsePresets(raw.presets, problems);
|
|
247
|
-
const keys: FireCodeKeys = {
|
|
248
|
-
rename: typeof rawKeys.rename === "string" ? rawKeys.rename : DEFAULT_KEYS.rename,
|
|
249
|
-
fast: typeof rawKeys.fast === "string" ? rawKeys.fast : DEFAULT_KEYS.fast,
|
|
250
|
-
};
|
|
251
|
-
checkKeys(keys, presets, problems);
|
|
252
|
-
|
|
253
|
-
// review / master / watcher 有问题时对应功能拒绝启动:静默补齐会拿用户没选的模型真实发起调用。
|
|
254
|
-
// 节内问题只在功能开启时进全局警告。
|
|
255
|
-
const section = <T>(
|
|
256
|
-
name: "review" | "master" | "watcher",
|
|
257
|
-
label: string,
|
|
258
|
-
parse: (record: Record<string, unknown>, problems: string[]) => T,
|
|
259
|
-
incomplete: (config: T) => string | undefined,
|
|
260
|
-
): { config: T; verdict: Section<T> } => {
|
|
261
|
-
const own: string[] = [];
|
|
262
|
-
if (raw[name] !== undefined && !isPlainObject(raw[name])) own.push(`${name} 必须是对象`);
|
|
263
|
-
const config = parse(asRecord(raw[name]), own);
|
|
264
|
-
if (features[name] !== false) problems.push(...own);
|
|
265
|
-
const reasons = [...blocking, ...own];
|
|
266
|
-
const missing = reasons.length ? undefined : incomplete(config);
|
|
267
|
-
if (missing) reasons.push(missing);
|
|
268
|
-
return { config, verdict: reasons.length ? { error: `${label}配置有问题,已停止:${reasons.join(";")}` } : { config } };
|
|
269
|
-
};
|
|
270
|
-
const review = section("review", "fire-review ", parseReviewConfig, (config) =>
|
|
271
|
-
config.advisor.model && config.reviewers.length ? undefined : "请显式完整配置 review");
|
|
272
|
-
const master = section("master", "Master ", parseMasterConfig, (config) =>
|
|
273
|
-
config.roles.length ? undefined : "请在 master.roles 至少配置一个角色");
|
|
274
|
-
const watcher = section("watcher", "观察员", parseWatcherConfig, () => undefined);
|
|
275
|
-
|
|
276
|
-
cached = {
|
|
277
|
-
config: { features, keys, presets, review: review.config, master: master.config, watcher: watcher.config },
|
|
278
|
-
problems,
|
|
279
|
-
review: review.verdict,
|
|
280
|
-
master: master.verdict,
|
|
281
|
-
watcher: watcher.verdict,
|
|
282
|
-
featuresBroken,
|
|
283
|
-
};
|
|
284
|
-
return cached;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// ---- 模型原子 ----
|
|
288
|
-
|
|
289
|
-
const THINKING_LEVELS = new Set<ThinkingLevelValue>([
|
|
290
|
-
"off",
|
|
291
|
-
"minimal",
|
|
292
|
-
"low",
|
|
293
|
-
"medium",
|
|
294
|
-
"high",
|
|
295
|
-
"xhigh",
|
|
296
|
-
"max",
|
|
297
|
-
]);
|
|
298
|
-
const FALLBACK_THINKING: ThinkingLevelValue = "medium";
|
|
299
|
-
|
|
300
|
-
/**
|
|
301
|
-
* 解析 "provider/model/thinking":按最后一个斜杠切出思考档,前半必须仍是 provider/model。
|
|
302
|
-
* 任何位置的模型配置都走这里,解析失败只记录问题并留空模型,让上层拒绝启动。
|
|
303
|
-
* 每个字段只报一条问题,且必带目标形状——两段式旧写法会同时踩中两项校验,逐项报错说不出该改成什么。
|
|
304
|
-
* 旧的分字段与两段式写法一律拒绝、不做兼容:兼容层会把三种写法固化成三套事实源。
|
|
305
|
-
*/
|
|
306
|
-
export function parseModelAtom(value: unknown, field: string, problems: string[]): ModelAtom {
|
|
307
|
-
const shape = `${field} 必须是“provider/model/thinking”字符串`;
|
|
308
|
-
if (typeof value !== "string" || !value) {
|
|
309
|
-
problems.push(shape);
|
|
310
|
-
return { model: "", thinking: FALLBACK_THINKING };
|
|
311
|
-
}
|
|
312
|
-
const slash = value.lastIndexOf("/");
|
|
313
|
-
const model = slash > 0 ? value.slice(0, slash) : "";
|
|
314
|
-
const thinking = slash > 0 ? value.slice(slash + 1) : value;
|
|
315
|
-
const providerSlash = model.indexOf("/");
|
|
316
|
-
const valid = THINKING_LEVELS.has(thinking as ThinkingLevelValue);
|
|
317
|
-
const faults: string[] = [];
|
|
318
|
-
if (providerSlash <= 0 || providerSlash === model.length - 1)
|
|
319
|
-
faults.push(`模型段不是 provider/model:${model || value}`);
|
|
320
|
-
if (!valid) faults.push(`思考档无效:${thinking}`);
|
|
321
|
-
if (faults.length) problems.push(`${shape}(${faults.join(";")})`);
|
|
322
|
-
return { model, thinking: valid ? (thinking as ThinkingLevelValue) : FALLBACK_THINKING };
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
// ---- presets 节 ----
|
|
326
|
-
|
|
327
|
-
const PRESET_KEYS = ["model", "tools", "instructions", "key"] as const;
|
|
328
|
-
|
|
329
|
-
function parsePresets(value: unknown, problems: string[]): Record<string, Preset> {
|
|
330
|
-
if (value === undefined) return {};
|
|
331
|
-
if (!isPlainObject(value)) {
|
|
332
|
-
problems.push("presets 必须是对象");
|
|
333
|
-
return {};
|
|
334
|
-
}
|
|
335
|
-
return Object.fromEntries(
|
|
336
|
-
Object.entries(value).map(([name, raw]) => [name, parsePreset(raw, `presets.${name}`, problems)]),
|
|
337
|
-
);
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
/** preset 只在写了 model 时切模型;其余字段与模型原子互不依赖。 */
|
|
341
|
-
function parsePreset(value: unknown, field: string, problems: string[]): Preset {
|
|
342
|
-
if (!isPlainObject(value)) {
|
|
343
|
-
problems.push(`${field} 必须是对象`);
|
|
344
|
-
return {};
|
|
345
|
-
}
|
|
346
|
-
rejectUnknownKeys(value, PRESET_KEYS, field, problems);
|
|
347
|
-
return {
|
|
348
|
-
...(value.model === undefined
|
|
349
|
-
? {}
|
|
350
|
-
: { model: parseModelAtom(value.model, `${field}.model`, problems) }),
|
|
351
|
-
...(value.tools === undefined ? {} : { tools: stringArray(value.tools, `${field}.tools`, problems) }),
|
|
352
|
-
...(value.instructions === undefined
|
|
353
|
-
? {}
|
|
354
|
-
: { instructions: stringValue(value.instructions, `${field}.instructions`, problems) }),
|
|
355
|
-
...(value.key === undefined ? {} : { key: stringValue(value.key, `${field}.key`, problems) }),
|
|
356
|
-
};
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// ---- review 节 ----
|
|
360
|
-
|
|
361
|
-
const REVIEW_KEYS = new Set([
|
|
362
|
-
"advisor",
|
|
363
|
-
"reviewers",
|
|
364
|
-
"maxRounds",
|
|
365
|
-
"advisorAfterFailures",
|
|
366
|
-
"timeoutMinutes",
|
|
367
|
-
"tools",
|
|
368
|
-
"language",
|
|
369
|
-
]);
|
|
370
|
-
const DEFAULT_TOOLS = ["read", "grep", "find", "ls", "bash"];
|
|
371
|
-
const LANGUAGES = new Set<Language>(["zh", "en"]);
|
|
372
|
-
|
|
373
|
-
/** 导出供测试:严格拒绝未知字段(含嵌套),类型错误一律记录而非静默回退。 */
|
|
374
|
-
export function parseReviewConfig(raw: Record<string, unknown>, problems: string[]): ReviewConfig {
|
|
375
|
-
for (const key of Object.keys(raw)) {
|
|
376
|
-
if (REVIEW_KEYS.has(key)) continue;
|
|
377
|
-
problems.push(key === "background"
|
|
378
|
-
? "review.background 已随审查子进程层删除,请直接移除该键"
|
|
379
|
-
: `未知字段 review.${key}`);
|
|
380
|
-
}
|
|
381
|
-
// advisor 与 reviewers 缺失由模型原子解析自己报形状,不再叠一条泛化的“必须显式配置”。
|
|
382
|
-
for (const key of REVIEW_KEYS)
|
|
383
|
-
if (key !== "advisor" && key !== "reviewers" && !(key in raw))
|
|
384
|
-
problems.push(`review.${key} 必须显式配置`);
|
|
385
|
-
const advisor = parseModelAtom(raw.advisor, "review.advisor", problems);
|
|
386
|
-
const reviewers = reviewModels(raw.reviewers, problems);
|
|
387
|
-
return {
|
|
388
|
-
advisor,
|
|
389
|
-
reviewers,
|
|
390
|
-
maxRounds: reviewInt(raw.maxRounds, "review.maxRounds", 5, 1, 10, problems),
|
|
391
|
-
advisorAfterFailures: reviewInt(raw.advisorAfterFailures, "review.advisorAfterFailures", 2, 1, 5, problems),
|
|
392
|
-
timeoutMinutes: reviewInt(raw.timeoutMinutes, "review.timeoutMinutes", 20, 1, 60, problems),
|
|
393
|
-
tools: reviewTools(raw.tools, problems),
|
|
394
|
-
language: reviewLanguage(raw.language, problems),
|
|
395
|
-
};
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
function reviewModels(value: unknown, problems: string[]): ModelAtom[] {
|
|
399
|
-
if (!Array.isArray(value) || value.length === 0 || value.length > 5) {
|
|
400
|
-
problems.push("review.reviewers 必须包含 1–5 个模型原子");
|
|
401
|
-
return [];
|
|
402
|
-
}
|
|
403
|
-
return value.map((item, index) => parseModelAtom(item, `review.reviewers[${index}]`, problems));
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
function reviewInt(
|
|
407
|
-
value: unknown,
|
|
408
|
-
field: string,
|
|
409
|
-
fallback: number,
|
|
410
|
-
min: number,
|
|
411
|
-
max: number,
|
|
412
|
-
problems: string[],
|
|
413
|
-
): number {
|
|
414
|
-
if (value === undefined) return fallback;
|
|
415
|
-
if (typeof value !== "number" || !Number.isInteger(value) || value < min || value > max) {
|
|
416
|
-
problems.push(`${field} 必须是 ${min}–${max} 的整数`);
|
|
417
|
-
return fallback;
|
|
418
|
-
}
|
|
419
|
-
return value;
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
function reviewTools(value: unknown, problems: string[]): string[] {
|
|
423
|
-
if (value === undefined) return [...DEFAULT_TOOLS];
|
|
424
|
-
if (!Array.isArray(value)) {
|
|
425
|
-
problems.push("review.tools 必须是字符串数组");
|
|
426
|
-
return [...DEFAULT_TOOLS];
|
|
427
|
-
}
|
|
428
|
-
const tools = value.filter((item): item is string => typeof item === "string" && item.length > 0);
|
|
429
|
-
if (tools.length !== value.length || tools.length === 0)
|
|
430
|
-
problems.push("review.tools 必须是非空字符串数组");
|
|
431
|
-
return tools.length > 0 ? tools : [...DEFAULT_TOOLS];
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
// ---- master 节 ----
|
|
435
|
-
|
|
436
|
-
/** 导出供测试:与 review 节同样严格拒绝未知字段,类型错误记录而非静默回退。 */
|
|
437
|
-
export function parseMasterConfig(raw: Record<string, unknown>, problems: string[]): MasterConfig {
|
|
438
|
-
for (const key of Object.keys(raw))
|
|
439
|
-
if (key !== "roles" && key !== "workerExcludeExtensions" && key !== "autoActivate")
|
|
440
|
-
problems.push(`未知字段 master.${key}`);
|
|
441
|
-
const exclusions = stringArray(raw.workerExcludeExtensions, "master.workerExcludeExtensions", problems);
|
|
442
|
-
const autoActivate = booleanValue(raw.autoActivate, "master.autoActivate", true, problems);
|
|
443
|
-
if (raw.roles === undefined)
|
|
444
|
-
return { roles: [], workerExcludeExtensions: exclusions, autoActivate };
|
|
445
|
-
if (!isPlainObject(raw.roles) || Object.keys(raw.roles).length === 0) {
|
|
446
|
-
problems.push("master.roles 必须是至少包含一个角色的对象");
|
|
447
|
-
return { roles: [], workerExcludeExtensions: exclusions, autoActivate };
|
|
448
|
-
}
|
|
449
|
-
const roles = Object.entries(raw.roles).map(([role, value]) =>
|
|
450
|
-
masterRole(value, `master.roles.${role}`, role, problems));
|
|
451
|
-
return { roles, workerExcludeExtensions: exclusions, autoActivate };
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
function masterRole(value: unknown, field: string, role: string, problems: string[]): MasterRole {
|
|
455
|
-
const record = asRecord(value);
|
|
456
|
-
rejectUnknownKeys(record, ["model", "use", "fallback"], field, problems);
|
|
457
|
-
const atom = parseModelAtom(record.model, `${field}.model`, problems);
|
|
458
|
-
const use = typeof record.use === "string" && record.use ? record.use : "";
|
|
459
|
-
if (!use) problems.push(`${field}.use 必须是非空字符串`);
|
|
460
|
-
const fallback = masterFallback(record.fallback, `${field}.fallback`, problems);
|
|
461
|
-
return { role, ...atom, use, fallback };
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
function masterFallback(value: unknown, field: string, problems: string[]): ModelAtom[] {
|
|
465
|
-
if (value === undefined) return [];
|
|
466
|
-
if (!Array.isArray(value) || value.length > 2) {
|
|
467
|
-
problems.push(`${field} 必须是至多 2 项的数组`);
|
|
468
|
-
return [];
|
|
469
|
-
}
|
|
470
|
-
return value.map((item, index) => parseModelAtom(item, `${field}[${index}]`, problems));
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
// ---- watcher 节 ----
|
|
474
|
-
|
|
475
|
-
const WATCHER_KEYS = ["enabled", "model", "context"] as const;
|
|
476
|
-
const WATCHER_CONTEXTS = new Set<WatcherContext>(["minimal", "full"]);
|
|
477
|
-
|
|
478
|
-
/** 导出供测试:model 必填(含思考档),enabled 默认 true、context 默认 minimal。 */
|
|
479
|
-
export function parseWatcherConfig(raw: Record<string, unknown>, problems: string[]): WatcherConfig {
|
|
480
|
-
rejectUnknownKeys(raw, WATCHER_KEYS, "watcher", problems);
|
|
481
|
-
const enabled = booleanValue(raw.enabled, "watcher.enabled", true, problems);
|
|
482
|
-
// 模型原子必填:缺失或写错时留空模型并记录问题,观察员据此拒绝启动。
|
|
483
|
-
const atom = parseModelAtom(raw.model, "watcher.model", problems);
|
|
484
|
-
let context: WatcherContext = "minimal";
|
|
485
|
-
if (raw.context !== undefined) {
|
|
486
|
-
if (typeof raw.context === "string" && WATCHER_CONTEXTS.has(raw.context as WatcherContext))
|
|
487
|
-
context = raw.context as WatcherContext;
|
|
488
|
-
else problems.push("watcher.context 必须是 minimal 或 full");
|
|
489
|
-
}
|
|
490
|
-
return { enabled, ...atom, context };
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
function reviewLanguage(value: unknown, problems: string[]): Language {
|
|
494
|
-
if (value === undefined) return "zh";
|
|
495
|
-
if (typeof value !== "string" || !LANGUAGES.has(value as Language)) {
|
|
496
|
-
problems.push("review.language 必须是 zh 或 en");
|
|
497
|
-
return "zh";
|
|
498
|
-
}
|
|
499
|
-
return value as Language;
|
|
500
|
-
}
|
package/deliver.ts
DELETED
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 统一投递入口(Master 事件与观察员发言共用):宿主流式中投自定义卡片、经
|
|
3
|
-
* steer 队列在句缝送达;会话歇透时改走 sendUserMessage 前门唤起(只告知不唤醒的
|
|
4
|
-
* 结果改走 inform,歇透时直接追加)。唤醒走前门而非 triggerTurn 的原因(#33)与回合中不得立即追加(#28)见根 AGENTS.md 硬约束。
|
|
5
|
-
* 宿主的扩展 sendUserMessage 返回 void、不等回合:以宿主记录这条消息为送达,没进回合就改走 steer 补投(tests/delivery-contract.test.ts 钉住)。
|
|
6
|
-
*
|
|
7
|
-
* 忙闲判断与发送必须在同一事件循环节拍内完成,两者之间禁止 await:会话落定
|
|
8
|
-
* 是下一节拍的事件,同节拍读到的忙闲不会骑墙;宿主在回合结束前清空 steer
|
|
9
|
-
* 队列,忙时入队的消息以同回合续跑送达,不会沦为唤醒者。
|
|
10
|
-
*/
|
|
11
|
-
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
12
|
-
import { textOf } from "./format.js";
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* 机器消息的信封是唯一事实源:模型上下文里的来源标记、卡片与折叠界面的识别都从这里来。
|
|
16
|
-
* 一条消息可含多个信封(并发落定的事件各占一个);整条文本恰好由信封构成才算机器消息。
|
|
17
|
-
*/
|
|
18
|
-
export const ENVELOPE_TAGS = ["firecode_master_event", "firecode_watcher", "firecode_review"] as const;
|
|
19
|
-
export type EnvelopeTag = (typeof ENVELOPE_TAGS)[number];
|
|
20
|
-
|
|
21
|
-
export interface ParsedEnvelope {
|
|
22
|
-
tag: EnvelopeTag;
|
|
23
|
-
body: string;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function wrapEnvelope(tag: EnvelopeTag, body: string): string {
|
|
27
|
-
return `<${tag}>\n${body}\n</${tag}>`;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const ENVELOPE = new RegExp(`<(${ENVELOPE_TAGS.join("|")})>\\n([\\s\\S]*?)\\n</\\1>\\s*`, "uy");
|
|
31
|
-
|
|
32
|
-
export function parseEnvelopes(text: string): ParsedEnvelope[] | undefined {
|
|
33
|
-
const source = text.trim();
|
|
34
|
-
const found: ParsedEnvelope[] = [];
|
|
35
|
-
let end = 0;
|
|
36
|
-
for (;;) {
|
|
37
|
-
ENVELOPE.lastIndex = end;
|
|
38
|
-
const match = ENVELOPE.exec(source);
|
|
39
|
-
if (!match) break;
|
|
40
|
-
found.push({ tag: match[1] as EnvelopeTag, body: match[2] });
|
|
41
|
-
end = ENVELOPE.lastIndex;
|
|
42
|
-
}
|
|
43
|
-
return found.length && end === source.length ? found : undefined;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export interface Delivery {
|
|
47
|
-
customType: string;
|
|
48
|
-
/** 已按信封格式包好的正文,同时是卡片渲染的唯一数据源。 */
|
|
49
|
-
content: string;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* 统一投递,resolve 即“已交给指挥官”:
|
|
54
|
-
* - 主回合在跑:卡片进 steer 队列,入队即交付;
|
|
55
|
-
* - 主回合歇透:前门 sendUserMessage 唤起,见 wake。
|
|
56
|
-
*/
|
|
57
|
-
export async function deliver(
|
|
58
|
-
pi: ExtensionAPI,
|
|
59
|
-
ctx: ExtensionContext,
|
|
60
|
-
envelope: Delivery,
|
|
61
|
-
): Promise<void> {
|
|
62
|
-
if (ctx.isIdle()) return wake(pi, envelope);
|
|
63
|
-
steer(pi, envelope);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* 告知不唤醒:指挥官没在等的结果(用户在子代理视图里直接派的运行)。主回合在跑时与 deliver 相同,经 steer 队列句缝送达;
|
|
68
|
-
* 主会话歇透时以不带 triggerTurn 的 sendMessage 追加为会话记录,下一回合自然进上下文,resolve 即已写入。
|
|
69
|
-
* 歇透时追加是安全的(核对宿主 AgentSession.sendCustomMessage):不在流式中且不触发回合时,宿主当场写会话树并刷新
|
|
70
|
-
* 上下文,追加在最后一条消息之后,不夹进工具调用与结果之间,已有前缀不变、提示词缓存不重写。#28 的快照分叉只发生在
|
|
71
|
-
* 回合进行中立即追加;这里也不经 triggerTurn,与 #33 无关。
|
|
72
|
-
*/
|
|
73
|
-
export async function inform(
|
|
74
|
-
pi: ExtensionAPI,
|
|
75
|
-
ctx: ExtensionContext,
|
|
76
|
-
envelope: Delivery,
|
|
77
|
-
): Promise<void> {
|
|
78
|
-
if (!ctx.isIdle()) return steer(pi, envelope);
|
|
79
|
-
pi.sendMessage({ customType: envelope.customType, content: envelope.content, display: true });
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function steer(pi: ExtensionAPI, envelope: Delivery): void {
|
|
83
|
-
pi.sendMessage(
|
|
84
|
-
{ customType: envelope.customType, content: envelope.content, display: true },
|
|
85
|
-
{ deliverAs: "steer" },
|
|
86
|
-
);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* 前门唤起按事实确认送达。宿主的扩展 sendUserMessage 返回 void、不等回合;开回合前被拒时只走宿主
|
|
91
|
-
* emitError,扩展订阅不到。所以等下一个回合的第一条消息:
|
|
92
|
-
* - 正是这条信封(role=user、正文原样):它开启了这一回合,送达;
|
|
93
|
-
* - 是别的(用户自己发的话、其他扩展唤起的消息):这条没进来,会话此刻正忙,同一节拍改走 steer 补投,句缝送达。
|
|
94
|
-
* 两种情况下 resolve 时指挥官回合都已在跑,Master 扣在飞数不会让 busy.ts 误报歇下。
|
|
95
|
-
* 之后再没有任何回合时投递保持未完成:宁可不歇下,也不误报歇下;没有计时器。
|
|
96
|
-
* 不以任意 agent_start 为送达(宿主拒绝后用户自己开的回合会被误认,事件被 ack 却没进上下文);不加超时(超时后仍判断不了是否送达)。
|
|
97
|
-
*/
|
|
98
|
-
function wake(pi: ExtensionAPI, envelope: Delivery): Promise<void> {
|
|
99
|
-
const delivered = Promise.withResolvers<void>();
|
|
100
|
-
let running = false;
|
|
101
|
-
const unsubscribe = [
|
|
102
|
-
pi.on("agent_start", () => {
|
|
103
|
-
running = true;
|
|
104
|
-
}),
|
|
105
|
-
pi.on("message_start", ({ message }) => {
|
|
106
|
-
if (!running) return;
|
|
107
|
-
for (const off of unsubscribe) off();
|
|
108
|
-
if (message.role !== "user" || textOf(message.content) !== envelope.content) steer(pi, envelope);
|
|
109
|
-
delivered.resolve();
|
|
110
|
-
}),
|
|
111
|
-
];
|
|
112
|
-
try {
|
|
113
|
-
pi.sendUserMessage(envelope.content);
|
|
114
|
-
} catch (error) {
|
|
115
|
-
for (const off of unsubscribe) off();
|
|
116
|
-
throw error;
|
|
117
|
-
}
|
|
118
|
-
return delivered.promise;
|
|
119
|
-
}
|