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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-pi",
3
- "version": "0.1.21",
3
+ "version": "0.1.22",
4
4
  "description": "One-click setup for pi-coding-agent. Like oh-my-zsh for pi.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
- ): Promise<void> {
142
- const config = DEFAULT_ANT_CONFIGS[caste];
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
  }