oh-pi 0.1.71 → 0.1.72

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-pi",
3
- "version": "0.1.71",
3
+ "version": "0.1.72",
4
4
  "description": "One-click setup for pi-coding-agent. Like oh-my-zsh for pi.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -228,6 +228,18 @@ export class Nest {
228
228
  return this.pheromoneCache;
229
229
  }
230
230
 
231
+ /** 统计指定文件相关的 warning/repellent 信息素数量 */
232
+ countWarnings(files: string[]): number {
233
+ this.getAllPheromones();
234
+ let count = 0;
235
+ for (const f of files) {
236
+ for (const p of this.pheromoneByFile.get(f) ?? []) {
237
+ if (p.type === "warning" || p.type === "repellent") count++;
238
+ }
239
+ }
240
+ return count;
241
+ }
242
+
231
243
  /** 读取与特定文件相关的信息素摘要 */
232
244
  getPheromoneContext(files: string[], limit = 20): string {
233
245
  const relevant = this.getAllPheromones()
@@ -76,7 +76,7 @@ Output format (MUST follow exactly):
76
76
  PASS or FAIL with summary.`,
77
77
  };
78
78
 
79
- export function buildPrompt(task: Task, pheromoneContext: string, castePrompt: string, maxTurns?: number): string {
79
+ export function buildPrompt(task: Task, pheromoneContext: string, castePrompt: string, maxTurns?: number, tandem?: { parentResult?: string; priorError?: string }): string {
80
80
  let prompt = castePrompt + "\n\n";
81
81
  if (maxTurns) {
82
82
  prompt += `## ⚠️ Turn Limit\nYou have a MAXIMUM of ${maxTurns} turns. Plan accordingly — reserve your LAST turn to output the structured result format above. Do NOT waste turns on unnecessary exploration.\n\n`;
@@ -84,6 +84,12 @@ export function buildPrompt(task: Task, pheromoneContext: string, castePrompt: s
84
84
  if (pheromoneContext) {
85
85
  prompt += `## Colony Pheromone Trail (intelligence from other ants)\n${pheromoneContext}\n\n`;
86
86
  }
87
+ if (tandem?.parentResult) {
88
+ prompt += `## Tandem Context (from parent task)\n${tandem.parentResult.slice(0, 3000)}\n\n`;
89
+ }
90
+ if (tandem?.priorError) {
91
+ prompt += `## ⚠️ Prior Attempt Failed\nA previous ant failed on this task. Learn from their mistake:\n${tandem.priorError.slice(0, 1500)}\n\n`;
92
+ }
87
93
  prompt += `## Your Assignment\n**Task:** ${task.title}\n**Description:** ${task.description}\n`;
88
94
  if (task.files.length > 0) {
89
95
  prompt += `**Files scope:** ${task.files.join(", ")}\n`;
@@ -98,6 +98,44 @@ function childTaskFromParsed(
98
98
  };
99
99
  }
100
100
 
101
+ /**
102
+ * Bio 5: 蚁群投票 — 合并多 Scout 产生的重复任务
103
+ * 相同文件集合的任务合并,被多 Scout 提及的任务 priority 提升
104
+ */
105
+ function quorumMergeTasks(nest: Nest): void {
106
+ const tasks = nest.getAllTasks().filter(t =>
107
+ (t.caste === "worker" || t.caste === "drone") && t.status === "pending"
108
+ );
109
+ if (tasks.length < 2) return;
110
+
111
+ // 按文件集合分组(排序后 join 作为 key)
112
+ const groups = new Map<string, Task[]>();
113
+ for (const t of tasks) {
114
+ const key = [...t.files].sort().join("|") || t.title;
115
+ const arr = groups.get(key) ?? [];
116
+ arr.push(t);
117
+ groups.set(key, arr);
118
+ }
119
+
120
+ for (const [, group] of groups) {
121
+ if (group.length < 2) continue;
122
+ // 保留第一个,删除重复的,合并 description
123
+ const keeper = group[0];
124
+ // Quorum 达成:被多 Scout 提及 → priority 提升
125
+ keeper.priority = Math.max(1, keeper.priority - 1) as 1 | 2 | 3 | 4 | 5;
126
+ // 合并其他任务的 context 到 keeper
127
+ for (let i = 1; i < group.length; i++) {
128
+ const dup = group[i];
129
+ if (dup.context && dup.context !== keeper.context) {
130
+ keeper.context = (keeper.context || "") + "\n\n--- Additional scout context ---\n" + dup.context;
131
+ }
132
+ // 标记重复任务为 done(已合并)
133
+ nest.updateTaskStatus(dup.id, "done", `Merged into ${keeper.id} (quorum)`);
134
+ }
135
+ nest.writeTask(keeper);
136
+ }
137
+ }
138
+
101
139
  function makeReviewTask(completedTasks: Task[]): Task {
102
140
  const files = [...new Set(completedTasks.flatMap(t => t.files))];
103
141
  return {
@@ -157,25 +195,49 @@ interface WaveOptions {
157
195
  importGraph?: ImportGraph;
158
196
  }
159
197
 
198
+ /**
199
+ * Bio 6: 尸体清理 — 错误模式分类
200
+ */
201
+ function classifyError(errStr: string): string {
202
+ if (errStr.includes("TypeError") || errStr.includes("type") || errStr.includes("TS")) return "type_error";
203
+ if (errStr.includes("permission") || errStr.includes("401") || errStr.includes("EACCES")) return "permission";
204
+ if (errStr.includes("timeout") || errStr.includes("Timeout") || errStr.includes("ETIMEDOUT")) return "timeout";
205
+ if (errStr.includes("ENOENT") || errStr.includes("not found") || errStr.includes("Cannot find")) return "not_found";
206
+ if (errStr.includes("syntax") || errStr.includes("SyntaxError") || errStr.includes("Unexpected")) return "syntax";
207
+ if (errStr.includes("429") || errStr.includes("rate limit")) return "rate_limit";
208
+ return "unknown";
209
+ }
210
+
160
211
  /**
161
212
  * 并发执行一批蚂蚁,自适应调节并发度
162
213
  */
163
214
  async function runAntWave(opts: WaveOptions): Promise<"ok" | "budget"> {
164
215
  const { nest, cwd, caste, signal, callbacks, currentModel, emitSignal } = opts;
165
216
  const casteModel = opts.modelOverrides?.[caste] || currentModel;
166
- const config = { ...DEFAULT_ANT_CONFIGS[caste], model: casteModel };
217
+ const baseConfig = { ...DEFAULT_ANT_CONFIGS[caste], model: casteModel };
167
218
 
168
219
  let backoffMs = 0; // 429 退避时间
169
220
  let consecutiveRateLimits = 0; // 连续限流计数
170
221
  const retryCount = new Map<string, number>(); // taskId → retry count
171
222
  const MAX_RETRIES = 2;
172
223
 
224
+ // Bio 6: 尸体清理 — 错误模式追踪
225
+ const errorPatterns = new Map<string, { count: number; files: Set<string>; errors: string[] }>();
226
+
173
227
  const runOne = async (): Promise<"done" | "empty" | "rate_limited" | "budget"> => {
174
228
  // Budget 刹车:预算用完就不出发(drone 免费,不检查)
175
229
  const state = nest.getStateLight();
176
230
  if (state.maxCost != null && caste !== "drone") {
177
231
  const spent = state.ants.reduce((s, a) => s + a.usage.cost, 0);
178
232
  if (spent >= state.maxCost) return "budget";
233
+
234
+ // Bio 4: 巢穴温度 — 成本渐进调控
235
+ const temperature = spent / state.maxCost;
236
+ if (temperature > 0.9) {
237
+ // 紧急模式:只跑 priority 1 任务
238
+ const pending = state.tasks.filter(t => t.status === "pending" && t.caste === caste);
239
+ if (!pending.some(t => t.priority === 1)) return "budget";
240
+ }
179
241
  }
180
242
 
181
243
  const task = nest.claimNextTask(caste, "queen");
@@ -194,6 +256,14 @@ async function runAntWave(opts: WaveOptions): Promise<"ok" | "budget"> {
194
256
  const antAbort = new AbortController();
195
257
  signal?.addEventListener("abort", () => antAbort.abort(), { once: true });
196
258
  const antSignal = antAbort.signal;
259
+ // Bio 7: 年龄多态 — 前期保守,后期收敛
260
+ const progress = state.metrics.tasksTotal > 0 ? state.metrics.tasksDone / state.metrics.tasksTotal : 0;
261
+ const config = { ...baseConfig };
262
+ if (progress < 0.3) {
263
+ config.maxTurns = Math.max(baseConfig.maxTurns - 3, 5); // 前期保守
264
+ } else if (progress > 0.7) {
265
+ config.maxTurns = Math.max(baseConfig.maxTurns - 5, 5); // 后期收敛,只修复收尾
266
+ }
197
267
  const antPromise = caste === "drone"
198
268
  ? runDrone(cwd, nest, task)
199
269
  : spawnAnt(cwd, nest, task, config, antSignal, callbacks.onAntStream, opts.authStorage, opts.modelRegistry);
@@ -220,8 +290,11 @@ async function runAntWave(opts: WaveOptions): Promise<"ok" | "budget"> {
220
290
  }
221
291
 
222
292
  // 蚂蚁产生的子任务加入巢穴(限制繁殖上限,防止任务膨胀)
293
+ // Bio 7: 年龄多态 — 后期限制子任务生成
294
+ const m = curState.metrics;
295
+ const colonyProgress = m.tasksTotal > 0 ? m.tasksDone / m.tasksTotal : 0;
223
296
  const MAX_TOTAL_TASKS = 30;
224
- const MAX_SUB_PER_TASK = 5;
297
+ const MAX_SUB_PER_TASK = colonyProgress > 0.7 ? 2 : 5; // 后期收敛
225
298
  const accepted = result.newTasks.slice(0, MAX_SUB_PER_TASK);
226
299
  for (const sub of accepted) {
227
300
  if (nest.getAllTasks().length >= MAX_TOTAL_TASKS) break;
@@ -240,13 +313,14 @@ async function runAntWave(opts: WaveOptions): Promise<"ok" | "budget"> {
240
313
  nest.addSubTask(task.id, child);
241
314
  }
242
315
 
243
- // 路径强化:成功完成释放 completion 信息素,强化相关文件路径
316
+ // 路径强化:成功完成释放 completion 信息素,强度与任务规模成正比(招募信号)
244
317
  if (task.files.length > 0) {
318
+ const recruitStrength = Math.min(1.0, 0.5 + task.files.length * 0.1 + result.newTasks.length * 0.15);
245
319
  nest.dropPheromone({
246
320
  id: makePheromoneId(), type: "completion", antId: result.ant.id,
247
321
  antCaste: caste, taskId: task.id,
248
322
  content: `Success: ${task.title}`,
249
- files: task.files, strength: 1.0, createdAt: Date.now(),
323
+ files: task.files, strength: recruitStrength, createdAt: Date.now(),
250
324
  });
251
325
  }
252
326
 
@@ -264,16 +338,49 @@ async function runAntWave(opts: WaveOptions): Promise<"ok" | "budget"> {
264
338
  retryCount.set(task.id, count + 1);
265
339
  nest.updateTaskStatus(task.id, "pending");
266
340
  } else {
267
- // 负信息素:失败任务释放 warning,阻止后续蚂蚁走同一条路
341
+ // 负信息素:失败任务释放 warning,强度与任务规模成正比
268
342
  if (task.files.length > 0) {
343
+ const warnStrength = Math.min(1.0, 0.5 + task.files.length * 0.1);
269
344
  nest.dropPheromone({
270
345
  id: makePheromoneId(), type: "warning", antId: "queen",
271
346
  antCaste: caste, taskId: task.id,
272
347
  content: `Failed: ${task.title} — ${String(e).slice(0, 100)}`,
273
- files: task.files, strength: 1.0, createdAt: Date.now(),
348
+ files: task.files, strength: warnStrength, createdAt: Date.now(),
274
349
  });
275
350
  }
276
351
  nest.updateTaskStatus(task.id, "failed", undefined, String(e));
352
+
353
+ // Bio 6: 尸体清理 — 错误模式追踪 + 诊断任务
354
+ const pattern = classifyError(errStr);
355
+ const entry = errorPatterns.get(pattern) ?? { count: 0, files: new Set<string>(), errors: [] };
356
+ entry.count++;
357
+ for (const f of task.files) entry.files.add(f);
358
+ entry.errors.push(errStr.slice(0, 200));
359
+ errorPatterns.set(pattern, entry);
360
+
361
+ if (entry.count >= 2 && entry.files.size > 0) {
362
+ const affectedFiles = [...entry.files];
363
+ // 释放 repellent 信息素
364
+ nest.dropPheromone({
365
+ id: makePheromoneId(), type: "repellent", antId: "queen",
366
+ antCaste: caste, taskId: task.id,
367
+ content: `Recurring ${pattern} errors (${entry.count}x): ${entry.errors[0]?.slice(0, 80)}`,
368
+ files: affectedFiles, strength: 1.0, createdAt: Date.now(),
369
+ });
370
+ // 生成诊断任务(仅首次触发)
371
+ if (entry.count === 2 && nest.getAllTasks().length < 30) {
372
+ const diagTask: Task = {
373
+ id: makeTaskId(), parentId: null,
374
+ title: `Diagnose recurring ${pattern} errors`,
375
+ description: `Multiple ants failed with ${pattern} errors on these files:\n${affectedFiles.map(f => `- ${f}`).join("\n")}\n\nErrors:\n${entry.errors.map(e => `- ${e}`).join("\n")}\n\nInvestigate root cause and generate fix tasks.`,
376
+ caste: "scout", status: "pending", priority: 1,
377
+ files: affectedFiles, claimedBy: null, result: null, error: null,
378
+ spawnedTasks: [], createdAt: Date.now(), startedAt: null, finishedAt: null,
379
+ };
380
+ nest.writeTask(diagTask);
381
+ emitSignal("working", `Diagnosing recurring ${pattern} errors...`);
382
+ }
383
+ }
277
384
  }
278
385
  return "done";
279
386
  }
@@ -422,11 +529,38 @@ export async function runColony(opts: QueenOptions): Promise<ColonyState> {
422
529
  };
423
530
 
424
531
  try {
425
- // ═══ Phase 1: 侦察(快速单次,不再多轮接力) ═══
426
- callbacks.onPhase?.("scouting", "Dispatching scout ant to explore codebase...");
427
- emitSignal("scouting", "Exploring codebase...");
532
+ // ═══ Phase 1: 侦察(Bio 5: 蚁群投票 — 复杂目标派多 Scout) ═══
533
+ const scoutCount = opts.goal.length > 500 ? 3 : opts.goal.length > 200 ? 2 : 1;
534
+ if (scoutCount > 1) {
535
+ // 多 Scout 并行:为每只 Scout 创建独立任务
536
+ for (let i = 1; i < scoutCount; i++) {
537
+ const extraScout: Task = {
538
+ id: makeTaskId(),
539
+ parentId: null,
540
+ title: `Scout ${i + 1}: explore codebase for goal`,
541
+ description: `Explore the codebase from a different angle and identify files, modules, and dependencies relevant to this goal:\n\n${opts.goal}\n\nFocus on areas other scouts might miss. Be thorough.`,
542
+ caste: "scout",
543
+ status: "pending",
544
+ priority: 1,
545
+ files: [],
546
+ claimedBy: null,
547
+ result: null,
548
+ error: null,
549
+ spawnedTasks: [],
550
+ createdAt: Date.now(),
551
+ startedAt: null,
552
+ finishedAt: null,
553
+ };
554
+ nest.writeTask(extraScout);
555
+ }
556
+ }
557
+ callbacks.onPhase?.("scouting", `Dispatching ${scoutCount} scout ant(s) to explore codebase...`);
558
+ emitSignal("scouting", `${scoutCount} scouts exploring...`);
428
559
  await runAntWave({ ...waveBase, caste: "scout" });
429
560
 
561
+ // Bio 5: 合并多 Scout 产生的重复任务
562
+ if (scoutCount > 1) quorumMergeTasks(nest);
563
+
430
564
  let workerTasks = nest.getAllTasks().filter(t => (t.caste === "worker" || t.caste === "drone") && t.status === "pending");
431
565
 
432
566
  // 只在完全没有 worker 任务时才重试一次
@@ -514,11 +648,14 @@ export async function runColony(opts: QueenOptions): Promise<ColonyState> {
514
648
  }
515
649
 
516
650
  // ═══ 持续探索:Worker 完成后检查是否有新发现,有则再派 Scout ═══
651
+ // Bio 4: 巢穴温度 — 超过 50% 预算禁止新 Scout 探索
517
652
  const discoveries = nest.getAllPheromones().filter(p => p.type === "discovery");
518
653
  const allDone = nest.getAllTasks().filter(t => t.status === "done");
519
- if (discoveries.length > allDone.length) {
520
- const spent = nest.getStateLight().ants.reduce((s, a) => s + a.usage.cost, 0);
521
- if (spent < (nest.getStateLight().maxCost ?? Infinity)) {
654
+ const preExploreSpent = nest.getStateLight().ants.reduce((s, a) => s + a.usage.cost, 0);
655
+ const preExploreBudget = nest.getStateLight().maxCost ?? Infinity;
656
+ const costTemperature = preExploreSpent / preExploreBudget;
657
+ if (discoveries.length > allDone.length && costTemperature < 0.5) {
658
+ if (preExploreSpent < preExploreBudget) {
522
659
  callbacks.onPhase?.("scouting", "Re-exploring based on new discoveries...");
523
660
  emitSignal("scouting", "Re-exploring...");
524
661
  await runAntWave({ ...waveBase, caste: "scout" });
@@ -181,9 +181,22 @@ export async function spawnAnt(
181
181
  nest.updateAnt(ant);
182
182
  nest.updateTaskStatus(task.id, "active");
183
183
 
184
+ // Bio 2: 任务难度感知 — 动态 maxTurns
185
+ const warnings = nest.countWarnings(task.files);
186
+ const difficultyTurns = Math.min(25, (antConfig.maxTurns || 15) + task.files.length + warnings * 2);
187
+ const effectiveMaxTurns = antConfig.caste === "drone" ? 1 : difficultyTurns;
188
+
189
+ // Bio 3: 串联觅食 — 继承父任务 result 和失败前任 error
190
+ const tandem: { parentResult?: string; priorError?: string } = {};
191
+ if (task.parentId) {
192
+ const parent = nest.getTask(task.parentId);
193
+ if (parent?.result) tandem.parentResult = parent.result;
194
+ }
195
+ if (task.error) tandem.priorError = task.error;
196
+
184
197
  const pheromoneCtx = nest.getPheromoneContext(task.files);
185
198
  const castePrompt = CASTE_PROMPTS[antConfig.caste];
186
- const systemPrompt = buildPrompt(task, pheromoneCtx, castePrompt, antConfig.maxTurns);
199
+ const systemPrompt = buildPrompt(task, pheromoneCtx, castePrompt, effectiveMaxTurns, tandem);
187
200
 
188
201
  const auth = authStorage ?? new AuthStorage();
189
202
  const registry = modelRegistry ?? new ModelRegistry(auth);