oh-pi 0.1.61 → 0.1.63
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
|
@@ -14,7 +14,8 @@ import { join } from "node:path";
|
|
|
14
14
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
15
15
|
import { Text, Container, Spacer, matchesKey } from "@mariozechner/pi-tui";
|
|
16
16
|
import { Type } from "@sinclair/typebox";
|
|
17
|
-
import { runColony, type QueenCallbacks } from "./queen.js";
|
|
17
|
+
import { runColony, resumeColony, type QueenCallbacks } from "./queen.js";
|
|
18
|
+
import { Nest } from "./nest.js";
|
|
18
19
|
import type { ColonyState, ColonyMetrics, AntStreamEvent } from "./types.js";
|
|
19
20
|
|
|
20
21
|
// ═══ Helpers ═══
|
|
@@ -174,7 +175,7 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
174
175
|
modelOverrides: Record<string, string>;
|
|
175
176
|
cwd: string;
|
|
176
177
|
modelRegistry?: any;
|
|
177
|
-
}) {
|
|
178
|
+
}, resume = false) {
|
|
178
179
|
if (activeColony) {
|
|
179
180
|
pi.events.emit("ant-colony:notify", { msg: "A colony is already running. Use /colony-stop first.", level: "warning" });
|
|
180
181
|
return;
|
|
@@ -236,7 +237,7 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
236
237
|
appendFileSync(gitignorePath, `${gitContent.length && !gitContent.endsWith("\n") ? "\n" : ""}.ant-colony/\n`);
|
|
237
238
|
}
|
|
238
239
|
|
|
239
|
-
|
|
240
|
+
const colonyOpts = {
|
|
240
241
|
cwd: params.cwd,
|
|
241
242
|
goal: params.goal,
|
|
242
243
|
maxAnts: params.maxAnts,
|
|
@@ -247,7 +248,8 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
247
248
|
callbacks,
|
|
248
249
|
authStorage: undefined,
|
|
249
250
|
modelRegistry: params.modelRegistry,
|
|
250
|
-
}
|
|
251
|
+
};
|
|
252
|
+
colony.promise = resume ? resumeColony(colonyOpts) : runColony(colonyOpts);
|
|
251
253
|
|
|
252
254
|
activeColony = colony;
|
|
253
255
|
throttledRender();
|
|
@@ -569,6 +571,30 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
569
571
|
},
|
|
570
572
|
});
|
|
571
573
|
|
|
574
|
+
pi.registerCommand("colony-resume", {
|
|
575
|
+
description: "Resume a colony from its last checkpoint",
|
|
576
|
+
async handler(_args, ctx) {
|
|
577
|
+
if (activeColony) {
|
|
578
|
+
ctx.ui.notify("A colony is already running.", "warning");
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
const found = Nest.findResumable(ctx.cwd);
|
|
582
|
+
if (!found) {
|
|
583
|
+
ctx.ui.notify("No resumable colony found.", "info");
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
ctx.ui.notify(`🐜 Resuming colony: ${found.state.goal.slice(0, 60)}...`, "info");
|
|
587
|
+
launchBackgroundColony({
|
|
588
|
+
cwd: ctx.cwd,
|
|
589
|
+
goal: found.state.goal,
|
|
590
|
+
maxCost: found.state.maxCost ?? undefined,
|
|
591
|
+
currentModel: ctx.currentModel,
|
|
592
|
+
modelOverrides: {},
|
|
593
|
+
modelRegistry: ctx.modelRegistry,
|
|
594
|
+
}, true);
|
|
595
|
+
},
|
|
596
|
+
});
|
|
597
|
+
|
|
572
598
|
// ═══ Cleanup on shutdown ═══
|
|
573
599
|
pi.on("session_shutdown", async () => {
|
|
574
600
|
if (activeColony) {
|
|
@@ -263,4 +263,33 @@ export class Nest {
|
|
|
263
263
|
private readJson<T>(file: string): T {
|
|
264
264
|
return JSON.parse(fs.readFileSync(file, "utf-8")) as T;
|
|
265
265
|
}
|
|
266
|
+
|
|
267
|
+
/** 查找可恢复的蚁群(状态为 working/scouting/reviewing 且未完成) */
|
|
268
|
+
static findResumable(cwd: string): { colonyId: string; state: ColonyState } | null {
|
|
269
|
+
const parentDir = path.join(cwd, ".ant-colony");
|
|
270
|
+
try {
|
|
271
|
+
for (const dir of fs.readdirSync(parentDir)) {
|
|
272
|
+
const stateFile = path.join(parentDir, dir, "state.json");
|
|
273
|
+
if (!fs.existsSync(stateFile)) continue;
|
|
274
|
+
const state = JSON.parse(fs.readFileSync(stateFile, "utf-8")) as ColonyState;
|
|
275
|
+
if (!state.finishedAt && state.status !== "done" && state.status !== "failed" && state.status !== "budget_exceeded") {
|
|
276
|
+
return { colonyId: dir, state };
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
} catch { /* no .ant-colony dir */ }
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** 从已有文件恢复,不调用 init */
|
|
284
|
+
restore(): void {
|
|
285
|
+
this.stateCache = this.readJson<ColonyState>(this.stateFile);
|
|
286
|
+
// 将 claimed/active 任务重置为 pending(蚂蚁已死)
|
|
287
|
+
for (const task of this.getAllTasks()) {
|
|
288
|
+
if (task.status === "claimed" || task.status === "active") {
|
|
289
|
+
task.status = "pending";
|
|
290
|
+
task.claimedBy = undefined;
|
|
291
|
+
this.writeTask(task);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
266
295
|
}
|
|
@@ -463,6 +463,37 @@ export async function runColony(opts: QueenOptions): Promise<ColonyState> {
|
|
|
463
463
|
}
|
|
464
464
|
}
|
|
465
465
|
|
|
466
|
+
// ═══ 持续探索:Worker 完成后检查是否有新发现,有则再派 Scout ═══
|
|
467
|
+
const discoveries = nest.getAllPheromones().filter(p => p.type === "discovery");
|
|
468
|
+
const allDone = nest.getAllTasks().filter(t => t.status === "done");
|
|
469
|
+
if (discoveries.length > allDone.length && nest.getState().maxCost != null) {
|
|
470
|
+
const spent = nest.getState().ants.reduce((s, a) => s + a.usage.cost, 0);
|
|
471
|
+
if (spent < (nest.getState().maxCost ?? Infinity)) {
|
|
472
|
+
callbacks.onPhase?.("scouting", "Re-exploring based on new discoveries...");
|
|
473
|
+
emitSignal("scouting", "Re-exploring...");
|
|
474
|
+
await runAntWave({ ...waveBase, caste: "scout" });
|
|
475
|
+
|
|
476
|
+
const newTasks = nest.getAllTasks().filter(t =>
|
|
477
|
+
(t.caste === "worker" || t.caste === "drone") && t.status === "pending"
|
|
478
|
+
);
|
|
479
|
+
if (newTasks.length > 0) {
|
|
480
|
+
const drones = newTasks.filter(t => t.caste === "drone");
|
|
481
|
+
if (drones.length > 0) await runAntWave({ ...waveBase, caste: "drone" });
|
|
482
|
+
|
|
483
|
+
callbacks.onPhase?.("working", `${newTasks.length} new tasks from re-exploration`);
|
|
484
|
+
emitSignal("working", `${newTasks.length} new tasks`);
|
|
485
|
+
const result = await runAntWave({ ...waveBase, caste: "worker" });
|
|
486
|
+
if (result === "budget") {
|
|
487
|
+
nest.updateState({ status: "budget_exceeded", finishedAt: Date.now() });
|
|
488
|
+
emitSignal("budget_exceeded", "Budget exhausted");
|
|
489
|
+
const budgetState = nest.getState();
|
|
490
|
+
callbacks.onComplete?.(budgetState);
|
|
491
|
+
return budgetState;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
466
497
|
// ═══ Auto-check: run tsc before soldier review ═══
|
|
467
498
|
let tscPassed = true;
|
|
468
499
|
try {
|
|
@@ -511,3 +542,76 @@ export async function runColony(opts: QueenOptions): Promise<ColonyState> {
|
|
|
511
542
|
cleanup();
|
|
512
543
|
}
|
|
513
544
|
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* 从检查点恢复蚁群 — 跳过已完成的阶段,继续执行未完成的任务
|
|
548
|
+
*/
|
|
549
|
+
export async function resumeColony(opts: QueenOptions): Promise<ColonyState> {
|
|
550
|
+
const found = Nest.findResumable(opts.cwd);
|
|
551
|
+
if (!found) return runColony(opts); // 无可恢复状态,正常启动
|
|
552
|
+
|
|
553
|
+
const nest = new Nest(opts.cwd, found.colonyId);
|
|
554
|
+
nest.restore();
|
|
555
|
+
|
|
556
|
+
const { signal, callbacks } = opts;
|
|
557
|
+
const waveBase: Omit<WaveOptions, "caste"> = {
|
|
558
|
+
nest, cwd: opts.cwd, signal, callbacks,
|
|
559
|
+
currentModel: opts.currentModel,
|
|
560
|
+
modelOverrides: opts.modelOverrides,
|
|
561
|
+
authStorage: opts.authStorage,
|
|
562
|
+
modelRegistry: opts.modelRegistry,
|
|
563
|
+
};
|
|
564
|
+
|
|
565
|
+
const emitSignal = (phase: ColonyState["status"], message: string) => {
|
|
566
|
+
const m = nest.getState().metrics;
|
|
567
|
+
const active = nest.getState().ants.filter(a => a.status === "working").length;
|
|
568
|
+
const progress = m.tasksTotal > 0 ? m.tasksDone / m.tasksTotal : 0;
|
|
569
|
+
callbacks.onSignal?.({ phase, progress, active, cost: m.totalCost, message });
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
const cleanup = () => {
|
|
573
|
+
nest.destroy();
|
|
574
|
+
const parentDir = path.join(opts.cwd, ".ant-colony");
|
|
575
|
+
try {
|
|
576
|
+
const entries = fs.readdirSync(parentDir);
|
|
577
|
+
if (entries.length === 0) fs.rmdirSync(parentDir);
|
|
578
|
+
} catch { /* ignore */ }
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
callbacks.onPhase?.("working", "Resuming colony from checkpoint...");
|
|
582
|
+
emitSignal("working", "Resuming...");
|
|
583
|
+
|
|
584
|
+
try {
|
|
585
|
+
// 执行所有 pending 任务
|
|
586
|
+
const pendingDrones = nest.getAllTasks().filter(t => t.caste === "drone" && t.status === "pending");
|
|
587
|
+
if (pendingDrones.length > 0) await runAntWave({ ...waveBase, caste: "drone" });
|
|
588
|
+
|
|
589
|
+
const pendingWorkers = nest.getAllTasks().filter(t => t.caste === "worker" && t.status === "pending");
|
|
590
|
+
if (pendingWorkers.length > 0) {
|
|
591
|
+
const result = await runAntWave({ ...waveBase, caste: "worker" });
|
|
592
|
+
if (result === "budget") {
|
|
593
|
+
nest.updateState({ status: "budget_exceeded", finishedAt: Date.now() });
|
|
594
|
+
emitSignal("budget_exceeded", "Budget exhausted");
|
|
595
|
+
const s = nest.getState();
|
|
596
|
+
callbacks.onComplete?.(s);
|
|
597
|
+
return s;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const finalMetrics = updateMetrics(nest);
|
|
602
|
+
nest.updateState({ status: "done", finishedAt: Date.now(), metrics: finalMetrics });
|
|
603
|
+
const finalState = nest.getState();
|
|
604
|
+
callbacks.onComplete?.(finalState);
|
|
605
|
+
emitSignal("done", `Resumed: ${finalMetrics.tasksDone}/${finalMetrics.tasksTotal} tasks done`);
|
|
606
|
+
return finalState;
|
|
607
|
+
|
|
608
|
+
} catch (e) {
|
|
609
|
+
nest.updateState({ status: "failed", finishedAt: Date.now() });
|
|
610
|
+
const failState = nest.getState();
|
|
611
|
+
callbacks.onComplete?.(failState);
|
|
612
|
+
emitSignal("failed", String(e).slice(0, 100));
|
|
613
|
+
return failState;
|
|
614
|
+
} finally {
|
|
615
|
+
cleanup();
|
|
616
|
+
}
|
|
617
|
+
}
|