oh-pi 0.1.29 → 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.
@@ -1,10 +1,9 @@
1
1
  import { writeFileSync, mkdirSync, readFileSync, copyFileSync, existsSync, readdirSync, statSync, rmSync } from "node:fs";
2
- import { join, dirname } from "node:path";
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
- const PKG_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
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 = join(PKG_ROOT, "pi-package", "agents", `${config.agents}.md`);
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 = join(PKG_ROOT, "pi-package", "extensions", ext);
142
- const fileSrc = join(PKG_ROOT, "pi-package", "extensions", `${ext}.ts`);
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 = join(PKG_ROOT, "pi-package", "prompts", `${p}.md`);
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 = join(PKG_ROOT, "pi-package", "skills", s);
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 = join(PKG_ROOT, "pi-package", "themes", `${config.theme}.json`);
177
+ const themeSrc = resources.theme(config.theme);
179
178
  try {
180
179
  copyFileSync(themeSrc, join(themeDir, `${config.theme}.json`));
181
180
  }
@@ -0,0 +1,8 @@
1
+ export declare const resources: {
2
+ agent: (name: string) => string;
3
+ extension: (name: string) => string;
4
+ extensionFile: (name: string) => string;
5
+ prompt: (name: string) => string;
6
+ skill: (name: string) => string;
7
+ theme: (name: string) => string;
8
+ };
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-pi",
3
- "version": "0.1.29",
3
+ "version": "0.1.30",
4
4
  "description": "One-click setup for pi-coding-agent. Like oh-my-zsh for pi.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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, AvailableModel } from "./types.js";
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
- function resolveModels(ctx: { modelRegistry: { getAvailable(): AvailableModel[] }; model?: { id: string } | undefined }): { overrides: Record<string, string>; available: AvailableModel[]; currentModel?: string } {
54
- const overrides: Record<string, string> = {};
55
- const current = ctx.model?.id;
56
- const available = ctx.modelRegistry.getAvailable();
57
-
58
- for (const caste of ["scout", "worker", "soldier"] as const) {
59
- const resolved = resolveModelForCaste(caste, available, current);
60
- if (resolved) overrides[caste] = resolved;
61
- }
62
-
63
- return { overrides, available, currentModel: current };
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
- // Resolve models: smart discovery from registry
106
- const { overrides: modelOverrides, available, currentModel } = resolveModels(ctx);
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
- let text = theme.fg("toolTitle", theme.bold("ant_colony "));
216
- text += theme.fg("accent", "🐜");
217
- const goal = args.goal?.length > 60 ? args.goal.slice(0, 57) + "..." : args.goal;
218
- text += "\n " + theme.fg("dim", goal || "...");
219
- if (args.maxAnts) text += theme.fg("muted", ` (max: ${args.maxAnts})`);
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
- let text = theme.fg("warning", "🐜 ") + theme.fg("toolTitle", details?.phase || "initializing...");
230
- const recent = log.slice(expanded ? -30 : -8);
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
- text += "\n" + recent.map(l => theme.fg("dim", l)).join("\n");
254
+ container.addChild(new Text(recent.map(l => theme.fg("dim", ` ${l}`)).join("\n"), 0, 0));
233
255
  }
234
- if (!expanded && log.length > 8) {
235
- text += "\n" + theme.fg("muted", `... ${log.length - 8} more (Ctrl+O to expand)`);
256
+ if (!expanded && log.length > 5) {
257
+ container.addChild(new Text(theme.fg("muted", ` ⋯ ${log.length - 5} more`), 0, 0));
236
258
  }
237
- return new Text(text, 0, 0);
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
- let text = `${icon} ${theme.fg("toolTitle", theme.bold("ant colony "))}`;
247
- text += theme.fg("accent", `${m.tasksDone}/${m.tasksTotal} tasks`);
248
- text += theme.fg("muted", ` | ${m.antsSpawned} ants | ${elapsed} | ${formatCost(m.totalCost)}`);
249
- text += theme.fg("muted", ` | peak ×${state.concurrency.optimal}`);
250
-
251
- // Compact task list
252
- for (const t of state.tasks.slice(0, 5)) {
253
- const ti = t.status === "done" ? theme.fg("success", "✓") : t.status === "failed" ? theme.fg("error", "✗") : theme.fg("muted", "○");
254
- text += `\n ${ti} ${theme.fg("dim", `[${t.caste}]`)} ${t.title.slice(0, 60)}`;
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 > 5) text += `\n ${theme.fg("muted", `... +${state.tasks.length - 5} more`)}`;
257
- text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`;
258
- return new Text(text, 0, 0);
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
- // Expanded view
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", ` | ${elapsed} | ${formatCost(m.totalCost)} | ${formatTokens(m.totalTokens)} tokens | peak ×${state.concurrency.optimal}`),
309
+ theme.fg("muted", ` ${elapsed} ${formatCost(m.totalCost)} ${formatTokens(m.totalTokens)} tokens`),
267
310
  0, 0,
268
311
  ));
269
- container.addChild(new Text(theme.fg("dim", state.goal), 0, 0));
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
- // Tasks
273
- container.addChild(new Text(theme.fg("muted", `─── Tasks (${m.tasksDone}/${m.tasksTotal}) ───`), 0, 0));
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
- let line = `${ti} ${theme.fg("accent", `[${t.caste}]`)} ${t.title}`;
280
- if (t.finishedAt && t.startedAt) line += theme.fg("dim", ` (${formatDuration(t.finishedAt - t.startedAt)})`);
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
- const preview = t.result.split("\n").slice(0, 2).join("\n").slice(0, 120);
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", ` ${t.error.slice(0, 120)}`), 0, 0));
333
+ container.addChild(new Text(theme.fg("error", ` ${t.error.slice(0, 100)}`), 0, 0));
288
334
  }
289
335
  }
290
336
 
291
- // Ants
337
+ // 蚂蚁区
292
338
  container.addChild(new Spacer(1));
293
- container.addChild(new Text(theme.fg("muted", `─── Ants (${m.antsSpawned}) ───`), 0, 0));
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
- `${ai} ${casteIcon(a.caste)} ${theme.fg("accent", a.id)} ${theme.fg("dim", `${dur} ${formatCost(a.usage.cost)} ${a.usage.turns}t`)}`,
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
- // Concurrency
349
+ // 并发 + 日志
304
350
  container.addChild(new Spacer(1));
305
351
  const c = state.concurrency;
306
352
  container.addChild(new Text(
307
- theme.fg("muted", `─── Concurrency ───`) + `\n` +
308
- theme.fg("dim", `current: ${c.current} | optimal: ${c.optimal} | range: ${c.min}-${c.max} | samples: ${c.history.length}`),
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", "─── Log ───"), 0, 0));
315
- for (const l of details.log.slice(-20)) {
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 = 10;
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 { fs.unlinkSync(this.lockFile); } catch { /* ignore */ }
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
- const wait = SPIN_MS + Math.random() * SPIN_MS;
233
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, wait);
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 { /* ignore */ }
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, AvailableModel,
19
+ ConcurrencyConfig, TaskPriority, ModelOverrides,
20
20
  } from "./types.js";
21
- import { DEFAULT_ANT_CONFIGS, resolveModelForCaste } from "./types.js";
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
- modelOverrides?: ModelOverrides;
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, modelOverrides, maxCost, availableModels, currentModel } = opts;
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: opts.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
- modelOverrides: opts.modelOverrides, maxCost: opts.maxCost,
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
- callbacks.onPhase("scouting", "Dispatching scout ants to explore codebase...");
368
- if (await runAntWave({ ...waveBase, caste: "scout" }) === "budget_exceeded")
369
- return budgetStop("Budget exceeded during scouting");
370
-
371
- // 检查侦察结果,如果没有产生工蚁任务,用侦察结果让女王自己拆
372
- const postScout = nest.getAllTasks();
373
- const workerTasks = postScout.filter(t => t.caste === "worker" && t.status === "pending");
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);
@@ -288,6 +288,7 @@ export async function spawnAnt(
288
288
 
289
289
  if (signal) {
290
290
  const kill = () => {
291
+ try { fs.unlinkSync(tmpFile); } catch { /* already cleaned */ }
291
292
  proc.kill("SIGTERM");
292
293
  setTimeout(() => { if (!proc.killed) proc.kill("SIGKILL"); }, 3000);
293
294
  };
@@ -19,79 +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
- /** Extract version score from model name: "claude-sonnet-4-0" → 4.0, "claude-3-5-haiku" → 3.5 */
46
- function modelVersionScore(id: string): number {
47
- // Match patterns like "4-0", "3-5", "4.5", "4o" etc.
48
- const nums = id.match(/(\d+)[-.](\d+)/g);
49
- if (!nums) {
50
- const single = id.match(/(\d+)/g);
51
- return single ? Math.max(...single.map(Number)) : 0;
52
- }
53
- // Take the highest version-like number pair
54
- return Math.max(...nums.map(n => {
55
- const [a, b] = n.split(/[-.]/).map(Number);
56
- return a + (b ?? 0) / 10;
57
- }));
58
- }
59
-
60
- /**
61
- * 根据 caste 的 ModelTier 从可用模型中匹配最合适的模型
62
- *
63
- * 策略:从模型名提取版本号排序
64
- * - fast(侦察蚁):选版本号最低的(轻量便宜)
65
- * - powerful(工蚁):选版本号最高的(最强)
66
- * - balanced(兵蚁):优先当前会话模型
67
- * - 所有 tier 最终 fallback 到 currentModel
68
- */
69
- export function resolveModelForCaste(
70
- caste: AntCaste,
71
- available: AvailableModel[],
72
- currentModel?: string,
73
- ): string | undefined {
74
- if (available.length === 0) return currentModel;
75
- const tier = CASTE_MODEL_TIER[caste];
76
-
77
- // 工蚁/兵蚁优先使用当前会话模型
78
- if (tier !== "fast" && currentModel) return currentModel;
79
-
80
- // 按版本号排序
81
- const scored = available.map(m => ({ id: m.id, score: modelVersionScore(m.id) }));
82
- scored.sort((a, b) => a.score - b.score);
83
-
84
- // 优先同 provider
85
- const provider = currentModel?.split("/")[0] ?? currentModel?.split("-")[0];
86
- const sameProvider = provider ? scored.filter(m => m.id.toLowerCase().includes(provider.toLowerCase())) : [];
87
- const pool = sameProvider.length > 0 ? sameProvider : scored;
88
-
89
- if (tier === "fast") return pool[pool.length - 1]?.id ?? currentModel;
90
- if (tier === "powerful") return pool[pool.length - 1]?.id ?? currentModel;
91
- // balanced: middle
92
- return pool[Math.floor(pool.length / 2)]?.id ?? currentModel;
93
- }
94
-
95
25
  // ═══ 任务 (Food Source) ═══
96
26
  export type TaskStatus = "pending" | "claimed" | "active" | "done" | "failed" | "blocked";
97
27
  export type TaskPriority = 1 | 2 | 3 | 4 | 5; // 1=highest