oh-pi 0.1.30 → 0.1.31

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.30",
3
+ "version": "0.1.31",
4
4
  "description": "One-click setup for pi-coding-agent. Like oh-my-zsh for pi.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -140,14 +140,13 @@ interface WaveOptions {
140
140
  currentModel: string;
141
141
  signal?: AbortSignal;
142
142
  callbacks: QueenCallbacks;
143
- maxCost?: number;
144
143
  }
145
144
 
146
145
  /**
147
146
  * 并发执行一批蚂蚁,自适应调节并发度
148
147
  */
149
- async function runAntWave(opts: WaveOptions): Promise<"ok" | "budget_exceeded"> {
150
- const { nest, cwd, caste, signal, callbacks, maxCost, currentModel } = opts;
148
+ async function runAntWave(opts: WaveOptions): Promise<"ok"> {
149
+ const { nest, cwd, caste, signal, callbacks, currentModel } = opts;
151
150
  const config = { ...DEFAULT_ANT_CONFIGS[caste], model: currentModel };
152
151
 
153
152
  let backoffMs = 0; // 429 退避时间
@@ -208,26 +207,6 @@ async function runAntWave(opts: WaveOptions): Promise<"ok" | "budget_exceeded">
208
207
  // 调度循环:持续派蚂蚁直到没有待处理任务
209
208
  let lastSampleTime = 0;
210
209
  while (!signal?.aborted) {
211
- // 预算检查:派蚂蚁前预估,基于已完成蚂蚁的平均 cost
212
- if (maxCost != null) {
213
- const ants = nest.getState().ants;
214
- const currentCost = ants.reduce((s, a) => s + a.usage.cost, 0);
215
- if (currentCost >= maxCost) {
216
- callbacks.onPhase("working", `Budget exceeded ($${currentCost.toFixed(3)} >= $${maxCost.toFixed(2)}). Stopping.`);
217
- return "budget_exceeded";
218
- }
219
- // 预估:如果剩余预算不够一只蚂蚁的平均花费,也停止
220
- const doneAnts = ants.filter(a => a.status === "done" && a.usage.cost > 0);
221
- if (doneAnts.length > 0) {
222
- const avgCost = doneAnts.reduce((s, a) => s + a.usage.cost, 0) / doneAnts.length;
223
- const remaining = maxCost - currentCost;
224
- if (remaining < avgCost * 0.5) {
225
- callbacks.onPhase("working", `Budget nearly exhausted ($${currentCost.toFixed(3)}, avg/ant: $${avgCost.toFixed(3)}). Stopping.`);
226
- return "budget_exceeded";
227
- }
228
- }
229
- }
230
-
231
210
  const state = nest.getState();
232
211
  const pending = state.tasks.filter(t => t.status === "pending" && t.caste === caste);
233
212
  if (pending.length === 0) break;
@@ -331,7 +310,7 @@ export async function runColony(opts: QueenOptions): Promise<ColonyState> {
331
310
  const { signal, callbacks } = opts;
332
311
  const waveBase: Omit<WaveOptions, "caste"> = {
333
312
  nest, cwd: opts.cwd, signal, callbacks,
334
- maxCost: opts.maxCost, currentModel: opts.currentModel,
313
+ currentModel: opts.currentModel,
335
314
  };
336
315
 
337
316
  const cleanup = () => {
@@ -343,15 +322,6 @@ export async function runColony(opts: QueenOptions): Promise<ColonyState> {
343
322
  } catch { /* ignore */ }
344
323
  };
345
324
 
346
- const budgetStop = (phase: string) => {
347
- nest.updateState({ status: "budget_exceeded", finishedAt: Date.now() });
348
- callbacks.onPhase("budget_exceeded" as any, phase);
349
- const s = nest.getState();
350
- callbacks.onComplete(s);
351
- cleanup();
352
- return s;
353
- };
354
-
355
325
  try {
356
326
  // ═══ Phase 1: 侦察(最多重试2次) ═══
357
327
  let scoutAttempt = 0;
@@ -363,8 +333,7 @@ export async function runColony(opts: QueenOptions): Promise<ColonyState> {
363
333
  ? "Dispatching scout ants to explore codebase..."
364
334
  : `Scout retry ${scoutAttempt}/${MAX_SCOUT_RETRIES}...`);
365
335
 
366
- if (await runAntWave({ ...waveBase, caste: "scout" }) === "budget_exceeded")
367
- return budgetStop("Budget exceeded during scouting");
336
+ await runAntWave({ ...waveBase, caste: "scout" });
368
337
 
369
338
  workerTasks = nest.getAllTasks().filter(t => t.caste === "worker" && t.status === "pending");
370
339
  if (workerTasks.length > 0) break;
@@ -389,8 +358,7 @@ export async function runColony(opts: QueenOptions): Promise<ColonyState> {
389
358
  // ═══ Phase 2: 工作 ═══
390
359
  nest.updateState({ status: "working" });
391
360
  callbacks.onPhase("working", `${workerTasks.length} tasks discovered. Dispatching worker ants...`);
392
- if (await runAntWave({ ...waveBase, caste: "worker" }) === "budget_exceeded")
393
- return budgetStop("Budget exceeded during working");
361
+ await runAntWave({ ...waveBase, caste: "worker" });
394
362
 
395
363
  // 处理工蚁产生的子任务(可能有多轮)
396
364
  let rounds = 0;
@@ -401,8 +369,7 @@ export async function runColony(opts: QueenOptions): Promise<ColonyState> {
401
369
  if (remaining.length === 0) break;
402
370
  rounds++;
403
371
  callbacks.onPhase("working", `Round ${rounds + 1}: ${remaining.length} sub-tasks from workers...`);
404
- if (await runAntWave({ ...waveBase, caste: "worker" }) === "budget_exceeded")
405
- return budgetStop("Budget exceeded during sub-tasks");
372
+ await runAntWave({ ...waveBase, caste: "worker" });
406
373
  }
407
374
 
408
375
  // ═══ Phase 3: 审查 ═══
@@ -412,8 +379,7 @@ export async function runColony(opts: QueenOptions): Promise<ColonyState> {
412
379
  callbacks.onPhase("reviewing", "Dispatching soldier ants to review changes...");
413
380
  const reviewTask = makeReviewTask(completedWorkerTasks);
414
381
  nest.writeTask(reviewTask);
415
- if (await runAntWave({ ...waveBase, caste: "soldier" }) === "budget_exceeded")
416
- return budgetStop("Budget exceeded during review");
382
+ await runAntWave({ ...waveBase, caste: "soldier" });
417
383
 
418
384
  // 兵蚁产生的修复任务
419
385
  const fixTasks = nest.getAllTasks().filter(t =>
@@ -422,8 +388,7 @@ export async function runColony(opts: QueenOptions): Promise<ColonyState> {
422
388
  if (fixTasks.length > 0) {
423
389
  nest.updateState({ status: "working" });
424
390
  callbacks.onPhase("working", `${fixTasks.length} fix tasks from review. Dispatching workers...`);
425
- if (await runAntWave({ ...waveBase, caste: "worker" }) === "budget_exceeded")
426
- return budgetStop("Budget exceeded during fixes");
391
+ await runAntWave({ ...waveBase, caste: "worker" });
427
392
  }
428
393
  }
429
394
 
@@ -114,8 +114,11 @@ Output format (MUST follow exactly):
114
114
  PASS or FAIL with summary.`,
115
115
  };
116
116
 
117
- function buildPrompt(task: Task, pheromoneContext: string, castePrompt: string): string {
117
+ function buildPrompt(task: Task, pheromoneContext: string, castePrompt: string, maxTurns?: number): string {
118
118
  let prompt = castePrompt + "\n\n";
119
+ if (maxTurns) {
120
+ 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`;
121
+ }
119
122
  if (pheromoneContext) {
120
123
  prompt += `## Colony Pheromone Trail (intelligence from other ants)\n${pheromoneContext}\n\n`;
121
124
  }
@@ -223,7 +226,7 @@ export async function spawnAnt(
223
226
  // 构建 prompt
224
227
  const pheromoneCtx = nest.getPheromoneContext(task.files);
225
228
  const castePrompt = CASTE_PROMPTS[antConfig.caste];
226
- const fullPrompt = buildPrompt(task, pheromoneCtx, castePrompt);
229
+ const fullPrompt = buildPrompt(task, pheromoneCtx, castePrompt, antConfig.maxTurns);
227
230
  const tmpFile = writePromptFile(nest.dir, antId, fullPrompt);
228
231
 
229
232
  const args = [
@@ -246,6 +249,7 @@ export async function spawnAnt(
246
249
  nest.updateAnt(ant);
247
250
 
248
251
  let buffer = "";
252
+ let turnCount = 0;
249
253
 
250
254
  proc.stdout.on("data", (data) => {
251
255
  buffer += data.toString();
@@ -255,6 +259,15 @@ export async function spawnAnt(
255
259
  if (!line.trim()) continue;
256
260
  try {
257
261
  const event = JSON.parse(line);
262
+ if (event.type === "turn_end") {
263
+ turnCount++;
264
+ if (antConfig.maxTurns && turnCount === antConfig.maxTurns) {
265
+ stderr += `[ant-colony] Warning: ant reached maxTurns (${antConfig.maxTurns}), 1 grace turn remaining\n`;
266
+ } else if (antConfig.maxTurns && turnCount > antConfig.maxTurns) {
267
+ proc.kill("SIGTERM");
268
+ setTimeout(() => { if (!proc.killed) proc.kill("SIGKILL"); }, 3000);
269
+ }
270
+ }
258
271
  if (event.type === "message_end" && event.message) {
259
272
  messages.push(event.message);
260
273
  if (event.message.role === "assistant") {