pi-firecode 0.8.0 → 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 +18 -13
- 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
package/master/spawn.ts
CHANGED
|
@@ -3,17 +3,21 @@ import { basename, dirname } from "node:path";
|
|
|
3
3
|
import type { Model } from "@earendil-works/pi-ai";
|
|
4
4
|
import {
|
|
5
5
|
createAgentSession,
|
|
6
|
+
createCodemodeExtension,
|
|
6
7
|
DefaultResourceLoader,
|
|
7
8
|
getAgentDir,
|
|
8
9
|
ModelRuntime,
|
|
9
10
|
SessionManager,
|
|
10
11
|
type AgentSession,
|
|
12
|
+
type InlineExtension,
|
|
11
13
|
type ToolDefinition,
|
|
12
14
|
} from "@earendil-works/pi-coding-agent";
|
|
13
15
|
import { withSubsessionRole, type SubsessionRole } from "./role.js";
|
|
14
16
|
import type { WorkerThinking } from "./state.js";
|
|
15
17
|
|
|
16
18
|
export const IDLE_SESSION_TIMEOUT_MS = 10 * 60_000;
|
|
19
|
+
/** 宿主只给 CLI 主会话注入内置扩展;子会话自带 codemode(按 builtin 名受 settings 开关),激活仍由 tools 决定。 */
|
|
20
|
+
const BUILTIN_CODEMODE: InlineExtension = { name: "codemode", factory: createCodemodeExtension(), replaceable: true, builtin: true };
|
|
17
21
|
|
|
18
22
|
export type SessionPersistence =
|
|
19
23
|
| { type: "memory" }
|
|
@@ -39,7 +43,8 @@ export interface SpawnedSession {
|
|
|
39
43
|
readonly session: AgentSession;
|
|
40
44
|
readonly sessionPath?: string;
|
|
41
45
|
prompt(text: string): Promise<void>;
|
|
42
|
-
|
|
46
|
+
/** 先让会话内扩展按宿主契约收口(session_shutdown),再释放;resolve 即收口完成。 */
|
|
47
|
+
dispose(): Promise<void>;
|
|
43
48
|
}
|
|
44
49
|
|
|
45
50
|
interface HeldSession {
|
|
@@ -47,21 +52,41 @@ interface HeldSession {
|
|
|
47
52
|
session: AgentSession;
|
|
48
53
|
sessionPath?: string;
|
|
49
54
|
timer?: NodeJS.Timeout;
|
|
50
|
-
|
|
51
|
-
disposed: boolean;
|
|
55
|
+
releasing?: Promise<void>;
|
|
52
56
|
}
|
|
53
57
|
|
|
54
|
-
|
|
58
|
+
// 单写者登记必须进程唯一:宿主按文件重新求值模块图(见 role.ts),模块级集合在副本间互不可见。
|
|
59
|
+
const WRITERS_KEY = Symbol.for("firecode.session-writers");
|
|
60
|
+
const SESSION_WRITERS = ((globalThis as Record<symbol, unknown>)[WRITERS_KEY] ??= new Set<string>()) as Set<string>;
|
|
55
61
|
|
|
56
|
-
|
|
62
|
+
export interface PoolEnvironment {
|
|
63
|
+
agentDir?: string;
|
|
64
|
+
modelRuntime?: ModelRuntime;
|
|
65
|
+
idleTimeoutMs?: number;
|
|
66
|
+
/** 模型原子 id(provider/model)解析;默认用池内缓存的一份 ModelRuntime。 */
|
|
67
|
+
resolveModel?: (id: string) => Promise<Model<any>>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** 全插件唯一的进程内子会话入口:模型解析、单写者登记与热会话生命周期都在这里。 */
|
|
57
71
|
export class InProcessSessionPool {
|
|
58
72
|
private readonly held = new Map<string, HeldSession>();
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
73
|
+
private runtime?: Promise<ModelRuntime>;
|
|
74
|
+
private readonly releaseListeners = new Set<(sessionPath: string) => void>();
|
|
75
|
+
|
|
76
|
+
constructor(private readonly environment: PoolEnvironment = {}) {}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 把 "provider/model" 解析成模型。ModelRuntime 每个池只建一次(auth.json 与 models.json 只读一次),解析与建子会话共用;
|
|
80
|
+
* 扩展注册的 provider 在这里不可见,只能用内置 provider 与 models.json 里的模型。
|
|
81
|
+
*/
|
|
82
|
+
async resolveModel(id: string): Promise<Model<any>> {
|
|
83
|
+
if (this.environment.resolveModel) return this.environment.resolveModel(id);
|
|
84
|
+
const runtime = await this.modelRuntime();
|
|
85
|
+
const slash = id.indexOf("/");
|
|
86
|
+
const model = slash > 0 ? runtime.getModel(id.slice(0, slash), id.slice(slash + 1)) : undefined;
|
|
87
|
+
if (!model) throw new Error(`找不到模型:${id};子会话只能使用内置 provider 或 models.json 里的模型`);
|
|
88
|
+
return model;
|
|
89
|
+
}
|
|
65
90
|
|
|
66
91
|
async spawn(options: SpawnSessionOptions): Promise<SpawnedSession> {
|
|
67
92
|
const sessionPath = options.persistence.type === "file" ? options.persistence.sessionPath : undefined;
|
|
@@ -77,6 +102,7 @@ export class InProcessSessionPool {
|
|
|
77
102
|
cwd: options.cwd,
|
|
78
103
|
agentDir: this.environment.agentDir ?? getAgentDir(),
|
|
79
104
|
noContextFiles: !options.contextFiles,
|
|
105
|
+
extensionFactories: [BUILTIN_CODEMODE],
|
|
80
106
|
noExtensions: options.isolated,
|
|
81
107
|
noSkills: options.isolated,
|
|
82
108
|
noPromptTemplates: options.isolated,
|
|
@@ -96,7 +122,8 @@ export class InProcessSessionPool {
|
|
|
96
122
|
const result = await createAgentSession({
|
|
97
123
|
cwd: options.cwd,
|
|
98
124
|
agentDir: this.environment.agentDir,
|
|
99
|
-
|
|
125
|
+
// 与模型解析同一份:不传时宿主会为每个子会话重读一次 auth.json 与 models.json。
|
|
126
|
+
modelRuntime: await this.modelRuntime(),
|
|
100
127
|
model: options.model,
|
|
101
128
|
thinkingLevel: options.thinking,
|
|
102
129
|
tools: options.tools,
|
|
@@ -112,12 +139,8 @@ export class InProcessSessionPool {
|
|
|
112
139
|
}
|
|
113
140
|
|
|
114
141
|
const key = sessionPath ?? `memory:${crypto.randomUUID()}`;
|
|
115
|
-
const held: HeldSession = { key, session: created, sessionPath
|
|
142
|
+
const held: HeldSession = { key, session: created, sessionPath };
|
|
116
143
|
this.held.set(key, held);
|
|
117
|
-
held.unsubscribe = created.subscribe((event) => {
|
|
118
|
-
if (event.type === "agent_start") this.clearTimer(held);
|
|
119
|
-
if (event.type === "agent_settled") this.armIdleDisposal(held);
|
|
120
|
-
});
|
|
121
144
|
return {
|
|
122
145
|
session: created,
|
|
123
146
|
sessionPath,
|
|
@@ -126,6 +149,20 @@ export class InProcessSessionPool {
|
|
|
126
149
|
};
|
|
127
150
|
}
|
|
128
151
|
|
|
152
|
+
/** 热会话被释放(空闲到期或 dispose)后通知持有方放掉对它的订阅与引用;返回退订函数。 */
|
|
153
|
+
onRelease(listener: (sessionPath: string) => void): () => void {
|
|
154
|
+
this.releaseListeners.add(listener);
|
|
155
|
+
return () => this.releaseListeners.delete(listener);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private modelRuntime(): Promise<ModelRuntime> {
|
|
159
|
+
const agentDir = this.environment.agentDir ?? getAgentDir();
|
|
160
|
+
this.runtime ??= this.environment.modelRuntime
|
|
161
|
+
? Promise.resolve(this.environment.modelRuntime)
|
|
162
|
+
: ModelRuntime.create({ authPath: `${agentDir}/auth.json`, modelsPath: `${agentDir}/models.json` });
|
|
163
|
+
return this.runtime;
|
|
164
|
+
}
|
|
165
|
+
|
|
129
166
|
has(sessionPath: string): boolean {
|
|
130
167
|
return this.held.has(sessionPath);
|
|
131
168
|
}
|
|
@@ -137,38 +174,42 @@ export class InProcessSessionPool {
|
|
|
137
174
|
return held.session;
|
|
138
175
|
}
|
|
139
176
|
|
|
177
|
+
/** 空闲只由调用方判定:池不订阅会话事件自判空闲;超时后释放热会话,档案与 JSONL 保留。 */
|
|
140
178
|
markIdle(sessionPath: string): void {
|
|
141
179
|
const held = this.held.get(sessionPath);
|
|
142
|
-
if (held)
|
|
180
|
+
if (!held) return;
|
|
181
|
+
this.clearTimer(held);
|
|
182
|
+
held.timer = setTimeout(() => void this.release(held), this.environment.idleTimeoutMs ?? IDLE_SESSION_TIMEOUT_MS);
|
|
183
|
+
held.timer.unref?.();
|
|
143
184
|
}
|
|
144
185
|
|
|
145
|
-
dispose(sessionPath: string): boolean {
|
|
186
|
+
async dispose(sessionPath: string): Promise<boolean> {
|
|
146
187
|
const held = this.held.get(sessionPath);
|
|
147
188
|
if (!held) return false;
|
|
148
|
-
this.release(held);
|
|
189
|
+
await this.release(held);
|
|
149
190
|
return true;
|
|
150
191
|
}
|
|
151
192
|
|
|
152
|
-
disposeAll(): void {
|
|
153
|
-
|
|
193
|
+
async disposeAll(): Promise<void> {
|
|
194
|
+
await Promise.all([...this.held.values()].map((held) => this.release(held)));
|
|
154
195
|
}
|
|
155
196
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
held.
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
197
|
+
/** 镜像宿主替换会话的顺序:先 session_shutdown 让扩展收口,再 dispose 作废上下文。 */
|
|
198
|
+
private release(held: HeldSession): Promise<void> {
|
|
199
|
+
held.releasing ??= (async () => {
|
|
200
|
+
this.clearTimer(held);
|
|
201
|
+
if (this.held.get(held.key) === held) this.held.delete(held.key);
|
|
202
|
+
try {
|
|
203
|
+
await held.session.extensionRunner.emit({ type: "session_shutdown", reason: "quit" });
|
|
204
|
+
} finally {
|
|
205
|
+
held.session.dispose();
|
|
206
|
+
if (held.sessionPath) {
|
|
207
|
+
SESSION_WRITERS.delete(held.sessionPath);
|
|
208
|
+
for (const listener of this.releaseListeners) listener(held.sessionPath);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
})();
|
|
212
|
+
return held.releasing;
|
|
172
213
|
}
|
|
173
214
|
|
|
174
215
|
private clearTimer(held: HeldSession): void {
|
package/master/state.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
2
|
import { dirname, join } from "node:path";
|
|
4
3
|
|
|
5
|
-
const STATE_VERSION =
|
|
4
|
+
const STATE_VERSION = 9;
|
|
6
5
|
|
|
7
6
|
export const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
|
|
8
7
|
export type WorkerThinking = (typeof THINKING_LEVELS)[number];
|
|
@@ -18,6 +17,11 @@ export interface WorkerRef {
|
|
|
18
17
|
sessionPath: string;
|
|
19
18
|
cwd?: string;
|
|
20
19
|
interruptedAt?: number;
|
|
20
|
+
/**
|
|
21
|
+
* 启动序:start 在同步段按到达先后取的单调序号,活动列表与全过程视图按它排。必须持久化:并行 start 越过
|
|
22
|
+
* await 后落盘的先后(以及任何落盘时刻)与到达先后不一致,恢复后若靠别的字段排会换序。
|
|
23
|
+
*/
|
|
24
|
+
launch: number;
|
|
21
25
|
reviewNeeded?: boolean;
|
|
22
26
|
disposition?: WorkerDisposition;
|
|
23
27
|
}
|
|
@@ -79,9 +83,10 @@ export class LegacyMasterStateError extends Error {
|
|
|
79
83
|
}
|
|
80
84
|
}
|
|
81
85
|
|
|
82
|
-
|
|
86
|
+
/** 子代理池档案:与运行配置同一个 Pi Agent 目录(含 PI_CODING_AGENT_DIR 覆写),按主会话 id 分文件。 */
|
|
87
|
+
export function masterStatePath(agentDir: string, sessionId: string): string {
|
|
83
88
|
const safeId = sessionId.replace(/[^a-zA-Z0-9_-]/gu, "-");
|
|
84
|
-
return join(
|
|
89
|
+
return join(agentDir, "tmp", `firecode-master-${safeId}.json`);
|
|
85
90
|
}
|
|
86
91
|
|
|
87
92
|
export function loadMasterState(path: string): MasterState | undefined {
|
|
@@ -99,8 +104,8 @@ export function loadMasterState(path: string): MasterState | undefined {
|
|
|
99
104
|
throw new Error(`Master Worker Pool 状态不是合法 JSON:${path}`);
|
|
100
105
|
}
|
|
101
106
|
const version = (data as { version?: unknown } | null)?.version;
|
|
102
|
-
if (typeof version === "number" && version !== STATE_VERSION) throw new LegacyMasterStateError(version);
|
|
103
|
-
const state = restoreMasterState(data);
|
|
107
|
+
if (typeof version === "number" && version !== STATE_VERSION && version !== 8) throw new LegacyMasterStateError(version);
|
|
108
|
+
const state = restoreMasterState(version === 8 ? migrateFromV8(data as { workers?: unknown }) : data);
|
|
104
109
|
if (!state) throw new Error(`Master Worker Pool 状态结构无效:${path}`);
|
|
105
110
|
return state;
|
|
106
111
|
}
|
|
@@ -147,6 +152,24 @@ export class MasterStore {
|
|
|
147
152
|
}
|
|
148
153
|
}
|
|
149
154
|
|
|
155
|
+
/**
|
|
156
|
+
* v8 → v9 只差启动序:v8 的创建时间是同一先后的近似(并行 start 下可能有出入,从此以 launch 为准),没有创建时间的
|
|
157
|
+
* 是更早版本恢复来的、排最前。升级不丢池:池里是用户仍在用的子代理,丢弃会让指挥官失去它们的会话与审查义务。
|
|
158
|
+
*/
|
|
159
|
+
function migrateFromV8(data: { workers?: unknown }): unknown {
|
|
160
|
+
if (!Array.isArray(data.workers)) return data;
|
|
161
|
+
const workers = data.workers as Record<string, unknown>[];
|
|
162
|
+
const created = (index: number) => (typeof workers[index]?.createdAt === "number" ? workers[index].createdAt as number : -Infinity);
|
|
163
|
+
const order = workers.map((_, index) => index).sort((a, b) => created(a) - created(b) || a - b);
|
|
164
|
+
return {
|
|
165
|
+
version: STATE_VERSION,
|
|
166
|
+
workers: workers.map((worker, index) => {
|
|
167
|
+
const { createdAt: _createdAt, ...rest } = worker ?? {};
|
|
168
|
+
return { ...rest, launch: order.indexOf(index) + 1 };
|
|
169
|
+
}),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
150
173
|
export function requireWorker(state: MasterState, name: string): WorkerRef {
|
|
151
174
|
const worker = state.workers.find((candidate) => candidate.name === name);
|
|
152
175
|
if (!worker) throw new Error(`子代理不存在:${name}`);
|
|
@@ -191,6 +214,7 @@ function isWorker(value: unknown): value is WorkerRef {
|
|
|
191
214
|
if (record.interruptedAt !== undefined && (typeof record.interruptedAt !== "number" || record.interruptedAt <= 0))
|
|
192
215
|
return false;
|
|
193
216
|
if (record.reviewNeeded !== undefined && typeof record.reviewNeeded !== "boolean") return false;
|
|
217
|
+
if (typeof record.launch !== "number" || !Number.isInteger(record.launch) || record.launch <= 0) return false;
|
|
194
218
|
if (record.disposition !== undefined && record.disposition !== "pending" && record.disposition !== "reminded")
|
|
195
219
|
return false;
|
|
196
220
|
return true;
|