oh-pi 0.1.28 → 0.1.30
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/dist/utils/install.js +8 -9
- package/dist/utils/resources.d.ts +8 -0
- package/dist/utils/resources.js +15 -0
- package/package.json +1 -1
- package/pi-package/extensions/ant-colony/index.ts +112 -67
- package/pi-package/extensions/ant-colony/nest.ts +11 -7
- package/pi-package/extensions/ant-colony/queen.ts +33 -27
- package/pi-package/extensions/ant-colony/spawner.ts +1 -0
- package/pi-package/extensions/ant-colony/types.ts +0 -55
package/dist/utils/install.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { writeFileSync, mkdirSync, readFileSync, copyFileSync, existsSync, readdirSync, statSync, rmSync } from "node:fs";
|
|
2
|
-
import { join
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
2
|
+
import { join } from "node:path";
|
|
4
3
|
import { homedir } from "node:os";
|
|
5
4
|
import { execSync } from "node:child_process";
|
|
6
5
|
import { KEYBINDING_SCHEMES, MODEL_CAPABILITIES, PROVIDERS } from "../types.js";
|
|
7
|
-
|
|
6
|
+
import { resources } from "./resources.js";
|
|
8
7
|
/**
|
|
9
8
|
* 确保目录存在,若不存在则递归创建
|
|
10
9
|
* @param dir - 目标目录路径
|
|
@@ -123,7 +122,7 @@ export function applyConfig(config) {
|
|
|
123
122
|
writeFileSync(join(agentDir, "keybindings.json"), JSON.stringify(kb, null, 2));
|
|
124
123
|
}
|
|
125
124
|
// 5. AGENTS.md
|
|
126
|
-
const agentsSrc =
|
|
125
|
+
const agentsSrc = resources.agent(config.agents);
|
|
127
126
|
try {
|
|
128
127
|
let content = readFileSync(agentsSrc, "utf8");
|
|
129
128
|
if (config.locale && config.locale !== "en") {
|
|
@@ -138,8 +137,8 @@ export function applyConfig(config) {
|
|
|
138
137
|
const extDir = join(agentDir, "extensions");
|
|
139
138
|
cleanDir(extDir);
|
|
140
139
|
for (const ext of config.extensions) {
|
|
141
|
-
const dirSrc =
|
|
142
|
-
const fileSrc =
|
|
140
|
+
const dirSrc = resources.extension(ext);
|
|
141
|
+
const fileSrc = resources.extensionFile(ext);
|
|
143
142
|
if (existsSync(dirSrc) && statSync(dirSrc).isDirectory()) {
|
|
144
143
|
copyDir(dirSrc, join(extDir, ext));
|
|
145
144
|
}
|
|
@@ -154,7 +153,7 @@ export function applyConfig(config) {
|
|
|
154
153
|
const promptDir = join(agentDir, "prompts");
|
|
155
154
|
cleanDir(promptDir);
|
|
156
155
|
for (const p of config.prompts) {
|
|
157
|
-
const src =
|
|
156
|
+
const src = resources.prompt(p);
|
|
158
157
|
try {
|
|
159
158
|
copyFileSync(src, join(promptDir, `${p}.md`));
|
|
160
159
|
}
|
|
@@ -164,7 +163,7 @@ export function applyConfig(config) {
|
|
|
164
163
|
const skillDir = join(agentDir, "skills");
|
|
165
164
|
cleanDir(skillDir);
|
|
166
165
|
for (const s of config.skills) {
|
|
167
|
-
const srcDir =
|
|
166
|
+
const srcDir = resources.skill(s);
|
|
168
167
|
const destDir = join(skillDir, s);
|
|
169
168
|
ensureDir(destDir);
|
|
170
169
|
try {
|
|
@@ -175,7 +174,7 @@ export function applyConfig(config) {
|
|
|
175
174
|
// 9. Copy themes (only custom ones)
|
|
176
175
|
const themeDir = join(agentDir, "themes");
|
|
177
176
|
cleanDir(themeDir);
|
|
178
|
-
const themeSrc =
|
|
177
|
+
const themeSrc = resources.theme(config.theme);
|
|
179
178
|
try {
|
|
180
179
|
copyFileSync(themeSrc, join(themeDir, `${config.theme}.json`));
|
|
181
180
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 资源路径解析 — 解耦 install.ts 对 pi-package/ 目录的硬编码引用
|
|
3
|
+
*/
|
|
4
|
+
import { join, dirname } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
const PKG_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
7
|
+
const RESOURCES = join(PKG_ROOT, "pi-package");
|
|
8
|
+
export const resources = {
|
|
9
|
+
agent: (name) => join(RESOURCES, "agents", `${name}.md`),
|
|
10
|
+
extension: (name) => join(RESOURCES, "extensions", name),
|
|
11
|
+
extensionFile: (name) => join(RESOURCES, "extensions", `${name}.ts`),
|
|
12
|
+
prompt: (name) => join(RESOURCES, "prompts", `${name}.md`),
|
|
13
|
+
skill: (name) => join(RESOURCES, "skills", name),
|
|
14
|
+
theme: (name) => join(RESOURCES, "themes", `${name}.json`),
|
|
15
|
+
};
|
package/package.json
CHANGED
|
@@ -10,11 +10,10 @@
|
|
|
10
10
|
import { readFileSync, appendFileSync, existsSync } from "node:fs";
|
|
11
11
|
import { join } from "node:path";
|
|
12
12
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
13
|
-
import { Text, Container, Spacer } from "@mariozechner/pi-tui";
|
|
13
|
+
import { Text, Container, Spacer, Box } from "@mariozechner/pi-tui";
|
|
14
14
|
import { Type } from "@sinclair/typebox";
|
|
15
15
|
import { runColony, type QueenCallbacks } from "./queen.js";
|
|
16
|
-
import type { ColonyState, ColonyMetrics, Ant, Task
|
|
17
|
-
import { resolveModelForCaste } from "./types.js";
|
|
16
|
+
import type { ColonyState, ColonyMetrics, Ant, Task } from "./types.js";
|
|
18
17
|
|
|
19
18
|
interface ColonyDetails {
|
|
20
19
|
state: ColonyState | null;
|
|
@@ -49,20 +48,34 @@ function casteIcon(caste: string): string {
|
|
|
49
48
|
return caste === "scout" ? "🔍" : caste === "soldier" ? "🛡️" : "⚒️";
|
|
50
49
|
}
|
|
51
50
|
|
|
51
|
+
/** 渲染进度条 ▓░ */
|
|
52
|
+
function progressBar(done: number, total: number, width: number, theme: any): string {
|
|
53
|
+
if (total === 0) return "";
|
|
54
|
+
const pct = Math.min(done / total, 1);
|
|
55
|
+
const filled = Math.round(pct * width);
|
|
56
|
+
const empty = width - filled;
|
|
57
|
+
const bar = theme.fg("success", "█".repeat(filled)) + theme.fg("muted", "░".repeat(empty));
|
|
58
|
+
return `${bar} ${theme.fg("accent", `${done}/${total}`)}`;
|
|
59
|
+
}
|
|
52
60
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
/** 渲染阶段流水线 scout → work → review → done */
|
|
62
|
+
function phasePipeline(status: string, theme: any): string {
|
|
63
|
+
const phases = [
|
|
64
|
+
{ key: "scouting", icon: "🔍", label: "Scout" },
|
|
65
|
+
{ key: "working", icon: "⚒️", label: "Work" },
|
|
66
|
+
{ key: "reviewing", icon: "🛡️", label: "Review" },
|
|
67
|
+
{ key: "done", icon: "✅", label: "Done" },
|
|
68
|
+
];
|
|
69
|
+
const idx = phases.findIndex(p => p.key === status);
|
|
70
|
+
return phases.map((p, i) => {
|
|
71
|
+
const label = `${p.icon} ${p.label}`;
|
|
72
|
+
if (i < idx) return theme.fg("success", label);
|
|
73
|
+
if (i === idx) return theme.fg("accent", theme.bold(label));
|
|
74
|
+
return theme.fg("muted", label);
|
|
75
|
+
}).join(theme.fg("muted", " → "));
|
|
64
76
|
}
|
|
65
77
|
|
|
78
|
+
|
|
66
79
|
export default function antColonyExtension(pi: ExtensionAPI) {
|
|
67
80
|
|
|
68
81
|
// ═══ Auto-trigger: 注入蚁群意识,LLM 自动判断何时启动 ═══
|
|
@@ -102,8 +115,14 @@ For simple single-file tasks, work directly without the colony.`,
|
|
|
102
115
|
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
103
116
|
const details: ColonyDetails = { state: null, phase: "initializing", log: [] };
|
|
104
117
|
|
|
105
|
-
//
|
|
106
|
-
const
|
|
118
|
+
// 所有蚂蚁统一使用当前会话模型
|
|
119
|
+
const currentModel = ctx.model?.id;
|
|
120
|
+
if (!currentModel) {
|
|
121
|
+
return {
|
|
122
|
+
content: [{ type: "text", text: "Colony failed: no model available in current session" }],
|
|
123
|
+
isError: true,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
107
126
|
|
|
108
127
|
const emit = () => {
|
|
109
128
|
const summary = details.state
|
|
@@ -155,8 +174,6 @@ For simple single-file tasks, work directly without the colony.`,
|
|
|
155
174
|
goal: params.goal,
|
|
156
175
|
maxAnts: params.maxAnts,
|
|
157
176
|
maxCost: params.maxCost,
|
|
158
|
-
modelOverrides,
|
|
159
|
-
availableModels: available,
|
|
160
177
|
currentModel,
|
|
161
178
|
signal: signal ?? undefined,
|
|
162
179
|
callbacks,
|
|
@@ -212,108 +229,136 @@ For simple single-file tasks, work directly without the colony.`,
|
|
|
212
229
|
// ═══ TUI Rendering ═══
|
|
213
230
|
|
|
214
231
|
renderCall(args, theme) {
|
|
215
|
-
|
|
216
|
-
text
|
|
217
|
-
|
|
218
|
-
text +=
|
|
219
|
-
|
|
220
|
-
if (args.maxCost) text += theme.fg("warning", ` (budget: $${args.maxCost})`);
|
|
232
|
+
const goal = args.goal?.length > 70 ? args.goal.slice(0, 67) + "..." : args.goal;
|
|
233
|
+
let text = theme.fg("toolTitle", theme.bold("🐜 ant_colony"));
|
|
234
|
+
if (args.maxAnts) text += theme.fg("muted", ` ×${args.maxAnts}`);
|
|
235
|
+
if (args.maxCost) text += theme.fg("warning", ` $${args.maxCost}`);
|
|
236
|
+
text += "\n" + theme.fg("dim", ` ${goal || "..."}`);
|
|
221
237
|
return new Text(text, 0, 0);
|
|
222
238
|
},
|
|
223
239
|
|
|
224
240
|
renderResult(result, { expanded }, theme) {
|
|
225
241
|
const details = result.details as ColonyDetails | undefined;
|
|
242
|
+
|
|
243
|
+
// ─── 运行中 ───
|
|
226
244
|
if (!details?.state) {
|
|
227
|
-
// Still running or no state
|
|
228
245
|
const log = details?.log ?? [];
|
|
229
|
-
|
|
230
|
-
|
|
246
|
+
const container = new Container();
|
|
247
|
+
container.addChild(new Text(
|
|
248
|
+
theme.fg("warning", "🐜 ") + theme.fg("toolTitle", theme.bold("Colony ")) +
|
|
249
|
+
theme.fg("accent", details?.phase || "initializing..."),
|
|
250
|
+
0, 0,
|
|
251
|
+
));
|
|
252
|
+
const recent = log.slice(expanded ? -20 : -5);
|
|
231
253
|
if (recent.length > 0) {
|
|
232
|
-
|
|
254
|
+
container.addChild(new Text(recent.map(l => theme.fg("dim", ` ${l}`)).join("\n"), 0, 0));
|
|
233
255
|
}
|
|
234
|
-
if (!expanded && log.length >
|
|
235
|
-
|
|
256
|
+
if (!expanded && log.length > 5) {
|
|
257
|
+
container.addChild(new Text(theme.fg("muted", ` ⋯ ${log.length - 5} more`), 0, 0));
|
|
236
258
|
}
|
|
237
|
-
return
|
|
259
|
+
return container;
|
|
238
260
|
}
|
|
239
261
|
|
|
240
262
|
const state = details.state;
|
|
241
263
|
const m = state.metrics;
|
|
242
|
-
const icon = state.status === "done" ? theme.fg("success", "✓") : theme.fg("error", "✗");
|
|
243
264
|
const elapsed = state.finishedAt ? formatDuration(state.finishedAt - state.createdAt) : "?";
|
|
265
|
+
const ok = state.status === "done";
|
|
244
266
|
|
|
267
|
+
// ─── 折叠视图 ───
|
|
245
268
|
if (!expanded) {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
269
|
+
const container = new Container();
|
|
270
|
+
|
|
271
|
+
// 标题行:状态 + 统计
|
|
272
|
+
const icon = ok ? theme.fg("success", "✓") : theme.fg("error", "✗");
|
|
273
|
+
container.addChild(new Text(
|
|
274
|
+
`${icon} ${theme.fg("toolTitle", theme.bold("ant colony "))}` +
|
|
275
|
+
theme.fg("muted", `${elapsed} │ `) +
|
|
276
|
+
theme.fg("accent", `${m.antsSpawned} ants`) +
|
|
277
|
+
theme.fg("muted", ` │ ${formatTokens(m.totalTokens)} │ ${formatCost(m.totalCost)}`),
|
|
278
|
+
0, 0,
|
|
279
|
+
));
|
|
280
|
+
|
|
281
|
+
// 进度条
|
|
282
|
+
container.addChild(new Text(` ${progressBar(m.tasksDone, m.tasksTotal, 20, theme)} ${theme.fg("muted", `(${m.tasksFailed} failed)`)}`, 0, 0));
|
|
283
|
+
|
|
284
|
+
// 任务列表(最多6条)
|
|
285
|
+
for (const t of state.tasks.slice(0, 6)) {
|
|
286
|
+
const ti = t.status === "done" ? theme.fg("success", "✓")
|
|
287
|
+
: t.status === "failed" ? theme.fg("error", "✗")
|
|
288
|
+
: theme.fg("muted", "○");
|
|
289
|
+
container.addChild(new Text(
|
|
290
|
+
` ${ti} ${theme.fg("dim", `${casteIcon(t.caste)}`)} ${t.title.slice(0, 60)}`,
|
|
291
|
+
0, 0,
|
|
292
|
+
));
|
|
255
293
|
}
|
|
256
|
-
if (state.tasks.length >
|
|
257
|
-
|
|
258
|
-
|
|
294
|
+
if (state.tasks.length > 6) {
|
|
295
|
+
container.addChild(new Text(theme.fg("muted", ` ⋯ +${state.tasks.length - 6} more (Ctrl+O)`), 0, 0));
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return container;
|
|
259
299
|
}
|
|
260
300
|
|
|
261
|
-
//
|
|
301
|
+
// ─── 展开视图 ───
|
|
262
302
|
const container = new Container();
|
|
303
|
+
|
|
304
|
+
// 标题 + 阶段流水线
|
|
305
|
+
const icon = ok ? theme.fg("success", "✓") : theme.fg("error", "✗");
|
|
263
306
|
container.addChild(new Text(
|
|
264
307
|
`${icon} ${theme.fg("toolTitle", theme.bold("ant colony "))}` +
|
|
265
308
|
theme.fg("accent", state.status) +
|
|
266
|
-
theme.fg("muted", `
|
|
309
|
+
theme.fg("muted", ` │ ${elapsed} │ ${formatCost(m.totalCost)} │ ${formatTokens(m.totalTokens)} tokens`),
|
|
267
310
|
0, 0,
|
|
268
311
|
));
|
|
269
|
-
container.addChild(new Text(
|
|
312
|
+
container.addChild(new Text(` ${phasePipeline(state.status, theme)}`, 0, 0));
|
|
313
|
+
container.addChild(new Text(theme.fg("dim", ` ${state.goal}`), 0, 0));
|
|
314
|
+
|
|
315
|
+
// 进度条
|
|
270
316
|
container.addChild(new Spacer(1));
|
|
317
|
+
container.addChild(new Text(` ${progressBar(m.tasksDone, m.tasksTotal, 30, theme)}`, 0, 0));
|
|
271
318
|
|
|
272
|
-
//
|
|
273
|
-
container.addChild(new
|
|
319
|
+
// 任务区
|
|
320
|
+
container.addChild(new Spacer(1));
|
|
321
|
+
container.addChild(new Text(theme.fg("muted", ` ─── Tasks (${m.tasksDone}/${m.tasksTotal}) ───`), 0, 0));
|
|
274
322
|
for (const t of state.tasks) {
|
|
275
323
|
const ti = t.status === "done" ? theme.fg("success", "✓")
|
|
276
324
|
: t.status === "failed" ? theme.fg("error", "✗")
|
|
277
|
-
: t.status === "active" ? theme.fg("warning", "
|
|
325
|
+
: t.status === "active" ? theme.fg("warning", "◉")
|
|
278
326
|
: theme.fg("muted", "○");
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
container.addChild(new Text(line, 0, 0));
|
|
327
|
+
const dur = (t.finishedAt && t.startedAt) ? theme.fg("dim", ` ${formatDuration(t.finishedAt - t.startedAt)}`) : "";
|
|
328
|
+
container.addChild(new Text(` ${ti} ${casteIcon(t.caste)} ${t.title}${dur}`, 0, 0));
|
|
282
329
|
if (t.status === "done" && t.result) {
|
|
283
|
-
|
|
284
|
-
container.addChild(new Text(theme.fg("dim", ` ${preview}`), 0, 0));
|
|
330
|
+
container.addChild(new Text(theme.fg("dim", ` ${t.result.split("\n")[0]?.slice(0, 100)}`), 0, 0));
|
|
285
331
|
}
|
|
286
332
|
if (t.status === "failed" && t.error) {
|
|
287
|
-
container.addChild(new Text(theme.fg("error", `
|
|
333
|
+
container.addChild(new Text(theme.fg("error", ` ${t.error.slice(0, 100)}`), 0, 0));
|
|
288
334
|
}
|
|
289
335
|
}
|
|
290
336
|
|
|
291
|
-
//
|
|
337
|
+
// 蚂蚁区
|
|
292
338
|
container.addChild(new Spacer(1));
|
|
293
|
-
container.addChild(new Text(theme.fg("muted",
|
|
339
|
+
container.addChild(new Text(theme.fg("muted", ` ─── Ants (${m.antsSpawned}) ───`), 0, 0));
|
|
294
340
|
for (const a of state.ants) {
|
|
295
|
-
const ai = a.status === "done" ? theme.fg("success", "✓") : a.status === "failed" ? theme.fg("error", "✗") : theme.fg("warning", "
|
|
341
|
+
const ai = a.status === "done" ? theme.fg("success", "✓") : a.status === "failed" ? theme.fg("error", "✗") : theme.fg("warning", "◉");
|
|
296
342
|
const dur = a.finishedAt ? formatDuration(a.finishedAt - a.startedAt) : "...";
|
|
297
343
|
container.addChild(new Text(
|
|
298
|
-
|
|
344
|
+
` ${ai} ${casteIcon(a.caste)} ${theme.fg("accent", a.id)} ${theme.fg("dim", `${dur} │ ${formatCost(a.usage.cost)} │ ${a.usage.turns}t`)}`,
|
|
299
345
|
0, 0,
|
|
300
346
|
));
|
|
301
347
|
}
|
|
302
348
|
|
|
303
|
-
//
|
|
349
|
+
// 并发 + 日志
|
|
304
350
|
container.addChild(new Spacer(1));
|
|
305
351
|
const c = state.concurrency;
|
|
306
352
|
container.addChild(new Text(
|
|
307
|
-
theme.fg("muted",
|
|
308
|
-
theme.fg("dim", `current: ${c.current}
|
|
353
|
+
theme.fg("muted", ` ─── Concurrency ───`) + "\n" +
|
|
354
|
+
theme.fg("dim", ` current: ${c.current} │ optimal: ${c.optimal} │ range: ${c.min}-${c.max}`),
|
|
309
355
|
0, 0,
|
|
310
356
|
));
|
|
311
357
|
|
|
312
|
-
// Activity log
|
|
313
358
|
container.addChild(new Spacer(1));
|
|
314
|
-
container.addChild(new Text(theme.fg("muted",
|
|
315
|
-
for (const l of details.log.slice(-
|
|
316
|
-
container.addChild(new Text(theme.fg("dim", l), 0, 0));
|
|
359
|
+
container.addChild(new Text(theme.fg("muted", ` ─── Log ───`), 0, 0));
|
|
360
|
+
for (const l of details.log.slice(-15)) {
|
|
361
|
+
container.addChild(new Text(theme.fg("dim", ` ${l}`), 0, 0));
|
|
317
362
|
}
|
|
318
363
|
|
|
319
364
|
return container;
|
|
@@ -80,7 +80,7 @@ export class Nest {
|
|
|
80
80
|
.map(f => this.readJson<Task>(path.join(this.tasksDir, f)));
|
|
81
81
|
for (const t of tasks) this.taskCache.set(t.id, t);
|
|
82
82
|
return tasks;
|
|
83
|
-
} catch { return []; }
|
|
83
|
+
} catch (e) { console.error("[nest] failed to read tasks dir:", e); return []; }
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
claimTask(taskId: string, antId: string): boolean {
|
|
@@ -217,7 +217,7 @@ export class Nest {
|
|
|
217
217
|
|
|
218
218
|
private withStateLock<T>(fn: () => T): T {
|
|
219
219
|
const MAX_WAIT = 5000;
|
|
220
|
-
const SPIN_MS =
|
|
220
|
+
const SPIN_MS = 15;
|
|
221
221
|
const start = Date.now();
|
|
222
222
|
while (true) {
|
|
223
223
|
try {
|
|
@@ -225,18 +225,22 @@ export class Nest {
|
|
|
225
225
|
break;
|
|
226
226
|
} catch {
|
|
227
227
|
if (Date.now() - start > MAX_WAIT) {
|
|
228
|
-
//
|
|
229
|
-
try {
|
|
228
|
+
// 超时:检查锁持有者是否存活
|
|
229
|
+
try {
|
|
230
|
+
const holder = parseInt(fs.readFileSync(this.lockFile, "utf-8"), 10);
|
|
231
|
+
try { process.kill(holder, 0); } catch { /* 进程已死,清除死锁 */ fs.unlinkSync(this.lockFile); continue; }
|
|
232
|
+
} catch { /* 读取失败,强制清除 */ try { fs.unlinkSync(this.lockFile); } catch {} }
|
|
230
233
|
continue;
|
|
231
234
|
}
|
|
232
|
-
|
|
233
|
-
|
|
235
|
+
// 简单 busy-wait,避免 SharedArrayBuffer 依赖
|
|
236
|
+
const until = Date.now() + SPIN_MS + Math.random() * SPIN_MS;
|
|
237
|
+
while (Date.now() < until) { /* spin */ }
|
|
234
238
|
}
|
|
235
239
|
}
|
|
236
240
|
try {
|
|
237
241
|
return fn();
|
|
238
242
|
} finally {
|
|
239
|
-
try { fs.unlinkSync(this.lockFile); } catch { /*
|
|
243
|
+
try { fs.unlinkSync(this.lockFile); } catch { /* lock already removed */ }
|
|
240
244
|
}
|
|
241
245
|
}
|
|
242
246
|
|
|
@@ -16,9 +16,9 @@ import * as fs from "node:fs";
|
|
|
16
16
|
import * as path from "node:path";
|
|
17
17
|
import type {
|
|
18
18
|
ColonyState, Task, Ant, AntCaste, ColonyMetrics,
|
|
19
|
-
ConcurrencyConfig, TaskPriority, ModelOverrides,
|
|
19
|
+
ConcurrencyConfig, TaskPriority, ModelOverrides,
|
|
20
20
|
} from "./types.js";
|
|
21
|
-
import { DEFAULT_ANT_CONFIGS
|
|
21
|
+
import { DEFAULT_ANT_CONFIGS } from "./types.js";
|
|
22
22
|
import { Nest } from "./nest.js";
|
|
23
23
|
import { spawnAnt, makeTaskId } from "./spawner.js";
|
|
24
24
|
import { adapt, sampleSystem, defaultConcurrency } from "./concurrency.js";
|
|
@@ -36,9 +36,7 @@ export interface QueenOptions {
|
|
|
36
36
|
goal: string;
|
|
37
37
|
maxAnts?: number;
|
|
38
38
|
maxCost?: number;
|
|
39
|
-
|
|
40
|
-
availableModels?: AvailableModel[];
|
|
41
|
-
currentModel?: string;
|
|
39
|
+
currentModel: string;
|
|
42
40
|
signal?: AbortSignal;
|
|
43
41
|
callbacks: QueenCallbacks;
|
|
44
42
|
}
|
|
@@ -139,25 +137,18 @@ interface WaveOptions {
|
|
|
139
137
|
nest: Nest;
|
|
140
138
|
cwd: string;
|
|
141
139
|
caste: AntCaste;
|
|
140
|
+
currentModel: string;
|
|
142
141
|
signal?: AbortSignal;
|
|
143
142
|
callbacks: QueenCallbacks;
|
|
144
|
-
modelOverrides?: ModelOverrides;
|
|
145
143
|
maxCost?: number;
|
|
146
|
-
availableModels?: AvailableModel[];
|
|
147
|
-
currentModel?: string;
|
|
148
144
|
}
|
|
149
145
|
|
|
150
146
|
/**
|
|
151
147
|
* 并发执行一批蚂蚁,自适应调节并发度
|
|
152
148
|
*/
|
|
153
149
|
async function runAntWave(opts: WaveOptions): Promise<"ok" | "budget_exceeded"> {
|
|
154
|
-
const { nest, cwd, caste, signal, callbacks,
|
|
155
|
-
const config = { ...DEFAULT_ANT_CONFIGS[caste] };
|
|
156
|
-
if (modelOverrides?.[caste]) {
|
|
157
|
-
config.model = modelOverrides[caste];
|
|
158
|
-
} else if (!config.model && availableModels?.length) {
|
|
159
|
-
config.model = resolveModelForCaste(caste, availableModels, currentModel) ?? "";
|
|
160
|
-
}
|
|
150
|
+
const { nest, cwd, caste, signal, callbacks, maxCost, currentModel } = opts;
|
|
151
|
+
const config = { ...DEFAULT_ANT_CONFIGS[caste], model: currentModel };
|
|
161
152
|
|
|
162
153
|
let backoffMs = 0; // 429 退避时间
|
|
163
154
|
let consecutiveRateLimits = 0; // 连续限流计数
|
|
@@ -327,7 +318,7 @@ export async function runColony(opts: QueenOptions): Promise<ColonyState> {
|
|
|
327
318
|
startTime: Date.now(), throughputHistory: [],
|
|
328
319
|
},
|
|
329
320
|
maxCost: opts.maxCost ?? null,
|
|
330
|
-
modelOverrides:
|
|
321
|
+
modelOverrides: {},
|
|
331
322
|
createdAt: Date.now(),
|
|
332
323
|
finishedAt: null,
|
|
333
324
|
};
|
|
@@ -340,8 +331,7 @@ export async function runColony(opts: QueenOptions): Promise<ColonyState> {
|
|
|
340
331
|
const { signal, callbacks } = opts;
|
|
341
332
|
const waveBase: Omit<WaveOptions, "caste"> = {
|
|
342
333
|
nest, cwd: opts.cwd, signal, callbacks,
|
|
343
|
-
|
|
344
|
-
availableModels: opts.availableModels, currentModel: opts.currentModel,
|
|
334
|
+
maxCost: opts.maxCost, currentModel: opts.currentModel,
|
|
345
335
|
};
|
|
346
336
|
|
|
347
337
|
const cleanup = () => {
|
|
@@ -363,16 +353,32 @@ export async function runColony(opts: QueenOptions): Promise<ColonyState> {
|
|
|
363
353
|
};
|
|
364
354
|
|
|
365
355
|
try {
|
|
366
|
-
// ═══ Phase 1:
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
356
|
+
// ═══ Phase 1: 侦察(最多重试2次) ═══
|
|
357
|
+
let scoutAttempt = 0;
|
|
358
|
+
const MAX_SCOUT_RETRIES = 2;
|
|
359
|
+
let workerTasks: Task[] = [];
|
|
360
|
+
|
|
361
|
+
while (scoutAttempt <= MAX_SCOUT_RETRIES) {
|
|
362
|
+
callbacks.onPhase("scouting", scoutAttempt === 0
|
|
363
|
+
? "Dispatching scout ants to explore codebase..."
|
|
364
|
+
: `Scout retry ${scoutAttempt}/${MAX_SCOUT_RETRIES}...`);
|
|
365
|
+
|
|
366
|
+
if (await runAntWave({ ...waveBase, caste: "scout" }) === "budget_exceeded")
|
|
367
|
+
return budgetStop("Budget exceeded during scouting");
|
|
368
|
+
|
|
369
|
+
workerTasks = nest.getAllTasks().filter(t => t.caste === "worker" && t.status === "pending");
|
|
370
|
+
if (workerTasks.length > 0) break;
|
|
371
|
+
|
|
372
|
+
scoutAttempt++;
|
|
373
|
+
if (scoutAttempt <= MAX_SCOUT_RETRIES) {
|
|
374
|
+
// 重置失败的 scout 任务为 pending 以便重试
|
|
375
|
+
for (const t of nest.getAllTasks().filter(t => t.caste === "scout" && (t.status === "done" || t.status === "failed"))) {
|
|
376
|
+
nest.updateTaskStatus(t.id, "pending");
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
374
381
|
if (workerTasks.length === 0) {
|
|
375
|
-
// 侦察蚁没产生任务,标记失败
|
|
376
382
|
nest.updateState({ status: "failed", finishedAt: Date.now() });
|
|
377
383
|
const finalState = nest.getState();
|
|
378
384
|
callbacks.onComplete(finalState);
|
|
@@ -19,64 +19,9 @@ export const DEFAULT_ANT_CONFIGS: Record<AntCaste, Omit<AntConfig, "systemPrompt
|
|
|
19
19
|
soldier: { caste: "soldier", model: "", tools: ["read", "bash", "grep", "find", "ls"], maxTurns: 8 },
|
|
20
20
|
};
|
|
21
21
|
|
|
22
|
-
// ═══ 模型层级 ═══
|
|
23
|
-
export type ModelTier = "fast" | "balanced" | "powerful";
|
|
24
|
-
|
|
25
|
-
export const CASTE_MODEL_TIER: Record<AntCaste, ModelTier> = {
|
|
26
|
-
scout: "fast",
|
|
27
|
-
worker: "powerful",
|
|
28
|
-
soldier: "balanced",
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
export const MODEL_TIER_KEYWORDS: Record<ModelTier, string[]> = {
|
|
32
|
-
fast: ["haiku", "mini", "flash", "nano", "small"],
|
|
33
|
-
balanced: ["sonnet", "gpt-4o", "pro"],
|
|
34
|
-
powerful: ["opus", "o1", "o3", "deepthink"],
|
|
35
|
-
};
|
|
36
|
-
|
|
37
22
|
/** Per-caste model overrides from user config */
|
|
38
23
|
export type ModelOverrides = Partial<Record<AntCaste, string>>;
|
|
39
24
|
|
|
40
|
-
export interface AvailableModel {
|
|
41
|
-
id: string;
|
|
42
|
-
cost?: { input: number };
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* 根据 caste 的 ModelTier 从可用模型中匹配最合适的模型
|
|
47
|
-
* - fast(侦察蚁):关键词匹配轻量模型,优先同 provider
|
|
48
|
-
* - balanced/powerful(工蚁/兵蚁):优先用当前会话模型,否则关键词匹配
|
|
49
|
-
* - fallback:按 cost 排序选择
|
|
50
|
-
*/
|
|
51
|
-
export function resolveModelForCaste(
|
|
52
|
-
caste: AntCaste,
|
|
53
|
-
available: AvailableModel[],
|
|
54
|
-
currentModel?: string,
|
|
55
|
-
): string | undefined {
|
|
56
|
-
if (available.length === 0) return currentModel;
|
|
57
|
-
const tier = CASTE_MODEL_TIER[caste];
|
|
58
|
-
|
|
59
|
-
// 工蚁/兵蚁优先使用当前会话模型
|
|
60
|
-
if (tier !== "fast" && currentModel) return currentModel;
|
|
61
|
-
|
|
62
|
-
const keywords = MODEL_TIER_KEYWORDS[tier];
|
|
63
|
-
// 优先匹配同 provider(从 currentModel 提取 provider 前缀)
|
|
64
|
-
const provider = currentModel?.split("-")[0];
|
|
65
|
-
const sameProvider = provider ? available.filter(m => m.id.toLowerCase().startsWith(provider)) : [];
|
|
66
|
-
const pool = sameProvider.length > 0 ? sameProvider : available;
|
|
67
|
-
const match = pool.find(m => keywords.some(k => m.id.toLowerCase().includes(k)));
|
|
68
|
-
if (match) return match.id;
|
|
69
|
-
// 扩大搜索范围
|
|
70
|
-
if (sameProvider.length > 0) {
|
|
71
|
-
const allMatch = available.find(m => keywords.some(k => m.id.toLowerCase().includes(k)));
|
|
72
|
-
if (allMatch) return allMatch.id;
|
|
73
|
-
}
|
|
74
|
-
// Fallback:按 cost 排序
|
|
75
|
-
const sorted = [...available].sort((a, b) => (a.cost?.input ?? 0) - (b.cost?.input ?? 0));
|
|
76
|
-
if (tier === "fast") return currentModel ?? sorted[0].id;
|
|
77
|
-
return currentModel ?? sorted[sorted.length - 1].id;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
25
|
// ═══ 任务 (Food Source) ═══
|
|
81
26
|
export type TaskStatus = "pending" | "claimed" | "active" | "done" | "failed" | "blocked";
|
|
82
27
|
export type TaskPriority = 1 | 2 | 3 | 4 | 5; // 1=highest
|