pi-firecode 1.0.0 → 1.0.1
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 +55 -41
- package/README.zh-CN.md +74 -0
- package/config.example.jsonc +21 -22
- package/dist/config.example.jsonc +90 -0
- package/dist/index.js +11464 -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/master/runtime.ts
DELETED
|
@@ -1,259 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 一个指挥官会话的运行时:持久化档案(store)、按名字索引的 Worker 运行时事实(live)、事件发件箱与活动列表。
|
|
3
|
-
* 运行时事实只在进程内;档案与 JSONL 才是身份与续派的事实源。会话关闭后 closed 置位,迟到的异步续延一律作废。
|
|
4
|
-
*/
|
|
5
|
-
import { getAgentDir, type AgentSession, type AgentSessionEvent, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
6
|
-
import type { MasterRole } from "../config.js";
|
|
7
|
-
import { HEAT_COLORS, paint } from "../flame.js";
|
|
8
|
-
import type { ReviewProgress } from "../review/outcome.js";
|
|
9
|
-
import { ActivityList, visibleRows, type ActivityFacts, type SettledFact } from "./activity-list.js";
|
|
10
|
-
import { Outbox } from "./outbox.js";
|
|
11
|
-
import { openWorkerView } from "./worker-view.js";
|
|
12
|
-
import type { InProcessSessionPool } from "./spawn.js";
|
|
13
|
-
import { MasterStore, masterStatePath, type MasterState, type WorkerRef } from "./state.js";
|
|
14
|
-
|
|
15
|
-
/** 注册时定下、跨会话不变的配置与依赖。 */
|
|
16
|
-
export interface MasterSetup {
|
|
17
|
-
pi: ExtensionAPI;
|
|
18
|
-
pool: InProcessSessionPool;
|
|
19
|
-
roster: MasterRole[];
|
|
20
|
-
exclusions: string[];
|
|
21
|
-
/** Worker 系统提示;提示词文件坏了时抛错。 */
|
|
22
|
-
workerPrompt(): string;
|
|
23
|
-
/** fire-review 不可用的原因;可用时为 undefined。 */
|
|
24
|
-
reviewGate?: string;
|
|
25
|
-
interruptResumeMs: number;
|
|
26
|
-
/** 指挥官空闲时合并唤醒的安静窗口(见 outbox.ts)。 */
|
|
27
|
-
wakeQuietMs: number;
|
|
28
|
-
/** 在飞子代理数的唯一发布口(跨会话保持上次发布值以便配对)。 */
|
|
29
|
-
publishInFlight(count: number): void;
|
|
30
|
-
/** 本段会话进行中的起点(busy.ts 的 since,与上边框计时同一事实);歇下时为 undefined。 */
|
|
31
|
-
sessionSince(): number | undefined;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export interface CurrentTool {
|
|
35
|
-
tool: string;
|
|
36
|
-
args: unknown;
|
|
37
|
-
startedAt: number;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* 这次运行是谁派的。在飞数的定义是“指挥官在等结果的运行”:用户在全过程视图里直接派的运行指挥官并不在等,
|
|
42
|
-
* 不计入在飞数(主会话不因它进入进行中、不产生主会话轮记录),落定事件只告知不唤醒;
|
|
43
|
-
* 指挥官在这次运行中途又 send 给它时转为指挥官的(它从此在等)。
|
|
44
|
-
*/
|
|
45
|
-
export type RunOrigin = "master" | "view";
|
|
46
|
-
|
|
47
|
-
/** 一个 Worker 名下的全部运行时事实;kill 时整条删除。 */
|
|
48
|
-
export interface WorkerLive {
|
|
49
|
-
/** start 已占名但档案尚未落盘。 */
|
|
50
|
-
starting?: true;
|
|
51
|
-
/** send/review 的准备过程单飞。 */
|
|
52
|
-
transitioning?: true;
|
|
53
|
-
/** 当前回合的令牌;落定回调只认自己的令牌。 */
|
|
54
|
-
run?: symbol;
|
|
55
|
-
/** interrupt 标记的回合:它落定时按中断处理。 */
|
|
56
|
-
interruptedRun?: symbol;
|
|
57
|
-
interruptTimer?: NodeJS.Timeout;
|
|
58
|
-
/** 本次运行(start/send/review 投递)的起点:活动列表的实时耗时,以及从子代理会话里挑出属于这次运行的轮记录。 */
|
|
59
|
-
runStartedAt?: number;
|
|
60
|
-
/** 这次运行里用户在子代理全过程视图直接说的话;落定事件据此注明来源。 */
|
|
61
|
-
viewPrompts: string[];
|
|
62
|
-
origin: RunOrigin;
|
|
63
|
-
/** 最近一次输出(模型 token 或工具事件),活动列表据此判卡住。 */
|
|
64
|
-
lastOutputAt?: number;
|
|
65
|
-
currentTools: Map<string, CurrentTool>;
|
|
66
|
-
reviewProgress?: ReviewProgress;
|
|
67
|
-
/** 最近一次落定的时刻:池起释放计时、列表“落定 X 前”与活动列表冻结耗时共用这一份。 */
|
|
68
|
-
idleAt?: number;
|
|
69
|
-
/** 活动列表上的落定结局;ack 清掉失败与被中断,完成留到 kill。 */
|
|
70
|
-
outcome?: Omit<SettledFact, "at">;
|
|
71
|
-
observed?: { session: AgentSession; sessionPath: string; unsubscribe: () => void };
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const LIST_WIDGET_KEY = "firecode-master-list";
|
|
75
|
-
/** 边框身份:纯文字,子代理状态由输入框上方的活动列表承担。 */
|
|
76
|
-
const MASTER_IDENTITY = paint(HEAT_COLORS.orange, "指挥官");
|
|
77
|
-
|
|
78
|
-
export class MasterRuntime {
|
|
79
|
-
readonly store: MasterStore;
|
|
80
|
-
readonly outbox: Outbox;
|
|
81
|
-
readonly live = new Map<string, WorkerLive>();
|
|
82
|
-
/** 最近取出的启动序;接着档案里已有的最大值,恢复后新 start 仍排在后面。 */
|
|
83
|
-
private launchSeq: number;
|
|
84
|
-
private list?: ActivityList;
|
|
85
|
-
private closedValue = false;
|
|
86
|
-
private readonly stopReleaseWatch: () => void;
|
|
87
|
-
/** 子代理会话被接上订阅(冷启动或重开)时通知:全过程视图据此在第一条事件之前接上自己的订阅。 */
|
|
88
|
-
private readonly sessionListeners = new Set<(name: string) => void>();
|
|
89
|
-
/** 子代理被移除(kill 或启动失败撤票)时通知:全过程视图据此显示已移除。 */
|
|
90
|
-
private readonly removedListeners = new Set<(name: string) => void>();
|
|
91
|
-
|
|
92
|
-
constructor(readonly setup: MasterSetup, public ctx: ExtensionContext, restored?: MasterState) {
|
|
93
|
-
this.outbox = new Outbox(this);
|
|
94
|
-
this.store = new MasterStore(masterStatePath(getAgentDir(), ctx.sessionManager.getSessionId()), restored, () => this.render());
|
|
95
|
-
this.launchSeq = Math.max(0, ...this.store.state.workers.map((worker) => worker.launch));
|
|
96
|
-
// 池空闲释放热会话后放掉订阅:不再持有已关闭的会话。
|
|
97
|
-
this.stopReleaseWatch = setup.pool.onRelease((sessionPath) => {
|
|
98
|
-
for (const live of this.live.values())
|
|
99
|
-
if (live.observed?.sessionPath === sessionPath) this.unobserve(live);
|
|
100
|
-
});
|
|
101
|
-
ctx.ui.setWidget(LIST_WIDGET_KEY, (tui, theme) => {
|
|
102
|
-
this.list = new ActivityList(tui, theme, () => this.activityFacts(),
|
|
103
|
-
() => visibleRows(tui.terminal?.rows), (name) => void openWorkerView(this, name));
|
|
104
|
-
return this.list;
|
|
105
|
-
}, { placement: "aboveEditor" });
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
get closed(): boolean {
|
|
109
|
-
return this.closedValue;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
assertOpen(): void {
|
|
113
|
-
if (this.closedValue) throw new Error("Master 会话已替换,取消旧会话动作");
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/** 事实变化后的唯一重绘入口:在飞数、边框身份与活动列表。 */
|
|
117
|
-
render(): void {
|
|
118
|
-
if (this.closedValue) return;
|
|
119
|
-
this.outbox.scheduleInFlight();
|
|
120
|
-
this.ctx.ui.setStatus("master", MASTER_IDENTITY);
|
|
121
|
-
this.list?.sync();
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/** 新的一轮(人类输入)开始:活动列表的展开收起。 */
|
|
125
|
-
collapseList(): void {
|
|
126
|
-
this.list?.collapse();
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
liveOf(name: string): WorkerLive {
|
|
130
|
-
let live = this.live.get(name);
|
|
131
|
-
if (!live) this.live.set(name, live = { currentTools: new Map(), viewPrompts: [], origin: "master" });
|
|
132
|
-
return live;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/** start 同步段占名并取启动序(并发 start 越过后续 await 的先后不定,序号必须在此取)。 */
|
|
136
|
-
reserve(name: string): { live: WorkerLive; launch: number } {
|
|
137
|
-
const live: WorkerLive = { currentTools: new Map(), viewPrompts: [], origin: "master", starting: true };
|
|
138
|
-
this.live.set(name, live);
|
|
139
|
-
return { live, launch: ++this.launchSeq };
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* 移除子代理的唯一入口:删名下全部运行时事实与档案,档案确有这一票时通知订阅方。
|
|
144
|
-
* 运行时事实只删属于 expected 的那一条(kill 后同名重开的新票不受影响)。
|
|
145
|
-
*/
|
|
146
|
-
remove(name: string, expected = this.live.get(name)): void {
|
|
147
|
-
const live = this.live.get(name);
|
|
148
|
-
if (live && live === expected) {
|
|
149
|
-
clearTimeout(live.interruptTimer);
|
|
150
|
-
if (live.observed) this.unobserve(live);
|
|
151
|
-
this.live.delete(name);
|
|
152
|
-
}
|
|
153
|
-
const before = this.store.state;
|
|
154
|
-
if (this.store.dispatch({ type: "REMOVE_WORKER", name }) === before) return;
|
|
155
|
-
for (const notify of this.removedListeners) notify(name);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/** await 之后的唯一重读点:档案已被 kill(或同名换票)就释放热会话并放弃本次动作。 */
|
|
159
|
-
current(identity: Pick<WorkerRef, "name" | "sessionPath">): WorkerRef {
|
|
160
|
-
this.assertOpen();
|
|
161
|
-
const current = this.store.state.workers.find((worker) => worker.name === identity.name);
|
|
162
|
-
if (current?.sessionPath === identity.sessionPath) return current;
|
|
163
|
-
void this.setup.pool.dispose(identity.sessionPath);
|
|
164
|
-
throw new Error(`${identity.name} 已被 kill,取消本次动作`);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/** await 之后的写回只经这里:基于重读的最新档案做函数式更新,不拿 await 前的快照覆盖。 */
|
|
168
|
-
commit(identity: Pick<WorkerRef, "name" | "sessionPath">, update: (current: WorkerRef) => WorkerRef): WorkerRef {
|
|
169
|
-
const next = update(this.current(identity));
|
|
170
|
-
this.store.dispatch({ type: "UPSERT_WORKER", worker: next });
|
|
171
|
-
return next;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/** 同步读:档案仍是这一票时返回,否则 undefined(落定回调用,不抛)。 */
|
|
175
|
-
find(identity: Pick<WorkerRef, "name" | "sessionPath">): WorkerRef | undefined {
|
|
176
|
-
const current = this.store.state.workers.find((worker) => worker.name === identity.name);
|
|
177
|
-
return current?.sessionPath === identity.sessionPath ? current : undefined;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/** Worker 闲下来的唯一登记点:记落定时刻与结局,池据此起释放计时。 */
|
|
181
|
-
markIdle(worker: WorkerRef, outcome: WorkerLive["outcome"], at = Date.now()): void {
|
|
182
|
-
const live = this.liveOf(worker.name);
|
|
183
|
-
live.idleAt = at;
|
|
184
|
-
live.outcome = outcome;
|
|
185
|
-
live.currentTools.clear();
|
|
186
|
-
live.reviewProgress = undefined;
|
|
187
|
-
this.setup.pool.markIdle(worker.sessionPath);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/** 开始一次运行(start/send/review):起点与来源归这一次,旧的中断提醒作废。 */
|
|
191
|
-
beginRun(name: string, origin: RunOrigin = "master"): void {
|
|
192
|
-
const live = this.liveOf(name);
|
|
193
|
-
clearTimeout(live.interruptTimer);
|
|
194
|
-
live.interruptTimer = undefined;
|
|
195
|
-
live.runStartedAt = Date.now();
|
|
196
|
-
live.viewPrompts = [];
|
|
197
|
-
live.origin = origin;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/** 每个 Worker 只挂一个会话订阅;换了会话(释放后重开)才重挂。 */
|
|
201
|
-
observe(worker: WorkerRef, session: AgentSession, listener: (live: WorkerLive, event: AgentSessionEvent) => void): void {
|
|
202
|
-
this.assertOpen();
|
|
203
|
-
const live = this.liveOf(worker.name);
|
|
204
|
-
if (live.observed?.session === session) return;
|
|
205
|
-
if (live.observed) this.unobserve(live);
|
|
206
|
-
const unsubscribe = session.subscribe((event) => {
|
|
207
|
-
if (!this.closedValue && this.live.get(worker.name) === live) listener(live, event);
|
|
208
|
-
});
|
|
209
|
-
live.observed = { session, sessionPath: worker.sessionPath, unsubscribe };
|
|
210
|
-
for (const notify of this.sessionListeners) notify(worker.name);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
onWorkerSession(listener: (name: string) => void): () => void {
|
|
214
|
-
this.sessionListeners.add(listener);
|
|
215
|
-
return () => this.sessionListeners.delete(listener);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
onWorkerRemoved(listener: (name: string) => void): () => void {
|
|
219
|
-
this.removedListeners.add(listener);
|
|
220
|
-
return () => this.removedListeners.delete(listener);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
close(): void {
|
|
224
|
-
this.closedValue = true;
|
|
225
|
-
this.stopReleaseWatch();
|
|
226
|
-
for (const live of this.live.values()) {
|
|
227
|
-
clearTimeout(live.interruptTimer);
|
|
228
|
-
if (live.observed) this.unobserve(live);
|
|
229
|
-
}
|
|
230
|
-
this.live.clear();
|
|
231
|
-
this.outbox.close();
|
|
232
|
-
this.list?.dispose();
|
|
233
|
-
this.ctx.ui.setWidget(LIST_WIDGET_KEY, undefined);
|
|
234
|
-
this.ctx.ui.setStatus("master", undefined);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
private unobserve(live: WorkerLive): void {
|
|
238
|
-
live.observed?.unsubscribe();
|
|
239
|
-
live.observed = undefined;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
/** 活动列表的输入:档案加运行时事实的一次投影(全过程视图按同一顺序换子代理)。 */
|
|
243
|
-
activityFacts(): ActivityFacts {
|
|
244
|
-
const workers = this.store.state.workers;
|
|
245
|
-
const byPath = <T>(pick: (live: WorkerLive) => T | undefined) => new Map(workers.flatMap((worker) => {
|
|
246
|
-
const live = this.live.get(worker.name);
|
|
247
|
-
const value = live && pick(live);
|
|
248
|
-
return value === undefined ? [] : [[worker.sessionPath, value] as const];
|
|
249
|
-
}));
|
|
250
|
-
return {
|
|
251
|
-
workers,
|
|
252
|
-
currentTools: byPath((live) => (live.currentTools.size ? live.currentTools : undefined)),
|
|
253
|
-
reviewProgress: byPath((live) => live.reviewProgress),
|
|
254
|
-
runStartedAt: byPath((live) => live.runStartedAt),
|
|
255
|
-
lastOutputAt: byPath((live) => live.lastOutputAt),
|
|
256
|
-
settled: byPath((live) => (live.outcome && live.idleAt !== undefined ? { ...live.outcome, at: live.idleAt } : undefined)),
|
|
257
|
-
};
|
|
258
|
-
}
|
|
259
|
-
}
|
package/master/spawn.ts
DELETED
|
@@ -1,234 +0,0 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
|
-
import { basename, dirname } from "node:path";
|
|
3
|
-
import type { Model } from "@earendil-works/pi-ai";
|
|
4
|
-
import {
|
|
5
|
-
createAgentSession,
|
|
6
|
-
createCodemodeExtension,
|
|
7
|
-
DefaultResourceLoader,
|
|
8
|
-
getAgentDir,
|
|
9
|
-
ModelRuntime,
|
|
10
|
-
SessionManager,
|
|
11
|
-
type AgentSession,
|
|
12
|
-
type InlineExtension,
|
|
13
|
-
type ToolDefinition,
|
|
14
|
-
} from "@earendil-works/pi-coding-agent";
|
|
15
|
-
import { withSubsessionRole, type SubsessionRole } from "./role.js";
|
|
16
|
-
import type { WorkerThinking } from "./state.js";
|
|
17
|
-
|
|
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 };
|
|
21
|
-
|
|
22
|
-
export type SessionPersistence =
|
|
23
|
-
| { type: "memory" }
|
|
24
|
-
| { type: "file"; sessionPath: string; resume?: boolean };
|
|
25
|
-
|
|
26
|
-
export interface SpawnSessionOptions {
|
|
27
|
-
cwd: string;
|
|
28
|
-
model: Model<any>;
|
|
29
|
-
role: SubsessionRole;
|
|
30
|
-
thinking: WorkerThinking;
|
|
31
|
-
tools: string[];
|
|
32
|
-
/** 只属于本子会话的工具(如观察员的 advise);名字仍要出现在 tools 里才激活。 */
|
|
33
|
-
customTools?: ToolDefinition[];
|
|
34
|
-
excludeExtensions?: string[];
|
|
35
|
-
systemPrompt: { mode: "append" | "replace"; text: string };
|
|
36
|
-
contextFiles: boolean;
|
|
37
|
-
persistence: SessionPersistence;
|
|
38
|
-
/** 审查会话关闭自动扩展、Skill 与模板,保持评判政策不受被审项目改写。 */
|
|
39
|
-
isolated?: boolean;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export interface SpawnedSession {
|
|
43
|
-
readonly session: AgentSession;
|
|
44
|
-
readonly sessionPath?: string;
|
|
45
|
-
prompt(text: string): Promise<void>;
|
|
46
|
-
/** 先让会话内扩展按宿主契约收口(session_shutdown),再释放;resolve 即收口完成。 */
|
|
47
|
-
dispose(): Promise<void>;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
interface HeldSession {
|
|
51
|
-
key: string;
|
|
52
|
-
session: AgentSession;
|
|
53
|
-
sessionPath?: string;
|
|
54
|
-
timer?: NodeJS.Timeout;
|
|
55
|
-
releasing?: Promise<void>;
|
|
56
|
-
}
|
|
57
|
-
|
|
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>;
|
|
61
|
-
|
|
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
|
-
/** 全插件唯一的进程内子会话入口:模型解析、单写者登记与热会话生命周期都在这里。 */
|
|
71
|
-
export class InProcessSessionPool {
|
|
72
|
-
private readonly held = new Map<string, HeldSession>();
|
|
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
|
-
}
|
|
90
|
-
|
|
91
|
-
async spawn(options: SpawnSessionOptions): Promise<SpawnedSession> {
|
|
92
|
-
const sessionPath = options.persistence.type === "file" ? options.persistence.sessionPath : undefined;
|
|
93
|
-
if (sessionPath && SESSION_WRITERS.has(sessionPath))
|
|
94
|
-
throw new Error(`sessionPath 已有进程内会话持有:${sessionPath}`);
|
|
95
|
-
if (options.persistence.type === "file" && options.persistence.resume && !existsSync(sessionPath!))
|
|
96
|
-
throw new Error(`无法恢复子代理:会话文件不存在:${sessionPath}`);
|
|
97
|
-
if (sessionPath) SESSION_WRITERS.add(sessionPath);
|
|
98
|
-
|
|
99
|
-
let created: AgentSession;
|
|
100
|
-
try {
|
|
101
|
-
const loader = new DefaultResourceLoader({
|
|
102
|
-
cwd: options.cwd,
|
|
103
|
-
agentDir: this.environment.agentDir ?? getAgentDir(),
|
|
104
|
-
noContextFiles: !options.contextFiles,
|
|
105
|
-
extensionFactories: [BUILTIN_CODEMODE],
|
|
106
|
-
noExtensions: options.isolated,
|
|
107
|
-
noSkills: options.isolated,
|
|
108
|
-
noPromptTemplates: options.isolated,
|
|
109
|
-
...(options.systemPrompt.mode === "replace"
|
|
110
|
-
? { systemPrompt: options.systemPrompt.text }
|
|
111
|
-
: { appendSystemPrompt: [options.systemPrompt.text] }),
|
|
112
|
-
extensionsOverride: (base) => ({
|
|
113
|
-
...base,
|
|
114
|
-
extensions: base.extensions.filter((extension) =>
|
|
115
|
-
!matchesExtension(extension.path, options.excludeExtensions ?? [])),
|
|
116
|
-
}),
|
|
117
|
-
});
|
|
118
|
-
await withSubsessionRole(options.role, () => loader.reload());
|
|
119
|
-
if (loader.getExtensions().errors.length)
|
|
120
|
-
throw new Error(`子会话扩展加载失败:${JSON.stringify(loader.getExtensions().errors)}`);
|
|
121
|
-
const sessionManager = makeSessionManager(options.persistence, options.cwd);
|
|
122
|
-
const result = await createAgentSession({
|
|
123
|
-
cwd: options.cwd,
|
|
124
|
-
agentDir: this.environment.agentDir,
|
|
125
|
-
// 与模型解析同一份:不传时宿主会为每个子会话重读一次 auth.json 与 models.json。
|
|
126
|
-
modelRuntime: await this.modelRuntime(),
|
|
127
|
-
model: options.model,
|
|
128
|
-
thinkingLevel: options.thinking,
|
|
129
|
-
tools: options.tools,
|
|
130
|
-
...(options.customTools ? { customTools: options.customTools } : {}),
|
|
131
|
-
resourceLoader: loader,
|
|
132
|
-
sessionManager,
|
|
133
|
-
});
|
|
134
|
-
await result.session.bindExtensions({ mode: "print" });
|
|
135
|
-
created = result.session;
|
|
136
|
-
} catch (error) {
|
|
137
|
-
if (sessionPath) SESSION_WRITERS.delete(sessionPath);
|
|
138
|
-
throw error;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const key = sessionPath ?? `memory:${crypto.randomUUID()}`;
|
|
142
|
-
const held: HeldSession = { key, session: created, sessionPath };
|
|
143
|
-
this.held.set(key, held);
|
|
144
|
-
return {
|
|
145
|
-
session: created,
|
|
146
|
-
sessionPath,
|
|
147
|
-
prompt: (text) => created.prompt(text),
|
|
148
|
-
dispose: () => this.release(held),
|
|
149
|
-
};
|
|
150
|
-
}
|
|
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
|
-
|
|
166
|
-
has(sessionPath: string): boolean {
|
|
167
|
-
return this.held.has(sessionPath);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
getSession(sessionPath: string): AgentSession | undefined {
|
|
171
|
-
const held = this.held.get(sessionPath);
|
|
172
|
-
if (!held) return undefined;
|
|
173
|
-
this.clearTimer(held);
|
|
174
|
-
return held.session;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/** 空闲只由调用方判定:池不订阅会话事件自判空闲;超时后释放热会话,档案与 JSONL 保留。 */
|
|
178
|
-
markIdle(sessionPath: string): void {
|
|
179
|
-
const held = this.held.get(sessionPath);
|
|
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?.();
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
async dispose(sessionPath: string): Promise<boolean> {
|
|
187
|
-
const held = this.held.get(sessionPath);
|
|
188
|
-
if (!held) return false;
|
|
189
|
-
await this.release(held);
|
|
190
|
-
return true;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
async disposeAll(): Promise<void> {
|
|
194
|
-
await Promise.all([...this.held.values()].map((held) => this.release(held)));
|
|
195
|
-
}
|
|
196
|
-
|
|
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;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
private clearTimer(held: HeldSession): void {
|
|
216
|
-
if (held.timer) clearTimeout(held.timer);
|
|
217
|
-
held.timer = undefined;
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
export function preallocateWorkerSession(mainSessionPath: string, cwd: string): string {
|
|
222
|
-
const sessionPath = SessionManager.create(cwd, `${dirname(mainSessionPath)}/subagents`).getSessionFile();
|
|
223
|
-
if (!sessionPath) throw new Error("无法为子代理预分配 Pi session 路径");
|
|
224
|
-
return sessionPath;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
function makeSessionManager(persistence: SessionPersistence, cwd: string): SessionManager {
|
|
228
|
-
if (persistence.type === "memory") return SessionManager.inMemory(cwd);
|
|
229
|
-
return SessionManager.open(persistence.sessionPath, dirname(persistence.sessionPath), cwd);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function matchesExtension(path: string, exclusions: string[]): boolean {
|
|
233
|
-
return exclusions.some((excluded) => excluded === path || excluded === basename(path));
|
|
234
|
-
}
|