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/outbox.ts
DELETED
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 事件发件箱:落定事件先以 pending entry 写进主会话,再经 deliver.ts 投递,成功后写 ack;reload 重投差集。
|
|
3
|
-
* 同一节拍内的落定合并成一条消息。在飞子代理数也在这里算:指挥官在等的 working/reviewing 加上它们的事件还在队列
|
|
4
|
-
* 或投递中的子代理,所以归零只发生在事件交给指挥官之后。视图起的运行(见 RunOrigin)不算在飞,它的事件只告知不唤醒。
|
|
5
|
-
*/
|
|
6
|
-
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
7
|
-
import { deliver, inform, wrapEnvelope } from "../deliver.js";
|
|
8
|
-
import { roundFromEntry } from "../tools/round.js";
|
|
9
|
-
import { MASTER_EVENT_TYPE, withElapsed, type MasterEvent } from "./event-format.js";
|
|
10
|
-
import type { MasterRuntime } from "./runtime.js";
|
|
11
|
-
|
|
12
|
-
const PENDING_EVENT_TYPE = "firecode-master-pending-event";
|
|
13
|
-
const EVENT_ACK_TYPE = "firecode-master-event-ack";
|
|
14
|
-
const EVENT_RETRY_MS = 5_000;
|
|
15
|
-
/** 指挥官空闲时,从第一条结果入队起最多等这么久就唤醒,即使结果还在陆续到达。 */
|
|
16
|
-
const WAKE_MAX_MS = 6_000;
|
|
17
|
-
|
|
18
|
-
export interface PendingMasterEvent {
|
|
19
|
-
id: string;
|
|
20
|
-
content: string;
|
|
21
|
-
worker?: string;
|
|
22
|
-
/** 指挥官没在等这个结果(视图起的运行):只告知不唤醒,也不计入在飞。 */
|
|
23
|
-
inform?: true;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export class Outbox {
|
|
27
|
-
private readonly queued: PendingMasterEvent[] = [];
|
|
28
|
-
/** 已交给 deliver、尚未确认送达的事件:投递完成前对应子代理仍算在飞。 */
|
|
29
|
-
private readonly delivering = new Set<PendingMasterEvent>();
|
|
30
|
-
private flushTimer?: NodeJS.Timeout;
|
|
31
|
-
private retrying = false;
|
|
32
|
-
/** 空闲合并窗口里第一条事件入队的时刻。 */
|
|
33
|
-
private firstQueuedAt?: number;
|
|
34
|
-
private inFlightScheduled = false;
|
|
35
|
-
|
|
36
|
-
constructor(private readonly active: MasterRuntime) {}
|
|
37
|
-
|
|
38
|
-
/** 新产出的事件:追加耗时、持久化为 pending、排队投递。 */
|
|
39
|
-
enqueue(produced: MasterEvent, worker?: string): void {
|
|
40
|
-
if (this.active.closed) return;
|
|
41
|
-
const informOnly = worker !== undefined && this.active.live.get(worker)?.origin === "view";
|
|
42
|
-
const event: PendingMasterEvent = {
|
|
43
|
-
id: crypto.randomUUID(),
|
|
44
|
-
content: this.withElapsed(produced, worker),
|
|
45
|
-
...(worker ? { worker } : {}),
|
|
46
|
-
...(informOnly ? { inform: true as const } : {}),
|
|
47
|
-
};
|
|
48
|
-
try {
|
|
49
|
-
this.active.setup.pi.appendEntry(PENDING_EVENT_TYPE, event);
|
|
50
|
-
} catch (error) {
|
|
51
|
-
this.active.ctx.ui.notify(`子代理结果持久化失败,crash 时可能丢失:${String(error)}`, "warning");
|
|
52
|
-
}
|
|
53
|
-
this.push(event);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** reload 重投 pending 与 ack 的差集:正文已带落定当时的耗时,原样再投。 */
|
|
57
|
-
replayUnacked(ctx: ExtensionContext): void {
|
|
58
|
-
for (const event of unackedEvents(ctx)) this.push(event);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* 在飞 = working/reviewing + 已落定但结果事件还在队列或投递中(投递失败重试期间也算)。
|
|
63
|
-
* 落定先改 store、随后才入队事件:同一同步段内合并成一次计算,避免中间闪出一次归零。
|
|
64
|
-
*/
|
|
65
|
-
scheduleInFlight(): void {
|
|
66
|
-
if (this.inFlightScheduled) return;
|
|
67
|
-
this.inFlightScheduled = true;
|
|
68
|
-
queueMicrotask(() => {
|
|
69
|
-
this.inFlightScheduled = false;
|
|
70
|
-
if (this.active.closed) return;
|
|
71
|
-
const names = new Set<string>();
|
|
72
|
-
for (const worker of this.active.store.state.workers)
|
|
73
|
-
if ((worker.status === "working" || worker.status === "reviewing") && this.active.live.get(worker.name)?.origin !== "view")
|
|
74
|
-
names.add(worker.name);
|
|
75
|
-
for (const event of [...this.queued, ...this.delivering]) if (event.worker && !event.inform) names.add(event.worker);
|
|
76
|
-
this.active.setup.publishInFlight(names.size);
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
close(): void {
|
|
81
|
-
clearTimeout(this.flushTimer);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* 指挥官在跑:立即投(句缝送达,不打断)。指挥官空闲:每次唤醒都是一个完整回合,陆续到达的一批结果
|
|
86
|
-
* 等一个安静窗口合并成一次唤醒——最后一条入队后 wakeQuietMs 内没有新结果才唤醒,从第一条起最多等 WAKE_MAX_MS。
|
|
87
|
-
* 只告知的事件不开窗口、立即追加;窗口已开就随那一批唤醒送达。
|
|
88
|
-
*/
|
|
89
|
-
private push(event: PendingMasterEvent): void {
|
|
90
|
-
this.queued.push(event);
|
|
91
|
-
this.scheduleInFlight();
|
|
92
|
-
if (this.retrying) return;
|
|
93
|
-
const quiet = this.active.setup.wakeQuietMs;
|
|
94
|
-
if (!this.active.ctx.isIdle() || quiet <= 0) {
|
|
95
|
-
if (!this.flushTimer || this.firstQueuedAt !== undefined) this.schedule(0);
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
if (event.inform) {
|
|
99
|
-
if (this.firstQueuedAt === undefined) this.schedule(0);
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
this.firstQueuedAt ??= Date.now();
|
|
103
|
-
this.schedule(Math.min(quiet, Math.max(0, this.firstQueuedAt + WAKE_MAX_MS - Date.now())));
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
private schedule(delay: number): void {
|
|
107
|
-
clearTimeout(this.flushTimer);
|
|
108
|
-
this.flushTimer = setTimeout(() => this.flush(), delay);
|
|
109
|
-
this.flushTimer.unref?.();
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
private flush(): void {
|
|
113
|
-
const { active } = this;
|
|
114
|
-
if (active.closed) return;
|
|
115
|
-
this.flushTimer = undefined;
|
|
116
|
-
this.retrying = false;
|
|
117
|
-
this.firstQueuedAt = undefined;
|
|
118
|
-
if (!this.queued.length) return;
|
|
119
|
-
const batch = this.queued.splice(0);
|
|
120
|
-
for (const event of batch) this.delivering.add(event);
|
|
121
|
-
const send = batch.every((event) => event.inform) ? inform : deliver;
|
|
122
|
-
send(active.setup.pi, active.ctx, {
|
|
123
|
-
customType: MASTER_EVENT_TYPE,
|
|
124
|
-
content: batch.map((event) => wrapEnvelope("firecode_master_event", event.content)).join("\n\n"),
|
|
125
|
-
}).then(() => {
|
|
126
|
-
if (active.closed) return;
|
|
127
|
-
for (const event of batch) this.delivering.delete(event);
|
|
128
|
-
try {
|
|
129
|
-
active.setup.pi.appendEntry(EVENT_ACK_TYPE, { ids: batch.map((event) => event.id) });
|
|
130
|
-
} catch (error) {
|
|
131
|
-
active.ctx.ui.notify(`子代理结果确认写入失败,reload 后可能重复投递:${String(error)}`, "warning");
|
|
132
|
-
}
|
|
133
|
-
for (const event of batch) {
|
|
134
|
-
if (!event.worker) continue;
|
|
135
|
-
const worker = active.store.state.workers.find((candidate) => candidate.name === event.worker);
|
|
136
|
-
if (worker?.status === "idle" && worker.disposition !== "reminded")
|
|
137
|
-
active.store.dispatch({ type: "UPSERT_WORKER", worker: { ...worker, disposition: "pending" } });
|
|
138
|
-
}
|
|
139
|
-
this.scheduleInFlight();
|
|
140
|
-
}, (error) => {
|
|
141
|
-
if (active.closed) return;
|
|
142
|
-
for (const event of batch) this.delivering.delete(event);
|
|
143
|
-
this.queued.unshift(...batch);
|
|
144
|
-
active.ctx.ui.notify(`子代理结果投递失败,将自动重试:${String(error)}`, "warning");
|
|
145
|
-
this.retrying = true;
|
|
146
|
-
this.schedule(EVENT_RETRY_MS);
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* 本次运行耗时 = 这次运行(start/send/review 起点之后)在子代理会话里写下的轮记录之和:fallback 续跑或审查里的
|
|
152
|
-
* 修复回合各自成段,合起来才是这次运行。子代理排除了 FireCode(没有轮记录器)或 reload 后起点丢失时省略。
|
|
153
|
-
* 读得到:宿主在 prompt 结束前发 agent_settled,审查则先写终态 checkpoint 再释放占用,记录都早于这里的读取。
|
|
154
|
-
*/
|
|
155
|
-
private withElapsed(produced: MasterEvent, worker?: string): string {
|
|
156
|
-
const live = worker === undefined ? undefined : this.active.live.get(worker);
|
|
157
|
-
const since = live?.runStartedAt;
|
|
158
|
-
const branch = live?.observed?.session.sessionManager.getBranch() ?? [];
|
|
159
|
-
const rounds = since === undefined ? [] : branch.flatMap((entry) => {
|
|
160
|
-
const round = roundFromEntry(entry);
|
|
161
|
-
return round && round.at >= since ? [round] : [];
|
|
162
|
-
});
|
|
163
|
-
const sessionSince = this.active.setup.sessionSince();
|
|
164
|
-
return withElapsed(produced, {
|
|
165
|
-
...(rounds.length ? { run: rounds.reduce((total, round) => total + round.elapsed, 0) } : {}),
|
|
166
|
-
...(sessionSince === undefined ? {} : { task: Date.now() - sessionSince }),
|
|
167
|
-
});
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function unackedEvents(ctx: ExtensionContext): PendingMasterEvent[] {
|
|
172
|
-
const pending = new Map<string, PendingMasterEvent>();
|
|
173
|
-
const acked = new Set<string>();
|
|
174
|
-
for (const entry of ctx.sessionManager.getEntries()) {
|
|
175
|
-
if (entry.type !== "custom" || !entry.data || typeof entry.data !== "object") continue;
|
|
176
|
-
const data = entry.data as Record<string, unknown>;
|
|
177
|
-
if (entry.customType === PENDING_EVENT_TYPE && typeof data.id === "string" && typeof data.content === "string")
|
|
178
|
-
pending.set(data.id, {
|
|
179
|
-
id: data.id,
|
|
180
|
-
content: data.content,
|
|
181
|
-
...(typeof data.worker === "string" ? { worker: data.worker } : {}),
|
|
182
|
-
...(data.inform === true ? { inform: true as const } : {}),
|
|
183
|
-
});
|
|
184
|
-
if (entry.customType === EVENT_ACK_TYPE && Array.isArray(data.ids))
|
|
185
|
-
for (const id of data.ids) if (typeof id === "string") acked.add(id);
|
|
186
|
-
}
|
|
187
|
-
return [...pending.values()].filter((event) => !acked.has(event.id));
|
|
188
|
-
}
|
package/master/prompt.ts
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
2
|
-
import { dirname, join } from "node:path";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
|
-
|
|
5
|
-
const PROMPTS_DIR = join(dirname(fileURLToPath(import.meta.url)), "prompts");
|
|
6
|
-
|
|
7
|
-
export type MasterPromptKind = "master" | "worker";
|
|
8
|
-
|
|
9
|
-
export function readMasterPrompt(kind: MasterPromptKind): string {
|
|
10
|
-
let prompt: string;
|
|
11
|
-
try {
|
|
12
|
-
prompt = readFileSync(join(PROMPTS_DIR, `${kind}.zh.md`), "utf8");
|
|
13
|
-
} catch (error) {
|
|
14
|
-
throw new Error(`Master ${kind} prompt 读取失败:${error instanceof Error ? error.message : String(error)}`);
|
|
15
|
-
}
|
|
16
|
-
if (!prompt.trim()) throw new Error(`Master ${kind} prompt 为空`);
|
|
17
|
-
return prompt.trim();
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function assembleMasterPrompt(prompt: string, roster: string): string {
|
|
21
|
-
return `${prompt}\n\n角色表:${roster}。`;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function assembleWorkerPrompt(prompt: string, name: string): string {
|
|
25
|
-
return `<firecode_worker name="${name}">\n${prompt}\n</firecode_worker>`;
|
|
26
|
-
}
|
package/master/role.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
-
|
|
3
|
-
export type SubsessionRole = "worker" | "observer" | "reviewer" | "advisor";
|
|
4
|
-
|
|
5
|
-
// pi 的扩展加载器(jiti,moduleCache: false)在子会话 cwd 变化或宿主 reload 后会重新
|
|
6
|
-
// 求值整个模块图:模块级变量在两份拷贝间互不相通,角色标记必须挂在进程唯一的 globalThis
|
|
7
|
-
// 上,否则新拷贝读不到 spawn 侧设置的角色,watcher/master 会级联注册进子会话。
|
|
8
|
-
const ROLE_KEY = Symbol.for("firecode.subsession-role");
|
|
9
|
-
const shared = globalThis as Record<symbol, AsyncLocalStorage<SubsessionRole> | undefined>;
|
|
10
|
-
const ROLE = (shared[ROLE_KEY] ??= new AsyncLocalStorage<SubsessionRole>());
|
|
11
|
-
|
|
12
|
-
export function currentSubsessionRole(): SubsessionRole | undefined {
|
|
13
|
-
return ROLE.getStore();
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export function withSubsessionRole<T>(role: SubsessionRole, run: () => Promise<T>): Promise<T> {
|
|
17
|
-
return ROLE.run(role, run);
|
|
18
|
-
}
|
package/master/run.ts
DELETED
|
@@ -1,315 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 回合编排:打开 Worker 会话、跑一个回合、按终态落定(成功/失败/中断/fallback 续跑)、审查监视与中断续跑提醒。
|
|
3
|
-
* 所有 await 之后的写回经 runtime.current/commit 重读档案:kill 赢过迟到的异步写回。
|
|
4
|
-
*/
|
|
5
|
-
import type { AgentSession } from "@earendil-works/pi-coding-agent";
|
|
6
|
-
import type { MasterRole, ModelAtom } from "../config.js";
|
|
7
|
-
import { wrapEnvelope } from "../deliver.js";
|
|
8
|
-
import { clip, firstSentence, textOf } from "../format.js";
|
|
9
|
-
import { outcomeOfEntry, readReviewOutcome, reviewProgressOf, type ReviewOutcome } from "../review/outcome.js";
|
|
10
|
-
import { masterEvent, type MasterEvent } from "./event-format.js";
|
|
11
|
-
import { assembleWorkerPrompt } from "./prompt.js";
|
|
12
|
-
import type { MasterRuntime, WorkerLive } from "./runtime.js";
|
|
13
|
-
import type { WorkerRef } from "./state.js";
|
|
14
|
-
|
|
15
|
-
export const WORKER_TOOLS = ["read", "bash", "edit", "write"];
|
|
16
|
-
/** Worker 跟随指挥官是否启用 codemode;on/only 由 Worker 会话读到的同一份 settings 决定,不另传。 */
|
|
17
|
-
const CODEMODE_TOOL = "codemode";
|
|
18
|
-
const FAULT_SUMMARY_WIDTH = 80;
|
|
19
|
-
/** 算作“有输出”的子会话事件:模型 token 流与工具执行;活动列表据此判卡住。 */
|
|
20
|
-
const OUTPUT_EVENTS = new Set(["message_update", "tool_execution_start", "tool_execution_update", "tool_execution_end"]);
|
|
21
|
-
|
|
22
|
-
interface WorkerTerminal {
|
|
23
|
-
text: string;
|
|
24
|
-
stopReason?: string;
|
|
25
|
-
errorMessage?: string;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** 新建(start)或恢复(send/review 冷启动)Worker 会话;会话路径是档案身份的唯一事实源。 */
|
|
29
|
-
export async function spawnWorker(active: MasterRuntime, worker: WorkerRef, resume: boolean): Promise<AgentSession> {
|
|
30
|
-
const { pool, exclusions } = active.setup;
|
|
31
|
-
const model = await pool.resolveModel(worker.model);
|
|
32
|
-
active.assertOpen();
|
|
33
|
-
const spawned = await pool.spawn({
|
|
34
|
-
cwd: worker.cwd ?? process.cwd(),
|
|
35
|
-
role: "worker",
|
|
36
|
-
model,
|
|
37
|
-
thinking: worker.thinking,
|
|
38
|
-
tools: active.setup.pi.getActiveTools().includes(CODEMODE_TOOL) ? [...WORKER_TOOLS, CODEMODE_TOOL] : WORKER_TOOLS,
|
|
39
|
-
excludeExtensions: exclusions,
|
|
40
|
-
systemPrompt: { mode: "append", text: assembleWorkerPrompt(active.setup.workerPrompt(), worker.name) },
|
|
41
|
-
contextFiles: true,
|
|
42
|
-
persistence: { type: "file", sessionPath: worker.sessionPath, ...(resume ? { resume: true } : {}) },
|
|
43
|
-
});
|
|
44
|
-
if (active.closed) {
|
|
45
|
-
await spawned.dispose();
|
|
46
|
-
active.assertOpen();
|
|
47
|
-
}
|
|
48
|
-
return spawned.session;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export async function openWorkerSession(active: MasterRuntime, worker: WorkerRef): Promise<AgentSession> {
|
|
52
|
-
active.assertOpen();
|
|
53
|
-
return active.setup.pool.getSession(worker.sessionPath) ?? spawnWorker(active, worker, true);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** 投影当前动作、卡住判定与审查进度的唯一订阅。 */
|
|
57
|
-
export function observeWorker(active: MasterRuntime, worker: WorkerRef, session: AgentSession): void {
|
|
58
|
-
active.observe(worker, session, (live, event) => {
|
|
59
|
-
if (OUTPUT_EVENTS.has(event.type)) live.lastOutputAt = Date.now();
|
|
60
|
-
if (event.type === "tool_execution_start")
|
|
61
|
-
live.currentTools.set(event.toolCallId, { tool: event.toolName, args: event.args, startedAt: Date.now() });
|
|
62
|
-
if (event.type === "tool_execution_end") live.currentTools.delete(event.toolCallId);
|
|
63
|
-
if (event.type === "entry_appended") live.reviewProgress = reviewProgressOf(event.entry) ?? live.reviewProgress;
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/** resolve 即回合已在飞:宿主 prompt 的前置阶段仍报空闲,期间 abort 会被丢弃,interrupt 必须晚于 preflight。 */
|
|
68
|
-
export async function runWorker(active: MasterRuntime, worker: WorkerRef, session: AgentSession, prompt: string): Promise<void> {
|
|
69
|
-
// 启动回合的路径都经这里:准备期间被 kill 的不调模型、不留热会话。
|
|
70
|
-
active.current(worker);
|
|
71
|
-
observeWorker(active, worker, session);
|
|
72
|
-
const live = active.liveOf(worker.name);
|
|
73
|
-
const run = Symbol(worker.name);
|
|
74
|
-
live.run = run;
|
|
75
|
-
const mine = () => !active.closed && active.live.get(worker.name) === live && live.run === run;
|
|
76
|
-
let terminal: WorkerTerminal | undefined;
|
|
77
|
-
const unsubscribeTerminal = session.subscribe((event) => {
|
|
78
|
-
if (mine() && event.type === "agent_end") terminal = captureWorkerTerminal(event.messages);
|
|
79
|
-
});
|
|
80
|
-
const settled = async (error?: unknown) => {
|
|
81
|
-
unsubscribeTerminal();
|
|
82
|
-
if (!mine()) return;
|
|
83
|
-
const stranded = session.clearQueue().steering;
|
|
84
|
-
if (stranded.length) active.outbox.enqueue(masterEvent.stranded(worker.name, stranded), worker.name);
|
|
85
|
-
live.run = undefined;
|
|
86
|
-
if (live.interruptedRun === run) {
|
|
87
|
-
live.interruptedRun = undefined;
|
|
88
|
-
settleInterrupted(active, worker);
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
if (terminal?.stopReason === "error" && error === undefined) {
|
|
92
|
-
await resumeWithFallback(active, worker, session, terminal, faultSummary(terminal));
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
settleWorker(active, worker, terminal, error);
|
|
96
|
-
};
|
|
97
|
-
const inflight = Promise.withResolvers<void>();
|
|
98
|
-
void session.prompt(prompt, { preflightResult: () => inflight.resolve() })
|
|
99
|
-
.then(() => settled(), settled)
|
|
100
|
-
.finally(() => inflight.resolve());
|
|
101
|
-
await inflight.promise;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function settleInterrupted(active: MasterRuntime, identity: WorkerRef): void {
|
|
105
|
-
const current = active.find(identity);
|
|
106
|
-
if (!current) return;
|
|
107
|
-
const interrupted: WorkerRef = { ...current, status: "idle", interruptedAt: Date.now() };
|
|
108
|
-
active.store.dispatch({ type: "UPSERT_WORKER", worker: interrupted });
|
|
109
|
-
active.markIdle(interrupted, { kind: "interrupted" }, interrupted.interruptedAt);
|
|
110
|
-
active.outbox.enqueue(masterEvent.interrupted(identity.name, current.reviewNeeded === true, takeViewPrompts(active, identity)), identity.name);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* 会话重载打断的回合:恢复后满时限仍未续派就提醒指挥官;按档案里的中断时刻补算剩余时间。指挥官自己 interrupt 的不提醒。
|
|
115
|
-
* 只提醒、不代发“继续”:工作说明归指挥官,插件盲续会复活它已作废的任务。
|
|
116
|
-
*/
|
|
117
|
-
export function armInterruptReminder(active: MasterRuntime, worker: WorkerRef): void {
|
|
118
|
-
if (active.closed) return;
|
|
119
|
-
const live = active.liveOf(worker.name);
|
|
120
|
-
clearTimeout(live.interruptTimer);
|
|
121
|
-
const delay = Math.max(0, (worker.interruptedAt ?? Date.now()) + active.setup.interruptResumeMs - Date.now());
|
|
122
|
-
live.interruptTimer = setTimeout(() => {
|
|
123
|
-
if (active.closed || active.live.get(worker.name) !== live) return;
|
|
124
|
-
live.interruptTimer = undefined;
|
|
125
|
-
const current = active.find(worker);
|
|
126
|
-
if (!current?.interruptedAt || current.interruptedAt !== worker.interruptedAt) return;
|
|
127
|
-
active.store.dispatch({ type: "UPSERT_WORKER", worker: { ...current, disposition: "reminded" } });
|
|
128
|
-
active.outbox.enqueue(masterEvent.resumeReminder(worker.name), worker.name);
|
|
129
|
-
}, delay);
|
|
130
|
-
live.interruptTimer.unref?.();
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* 任何 error 终态都沿角色链切换,不按关键词分类故障:供应商措辞多变,关键词白名单实测一次未命中,分类也不改变决策。
|
|
135
|
-
* 瞬时限流与 Claude 令牌换发在 error 之前已由宿主重试和 claude-sub 自愈处理。由插件而非宿主做:宿主不知道角色与档案。
|
|
136
|
-
*/
|
|
137
|
-
async function resumeWithFallback(
|
|
138
|
-
active: MasterRuntime,
|
|
139
|
-
identity: WorkerRef,
|
|
140
|
-
session: AgentSession,
|
|
141
|
-
terminal: WorkerTerminal,
|
|
142
|
-
reason: string,
|
|
143
|
-
): Promise<void> {
|
|
144
|
-
const current = active.current(identity);
|
|
145
|
-
const configuredRole = active.setup.roster.find((entry) => entry.role === current.role);
|
|
146
|
-
if (!configuredRole)
|
|
147
|
-
return settleWorker(active, current, terminal, new Error(`${terminalFailure(terminal)}\n角色 ${current.role} 已不在角色表,无法 fallback`));
|
|
148
|
-
const fallback = nextFallback(configuredRole, current);
|
|
149
|
-
if (!fallback)
|
|
150
|
-
return settleWorker(active, current, terminal, new Error(`${terminalFailure(terminal)}\n角色 ${current.role} 的 fallback 链已用尽`));
|
|
151
|
-
try {
|
|
152
|
-
const model = await active.setup.pool.resolveModel(fallback.model);
|
|
153
|
-
active.assertOpen();
|
|
154
|
-
await session.setModel(model);
|
|
155
|
-
active.assertOpen();
|
|
156
|
-
session.setThinkingLevel(fallback.thinking);
|
|
157
|
-
const switched = active.commit(current, (latest) => ({ ...latest, ...fallback, status: "working" }));
|
|
158
|
-
const from = modelAtomText(current);
|
|
159
|
-
const to = modelAtomText(fallback);
|
|
160
|
-
active.outbox.enqueue(masterEvent.modelSwitched(current.name, from, to, reason), current.name);
|
|
161
|
-
await runWorker(active, switched, session, fallbackResumePrompt(from, to, reason));
|
|
162
|
-
} catch (error) {
|
|
163
|
-
const failure = `${terminalFailure(terminal)}\nfallback 切换失败:${error instanceof Error ? error.message : String(error)}`;
|
|
164
|
-
settleWorker(active, current, terminal, new Error(failure));
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function settleWorker(active: MasterRuntime, identity: WorkerRef, terminal: WorkerTerminal | undefined, error?: unknown): void {
|
|
169
|
-
const current = active.find(identity);
|
|
170
|
-
if (!current) return;
|
|
171
|
-
const failure = error instanceof Error ? error.message : error === undefined ? terminalFailure(terminal) : String(error);
|
|
172
|
-
const idle: WorkerRef = { ...current, status: "idle" };
|
|
173
|
-
active.store.dispatch({ type: "UPSERT_WORKER", worker: idle });
|
|
174
|
-
// 活动列表展开行显示结果首句(失败是错误首句),不是千篇一律的“已返回”。
|
|
175
|
-
active.markIdle(idle, failure ? { kind: "failed", note: firstSentence(failure) } : { kind: "done", note: firstSentence(terminal!.text) });
|
|
176
|
-
const obligation = current.reviewNeeded === true;
|
|
177
|
-
const event: MasterEvent = failure
|
|
178
|
-
? masterEvent.failed(identity.name, failure, obligation, takeViewPrompts(active, identity))
|
|
179
|
-
: masterEvent.returned(identity.name, terminal!.text, obligation, takeViewPrompts(active, identity));
|
|
180
|
-
active.outbox.enqueue(event, identity.name);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/** 起审:在原 Worker 会话里跑 /fire-review,终态经 review/outcome.ts 读取后落定并投递。 */
|
|
184
|
-
export function monitorAndSettleReview(active: MasterRuntime, target: WorkerRef, session: AgentSession, previousRunId: string | undefined): void {
|
|
185
|
-
const live = active.liveOf(target.name);
|
|
186
|
-
const reviewing = () => {
|
|
187
|
-
const current = active.closed || active.live.get(target.name) !== live ? undefined : active.find(target);
|
|
188
|
-
return current?.status === "reviewing" ? current : undefined;
|
|
189
|
-
};
|
|
190
|
-
void monitorReview(session, target.sessionPath, previousRunId).then(
|
|
191
|
-
(outcome) => {
|
|
192
|
-
const current = reviewing();
|
|
193
|
-
if (!current) return;
|
|
194
|
-
const { reviewNeeded: _needed, ...fulfilled } = current;
|
|
195
|
-
const idle: WorkerRef = { ...(outcome.status === "passed" || outcome.status === "stopped" ? fulfilled : current), status: "idle" };
|
|
196
|
-
active.store.dispatch({ type: "UPSERT_WORKER", worker: idle });
|
|
197
|
-
active.markIdle(idle, reviewOutcomeRow(outcome));
|
|
198
|
-
active.outbox.enqueue(masterEvent.review(target.name, outcome, latestAssistantText(session.messages)), target.name);
|
|
199
|
-
},
|
|
200
|
-
(error) => {
|
|
201
|
-
const current = reviewing();
|
|
202
|
-
if (!current) return;
|
|
203
|
-
const idle: WorkerRef = { ...current, status: "idle" };
|
|
204
|
-
active.store.dispatch({ type: "UPSERT_WORKER", worker: idle });
|
|
205
|
-
active.markIdle(idle, { kind: "failed", note: "审查未完成" });
|
|
206
|
-
active.outbox.enqueue(masterEvent.reviewIncomplete(target.name, String(error)), target.name);
|
|
207
|
-
},
|
|
208
|
-
);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/** 这次运行里视图说过的话交给落定事件后清空:下一次运行另算。 */
|
|
212
|
-
function takeViewPrompts(active: MasterRuntime, worker: WorkerRef): string[] {
|
|
213
|
-
const live = active.liveOf(worker.name);
|
|
214
|
-
const prompts = live.viewPrompts;
|
|
215
|
-
live.viewPrompts = [];
|
|
216
|
-
return prompts;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
export function reviewRunId(outcome: ReviewOutcome): string | undefined {
|
|
220
|
-
return "runId" in outcome ? outcome.runId : undefined;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function monitorReview(session: AgentSession, sessionPath: string, previousRunId: string | undefined): Promise<ReviewOutcome> {
|
|
224
|
-
return new Promise((resolve, reject) => {
|
|
225
|
-
let done = false;
|
|
226
|
-
let unsubscribe = () => {};
|
|
227
|
-
const fail = (error: unknown) => {
|
|
228
|
-
if (done) return;
|
|
229
|
-
done = true;
|
|
230
|
-
unsubscribe();
|
|
231
|
-
reject(error);
|
|
232
|
-
};
|
|
233
|
-
const finish = (outcome: ReviewOutcome) => {
|
|
234
|
-
if (done) return;
|
|
235
|
-
const runId = reviewRunId(outcome);
|
|
236
|
-
if (outcome.status !== "error"
|
|
237
|
-
&& (!runId || runId === previousRunId || outcome.status === "in_progress" || outcome.status === "none")) return;
|
|
238
|
-
done = true;
|
|
239
|
-
unsubscribe();
|
|
240
|
-
resolve(outcome);
|
|
241
|
-
};
|
|
242
|
-
// 只看刚追加的那条记录,不每条都重读整份 JSONL;回合结束时再读一次文件兜底。
|
|
243
|
-
unsubscribe = session.subscribe((event) => {
|
|
244
|
-
const outcome = event.type === "entry_appended" ? outcomeOfEntry(event.entry) : undefined;
|
|
245
|
-
if (outcome) finish(outcome);
|
|
246
|
-
});
|
|
247
|
-
void session.prompt("/fire-review").then(
|
|
248
|
-
() => {
|
|
249
|
-
const outcome = readReviewOutcome(sessionPath);
|
|
250
|
-
const runId = reviewRunId(outcome);
|
|
251
|
-
if (outcome.status === "error") return finish(outcome);
|
|
252
|
-
if (!runId || runId === previousRunId) return fail(new Error("fire-review 审查未启动"));
|
|
253
|
-
finish(outcome);
|
|
254
|
-
},
|
|
255
|
-
fail,
|
|
256
|
-
);
|
|
257
|
-
});
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
/** 审查落定在活动列表上的结局:只有通过算完成,停止与未完成都是要指挥官看的失败行。 */
|
|
261
|
-
function reviewOutcomeRow(outcome: ReviewOutcome): WorkerLive["outcome"] {
|
|
262
|
-
if (outcome.status === "passed") return { kind: "done", note: "审查通过" };
|
|
263
|
-
return { kind: "failed", note: outcome.status === "stopped" ? "审查停止" : "审查未完成" };
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
function captureWorkerTerminal(
|
|
267
|
-
messages: Array<{ role: string; content?: unknown; stopReason?: string; errorMessage?: string }>,
|
|
268
|
-
): WorkerTerminal | undefined {
|
|
269
|
-
const message = messages.findLast((candidate) => candidate.role === "assistant");
|
|
270
|
-
if (!message) return undefined;
|
|
271
|
-
return {
|
|
272
|
-
text: textOf(message.content),
|
|
273
|
-
...(message.stopReason ? { stopReason: message.stopReason } : {}),
|
|
274
|
-
...(message.errorMessage ? { errorMessage: message.errorMessage } : {}),
|
|
275
|
-
};
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
function terminalFailure(terminal: WorkerTerminal | undefined): string | undefined {
|
|
279
|
-
if (!terminal) return "回合结束但未产生 assistant 终态";
|
|
280
|
-
if (terminal.stopReason === "error") return terminal.errorMessage || "供应商返回未知错误";
|
|
281
|
-
if (terminal.stopReason === "aborted") return `回合意外中止:${terminal.errorMessage || "供应商未提供原因"}`;
|
|
282
|
-
if (!terminal.text) return "回合结束但未产生回复";
|
|
283
|
-
return undefined;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
function faultSummary(terminal: WorkerTerminal): string {
|
|
287
|
-
const message = terminal.errorMessage?.trim();
|
|
288
|
-
if (!message) return "供应商返回未知错误";
|
|
289
|
-
const [first = message] = message.split(/(?<=[.。!!??])\s|\n/u);
|
|
290
|
-
return clip(first.trim(), FAULT_SUMMARY_WIDTH);
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
function latestAssistantText(messages: Array<{ role: string; content?: unknown }>): string {
|
|
294
|
-
return textOf(messages.findLast((candidate) => candidate.role === "assistant")?.content);
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
function nextFallback(role: MasterRole, worker: WorkerRef): ModelAtom | undefined {
|
|
298
|
-
const chain: ModelAtom[] = [role, ...role.fallback];
|
|
299
|
-
let index = chain.findIndex((atom) => atom.model === worker.model && atom.thinking === worker.thinking);
|
|
300
|
-
if (index < 0) index = chain.findIndex((atom) => atom.model === worker.model);
|
|
301
|
-
return chain[index + 1];
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
export function modelAtomText(atom: Pick<ModelAtom, "model" | "thinking">): string {
|
|
305
|
-
return `${atom.model}/${atom.thinking}`;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
/** 首次续派前置的现场核对提示。 */
|
|
309
|
-
export function resumeCheckPrompt(): string {
|
|
310
|
-
return wrapEnvelope("firecode_master_event", "上次被外部中断,先核对 git status 与现场再继续,避免重复执行已经发生的副作用。");
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
function fallbackResumePrompt(from: string, to: string, reason: string): string {
|
|
314
|
-
return wrapEnvelope("firecode_master_event", `供应商故障,已切换 ${from}→${to}(${reason})。沿用当前会话与原工作说明,从中断处继续,不要重复已经完成的副作用。`);
|
|
315
|
-
}
|