@ifi/oh-pi-ant-colony 0.2.4 โ 0.2.7
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/extensions/ant-colony/index.ts +258 -101
- package/extensions/ant-colony/nest.ts +14 -2
- package/package.json +1 -1
|
@@ -54,6 +54,8 @@ interface ColonyLogEntry {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
interface BackgroundColony {
|
|
57
|
+
/** Short identifier for this colony (c1, c2, ...). */
|
|
58
|
+
id: string;
|
|
57
59
|
goal: string;
|
|
58
60
|
abortController: AbortController;
|
|
59
61
|
state: ColonyState | null;
|
|
@@ -64,8 +66,30 @@ interface BackgroundColony {
|
|
|
64
66
|
}
|
|
65
67
|
|
|
66
68
|
export default function antColonyExtension(pi: ExtensionAPI) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
+
/** All running background colonies, keyed by short ID. */
|
|
70
|
+
const colonies = new Map<string, BackgroundColony>();
|
|
71
|
+
/** Auto-incrementing colony counter for generating IDs. */
|
|
72
|
+
let colonyCounter = 0;
|
|
73
|
+
|
|
74
|
+
/** Generate a short colony ID like c1, c2, ... */
|
|
75
|
+
function nextColonyId(): string {
|
|
76
|
+
colonyCounter++;
|
|
77
|
+
return `c${colonyCounter}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Resolve a colony by ID. If no ID given and exactly one colony is running,
|
|
82
|
+
* returns that one. Returns null if no match or ambiguous.
|
|
83
|
+
*/
|
|
84
|
+
function resolveColony(idArg?: string): BackgroundColony | null {
|
|
85
|
+
if (idArg) {
|
|
86
|
+
return colonies.get(idArg) ?? null;
|
|
87
|
+
}
|
|
88
|
+
if (colonies.size === 1) {
|
|
89
|
+
return colonies.values().next().value ?? null;
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
69
93
|
|
|
70
94
|
// Prevent main process polling from blocking: only allow explicit manual snapshots with cooldown
|
|
71
95
|
let lastBgStatusSnapshotAt = 0;
|
|
@@ -166,26 +190,30 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
166
190
|
}
|
|
167
191
|
|
|
168
192
|
renderHandler = () => {
|
|
169
|
-
if (
|
|
193
|
+
if (colonies.size === 0) {
|
|
170
194
|
return;
|
|
171
195
|
}
|
|
172
|
-
const
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
parts.push(
|
|
196
|
+
const statusParts: string[] = [];
|
|
197
|
+
for (const colony of colonies.values()) {
|
|
198
|
+
const { state } = colony;
|
|
199
|
+
const elapsed = state ? formatDuration(Date.now() - state.createdAt) : "0s";
|
|
200
|
+
const m = state?.metrics;
|
|
201
|
+
const phase = state?.status || "scouting";
|
|
202
|
+
const progress = calcProgress(m);
|
|
203
|
+
const pct = `${Math.round(progress * 100)}%`;
|
|
204
|
+
const active = colony.antStreams.size;
|
|
205
|
+
|
|
206
|
+
const parts = [`๐[${colony.id}] ${statusIcon(phase)} ${statusLabel(phase)}`];
|
|
207
|
+
parts.push(m ? `${m.tasksDone}/${m.tasksTotal} (${pct})` : `0/0 (${pct})`);
|
|
208
|
+
parts.push(`โก${active}`);
|
|
209
|
+
if (m) {
|
|
210
|
+
parts.push(formatCost(m.totalCost));
|
|
211
|
+
}
|
|
212
|
+
parts.push(elapsed);
|
|
213
|
+
statusParts.push(parts.join(" โ "));
|
|
185
214
|
}
|
|
186
|
-
parts.push(elapsed);
|
|
187
215
|
|
|
188
|
-
ctx.ui.setStatus("ant-colony",
|
|
216
|
+
ctx.ui.setStatus("ant-colony", statusParts.join(" ยท "));
|
|
189
217
|
};
|
|
190
218
|
clearHandler = () => {
|
|
191
219
|
ctx.ui.setStatus("ant-colony", undefined);
|
|
@@ -256,17 +284,11 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
256
284
|
modelRegistry?: any;
|
|
257
285
|
},
|
|
258
286
|
resume = false,
|
|
259
|
-
) {
|
|
260
|
-
|
|
261
|
-
pi.events.emit("ant-colony:notify", {
|
|
262
|
-
msg: "A colony is already running. Use /colony-stop first.",
|
|
263
|
-
level: "warning",
|
|
264
|
-
});
|
|
265
|
-
return;
|
|
266
|
-
}
|
|
267
|
-
|
|
287
|
+
): string {
|
|
288
|
+
const colonyId = nextColonyId();
|
|
268
289
|
const abortController = new AbortController();
|
|
269
290
|
const colony: BackgroundColony = {
|
|
291
|
+
id: colonyId,
|
|
270
292
|
goal: params.goal,
|
|
271
293
|
abortController,
|
|
272
294
|
state: null,
|
|
@@ -276,7 +298,7 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
276
298
|
promise: null as any, // set below
|
|
277
299
|
};
|
|
278
300
|
|
|
279
|
-
pushLog(colony, { level: "info", text:
|
|
301
|
+
pushLog(colony, { level: "info", text: `INITIALIZING ยท Colony [${colonyId}] launched in background` });
|
|
280
302
|
|
|
281
303
|
let lastPhase = "";
|
|
282
304
|
|
|
@@ -291,7 +313,7 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
291
313
|
pi.sendMessage(
|
|
292
314
|
{
|
|
293
315
|
customType: "ant-colony-progress",
|
|
294
|
-
content: `[COLONY_SIGNAL:${signal.phase.toUpperCase()}] ๐ ${signal.message} (${pct}%, ${formatCost(signal.cost)})`,
|
|
316
|
+
content: `[COLONY_SIGNAL:${signal.phase.toUpperCase()}] ๐[${colonyId}] ${signal.message} (${pct}%, ${formatCost(signal.cost)})`,
|
|
295
317
|
display: true,
|
|
296
318
|
},
|
|
297
319
|
{ triggerTurn: false, deliverAs: "followUp" },
|
|
@@ -327,7 +349,7 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
327
349
|
pi.sendMessage(
|
|
328
350
|
{
|
|
329
351
|
customType: "ant-colony-progress",
|
|
330
|
-
content: `[COLONY_SIGNAL:TASK_DONE] ๐ ${icon} ${task.title.slice(0, 60)} (${progress}, ${cost})`,
|
|
352
|
+
content: `[COLONY_SIGNAL:TASK_DONE] ๐[${colonyId}] ${icon} ${task.title.slice(0, 60)} (${progress}, ${cost})`,
|
|
331
353
|
display: true,
|
|
332
354
|
},
|
|
333
355
|
{ triggerTurn: false, deliverAs: "followUp" },
|
|
@@ -376,7 +398,7 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
376
398
|
};
|
|
377
399
|
colony.promise = resume ? resumeColony(colonyOpts) : runColony(colonyOpts);
|
|
378
400
|
|
|
379
|
-
|
|
401
|
+
colonies.set(colonyId, colony);
|
|
380
402
|
lastBgStatusSnapshotAt = 0;
|
|
381
403
|
throttledRender();
|
|
382
404
|
|
|
@@ -391,39 +413,44 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
391
413
|
text: `${ok ? "COMPLETE" : "FAILED"} ยท ${m.tasksDone}/${m.tasksTotal} ยท ${formatCost(m.totalCost)}`,
|
|
392
414
|
});
|
|
393
415
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
416
|
+
colonies.delete(colonyId);
|
|
417
|
+
if (colonies.size === 0) {
|
|
418
|
+
pi.events.emit("ant-colony:clear-ui");
|
|
419
|
+
}
|
|
397
420
|
|
|
398
421
|
// Inject results into conversation
|
|
399
422
|
pi.sendMessage(
|
|
400
423
|
{
|
|
401
424
|
customType: "ant-colony-report",
|
|
402
|
-
content: `[COLONY_SIGNAL:COMPLETE]\n${report}`,
|
|
425
|
+
content: `[COLONY_SIGNAL:COMPLETE] [${colonyId}]\n${report}`,
|
|
403
426
|
display: true,
|
|
404
427
|
},
|
|
405
428
|
{ triggerTurn: true, deliverAs: "followUp" },
|
|
406
429
|
);
|
|
407
430
|
|
|
408
431
|
pi.events.emit("ant-colony:notify", {
|
|
409
|
-
msg: `๐ Colony ${ok ? "completed" : "failed"}: ${m.tasksDone}/${m.tasksTotal} tasks โ ${formatCost(m.totalCost)}`,
|
|
432
|
+
msg: `๐[${colonyId}] Colony ${ok ? "completed" : "failed"}: ${m.tasksDone}/${m.tasksTotal} tasks โ ${formatCost(m.totalCost)}`,
|
|
410
433
|
level: ok ? "success" : "error",
|
|
411
434
|
});
|
|
412
435
|
})
|
|
413
436
|
.catch((e) => {
|
|
414
437
|
pushLog(colony, { level: "error", text: `CRASHED ยท ${String(e).slice(0, 120)}` });
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
438
|
+
colonies.delete(colonyId);
|
|
439
|
+
if (colonies.size === 0) {
|
|
440
|
+
pi.events.emit("ant-colony:clear-ui");
|
|
441
|
+
}
|
|
442
|
+
pi.events.emit("ant-colony:notify", { msg: `๐[${colonyId}] Colony crashed: ${e}`, level: "error" });
|
|
418
443
|
pi.sendMessage(
|
|
419
444
|
{
|
|
420
445
|
customType: "ant-colony-report",
|
|
421
|
-
content: `[COLONY_SIGNAL:FAILED]\n## ๐ Colony Crashed\n${e}`,
|
|
446
|
+
content: `[COLONY_SIGNAL:FAILED] [${colonyId}]\n## ๐ Colony Crashed\n${e}`,
|
|
422
447
|
display: true,
|
|
423
448
|
},
|
|
424
449
|
{ triggerTurn: true, deliverAs: "followUp" },
|
|
425
450
|
);
|
|
426
451
|
});
|
|
452
|
+
|
|
453
|
+
return colonyId;
|
|
427
454
|
}
|
|
428
455
|
|
|
429
456
|
// โโโ Custom message renderer for colony progress signals โโโ
|
|
@@ -496,8 +523,8 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
496
523
|
pi.registerShortcut("ctrl+shift+a", {
|
|
497
524
|
description: "Show ant colony details",
|
|
498
525
|
async handler(ctx) {
|
|
499
|
-
if (
|
|
500
|
-
ctx.ui.notify("No
|
|
526
|
+
if (colonies.size === 0) {
|
|
527
|
+
ctx.ui.notify("No colonies are currently running.", "info");
|
|
501
528
|
return;
|
|
502
529
|
}
|
|
503
530
|
|
|
@@ -507,9 +534,18 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
507
534
|
let cachedLines: string[] | undefined;
|
|
508
535
|
let currentTab: "tasks" | "streams" | "log" = "tasks";
|
|
509
536
|
let taskFilter: "all" | "active" | "done" | "failed" = "all";
|
|
537
|
+
/** Which colony to display (cycles with 'n'). */
|
|
538
|
+
let selectedColonyIdx = 0;
|
|
539
|
+
|
|
540
|
+
const getSelectedColony = (): BackgroundColony | null => {
|
|
541
|
+
const ids = [...colonies.keys()];
|
|
542
|
+
if (ids.length === 0) return null;
|
|
543
|
+
const idx = selectedColonyIdx % ids.length;
|
|
544
|
+
return colonies.get(ids[idx]) ?? null;
|
|
545
|
+
};
|
|
510
546
|
|
|
511
547
|
const buildLines = (width: number): string[] => {
|
|
512
|
-
const c =
|
|
548
|
+
const c = getSelectedColony();
|
|
513
549
|
if (!c) return [theme.fg("muted", " No colony running.")];
|
|
514
550
|
|
|
515
551
|
const lines: string[] = [];
|
|
@@ -525,8 +561,18 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
525
561
|
const activeAnts = c.antStreams.size;
|
|
526
562
|
const barWidth = Math.max(10, Math.min(24, w - 28));
|
|
527
563
|
|
|
564
|
+
// Show colony selector if multiple are running
|
|
565
|
+
if (colonies.size > 1) {
|
|
566
|
+
const ids = [...colonies.keys()];
|
|
567
|
+
const idx = selectedColonyIdx % ids.length;
|
|
568
|
+
const selector = ids
|
|
569
|
+
.map((id, i) => (i === idx ? theme.fg("accent", theme.bold(`[${id}]`)) : theme.fg("muted", id)))
|
|
570
|
+
.join(" ");
|
|
571
|
+
lines.push(` ${selector} ${theme.fg("dim", "(n = next colony)")}`);
|
|
572
|
+
}
|
|
573
|
+
|
|
528
574
|
lines.push(
|
|
529
|
-
theme.fg("accent", theme.bold(
|
|
575
|
+
theme.fg("accent", theme.bold(` ๐ Colony [${c.id}]`)) + theme.fg("muted", ` โ ${elapsed} โ ${cost}`),
|
|
530
576
|
);
|
|
531
577
|
lines.push(theme.fg("muted", ` Goal: ${trim(c.goal, w - 8)}`));
|
|
532
578
|
lines.push(
|
|
@@ -705,6 +751,7 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
705
751
|
else if (data.toLowerCase() === "a") taskFilter = "active";
|
|
706
752
|
else if (data.toLowerCase() === "d") taskFilter = "done";
|
|
707
753
|
else if (data.toLowerCase() === "f") taskFilter = "failed";
|
|
754
|
+
else if (data.toLowerCase() === "n") selectedColonyIdx++;
|
|
708
755
|
else return;
|
|
709
756
|
|
|
710
757
|
cachedWidth = undefined;
|
|
@@ -747,18 +794,6 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
747
794
|
}),
|
|
748
795
|
|
|
749
796
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
750
|
-
if (activeColony) {
|
|
751
|
-
return {
|
|
752
|
-
content: [
|
|
753
|
-
{
|
|
754
|
-
type: "text",
|
|
755
|
-
text: "A colony is already running in the background. Use /colony-stop to cancel it first.",
|
|
756
|
-
},
|
|
757
|
-
],
|
|
758
|
-
isError: true,
|
|
759
|
-
};
|
|
760
|
-
}
|
|
761
|
-
|
|
762
797
|
const currentModel = ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : null;
|
|
763
798
|
if (!currentModel) {
|
|
764
799
|
return {
|
|
@@ -788,13 +823,13 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
788
823
|
}
|
|
789
824
|
|
|
790
825
|
// Interactive mode: run in background
|
|
791
|
-
launchBackgroundColony(colonyParams);
|
|
826
|
+
const launchedId = launchBackgroundColony(colonyParams);
|
|
792
827
|
|
|
793
828
|
return {
|
|
794
829
|
content: [
|
|
795
830
|
{
|
|
796
831
|
type: "text",
|
|
797
|
-
text: `[COLONY_SIGNAL:LAUNCHED]\n๐ Colony launched in background.\nGoal: ${params.goal}\n\nThe colony runs autonomously in passive mode. Progress is pushed via [COLONY_SIGNAL:*] follow-up messages. Do not poll bg_colony_status unless the user explicitly asks for a manual snapshot.`,
|
|
832
|
+
text: `[COLONY_SIGNAL:LAUNCHED] [${launchedId}]\n๐ Colony [${launchedId}] launched in background (${colonies.size} active).\nGoal: ${params.goal}\n\nThe colony runs autonomously in passive mode. Progress is pushed via [COLONY_SIGNAL:*] follow-up messages. Do not poll bg_colony_status unless the user explicitly asks for a manual snapshot.`,
|
|
798
833
|
},
|
|
799
834
|
],
|
|
800
835
|
};
|
|
@@ -818,9 +853,17 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
818
853
|
container.addChild(
|
|
819
854
|
new Text(theme.fg("success", "โ ") + theme.fg("toolTitle", theme.bold("Colony launched in background")), 0, 0),
|
|
820
855
|
);
|
|
821
|
-
if (
|
|
822
|
-
|
|
823
|
-
|
|
856
|
+
if (colonies.size > 0) {
|
|
857
|
+
for (const colony of colonies.values()) {
|
|
858
|
+
container.addChild(new Text(theme.fg("muted", ` [${colony.id}] ${colony.goal.slice(0, 65)}`), 0, 0));
|
|
859
|
+
}
|
|
860
|
+
container.addChild(
|
|
861
|
+
new Text(
|
|
862
|
+
theme.fg("muted", ` ${colonies.size} active โ Ctrl+Shift+A for details โ /colony-stop to cancel`),
|
|
863
|
+
0,
|
|
864
|
+
0,
|
|
865
|
+
),
|
|
866
|
+
);
|
|
824
867
|
}
|
|
825
868
|
return container;
|
|
826
869
|
},
|
|
@@ -828,9 +871,8 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
828
871
|
|
|
829
872
|
// โโโ Helper: build status summary โโโ
|
|
830
873
|
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
const c = activeColony;
|
|
874
|
+
/** Build a status summary for a single colony. */
|
|
875
|
+
function buildColonyStatusText(c: BackgroundColony): string {
|
|
834
876
|
const state = c.state;
|
|
835
877
|
const elapsed = state ? formatDuration(Date.now() - state.createdAt) : "0s";
|
|
836
878
|
const m = state?.metrics;
|
|
@@ -853,15 +895,29 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
853
895
|
return lines.join("\n");
|
|
854
896
|
}
|
|
855
897
|
|
|
898
|
+
/** Build a status summary for all running colonies. */
|
|
899
|
+
function buildStatusText(): string {
|
|
900
|
+
if (colonies.size === 0) return "No colonies are currently running.";
|
|
901
|
+
if (colonies.size === 1) {
|
|
902
|
+
const colony = colonies.values().next().value;
|
|
903
|
+
return colony ? buildColonyStatusText(colony) : "No colonies are currently running.";
|
|
904
|
+
}
|
|
905
|
+
const parts: string[] = [`${colonies.size} colonies running:\n`];
|
|
906
|
+
for (const colony of colonies.values()) {
|
|
907
|
+
parts.push(`โโ [${colony.id}] โโ\n${buildColonyStatusText(colony)}\n`);
|
|
908
|
+
}
|
|
909
|
+
return parts.join("\n");
|
|
910
|
+
}
|
|
911
|
+
|
|
856
912
|
// โโโ Tool: bg_colony_status โโโ
|
|
857
913
|
pi.registerTool({
|
|
858
914
|
name: "bg_colony_status",
|
|
859
915
|
label: "Colony Status",
|
|
860
916
|
description:
|
|
861
|
-
"Optional manual snapshot for
|
|
917
|
+
"Optional manual snapshot for running colonies. Progress is pushed passively via COLONY_SIGNAL follow-up messages; call this only when the user explicitly asks.",
|
|
862
918
|
parameters: Type.Object({}),
|
|
863
919
|
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
864
|
-
if (
|
|
920
|
+
if (colonies.size === 0) {
|
|
865
921
|
return {
|
|
866
922
|
content: [{ type: "text" as const, text: "No colony is currently running." }],
|
|
867
923
|
};
|
|
@@ -902,70 +958,171 @@ export default function antColonyExtension(pi: ExtensionAPI) {
|
|
|
902
958
|
},
|
|
903
959
|
});
|
|
904
960
|
|
|
961
|
+
// โโโ Command: /colony โโโ
|
|
962
|
+
pi.registerCommand("colony", {
|
|
963
|
+
description: "Launch an ant colony swarm to accomplish a goal",
|
|
964
|
+
async handler(args, ctx) {
|
|
965
|
+
const goal = args.trim();
|
|
966
|
+
if (!goal) {
|
|
967
|
+
ctx.ui.notify("Usage: /colony <goal> โ describe what the colony should accomplish", "warning");
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
const currentModel = ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : null;
|
|
972
|
+
if (!currentModel) {
|
|
973
|
+
ctx.ui.notify("Colony failed: no model available in current session.", "error");
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
const id = launchBackgroundColony({
|
|
978
|
+
cwd: ctx.cwd,
|
|
979
|
+
goal,
|
|
980
|
+
currentModel,
|
|
981
|
+
modelOverrides: {},
|
|
982
|
+
modelRegistry: ctx.modelRegistry ?? undefined,
|
|
983
|
+
});
|
|
984
|
+
ctx.ui.notify(
|
|
985
|
+
`๐[${id}] Colony launched (${colonies.size} active): ${goal.slice(0, 70)}${goal.length > 70 ? "..." : ""}`,
|
|
986
|
+
"info",
|
|
987
|
+
);
|
|
988
|
+
},
|
|
989
|
+
});
|
|
990
|
+
|
|
991
|
+
// โโโ Command: /colony-count โโโ
|
|
992
|
+
pi.registerCommand("colony-count", {
|
|
993
|
+
description: "Show how many colonies are currently running",
|
|
994
|
+
async handler(_args, ctx) {
|
|
995
|
+
if (colonies.size === 0) {
|
|
996
|
+
ctx.ui.notify("No colonies running.", "info");
|
|
997
|
+
} else {
|
|
998
|
+
const ids = [...colonies.values()].map((c) => `[${c.id}] ${c.goal.slice(0, 50)}`).join("\n ");
|
|
999
|
+
ctx.ui.notify(`${colonies.size} active ${colonies.size === 1 ? "colony" : "colonies"}:\n ${ids}`, "info");
|
|
1000
|
+
}
|
|
1001
|
+
},
|
|
1002
|
+
});
|
|
1003
|
+
|
|
905
1004
|
// โโโ Command: /colony-status โโโ
|
|
906
1005
|
pi.registerCommand("colony-status", {
|
|
907
|
-
description: "Show current colony progress",
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
1006
|
+
description: "Show current colony progress (optionally specify ID: /colony-status c1)",
|
|
1007
|
+
getArgumentCompletions(prefix) {
|
|
1008
|
+
const items = [...colonies.keys()]
|
|
1009
|
+
.filter((id) => id.startsWith(prefix))
|
|
1010
|
+
.map((id) => {
|
|
1011
|
+
const c = colonies.get(id);
|
|
1012
|
+
return { value: id, label: `${id} โ ${c?.goal.slice(0, 50) ?? ""}` };
|
|
1013
|
+
});
|
|
1014
|
+
return items.length > 0 ? items : null;
|
|
1015
|
+
},
|
|
1016
|
+
async handler(args, ctx) {
|
|
1017
|
+
const idArg = args.trim() || undefined;
|
|
1018
|
+
if (colonies.size === 0) {
|
|
1019
|
+
ctx.ui.notify("No colonies are currently running.", "info");
|
|
911
1020
|
return;
|
|
912
1021
|
}
|
|
913
|
-
|
|
1022
|
+
if (idArg) {
|
|
1023
|
+
const colony = resolveColony(idArg);
|
|
1024
|
+
if (!colony) {
|
|
1025
|
+
ctx.ui.notify(`Colony "${idArg}" not found. Active: ${[...colonies.keys()].join(", ")}`, "warning");
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
1028
|
+
ctx.ui.notify(buildColonyStatusText(colony), "info");
|
|
1029
|
+
} else {
|
|
1030
|
+
ctx.ui.notify(buildStatusText(), "info");
|
|
1031
|
+
}
|
|
914
1032
|
},
|
|
915
1033
|
});
|
|
916
1034
|
|
|
917
1035
|
// โโโ Command: /colony-stop โโโ
|
|
918
1036
|
pi.registerCommand("colony-stop", {
|
|
919
|
-
description: "Stop
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
1037
|
+
description: "Stop a colony (specify ID, or stops all if none given)",
|
|
1038
|
+
getArgumentCompletions(prefix) {
|
|
1039
|
+
const items = [
|
|
1040
|
+
{ value: "all", label: "all โ Stop all running colonies" },
|
|
1041
|
+
...[...colonies.keys()]
|
|
1042
|
+
.filter((id) => id.startsWith(prefix))
|
|
1043
|
+
.map((id) => {
|
|
1044
|
+
const c = colonies.get(id);
|
|
1045
|
+
return { value: id, label: `${id} โ ${c?.goal.slice(0, 50) ?? ""}` };
|
|
1046
|
+
}),
|
|
1047
|
+
].filter((i) => i.value.startsWith(prefix));
|
|
1048
|
+
return items.length > 0 ? items : null;
|
|
1049
|
+
},
|
|
1050
|
+
async handler(args, ctx) {
|
|
1051
|
+
const idArg = args.trim() || undefined;
|
|
1052
|
+
if (colonies.size === 0) {
|
|
1053
|
+
ctx.ui.notify("No colonies are currently running.", "info");
|
|
923
1054
|
return;
|
|
924
1055
|
}
|
|
925
|
-
|
|
926
|
-
|
|
1056
|
+
if (!idArg || idArg === "all") {
|
|
1057
|
+
const count = colonies.size;
|
|
1058
|
+
for (const colony of colonies.values()) {
|
|
1059
|
+
colony.abortController.abort();
|
|
1060
|
+
}
|
|
1061
|
+
ctx.ui.notify(`๐ Abort signal sent to ${count} ${count === 1 ? "colony" : "colonies"}.`, "warning");
|
|
1062
|
+
} else {
|
|
1063
|
+
const colony = resolveColony(idArg);
|
|
1064
|
+
if (!colony) {
|
|
1065
|
+
ctx.ui.notify(`Colony "${idArg}" not found. Active: ${[...colonies.keys()].join(", ")}`, "warning");
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
colony.abortController.abort();
|
|
1069
|
+
ctx.ui.notify(`๐[${colony.id}] Abort signal sent. Waiting for ants to finish...`, "warning");
|
|
1070
|
+
}
|
|
927
1071
|
},
|
|
928
1072
|
});
|
|
929
1073
|
|
|
930
1074
|
pi.registerCommand("colony-resume", {
|
|
931
|
-
description: "Resume
|
|
932
|
-
async handler(
|
|
933
|
-
|
|
934
|
-
|
|
1075
|
+
description: "Resume colonies from their last checkpoint (resumes all resumable by default)",
|
|
1076
|
+
async handler(args, ctx) {
|
|
1077
|
+
const all = Nest.findAllResumable(ctx.cwd);
|
|
1078
|
+
if (all.length === 0) {
|
|
1079
|
+
ctx.ui.notify("No resumable colonies found.", "info");
|
|
935
1080
|
return;
|
|
936
1081
|
}
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
1082
|
+
|
|
1083
|
+
// If an argument is given, try to match a specific colony ID
|
|
1084
|
+
const target = args.trim();
|
|
1085
|
+
const toResume = target ? all.filter((r) => r.colonyId === target) : [all[0]];
|
|
1086
|
+
|
|
1087
|
+
if (toResume.length === 0) {
|
|
1088
|
+
ctx.ui.notify(`Colony "${target}" not found. Resumable: ${all.map((r) => r.colonyId).join(", ")}`, "warning");
|
|
940
1089
|
return;
|
|
941
1090
|
}
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
1091
|
+
|
|
1092
|
+
for (const found of toResume) {
|
|
1093
|
+
const id = launchBackgroundColony(
|
|
1094
|
+
{
|
|
1095
|
+
cwd: ctx.cwd,
|
|
1096
|
+
goal: found.state.goal,
|
|
1097
|
+
maxCost: found.state.maxCost ?? undefined,
|
|
1098
|
+
currentModel: ctx.currentModel,
|
|
1099
|
+
modelOverrides: {},
|
|
1100
|
+
modelRegistry: ctx.modelRegistry,
|
|
1101
|
+
},
|
|
1102
|
+
true,
|
|
1103
|
+
);
|
|
1104
|
+
ctx.ui.notify(`๐[${id}] Resuming: ${found.state.goal.slice(0, 60)}...`, "info");
|
|
1105
|
+
}
|
|
954
1106
|
},
|
|
955
1107
|
});
|
|
956
1108
|
|
|
957
1109
|
// โโโ Cleanup on shutdown โโโ
|
|
958
1110
|
pi.on("session_shutdown", async () => {
|
|
959
|
-
if (
|
|
960
|
-
|
|
961
|
-
|
|
1111
|
+
if (colonies.size > 0) {
|
|
1112
|
+
for (const colony of colonies.values()) {
|
|
1113
|
+
colony.abortController.abort();
|
|
1114
|
+
}
|
|
1115
|
+
// Wait for all colonies to finish gracefully (max 5s)
|
|
962
1116
|
try {
|
|
963
|
-
await Promise.race([
|
|
1117
|
+
await Promise.race([
|
|
1118
|
+
Promise.all([...colonies.values()].map((c) => c.promise)),
|
|
1119
|
+
new Promise((r) => setTimeout(r, 5000)),
|
|
1120
|
+
]);
|
|
964
1121
|
} catch {
|
|
965
1122
|
/* ignore */
|
|
966
1123
|
}
|
|
967
1124
|
pi.events.emit("ant-colony:clear-ui");
|
|
968
|
-
|
|
1125
|
+
colonies.clear();
|
|
969
1126
|
}
|
|
970
1127
|
});
|
|
971
1128
|
}
|
|
@@ -494,7 +494,18 @@ export class Nest {
|
|
|
494
494
|
* scouting, working, or reviewing and has no `finishedAt` timestamp).
|
|
495
495
|
*/
|
|
496
496
|
static findResumable(cwd: string): { colonyId: string; state: ColonyState } | null {
|
|
497
|
+
const all = Nest.findAllResumable(cwd);
|
|
498
|
+
return all.length > 0 ? all[0] : null;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Find all resumable colonies in the working directory.
|
|
503
|
+
* Returns colonies whose state is incomplete (not done/failed/budget_exceeded).
|
|
504
|
+
* Sorted by `createdAt` descending so the most recent colony is first.
|
|
505
|
+
*/
|
|
506
|
+
static findAllResumable(cwd: string): Array<{ colonyId: string; state: ColonyState }> {
|
|
497
507
|
const parentDir = path.join(cwd, ".ant-colony");
|
|
508
|
+
const results: Array<{ colonyId: string; state: ColonyState }> = [];
|
|
498
509
|
try {
|
|
499
510
|
for (const dir of fs.readdirSync(parentDir)) {
|
|
500
511
|
const stateFile = path.join(parentDir, dir, "state.json");
|
|
@@ -508,13 +519,14 @@ export class Nest {
|
|
|
508
519
|
state.status !== "failed" &&
|
|
509
520
|
state.status !== "budget_exceeded"
|
|
510
521
|
) {
|
|
511
|
-
|
|
522
|
+
results.push({ colonyId: dir, state });
|
|
512
523
|
}
|
|
513
524
|
}
|
|
514
525
|
} catch {
|
|
515
526
|
// No .ant-colony directory โ nothing to resume
|
|
516
527
|
}
|
|
517
|
-
|
|
528
|
+
results.sort((a, b) => (b.state.createdAt ?? 0) - (a.state.createdAt ?? 0));
|
|
529
|
+
return results;
|
|
518
530
|
}
|
|
519
531
|
|
|
520
532
|
/**
|