oh-pi 0.1.58 → 0.1.59
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/package.json
CHANGED
|
@@ -60,7 +60,6 @@ interface BackgroundColony {
|
|
|
60
60
|
abortController: AbortController;
|
|
61
61
|
state: ColonyState | null;
|
|
62
62
|
phase: string;
|
|
63
|
-
log: string[];
|
|
64
63
|
antStreams: Map<string, AntStreamState>;
|
|
65
64
|
promise: Promise<ColonyState>;
|
|
66
65
|
}
|
|
@@ -84,20 +83,13 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
84
83
|
pi.on("session_start", async (_event, ctx) => {
|
|
85
84
|
pi.events.on("ant-colony:render", () => {
|
|
86
85
|
if (!activeColony) return;
|
|
87
|
-
const { state
|
|
88
|
-
const active = antStreams.size;
|
|
86
|
+
const { state } = activeColony;
|
|
89
87
|
const elapsed = state ? formatDuration(Date.now() - state.createdAt) : "0s";
|
|
90
88
|
const m = state?.metrics;
|
|
91
89
|
const colonyStatus = state?.status || "scouting";
|
|
92
|
-
const ants = state?.ants || [];
|
|
93
|
-
const turns = ants.reduce((s, a) => s + a.usage.turns, 0);
|
|
94
|
-
const outTok = ants.reduce((s, a) => s + a.usage.output, 0);
|
|
95
90
|
|
|
96
91
|
const parts = [`🐜 ${statusIcon(colonyStatus)}`];
|
|
97
92
|
if (m) parts.push(`${m.tasksDone}/${m.tasksTotal}`);
|
|
98
|
-
parts.push(`${active}⚡`);
|
|
99
|
-
parts.push(`${turns}↻`);
|
|
100
|
-
parts.push(formatTokens(outTok) + "↑");
|
|
101
93
|
if (m) parts.push(formatCost(m.totalCost));
|
|
102
94
|
parts.push(elapsed);
|
|
103
95
|
|
|
@@ -129,18 +121,7 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
129
121
|
appendFileSync(gitignorePath, `${gitContent.length && !gitContent.endsWith("\n") ? "\n" : ""}.ant-colony/\n`);
|
|
130
122
|
}
|
|
131
123
|
|
|
132
|
-
const
|
|
133
|
-
const callbacks: QueenCallbacks = {
|
|
134
|
-
onPhase(_phase, detail) { log.push(detail); },
|
|
135
|
-
onAntSpawn(ant, task) { log.push(` ${casteIcon(ant.caste)} ${ant.caste} → ${task.title.slice(0, 50)}`); },
|
|
136
|
-
onAntDone(ant, task) {
|
|
137
|
-
const dur = ant.finishedAt ? formatDuration(ant.finishedAt - ant.startedAt) : "?";
|
|
138
|
-
log.push(` ${ant.status === "done" ? "✓" : "✗"} ${ant.caste} (${dur}) → ${task.title.slice(0, 50)}`);
|
|
139
|
-
},
|
|
140
|
-
onAntStream() {},
|
|
141
|
-
onProgress() {},
|
|
142
|
-
onComplete() {},
|
|
143
|
-
};
|
|
124
|
+
const callbacks: QueenCallbacks = {};
|
|
144
125
|
|
|
145
126
|
try {
|
|
146
127
|
const state = await runColony({
|
|
@@ -160,14 +141,14 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
160
141
|
const report = [
|
|
161
142
|
`## 🐜 Ant Colony Report`,
|
|
162
143
|
`**Goal:** ${state.goal}`,
|
|
163
|
-
`**Status:** ${statusIcon(state.status)} ${state.status} │ ${elapsed}`,
|
|
164
|
-
`**
|
|
144
|
+
`**Status:** ${statusIcon(state.status)} ${state.status} │ ${elapsed} │ ${formatCost(m.totalCost)}`,
|
|
145
|
+
`**Tasks:** ${m.tasksDone}/${m.tasksTotal} done${m.tasksFailed > 0 ? `, ${m.tasksFailed} failed` : ""}`,
|
|
165
146
|
``,
|
|
166
147
|
...state.tasks.filter(t => t.status === "done").map(t =>
|
|
167
|
-
`- ✓ **${t.title}
|
|
148
|
+
`- ✓ **${t.title}**`
|
|
168
149
|
),
|
|
169
150
|
...state.tasks.filter(t => t.status === "failed").map(t =>
|
|
170
|
-
`- ✗ **${t.title}** — ${t.error?.slice(0,
|
|
151
|
+
`- ✗ **${t.title}** — ${t.error?.slice(0, 80) || "unknown"}`
|
|
171
152
|
),
|
|
172
153
|
].join("\n");
|
|
173
154
|
|
|
@@ -205,32 +186,27 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
205
186
|
abortController,
|
|
206
187
|
state: null,
|
|
207
188
|
phase: "initializing",
|
|
208
|
-
log: [],
|
|
209
189
|
antStreams: new Map(),
|
|
210
190
|
promise: null as any, // set below
|
|
211
191
|
};
|
|
212
192
|
|
|
213
193
|
const callbacks: QueenCallbacks = {
|
|
194
|
+
onSignal(signal) {
|
|
195
|
+
colony.phase = signal.message;
|
|
196
|
+
throttledRender();
|
|
197
|
+
},
|
|
214
198
|
onPhase(phase, detail) {
|
|
215
199
|
colony.phase = detail;
|
|
216
|
-
colony.log.push(`[${new Date().toLocaleTimeString()}] ${statusIcon(phase)} ${detail}`);
|
|
217
200
|
throttledRender();
|
|
218
201
|
},
|
|
219
202
|
onAntSpawn(ant, task) {
|
|
220
203
|
colony.antStreams.set(ant.id, {
|
|
221
|
-
antId: ant.id,
|
|
222
|
-
caste: ant.caste,
|
|
223
|
-
lastLine: "starting...",
|
|
224
|
-
tokens: 0,
|
|
204
|
+
antId: ant.id, caste: ant.caste, lastLine: "starting...", tokens: 0,
|
|
225
205
|
});
|
|
226
|
-
colony.log.push(` ${casteIcon(ant.caste)} ${ant.caste} ant dispatched → ${task.title.slice(0, 50)}`);
|
|
227
206
|
throttledRender();
|
|
228
207
|
},
|
|
229
|
-
onAntDone(ant
|
|
208
|
+
onAntDone(ant) {
|
|
230
209
|
colony.antStreams.delete(ant.id);
|
|
231
|
-
const dur = ant.finishedAt ? formatDuration(ant.finishedAt - ant.startedAt) : "?";
|
|
232
|
-
const icon = ant.status === "done" ? "✓" : "✗";
|
|
233
|
-
colony.log.push(` ${icon} ${ant.caste} finished (${dur}, ${formatCost(ant.usage.cost)}) → ${task.title.slice(0, 50)}`);
|
|
234
210
|
throttledRender();
|
|
235
211
|
},
|
|
236
212
|
onAntStream(event: AntStreamEvent) {
|
|
@@ -240,7 +216,6 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
240
216
|
const lines = event.totalText.split("\n").filter(l => l.trim());
|
|
241
217
|
stream.lastLine = lines[lines.length - 1]?.trim() || "...";
|
|
242
218
|
}
|
|
243
|
-
throttledRender();
|
|
244
219
|
},
|
|
245
220
|
onProgress(metrics) {
|
|
246
221
|
if (colony.state) colony.state.metrics = metrics;
|
|
@@ -285,25 +260,15 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
285
260
|
|
|
286
261
|
const report = [
|
|
287
262
|
`## 🐜 Ant Colony Report`,
|
|
288
|
-
``,
|
|
289
263
|
`**Goal:** ${state.goal}`,
|
|
290
|
-
`**Status:** ${statusIcon(state.status)} ${state.status}`,
|
|
291
|
-
`**
|
|
292
|
-
...(state.maxCost != null ? [`**Budget:** ${formatCost(m.totalCost)} / ${formatCost(state.maxCost)}`] : []),
|
|
293
|
-
``,
|
|
294
|
-
`### Metrics`,
|
|
295
|
-
`- Tasks: ${m.tasksDone}/${m.tasksTotal} done, ${m.tasksFailed} failed`,
|
|
296
|
-
`- Ants spawned: ${m.antsSpawned}`,
|
|
297
|
-
`- Tokens: ${formatTokens(m.totalTokens)}`,
|
|
298
|
-
`- Cost: ${formatCost(m.totalCost)}`,
|
|
299
|
-
`- Peak concurrency: ${state.concurrency.optimal}`,
|
|
264
|
+
`**Status:** ${statusIcon(state.status)} ${state.status} │ ${elapsed} │ ${formatCost(m.totalCost)}`,
|
|
265
|
+
`**Tasks:** ${m.tasksDone}/${m.tasksTotal} done${m.tasksFailed > 0 ? `, ${m.tasksFailed} failed` : ""}`,
|
|
300
266
|
``,
|
|
301
|
-
`### Task Results`,
|
|
302
267
|
...state.tasks.filter(t => t.status === "done").map(t =>
|
|
303
|
-
`- ✓ **${t.title}
|
|
268
|
+
`- ✓ **${t.title}**`
|
|
304
269
|
),
|
|
305
270
|
...state.tasks.filter(t => t.status === "failed").map(t =>
|
|
306
|
-
`- ✗ **${t.title}** — ${t.error?.slice(0,
|
|
271
|
+
`- ✗ **${t.title}** — ${t.error?.slice(0, 80) || "unknown"}`
|
|
307
272
|
),
|
|
308
273
|
].join("\n");
|
|
309
274
|
|
|
@@ -420,22 +385,10 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
420
385
|
// ── Active Ants ──
|
|
421
386
|
const streams = Array.from(c.antStreams.values());
|
|
422
387
|
if (streams.length > 0) {
|
|
423
|
-
lines.push(theme.fg("accent",
|
|
424
|
-
for (const s of streams) {
|
|
425
|
-
const line = s.lastLine.length > w - 20 ? s.lastLine.slice(0, w - 23) + "..." : s.lastLine;
|
|
426
|
-
lines.push(` ${casteIcon(s.caste)} ${theme.fg("accent", s.antId.slice(0, 14))} ${theme.fg("dim", `${s.tokens}tok`)} ${theme.fg("muted", "▸")} ${line}`);
|
|
427
|
-
}
|
|
388
|
+
lines.push(theme.fg("accent", ` Active: ${streams.length} ants working`));
|
|
428
389
|
lines.push("");
|
|
429
390
|
}
|
|
430
391
|
|
|
431
|
-
// ── Log (last 8) ──
|
|
432
|
-
if (c.log.length > 0) {
|
|
433
|
-
lines.push(theme.fg("accent", " Log"));
|
|
434
|
-
for (const l of c.log.slice(-8)) {
|
|
435
|
-
lines.push(theme.fg("dim", ` ${l.slice(0, w - 2)}`));
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
|
|
439
392
|
lines.push("");
|
|
440
393
|
lines.push(theme.fg("muted", " esc close"));
|
|
441
394
|
return lines;
|
|
@@ -566,38 +519,14 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
566
519
|
const state = c.state;
|
|
567
520
|
const elapsed = state ? formatDuration(Date.now() - state.createdAt) : "0s";
|
|
568
521
|
const m = state?.metrics;
|
|
569
|
-
const
|
|
570
|
-
const ants = state?.ants || [];
|
|
571
|
-
const streams = Array.from(c.antStreams.values());
|
|
522
|
+
const phase = state?.status || "scouting";
|
|
572
523
|
|
|
573
524
|
const lines: string[] = [
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
`**Phase:** ${c.phase}`,
|
|
577
|
-
`**Duration:** ${elapsed}`,
|
|
525
|
+
`🐜 ${statusIcon(phase)} ${c.goal.slice(0, 80)}`,
|
|
526
|
+
`${phase} │ ${m ? `${m.tasksDone}/${m.tasksTotal} tasks` : "starting"} │ ${m ? formatCost(m.totalCost) : "$0"} │ ${elapsed}`,
|
|
578
527
|
];
|
|
579
528
|
|
|
580
|
-
if (m) {
|
|
581
|
-
lines.push(`**Tasks:** ${m.tasksDone}/${m.tasksTotal} done, ${m.tasksFailed} failed`);
|
|
582
|
-
lines.push(`**Ants spawned:** ${m.antsSpawned} | **Active:** ${streams.length}`);
|
|
583
|
-
lines.push(`**Cost:** ${formatCost(m.totalCost)} | **Tokens:** ${formatTokens(m.totalTokens)}`);
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
if (tasks.length > 0) {
|
|
587
|
-
lines.push("", "### Tasks");
|
|
588
|
-
for (const t of tasks) {
|
|
589
|
-
const icon = t.status === "done" ? "✓" : t.status === "failed" ? "✗" : t.status === "active" ? "●" : "○";
|
|
590
|
-
const dur = t.finishedAt && t.startedAt ? ` (${formatDuration(t.finishedAt - t.startedAt)})` : "";
|
|
591
|
-
lines.push(`- ${icon} [${t.caste}] ${t.title}${dur}`);
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
if (streams.length > 0) {
|
|
596
|
-
lines.push("", "### Active Ants");
|
|
597
|
-
for (const s of streams) {
|
|
598
|
-
lines.push(`- ${casteIcon(s.caste)} ${s.antId.slice(0, 14)} | ${s.tokens}tok | ${s.lastLine.slice(0, 60)}`);
|
|
599
|
-
}
|
|
600
|
-
}
|
|
529
|
+
if (m && m.tasksFailed > 0) lines.push(`⚠ ${m.tasksFailed} failed`);
|
|
601
530
|
|
|
602
531
|
return lines.join("\n");
|
|
603
532
|
}
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
import * as fs from "node:fs";
|
|
16
16
|
import * as path from "node:path";
|
|
17
17
|
import type {
|
|
18
|
-
ColonyState, Task, Ant, AntCaste, ColonyMetrics,
|
|
18
|
+
ColonyState, Task, Ant, AntCaste, ColonyMetrics, ColonySignal,
|
|
19
19
|
ConcurrencyConfig, TaskPriority, ModelOverrides, AntStreamEvent,
|
|
20
20
|
} from "./types.js";
|
|
21
21
|
import { DEFAULT_ANT_CONFIGS } from "./types.js";
|
|
@@ -26,12 +26,15 @@ import { buildImportGraph, taskDependsOn, type ImportGraph } from "./deps.js";
|
|
|
26
26
|
import type { AuthStorage, ModelRegistry } from "@mariozechner/pi-coding-agent";
|
|
27
27
|
|
|
28
28
|
export interface QueenCallbacks {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
/** 抽象信号 — 观察者只需实现这一个 */
|
|
30
|
+
onSignal?(signal: ColonySignal): void;
|
|
31
|
+
/** 以下为细粒度回调(verbose 模式,可选) */
|
|
32
|
+
onPhase?(phase: ColonyState["status"], detail: string): void;
|
|
33
|
+
onAntSpawn?(ant: Ant, task: Task): void;
|
|
34
|
+
onAntDone?(ant: Ant, task: Task, output: string): void;
|
|
32
35
|
onAntStream?(event: AntStreamEvent): void;
|
|
33
|
-
onProgress(metrics: ColonyMetrics): void;
|
|
34
|
-
onComplete(state: ColonyState): void;
|
|
36
|
+
onProgress?(metrics: ColonyMetrics): void;
|
|
37
|
+
onComplete?(state: ColonyState): void;
|
|
35
38
|
}
|
|
36
39
|
|
|
37
40
|
export interface QueenOptions {
|
|
@@ -176,13 +179,13 @@ async function runAntWave(opts: WaveOptions): Promise<"ok"> {
|
|
|
176
179
|
usage: { input: 0, output: 0, cost: 0, turns: 0 },
|
|
177
180
|
startedAt: Date.now(), finishedAt: null,
|
|
178
181
|
};
|
|
179
|
-
callbacks.onAntSpawn(ant, task);
|
|
182
|
+
callbacks.onAntSpawn?.(ant, task);
|
|
180
183
|
|
|
181
184
|
try {
|
|
182
185
|
const result = caste === "drone"
|
|
183
186
|
? await runDrone(cwd, nest, task)
|
|
184
187
|
: await spawnAnt(cwd, nest, task, config, signal, callbacks.onAntStream, opts.authStorage, opts.modelRegistry);
|
|
185
|
-
callbacks.onAntDone(result.ant, task, result.output);
|
|
188
|
+
callbacks.onAntDone?.(result.ant, task, result.output);
|
|
186
189
|
|
|
187
190
|
if (result.rateLimited) {
|
|
188
191
|
return "rate_limited";
|
|
@@ -207,7 +210,8 @@ async function runAntWave(opts: WaveOptions): Promise<"ok"> {
|
|
|
207
210
|
|
|
208
211
|
// 更新指标
|
|
209
212
|
const metrics = updateMetrics(nest);
|
|
210
|
-
callbacks.onProgress(metrics);
|
|
213
|
+
callbacks.onProgress?.(metrics);
|
|
214
|
+
emitSignal("working", `${metrics.tasksDone}/${metrics.tasksTotal} tasks done`);
|
|
211
215
|
|
|
212
216
|
return "done";
|
|
213
217
|
} catch (e) {
|
|
@@ -230,7 +234,7 @@ async function runAntWave(opts: WaveOptions): Promise<"ok"> {
|
|
|
230
234
|
|
|
231
235
|
// 429 退避:短暂等待后恢复,连续限流才逐步加长
|
|
232
236
|
if (backoffMs > 0) {
|
|
233
|
-
callbacks.onPhase("working", `Rate limited (429). Waiting ${Math.round(backoffMs / 1000)}s...`);
|
|
237
|
+
callbacks.onPhase?.("working", `Rate limited (429). Waiting ${Math.round(backoffMs / 1000)}s...`);
|
|
234
238
|
await new Promise(r => setTimeout(r, backoffMs));
|
|
235
239
|
}
|
|
236
240
|
|
|
@@ -349,9 +353,17 @@ export async function runColony(opts: QueenOptions): Promise<ColonyState> {
|
|
|
349
353
|
} catch { /* ignore */ }
|
|
350
354
|
};
|
|
351
355
|
|
|
356
|
+
const emitSignal = (phase: ColonyState["status"], message: string) => {
|
|
357
|
+
const m = nest.getState().metrics;
|
|
358
|
+
const active = nest.getState().ants.filter(a => a.status === "working").length;
|
|
359
|
+
const progress = m.tasksTotal > 0 ? m.tasksDone / m.tasksTotal : 0;
|
|
360
|
+
callbacks.onSignal?.({ phase, progress, active, cost: m.totalCost, message });
|
|
361
|
+
};
|
|
362
|
+
|
|
352
363
|
try {
|
|
353
364
|
// ═══ Phase 1: 侦察(快速单次,不再多轮接力) ═══
|
|
354
|
-
callbacks.onPhase("scouting", "Dispatching scout ant to explore codebase...");
|
|
365
|
+
callbacks.onPhase?.("scouting", "Dispatching scout ant to explore codebase...");
|
|
366
|
+
emitSignal("scouting", "Exploring codebase...");
|
|
355
367
|
await runAntWave({ ...waveBase, caste: "scout" });
|
|
356
368
|
|
|
357
369
|
let workerTasks = nest.getAllTasks().filter(t => (t.caste === "worker" || t.caste === "drone") && t.status === "pending");
|
|
@@ -380,7 +392,8 @@ export async function runColony(opts: QueenOptions): Promise<ColonyState> {
|
|
|
380
392
|
finishedAt: null,
|
|
381
393
|
};
|
|
382
394
|
nest.writeTask(relayTask);
|
|
383
|
-
callbacks.onPhase("scouting", "Scout relay: generating worker tasks...");
|
|
395
|
+
callbacks.onPhase?.("scouting", "Scout relay: generating worker tasks...");
|
|
396
|
+
emitSignal("scouting", "Retrying scout...");
|
|
384
397
|
await runAntWave({ ...waveBase, caste: "scout" });
|
|
385
398
|
workerTasks = nest.getAllTasks().filter(t => (t.caste === "worker" || t.caste === "drone") && t.status === "pending");
|
|
386
399
|
}
|
|
@@ -388,7 +401,8 @@ export async function runColony(opts: QueenOptions): Promise<ColonyState> {
|
|
|
388
401
|
if (workerTasks.length === 0) {
|
|
389
402
|
nest.updateState({ status: "failed", finishedAt: Date.now() });
|
|
390
403
|
const finalState = nest.getState();
|
|
391
|
-
callbacks.onComplete(finalState);
|
|
404
|
+
callbacks.onComplete?.(finalState);
|
|
405
|
+
emitSignal("failed", "No tasks generated");
|
|
392
406
|
return finalState;
|
|
393
407
|
}
|
|
394
408
|
|
|
@@ -408,11 +422,13 @@ export async function runColony(opts: QueenOptions): Promise<ColonyState> {
|
|
|
408
422
|
// 先执行 drone 任务(零 LLM 成本)
|
|
409
423
|
const droneTasks = nest.getAllTasks().filter(t => t.caste === "drone" && t.status === "pending");
|
|
410
424
|
if (droneTasks.length > 0) {
|
|
411
|
-
callbacks.onPhase("working", `${droneTasks.length} drone tasks. Executing rules...`);
|
|
425
|
+
callbacks.onPhase?.("working", `${droneTasks.length} drone tasks. Executing rules...`);
|
|
426
|
+
emitSignal("working", `${droneTasks.length} drone tasks`);
|
|
412
427
|
await runAntWave({ ...waveBase, caste: "drone" });
|
|
413
428
|
}
|
|
414
429
|
|
|
415
|
-
callbacks.onPhase("working", `${workerTasks.length} tasks discovered. Dispatching worker ants...`);
|
|
430
|
+
callbacks.onPhase?.("working", `${workerTasks.length} tasks discovered. Dispatching worker ants...`);
|
|
431
|
+
emitSignal("working", `${workerTasks.length} tasks to do`);
|
|
416
432
|
await runAntWave({ ...waveBase, caste: "worker" });
|
|
417
433
|
|
|
418
434
|
// 处理工蚁产生的子任务(可能有多轮)
|
|
@@ -427,7 +443,7 @@ export async function runColony(opts: QueenOptions): Promise<ColonyState> {
|
|
|
427
443
|
);
|
|
428
444
|
if (remaining.length === 0) break;
|
|
429
445
|
rounds++;
|
|
430
|
-
callbacks.onPhase("working", `Round ${rounds + 1}: ${remaining.length} sub-tasks from workers...`);
|
|
446
|
+
callbacks.onPhase?.("working", `Round ${rounds + 1}: ${remaining.length} sub-tasks from workers...`);
|
|
431
447
|
await runAntWave({ ...waveBase, caste: "worker" });
|
|
432
448
|
}
|
|
433
449
|
|
|
@@ -444,7 +460,8 @@ export async function runColony(opts: QueenOptions): Promise<ColonyState> {
|
|
|
444
460
|
const completedWorkerTasks = nest.getAllTasks().filter(t => t.caste === "worker" && t.status === "done");
|
|
445
461
|
if (completedWorkerTasks.length > 0 && (!tscPassed || completedWorkerTasks.length > 3)) {
|
|
446
462
|
nest.updateState({ status: "reviewing" });
|
|
447
|
-
callbacks.onPhase("reviewing", "Dispatching soldier ants to review changes...");
|
|
463
|
+
callbacks.onPhase?.("reviewing", "Dispatching soldier ants to review changes...");
|
|
464
|
+
emitSignal("reviewing", "Reviewing changes...");
|
|
448
465
|
const reviewTask = makeReviewTask(completedWorkerTasks);
|
|
449
466
|
nest.writeTask(reviewTask);
|
|
450
467
|
await runAntWave({ ...waveBase, caste: "soldier" });
|
|
@@ -455,7 +472,7 @@ export async function runColony(opts: QueenOptions): Promise<ColonyState> {
|
|
|
455
472
|
);
|
|
456
473
|
if (fixTasks.length > 0) {
|
|
457
474
|
nest.updateState({ status: "working" });
|
|
458
|
-
callbacks.onPhase("working", `${fixTasks.length} fix tasks from review. Dispatching workers...`);
|
|
475
|
+
callbacks.onPhase?.("working", `${fixTasks.length} fix tasks from review. Dispatching workers...`);
|
|
459
476
|
await runAntWave({ ...waveBase, caste: "worker" });
|
|
460
477
|
}
|
|
461
478
|
}
|
|
@@ -464,13 +481,15 @@ export async function runColony(opts: QueenOptions): Promise<ColonyState> {
|
|
|
464
481
|
const finalMetrics = updateMetrics(nest);
|
|
465
482
|
nest.updateState({ status: "done", finishedAt: Date.now(), metrics: finalMetrics });
|
|
466
483
|
const finalState = nest.getState();
|
|
467
|
-
callbacks.onComplete(finalState);
|
|
484
|
+
callbacks.onComplete?.(finalState);
|
|
485
|
+
emitSignal("done", `${finalMetrics.tasksDone}/${finalMetrics.tasksTotal} tasks done`);
|
|
468
486
|
return finalState;
|
|
469
487
|
|
|
470
488
|
} catch (e) {
|
|
471
489
|
nest.updateState({ status: "failed", finishedAt: Date.now() });
|
|
472
490
|
const failState = nest.getState();
|
|
473
|
-
callbacks.onComplete(failState);
|
|
491
|
+
callbacks.onComplete?.(failState);
|
|
492
|
+
emitSignal("failed", String(e).slice(0, 100));
|
|
474
493
|
return failState;
|
|
475
494
|
} finally {
|
|
476
495
|
cleanup();
|
|
@@ -132,3 +132,12 @@ export interface ColonyMetrics {
|
|
|
132
132
|
startTime: number;
|
|
133
133
|
throughputHistory: number[]; // tasks/min 滑动窗口
|
|
134
134
|
}
|
|
135
|
+
|
|
136
|
+
/** 蚁群抽象信号 — 观察者只需关注这一个 */
|
|
137
|
+
export interface ColonySignal {
|
|
138
|
+
phase: ColonyState["status"];
|
|
139
|
+
progress: number; // 0-1
|
|
140
|
+
active: number; // 当前工作中的蚂蚁数
|
|
141
|
+
cost: number;
|
|
142
|
+
message: string; // 一句话描述
|
|
143
|
+
}
|