oh-pi 0.1.62 → 0.1.64
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;
|
|
@@ -190,9 +191,21 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
190
191
|
promise: null as any, // set below
|
|
191
192
|
};
|
|
192
193
|
|
|
194
|
+
let lastPhase = "";
|
|
195
|
+
|
|
193
196
|
const callbacks: QueenCallbacks = {
|
|
194
197
|
onSignal(signal) {
|
|
195
198
|
colony.phase = signal.message;
|
|
199
|
+
// 阶段切换时注入消息到主进程对话流
|
|
200
|
+
if (signal.phase !== lastPhase) {
|
|
201
|
+
lastPhase = signal.phase;
|
|
202
|
+
const pct = Math.round(signal.progress * 100);
|
|
203
|
+
pi.sendMessage({
|
|
204
|
+
customType: "ant-colony-progress",
|
|
205
|
+
content: `[COLONY_SIGNAL:${signal.phase.toUpperCase()}] 🐜 ${signal.message} (${pct}%, ${formatCost(signal.cost)})`,
|
|
206
|
+
display: false,
|
|
207
|
+
}, { triggerTurn: false, deliverAs: "followUp" });
|
|
208
|
+
}
|
|
196
209
|
throttledRender();
|
|
197
210
|
},
|
|
198
211
|
onPhase(phase, detail) {
|
|
@@ -236,7 +249,7 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
236
249
|
appendFileSync(gitignorePath, `${gitContent.length && !gitContent.endsWith("\n") ? "\n" : ""}.ant-colony/\n`);
|
|
237
250
|
}
|
|
238
251
|
|
|
239
|
-
|
|
252
|
+
const colonyOpts = {
|
|
240
253
|
cwd: params.cwd,
|
|
241
254
|
goal: params.goal,
|
|
242
255
|
maxAnts: params.maxAnts,
|
|
@@ -247,7 +260,8 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
247
260
|
callbacks,
|
|
248
261
|
authStorage: undefined,
|
|
249
262
|
modelRegistry: params.modelRegistry,
|
|
250
|
-
}
|
|
263
|
+
};
|
|
264
|
+
colony.promise = resume ? resumeColony(colonyOpts) : runColony(colonyOpts);
|
|
251
265
|
|
|
252
266
|
activeColony = colony;
|
|
253
267
|
throttledRender();
|
|
@@ -569,6 +583,30 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
569
583
|
},
|
|
570
584
|
});
|
|
571
585
|
|
|
586
|
+
pi.registerCommand("colony-resume", {
|
|
587
|
+
description: "Resume a colony from its last checkpoint",
|
|
588
|
+
async handler(_args, ctx) {
|
|
589
|
+
if (activeColony) {
|
|
590
|
+
ctx.ui.notify("A colony is already running.", "warning");
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
const found = Nest.findResumable(ctx.cwd);
|
|
594
|
+
if (!found) {
|
|
595
|
+
ctx.ui.notify("No resumable colony found.", "info");
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
ctx.ui.notify(`🐜 Resuming colony: ${found.state.goal.slice(0, 60)}...`, "info");
|
|
599
|
+
launchBackgroundColony({
|
|
600
|
+
cwd: ctx.cwd,
|
|
601
|
+
goal: found.state.goal,
|
|
602
|
+
maxCost: found.state.maxCost ?? undefined,
|
|
603
|
+
currentModel: ctx.currentModel,
|
|
604
|
+
modelOverrides: {},
|
|
605
|
+
modelRegistry: ctx.modelRegistry,
|
|
606
|
+
}, true);
|
|
607
|
+
},
|
|
608
|
+
});
|
|
609
|
+
|
|
572
610
|
// ═══ Cleanup on shutdown ═══
|
|
573
611
|
pi.on("session_shutdown", async () => {
|
|
574
612
|
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
|
}
|
|
@@ -542,3 +542,76 @@ export async function runColony(opts: QueenOptions): Promise<ColonyState> {
|
|
|
542
542
|
cleanup();
|
|
543
543
|
}
|
|
544
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
|
+
}
|