oh-pi 0.1.21 → 0.1.22
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
|
@@ -37,7 +37,7 @@ function formatTokens(n: number): string {
|
|
|
37
37
|
function statusIcon(status: string): string {
|
|
38
38
|
const icons: Record<string, string> = {
|
|
39
39
|
scouting: "🔍", working: "⚒️", reviewing: "🛡️",
|
|
40
|
-
done: "✅", failed: "❌",
|
|
40
|
+
done: "✅", failed: "❌", budget_exceeded: "💰",
|
|
41
41
|
};
|
|
42
42
|
return icons[status] || "🐜";
|
|
43
43
|
}
|
|
@@ -79,11 +79,20 @@ For simple single-file tasks, work directly without the colony.`,
|
|
|
79
79
|
parameters: Type.Object({
|
|
80
80
|
goal: Type.String({ description: "What the colony should accomplish" }),
|
|
81
81
|
maxAnts: Type.Optional(Type.Number({ description: "Max concurrent ants (default: auto-adapt)", minimum: 1, maximum: 8 })),
|
|
82
|
+
maxCost: Type.Optional(Type.Number({ description: "Max cost budget in USD (default: unlimited)", minimum: 0.01 })),
|
|
82
83
|
}),
|
|
83
84
|
|
|
84
85
|
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
85
86
|
const details: ColonyDetails = { state: null, phase: "initializing", log: [] };
|
|
86
87
|
|
|
88
|
+
// Resolve models: use current session model for worker/soldier, find a cheap model for scout
|
|
89
|
+
const currentModel = ctx.model?.id;
|
|
90
|
+
const modelOverrides: Record<string, string> = {};
|
|
91
|
+
if (currentModel) {
|
|
92
|
+
modelOverrides.worker = currentModel;
|
|
93
|
+
modelOverrides.soldier = currentModel;
|
|
94
|
+
}
|
|
95
|
+
|
|
87
96
|
const emit = () => {
|
|
88
97
|
const summary = details.state
|
|
89
98
|
? `${statusIcon(details.state.status)} Colony: ${details.phase}`
|
|
@@ -126,6 +135,8 @@ For simple single-file tasks, work directly without the colony.`,
|
|
|
126
135
|
cwd: ctx.cwd,
|
|
127
136
|
goal: params.goal,
|
|
128
137
|
maxAnts: params.maxAnts,
|
|
138
|
+
maxCost: params.maxCost,
|
|
139
|
+
modelOverrides,
|
|
129
140
|
signal: signal ?? undefined,
|
|
130
141
|
callbacks,
|
|
131
142
|
});
|
|
@@ -140,6 +151,7 @@ For simple single-file tasks, work directly without the colony.`,
|
|
|
140
151
|
`**Goal:** ${state.goal}`,
|
|
141
152
|
`**Status:** ${statusIcon(state.status)} ${state.status}`,
|
|
142
153
|
`**Duration:** ${elapsed}`,
|
|
154
|
+
...(state.maxCost != null ? [`**Budget:** ${formatCost(m.totalCost)} / ${formatCost(state.maxCost)}`] : []),
|
|
143
155
|
``,
|
|
144
156
|
`### Metrics`,
|
|
145
157
|
`- Tasks: ${m.tasksDone}/${m.tasksTotal} done, ${m.tasksFailed} failed`,
|
|
@@ -165,7 +177,7 @@ For simple single-file tasks, work directly without the colony.`,
|
|
|
165
177
|
return {
|
|
166
178
|
content: [{ type: "text", text: report }],
|
|
167
179
|
details: { ...details },
|
|
168
|
-
isError: state.status === "failed",
|
|
180
|
+
isError: state.status === "failed" || state.status === "budget_exceeded",
|
|
169
181
|
};
|
|
170
182
|
} catch (e) {
|
|
171
183
|
return {
|
|
@@ -184,6 +196,7 @@ For simple single-file tasks, work directly without the colony.`,
|
|
|
184
196
|
const goal = args.goal?.length > 60 ? args.goal.slice(0, 57) + "..." : args.goal;
|
|
185
197
|
text += "\n " + theme.fg("dim", goal || "...");
|
|
186
198
|
if (args.maxAnts) text += theme.fg("muted", ` (max: ${args.maxAnts})`);
|
|
199
|
+
if (args.maxCost) text += theme.fg("warning", ` (budget: $${args.maxCost})`);
|
|
187
200
|
return new Text(text, 0, 0);
|
|
188
201
|
},
|
|
189
202
|
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import type {
|
|
16
16
|
ColonyState, Task, Ant, AntCaste, ColonyMetrics,
|
|
17
|
-
ConcurrencyConfig, TaskPriority,
|
|
17
|
+
ConcurrencyConfig, TaskPriority, ModelOverrides,
|
|
18
18
|
} from "./types.js";
|
|
19
19
|
import { DEFAULT_ANT_CONFIGS } from "./types.js";
|
|
20
20
|
import { Nest } from "./nest.js";
|
|
@@ -33,6 +33,8 @@ export interface QueenOptions {
|
|
|
33
33
|
cwd: string;
|
|
34
34
|
goal: string;
|
|
35
35
|
maxAnts?: number;
|
|
36
|
+
maxCost?: number;
|
|
37
|
+
modelOverrides?: ModelOverrides;
|
|
36
38
|
signal?: AbortSignal;
|
|
37
39
|
callbacks: QueenCallbacks;
|
|
38
40
|
}
|
|
@@ -138,8 +140,11 @@ async function runAntWave(
|
|
|
138
140
|
caste: AntCaste,
|
|
139
141
|
signal: AbortSignal | undefined,
|
|
140
142
|
callbacks: QueenCallbacks,
|
|
141
|
-
|
|
142
|
-
|
|
143
|
+
modelOverrides?: ModelOverrides,
|
|
144
|
+
maxCost?: number,
|
|
145
|
+
): Promise<"ok" | "budget_exceeded"> {
|
|
146
|
+
const config = { ...DEFAULT_ANT_CONFIGS[caste] };
|
|
147
|
+
if (modelOverrides?.[caste]) config.model = modelOverrides[caste];
|
|
143
148
|
|
|
144
149
|
let backoffMs = 0; // 429 退避时间
|
|
145
150
|
|
|
@@ -191,6 +196,15 @@ async function runAntWave(
|
|
|
191
196
|
|
|
192
197
|
// 调度循环:持续派蚂蚁直到没有待处理任务
|
|
193
198
|
while (!signal?.aborted) {
|
|
199
|
+
// Budget check
|
|
200
|
+
if (maxCost != null) {
|
|
201
|
+
const currentCost = nest.getState().ants.reduce((s, a) => s + a.usage.cost, 0);
|
|
202
|
+
if (currentCost >= maxCost) {
|
|
203
|
+
callbacks.onPhase("working", `Budget exceeded ($${currentCost.toFixed(3)} >= $${maxCost.toFixed(2)}). Stopping.`);
|
|
204
|
+
return "budget_exceeded";
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
194
208
|
const state = nest.getState();
|
|
195
209
|
const pending = state.tasks.filter(t => t.status === "pending" && t.caste === caste);
|
|
196
210
|
if (pending.length === 0) break;
|
|
@@ -254,6 +268,7 @@ async function runAntWave(
|
|
|
254
268
|
backoffMs = 0;
|
|
255
269
|
}
|
|
256
270
|
}
|
|
271
|
+
return "ok";
|
|
257
272
|
}
|
|
258
273
|
|
|
259
274
|
/**
|
|
@@ -276,6 +291,8 @@ export async function runColony(opts: QueenOptions): Promise<ColonyState> {
|
|
|
276
291
|
antsSpawned: 0, totalCost: 0, totalTokens: 0,
|
|
277
292
|
startTime: Date.now(), throughputHistory: [],
|
|
278
293
|
},
|
|
294
|
+
maxCost: opts.maxCost ?? null,
|
|
295
|
+
modelOverrides: opts.modelOverrides ?? {},
|
|
279
296
|
createdAt: Date.now(),
|
|
280
297
|
finishedAt: null,
|
|
281
298
|
};
|
|
@@ -286,11 +303,22 @@ export async function runColony(opts: QueenOptions): Promise<ColonyState> {
|
|
|
286
303
|
|
|
287
304
|
nest.init(initialState);
|
|
288
305
|
const { signal, callbacks } = opts;
|
|
306
|
+
const mo = opts.modelOverrides;
|
|
307
|
+
const mc = opts.maxCost;
|
|
308
|
+
|
|
309
|
+
const budgetStop = (phase: string) => {
|
|
310
|
+
nest.updateState({ status: "budget_exceeded", finishedAt: Date.now() });
|
|
311
|
+
callbacks.onPhase("budget_exceeded" as any, phase);
|
|
312
|
+
const s = nest.getState();
|
|
313
|
+
callbacks.onComplete(s);
|
|
314
|
+
return s;
|
|
315
|
+
};
|
|
289
316
|
|
|
290
317
|
try {
|
|
291
318
|
// ═══ Phase 1: 侦察 ═══
|
|
292
319
|
callbacks.onPhase("scouting", "Dispatching scout ants to explore codebase...");
|
|
293
|
-
await runAntWave(nest, opts.cwd, "scout", signal, callbacks)
|
|
320
|
+
if (await runAntWave(nest, opts.cwd, "scout", signal, callbacks, mo, mc) === "budget_exceeded")
|
|
321
|
+
return budgetStop("Budget exceeded during scouting");
|
|
294
322
|
|
|
295
323
|
// 检查侦察结果,如果没有产生工蚁任务,用侦察结果让女王自己拆
|
|
296
324
|
const postScout = nest.getAllTasks();
|
|
@@ -306,7 +334,8 @@ export async function runColony(opts: QueenOptions): Promise<ColonyState> {
|
|
|
306
334
|
// ═══ Phase 2: 工作 ═══
|
|
307
335
|
nest.updateState({ status: "working" });
|
|
308
336
|
callbacks.onPhase("working", `${workerTasks.length} tasks discovered. Dispatching worker ants...`);
|
|
309
|
-
await runAntWave(nest, opts.cwd, "worker", signal, callbacks)
|
|
337
|
+
if (await runAntWave(nest, opts.cwd, "worker", signal, callbacks, mo, mc) === "budget_exceeded")
|
|
338
|
+
return budgetStop("Budget exceeded during working");
|
|
310
339
|
|
|
311
340
|
// 处理工蚁产生的子任务(可能有多轮)
|
|
312
341
|
let rounds = 0;
|
|
@@ -317,7 +346,8 @@ export async function runColony(opts: QueenOptions): Promise<ColonyState> {
|
|
|
317
346
|
if (remaining.length === 0) break;
|
|
318
347
|
rounds++;
|
|
319
348
|
callbacks.onPhase("working", `Round ${rounds + 1}: ${remaining.length} sub-tasks from workers...`);
|
|
320
|
-
await runAntWave(nest, opts.cwd, "worker", signal, callbacks)
|
|
349
|
+
if (await runAntWave(nest, opts.cwd, "worker", signal, callbacks, mo, mc) === "budget_exceeded")
|
|
350
|
+
return budgetStop("Budget exceeded during sub-tasks");
|
|
321
351
|
}
|
|
322
352
|
|
|
323
353
|
// ═══ Phase 3: 审查 ═══
|
|
@@ -327,7 +357,8 @@ export async function runColony(opts: QueenOptions): Promise<ColonyState> {
|
|
|
327
357
|
callbacks.onPhase("reviewing", "Dispatching soldier ants to review changes...");
|
|
328
358
|
const reviewTask = makeReviewTask(completedWorkerTasks);
|
|
329
359
|
nest.writeTask(reviewTask);
|
|
330
|
-
await runAntWave(nest, opts.cwd, "soldier", signal, callbacks)
|
|
360
|
+
if (await runAntWave(nest, opts.cwd, "soldier", signal, callbacks, mo, mc) === "budget_exceeded")
|
|
361
|
+
return budgetStop("Budget exceeded during review");
|
|
331
362
|
|
|
332
363
|
// 兵蚁产生的修复任务
|
|
333
364
|
const fixTasks = nest.getAllTasks().filter(t =>
|
|
@@ -336,7 +367,8 @@ export async function runColony(opts: QueenOptions): Promise<ColonyState> {
|
|
|
336
367
|
if (fixTasks.length > 0) {
|
|
337
368
|
nest.updateState({ status: "working" });
|
|
338
369
|
callbacks.onPhase("working", `${fixTasks.length} fix tasks from review. Dispatching workers...`);
|
|
339
|
-
await runAntWave(nest, opts.cwd, "worker", signal, callbacks)
|
|
370
|
+
if (await runAntWave(nest, opts.cwd, "worker", signal, callbacks, mo, mc) === "budget_exceeded")
|
|
371
|
+
return budgetStop("Budget exceeded during fixes");
|
|
340
372
|
}
|
|
341
373
|
}
|
|
342
374
|
|
|
@@ -19,6 +19,9 @@ export const DEFAULT_ANT_CONFIGS: Record<AntCaste, Omit<AntConfig, "systemPrompt
|
|
|
19
19
|
soldier: { caste: "soldier", model: "claude-sonnet-4-5", tools: ["read", "bash", "grep", "find", "ls"], maxTurns: 8 },
|
|
20
20
|
};
|
|
21
21
|
|
|
22
|
+
/** Per-caste model overrides from user config */
|
|
23
|
+
export type ModelOverrides = Partial<Record<AntCaste, string>>;
|
|
24
|
+
|
|
22
25
|
// ═══ 任务 (Food Source) ═══
|
|
23
26
|
export type TaskStatus = "pending" | "claimed" | "active" | "done" | "failed" | "blocked";
|
|
24
27
|
export type TaskPriority = 1 | 2 | 3 | 4 | 5; // 1=highest
|
|
@@ -79,12 +82,14 @@ export interface Ant {
|
|
|
79
82
|
export interface ColonyState {
|
|
80
83
|
id: string;
|
|
81
84
|
goal: string;
|
|
82
|
-
status: "scouting" | "working" | "reviewing" | "done" | "failed";
|
|
85
|
+
status: "scouting" | "working" | "reviewing" | "done" | "failed" | "budget_exceeded";
|
|
83
86
|
tasks: Task[];
|
|
84
87
|
ants: Ant[];
|
|
85
88
|
pheromones: Pheromone[];
|
|
86
89
|
concurrency: ConcurrencyConfig;
|
|
87
90
|
metrics: ColonyMetrics;
|
|
91
|
+
maxCost: number | null; // cost budget in USD, null = unlimited
|
|
92
|
+
modelOverrides: ModelOverrides;
|
|
88
93
|
createdAt: number;
|
|
89
94
|
finishedAt: number | null;
|
|
90
95
|
}
|