@ilya-lesikov/pi-pi 0.1.0 → 0.3.0

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.
@@ -13,12 +13,13 @@ function getLockfileFs(): typeof import("fs") | undefined {
13
13
  }
14
14
  import type { TimeoutConfig } from "./config.js";
15
15
 
16
- export type TaskType = "implement" | "debug" | "brainstorm";
16
+ export type TaskType = "implement" | "debug" | "brainstorm" | "review";
17
17
 
18
18
  export type ImplementPhase = "brainstorm" | "plan" | "implement" | "done";
19
19
  export type DebugPhase = "debug" | "plan" | "implement" | "done";
20
20
  export type BrainstormPhase = "brainstorm" | "plan" | "implement" | "done";
21
- export type Phase = ImplementPhase | DebugPhase | BrainstormPhase;
21
+ export type ReviewPhase = "review" | "plan" | "implement" | "done";
22
+ export type Phase = ImplementPhase | DebugPhase | BrainstormPhase | ReviewPhase;
22
23
 
23
24
  export interface TaskState {
24
25
  phase: Phase;
@@ -31,6 +32,7 @@ export interface TaskState {
31
32
  from: string | null;
32
33
  description: string;
33
34
  startedAt: string;
35
+
34
36
  }
35
37
 
36
38
  export interface TaskInfo {
@@ -60,7 +62,7 @@ export function createTask(cwd: string, type: TaskType, description: string): st
60
62
  mkdirSync(taskDir, { recursive: true });
61
63
 
62
64
  const state: TaskState = {
63
- phase: type === "implement" ? "brainstorm" : type === "debug" ? "debug" : "brainstorm",
65
+ phase: type === "implement" ? "brainstorm" : type === "debug" ? "debug" : type === "review" ? "review" : "brainstorm",
64
66
  step: "llm_work",
65
67
  reviewCycle: null,
66
68
  reviewPass: 0,
@@ -77,10 +79,7 @@ export function loadTask(taskDir: string): TaskState {
77
79
  const sp = taskStatePath(taskDir);
78
80
  const raw = readFileSync(sp, "utf-8");
79
81
  try {
80
- const state = JSON.parse(raw) as TaskState & { phase?: string };
81
- if (state.phase === "planning") {
82
- state.phase = "plan";
83
- }
82
+ const state = JSON.parse(raw) as TaskState;
84
83
  return state;
85
84
  } catch (err: any) {
86
85
  throw new Error(`Failed to parse ${sp}: ${err.message}`);
@@ -95,7 +94,7 @@ export function listTasks(cwd: string, type?: TaskType): TaskInfo[] {
95
94
  const base = stateDir(cwd);
96
95
  if (!existsSync(base)) return [];
97
96
 
98
- const types: TaskType[] = type ? [type] : ["implement", "debug", "brainstorm"];
97
+ const types: TaskType[] = type ? [type] : ["implement", "debug", "brainstorm", "review"];
99
98
  const results: TaskInfo[] = [];
100
99
 
101
100
  for (const t of types) {
@@ -196,7 +195,7 @@ export function taskName(taskDir: string): string {
196
195
  const state = loadTask(taskDir);
197
196
  let desc = state.description ?? "";
198
197
 
199
- if (["implement", "debug", "brainstorm"].includes(desc)) {
198
+ if (["implement", "debug", "brainstorm", "review"].includes(desc)) {
200
199
  const urPath = join(taskDir, "USER_REQUEST.md");
201
200
  if (existsSync(urPath)) {
202
201
  const content = readFileSync(urPath, "utf-8");
@@ -0,0 +1,309 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+
5
+ export interface ModelUsage {
6
+ inputTokens: number;
7
+ outputTokens: number;
8
+ cacheReadTokens: number;
9
+ cacheWriteTokens: number;
10
+ turns: number;
11
+ }
12
+
13
+ export interface UsageTracker {
14
+ recordTurn(modelId: string, provider: string, input: number, output: number, cacheRead: number, cacheWrite: number, cost: number): void;
15
+ recordSubagentCompletion(tokens: { input?: number; output?: number; total?: number }, cost?: number, meta?: { description?: string; agentType?: string; modelId?: string; durationMs?: number; toolUses?: number }): void;
16
+ loadFromSummary(summary: Record<string, unknown>): void;
17
+ getTotalInputTokens(): number;
18
+ getTotalOutputTokens(): number;
19
+ getTotalCacheReadTokens(): number;
20
+ getTotalCacheWriteTokens(): number;
21
+ getTotalCost(): number;
22
+ getCacheHitRate(): number;
23
+ getPerModelUsage(): Record<string, ModelUsage>;
24
+ getSubagentTotals(): { inputTokens: number; outputTokens: number; cost: number };
25
+ getSubagentList(): SubagentUsage[];
26
+ toSummary(): object;
27
+ reset(): void;
28
+ }
29
+
30
+ export interface SubagentUsage {
31
+ description: string;
32
+ agentType: string;
33
+ modelId: string;
34
+ inputTokens: number;
35
+ outputTokens: number;
36
+ cacheReadTokens: number;
37
+ cacheWriteTokens: number;
38
+ cost: number;
39
+ durationMs: number;
40
+ toolUses: number;
41
+ }
42
+
43
+ interface TrackerState {
44
+ startedAt: string;
45
+ totalInputTokens: number;
46
+ totalOutputTokens: number;
47
+ totalCacheReadTokens: number;
48
+ totalCacheWriteTokens: number;
49
+ totalCost: number;
50
+ totalTurns: number;
51
+ subagentInputTokens: number;
52
+ subagentOutputTokens: number;
53
+ subagentCost: number;
54
+ models: Map<string, ModelUsage>;
55
+ subagents: SubagentUsage[];
56
+ }
57
+
58
+ function toFiniteNumber(value: unknown): number {
59
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
60
+ }
61
+
62
+ function resolveAgentDir(): string {
63
+ const envKey = "PI_CODING_AGENT_DIR";
64
+ const envDir = process.env[envKey];
65
+ if (envDir) {
66
+ if (envDir === "~") return homedir();
67
+ if (envDir.startsWith("~/")) return homedir() + envDir.slice(1);
68
+ return envDir;
69
+ }
70
+ return join(homedir(), ".pi", "agent");
71
+ }
72
+
73
+ function createInitialState(): TrackerState {
74
+ return {
75
+ startedAt: new Date().toISOString(),
76
+ totalInputTokens: 0,
77
+ totalOutputTokens: 0,
78
+ totalCacheReadTokens: 0,
79
+ totalCacheWriteTokens: 0,
80
+ totalCost: 0,
81
+ totalTurns: 0,
82
+ subagentInputTokens: 0,
83
+ subagentOutputTokens: 0,
84
+ subagentCost: 0,
85
+ models: new Map<string, ModelUsage>(),
86
+ subagents: [],
87
+ };
88
+ }
89
+
90
+ export function createUsageTracker(): UsageTracker {
91
+ const state = createInitialState();
92
+
93
+ return {
94
+ recordTurn(modelId: string, _provider: string, input: number, output: number, cacheRead: number, cacheWrite: number, cost: number): void {
95
+ const safeInput = toFiniteNumber(input);
96
+ const safeOutput = toFiniteNumber(output);
97
+ const safeCacheRead = toFiniteNumber(cacheRead);
98
+ const safeCacheWrite = toFiniteNumber(cacheWrite);
99
+ const safeCost = toFiniteNumber(cost);
100
+
101
+ state.totalInputTokens += safeInput;
102
+ state.totalOutputTokens += safeOutput;
103
+ state.totalCacheReadTokens += safeCacheRead;
104
+ state.totalCacheWriteTokens += safeCacheWrite;
105
+ state.totalCost += safeCost;
106
+ state.totalTurns += 1;
107
+
108
+ const key = modelId || "unknown-model";
109
+ const usage = state.models.get(key) ?? {
110
+ inputTokens: 0,
111
+ outputTokens: 0,
112
+ cacheReadTokens: 0,
113
+ cacheWriteTokens: 0,
114
+ turns: 0,
115
+ };
116
+
117
+ usage.inputTokens += safeInput;
118
+ usage.outputTokens += safeOutput;
119
+ usage.cacheReadTokens += safeCacheRead;
120
+ usage.cacheWriteTokens += safeCacheWrite;
121
+ usage.turns += 1;
122
+ state.models.set(key, usage);
123
+ },
124
+
125
+ recordSubagentCompletion(
126
+ tokens: { input?: number; output?: number; cacheRead?: number; cacheWrite?: number; total?: number; cost?: number },
127
+ cost?: number,
128
+ meta?: { description?: string; agentType?: string; modelId?: string; durationMs?: number; toolUses?: number },
129
+ ): void {
130
+ const safeInput = toFiniteNumber(tokens.input);
131
+ const safeOutput = toFiniteNumber(tokens.output);
132
+ const safeTotal = toFiniteNumber(tokens.total);
133
+ const safeCacheRead = toFiniteNumber(tokens.cacheRead);
134
+ const safeCacheWrite = toFiniteNumber(tokens.cacheWrite);
135
+ const safeCost = toFiniteNumber(tokens.cost ?? cost);
136
+
137
+ const effectiveInput = safeInput === 0 && safeOutput === 0 ? safeTotal : safeInput;
138
+
139
+ state.subagentInputTokens += effectiveInput;
140
+ state.subagentOutputTokens += safeOutput;
141
+ state.subagentCost += safeCost;
142
+
143
+ state.subagents.push({
144
+ description: meta?.description ?? "unknown",
145
+ agentType: meta?.agentType ?? "unknown",
146
+ modelId: meta?.modelId ?? "unknown",
147
+ inputTokens: effectiveInput,
148
+ outputTokens: safeOutput,
149
+ cacheReadTokens: safeCacheRead,
150
+ cacheWriteTokens: safeCacheWrite,
151
+ cost: safeCost,
152
+ durationMs: toFiniteNumber(meta?.durationMs),
153
+ toolUses: toFiniteNumber(meta?.toolUses),
154
+ });
155
+ },
156
+
157
+ loadFromSummary(summary: Record<string, unknown>): void {
158
+ const totals = summary.totals as Record<string, unknown> | undefined;
159
+ const subagents = summary.subagents as Record<string, unknown> | undefined;
160
+ const models = summary.models as Record<string, Record<string, unknown>> | undefined;
161
+ if (totals) {
162
+ state.totalInputTokens = toFiniteNumber(totals.inputTokens);
163
+ state.totalOutputTokens = toFiniteNumber(totals.outputTokens);
164
+ state.totalCacheReadTokens = toFiniteNumber(totals.cacheReadTokens);
165
+ state.totalCacheWriteTokens = toFiniteNumber(totals.cacheWriteTokens);
166
+ state.totalCost = toFiniteNumber(totals.cost);
167
+ state.totalTurns = toFiniteNumber(totals.turns);
168
+ }
169
+ if (Array.isArray(summary.subagents)) {
170
+ for (const sa of summary.subagents as Record<string, unknown>[]) {
171
+ const entry: SubagentUsage = {
172
+ description: typeof sa.description === "string" ? sa.description : "unknown",
173
+ agentType: typeof sa.agentType === "string" ? sa.agentType : "unknown",
174
+ modelId: typeof sa.modelId === "string" ? sa.modelId : "unknown",
175
+ inputTokens: toFiniteNumber(sa.inputTokens),
176
+ outputTokens: toFiniteNumber(sa.outputTokens),
177
+ cacheReadTokens: toFiniteNumber(sa.cacheReadTokens),
178
+ cacheWriteTokens: toFiniteNumber(sa.cacheWriteTokens),
179
+ cost: toFiniteNumber(sa.cost),
180
+ durationMs: toFiniteNumber(sa.durationMs),
181
+ toolUses: toFiniteNumber(sa.toolUses),
182
+ };
183
+ state.subagents.push(entry);
184
+ state.subagentInputTokens += entry.inputTokens;
185
+ state.subagentOutputTokens += entry.outputTokens;
186
+ state.subagentCost += entry.cost;
187
+ }
188
+ }
189
+ if (models) {
190
+ for (const [modelId, usage] of Object.entries(models)) {
191
+ state.models.set(modelId, {
192
+ inputTokens: toFiniteNumber(usage.inputTokens),
193
+ outputTokens: toFiniteNumber(usage.outputTokens),
194
+ cacheReadTokens: toFiniteNumber(usage.cacheReadTokens),
195
+ cacheWriteTokens: toFiniteNumber(usage.cacheWriteTokens),
196
+ turns: toFiniteNumber(usage.turns),
197
+ });
198
+ }
199
+ }
200
+ if (typeof summary.startedAt === "string") {
201
+ state.startedAt = summary.startedAt;
202
+ }
203
+ },
204
+
205
+ getTotalInputTokens(): number {
206
+ return state.totalInputTokens;
207
+ },
208
+
209
+ getTotalOutputTokens(): number {
210
+ return state.totalOutputTokens;
211
+ },
212
+
213
+ getTotalCacheReadTokens(): number {
214
+ return state.totalCacheReadTokens;
215
+ },
216
+
217
+ getTotalCacheWriteTokens(): number {
218
+ return state.totalCacheWriteTokens;
219
+ },
220
+
221
+ getTotalCost(): number {
222
+ return state.totalCost;
223
+ },
224
+
225
+ getCacheHitRate(): number {
226
+ const denominator = state.totalCacheReadTokens + state.totalInputTokens;
227
+ if (denominator <= 0) return 0;
228
+ return state.totalCacheReadTokens / denominator;
229
+ },
230
+
231
+ getPerModelUsage(): Record<string, ModelUsage> {
232
+ const out: Record<string, ModelUsage> = {};
233
+ for (const [model, usage] of state.models.entries()) {
234
+ out[model] = { ...usage };
235
+ }
236
+ return out;
237
+ },
238
+
239
+ getSubagentTotals(): { inputTokens: number; outputTokens: number; cost: number } {
240
+ return {
241
+ inputTokens: state.subagentInputTokens,
242
+ outputTokens: state.subagentOutputTokens,
243
+ cost: state.subagentCost,
244
+ };
245
+ },
246
+
247
+ getSubagentList(): SubagentUsage[] {
248
+ return [...state.subagents];
249
+ },
250
+
251
+ toSummary(): object {
252
+ return {
253
+ startedAt: state.startedAt,
254
+ endedAt: new Date().toISOString(),
255
+ totals: {
256
+ inputTokens: state.totalInputTokens,
257
+ outputTokens: state.totalOutputTokens,
258
+ cacheReadTokens: state.totalCacheReadTokens,
259
+ cacheWriteTokens: state.totalCacheWriteTokens,
260
+ cost: Number(state.totalCost.toFixed(6)),
261
+ turns: state.totalTurns,
262
+ },
263
+ subagents: [...state.subagents],
264
+ models: this.getPerModelUsage(),
265
+ };
266
+ },
267
+
268
+ reset(): void {
269
+ const next = createInitialState();
270
+ state.startedAt = next.startedAt;
271
+ state.totalInputTokens = 0;
272
+ state.totalOutputTokens = 0;
273
+ state.totalCacheReadTokens = 0;
274
+ state.totalCacheWriteTokens = 0;
275
+ state.totalCost = 0;
276
+ state.totalTurns = 0;
277
+ state.subagentInputTokens = 0;
278
+ state.subagentOutputTokens = 0;
279
+ state.subagentCost = 0;
280
+ state.models.clear();
281
+ state.subagents = [];
282
+ },
283
+ };
284
+ }
285
+
286
+ export function loadUsageSummary(sessionId: string): Record<string, unknown> | null {
287
+ const usageDir = join(resolveAgentDir(), "extensions", "pp", "usage");
288
+ const filePath = join(usageDir, `${sessionId}.json`);
289
+ if (!existsSync(filePath)) return null;
290
+ try {
291
+ return JSON.parse(readFileSync(filePath, "utf-8")) as Record<string, unknown>;
292
+ } catch {
293
+ return null;
294
+ }
295
+ }
296
+
297
+ export function dumpUsageSummary(tracker: UsageTracker, sessionId: string): void {
298
+ const usageDir = join(resolveAgentDir(), "extensions", "pp", "usage");
299
+ mkdirSync(usageDir, { recursive: true });
300
+
301
+ const summary = tracker.toSummary() as Record<string, unknown>;
302
+ const payload = {
303
+ sessionId,
304
+ ...summary,
305
+ };
306
+
307
+ const outPath = join(usageDir, `${sessionId}.json`);
308
+ writeFileSync(outPath, JSON.stringify(payload, null, 2) + "\n", "utf-8");
309
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ilya-lesikov/pi-pi",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Pi-Pi Coding Agent: based on Pi Coding Agent, but twice as good",
5
5
  "keywords": [
6
6
  "pi-package",