oh-pi 0.1.53 → 0.1.55
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
|
@@ -43,7 +43,7 @@ function statusIcon(status: string): string {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
function casteIcon(caste: string): string {
|
|
46
|
-
return caste === "scout" ? "🔍" : caste === "soldier" ? "🛡️" : "⚒️";
|
|
46
|
+
return caste === "scout" ? "🔍" : caste === "soldier" ? "🛡️" : caste === "drone" ? "⚙️" : "⚒️";
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
// ═══ Background colony state ═══
|
|
@@ -20,7 +20,7 @@ import type {
|
|
|
20
20
|
} from "./types.js";
|
|
21
21
|
import { DEFAULT_ANT_CONFIGS } from "./types.js";
|
|
22
22
|
import { Nest } from "./nest.js";
|
|
23
|
-
import { spawnAnt, makeTaskId } from "./spawner.js";
|
|
23
|
+
import { spawnAnt, runDrone, makeTaskId } from "./spawner.js";
|
|
24
24
|
import { adapt, sampleSystem, defaultConcurrency } from "./concurrency.js";
|
|
25
25
|
import type { AuthStorage, ModelRegistry } from "@mariozechner/pi-coding-agent";
|
|
26
26
|
|
|
@@ -72,7 +72,7 @@ function makeInitialScoutTask(goal: string): Task {
|
|
|
72
72
|
|
|
73
73
|
function childTaskFromParsed(
|
|
74
74
|
parentId: string,
|
|
75
|
-
parsed: { title: string; description: string; files: string[]; caste: AntCaste; priority: TaskPriority },
|
|
75
|
+
parsed: { title: string; description: string; files: string[]; caste: AntCaste; priority: TaskPriority; context?: string },
|
|
76
76
|
): Task {
|
|
77
77
|
return {
|
|
78
78
|
id: makeTaskId(),
|
|
@@ -83,6 +83,7 @@ function childTaskFromParsed(
|
|
|
83
83
|
status: "pending",
|
|
84
84
|
priority: parsed.priority,
|
|
85
85
|
files: parsed.files,
|
|
86
|
+
context: parsed.context || undefined,
|
|
86
87
|
claimedBy: null,
|
|
87
88
|
result: null,
|
|
88
89
|
error: null,
|
|
@@ -176,7 +177,9 @@ async function runAntWave(opts: WaveOptions): Promise<"ok"> {
|
|
|
176
177
|
callbacks.onAntSpawn(ant, task);
|
|
177
178
|
|
|
178
179
|
try {
|
|
179
|
-
const result =
|
|
180
|
+
const result = caste === "drone"
|
|
181
|
+
? await runDrone(cwd, nest, task)
|
|
182
|
+
: await spawnAnt(cwd, nest, task, config, signal, callbacks.onAntStream, opts.authStorage, opts.modelRegistry);
|
|
180
183
|
callbacks.onAntDone(result.ant, task, result.output);
|
|
181
184
|
|
|
182
185
|
if (result.rateLimited) {
|
|
@@ -343,7 +346,7 @@ export async function runColony(opts: QueenOptions): Promise<ColonyState> {
|
|
|
343
346
|
callbacks.onPhase("scouting", "Dispatching scout ant to explore codebase...");
|
|
344
347
|
await runAntWave({ ...waveBase, caste: "scout" });
|
|
345
348
|
|
|
346
|
-
let workerTasks = nest.getAllTasks().filter(t => t.caste === "worker" && t.status === "pending");
|
|
349
|
+
let workerTasks = nest.getAllTasks().filter(t => (t.caste === "worker" || t.caste === "drone") && t.status === "pending");
|
|
347
350
|
|
|
348
351
|
// 只在完全没有 worker 任务时才重试一次
|
|
349
352
|
if (workerTasks.length === 0) {
|
|
@@ -371,7 +374,7 @@ export async function runColony(opts: QueenOptions): Promise<ColonyState> {
|
|
|
371
374
|
nest.writeTask(relayTask);
|
|
372
375
|
callbacks.onPhase("scouting", "Scout relay: generating worker tasks...");
|
|
373
376
|
await runAntWave({ ...waveBase, caste: "scout" });
|
|
374
|
-
workerTasks = nest.getAllTasks().filter(t => t.caste === "worker" && t.status === "pending");
|
|
377
|
+
workerTasks = nest.getAllTasks().filter(t => (t.caste === "worker" || t.caste === "drone") && t.status === "pending");
|
|
375
378
|
}
|
|
376
379
|
|
|
377
380
|
if (workerTasks.length === 0) {
|
|
@@ -383,12 +386,24 @@ export async function runColony(opts: QueenOptions): Promise<ColonyState> {
|
|
|
383
386
|
|
|
384
387
|
// ═══ Phase 2: 工作 ═══
|
|
385
388
|
nest.updateState({ status: "working" });
|
|
389
|
+
|
|
390
|
+
// 先执行 drone 任务(零 LLM 成本)
|
|
391
|
+
const droneTasks = nest.getAllTasks().filter(t => t.caste === "drone" && t.status === "pending");
|
|
392
|
+
if (droneTasks.length > 0) {
|
|
393
|
+
callbacks.onPhase("working", `${droneTasks.length} drone tasks. Executing rules...`);
|
|
394
|
+
await runAntWave({ ...waveBase, caste: "drone" });
|
|
395
|
+
}
|
|
396
|
+
|
|
386
397
|
callbacks.onPhase("working", `${workerTasks.length} tasks discovered. Dispatching worker ants...`);
|
|
387
398
|
await runAntWave({ ...waveBase, caste: "worker" });
|
|
388
399
|
|
|
389
400
|
// 处理工蚁产生的子任务(可能有多轮)
|
|
390
401
|
let rounds = 0;
|
|
391
402
|
while (rounds < 3) {
|
|
403
|
+
// 先跑 drone 子任务
|
|
404
|
+
const pendingDrones = nest.getAllTasks().filter(t => t.caste === "drone" && t.status === "pending");
|
|
405
|
+
if (pendingDrones.length > 0) await runAntWave({ ...waveBase, caste: "drone" });
|
|
406
|
+
|
|
392
407
|
const remaining = nest.getAllTasks().filter(t =>
|
|
393
408
|
t.caste === "worker" && (t.status === "pending" || t.status === "blocked")
|
|
394
409
|
);
|
|
@@ -398,9 +413,18 @@ export async function runColony(opts: QueenOptions): Promise<ColonyState> {
|
|
|
398
413
|
await runAntWave({ ...waveBase, caste: "worker" });
|
|
399
414
|
}
|
|
400
415
|
|
|
416
|
+
// ═══ Auto-check: run tsc before soldier review ═══
|
|
417
|
+
let tscPassed = true;
|
|
418
|
+
try {
|
|
419
|
+
const { execSync } = await import("node:child_process");
|
|
420
|
+
execSync("npx tsc --noEmit", { cwd: opts.cwd, timeout: 30000, stdio: "pipe" });
|
|
421
|
+
} catch {
|
|
422
|
+
tscPassed = false;
|
|
423
|
+
}
|
|
424
|
+
|
|
401
425
|
// ═══ Phase 3: 审查 ═══
|
|
402
426
|
const completedWorkerTasks = nest.getAllTasks().filter(t => t.caste === "worker" && t.status === "done");
|
|
403
|
-
if (completedWorkerTasks.length > 0) {
|
|
427
|
+
if (completedWorkerTasks.length > 0 && (!tscPassed || completedWorkerTasks.length > 3)) {
|
|
404
428
|
nest.updateState({ status: "reviewing" });
|
|
405
429
|
callbacks.onPhase("reviewing", "Dispatching soldier ants to review changes...");
|
|
406
430
|
const reviewTask = makeReviewTask(completedWorkerTasks);
|
|
@@ -57,6 +57,7 @@ export interface ParsedSubTask {
|
|
|
57
57
|
files: string[];
|
|
58
58
|
caste: AntCaste;
|
|
59
59
|
priority: 1 | 2 | 3 | 4 | 5;
|
|
60
|
+
context?: string;
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
const CASTE_PROMPTS: Record<AntCaste, string> = {
|
|
@@ -67,6 +68,7 @@ Behavior:
|
|
|
67
68
|
- Identify files, functions, dependencies related to the goal
|
|
68
69
|
- IMPORTANT: After EACH tool call, summarize what you found so far. Do NOT wait until the end.
|
|
69
70
|
- Report findings as structured intelligence for Worker Ants
|
|
71
|
+
- For each recommended task, include the KEY code snippets (with file:line) the worker will need — this saves workers from re-reading files
|
|
70
72
|
|
|
71
73
|
Output format (MUST follow exactly):
|
|
72
74
|
## Discoveries
|
|
@@ -79,6 +81,9 @@ For each task the colony should do next:
|
|
|
79
81
|
- files: <comma-separated file paths>
|
|
80
82
|
- caste: worker
|
|
81
83
|
- priority: <1-5, 1=highest>
|
|
84
|
+
- context: <relevant code snippets that the worker will need, with file:line references>
|
|
85
|
+
|
|
86
|
+
Use caste "drone" instead of "worker" for simple tasks that can be done with a single bash command (file copy, find-replace, formatting, running tests). Drone description should be the exact bash command to execute.
|
|
82
87
|
|
|
83
88
|
## Warnings
|
|
84
89
|
Any risks, blockers, or conflicts detected.`,
|
|
@@ -142,6 +147,9 @@ function buildPrompt(task: Task, pheromoneContext: string, castePrompt: string,
|
|
|
142
147
|
if (task.files.length > 0) {
|
|
143
148
|
prompt += `**Files scope:** ${task.files.join(", ")}\n`;
|
|
144
149
|
}
|
|
150
|
+
if (task.context) {
|
|
151
|
+
prompt += `\n## Pre-loaded Context (from scout)\n${task.context}\n`;
|
|
152
|
+
}
|
|
145
153
|
if (/[\u4e00-\u9fff]/.test(task.description)) {
|
|
146
154
|
prompt += '\nIMPORTANT: Follow the language requirements specified in the task description. If the task says to write in Chinese, write in Chinese.\n';
|
|
147
155
|
}
|
|
@@ -152,13 +160,18 @@ function buildPrompt(task: Task, pheromoneContext: string, castePrompt: string,
|
|
|
152
160
|
function parseSubTasks(output: string): ParsedSubTask[] {
|
|
153
161
|
const tasks: ParsedSubTask[] = [];
|
|
154
162
|
const regex = /### TASK:\s*(.+)\n(?:- description:\s*(.+)\n)?(?:- files:\s*(.+)\n)?(?:- caste:\s*(\w+)\n)?(?:- priority:\s*(\d))?/g;
|
|
163
|
+
const taskBlocks = output.split(/(?=### TASK:)/);
|
|
155
164
|
for (const m of output.matchAll(regex)) {
|
|
165
|
+
const block = taskBlocks.find(b => b.includes(`### TASK: ${m[1]?.trim()}`)) || "";
|
|
166
|
+
const ctxMatch = block.match(/- context:\s*([\s\S]*?)(?=### TASK:|## |\n\n|$)/);
|
|
167
|
+
const context = ctxMatch?.[1]?.trim() || undefined;
|
|
156
168
|
tasks.push({
|
|
157
169
|
title: m[1]?.trim() || "Untitled",
|
|
158
170
|
description: m[2]?.trim() || m[1]?.trim() || "",
|
|
159
171
|
files: (m[3]?.trim() || "").split(",").map(f => f.trim()).filter(Boolean),
|
|
160
172
|
caste: (m[4]?.trim() as AntCaste) || "worker",
|
|
161
173
|
priority: (parseInt(m[5] || "3") as 1 | 2 | 3 | 4 | 5) || 3,
|
|
174
|
+
context,
|
|
162
175
|
});
|
|
163
176
|
}
|
|
164
177
|
return tasks;
|
|
@@ -239,6 +252,49 @@ function makeMinimalResourceLoader(systemPrompt: string): ResourceLoader {
|
|
|
239
252
|
};
|
|
240
253
|
}
|
|
241
254
|
|
|
255
|
+
/**
|
|
256
|
+
* 运行 Drone — 纯规则执行,零 LLM 成本
|
|
257
|
+
* 任务描述即为要执行的 bash 命令
|
|
258
|
+
*/
|
|
259
|
+
export async function runDrone(
|
|
260
|
+
cwd: string,
|
|
261
|
+
nest: Nest,
|
|
262
|
+
task: Task,
|
|
263
|
+
): Promise<AntResult> {
|
|
264
|
+
const antId = makeAntId("drone");
|
|
265
|
+
const ant: Ant = {
|
|
266
|
+
id: antId, caste: "drone", status: "working", taskId: task.id,
|
|
267
|
+
pid: null, model: "none",
|
|
268
|
+
usage: { input: 0, output: 0, cost: 0, turns: 1 },
|
|
269
|
+
startedAt: Date.now(), finishedAt: null,
|
|
270
|
+
};
|
|
271
|
+
nest.updateAnt(ant);
|
|
272
|
+
nest.updateTaskStatus(task.id, "active");
|
|
273
|
+
|
|
274
|
+
try {
|
|
275
|
+
const { execSync } = await import("node:child_process");
|
|
276
|
+
const cmd = task.description;
|
|
277
|
+
const output = execSync(cmd, { cwd, encoding: "utf-8", timeout: 30000, stdio: "pipe" }).trim();
|
|
278
|
+
|
|
279
|
+
ant.status = "done";
|
|
280
|
+
ant.finishedAt = Date.now();
|
|
281
|
+
nest.updateAnt(ant);
|
|
282
|
+
nest.updateTaskStatus(task.id, "done", `## Completed\n${output || "(no output)"}`);
|
|
283
|
+
nest.dropPheromone({
|
|
284
|
+
id: makePheromoneId(), type: "completion", antId, antCaste: "drone",
|
|
285
|
+
taskId: task.id, content: `Drone executed: ${cmd.slice(0, 100)}`, files: task.files, strength: 1, createdAt: Date.now(),
|
|
286
|
+
});
|
|
287
|
+
return { ant, output, newTasks: [], pheromones: [], rateLimited: false };
|
|
288
|
+
} catch (e: any) {
|
|
289
|
+
const errStr = e.stderr?.toString() || String(e);
|
|
290
|
+
ant.status = "failed";
|
|
291
|
+
ant.finishedAt = Date.now();
|
|
292
|
+
nest.updateAnt(ant);
|
|
293
|
+
nest.updateTaskStatus(task.id, "failed", undefined, errStr.slice(0, 500));
|
|
294
|
+
return { ant, output: errStr, newTasks: [], pheromones: [], rateLimited: false };
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
242
298
|
/**
|
|
243
299
|
* 孵化并运行一只蚂蚁 — SDK 内嵌版
|
|
244
300
|
*/
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
// ═══ 蚂蚁角色 ═══
|
|
6
|
-
export type AntCaste = "scout" | "worker" | "soldier";
|
|
6
|
+
export type AntCaste = "scout" | "worker" | "soldier" | "drone";
|
|
7
7
|
|
|
8
8
|
export interface AntConfig {
|
|
9
9
|
caste: AntCaste;
|
|
@@ -17,6 +17,7 @@ export const DEFAULT_ANT_CONFIGS: Record<AntCaste, Omit<AntConfig, "systemPrompt
|
|
|
17
17
|
scout: { caste: "scout", model: "", tools: ["read", "bash", "grep", "find", "ls"], maxTurns: 8 },
|
|
18
18
|
worker: { caste: "worker", model: "", tools: ["read", "bash", "edit", "write", "grep", "find", "ls"], maxTurns: 15 },
|
|
19
19
|
soldier: { caste: "soldier", model: "", tools: ["read", "bash", "grep", "find", "ls"], maxTurns: 8 },
|
|
20
|
+
drone: { caste: "drone", model: "", tools: ["bash"], maxTurns: 1 },
|
|
20
21
|
};
|
|
21
22
|
|
|
22
23
|
/** Per-caste model overrides from user config */
|
|
@@ -35,6 +36,7 @@ export interface Task {
|
|
|
35
36
|
status: TaskStatus;
|
|
36
37
|
priority: TaskPriority;
|
|
37
38
|
files: string[]; // 锁定的文件
|
|
39
|
+
context?: string; // Scout 预加载的代码片段
|
|
38
40
|
claimedBy: string | null; // ant id
|
|
39
41
|
result: string | null;
|
|
40
42
|
error: string | null;
|