oh-pi 0.1.58 → 0.1.60
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 {
|
|
@@ -156,7 +159,7 @@ interface WaveOptions {
|
|
|
156
159
|
/**
|
|
157
160
|
* 并发执行一批蚂蚁,自适应调节并发度
|
|
158
161
|
*/
|
|
159
|
-
async function runAntWave(opts: WaveOptions): Promise<"ok"> {
|
|
162
|
+
async function runAntWave(opts: WaveOptions): Promise<"ok" | "budget"> {
|
|
160
163
|
const { nest, cwd, caste, signal, callbacks, currentModel } = opts;
|
|
161
164
|
const casteModel = opts.modelOverrides?.[caste] || currentModel;
|
|
162
165
|
const config = { ...DEFAULT_ANT_CONFIGS[caste], model: casteModel };
|
|
@@ -165,7 +168,19 @@ async function runAntWave(opts: WaveOptions): Promise<"ok"> {
|
|
|
165
168
|
let consecutiveRateLimits = 0; // 连续限流计数
|
|
166
169
|
const retriedTasks = new Set<string>(); // 防止重复重试
|
|
167
170
|
|
|
168
|
-
const runOne = async (): Promise<"done" | "empty" | "rate_limited"> => {
|
|
171
|
+
const runOne = async (): Promise<"done" | "empty" | "rate_limited" | "budget"> => {
|
|
172
|
+
// Budget 刹车:剩余预算不够一只蚂蚁的预估成本就不出发
|
|
173
|
+
const state = nest.getState();
|
|
174
|
+
if (state.maxCost != null && caste !== "drone") {
|
|
175
|
+
const spent = state.ants.reduce((s, a) => s + a.usage.cost, 0);
|
|
176
|
+
const remaining = state.maxCost - spent;
|
|
177
|
+
const doneAnts = state.ants.filter(a => a.status === "done" && a.usage.cost > 0);
|
|
178
|
+
const avgCost = doneAnts.length > 0
|
|
179
|
+
? doneAnts.reduce((s, a) => s + a.usage.cost, 0) / doneAnts.length
|
|
180
|
+
: 0.05;
|
|
181
|
+
if (remaining < avgCost * 1.5) return "budget";
|
|
182
|
+
}
|
|
183
|
+
|
|
169
184
|
const task = nest.nextPendingTask(caste);
|
|
170
185
|
if (!task) return "empty";
|
|
171
186
|
if (!nest.claimTask(task.id, "queen")) return "empty";
|
|
@@ -176,13 +191,13 @@ async function runAntWave(opts: WaveOptions): Promise<"ok"> {
|
|
|
176
191
|
usage: { input: 0, output: 0, cost: 0, turns: 0 },
|
|
177
192
|
startedAt: Date.now(), finishedAt: null,
|
|
178
193
|
};
|
|
179
|
-
callbacks.onAntSpawn(ant, task);
|
|
194
|
+
callbacks.onAntSpawn?.(ant, task);
|
|
180
195
|
|
|
181
196
|
try {
|
|
182
197
|
const result = caste === "drone"
|
|
183
198
|
? await runDrone(cwd, nest, task)
|
|
184
199
|
: await spawnAnt(cwd, nest, task, config, signal, callbacks.onAntStream, opts.authStorage, opts.modelRegistry);
|
|
185
|
-
callbacks.onAntDone(result.ant, task, result.output);
|
|
200
|
+
callbacks.onAntDone?.(result.ant, task, result.output);
|
|
186
201
|
|
|
187
202
|
if (result.rateLimited) {
|
|
188
203
|
return "rate_limited";
|
|
@@ -207,7 +222,8 @@ async function runAntWave(opts: WaveOptions): Promise<"ok"> {
|
|
|
207
222
|
|
|
208
223
|
// 更新指标
|
|
209
224
|
const metrics = updateMetrics(nest);
|
|
210
|
-
callbacks.onProgress(metrics);
|
|
225
|
+
callbacks.onProgress?.(metrics);
|
|
226
|
+
emitSignal("working", `${metrics.tasksDone}/${metrics.tasksTotal} tasks done`);
|
|
211
227
|
|
|
212
228
|
return "done";
|
|
213
229
|
} catch (e) {
|
|
@@ -230,7 +246,7 @@ async function runAntWave(opts: WaveOptions): Promise<"ok"> {
|
|
|
230
246
|
|
|
231
247
|
// 429 退避:短暂等待后恢复,连续限流才逐步加长
|
|
232
248
|
if (backoffMs > 0) {
|
|
233
|
-
callbacks.onPhase("working", `Rate limited (429). Waiting ${Math.round(backoffMs / 1000)}s...`);
|
|
249
|
+
callbacks.onPhase?.("working", `Rate limited (429). Waiting ${Math.round(backoffMs / 1000)}s...`);
|
|
234
250
|
await new Promise(r => setTimeout(r, backoffMs));
|
|
235
251
|
}
|
|
236
252
|
|
|
@@ -276,12 +292,16 @@ async function runAntWave(opts: WaveOptions): Promise<"ok"> {
|
|
|
276
292
|
}
|
|
277
293
|
|
|
278
294
|
const batch = Math.min(slotsAvailable, pending.length);
|
|
279
|
-
const promises: Promise<"done" | "empty" | "rate_limited">[] = [];
|
|
295
|
+
const promises: Promise<"done" | "empty" | "rate_limited" | "budget">[] = [];
|
|
280
296
|
for (let i = 0; i < batch; i++) {
|
|
281
297
|
promises.push(runOne());
|
|
282
298
|
}
|
|
283
299
|
const results = await Promise.all(promises);
|
|
284
300
|
|
|
301
|
+
if (results.includes("budget")) {
|
|
302
|
+
return "budget";
|
|
303
|
+
}
|
|
304
|
+
|
|
285
305
|
// 429 处理:降低并发 + 渐进退避(2s → 5s → 10s,上限 10s)
|
|
286
306
|
if (results.includes("rate_limited")) {
|
|
287
307
|
consecutiveRateLimits++;
|
|
@@ -349,9 +369,17 @@ export async function runColony(opts: QueenOptions): Promise<ColonyState> {
|
|
|
349
369
|
} catch { /* ignore */ }
|
|
350
370
|
};
|
|
351
371
|
|
|
372
|
+
const emitSignal = (phase: ColonyState["status"], message: string) => {
|
|
373
|
+
const m = nest.getState().metrics;
|
|
374
|
+
const active = nest.getState().ants.filter(a => a.status === "working").length;
|
|
375
|
+
const progress = m.tasksTotal > 0 ? m.tasksDone / m.tasksTotal : 0;
|
|
376
|
+
callbacks.onSignal?.({ phase, progress, active, cost: m.totalCost, message });
|
|
377
|
+
};
|
|
378
|
+
|
|
352
379
|
try {
|
|
353
380
|
// ═══ Phase 1: 侦察(快速单次,不再多轮接力) ═══
|
|
354
|
-
callbacks.onPhase("scouting", "Dispatching scout ant to explore codebase...");
|
|
381
|
+
callbacks.onPhase?.("scouting", "Dispatching scout ant to explore codebase...");
|
|
382
|
+
emitSignal("scouting", "Exploring codebase...");
|
|
355
383
|
await runAntWave({ ...waveBase, caste: "scout" });
|
|
356
384
|
|
|
357
385
|
let workerTasks = nest.getAllTasks().filter(t => (t.caste === "worker" || t.caste === "drone") && t.status === "pending");
|
|
@@ -380,7 +408,8 @@ export async function runColony(opts: QueenOptions): Promise<ColonyState> {
|
|
|
380
408
|
finishedAt: null,
|
|
381
409
|
};
|
|
382
410
|
nest.writeTask(relayTask);
|
|
383
|
-
callbacks.onPhase("scouting", "Scout relay: generating worker tasks...");
|
|
411
|
+
callbacks.onPhase?.("scouting", "Scout relay: generating worker tasks...");
|
|
412
|
+
emitSignal("scouting", "Retrying scout...");
|
|
384
413
|
await runAntWave({ ...waveBase, caste: "scout" });
|
|
385
414
|
workerTasks = nest.getAllTasks().filter(t => (t.caste === "worker" || t.caste === "drone") && t.status === "pending");
|
|
386
415
|
}
|
|
@@ -388,7 +417,8 @@ export async function runColony(opts: QueenOptions): Promise<ColonyState> {
|
|
|
388
417
|
if (workerTasks.length === 0) {
|
|
389
418
|
nest.updateState({ status: "failed", finishedAt: Date.now() });
|
|
390
419
|
const finalState = nest.getState();
|
|
391
|
-
callbacks.onComplete(finalState);
|
|
420
|
+
callbacks.onComplete?.(finalState);
|
|
421
|
+
emitSignal("failed", "No tasks generated");
|
|
392
422
|
return finalState;
|
|
393
423
|
}
|
|
394
424
|
|
|
@@ -408,16 +438,17 @@ export async function runColony(opts: QueenOptions): Promise<ColonyState> {
|
|
|
408
438
|
// 先执行 drone 任务(零 LLM 成本)
|
|
409
439
|
const droneTasks = nest.getAllTasks().filter(t => t.caste === "drone" && t.status === "pending");
|
|
410
440
|
if (droneTasks.length > 0) {
|
|
411
|
-
callbacks.onPhase("working", `${droneTasks.length} drone tasks. Executing rules...`);
|
|
441
|
+
callbacks.onPhase?.("working", `${droneTasks.length} drone tasks. Executing rules...`);
|
|
442
|
+
emitSignal("working", `${droneTasks.length} drone tasks`);
|
|
412
443
|
await runAntWave({ ...waveBase, caste: "drone" });
|
|
413
444
|
}
|
|
414
445
|
|
|
415
|
-
callbacks.onPhase("working", `${workerTasks.length} tasks discovered. Dispatching worker ants...`);
|
|
446
|
+
callbacks.onPhase?.("working", `${workerTasks.length} tasks discovered. Dispatching worker ants...`);
|
|
447
|
+
emitSignal("working", `${workerTasks.length} tasks to do`);
|
|
416
448
|
await runAntWave({ ...waveBase, caste: "worker" });
|
|
417
449
|
|
|
418
|
-
//
|
|
419
|
-
|
|
420
|
-
while (rounds < 3) {
|
|
450
|
+
// 处理工蚁产生的子任务(budget 驱动,无硬限制)
|
|
451
|
+
while (true) {
|
|
421
452
|
// 先跑 drone 子任务
|
|
422
453
|
const pendingDrones = nest.getAllTasks().filter(t => t.caste === "drone" && t.status === "pending");
|
|
423
454
|
if (pendingDrones.length > 0) await runAntWave({ ...waveBase, caste: "drone" });
|
|
@@ -426,9 +457,15 @@ export async function runColony(opts: QueenOptions): Promise<ColonyState> {
|
|
|
426
457
|
t.caste === "worker" && (t.status === "pending" || t.status === "blocked")
|
|
427
458
|
);
|
|
428
459
|
if (remaining.length === 0) break;
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
460
|
+
callbacks.onPhase?.("working", `${remaining.length} sub-tasks from workers...`);
|
|
461
|
+
const result = await runAntWave({ ...waveBase, caste: "worker" });
|
|
462
|
+
if (result === "budget") {
|
|
463
|
+
nest.updateState({ status: "budget_exceeded", finishedAt: Date.now() });
|
|
464
|
+
emitSignal("budget_exceeded", "Budget exhausted");
|
|
465
|
+
const budgetState = nest.getState();
|
|
466
|
+
callbacks.onComplete?.(budgetState);
|
|
467
|
+
return budgetState;
|
|
468
|
+
}
|
|
432
469
|
}
|
|
433
470
|
|
|
434
471
|
// ═══ Auto-check: run tsc before soldier review ═══
|
|
@@ -444,7 +481,8 @@ export async function runColony(opts: QueenOptions): Promise<ColonyState> {
|
|
|
444
481
|
const completedWorkerTasks = nest.getAllTasks().filter(t => t.caste === "worker" && t.status === "done");
|
|
445
482
|
if (completedWorkerTasks.length > 0 && (!tscPassed || completedWorkerTasks.length > 3)) {
|
|
446
483
|
nest.updateState({ status: "reviewing" });
|
|
447
|
-
callbacks.onPhase("reviewing", "Dispatching soldier ants to review changes...");
|
|
484
|
+
callbacks.onPhase?.("reviewing", "Dispatching soldier ants to review changes...");
|
|
485
|
+
emitSignal("reviewing", "Reviewing changes...");
|
|
448
486
|
const reviewTask = makeReviewTask(completedWorkerTasks);
|
|
449
487
|
nest.writeTask(reviewTask);
|
|
450
488
|
await runAntWave({ ...waveBase, caste: "soldier" });
|
|
@@ -455,7 +493,7 @@ export async function runColony(opts: QueenOptions): Promise<ColonyState> {
|
|
|
455
493
|
);
|
|
456
494
|
if (fixTasks.length > 0) {
|
|
457
495
|
nest.updateState({ status: "working" });
|
|
458
|
-
callbacks.onPhase("working", `${fixTasks.length} fix tasks from review. Dispatching workers...`);
|
|
496
|
+
callbacks.onPhase?.("working", `${fixTasks.length} fix tasks from review. Dispatching workers...`);
|
|
459
497
|
await runAntWave({ ...waveBase, caste: "worker" });
|
|
460
498
|
}
|
|
461
499
|
}
|
|
@@ -464,13 +502,15 @@ export async function runColony(opts: QueenOptions): Promise<ColonyState> {
|
|
|
464
502
|
const finalMetrics = updateMetrics(nest);
|
|
465
503
|
nest.updateState({ status: "done", finishedAt: Date.now(), metrics: finalMetrics });
|
|
466
504
|
const finalState = nest.getState();
|
|
467
|
-
callbacks.onComplete(finalState);
|
|
505
|
+
callbacks.onComplete?.(finalState);
|
|
506
|
+
emitSignal("done", `${finalMetrics.tasksDone}/${finalMetrics.tasksTotal} tasks done`);
|
|
468
507
|
return finalState;
|
|
469
508
|
|
|
470
509
|
} catch (e) {
|
|
471
510
|
nest.updateState({ status: "failed", finishedAt: Date.now() });
|
|
472
511
|
const failState = nest.getState();
|
|
473
|
-
callbacks.onComplete(failState);
|
|
512
|
+
callbacks.onComplete?.(failState);
|
|
513
|
+
emitSignal("failed", String(e).slice(0, 100));
|
|
474
514
|
return failState;
|
|
475
515
|
} finally {
|
|
476
516
|
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
|
+
}
|