Skip to content

Commit ba4ab29

Browse files
committed
feat(loop): integrate token usage tracking into phases
- Call builder.resetStats() at start of each phase - Log session summaries after plan, build, and eval phases - Record token usage and costs in metrics after each operation - Include phase labels (Plan, Task, Eval) in summary logs - Track stats for both successful and failed task executions Signed-off-by: leocavalcante <leo@cavalcante.dev>
1 parent 84cef05 commit ba4ab29

File tree

2 files changed

+89
-1
lines changed

2 files changed

+89
-1
lines changed

src/loop.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {
3535
recordRetry,
3636
recordTaskCompleted,
3737
recordTaskFailed,
38+
recordTokenUsage,
3839
saveMetrics,
3940
} from "./metrics.ts"
4041
import { getTasks, getUncompletedTasks, markTaskComplete, validatePlan } from "./plan.ts"
@@ -271,6 +272,9 @@ async function runPlanPhase(
271272
config: Config,
272273
metrics: Metrics,
273274
): Promise<Metrics> {
275+
// Reset stats for this phase
276+
builder.resetStats()
277+
274278
let planContent: string
275279
let ideaToRemove: { path: string; filename: string } | null = null
276280

@@ -398,6 +402,11 @@ async function runPlanPhase(
398402
state.totalTasks = tasks.length
399403
state.sessionId = builder.getSessionId()
400404

405+
// Log phase summary and record token usage
406+
const stats = builder.getStats()
407+
logger.summary(stats, "Plan")
408+
metrics = recordTokenUsage(metrics, stats.inputTokens, stats.outputTokens, stats.costUsd)
409+
401410
return metrics
402411
}
403412

@@ -412,6 +421,9 @@ async function runBuildPhase(
412421
config: Config,
413422
metrics: Metrics,
414423
): Promise<Metrics> {
424+
// Reset stats for this task
425+
builder.resetStats()
426+
415427
// Ensure we have an active session (handles resume from saved state)
416428
await builder.ensureSession(state.cycle, `Cycle ${state.cycle}`)
417429
state.sessionId = builder.getSessionId()
@@ -469,6 +481,11 @@ async function runBuildPhase(
469481

470482
logger.success(`Task ${state.currentTaskNum}/${tasks.length} complete`)
471483

484+
// Log task summary and record token usage
485+
const stats = builder.getStats()
486+
logger.summary(stats, "Task")
487+
metrics = recordTokenUsage(metrics, stats.inputTokens, stats.outputTokens, stats.costUsd)
488+
472489
// Record task completion in metrics
473490
metrics = recordTaskCompleted(metrics)
474491

@@ -479,7 +496,10 @@ async function runBuildPhase(
479496
}
480497
} else {
481498
logger.logError(`Task failed: ${result.error}`)
482-
// Continue to next task or retry logic could go here
499+
// Log task summary even on failure
500+
const stats = builder.getStats()
501+
logger.summary(stats, "Task (failed)")
502+
metrics = recordTokenUsage(metrics, stats.inputTokens, stats.outputTokens, stats.costUsd)
483503
}
484504

485505
// Pause between tasks
@@ -501,6 +521,9 @@ async function runEvalPhase(
501521
config: Config,
502522
metrics: Metrics,
503523
): Promise<Metrics> {
524+
// Reset stats for this phase
525+
builder.resetStats()
526+
504527
// Read current plan for eval
505528
const planContent = await readFileOrNull(paths.currentPlan)
506529
if (!planContent) {
@@ -514,6 +537,11 @@ async function runEvalPhase(
514537
const result = parseEval(response)
515538
const reason = extractEvalReason(response)
516539

540+
// Log eval summary and record token usage
541+
const stats = builder.getStats()
542+
logger.summary(stats, "Eval")
543+
metrics = recordTokenUsage(metrics, stats.inputTokens, stats.outputTokens, stats.costUsd)
544+
517545
if (isComplete(result)) {
518546
logger.success(`Cycle ${state.cycle} complete!`)
519547
if (reason) {

src/metrics.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ const DEFAULT_METRICS: Metrics = {
1919
ideasProcessed: 0,
2020
totalCycleDurationMs: 0,
2121
lastActivityTime: "",
22+
totalInputTokens: 0,
23+
totalOutputTokens: 0,
24+
totalCostUsd: 0,
2225
}
2326

2427
/**
@@ -48,6 +51,9 @@ export async function loadMetrics(metricsFile: string): Promise<Metrics> {
4851
totalCycleDurationMs: parsed.totalCycleDurationMs ?? DEFAULT_METRICS.totalCycleDurationMs,
4952
firstRunTime: parsed.firstRunTime,
5053
lastActivityTime: parsed.lastActivityTime ?? getISOTimestamp(),
54+
totalInputTokens: parsed.totalInputTokens ?? DEFAULT_METRICS.totalInputTokens,
55+
totalOutputTokens: parsed.totalOutputTokens ?? DEFAULT_METRICS.totalOutputTokens,
56+
totalCostUsd: parsed.totalCostUsd ?? DEFAULT_METRICS.totalCostUsd,
5157
}
5258
} catch {
5359
// Invalid JSON, return defaults
@@ -161,6 +167,28 @@ export function recordIdeaProcessed(metrics: Metrics): Metrics {
161167
}
162168
}
163169

170+
/**
171+
* Record token usage and cost for an operation.
172+
* @param metrics - Current metrics
173+
* @param inputTokens - Number of input tokens used
174+
* @param outputTokens - Number of output tokens used
175+
* @param costUsd - Estimated cost in USD
176+
* @returns Updated metrics
177+
*/
178+
export function recordTokenUsage(
179+
metrics: Metrics,
180+
inputTokens: number,
181+
outputTokens: number,
182+
costUsd: number,
183+
): Metrics {
184+
return {
185+
...metrics,
186+
totalInputTokens: metrics.totalInputTokens + inputTokens,
187+
totalOutputTokens: metrics.totalOutputTokens + outputTokens,
188+
totalCostUsd: metrics.totalCostUsd + costUsd,
189+
}
190+
}
191+
164192
/**
165193
* Get average cycle duration in milliseconds.
166194
* @param metrics - Current metrics
@@ -202,6 +230,8 @@ export function formatMetricsSummary(metrics: Metrics): string {
202230
`Retries: ${metrics.totalRetries} total`,
203231
`Ideas: ${metrics.ideasProcessed} processed`,
204232
`Avg cycle duration: ${avgDurationStr}`,
233+
`Tokens: ${formatTokenCount(metrics.totalInputTokens)} in, ${formatTokenCount(metrics.totalOutputTokens)} out`,
234+
`Estimated cost: ${formatCost(metrics.totalCostUsd)}`,
205235
]
206236

207237
if (metrics.firstRunTime) {
@@ -211,6 +241,33 @@ export function formatMetricsSummary(metrics: Metrics): string {
211241
return lines.join("\n")
212242
}
213243

244+
/**
245+
* Format token count in a human-readable way.
246+
* @param tokens - Number of tokens
247+
* @returns Formatted string (e.g., "1.2M", "500K", "1,234")
248+
*/
249+
export function formatTokenCount(tokens: number): string {
250+
if (tokens >= 1_000_000) {
251+
return `${(tokens / 1_000_000).toFixed(1)}M`
252+
}
253+
if (tokens >= 1_000) {
254+
return `${(tokens / 1_000).toFixed(1)}K`
255+
}
256+
return tokens.toLocaleString()
257+
}
258+
259+
/**
260+
* Format cost in USD.
261+
* @param cost - Cost in USD
262+
* @returns Formatted string (e.g., "$1.23", "$0.05")
263+
*/
264+
export function formatCost(cost: number): string {
265+
if (cost < 0.01) {
266+
return `$${cost.toFixed(4)}`
267+
}
268+
return `$${cost.toFixed(2)}`
269+
}
270+
214271
/**
215272
* Format duration in milliseconds to human-readable string.
216273
* @param ms - Duration in milliseconds
@@ -243,5 +300,8 @@ export function resetMetrics(): Metrics {
243300
...DEFAULT_METRICS,
244301
firstRunTime: getISOTimestamp(),
245302
lastActivityTime: getISOTimestamp(),
303+
totalInputTokens: 0,
304+
totalOutputTokens: 0,
305+
totalCostUsd: 0,
246306
}
247307
}

0 commit comments

Comments
 (0)