pi-firecode 0.8.1 → 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 +17 -12
- 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/index.ts
CHANGED
|
@@ -1,280 +1,114 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
/**
|
|
2
|
+
* /fire-master 的注册入口:激活与停用、命令、两个工具与生命周期。
|
|
3
|
+
* 运行时事实在 runtime.ts,回合编排在 run.ts,七个动作在 actions.ts,发件箱在 outbox.ts,工具行在 list-view.ts。
|
|
4
|
+
*/
|
|
5
5
|
import { StringEnum, Type } from "@earendil-works/pi-ai";
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
type AgentSessionEvent,
|
|
12
|
-
type ExtensionAPI,
|
|
13
|
-
type ExtensionContext,
|
|
14
|
-
} from "@earendil-works/pi-coding-agent";
|
|
15
|
-
import { MASTER_ROLES, loadConfig, type MasterModelAtom, type MasterRole } from "../config.js";
|
|
16
|
-
import { deliver } from "../deliver.js";
|
|
17
|
-
import { formatDuration } from "../format.js";
|
|
18
|
-
import { readReviewOutcome, type ReviewOutcome } from "../review/outcome.js";
|
|
19
|
-
import { ToolLine, makeResultRenderer } from "../tools/line.js";
|
|
20
|
-
import type { Part } from "../tools/parts.js";
|
|
6
|
+
import { getAgentDir, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import { HERDR_WORKING_CHANNEL, HERDR_WORKING_LABEL, WORKERS_CHANNEL, type HerdrWorkingPayload, type WorkersPayload, watchBusy } from "../busy.js";
|
|
8
|
+
import { loadConfig, type MasterRole } from "../config.js";
|
|
9
|
+
import { ToolLine } from "../tools/line.js";
|
|
10
|
+
import { ACTION_HANDLERS, ACTIONS } from "./actions.js";
|
|
21
11
|
import { registerMasterEventRenderer } from "./event-card.js";
|
|
22
|
-
import {
|
|
23
|
-
import { assembleMasterPrompt, assembleWorkerPrompt, readMasterPrompt } from "./prompt.js";
|
|
24
|
-
import { InProcessSessionPool, preallocateWorkerSession } from "./spawn.js";
|
|
12
|
+
import { registerWorkerGuard } from "./guard.js";
|
|
25
13
|
import {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
type WorkerRef,
|
|
34
|
-
type WorkerStatus,
|
|
35
|
-
} from "./state.js";
|
|
14
|
+
compactWorker, currentWorkerAction, expandedWorkerList, listMeta, renderSubagentsResult, statusText, subagentsCallParts,
|
|
15
|
+
} from "./list-view.js";
|
|
16
|
+
import { assembleMasterPrompt, readMasterPrompt } from "./prompt.js";
|
|
17
|
+
import { armInterruptReminder, modelAtomText } from "./run.js";
|
|
18
|
+
import { MasterRuntime, type MasterSetup } from "./runtime.js";
|
|
19
|
+
import { InProcessSessionPool } from "./spawn.js";
|
|
20
|
+
import { loadMasterState, masterStatePath, recoverMasterState, THINKING_LEVELS, type MasterState } from "./state.js";
|
|
36
21
|
|
|
37
22
|
const MASTER_TOOL = "subagents";
|
|
38
23
|
const MASTER_LIST_TOOL = "subagents_list";
|
|
39
24
|
const MASTER_TOOLS = [MASTER_TOOL, MASTER_LIST_TOOL];
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
content: string;
|
|
48
|
-
worker?: string;
|
|
49
|
-
}
|
|
25
|
+
const INTERRUPT_RESUME_MS = 5 * 60_000;
|
|
26
|
+
/**
|
|
27
|
+
* 一批结果陆续返回时,最后一条后静默这么久才唤醒空闲的指挥官:合并唤醒,单条结果最多多等这一下。
|
|
28
|
+
* 这是业务语义的等待:“这一批还会不会马上再来一条”没有事件能回答。同批结果相邻间隔约 2~3 秒,而唤醒回合本身要数秒,
|
|
29
|
+
* 1.5 秒足以把紧挨着的并进一次唤醒,回合开跑后再到的走句缝。
|
|
30
|
+
*/
|
|
31
|
+
const WAKE_QUIET_MS = 1_500;
|
|
50
32
|
|
|
51
33
|
interface MasterDependencies {
|
|
52
|
-
resolveModel?: (id: string) => Promise<Model<any>>;
|
|
53
34
|
pool?: InProcessSessionPool;
|
|
54
35
|
interruptResumeMs?: number;
|
|
36
|
+
wakeQuietMs?: number;
|
|
55
37
|
}
|
|
56
38
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
interface ReviewProgress {
|
|
63
|
-
kind: "review";
|
|
64
|
-
round: number;
|
|
65
|
-
settled: number;
|
|
66
|
-
total: number;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
interface WorkerTerminal {
|
|
70
|
-
text: string;
|
|
71
|
-
stopReason?: string;
|
|
72
|
-
errorMessage?: string;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
interface ObservedSession {
|
|
76
|
-
session: AgentSession;
|
|
77
|
-
unsubscribe: () => void;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
interface MasterRuntime {
|
|
81
|
-
ctx: ExtensionContext;
|
|
82
|
-
store: MasterStore;
|
|
83
|
-
pool: InProcessSessionPool;
|
|
84
|
-
events: PendingMasterEvent[];
|
|
85
|
-
currentTools: Map<string, Map<string, CurrentTool>>;
|
|
86
|
-
idleSince: Map<string, number>;
|
|
87
|
-
reviewProgress: Map<string, ReviewProgress>;
|
|
88
|
-
observedSessions: Map<string, ObservedSession>;
|
|
89
|
-
flushTimer?: NodeJS.Timeout;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
export function registerMaster(
|
|
93
|
-
pi: ExtensionAPI,
|
|
94
|
-
dependencies: MasterDependencies = {},
|
|
95
|
-
worker = false,
|
|
96
|
-
): void {
|
|
97
|
-
if (worker) {
|
|
98
|
-
pi.on("tool_call", async (event, ctx) => {
|
|
99
|
-
if (!isToolCallEventType("edit", event) && !isToolCallEventType("write", event)) return;
|
|
100
|
-
const reason = await outsideCheckoutReason(event.input.path, ctx.cwd);
|
|
101
|
-
if (reason) return { block: true, reason };
|
|
102
|
-
});
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
let runtime: MasterRuntime | undefined;
|
|
106
|
-
const loaded = loadMasterConfiguration();
|
|
39
|
+
export function registerMaster(pi: ExtensionAPI, dependencies: MasterDependencies = {}, worker = false): void {
|
|
40
|
+
// Worker 会话里只注册 checkout 守卫,不注册命令、工具与生命周期。
|
|
41
|
+
if (worker) return registerWorkerGuard(pi);
|
|
42
|
+
const loaded = loadConfig().master;
|
|
107
43
|
const prompts = loadMasterPrompts();
|
|
108
44
|
const startupError = "error" in loaded ? loaded.error : "error" in prompts ? prompts.error : undefined;
|
|
109
|
-
const roster = "error" in loaded ? [] : loaded.roles;
|
|
110
|
-
const
|
|
111
|
-
const autoActivate = "error" in loaded ? false : loaded.autoActivate;
|
|
45
|
+
const roster = "error" in loaded ? [] : loaded.config.roles;
|
|
46
|
+
const autoActivate = "error" in loaded ? false : loaded.config.autoActivate;
|
|
112
47
|
const requirePrompts = () => {
|
|
113
48
|
if ("error" in prompts) throw new Error(prompts.error);
|
|
114
49
|
return prompts;
|
|
115
50
|
};
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
51
|
+
// 在飞子代理数的唯一发布者;herdr:working 只在 0↔正数跃迁时发布,active 按计数配对。
|
|
52
|
+
let publishedInFlight = 0;
|
|
53
|
+
const publishInFlight = (count: number, teardown = false) => {
|
|
54
|
+
if (count === publishedInFlight) return;
|
|
55
|
+
const wasBusy = publishedInFlight > 0;
|
|
56
|
+
publishedInFlight = count;
|
|
57
|
+
pi.events.emit(WORKERS_CHANNEL, { inFlight: count, ...(teardown ? { teardown: true as const } : {}) } satisfies WorkersPayload);
|
|
58
|
+
if (wasBusy !== count > 0)
|
|
59
|
+
pi.events.emit(HERDR_WORKING_CHANNEL, { active: count > 0, label: HERDR_WORKING_LABEL } satisfies HerdrWorkingPayload);
|
|
60
|
+
};
|
|
61
|
+
// 事件末尾的“当前任务”耗时是给指挥官的时间信号(Opus 5.5 据已用时间安排并行);起点只取 busy.ts。
|
|
62
|
+
let sessionSince: number | undefined;
|
|
63
|
+
watchBusy(pi, { onChange: (view) => { sessionSince = view.since; } });
|
|
64
|
+
const setup: MasterSetup = {
|
|
65
|
+
pi,
|
|
66
|
+
pool: dependencies.pool ?? new InProcessSessionPool(),
|
|
67
|
+
roster,
|
|
68
|
+
exclusions: "error" in loaded ? [] : loaded.config.workerExcludeExtensions,
|
|
69
|
+
workerPrompt: () => requirePrompts().worker,
|
|
70
|
+
reviewGate: reviewGateError(),
|
|
71
|
+
interruptResumeMs: dependencies.interruptResumeMs ?? INTERRUPT_RESUME_MS,
|
|
72
|
+
wakeQuietMs: dependencies.wakeQuietMs ?? WAKE_QUIET_MS,
|
|
73
|
+
publishInFlight,
|
|
74
|
+
sessionSince: () => sessionSince,
|
|
75
|
+
};
|
|
76
|
+
let runtime: MasterRuntime | undefined;
|
|
123
77
|
registerMasterEventRenderer(pi);
|
|
124
78
|
|
|
125
79
|
const setTools = (active: boolean) => {
|
|
126
80
|
const tools = pi.getActiveTools().filter((name) => !MASTER_TOOLS.includes(name));
|
|
127
81
|
pi.setActiveTools(active ? [...tools, ...MASTER_TOOLS] : tools);
|
|
128
82
|
};
|
|
129
|
-
const ownsRuntime = (active: MasterRuntime): boolean => runtime === active;
|
|
130
|
-
const requireRuntimeOwner = (active: MasterRuntime): void => {
|
|
131
|
-
if (!ownsRuntime(active)) throw new Error("Master 会话已替换,取消旧会话动作");
|
|
132
|
-
};
|
|
133
|
-
let spinFrame = 0;
|
|
134
|
-
let spinTimer: ReturnType<typeof setInterval> | undefined;
|
|
135
|
-
/** 计时器只在活动期存活:状态变化起停,全部落定即停,无常驻轮询。 */
|
|
136
|
-
const syncSpinner = (active: boolean) => {
|
|
137
|
-
if (active === (spinTimer !== undefined)) return;
|
|
138
|
-
if (!active) {
|
|
139
|
-
clearInterval(spinTimer);
|
|
140
|
-
spinTimer = undefined;
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
spinTimer = setInterval(() => {
|
|
144
|
-
spinFrame += 1;
|
|
145
|
-
renderStatus();
|
|
146
|
-
}, SPINNER_MS);
|
|
147
|
-
spinTimer.unref?.();
|
|
148
|
-
};
|
|
149
|
-
const renderStatus = () => {
|
|
150
|
-
if (!runtime) return;
|
|
151
|
-
const workers = runtime.store.state.workers;
|
|
152
|
-
syncSpinner(masterActive(workers));
|
|
153
|
-
runtime.ctx.ui.setStatus("master", masterStatusLine(workers, runtime.ctx.ui.theme, spinFrame));
|
|
154
|
-
};
|
|
155
83
|
const activate = (ctx: ExtensionContext, restored?: MasterState): MasterRuntime => {
|
|
156
84
|
if (startupError) throw new Error(startupError);
|
|
157
85
|
if (runtime) {
|
|
158
86
|
runtime.ctx = ctx;
|
|
159
87
|
return runtime;
|
|
160
88
|
}
|
|
161
|
-
|
|
162
|
-
const store = new MasterStore(masterStatePath(ctx.sessionManager.getSessionId()), restored, () => {
|
|
163
|
-
if (ownsRuntime(active)) renderStatus();
|
|
164
|
-
});
|
|
165
|
-
active = {
|
|
166
|
-
ctx,
|
|
167
|
-
store,
|
|
168
|
-
pool,
|
|
169
|
-
events: [],
|
|
170
|
-
currentTools: new Map(),
|
|
171
|
-
idleSince: new Map(),
|
|
172
|
-
reviewProgress: new Map(),
|
|
173
|
-
observedSessions: new Map(),
|
|
174
|
-
};
|
|
175
|
-
runtime = active;
|
|
89
|
+
const active = runtime = new MasterRuntime(setup, ctx, restored);
|
|
176
90
|
setTools(true);
|
|
177
|
-
if (store.discardedLegacyVersion !== undefined)
|
|
178
|
-
ctx.ui.notify(`旧版 v${store.discardedLegacyVersion} 子代理池已丢弃并从空池重建;旧运行时进程不会纳入新池,请手动清理`, "warning");
|
|
179
|
-
|
|
180
|
-
renderStatus();
|
|
91
|
+
if (active.store.discardedLegacyVersion !== undefined)
|
|
92
|
+
ctx.ui.notify(`旧版 v${active.store.discardedLegacyVersion} 子代理池已丢弃并从空池重建;旧运行时进程不会纳入新池,请手动清理`, "warning");
|
|
93
|
+
active.render();
|
|
181
94
|
return active;
|
|
182
95
|
};
|
|
183
|
-
|
|
96
|
+
/** 会话关闭先清空当前 runtime,再释放池、订阅与定时器:迟到任务不写状态、投递、UI 或持久化。 */
|
|
97
|
+
const deactivate = async () => {
|
|
184
98
|
const active = runtime;
|
|
185
99
|
runtime = undefined;
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
activeRuns.clear();
|
|
191
|
-
interruptedRuns.clear();
|
|
192
|
-
startingNames.clear();
|
|
193
|
-
transitioningNames.clear();
|
|
194
|
-
if (active?.flushTimer) clearTimeout(active.flushTimer);
|
|
195
|
-
for (const observed of active?.observedSessions.values() ?? []) observed.unsubscribe();
|
|
196
|
-
active?.ctx.ui.setStatus("master", undefined);
|
|
100
|
+
active?.close();
|
|
101
|
+
// 遗弃在飞子代理不是歇下:带 teardown 归零,busy.ts 只结束本段。
|
|
102
|
+
publishInFlight(0, true);
|
|
103
|
+
await setup.pool.disposeAll();
|
|
197
104
|
setTools(false);
|
|
198
105
|
};
|
|
199
|
-
|
|
200
|
-
if (!ownsRuntime(active)) return;
|
|
201
|
-
active.flushTimer = undefined;
|
|
202
|
-
if (!active.events.length) return;
|
|
203
|
-
const batch = active.events.splice(0);
|
|
204
|
-
const content = batch.map((event) => event.content).join("\n\n");
|
|
205
|
-
deliver(pi, active.ctx, {
|
|
206
|
-
customType: MASTER_EVENT_TYPE,
|
|
207
|
-
content: masterEventEnvelope(content),
|
|
208
|
-
details: masterEventDetails(batch.map((event) => event.content)),
|
|
209
|
-
}).then(() => {
|
|
210
|
-
if (!ownsRuntime(active)) return;
|
|
211
|
-
try {
|
|
212
|
-
pi.appendEntry(EVENT_ACK_TYPE, { ids: batch.map((event) => event.id) });
|
|
213
|
-
} catch (error) {
|
|
214
|
-
active.ctx.ui.notify(`子代理结果确认写入失败,reload 后可能重复投递:${String(error)}`, "warning");
|
|
215
|
-
}
|
|
216
|
-
for (const event of batch) {
|
|
217
|
-
if (!event.worker) continue;
|
|
218
|
-
const worker = active.store.state.workers.find((candidate) => candidate.name === event.worker);
|
|
219
|
-
if (worker?.status === "idle" && worker.disposition !== "reminded")
|
|
220
|
-
active.store.dispatch({ type: "UPSERT_WORKER", worker: { ...worker, disposition: "pending" } });
|
|
221
|
-
}
|
|
222
|
-
}, (error) => {
|
|
223
|
-
if (!ownsRuntime(active)) return;
|
|
224
|
-
active.events.unshift(...batch);
|
|
225
|
-
active.ctx.ui.notify(`子代理结果投递失败,将自动重试:${String(error)}`, "warning");
|
|
226
|
-
active.flushTimer = setTimeout(() => flushEvents(active), EVENT_RETRY_MS);
|
|
227
|
-
active.flushTimer.unref?.();
|
|
228
|
-
});
|
|
229
|
-
};
|
|
230
|
-
const enqueueEvent = (
|
|
231
|
-
active: MasterRuntime,
|
|
232
|
-
content: string,
|
|
233
|
-
worker?: string,
|
|
234
|
-
persist = true,
|
|
235
|
-
id = crypto.randomUUID(),
|
|
236
|
-
) => {
|
|
237
|
-
if (!ownsRuntime(active)) return;
|
|
238
|
-
const event: PendingMasterEvent = { id, content, ...(worker ? { worker } : {}) };
|
|
239
|
-
if (persist) {
|
|
240
|
-
try {
|
|
241
|
-
pi.appendEntry(PENDING_EVENT_TYPE, event);
|
|
242
|
-
} catch (error) {
|
|
243
|
-
active.ctx.ui.notify(`子代理结果持久化失败,crash 时可能丢失:${String(error)}`, "warning");
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
active.events.push(event);
|
|
247
|
-
if (!active.flushTimer) {
|
|
248
|
-
active.flushTimer = setTimeout(() => flushEvents(active), 0);
|
|
249
|
-
active.flushTimer.unref?.();
|
|
250
|
-
}
|
|
251
|
-
};
|
|
252
|
-
const clearInterruptTimer = (name: string) => {
|
|
253
|
-
const timer = interruptTimers.get(name);
|
|
254
|
-
if (timer) clearTimeout(timer);
|
|
255
|
-
interruptTimers.delete(name);
|
|
256
|
-
};
|
|
257
|
-
const armInterruptReminder = (active: MasterRuntime, worker: WorkerRef) => {
|
|
258
|
-
if (!ownsRuntime(active)) return;
|
|
259
|
-
clearInterruptTimer(worker.name);
|
|
260
|
-
const duration = dependencies.interruptResumeMs ?? 5 * 60_000;
|
|
261
|
-
const delay = Math.max(0, (worker.interruptedAt ?? Date.now()) + duration - Date.now());
|
|
262
|
-
const timer = setTimeout(() => {
|
|
263
|
-
if (!ownsRuntime(active)) return;
|
|
264
|
-
interruptTimers.delete(worker.name);
|
|
265
|
-
const current = active.store.state.workers.find((candidate) => candidate.name === worker.name);
|
|
266
|
-
if (!current?.interruptedAt || current.interruptedAt !== worker.interruptedAt) return;
|
|
267
|
-
active.store.dispatch({ type: "UPSERT_WORKER", worker: { ...current, disposition: "reminded" } });
|
|
268
|
-
enqueueEvent(active, `子代理 ${worker.name} 自动续跑提醒:上次回合被外部中断,请 send 续派或 kill 收口`, worker.name);
|
|
269
|
-
}, delay);
|
|
270
|
-
timer.unref?.();
|
|
271
|
-
interruptTimers.set(worker.name, timer);
|
|
272
|
-
};
|
|
106
|
+
/** reload 恢复:在飞状态收敛为 idle + interruptedAt,补挂续跑提醒,重投未确认事件。 */
|
|
273
107
|
const activateSession = (ctx: ExtensionContext): MasterRuntime => {
|
|
274
108
|
if (runtime) return activate(ctx);
|
|
275
109
|
let restored: MasterState | undefined;
|
|
276
110
|
try {
|
|
277
|
-
restored = loadMasterState(masterStatePath(ctx.sessionManager.getSessionId()));
|
|
111
|
+
restored = loadMasterState(masterStatePath(getAgentDir(), ctx.sessionManager.getSessionId()));
|
|
278
112
|
} catch {
|
|
279
113
|
return activate(ctx);
|
|
280
114
|
}
|
|
@@ -287,141 +121,9 @@ export function registerMaster(
|
|
|
287
121
|
if (worker.interruptedAt) armInterruptReminder(active, worker);
|
|
288
122
|
}
|
|
289
123
|
}
|
|
290
|
-
|
|
291
|
-
enqueueEvent(active, event.content, event.worker, false, event.id);
|
|
124
|
+
active.outbox.replayUnacked(ctx);
|
|
292
125
|
return active;
|
|
293
126
|
};
|
|
294
|
-
const currentWorker = (active: MasterRuntime, identity: WorkerRef) => {
|
|
295
|
-
requireRuntimeOwner(active);
|
|
296
|
-
const current = active.store.state.workers.find((worker) => worker.name === identity.name);
|
|
297
|
-
if (current?.sessionPath === identity.sessionPath) return current;
|
|
298
|
-
active.pool.dispose(identity.sessionPath);
|
|
299
|
-
throw new Error(`${identity.name} 已被 kill,取消本次动作`);
|
|
300
|
-
};
|
|
301
|
-
const openWorkerSession = async (active: MasterRuntime, worker: WorkerRef) => {
|
|
302
|
-
requireRuntimeOwner(active);
|
|
303
|
-
const hot = active.pool.getSession(worker.sessionPath);
|
|
304
|
-
if (hot) return hot;
|
|
305
|
-
const model = await (dependencies.resolveModel ?? resolveConfiguredModel)(worker.model);
|
|
306
|
-
requireRuntimeOwner(active);
|
|
307
|
-
const spawned = await active.pool.spawn({
|
|
308
|
-
cwd: worker.cwd ?? process.cwd(),
|
|
309
|
-
role: "worker",
|
|
310
|
-
model,
|
|
311
|
-
thinking: worker.thinking,
|
|
312
|
-
tools: WORKER_TOOLS,
|
|
313
|
-
excludeExtensions: exclusions,
|
|
314
|
-
systemPrompt: { mode: "append", text: assembleWorkerPrompt(requirePrompts().worker, worker.name) },
|
|
315
|
-
contextFiles: true,
|
|
316
|
-
persistence: { type: "file", sessionPath: worker.sessionPath, resume: true },
|
|
317
|
-
});
|
|
318
|
-
if (!ownsRuntime(active)) {
|
|
319
|
-
spawned.dispose();
|
|
320
|
-
requireRuntimeOwner(active);
|
|
321
|
-
}
|
|
322
|
-
return spawned.session;
|
|
323
|
-
};
|
|
324
|
-
const observeWorker = (active: MasterRuntime, sessionPath: string, session: AgentSession) => {
|
|
325
|
-
requireRuntimeOwner(active);
|
|
326
|
-
const previous = active.observedSessions.get(sessionPath);
|
|
327
|
-
if (previous?.session === session) return;
|
|
328
|
-
previous?.unsubscribe();
|
|
329
|
-
const unsubscribe = session.subscribe((event: AgentSessionEvent) => {
|
|
330
|
-
if (!ownsRuntime(active)) return;
|
|
331
|
-
if (event.type === "tool_execution_start") {
|
|
332
|
-
const tools = active.currentTools.get(sessionPath) ?? new Map<string, CurrentTool>();
|
|
333
|
-
tools.set(event.toolCallId, { tool: event.toolName, startedAt: Date.now() });
|
|
334
|
-
active.currentTools.set(sessionPath, tools);
|
|
335
|
-
}
|
|
336
|
-
if (event.type === "tool_execution_end") {
|
|
337
|
-
const tools = active.currentTools.get(sessionPath);
|
|
338
|
-
tools?.delete(event.toolCallId);
|
|
339
|
-
if (!tools?.size) active.currentTools.delete(sessionPath);
|
|
340
|
-
}
|
|
341
|
-
if (event.type === "entry_appended") {
|
|
342
|
-
const progress = reviewProgressFromEntry(event.entry);
|
|
343
|
-
if (progress) active.reviewProgress.set(sessionPath, progress);
|
|
344
|
-
}
|
|
345
|
-
});
|
|
346
|
-
active.observedSessions.set(sessionPath, { session, unsubscribe });
|
|
347
|
-
};
|
|
348
|
-
const runWorker = (active: MasterRuntime, worker: WorkerRef, session: Awaited<ReturnType<typeof openWorkerSession>>, prompt: string) => {
|
|
349
|
-
requireRuntimeOwner(active);
|
|
350
|
-
observeWorker(active, worker.sessionPath, session);
|
|
351
|
-
const run = Symbol(worker.name);
|
|
352
|
-
let terminal: WorkerTerminal | undefined;
|
|
353
|
-
activeRuns.set(worker.sessionPath, run);
|
|
354
|
-
const unsubscribeTerminal = session.subscribe((event) => {
|
|
355
|
-
if (!ownsRuntime(active) || activeRuns.get(worker.sessionPath) !== run || event.type !== "agent_end") return;
|
|
356
|
-
terminal = captureWorkerTerminal(event.messages);
|
|
357
|
-
});
|
|
358
|
-
const settled = async (error?: unknown) => {
|
|
359
|
-
unsubscribeTerminal();
|
|
360
|
-
if (!ownsRuntime(active) || activeRuns.get(worker.sessionPath) !== run) return;
|
|
361
|
-
activeRuns.delete(worker.sessionPath);
|
|
362
|
-
if (interruptedRuns.get(worker.sessionPath) === run) {
|
|
363
|
-
interruptedRuns.delete(worker.sessionPath);
|
|
364
|
-
const current = active.store.state.workers.find((candidate) => candidate.name === worker.name);
|
|
365
|
-
if (!current || current.sessionPath !== worker.sessionPath) return;
|
|
366
|
-
const interrupted: WorkerRef = { ...current, status: "idle", interruptedAt: Date.now() };
|
|
367
|
-
active.store.dispatch({ type: "UPSERT_WORKER", worker: interrupted });
|
|
368
|
-
active.currentTools.delete(worker.sessionPath);
|
|
369
|
-
active.idleSince.set(worker.sessionPath, Date.now());
|
|
370
|
-
enqueueEvent(active, `子代理 ${worker.name} 已中断,会话与审查义务均已保留`, worker.name);
|
|
371
|
-
armInterruptReminder(active, interrupted);
|
|
372
|
-
return;
|
|
373
|
-
}
|
|
374
|
-
const fault = providerFaultReason(terminal);
|
|
375
|
-
if (fault && error === undefined) {
|
|
376
|
-
await resumeWithFallback(active, worker, session, terminal!, fault);
|
|
377
|
-
return;
|
|
378
|
-
}
|
|
379
|
-
const content = settleWorker(active, worker, terminal, error);
|
|
380
|
-
if (content) enqueueEvent(active, content, worker.name);
|
|
381
|
-
};
|
|
382
|
-
void session.prompt(prompt).then(() => void settled(), (error) => void settled(error));
|
|
383
|
-
};
|
|
384
|
-
const resumeWithFallback = async (
|
|
385
|
-
active: MasterRuntime,
|
|
386
|
-
identity: WorkerRef,
|
|
387
|
-
session: Awaited<ReturnType<typeof openWorkerSession>>,
|
|
388
|
-
terminal: WorkerTerminal,
|
|
389
|
-
reason: string,
|
|
390
|
-
) => {
|
|
391
|
-
const current = currentWorker(active, identity);
|
|
392
|
-
const configuredRole = roster.find((entry) => entry.role === current.role);
|
|
393
|
-
if (!configuredRole) {
|
|
394
|
-
const failure = `${terminalFailure(terminal)}\n角色 ${current.role} 已不在角色表,无法 fallback`;
|
|
395
|
-
const content = settleWorker(active, current, terminal, new Error(failure));
|
|
396
|
-
if (content) enqueueEvent(active, content, current.name);
|
|
397
|
-
return;
|
|
398
|
-
}
|
|
399
|
-
const fallback = nextFallback(configuredRole, current);
|
|
400
|
-
if (!fallback) {
|
|
401
|
-
const failure = `${terminalFailure(terminal)}\n角色 ${current.role} 的 fallback 链已用尽`;
|
|
402
|
-
const content = settleWorker(active, current, terminal, new Error(failure));
|
|
403
|
-
if (content) enqueueEvent(active, content, current.name);
|
|
404
|
-
return;
|
|
405
|
-
}
|
|
406
|
-
try {
|
|
407
|
-
const model = await (dependencies.resolveModel ?? resolveConfiguredModel)(fallback.model);
|
|
408
|
-
requireRuntimeOwner(active);
|
|
409
|
-
await session.setModel(model);
|
|
410
|
-
requireRuntimeOwner(active);
|
|
411
|
-
session.setThinkingLevel(fallback.thinking);
|
|
412
|
-
const latest = currentWorker(active, current);
|
|
413
|
-
const switched: WorkerRef = { ...latest, ...fallback, status: "working" };
|
|
414
|
-
active.store.dispatch({ type: "UPSERT_WORKER", worker: switched });
|
|
415
|
-
const from = modelAtomText(current);
|
|
416
|
-
const to = modelAtomText(fallback);
|
|
417
|
-
enqueueEvent(active, `子代理 ${current.name} 已切换 ${from}→${to}(${reason}),正在同一会话自动续跑`, current.name);
|
|
418
|
-
runWorker(active, switched, session, fallbackResumePrompt(from, to, reason));
|
|
419
|
-
} catch (error) {
|
|
420
|
-
const failure = `${terminalFailure(terminal)}\nfallback 切换失败:${error instanceof Error ? error.message : String(error)}`;
|
|
421
|
-
const content = settleWorker(active, current, terminal, new Error(failure));
|
|
422
|
-
if (content) enqueueEvent(active, content, current.name);
|
|
423
|
-
}
|
|
424
|
-
};
|
|
425
127
|
|
|
426
128
|
pi.registerCommand("fire-master", {
|
|
427
129
|
description: "翻转当前会话的指挥官模式;status 查看状态",
|
|
@@ -436,7 +138,7 @@ export function registerMaster(
|
|
|
436
138
|
return;
|
|
437
139
|
}
|
|
438
140
|
if (runtime) {
|
|
439
|
-
deactivate();
|
|
141
|
+
await deactivate();
|
|
440
142
|
ctx.ui.notify("指挥官模式已关闭", "info");
|
|
441
143
|
return;
|
|
442
144
|
}
|
|
@@ -449,6 +151,10 @@ export function registerMaster(
|
|
|
449
151
|
},
|
|
450
152
|
});
|
|
451
153
|
|
|
154
|
+
pi.on("input", (event) => {
|
|
155
|
+
if (event.source !== "extension") runtime?.collapseList();
|
|
156
|
+
});
|
|
157
|
+
|
|
452
158
|
pi.on("before_agent_start", async (event) => {
|
|
453
159
|
if (!runtime || !pi.getActiveTools().includes(MASTER_TOOL)) return;
|
|
454
160
|
return {
|
|
@@ -478,10 +184,7 @@ export function registerMaster(
|
|
|
478
184
|
return {
|
|
479
185
|
content: [{ type: "text" as const, text: JSON.stringify({ workers }) }],
|
|
480
186
|
details: {
|
|
481
|
-
workers: workers.map((worker) => ({
|
|
482
|
-
...worker,
|
|
483
|
-
currentAction: currentWorkerAction(active, worker),
|
|
484
|
-
})),
|
|
187
|
+
workers: workers.map((worker) => ({ ...worker, currentAction: currentWorkerAction(worker, active.live.get(worker.name)) })),
|
|
485
188
|
},
|
|
486
189
|
};
|
|
487
190
|
},
|
|
@@ -496,240 +199,26 @@ export function registerMaster(
|
|
|
496
199
|
new ToolLine({ label: "子代理", value: subagentsCallParts(args as Record<string, unknown>), clip: "end", theme, ctx }),
|
|
497
200
|
renderResult: renderSubagentsResult,
|
|
498
201
|
parameters: Type.Object({
|
|
499
|
-
action: StringEnum(
|
|
500
|
-
description: "七动作之一;等待状态变化,不要用 sleep 轮询。",
|
|
501
|
-
}),
|
|
202
|
+
action: StringEnum(ACTIONS, { description: "七动作之一;等待状态变化,不要用 sleep 轮询。" }),
|
|
502
203
|
worker: Type.String({ description: "start 起简短任务名;其余动作填目标 Worker。" }),
|
|
503
204
|
prompt: Type.Optional(Type.String({ description: "start/send 必填自包含任务说明,包括交付物、限制与验证要求。" })),
|
|
504
|
-
|
|
205
|
+
// 角色词只来自角色表,代码不持有固定词表;代价是角色名拼错无法在加载时报出。
|
|
206
|
+
role: Type.Optional(StringEnum(roster.map((entry) => entry.role), { description: "start 必填角色表中的角色;send 可选,传入时切换角色,省略则沿用。" })),
|
|
505
207
|
thinking: Type.Optional(StringEnum(THINKING_LEVELS, { description: "可选思考档覆盖;省略时使用角色原子档或当前档。" })),
|
|
506
|
-
cwd: Type.Optional(Type.String({ description: "
|
|
208
|
+
cwd: Type.Optional(Type.String({ description: "Worker 工作目录的绝对路径;start 默认当前目录,send 给空闲 Worker 换检出时带上(同一会话重开)。" })),
|
|
507
209
|
review: Type.Optional(Type.Boolean({ description: "按审查纪律为 start/send 记录义务;true 不自动开审。" })),
|
|
508
210
|
}),
|
|
509
211
|
async execute(_id, params: Record<string, unknown>, _signal, _update, ctx) {
|
|
510
212
|
const active = runtime;
|
|
511
213
|
if (!active) throw new Error("subagents 只在 Master 中可用");
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
activeRuns.delete(target.sessionPath);
|
|
516
|
-
interruptedRuns.delete(target.sessionPath);
|
|
517
|
-
active.currentTools.delete(target.sessionPath);
|
|
518
|
-
active.idleSince.delete(target.sessionPath);
|
|
519
|
-
active.reviewProgress.delete(target.sessionPath);
|
|
520
|
-
active.observedSessions.get(target.sessionPath)?.unsubscribe();
|
|
521
|
-
active.observedSessions.delete(target.sessionPath);
|
|
522
|
-
active.pool.dispose(target.sessionPath);
|
|
523
|
-
active.store.dispatch({ type: "REMOVE_WORKER", name: target.name });
|
|
524
|
-
return toolResult({ killed: true });
|
|
525
|
-
}
|
|
526
|
-
if (params.action === "tail") {
|
|
527
|
-
const target = requireWorker(active.store.state, requiredString(params.worker, "worker"));
|
|
528
|
-
return { content: [{ type: "text" as const, text: await readWorkerTrace(target) }] };
|
|
529
|
-
}
|
|
530
|
-
if (params.action === "ack") {
|
|
531
|
-
const target = requireWorker(active.store.state, requiredString(params.worker, "worker"));
|
|
532
|
-
if (target.reviewNeeded) throw new Error(`${target.name} 此票有审查义务,完成 review 后才能 ack`);
|
|
533
|
-
if (target.status !== "idle") throw new Error(`${target.name} 正在 ${target.status},不能 ack`);
|
|
534
|
-
if (target.disposition) {
|
|
535
|
-
const { disposition: _disposition, ...rest } = target;
|
|
536
|
-
active.store.dispatch({ type: "UPSERT_WORKER", worker: rest });
|
|
537
|
-
}
|
|
538
|
-
return toolResult({ acked: true });
|
|
539
|
-
}
|
|
540
|
-
if (params.action === "review") {
|
|
541
|
-
if (reviewGate) throw new Error(reviewGate);
|
|
542
|
-
const target = requireWorker(active.store.state, requiredString(params.worker, "worker"));
|
|
543
|
-
if (target.status !== "idle" || transitioningNames.has(target.name))
|
|
544
|
-
throw new Error(`${target.name} 正在处理其他动作,不能 review`);
|
|
545
|
-
transitioningNames.add(target.name);
|
|
546
|
-
try {
|
|
547
|
-
const session = await openWorkerSession(active, target);
|
|
548
|
-
await session.waitForIdle();
|
|
549
|
-
requireRuntimeOwner(active);
|
|
550
|
-
observeWorker(active, target.sessionPath, session);
|
|
551
|
-
const current = currentWorker(active, target);
|
|
552
|
-
const previousRunId = reviewRunId(readReviewOutcome(target.sessionPath));
|
|
553
|
-
const { disposition: _disposition, interruptedAt: _interruptedAt, ...rest } = current;
|
|
554
|
-
clearInterruptTimer(target.name);
|
|
555
|
-
const reviewing: WorkerRef = { ...rest, status: "reviewing" };
|
|
556
|
-
active.idleSince.delete(target.sessionPath);
|
|
557
|
-
active.store.dispatch({ type: "UPSERT_WORKER", worker: reviewing });
|
|
558
|
-
void monitorReview(session, target.sessionPath, previousRunId).then(
|
|
559
|
-
(outcome) => {
|
|
560
|
-
if (!ownsRuntime(active)) return;
|
|
561
|
-
const current = active.store.state.workers.find((worker) => worker.name === target.name);
|
|
562
|
-
if (!current || current.sessionPath !== target.sessionPath || current.status !== "reviewing") return;
|
|
563
|
-
const { reviewNeeded: _needed, ...fulfilled } = current;
|
|
564
|
-
const worker = outcome.status === "passed" || outcome.status === "stopped"
|
|
565
|
-
? fulfilled
|
|
566
|
-
: current;
|
|
567
|
-
active.store.dispatch({ type: "UPSERT_WORKER", worker: { ...worker, status: "idle" } });
|
|
568
|
-
active.reviewProgress.delete(target.sessionPath);
|
|
569
|
-
active.idleSince.set(target.sessionPath, Date.now());
|
|
570
|
-
active.pool.markIdle(target.sessionPath);
|
|
571
|
-
enqueueEvent(active, reviewOutcomeText(target.name, outcome, session.messages), target.name);
|
|
572
|
-
},
|
|
573
|
-
(error) => {
|
|
574
|
-
if (!ownsRuntime(active)) return;
|
|
575
|
-
const current = active.store.state.workers.find((worker) => worker.name === target.name);
|
|
576
|
-
if (!current || current.status !== "reviewing") return;
|
|
577
|
-
active.store.dispatch({ type: "UPSERT_WORKER", worker: { ...current, status: "idle" } });
|
|
578
|
-
active.reviewProgress.delete(target.sessionPath);
|
|
579
|
-
active.idleSince.set(target.sessionPath, Date.now());
|
|
580
|
-
active.pool.markIdle(target.sessionPath);
|
|
581
|
-
enqueueEvent(active, `子代理 ${target.name} 审查未完成:${String(error)}`, target.name);
|
|
582
|
-
},
|
|
583
|
-
);
|
|
584
|
-
return toolResult({ reviewing: true });
|
|
585
|
-
} finally {
|
|
586
|
-
if (ownsRuntime(active)) transitioningNames.delete(target.name);
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
if (params.action === "interrupt") {
|
|
590
|
-
const target = requireWorker(active.store.state, requiredString(params.worker, "worker"));
|
|
591
|
-
if (target.status !== "working") throw new Error(`${target.name} 当前是 ${target.status},不能 interrupt`);
|
|
592
|
-
const session = active.pool.getSession(target.sessionPath);
|
|
593
|
-
if (!session) throw new Error(`${target.name} 的进程内会话已释放,无法 interrupt`);
|
|
594
|
-
const run = activeRuns.get(target.sessionPath);
|
|
595
|
-
if (!run) throw new Error(`${target.name} 当前没有可中断的回合`);
|
|
596
|
-
interruptedRuns.set(target.sessionPath, run);
|
|
597
|
-
try {
|
|
598
|
-
await session.abort();
|
|
599
|
-
requireRuntimeOwner(active);
|
|
600
|
-
return toolResult({ interrupted: true });
|
|
601
|
-
} catch (error) {
|
|
602
|
-
if (ownsRuntime(active) && interruptedRuns.get(target.sessionPath) === run) interruptedRuns.delete(target.sessionPath);
|
|
603
|
-
throw error;
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
if (params.action === "send") {
|
|
607
|
-
if (params.review === true && reviewGate) throw new Error(reviewGate);
|
|
608
|
-
const target = requireWorker(active.store.state, requiredString(params.worker, "worker"));
|
|
609
|
-
if (target.status !== "idle" || transitioningNames.has(target.name))
|
|
610
|
-
throw new Error(`${target.name} 正在处理其他动作;急件先 interrupt 再 send`);
|
|
611
|
-
const requestedRole = optionalString(params.role);
|
|
612
|
-
const selection = requestedRole ? resolveRole(roster, requestedRole) : undefined;
|
|
613
|
-
const requestedThinking = optionalString(params.thinking);
|
|
614
|
-
if (requestedThinking && !THINKING_LEVELS.includes(requestedThinking as WorkerRef["thinking"]))
|
|
615
|
-
throw new Error(`thinking 值无效:${requestedThinking}`);
|
|
616
|
-
const prompt = requiredString(params.prompt, "prompt");
|
|
617
|
-
validateDelegationText(prompt);
|
|
618
|
-
transitioningNames.add(target.name);
|
|
619
|
-
try {
|
|
620
|
-
const nextModel = selection
|
|
621
|
-
? await (dependencies.resolveModel ?? resolveConfiguredModel)(selection.model)
|
|
622
|
-
: undefined;
|
|
623
|
-
requireRuntimeOwner(active);
|
|
624
|
-
const session = await openWorkerSession(active, target);
|
|
625
|
-
await session.waitForIdle();
|
|
626
|
-
requireRuntimeOwner(active);
|
|
627
|
-
let role = target.role;
|
|
628
|
-
let model = target.model;
|
|
629
|
-
let thinking = target.thinking;
|
|
630
|
-
if (selection && nextModel) {
|
|
631
|
-
await session.setModel(nextModel);
|
|
632
|
-
requireRuntimeOwner(active);
|
|
633
|
-
role = selection.role;
|
|
634
|
-
model = selection.model;
|
|
635
|
-
thinking = selection.thinking;
|
|
636
|
-
}
|
|
637
|
-
if (selection || requestedThinking) {
|
|
638
|
-
thinking = requestedThinking as WorkerRef["thinking"] | undefined ?? thinking;
|
|
639
|
-
session.setThinkingLevel(thinking);
|
|
640
|
-
}
|
|
641
|
-
const current = currentWorker(active, target);
|
|
642
|
-
const { disposition: _disposition, interruptedAt, ...rest } = current;
|
|
643
|
-
const activeWorker: WorkerRef = {
|
|
644
|
-
...rest,
|
|
645
|
-
role,
|
|
646
|
-
model,
|
|
647
|
-
thinking,
|
|
648
|
-
status: "working",
|
|
649
|
-
...(params.review === true || target.reviewNeeded ? { reviewNeeded: true } : {}),
|
|
650
|
-
};
|
|
651
|
-
clearInterruptTimer(target.name);
|
|
652
|
-
active.idleSince.delete(target.sessionPath);
|
|
653
|
-
active.store.dispatch({ type: "UPSERT_WORKER", worker: activeWorker });
|
|
654
|
-
const text = interruptedAt ? `${resumeCheckPrompt()}\n\n${prompt}` : prompt;
|
|
655
|
-
runWorker(active, activeWorker, session, text);
|
|
656
|
-
return toolResult({ sent: true });
|
|
657
|
-
} finally {
|
|
658
|
-
if (ownsRuntime(active)) transitioningNames.delete(target.name);
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
if (params.action !== "start") throw new Error(`未知 subagents action:${String(params.action)}`);
|
|
662
|
-
if (params.review === true && reviewGate) throw new Error(reviewGate);
|
|
663
|
-
if (typeof params.worker !== "string" || !params.worker.trim())
|
|
664
|
-
throw new Error("start 需要 worker:给子代理起个简短任务名(如 fix-auth、repo-scan)");
|
|
665
|
-
const name = params.worker.trim();
|
|
666
|
-
validateWorkerName(name);
|
|
667
|
-
if (active.store.state.workers.some((worker) => worker.name === name) || startingNames.has(name))
|
|
668
|
-
throw new Error(`子代理已存在:${name}`);
|
|
669
|
-
const inFlight = active.store.state.workers.filter((worker) => worker.status === "working" || worker.status === "reviewing");
|
|
670
|
-
if (inFlight.length + startingNames.size >= 15)
|
|
671
|
-
throw new Error(`Worker 并发上限 15,当前在飞:${[...inFlight.map((worker) => worker.name), ...startingNames].join("、")}`);
|
|
672
|
-
const prompt = requiredString(params.prompt, "prompt");
|
|
673
|
-
validateDelegationText(prompt);
|
|
674
|
-
const selectedRole = resolveRole(roster, requiredString(params.role, "start 必须指定 role"));
|
|
675
|
-
const requestedThinking = optionalString(params.thinking);
|
|
676
|
-
if (requestedThinking && !THINKING_LEVELS.includes(requestedThinking as WorkerRef["thinking"]))
|
|
677
|
-
throw new Error(`thinking 值无效:${requestedThinking}`);
|
|
678
|
-
const selection = {
|
|
679
|
-
...selectedRole,
|
|
680
|
-
thinking: requestedThinking as WorkerRef["thinking"] | undefined ?? selectedRole.thinking,
|
|
681
|
-
};
|
|
682
|
-
startingNames.add(name);
|
|
683
|
-
try {
|
|
684
|
-
const cwd = await resolveWorkerCwd(optionalString(params.cwd) ?? ctx.cwd);
|
|
685
|
-
requireRuntimeOwner(active);
|
|
686
|
-
const mainSessionPath = ctx.sessionManager.getSessionFile?.();
|
|
687
|
-
if (!mainSessionPath) throw new Error("主会话尚未落盘,无法创建子代理会话目录");
|
|
688
|
-
const sessionPath = preallocateWorkerSession(mainSessionPath, cwd);
|
|
689
|
-
const worker: WorkerRef = {
|
|
690
|
-
name,
|
|
691
|
-
role: selection.role,
|
|
692
|
-
model: selection.model,
|
|
693
|
-
thinking: selection.thinking,
|
|
694
|
-
status: "working",
|
|
695
|
-
sessionPath,
|
|
696
|
-
cwd,
|
|
697
|
-
...(params.review === true ? { reviewNeeded: true } : {}),
|
|
698
|
-
};
|
|
699
|
-
active.store.dispatch({ type: "UPSERT_WORKER", worker });
|
|
700
|
-
startingNames.delete(name);
|
|
701
|
-
try {
|
|
702
|
-
const model = await (dependencies.resolveModel ?? resolveConfiguredModel)(selection.model);
|
|
703
|
-
requireRuntimeOwner(active);
|
|
704
|
-
const spawned = await active.pool.spawn({
|
|
705
|
-
cwd,
|
|
706
|
-
role: "worker",
|
|
707
|
-
model,
|
|
708
|
-
thinking: selection.thinking,
|
|
709
|
-
tools: WORKER_TOOLS,
|
|
710
|
-
excludeExtensions: exclusions,
|
|
711
|
-
systemPrompt: { mode: "append", text: assembleWorkerPrompt(requirePrompts().worker, name) },
|
|
712
|
-
contextFiles: true,
|
|
713
|
-
persistence: { type: "file", sessionPath },
|
|
714
|
-
});
|
|
715
|
-
if (!ownsRuntime(active)) {
|
|
716
|
-
spawned.dispose();
|
|
717
|
-
requireRuntimeOwner(active);
|
|
718
|
-
}
|
|
719
|
-
runWorker(active, worker, spawned.session, prompt);
|
|
720
|
-
return toolResult({ started: true, worker: compactWorker(worker) });
|
|
721
|
-
} catch (error) {
|
|
722
|
-
if (ownsRuntime(active)) active.store.dispatch({ type: "REMOVE_WORKER", name });
|
|
723
|
-
throw error;
|
|
724
|
-
}
|
|
725
|
-
} finally {
|
|
726
|
-
if (ownsRuntime(active)) startingNames.delete(name);
|
|
727
|
-
}
|
|
214
|
+
const handler = ACTION_HANDLERS[params.action as keyof typeof ACTION_HANDLERS];
|
|
215
|
+
if (!handler) throw new Error(`未知 subagents action:${String(params.action)}`);
|
|
216
|
+
return handler(active, params, ctx);
|
|
728
217
|
},
|
|
729
218
|
});
|
|
730
219
|
|
|
731
|
-
pi.on("session_start", (_event, ctx) => {
|
|
732
|
-
deactivate();
|
|
220
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
221
|
+
await deactivate();
|
|
733
222
|
if (!autoActivate) return;
|
|
734
223
|
try {
|
|
735
224
|
activateSession(ctx);
|
|
@@ -741,460 +230,23 @@ export function registerMaster(
|
|
|
741
230
|
pi.on("session_shutdown", () => deactivate());
|
|
742
231
|
}
|
|
743
232
|
|
|
744
|
-
function settleWorker(
|
|
745
|
-
active: MasterRuntime,
|
|
746
|
-
identity: WorkerRef,
|
|
747
|
-
terminal: WorkerTerminal | undefined,
|
|
748
|
-
error?: unknown,
|
|
749
|
-
): string | undefined {
|
|
750
|
-
const current = active.store.state.workers.find((worker) => worker.name === identity.name);
|
|
751
|
-
if (!current || current.sessionPath !== identity.sessionPath) return undefined;
|
|
752
|
-
active.store.dispatch({ type: "UPSERT_WORKER", worker: { ...current, status: "idle" } });
|
|
753
|
-
active.currentTools.delete(identity.sessionPath);
|
|
754
|
-
active.idleSince.set(identity.sessionPath, Date.now());
|
|
755
|
-
const obligation = current.reviewNeeded ? "\n此票有审查义务,请显式 review。" : "";
|
|
756
|
-
const failure = error instanceof Error ? error.message : error === undefined ? terminalFailure(terminal) : String(error);
|
|
757
|
-
return failure
|
|
758
|
-
? `子代理 ${identity.name} 已停下\n${sectionLine("error")}\n${failure}${obligation}`
|
|
759
|
-
: `子代理 ${identity.name} 已停下\n${sectionLine("reply")}\n${terminal!.text}${obligation}`;
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
function unackedEvents(ctx: ExtensionContext): PendingMasterEvent[] {
|
|
763
|
-
const entries = ctx.sessionManager.getEntries?.() ?? [];
|
|
764
|
-
const pending = new Map<string, PendingMasterEvent>();
|
|
765
|
-
const acked = new Set<string>();
|
|
766
|
-
for (const entry of entries) {
|
|
767
|
-
if (!entry || typeof entry !== "object") continue;
|
|
768
|
-
const record = entry as { type?: unknown; customType?: unknown; data?: unknown };
|
|
769
|
-
if (record.type !== "custom" || !record.data || typeof record.data !== "object") continue;
|
|
770
|
-
const data = record.data as Record<string, unknown>;
|
|
771
|
-
if (record.customType === PENDING_EVENT_TYPE && typeof data.id === "string" && typeof data.content === "string")
|
|
772
|
-
pending.set(data.id, { id: data.id, content: data.content, ...(typeof data.worker === "string" ? { worker: data.worker } : {}) });
|
|
773
|
-
if (record.customType === EVENT_ACK_TYPE && Array.isArray(data.ids))
|
|
774
|
-
for (const id of data.ids) if (typeof id === "string") acked.add(id);
|
|
775
|
-
}
|
|
776
|
-
return [...pending.values()].filter((event) => !acked.has(event.id));
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
function monitorReview(
|
|
780
|
-
session: { subscribe: (listener: (event: { type: string }) => void) => () => void; prompt: (text: string) => Promise<void> },
|
|
781
|
-
sessionPath: string,
|
|
782
|
-
previousRunId: string | undefined,
|
|
783
|
-
): Promise<ReviewOutcome> {
|
|
784
|
-
return new Promise((resolve, reject) => {
|
|
785
|
-
let settled = false;
|
|
786
|
-
let unsubscribe = () => {};
|
|
787
|
-
const fail = (error: unknown) => {
|
|
788
|
-
if (settled) return;
|
|
789
|
-
settled = true;
|
|
790
|
-
unsubscribe();
|
|
791
|
-
reject(error);
|
|
792
|
-
};
|
|
793
|
-
const finish = (outcome: ReviewOutcome) => {
|
|
794
|
-
if (settled) return;
|
|
795
|
-
const runId = reviewRunId(outcome);
|
|
796
|
-
if (outcome.status !== "error"
|
|
797
|
-
&& (!runId || runId === previousRunId || outcome.status === "in_progress" || outcome.status === "none")) return;
|
|
798
|
-
settled = true;
|
|
799
|
-
unsubscribe();
|
|
800
|
-
resolve(outcome);
|
|
801
|
-
};
|
|
802
|
-
unsubscribe = session.subscribe((event) => {
|
|
803
|
-
if (event.type === "entry_appended") finish(readReviewOutcome(sessionPath));
|
|
804
|
-
});
|
|
805
|
-
void session.prompt("/fire-review").then(
|
|
806
|
-
() => {
|
|
807
|
-
const outcome = readReviewOutcome(sessionPath);
|
|
808
|
-
const runId = reviewRunId(outcome);
|
|
809
|
-
if (outcome.status === "error") return finish(outcome);
|
|
810
|
-
if (!runId || runId === previousRunId)
|
|
811
|
-
return fail(new Error("fire-review 审查未启动"));
|
|
812
|
-
finish(outcome);
|
|
813
|
-
},
|
|
814
|
-
fail,
|
|
815
|
-
);
|
|
816
|
-
});
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
function reviewRunId(outcome: ReviewOutcome): string | undefined {
|
|
820
|
-
return "runId" in outcome ? outcome.runId : undefined;
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
function reviewOutcomeText(
|
|
824
|
-
name: string,
|
|
825
|
-
outcome: ReviewOutcome,
|
|
826
|
-
messages: Array<{ role: string; content?: unknown }>,
|
|
827
|
-
): string {
|
|
828
|
-
const reply = latestAssistantText(messages) || "(无回复)";
|
|
829
|
-
if (outcome.status === "passed") return `子代理 ${name} 审查通过(${outcome.rounds} 轮)\n${sectionLine("finalReply")}\n${reply}`;
|
|
830
|
-
if (outcome.status === "stopped") return `子代理 ${name} 审查停止(${outcome.rounds} 轮)${outcome.advisorAdvice ? `:${outcome.advisorAdvice}` : ""}\n${sectionLine("finalReply")}\n${reply}`;
|
|
831
|
-
if (outcome.status === "failed") return `子代理 ${name} 审查未完成:${outcome.reason}\n${sectionLine("finalReply")}\n${reply}`;
|
|
832
|
-
if (outcome.status === "error") return `子代理 ${name} 审查读取失败:${outcome.message}`;
|
|
833
|
-
return `子代理 ${name} 审查未完成`;
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
function captureWorkerTerminal(
|
|
837
|
-
messages: Array<{ role: string; content?: unknown; stopReason?: string; errorMessage?: string }>,
|
|
838
|
-
): WorkerTerminal | undefined {
|
|
839
|
-
const message = messages.findLast((candidate) => candidate.role === "assistant");
|
|
840
|
-
if (!message) return undefined;
|
|
841
|
-
return {
|
|
842
|
-
text: assistantText(message.content),
|
|
843
|
-
...(message.stopReason ? { stopReason: message.stopReason } : {}),
|
|
844
|
-
...(message.errorMessage ? { errorMessage: message.errorMessage } : {}),
|
|
845
|
-
};
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
function terminalFailure(terminal: WorkerTerminal | undefined): string | undefined {
|
|
849
|
-
if (!terminal) return "回合结束但未产生 assistant 终态";
|
|
850
|
-
if (terminal.stopReason === "error") return terminal.errorMessage || "供应商返回未知错误";
|
|
851
|
-
if (terminal.stopReason === "aborted") return `回合意外中止:${terminal.errorMessage || "供应商未提供原因"}`;
|
|
852
|
-
if (!terminal.text) return "回合结束但未产生回复";
|
|
853
|
-
return undefined;
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
function providerFaultReason(terminal: WorkerTerminal | undefined): string | undefined {
|
|
857
|
-
if (terminal?.stopReason !== "error" || !terminal.errorMessage) return undefined;
|
|
858
|
-
const message = terminal.errorMessage;
|
|
859
|
-
if (/GoUsageLimitError|FreeUsageLimitError|Monthly usage limit reached|available balance|insufficient_quota|out of budget|quota (?:exceeded|exhausted)|billing/iu.test(message))
|
|
860
|
-
return "额度或计费耗尽";
|
|
861
|
-
if (/(?:model|deployment).*(?:not found|does not exist|unavailable|not available|unsupported)|(?:not found|unavailable).*(?:model|deployment)/iu.test(message))
|
|
862
|
-
return "模型不可用或找不到";
|
|
863
|
-
if (/\b(?:500|502|503|504|524)\b|service.?unavailable|internal.?server.?error/iu.test(message))
|
|
864
|
-
return "持续 5xx,宿主重试已用尽";
|
|
865
|
-
return undefined;
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
function latestAssistantText(messages: Array<{ role: string; content?: unknown }>): string {
|
|
869
|
-
const message = messages.findLast((candidate) => candidate.role === "assistant");
|
|
870
|
-
return assistantText(message?.content);
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
function assistantText(content: unknown): string {
|
|
874
|
-
if (!Array.isArray(content)) return "";
|
|
875
|
-
return content
|
|
876
|
-
.filter((part): part is { type: "text"; text: string } =>
|
|
877
|
-
!!part && typeof part === "object" && (part as { type?: unknown }).type === "text"
|
|
878
|
-
&& typeof (part as { text?: unknown }).text === "string")
|
|
879
|
-
.map((part) => part.text)
|
|
880
|
-
.join("\n");
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
async function resolveConfiguredModel(id: string): Promise<Model<any>> {
|
|
884
|
-
const runtime = await ModelRuntime.create({
|
|
885
|
-
authPath: `${getAgentDir()}/auth.json`,
|
|
886
|
-
modelsPath: `${getAgentDir()}/models.json`,
|
|
887
|
-
});
|
|
888
|
-
const slash = id.indexOf("/");
|
|
889
|
-
const model = slash > 0 ? runtime.getModel(id.slice(0, slash), id.slice(slash + 1)) : undefined;
|
|
890
|
-
if (!model) throw new Error(`找不到模型:${id}`);
|
|
891
|
-
return model;
|
|
892
|
-
}
|
|
893
|
-
|
|
894
233
|
function reviewGateError(): string | undefined {
|
|
895
234
|
const loaded = loadConfig();
|
|
896
235
|
if (loaded.config.features.review === false) return "fire-review 已关闭,不能挂审查义务或发起审查";
|
|
897
|
-
|
|
898
|
-
problem.startsWith("review") || problem.startsWith("未知字段 review.") || problem.startsWith("config.jsonc"));
|
|
899
|
-
return problems.length ? `fire-review 配置有问题,已停止:${problems.join(";")}` : undefined;
|
|
236
|
+
return "error" in loaded.review ? loaded.review.error : undefined;
|
|
900
237
|
}
|
|
901
238
|
|
|
902
239
|
function loadMasterPrompts() {
|
|
903
240
|
try {
|
|
904
|
-
return {
|
|
905
|
-
master: readMasterPrompt("master"),
|
|
906
|
-
worker: readMasterPrompt("worker"),
|
|
907
|
-
};
|
|
241
|
+
return { master: readMasterPrompt("master"), worker: readMasterPrompt("worker") };
|
|
908
242
|
} catch (error) {
|
|
909
243
|
return { error: error instanceof Error ? error.message : String(error) };
|
|
910
244
|
}
|
|
911
245
|
}
|
|
912
246
|
|
|
913
|
-
function loadMasterConfiguration() {
|
|
914
|
-
let loaded: ReturnType<typeof loadConfig>;
|
|
915
|
-
try {
|
|
916
|
-
loaded = loadConfig();
|
|
917
|
-
} catch (error) {
|
|
918
|
-
return { error: `Master 配置读取失败:${error instanceof Error ? error.message : String(error)}` };
|
|
919
|
-
}
|
|
920
|
-
const problems = loaded.problems.filter((problem) => problem.startsWith("master") || problem.startsWith("未知字段 master.") || problem.startsWith("未知角色 master.") || problem.startsWith("config.jsonc") || problem.startsWith("features"));
|
|
921
|
-
if (problems.length) return { error: `Master 配置有问题,已停止:${problems.join(";")}` };
|
|
922
|
-
if (!loaded.config.master.roles.length) return { error: "Master 配置有问题,已停止:请在 master.roles 至少配置一个角色" };
|
|
923
|
-
return loaded.config.master;
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
function resolveRole(roles: MasterRole[], role: string): MasterRole {
|
|
927
|
-
const entry = roles.find((candidate) => candidate.role === role);
|
|
928
|
-
if (!entry) throw new Error(`角色未配置:${role}。已配置角色:${roles.map((candidate) => candidate.role).join("、")}`);
|
|
929
|
-
return entry;
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
function nextFallback(role: MasterRole, worker: WorkerRef): MasterModelAtom | undefined {
|
|
933
|
-
const chain: MasterModelAtom[] = [role, ...role.fallback];
|
|
934
|
-
let index = chain.findIndex((atom) => atom.model === worker.model && atom.thinking === worker.thinking);
|
|
935
|
-
if (index < 0) index = chain.findIndex((atom) => atom.model === worker.model);
|
|
936
|
-
return chain[index + 1];
|
|
937
|
-
}
|
|
938
|
-
|
|
939
247
|
function rosterText(models: MasterRole[]): string {
|
|
940
248
|
return models.map((entry) => {
|
|
941
|
-
const fallback = entry.fallback.length
|
|
942
|
-
? `,fallback ${entry.fallback.map(modelAtomText).join(" → ")}`
|
|
943
|
-
: "";
|
|
249
|
+
const fallback = entry.fallback.length ? `,fallback ${entry.fallback.map(modelAtomText).join(" → ")}` : "";
|
|
944
250
|
return `${entry.role}:${modelAtomText(entry)}(${entry.use}${fallback})`;
|
|
945
251
|
}).join(";");
|
|
946
252
|
}
|
|
947
|
-
|
|
948
|
-
function modelAtomText(atom: Pick<MasterModelAtom, "model" | "thinking">): string {
|
|
949
|
-
return `${atom.model}/${atom.thinking}`;
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
function masterEventEnvelope(content: string): string {
|
|
953
|
-
return `<firecode_master_event>\n${content}\n</firecode_master_event>`;
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
function resumeCheckPrompt(): string {
|
|
957
|
-
return masterEventEnvelope("上次被外部中断,先核对 git status 与现场再继续,避免重复执行已经发生的副作用。");
|
|
958
|
-
}
|
|
959
|
-
|
|
960
|
-
function fallbackResumePrompt(from: string, to: string, reason: string): string {
|
|
961
|
-
return masterEventEnvelope(`供应商故障,已切换 ${from}→${to}(${reason})。沿用当前会话与原工作说明,从中断处继续,不要重复已经完成的副作用。`);
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
const ACTION_VERB: Record<string, string> = { start: "启动", list: "查看", kill: "移除", send: "发送", interrupt: "中断", review: "审查", tail: "近况", ack: "待命" };
|
|
965
|
-
function subagentsCallParts(args: Record<string, unknown>): Part[] {
|
|
966
|
-
const action = typeof args.action === "string" ? args.action : "?";
|
|
967
|
-
const parts: Part[] = [{ text: ACTION_VERB[action] ?? action, bold: true }];
|
|
968
|
-
const target = optionalString(args.worker);
|
|
969
|
-
if (target) parts.push({ text: ` ${target}`, color: "accent" });
|
|
970
|
-
const role = optionalString(args.role);
|
|
971
|
-
if ((action === "start" || action === "send") && role) parts.push({ text: ` · ${role}`, color: "muted" });
|
|
972
|
-
const prompt = optionalString(args.prompt)?.split("\n", 1)[0];
|
|
973
|
-
if (prompt && action === "start") parts.push({ text: ` — ${prompt}`, color: "muted" });
|
|
974
|
-
return parts;
|
|
975
|
-
}
|
|
976
|
-
|
|
977
|
-
const renderSubagentsResult = makeResultRenderer(false);
|
|
978
|
-
const STATUS_WORD = { working: "工作", idle: "空闲", reviewing: "审查" } satisfies Record<WorkerStatus, string>;
|
|
979
|
-
const SPINNER_FRAMES = [..."⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"];
|
|
980
|
-
const SPINNER_MS = 120;
|
|
981
|
-
|
|
982
|
-
function reviewProgressFromEntry(entry: unknown): ReviewProgress | undefined {
|
|
983
|
-
if (!entry || typeof entry !== "object") return undefined;
|
|
984
|
-
const record = entry as { type?: unknown; customType?: unknown; data?: unknown };
|
|
985
|
-
if (record.type !== "custom" || record.customType !== "firecode-review-checkpoint") return undefined;
|
|
986
|
-
if (!record.data || typeof record.data !== "object") return undefined;
|
|
987
|
-
const active = (record.data as { active?: unknown }).active;
|
|
988
|
-
if (!active || typeof active !== "object") return undefined;
|
|
989
|
-
const progress = active as { round?: unknown; settledCount?: unknown; reviewers?: unknown };
|
|
990
|
-
if (
|
|
991
|
-
typeof progress.round !== "number"
|
|
992
|
-
|| typeof progress.settledCount !== "number"
|
|
993
|
-
|| !Array.isArray(progress.reviewers)
|
|
994
|
-
) return undefined;
|
|
995
|
-
return {
|
|
996
|
-
kind: "review",
|
|
997
|
-
round: progress.round,
|
|
998
|
-
settled: progress.settledCount,
|
|
999
|
-
total: progress.reviewers.length,
|
|
1000
|
-
};
|
|
1001
|
-
}
|
|
1002
|
-
|
|
1003
|
-
function currentWorkerAction(active: MasterRuntime, worker: ReturnType<typeof compactWorker>) {
|
|
1004
|
-
if (worker.status === "reviewing") return active.reviewProgress.get(worker.session);
|
|
1005
|
-
if (worker.status === "idle") {
|
|
1006
|
-
let since = active.idleSince.get(worker.session);
|
|
1007
|
-
try {
|
|
1008
|
-
since ??= statSync(worker.session).mtimeMs;
|
|
1009
|
-
} catch {
|
|
1010
|
-
// 缺失档案仍可 list;真正恢复时由 send 明确报错。
|
|
1011
|
-
}
|
|
1012
|
-
return { kind: "idle" as const, ...(since ? { since } : {}) };
|
|
1013
|
-
}
|
|
1014
|
-
const current = [...(active.currentTools.get(worker.session)?.values() ?? [])].at(-1);
|
|
1015
|
-
return current ? { kind: "tool" as const, ...current } : undefined;
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
function expandedWorkerList(
|
|
1019
|
-
workers: unknown[],
|
|
1020
|
-
theme: ExtensionContext["ui"]["theme"],
|
|
1021
|
-
context: Parameters<typeof renderSubagentsResult>[3],
|
|
1022
|
-
) {
|
|
1023
|
-
return {
|
|
1024
|
-
invalidate() {},
|
|
1025
|
-
render(width: number): string[] {
|
|
1026
|
-
return ["", ...workers.flatMap((value) => {
|
|
1027
|
-
const worker = value as Record<string, unknown>;
|
|
1028
|
-
const action = worker.currentAction as {
|
|
1029
|
-
kind?: string;
|
|
1030
|
-
tool?: string;
|
|
1031
|
-
startedAt?: number;
|
|
1032
|
-
since?: number;
|
|
1033
|
-
round?: number;
|
|
1034
|
-
settled?: number;
|
|
1035
|
-
total?: number;
|
|
1036
|
-
} | undefined;
|
|
1037
|
-
const actionParts: Part[] = action?.kind === "tool" && action.tool && action.startedAt
|
|
1038
|
-
? [{
|
|
1039
|
-
text: ` · ${action.tool} · 已 ${formatDuration(Math.max(0, Date.now() - action.startedAt))}`,
|
|
1040
|
-
color: "accent",
|
|
1041
|
-
}]
|
|
1042
|
-
: action?.kind === "idle"
|
|
1043
|
-
? [{
|
|
1044
|
-
text: action.since ? ` · 落定 ${formatDuration(Date.now() - action.since)}前` : " · 已落定",
|
|
1045
|
-
color: "muted",
|
|
1046
|
-
}]
|
|
1047
|
-
: action?.kind === "review"
|
|
1048
|
-
? [{ text: ` · 第 ${action.round} 轮 · 审查者 ${action.settled}/${action.total}`, color: "accent" }]
|
|
1049
|
-
: [];
|
|
1050
|
-
return new ToolLine({
|
|
1051
|
-
label: String(worker.name),
|
|
1052
|
-
value: [
|
|
1053
|
-
{ text: roleStatusText(worker), color: "accent" },
|
|
1054
|
-
...actionParts,
|
|
1055
|
-
{ text: ` · ${String(worker.model).split("/").pop()}/${String(worker.thinking)}`, color: "muted" },
|
|
1056
|
-
],
|
|
1057
|
-
clip: "end",
|
|
1058
|
-
theme,
|
|
1059
|
-
ctx: { ...context, state: {}, expanded: false },
|
|
1060
|
-
}).render(width);
|
|
1061
|
-
})];
|
|
1062
|
-
},
|
|
1063
|
-
};
|
|
1064
|
-
}
|
|
1065
|
-
function listMeta(workers: unknown[]): Part[] {
|
|
1066
|
-
if (!workers.length) return [{ text: " — 池 0", color: "muted" }];
|
|
1067
|
-
return [{ text: ` — 池 ${workers.length}:${workers.map((value) => {
|
|
1068
|
-
const worker = value as Record<string, unknown>;
|
|
1069
|
-
return `${String(worker.name)} ${roleStatusText(worker)}`;
|
|
1070
|
-
}).join(" · ")}`, color: "muted" }];
|
|
1071
|
-
}
|
|
1072
|
-
/** 角色为主的状态投影:「工程师·工作」;档案缺角色时退到纯状态词。 */
|
|
1073
|
-
function roleStatusText(worker: { role?: unknown; status?: unknown }): string {
|
|
1074
|
-
const status = STATUS_WORD[worker.status as WorkerStatus] ?? String(worker.status);
|
|
1075
|
-
return worker.role ? `${String(worker.role)}·${status}` : status;
|
|
1076
|
-
}
|
|
1077
|
-
/** 有子代理在飞(工作或审查)时底栏活动动画运转,也是动画计时器的唯一起停判据。 */
|
|
1078
|
-
export function masterActive(workers: ReadonlyArray<Pick<WorkerRef, "status">>): boolean {
|
|
1079
|
-
return workers.some((worker) => worker.status !== "idle");
|
|
1080
|
-
}
|
|
1081
|
-
export function masterStatusLine(
|
|
1082
|
-
workers: ReadonlyArray<Pick<WorkerRef, "status" | "role">>,
|
|
1083
|
-
theme: Pick<ExtensionContext["ui"]["theme"], "fg">,
|
|
1084
|
-
frame = 0,
|
|
1085
|
-
): string {
|
|
1086
|
-
if (!workers.length) return theme.fg("dim", "👑 指挥官");
|
|
1087
|
-
const spinner = masterActive(workers) ? `${SPINNER_FRAMES[frame % SPINNER_FRAMES.length]} ` : "";
|
|
1088
|
-
const byRole = new Map<string, number>();
|
|
1089
|
-
let idle = 0;
|
|
1090
|
-
for (const worker of workers) {
|
|
1091
|
-
if (worker.status === "idle") idle += 1;
|
|
1092
|
-
else {
|
|
1093
|
-
const initial = roleInitial(worker.role);
|
|
1094
|
-
byRole.set(initial, (byRole.get(initial) ?? 0) + 1);
|
|
1095
|
-
}
|
|
1096
|
-
}
|
|
1097
|
-
const counts = [...byRole].map(([initial, count]) => `${initial}${count}`);
|
|
1098
|
-
if (idle) counts.push(`闲${idle}`);
|
|
1099
|
-
return theme.fg("dim", `👑 ${spinner}${counts.join("·")}`);
|
|
1100
|
-
}
|
|
1101
|
-
/** 角色首字(按 code point 取,兼容 emoji 角色名);无角色时以工作态首字兜底。 */
|
|
1102
|
-
function roleInitial(role: string | undefined): string {
|
|
1103
|
-
return [...(role ?? "")][0] ?? STATUS_WORD.working[0]!;
|
|
1104
|
-
}
|
|
1105
|
-
export function statusText(workers: WorkerRef[]): string {
|
|
1106
|
-
return workers.length
|
|
1107
|
-
? workers.map((worker) => `${worker.name} ${roleStatusText(worker)} ${worker.model.split("/").pop()}`).join("\n")
|
|
1108
|
-
: "没有子代理";
|
|
1109
|
-
}
|
|
1110
|
-
function compactWorker(worker: WorkerRef) {
|
|
1111
|
-
return {
|
|
1112
|
-
name: worker.name,
|
|
1113
|
-
role: worker.role,
|
|
1114
|
-
status: worker.status,
|
|
1115
|
-
model: worker.model,
|
|
1116
|
-
thinking: worker.thinking,
|
|
1117
|
-
session: worker.sessionPath,
|
|
1118
|
-
...(worker.interruptedAt ? { interruptedAt: worker.interruptedAt } : {}),
|
|
1119
|
-
...(worker.reviewNeeded ? { reviewNeeded: true } : {}),
|
|
1120
|
-
...(worker.disposition ? { disposition: worker.disposition } : {}),
|
|
1121
|
-
};
|
|
1122
|
-
}
|
|
1123
|
-
|
|
1124
|
-
async function readWorkerTrace(worker: WorkerRef): Promise<string> {
|
|
1125
|
-
let raw: string;
|
|
1126
|
-
try {
|
|
1127
|
-
raw = await readFile(worker.sessionPath, "utf8");
|
|
1128
|
-
} catch (error) {
|
|
1129
|
-
throw new Error(`无法读取子代理 ${worker.name} 会话:${error instanceof Error ? error.message : String(error)}`);
|
|
1130
|
-
}
|
|
1131
|
-
const lines: string[] = [];
|
|
1132
|
-
for (const line of raw.split(/\r?\n/u)) {
|
|
1133
|
-
if (!line) continue;
|
|
1134
|
-
try {
|
|
1135
|
-
const entry = JSON.parse(line) as { type?: string; message?: { role?: string; content?: unknown } };
|
|
1136
|
-
if (entry.type !== "message" || !entry.message?.role) continue;
|
|
1137
|
-
const text = messageText(entry.message.content);
|
|
1138
|
-
if (text) lines.push(`${entry.message.role}: ${text}`);
|
|
1139
|
-
} catch {
|
|
1140
|
-
// 正在追加的尾行可暂时不完整;近况保留此前完整记录。
|
|
1141
|
-
}
|
|
1142
|
-
}
|
|
1143
|
-
return `子代理 ${worker.name} 近况(${worker.status})\n${lines.join("\n").slice(-4_000)}`;
|
|
1144
|
-
}
|
|
1145
|
-
|
|
1146
|
-
function messageText(content: unknown): string {
|
|
1147
|
-
if (typeof content === "string") return content;
|
|
1148
|
-
if (!Array.isArray(content)) return "";
|
|
1149
|
-
return content
|
|
1150
|
-
.filter((part): part is { type: "text"; text: string } =>
|
|
1151
|
-
!!part && typeof part === "object" && (part as { type?: unknown }).type === "text"
|
|
1152
|
-
&& typeof (part as { text?: unknown }).text === "string")
|
|
1153
|
-
.map((part) => part.text)
|
|
1154
|
-
.join("\n");
|
|
1155
|
-
}
|
|
1156
|
-
function toolResult(value: unknown) {
|
|
1157
|
-
return { content: [{ type: "text" as const, text: JSON.stringify(value) }], details: value };
|
|
1158
|
-
}
|
|
1159
|
-
function requiredString(value: unknown, field: string): string {
|
|
1160
|
-
if (typeof value !== "string" || !value.trim()) throw new Error(`${field} 不能为空`);
|
|
1161
|
-
return value.trim();
|
|
1162
|
-
}
|
|
1163
|
-
function optionalString(value: unknown): string | undefined {
|
|
1164
|
-
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
1165
|
-
}
|
|
1166
|
-
function validateWorkerName(name: string): void {
|
|
1167
|
-
if (!/^[a-z][a-z0-9_-]{0,31}$/u.test(name)) throw new Error("Worker name 必须匹配 [a-z][a-z0-9_-]{0,31}");
|
|
1168
|
-
}
|
|
1169
|
-
function validateDelegationText(prompt: string): void {
|
|
1170
|
-
const text = prompt.trimStart();
|
|
1171
|
-
if (/^\/skills?:/u.test(text) && !text.startsWith("/skill:tdd ")) throw new Error("委派文本只允许 /skill:tdd 技能前缀");
|
|
1172
|
-
}
|
|
1173
|
-
async function resolveWorkerCwd(path: string): Promise<string> {
|
|
1174
|
-
if (!isAbsolute(path)) throw new Error("cwd 必须是已存在的绝对目录");
|
|
1175
|
-
try {
|
|
1176
|
-
return await realpath(path);
|
|
1177
|
-
} catch {
|
|
1178
|
-
throw new Error(`cwd 不存在:${path}`);
|
|
1179
|
-
}
|
|
1180
|
-
}
|
|
1181
|
-
async function outsideCheckoutReason(path: string, cwd: string): Promise<string | undefined> {
|
|
1182
|
-
const root = await realpath(cwd);
|
|
1183
|
-
const target = await canonicalWritePath(resolve(cwd, path));
|
|
1184
|
-
const local = relative(root, target);
|
|
1185
|
-
return local === ".." || local.startsWith(`..${sep}`) || isAbsolute(local) ? `子代理只能修改当前 checkout:${path}` : undefined;
|
|
1186
|
-
}
|
|
1187
|
-
async function canonicalWritePath(path: string): Promise<string> {
|
|
1188
|
-
let ancestor = path;
|
|
1189
|
-
const missing: string[] = [];
|
|
1190
|
-
while (true) {
|
|
1191
|
-
try {
|
|
1192
|
-
return resolve(await realpath(ancestor), ...missing.reverse());
|
|
1193
|
-
} catch {
|
|
1194
|
-
const parent = dirname(ancestor);
|
|
1195
|
-
if (parent === ancestor) return path;
|
|
1196
|
-
missing.push(basename(ancestor));
|
|
1197
|
-
ancestor = parent;
|
|
1198
|
-
}
|
|
1199
|
-
}
|
|
1200
|
-
}
|