oh-pi 0.1.48 → 0.1.49
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
|
@@ -14,10 +14,10 @@ const CPU_CORES = os.cpus().length;
|
|
|
14
14
|
|
|
15
15
|
export function defaultConcurrency(): ConcurrencyConfig {
|
|
16
16
|
return {
|
|
17
|
-
current:
|
|
17
|
+
current: 2,
|
|
18
18
|
min: 1,
|
|
19
19
|
max: Math.min(CPU_CORES, 8),
|
|
20
|
-
optimal:
|
|
20
|
+
optimal: 3,
|
|
21
21
|
history: [],
|
|
22
22
|
};
|
|
23
23
|
}
|
|
@@ -68,8 +68,8 @@ export function adapt(config: ConcurrencyConfig, pendingTasks: number): Concurre
|
|
|
68
68
|
const taskCap = Math.min(pendingTasks, config.max);
|
|
69
69
|
|
|
70
70
|
if (samples.length < 3) {
|
|
71
|
-
//
|
|
72
|
-
next.current = Math.min(2, taskCap);
|
|
71
|
+
// 冷启动:直接给一半 max,快速利用并发
|
|
72
|
+
next.current = Math.min(Math.ceil(config.max / 2), taskCap);
|
|
73
73
|
return next;
|
|
74
74
|
}
|
|
75
75
|
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import { readFileSync, appendFileSync, existsSync } from "node:fs";
|
|
13
13
|
import { join } from "node:path";
|
|
14
14
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
15
|
-
import { Text, Container, Spacer } from "@mariozechner/pi-tui";
|
|
15
|
+
import { Text, Container, Spacer, matchesKey } from "@mariozechner/pi-tui";
|
|
16
16
|
import { Type } from "@sinclair/typebox";
|
|
17
17
|
import { runColony, type QueenCallbacks } from "./queen.js";
|
|
18
18
|
import type { ColonyState, ColonyMetrics, AntStreamEvent } from "./types.js";
|
|
@@ -46,30 +46,6 @@ function casteIcon(caste: string): string {
|
|
|
46
46
|
return caste === "scout" ? "🔍" : caste === "soldier" ? "🛡️" : "⚒️";
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
function progressBar(done: number, total: number, width: number, theme: any): string {
|
|
50
|
-
if (total === 0) return "";
|
|
51
|
-
const pct = Math.min(done / total, 1);
|
|
52
|
-
const filled = Math.round(pct * width);
|
|
53
|
-
const empty = width - filled;
|
|
54
|
-
return theme.fg("success", "█".repeat(filled)) + theme.fg("muted", "░".repeat(empty)) + " " + theme.fg("accent", `${done}/${total}`);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function phasePipeline(status: string, theme: any): string {
|
|
58
|
-
const phases = [
|
|
59
|
-
{ key: "scouting", icon: "🔍", label: "Scout" },
|
|
60
|
-
{ key: "working", icon: "⚒️", label: "Work" },
|
|
61
|
-
{ key: "reviewing", icon: "🛡️", label: "Review" },
|
|
62
|
-
{ key: "done", icon: "✅", label: "Done" },
|
|
63
|
-
];
|
|
64
|
-
const idx = phases.findIndex(p => p.key === status);
|
|
65
|
-
return phases.map((p, i) => {
|
|
66
|
-
const label = `${p.icon} ${p.label}`;
|
|
67
|
-
if (i < idx) return theme.fg("success", label);
|
|
68
|
-
if (i === idx) return theme.fg("accent", theme.bold(label));
|
|
69
|
-
return theme.fg("muted", label);
|
|
70
|
-
}).join(theme.fg("muted", " → "));
|
|
71
|
-
}
|
|
72
|
-
|
|
73
49
|
// ═══ Background colony state ═══
|
|
74
50
|
|
|
75
51
|
interface AntStreamState {
|
|
@@ -94,64 +70,41 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
94
70
|
// 当前运行中的后台蚁群(同时只允许一个)
|
|
95
71
|
let activeColony: BackgroundColony | null = null;
|
|
96
72
|
|
|
97
|
-
// ───
|
|
73
|
+
// ─── Status 渲染 ───
|
|
98
74
|
|
|
99
75
|
let lastRender = 0;
|
|
100
76
|
const throttledRender = () => {
|
|
101
77
|
const now = Date.now();
|
|
102
|
-
if (now - lastRender <
|
|
78
|
+
if (now - lastRender < 500) return;
|
|
103
79
|
lastRender = now;
|
|
104
|
-
|
|
105
|
-
renderStatus();
|
|
80
|
+
pi.events.emit("ant-colony:render");
|
|
106
81
|
};
|
|
107
82
|
|
|
108
|
-
|
|
109
|
-
if (!activeColony) return;
|
|
110
|
-
const { state, phase, antStreams } = activeColony;
|
|
111
|
-
const streams = Array.from(antStreams.values());
|
|
112
|
-
const lines: string[] = [];
|
|
113
|
-
|
|
114
|
-
const elapsed = state ? formatDuration(Date.now() - state.createdAt) : "0s";
|
|
115
|
-
const cost = state ? formatCost(state.metrics.totalCost) : "$0";
|
|
116
|
-
lines.push(`🐜 Colony: ${phase} │ ${elapsed} │ ${cost}`);
|
|
117
|
-
|
|
118
|
-
if (state && state.metrics.tasksTotal > 0) {
|
|
119
|
-
const m = state.metrics;
|
|
120
|
-
const pct = Math.round((m.tasksDone / m.tasksTotal) * 100);
|
|
121
|
-
const filled = Math.round(pct / 5);
|
|
122
|
-
lines.push(` ${"█".repeat(filled)}${"░".repeat(20 - filled)} ${m.tasksDone}/${m.tasksTotal} (${pct}%)`);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
for (const s of streams.slice(-4)) {
|
|
126
|
-
const icon = casteIcon(s.caste);
|
|
127
|
-
const line = s.lastLine.length > 60 ? s.lastLine.slice(0, 57) + "..." : s.lastLine;
|
|
128
|
-
lines.push(` ${icon} ${s.antId.slice(0, 15)} ▸ ${line || "..."}`);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
pi.events.emit("ant-colony:widget", lines);
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
const renderStatus = () => {
|
|
135
|
-
if (!activeColony) return;
|
|
136
|
-
const { state, antStreams } = activeColony;
|
|
137
|
-
if (!state) return;
|
|
138
|
-
const m = state.metrics;
|
|
139
|
-
const active = antStreams.size;
|
|
140
|
-
pi.events.emit("ant-colony:status",
|
|
141
|
-
`🐜 ${statusIcon(state.status)} ${m.tasksDone}/${m.tasksTotal} tasks │ ${active} active │ ${formatCost(m.totalCost)}`
|
|
142
|
-
);
|
|
143
|
-
};
|
|
144
|
-
|
|
145
|
-
// 监听自己的事件来更新 UI(确保在有 ctx 的上下文中)
|
|
83
|
+
// 监听事件来更新 UI(确保在有 ctx 的上下文中)
|
|
146
84
|
pi.on("session_start", async (_event, ctx) => {
|
|
147
|
-
pi.events.on("ant-colony:
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
85
|
+
pi.events.on("ant-colony:render", () => {
|
|
86
|
+
if (!activeColony) return;
|
|
87
|
+
const { state, antStreams } = activeColony;
|
|
88
|
+
const active = antStreams.size;
|
|
89
|
+
const elapsed = state ? formatDuration(Date.now() - state.createdAt) : "0s";
|
|
90
|
+
const m = state?.metrics;
|
|
91
|
+
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
|
+
|
|
96
|
+
const parts = [`🐜 ${statusIcon(colonyStatus)}`];
|
|
97
|
+
if (m) parts.push(`${m.tasksDone}/${m.tasksTotal}`);
|
|
98
|
+
parts.push(`${active}⚡`);
|
|
99
|
+
parts.push(`${turns}↻`);
|
|
100
|
+
parts.push(formatTokens(outTok) + "↑");
|
|
101
|
+
if (m) parts.push(formatCost(m.totalCost));
|
|
102
|
+
parts.push(elapsed);
|
|
103
|
+
|
|
104
|
+
ctx.ui.setStatus("ant-colony", parts.join(" │ "));
|
|
152
105
|
});
|
|
106
|
+
|
|
153
107
|
pi.events.on("ant-colony:clear-ui", () => {
|
|
154
|
-
ctx.ui.setWidget("ant-colony", undefined);
|
|
155
108
|
ctx.ui.setStatus("ant-colony", undefined);
|
|
156
109
|
});
|
|
157
110
|
pi.events.on("ant-colony:notify", (data: { msg: string; level: "info" | "success" | "warning" | "error" }) => {
|
|
@@ -361,7 +314,7 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
361
314
|
// 注入结果到对话
|
|
362
315
|
pi.sendMessage({
|
|
363
316
|
customType: "ant-colony-report",
|
|
364
|
-
content: report
|
|
317
|
+
content: `[COLONY_SIGNAL:COMPLETE]\n${report}`,
|
|
365
318
|
display: true,
|
|
366
319
|
}, { triggerTurn: true, deliverAs: "followUp" });
|
|
367
320
|
|
|
@@ -373,6 +326,11 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
373
326
|
pi.events.emit("ant-colony:clear-ui");
|
|
374
327
|
activeColony = null;
|
|
375
328
|
pi.events.emit("ant-colony:notify", { msg: `🐜 Colony crashed: ${e}`, level: "error" });
|
|
329
|
+
pi.sendMessage({
|
|
330
|
+
customType: "ant-colony-report",
|
|
331
|
+
content: `[COLONY_SIGNAL:FAILED]\n## 🐜 Colony Crashed\n${e}`,
|
|
332
|
+
display: true,
|
|
333
|
+
}, { triggerTurn: true, deliverAs: "followUp" });
|
|
376
334
|
});
|
|
377
335
|
}
|
|
378
336
|
|
|
@@ -390,7 +348,7 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
390
348
|
} catch { /* ignore */ }
|
|
391
349
|
|
|
392
350
|
const colonyStatus = activeColony
|
|
393
|
-
? `\n[
|
|
351
|
+
? `\n[COLONY_SIGNAL:RUNNING] A colony is currently running in the background for goal: "${activeColony.goal.slice(0, 100)}". Do NOT launch another colony. Do NOT check progress or assume failure. You will receive [COLONY_SIGNAL:COMPLETE] automatically when it finishes. If the user asks about colony status, tell them it's still running. Use /colony-stop to cancel.`
|
|
394
352
|
: "";
|
|
395
353
|
|
|
396
354
|
return {
|
|
@@ -405,8 +363,17 @@ You have the ant_colony tool. Use it automatically when:
|
|
|
405
363
|
Do NOT ask for confirmation. Call ant_colony directly with a clear goal.
|
|
406
364
|
For simple single-file tasks, work directly without the colony.
|
|
407
365
|
|
|
408
|
-
|
|
409
|
-
|
|
366
|
+
[COLONY SIGNAL PROTOCOL]
|
|
367
|
+
The colony communicates via signals. You MUST obey these:
|
|
368
|
+
- [COLONY_SIGNAL:LAUNCHED] — Colony started. Do NOT poll, check, or assume failure. Wait for completion signal.
|
|
369
|
+
- [COLONY_SIGNAL:RUNNING] — Colony is active. Do NOT launch another or check progress.
|
|
370
|
+
- [COLONY_SIGNAL:COMPLETE] — Colony finished. Review the report and summarize results to the user.
|
|
371
|
+
- [COLONY_SIGNAL:FAILED] — Colony crashed. Report the error to the user.
|
|
372
|
+
|
|
373
|
+
After launching a colony, your ONLY correct behavior is:
|
|
374
|
+
1. Tell the user the colony is running
|
|
375
|
+
2. Continue chatting about OTHER topics if the user asks
|
|
376
|
+
3. Wait for [COLONY_SIGNAL:COMPLETE] or [COLONY_SIGNAL:FAILED] — do NOT guess the outcome
|
|
410
377
|
${modelList ? `
|
|
411
378
|
[COLONY MODEL SELECTION]
|
|
412
379
|
Available models: ${modelList}
|
|
@@ -482,7 +449,7 @@ Strategy for choosing per-caste models:
|
|
|
482
449
|
launchBackgroundColony(colonyParams);
|
|
483
450
|
|
|
484
451
|
return {
|
|
485
|
-
content: [{ type: "text", text:
|
|
452
|
+
content: [{ type: "text", text: `[COLONY_SIGNAL:LAUNCHED]\n🐜 Colony launched in background.\nGoal: ${params.goal}\n\n⚠️ IMPORTANT: The colony is now running autonomously. Do NOT check progress, do NOT ask about status, do NOT assume failure. You will receive a [COLONY_SIGNAL:COMPLETE] message automatically when it finishes. Continue chatting about other topics or wait silently.` }],
|
|
486
453
|
};
|
|
487
454
|
},
|
|
488
455
|
|
|
@@ -508,7 +475,7 @@ Strategy for choosing per-caste models:
|
|
|
508
475
|
));
|
|
509
476
|
if (activeColony) {
|
|
510
477
|
container.addChild(new Text(theme.fg("dim", ` Goal: ${activeColony.goal.slice(0, 70)}`), 0, 0));
|
|
511
|
-
container.addChild(new Text(theme.fg("muted", `
|
|
478
|
+
container.addChild(new Text(theme.fg("muted", ` Ctrl+Shift+A for details │ /colony-stop to cancel`), 0, 0));
|
|
512
479
|
}
|
|
513
480
|
return container;
|
|
514
481
|
},
|
|
@@ -550,6 +517,99 @@ Strategy for choosing per-caste models:
|
|
|
550
517
|
return container;
|
|
551
518
|
});
|
|
552
519
|
|
|
520
|
+
// ═══ Shortcut: Ctrl+Shift+A 展开蚁群详情 ═══
|
|
521
|
+
pi.registerShortcut("ctrl+shift+a", {
|
|
522
|
+
description: "Show ant colony details",
|
|
523
|
+
async handler(ctx) {
|
|
524
|
+
if (!activeColony) {
|
|
525
|
+
ctx.ui.notify("No colony is currently running.", "info");
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
await ctx.ui.custom<void>((tui, theme, _kb, done) => {
|
|
530
|
+
let cachedWidth: number | undefined;
|
|
531
|
+
let cachedLines: string[] | undefined;
|
|
532
|
+
|
|
533
|
+
const buildLines = (width: number): string[] => {
|
|
534
|
+
const c = activeColony;
|
|
535
|
+
if (!c) return [theme.fg("muted", " No colony running.")];
|
|
536
|
+
|
|
537
|
+
const lines: string[] = [];
|
|
538
|
+
const w = width - 2; // padding
|
|
539
|
+
|
|
540
|
+
// ── Header ──
|
|
541
|
+
const elapsed = c.state ? formatDuration(Date.now() - c.state.createdAt) : "0s";
|
|
542
|
+
const cost = c.state ? formatCost(c.state.metrics.totalCost) : "$0";
|
|
543
|
+
lines.push(theme.fg("accent", theme.bold(` 🐜 Colony Details`)) + theme.fg("muted", ` │ ${elapsed} │ ${cost}`));
|
|
544
|
+
lines.push(theme.fg("dim", ` Goal: ${c.goal.slice(0, w - 8)}`));
|
|
545
|
+
lines.push("");
|
|
546
|
+
|
|
547
|
+
// ── Tasks ──
|
|
548
|
+
const tasks = c.state?.tasks || [];
|
|
549
|
+
if (tasks.length > 0) {
|
|
550
|
+
lines.push(theme.fg("accent", " Tasks"));
|
|
551
|
+
for (const t of tasks.slice(0, 15)) {
|
|
552
|
+
const icon = t.status === "done" ? theme.fg("success", "✓")
|
|
553
|
+
: t.status === "failed" ? theme.fg("error", "✗")
|
|
554
|
+
: t.status === "active" ? theme.fg("warning", "●")
|
|
555
|
+
: theme.fg("dim", "○");
|
|
556
|
+
const dur = t.finishedAt && t.startedAt ? theme.fg("dim", ` ${formatDuration(t.finishedAt - t.startedAt)}`) : "";
|
|
557
|
+
lines.push(` ${icon} ${casteIcon(t.caste)} ${theme.fg("text", t.title.slice(0, w - 12))}${dur}`);
|
|
558
|
+
}
|
|
559
|
+
if (tasks.length > 15) lines.push(theme.fg("muted", ` ⋯ +${tasks.length - 15} more`));
|
|
560
|
+
lines.push("");
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// ── Active Ants ──
|
|
564
|
+
const streams = Array.from(c.antStreams.values());
|
|
565
|
+
if (streams.length > 0) {
|
|
566
|
+
lines.push(theme.fg("accent", " Active Ants"));
|
|
567
|
+
for (const s of streams) {
|
|
568
|
+
const line = s.lastLine.length > w - 20 ? s.lastLine.slice(0, w - 23) + "..." : s.lastLine;
|
|
569
|
+
lines.push(` ${casteIcon(s.caste)} ${theme.fg("accent", s.antId.slice(0, 14))} ${theme.fg("dim", `${s.tokens}tok`)} ${theme.fg("muted", "▸")} ${line}`);
|
|
570
|
+
}
|
|
571
|
+
lines.push("");
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// ── Log (last 8) ──
|
|
575
|
+
if (c.log.length > 0) {
|
|
576
|
+
lines.push(theme.fg("accent", " Log"));
|
|
577
|
+
for (const l of c.log.slice(-8)) {
|
|
578
|
+
lines.push(theme.fg("dim", ` ${l.slice(0, w - 2)}`));
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
lines.push("");
|
|
583
|
+
lines.push(theme.fg("muted", " esc close"));
|
|
584
|
+
return lines;
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
// 定时刷新
|
|
588
|
+
const timer = setInterval(() => {
|
|
589
|
+
cachedWidth = undefined;
|
|
590
|
+
cachedLines = undefined;
|
|
591
|
+
tui.requestRender();
|
|
592
|
+
}, 1000);
|
|
593
|
+
|
|
594
|
+
return {
|
|
595
|
+
render(width: number): string[] {
|
|
596
|
+
if (cachedLines && cachedWidth === width) return cachedLines;
|
|
597
|
+
cachedLines = buildLines(width);
|
|
598
|
+
cachedWidth = width;
|
|
599
|
+
return cachedLines;
|
|
600
|
+
},
|
|
601
|
+
invalidate() { cachedWidth = undefined; cachedLines = undefined; },
|
|
602
|
+
handleInput(data: string) {
|
|
603
|
+
if (matchesKey(data, "escape")) {
|
|
604
|
+
clearInterval(timer);
|
|
605
|
+
done(undefined);
|
|
606
|
+
}
|
|
607
|
+
},
|
|
608
|
+
};
|
|
609
|
+
}, { overlay: true, overlayOptions: { anchor: "center", width: "80%", maxHeight: "80%" } });
|
|
610
|
+
},
|
|
611
|
+
});
|
|
612
|
+
|
|
553
613
|
// ═══ Command: /colony-stop ═══
|
|
554
614
|
pi.registerCommand("colony-stop", {
|
|
555
615
|
description: "Stop the running background colony",
|
|
@@ -21,6 +21,7 @@ export class Nest {
|
|
|
21
21
|
private pheromoneCache: Pheromone[] = [];
|
|
22
22
|
private pheromoneOffset: number = 0;
|
|
23
23
|
private taskCache: Map<string, Task> = new Map();
|
|
24
|
+
private stateCache: ColonyState | null = null;
|
|
24
25
|
|
|
25
26
|
constructor(private cwd: string, private colonyId: string) {
|
|
26
27
|
this.dir = path.join(cwd, ".ant-colony", colonyId);
|
|
@@ -35,13 +36,16 @@ export class Nest {
|
|
|
35
36
|
|
|
36
37
|
init(state: ColonyState): void {
|
|
37
38
|
this.writeJson(this.stateFile, state);
|
|
39
|
+
this.stateCache = state;
|
|
38
40
|
this.taskCache.clear();
|
|
39
41
|
for (const t of state.tasks) this.writeTask(t);
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
getState(): ColonyState {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
+
if (!this.stateCache) {
|
|
46
|
+
this.stateCache = this.readJson<ColonyState>(this.stateFile);
|
|
47
|
+
}
|
|
48
|
+
const base = { ...this.stateCache };
|
|
45
49
|
base.tasks = this.getAllTasks();
|
|
46
50
|
base.pheromones = this.getAllPheromones();
|
|
47
51
|
return base;
|
|
@@ -49,9 +53,11 @@ export class Nest {
|
|
|
49
53
|
|
|
50
54
|
updateState(patch: Partial<Pick<ColonyState, "status" | "concurrency" | "metrics" | "ants" | "finishedAt">>): void {
|
|
51
55
|
this.withStateLock(() => {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
56
|
+
if (!this.stateCache) {
|
|
57
|
+
this.stateCache = this.readJson<ColonyState>(this.stateFile);
|
|
58
|
+
}
|
|
59
|
+
Object.assign(this.stateCache, patch);
|
|
60
|
+
this.writeJson(this.stateFile, this.stateCache);
|
|
55
61
|
});
|
|
56
62
|
}
|
|
57
63
|
|
|
@@ -186,11 +192,13 @@ export class Nest {
|
|
|
186
192
|
|
|
187
193
|
updateAnt(ant: Ant): void {
|
|
188
194
|
this.withStateLock(() => {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
195
|
+
if (!this.stateCache) {
|
|
196
|
+
this.stateCache = this.readJson<ColonyState>(this.stateFile);
|
|
197
|
+
}
|
|
198
|
+
const idx = this.stateCache.ants.findIndex(a => a.id === ant.id);
|
|
199
|
+
if (idx >= 0) this.stateCache.ants[idx] = ant;
|
|
200
|
+
else this.stateCache.ants.push(ant);
|
|
201
|
+
this.writeJson(this.stateFile, this.stateCache);
|
|
194
202
|
});
|
|
195
203
|
}
|
|
196
204
|
|
|
@@ -198,12 +206,14 @@ export class Nest {
|
|
|
198
206
|
|
|
199
207
|
recordSample(sample: ConcurrencySample): void {
|
|
200
208
|
this.withStateLock(() => {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
209
|
+
if (!this.stateCache) {
|
|
210
|
+
this.stateCache = this.readJson<ColonyState>(this.stateFile);
|
|
211
|
+
}
|
|
212
|
+
this.stateCache.concurrency.history.push(sample);
|
|
213
|
+
if (this.stateCache.concurrency.history.length > 30) {
|
|
214
|
+
this.stateCache.concurrency.history = this.stateCache.concurrency.history.slice(-30);
|
|
205
215
|
}
|
|
206
|
-
this.writeJson(this.stateFile,
|
|
216
|
+
this.writeJson(this.stateFile, this.stateCache);
|
|
207
217
|
});
|
|
208
218
|
}
|
|
209
219
|
|
|
@@ -236,9 +236,9 @@ async function runAntWave(opts: WaveOptions): Promise<"ok"> {
|
|
|
236
236
|
}
|
|
237
237
|
}
|
|
238
238
|
|
|
239
|
-
// 自适应并发(每
|
|
239
|
+
// 自适应并发(每 2000ms 采样一次)
|
|
240
240
|
const now = Date.now();
|
|
241
|
-
if (now - lastSampleTime >=
|
|
241
|
+
if (now - lastSampleTime >= 2000) {
|
|
242
242
|
lastSampleTime = now;
|
|
243
243
|
const completedRecently = state.tasks.filter(t =>
|
|
244
244
|
t.status === "done" && t.finishedAt && t.finishedAt > now - 120000
|
|
@@ -260,7 +260,7 @@ async function runAntWave(opts: WaveOptions): Promise<"ok"> {
|
|
|
260
260
|
|
|
261
261
|
if (slotsAvailable === 0) {
|
|
262
262
|
// 等待一下再检查
|
|
263
|
-
await new Promise(r => setTimeout(r,
|
|
263
|
+
await new Promise(r => setTimeout(r, 500));
|
|
264
264
|
continue;
|
|
265
265
|
}
|
|
266
266
|
|
|
@@ -336,51 +336,39 @@ export async function runColony(opts: QueenOptions): Promise<ColonyState> {
|
|
|
336
336
|
};
|
|
337
337
|
|
|
338
338
|
try {
|
|
339
|
-
// ═══ Phase 1:
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
let workerTasks: Task[] = [];
|
|
339
|
+
// ═══ Phase 1: 侦察(快速单次,不再多轮接力) ═══
|
|
340
|
+
callbacks.onPhase("scouting", "Dispatching scout ant to explore codebase...");
|
|
341
|
+
await runAntWave({ ...waveBase, caste: "scout" });
|
|
343
342
|
|
|
344
|
-
|
|
345
|
-
callbacks.onPhase("scouting", scoutAttempt === 0
|
|
346
|
-
? "Dispatching scout ants to explore codebase..."
|
|
347
|
-
: `Scout relay ${scoutAttempt}/${MAX_SCOUT_RETRIES} (building on previous discoveries)...`);
|
|
343
|
+
let workerTasks = nest.getAllTasks().filter(t => t.caste === "worker" && t.status === "pending");
|
|
348
344
|
|
|
345
|
+
// 只在完全没有 worker 任务时才重试一次
|
|
346
|
+
if (workerTasks.length === 0) {
|
|
347
|
+
const pheromones = nest.getAllPheromones();
|
|
348
|
+
const hasDiscoveries = pheromones.some(p => p.type === "discovery");
|
|
349
|
+
const relayTask: Task = {
|
|
350
|
+
id: makeTaskId(),
|
|
351
|
+
parentId: null,
|
|
352
|
+
title: "Scout relay: generate worker tasks",
|
|
353
|
+
description: hasDiscoveries
|
|
354
|
+
? `Previous scout found information but didn't generate worker tasks. Generate concrete worker tasks based on discoveries.\n\nGoal:\n${opts.goal}`
|
|
355
|
+
: `Explore the codebase for this goal and generate worker tasks:\n\n${opts.goal}`,
|
|
356
|
+
caste: "scout",
|
|
357
|
+
status: "pending",
|
|
358
|
+
priority: 1,
|
|
359
|
+
files: [],
|
|
360
|
+
claimedBy: null,
|
|
361
|
+
result: null,
|
|
362
|
+
error: null,
|
|
363
|
+
spawnedTasks: [],
|
|
364
|
+
createdAt: Date.now(),
|
|
365
|
+
startedAt: null,
|
|
366
|
+
finishedAt: null,
|
|
367
|
+
};
|
|
368
|
+
nest.writeTask(relayTask);
|
|
369
|
+
callbacks.onPhase("scouting", "Scout relay: generating worker tasks...");
|
|
349
370
|
await runAntWave({ ...waveBase, caste: "scout" });
|
|
350
|
-
|
|
351
371
|
workerTasks = nest.getAllTasks().filter(t => t.caste === "worker" && t.status === "pending");
|
|
352
|
-
if (workerTasks.length > 0) break;
|
|
353
|
-
|
|
354
|
-
scoutAttempt++;
|
|
355
|
-
if (scoutAttempt <= MAX_SCOUT_RETRIES) {
|
|
356
|
-
// 接力:检查是否有信息素(前一只 scout 的部分发现)
|
|
357
|
-
const pheromones = nest.getAllPheromones();
|
|
358
|
-
const hasDiscoveries = pheromones.some(p => p.type === "discovery");
|
|
359
|
-
|
|
360
|
-
// 创建接力 scout 任务(而非重置旧任务)
|
|
361
|
-
const relayDescription = hasDiscoveries
|
|
362
|
-
? `Continue exploring the codebase. Previous scouts made partial discoveries (see pheromone trail). Focus on areas NOT yet explored and generate worker tasks.\n\nOriginal goal:\n${opts.goal}`
|
|
363
|
-
: `Explore the codebase and identify all files, modules, and dependencies relevant to this goal:\n\n${opts.goal}\n\nBe thorough. The colony depends on your intelligence.`;
|
|
364
|
-
|
|
365
|
-
const relayTask: Task = {
|
|
366
|
-
id: makeTaskId(),
|
|
367
|
-
parentId: null,
|
|
368
|
-
title: hasDiscoveries ? "Scout relay: continue exploration" : "Scout: explore codebase for goal",
|
|
369
|
-
description: relayDescription,
|
|
370
|
-
caste: "scout",
|
|
371
|
-
status: "pending",
|
|
372
|
-
priority: 1,
|
|
373
|
-
files: [],
|
|
374
|
-
claimedBy: null,
|
|
375
|
-
result: null,
|
|
376
|
-
error: null,
|
|
377
|
-
spawnedTasks: [],
|
|
378
|
-
createdAt: Date.now(),
|
|
379
|
-
startedAt: null,
|
|
380
|
-
finishedAt: null,
|
|
381
|
-
};
|
|
382
|
-
nest.writeTask(relayTask);
|
|
383
|
-
}
|
|
384
372
|
}
|
|
385
373
|
|
|
386
374
|
if (workerTasks.length === 0) {
|