-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathloop-format.ts
More file actions
143 lines (123 loc) · 5.4 KB
/
loop-format.ts
File metadata and controls
143 lines (123 loc) · 5.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
import { formatTokens, truncate } from './format'
import type { LoopSessionOutput } from '../loop'
import type { LoopUsageSummary, TokenBreakdown } from '../loop/token-usage'
import type { LoopUsageAggregate, LoopSessionUsageRepo } from '../storage/repos/loop-session-usage-repo'
import { mergeUsageSummaries } from '../loop/token-usage'
export { formatTokens } from './format'
export type { LoopUsageSummary } from '../loop/token-usage'
/**
* Build cumulative usage for a loop by merging persisted aggregate with live session output.
* Prevents double-counting by checking if the current session is already persisted.
*
* @param loopSessionUsageRepo - Repository to check persistence status and fetch aggregate
* @param projectId - Project identifier
* @param loopName - Loop name
* @param currentSessionId - Current session ID to check for persistence
* @param sessionOutput - Live session output (may be null if worktree unavailable)
*/
export function buildCumulativeUsage(
loopSessionUsageRepo: LoopSessionUsageRepo | undefined,
projectId: string,
loopName: string,
currentSessionId: string,
sessionOutput: LoopSessionOutput | null,
): LoopUsageSummary | null {
if (!loopSessionUsageRepo) {
// No repo available, return live usage only if present
return sessionOutput?.usageSummary ?? null
}
const persistedAggregate = loopSessionUsageRepo.getAggregate(projectId, loopName)
const sessionIsPersisted = loopSessionUsageRepo.hasSession(projectId, loopName, currentSessionId)
const persistedSummary = persistedAggregate ? aggregateToUsageSummary(persistedAggregate) : null
const liveSummary = sessionOutput?.usageSummary ?? null
if (sessionIsPersisted) {
// Current session already persisted - use persisted only to avoid double-counting
return persistedSummary
}
// Merge persisted + live (live session not yet persisted)
if (persistedSummary && liveSummary) {
return mergeUsageSummaries(persistedSummary, liveSummary)
}
// Return whichever one exists
return persistedSummary ?? liveSummary
}
/** Convert LoopUsageAggregate from database to LoopUsageSummary */
export function aggregateToUsageSummary(aggregate: LoopUsageAggregate): LoopUsageSummary {
const totalTokens: TokenBreakdown = {
input: aggregate.totalInputTokens,
output: aggregate.totalOutputTokens,
reasoning: aggregate.totalReasoningTokens,
cacheRead: aggregate.totalCacheReadTokens,
cacheWrite: aggregate.totalCacheWriteTokens,
}
const perModel = Object.entries(aggregate.byModel).map(([model, data]) => ({
model,
cost: data.cost,
tokens: {
input: data.inputTokens,
output: data.outputTokens,
reasoning: data.reasoningTokens,
cacheRead: data.cacheReadTokens,
cacheWrite: data.cacheWriteTokens,
},
messageCount: data.messageCount,
})).sort((a, b) => a.model.localeCompare(b.model))
return {
totalCost: aggregate.totalCost,
totalTokens,
perModel,
}
}
/** Format a LoopUsageSummary into deterministic total and per-model output */
export function formatUsageSummary(summary: LoopUsageSummary): string[] {
const lines: string[] = []
const costStr = `$${summary.totalCost.toFixed(4)}`
const t = summary.totalTokens
const tokensStr = `${formatTokens(t.input)} in / ${formatTokens(t.output)} out / ${formatTokens(t.reasoning)} reasoning / ${formatTokens(t.cacheRead)} cache read / ${formatTokens(t.cacheWrite)} cache write`
lines.push(`Total Cost: ${costStr} | Tokens: ${tokensStr}`)
if (summary.perModel.length > 0) {
lines.push('Per-model usage:')
for (const modelUsage of summary.perModel) {
const modelCost = `$${modelUsage.cost.toFixed(4)}`
const mt = modelUsage.tokens
const modelTokensStr = `${formatTokens(mt.input)} in / ${formatTokens(mt.output)} out / ${formatTokens(mt.reasoning)} reasoning / ${formatTokens(mt.cacheRead)} cache read / ${formatTokens(mt.cacheWrite)} cache write`
lines.push(` ${modelUsage.model}: ${modelCost} | ${modelTokensStr}`)
}
}
return lines
}
export function formatSessionOutput(
sessionOutput: LoopSessionOutput,
): string[] {
const lines: string[] = []
if (sessionOutput.messages.length > 0) {
lines.push('Recent Activity:')
for (const msg of sessionOutput.messages) {
const preview = truncate(msg.text.replace(/\n/g, ' ').trim(), 1000)
lines.push(` [assistant] ${preview}`)
}
lines.push('')
}
// Use formatUsageSummary if usageSummary is available, otherwise format inline
if (sessionOutput.usageSummary) {
const usageLines = formatUsageSummary(sessionOutput.usageSummary)
for (const line of usageLines) {
lines.push(` ${line}`)
}
} else {
// Fallback to inline formatting for backward compatibility
const costStr = `$${sessionOutput.totalCost.toFixed(4)}`
const t = sessionOutput.totalTokens
const tokensStr = `${formatTokens(t.input)} in / ${formatTokens(t.output)} out / ${formatTokens(t.reasoning)} reasoning / ${formatTokens(t.cacheRead)} cache read / ${formatTokens(t.cacheWrite)} cache write`
lines.push(` Cost: ${costStr} | Tokens: ${tokensStr}`)
}
if (sessionOutput.fileChanges) {
const fc = sessionOutput.fileChanges
lines.push(` Files changed: ${fc.files} (+${fc.additions}/-${fc.deletions} lines)`)
}
return lines
}
export function formatAuditResult(auditResult: string): string[] {
const auditPreview = truncate(auditResult, 300)
return ['', 'Last Audit:', ` ${auditPreview}`]
}