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
|
@@ -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"
|
|
150
|
-
const { nest, cwd, caste, signal, callbacks,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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") {
|