opencode-metrics-plugin 0.1.5
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/LICENSE +21 -0
- package/README.md +390 -0
- package/dist/backfill-cli.d.ts +1 -0
- package/dist/backfill-cli.js +74 -0
- package/dist/backfill.d.ts +58 -0
- package/dist/backfill.js +550 -0
- package/dist/compile-analyzer.d.ts +40 -0
- package/dist/compile-analyzer.js +161 -0
- package/dist/dirs.d.ts +34 -0
- package/dist/dirs.js +44 -0
- package/dist/event-logger.d.ts +20 -0
- package/dist/event-logger.js +329 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.js +61 -0
- package/dist/logger.d.ts +10 -0
- package/dist/logger.js +76 -0
- package/dist/metrics-engine.d.ts +37 -0
- package/dist/metrics-engine.js +331 -0
- package/dist/metrics-handlers.d.ts +9 -0
- package/dist/metrics-handlers.js +648 -0
- package/dist/metrics-output.d.ts +11 -0
- package/dist/metrics-output.js +673 -0
- package/dist/metrics-steps.d.ts +19 -0
- package/dist/metrics-steps.js +90 -0
- package/dist/metrics-types.d.ts +381 -0
- package/dist/metrics-types.js +102 -0
- package/dist/summary-store.d.ts +150 -0
- package/dist/summary-store.js +560 -0
- package/package.json +43 -0
|
@@ -0,0 +1,673 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { log } from "./logger.js";
|
|
4
|
+
import { createTokenUsage } from "./metrics-types.js";
|
|
5
|
+
import { getMetricsDir, getSummaryFile } from "./dirs.js";
|
|
6
|
+
import { upsertSessionSnapshot } from "./summary-store.js";
|
|
7
|
+
import { buildSteps, extractStepContent } from "./metrics-steps.js";
|
|
8
|
+
function filterByKeys(source, keys) {
|
|
9
|
+
const result = new Map();
|
|
10
|
+
for (const key of keys) {
|
|
11
|
+
const val = source.get(key);
|
|
12
|
+
if (val !== undefined)
|
|
13
|
+
result.set(key, val);
|
|
14
|
+
}
|
|
15
|
+
return result;
|
|
16
|
+
}
|
|
17
|
+
function flushMetrics(sessionId, state, now, dirs) {
|
|
18
|
+
// Finalize agent usage for current agent
|
|
19
|
+
if (state.agent.current) {
|
|
20
|
+
const elapsed = now - state.agent.lastSwitchTime;
|
|
21
|
+
const prev = state.agent.usage.get(state.agent.current) ?? 0;
|
|
22
|
+
state.agent.usage.set(state.agent.current, prev + elapsed);
|
|
23
|
+
}
|
|
24
|
+
const endTime = now;
|
|
25
|
+
const duration = endTime - state.startTime;
|
|
26
|
+
// Calculate cache hit rate
|
|
27
|
+
const denom = state.tokens.input + state.tokens.cacheRead;
|
|
28
|
+
const cacheHitRate = denom > 0 ? state.tokens.cacheRead / denom : 0;
|
|
29
|
+
// Build tool distribution from Map to Record
|
|
30
|
+
const toolDistribution = {};
|
|
31
|
+
for (const [tool, stats] of state.tools.distribution) {
|
|
32
|
+
toolDistribution[tool] = {
|
|
33
|
+
calls: stats.calls,
|
|
34
|
+
errors: stats.errors,
|
|
35
|
+
avgDuration: stats.calls > 0 ? stats.totalDuration / stats.calls : 0,
|
|
36
|
+
maxDuration: stats.maxDuration,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
// Build model token distribution from Map to Record
|
|
40
|
+
const modelTokenDistribution = {};
|
|
41
|
+
for (const [modelId, tokens] of state.model.tokenDistribution) {
|
|
42
|
+
modelTokenDistribution[modelId] = { ...tokens };
|
|
43
|
+
}
|
|
44
|
+
// Build agent usage from Map to Record
|
|
45
|
+
const agentUsage = {};
|
|
46
|
+
for (const [agent, usage] of state.agent.usage) {
|
|
47
|
+
agentUsage[agent] = usage;
|
|
48
|
+
}
|
|
49
|
+
const totalCompleted = state.tools.completedCalls + state.tools.errorCalls;
|
|
50
|
+
const successRate = totalCompleted > 0 ? state.tools.completedCalls / totalCompleted : 0;
|
|
51
|
+
// Consolidate header with agent/model info
|
|
52
|
+
const header = {
|
|
53
|
+
...state.sessionMeta,
|
|
54
|
+
sessionId,
|
|
55
|
+
startTime: new Date(state.startTime).toISOString(),
|
|
56
|
+
agent: state.agent.current || state.sessionMeta.agent,
|
|
57
|
+
agentSwitches: state.agent.switches,
|
|
58
|
+
agentUsage,
|
|
59
|
+
model: state.model.current || state.sessionMeta.model,
|
|
60
|
+
modelSwitches: state.model.switches,
|
|
61
|
+
modelTokenDistribution,
|
|
62
|
+
};
|
|
63
|
+
// Aggregate parent step tokens from messageMap (covers message.updated without step-finish)
|
|
64
|
+
const parentStepTokens = createTokenUsage();
|
|
65
|
+
for (const entry of state.messageMap.values()) {
|
|
66
|
+
parentStepTokens.input += entry.tokens.input;
|
|
67
|
+
parentStepTokens.output += entry.tokens.output;
|
|
68
|
+
parentStepTokens.reasoning += entry.tokens.reasoning;
|
|
69
|
+
parentStepTokens.cacheRead += entry.tokens.cacheRead;
|
|
70
|
+
parentStepTokens.cacheWrite += entry.tokens.cacheWrite;
|
|
71
|
+
parentStepTokens.total += entry.tokens.total;
|
|
72
|
+
}
|
|
73
|
+
// Extract content from parent log once (sub-agent events are also written here)
|
|
74
|
+
const allContentMaps = extractStepContent(sessionId, { eventsDir: dirs?.eventsDir });
|
|
75
|
+
// Count parent steps (messages with finish + tokens)
|
|
76
|
+
const parentStepCount = [...state.messageMap.values()].filter(m => m.finish && m.tokens.total > 0).length;
|
|
77
|
+
// Build sub-agents first so we can aggregate their tokens/tools
|
|
78
|
+
const subagents = [];
|
|
79
|
+
let subAgentTotalCalls = 0;
|
|
80
|
+
let subAgentStepCount = 0;
|
|
81
|
+
const subAgentTokens = createTokenUsage();
|
|
82
|
+
log.info("[Metrics] flushMetrics subagents", { sessionId, subAgentsCount: state.subAgents.size, subAgentsKeys: [...state.subAgents.keys()] });
|
|
83
|
+
for (const [childId, sub] of state.subAgents) {
|
|
84
|
+
// Filter content maps to this sub-agent's message IDs
|
|
85
|
+
const subMsgIds = new Set(sub.messageMap.keys());
|
|
86
|
+
const subContentMaps = {
|
|
87
|
+
textMap: filterByKeys(allContentMaps.textMap, subMsgIds),
|
|
88
|
+
reasoningMap: filterByKeys(allContentMaps.reasoningMap, subMsgIds),
|
|
89
|
+
toolOutputMap: allContentMaps.toolOutputMap,
|
|
90
|
+
};
|
|
91
|
+
const steps = buildSteps(sub, subContentMaps);
|
|
92
|
+
// Aggregate sub-agent tokens from messageMap
|
|
93
|
+
const subTokens = createTokenUsage();
|
|
94
|
+
for (const entry of sub.messageMap.values()) {
|
|
95
|
+
subTokens.input += entry.tokens.input;
|
|
96
|
+
subTokens.output += entry.tokens.output;
|
|
97
|
+
subTokens.reasoning += entry.tokens.reasoning;
|
|
98
|
+
subTokens.cacheRead += entry.tokens.cacheRead;
|
|
99
|
+
subTokens.cacheWrite += entry.tokens.cacheWrite;
|
|
100
|
+
subTokens.total += entry.tokens.total;
|
|
101
|
+
}
|
|
102
|
+
// Aggregate sub-agent tool stats + distribution from toolCallMap
|
|
103
|
+
let totalCalls = 0;
|
|
104
|
+
let completedCalls = 0;
|
|
105
|
+
let errorCalls = 0;
|
|
106
|
+
const subDistribution = {};
|
|
107
|
+
let subSlowestCall = { tool: "", duration: 0 };
|
|
108
|
+
for (const tc of sub.toolCallMap.values()) {
|
|
109
|
+
if (tc.status === "completed") {
|
|
110
|
+
totalCalls++;
|
|
111
|
+
completedCalls++;
|
|
112
|
+
}
|
|
113
|
+
else if (tc.status === "error") {
|
|
114
|
+
totalCalls++;
|
|
115
|
+
errorCalls++;
|
|
116
|
+
}
|
|
117
|
+
if (tc.status === "completed" || tc.status === "error") {
|
|
118
|
+
const duration = tc.endMs && tc.startMs ? tc.endMs - tc.startMs : 0;
|
|
119
|
+
const stats = subDistribution[tc.tool] ?? { calls: 0, errors: 0, totalDuration: 0, maxDuration: 0 };
|
|
120
|
+
stats.calls++;
|
|
121
|
+
stats.totalDuration += duration;
|
|
122
|
+
if (duration > stats.maxDuration)
|
|
123
|
+
stats.maxDuration = duration;
|
|
124
|
+
if (tc.status === "error")
|
|
125
|
+
stats.errors++;
|
|
126
|
+
subDistribution[tc.tool] = stats;
|
|
127
|
+
if (duration > subSlowestCall.duration)
|
|
128
|
+
subSlowestCall = { tool: tc.tool, duration };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const subSuccessRate = totalCalls > 0 ? completedCalls / totalCalls : 0;
|
|
132
|
+
// Convert distribution totalDuration 鈫?avgDuration
|
|
133
|
+
const distribution = {};
|
|
134
|
+
for (const [tool, stats] of Object.entries(subDistribution)) {
|
|
135
|
+
distribution[tool] = {
|
|
136
|
+
calls: stats.calls,
|
|
137
|
+
errors: stats.errors,
|
|
138
|
+
avgDuration: stats.calls > 0 ? stats.totalDuration / stats.calls : 0,
|
|
139
|
+
maxDuration: stats.maxDuration,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
// Accumulate into parent totals
|
|
143
|
+
subAgentTokens.input += subTokens.input;
|
|
144
|
+
subAgentTokens.output += subTokens.output;
|
|
145
|
+
subAgentTokens.reasoning += subTokens.reasoning;
|
|
146
|
+
subAgentTokens.cacheRead += subTokens.cacheRead;
|
|
147
|
+
subAgentTokens.cacheWrite += subTokens.cacheWrite;
|
|
148
|
+
subAgentTokens.total += subTokens.total;
|
|
149
|
+
subAgentTotalCalls += totalCalls;
|
|
150
|
+
subAgentStepCount += steps.length;
|
|
151
|
+
subagents.push({
|
|
152
|
+
sessionId: childId,
|
|
153
|
+
agent: sub.agentName,
|
|
154
|
+
title: sub.title,
|
|
155
|
+
steps,
|
|
156
|
+
tokens: subTokens,
|
|
157
|
+
tools: {
|
|
158
|
+
totalCalls,
|
|
159
|
+
invalidCalls: 0,
|
|
160
|
+
successRate: subSuccessRate,
|
|
161
|
+
distribution,
|
|
162
|
+
slowestCall: subSlowestCall,
|
|
163
|
+
},
|
|
164
|
+
...(sub._pendingUserMessage.length > 0
|
|
165
|
+
? { userMessages: [...sub._pendingUserMessage] }
|
|
166
|
+
: {}),
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
// Build planning metrics 鈥?summary from last call's todos, history in calls[]
|
|
170
|
+
const planning = (() => {
|
|
171
|
+
const calls = state.planningCalls;
|
|
172
|
+
const rounds = calls.length;
|
|
173
|
+
const empty = {
|
|
174
|
+
planningRounds: 0, totalPlanningDuration: 0, avgPlanningDuration: 0,
|
|
175
|
+
totalTasks: 0, completedTasks: 0, inProgressTasks: 0, pendingTasks: 0,
|
|
176
|
+
completionRate: 0, priorityDistribution: {}, calls: [],
|
|
177
|
+
};
|
|
178
|
+
if (rounds === 0)
|
|
179
|
+
return empty;
|
|
180
|
+
const totalDuration = calls.reduce((sum, c) => sum + c.durationMs, 0);
|
|
181
|
+
const lastTodos = calls[calls.length - 1].todos;
|
|
182
|
+
const totalTasks = lastTodos.length;
|
|
183
|
+
const completedTasks = lastTodos.filter(t => t.status === "completed").length;
|
|
184
|
+
const inProgressTasks = lastTodos.filter(t => t.status === "in_progress").length;
|
|
185
|
+
const pendingTasks = lastTodos.filter(t => t.status === "pending").length;
|
|
186
|
+
const priorityDistribution = {};
|
|
187
|
+
for (const t of lastTodos) {
|
|
188
|
+
priorityDistribution[t.priority] = (priorityDistribution[t.priority] || 0) + 1;
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
planningRounds: rounds,
|
|
192
|
+
totalPlanningDuration: totalDuration,
|
|
193
|
+
avgPlanningDuration: Math.round(totalDuration / rounds),
|
|
194
|
+
totalTasks,
|
|
195
|
+
completedTasks,
|
|
196
|
+
inProgressTasks,
|
|
197
|
+
pendingTasks,
|
|
198
|
+
completionRate: totalTasks > 0 ? completedTasks / totalTasks : 0,
|
|
199
|
+
priorityDistribution,
|
|
200
|
+
calls,
|
|
201
|
+
};
|
|
202
|
+
})();
|
|
203
|
+
const systemPrompts = {};
|
|
204
|
+
for (const [modelId, system] of state.systemPrompts) {
|
|
205
|
+
systemPrompts[modelId] = system;
|
|
206
|
+
}
|
|
207
|
+
const output = {
|
|
208
|
+
sessionId,
|
|
209
|
+
startTime: state.startTime,
|
|
210
|
+
endTime,
|
|
211
|
+
duration,
|
|
212
|
+
systemPrompts,
|
|
213
|
+
rounds: state.rounds,
|
|
214
|
+
tokens: (() => {
|
|
215
|
+
const totalTokens = Math.max(state.tokens.total, parentStepTokens.total) + subAgentTokens.total;
|
|
216
|
+
const totalSteps = parentStepCount + subAgentStepCount;
|
|
217
|
+
return {
|
|
218
|
+
// Use the larger of step-finish tokens or messageMap tokens for parent
|
|
219
|
+
input: Math.max(state.tokens.input, parentStepTokens.input) + subAgentTokens.input,
|
|
220
|
+
output: Math.max(state.tokens.output, parentStepTokens.output) + subAgentTokens.output,
|
|
221
|
+
reasoning: Math.max(state.tokens.reasoning, parentStepTokens.reasoning) + subAgentTokens.reasoning,
|
|
222
|
+
cacheRead: Math.max(state.tokens.cacheRead, parentStepTokens.cacheRead) + subAgentTokens.cacheRead,
|
|
223
|
+
cacheWrite: Math.max(state.tokens.cacheWrite, parentStepTokens.cacheWrite) + subAgentTokens.cacheWrite,
|
|
224
|
+
total: totalTokens,
|
|
225
|
+
cacheHitRate,
|
|
226
|
+
avgTokensPerStep: totalSteps > 0 ? Math.round(totalTokens / totalSteps) : 0,
|
|
227
|
+
};
|
|
228
|
+
})(),
|
|
229
|
+
tools: {
|
|
230
|
+
totalCalls: state.tools.totalCalls + subAgentTotalCalls,
|
|
231
|
+
invalidCalls: state.tools.invalidCalls,
|
|
232
|
+
successRate,
|
|
233
|
+
distribution: toolDistribution,
|
|
234
|
+
slowestCall: state.tools.slowestCall,
|
|
235
|
+
},
|
|
236
|
+
compactions: state.compactions,
|
|
237
|
+
anomaly: state.anomaly,
|
|
238
|
+
stages: state.stages.map(s => ({
|
|
239
|
+
stage: s.stage,
|
|
240
|
+
duration: s.endTime - s.startTime,
|
|
241
|
+
hvigorwCalls: s.hvigorwCalls,
|
|
242
|
+
tokens: { ...s.tokens },
|
|
243
|
+
})),
|
|
244
|
+
header,
|
|
245
|
+
codeStats: {
|
|
246
|
+
etsLines: state.compileStats.etsLines,
|
|
247
|
+
buildSuccess: state.compileStats.lastBuildSuccess,
|
|
248
|
+
fixCompileCount: state.compileStats.hvigorwCalls,
|
|
249
|
+
totalCompileErrors: state.compileStats.hvigorwErrors,
|
|
250
|
+
firstBuildPerRound: state.compileStats.firstBuildPerRound,
|
|
251
|
+
firstBuildPassRate: state.compileStats.firstBuildPerRound.length > 0
|
|
252
|
+
? state.compileStats.firstBuildPerRound.filter(Boolean).length / state.compileStats.firstBuildPerRound.length
|
|
253
|
+
: 0,
|
|
254
|
+
errorCodes: [...state.compileStats.errorCodes.entries()]
|
|
255
|
+
.map(([code, info]) => ({ code, ...info }))
|
|
256
|
+
.sort((a, b) => b.count - a.count),
|
|
257
|
+
warnings: (() => {
|
|
258
|
+
const entries = [];
|
|
259
|
+
const byType = {};
|
|
260
|
+
let total = 0;
|
|
261
|
+
for (const [type, info] of state.compileStats.warnings) {
|
|
262
|
+
byType[type] = info.count;
|
|
263
|
+
total += info.count;
|
|
264
|
+
for (const entry of info.entries)
|
|
265
|
+
entries.push(entry);
|
|
266
|
+
}
|
|
267
|
+
return { total, byType, entries };
|
|
268
|
+
})(),
|
|
269
|
+
fixCycles: {
|
|
270
|
+
successRate: state.compileStats.fixCycles.length > 0
|
|
271
|
+
? state.compileStats.fixCycles.filter(c => c.successTime > 0).length / state.compileStats.fixCycles.length
|
|
272
|
+
: 0,
|
|
273
|
+
avgAttempts: state.compileStats.fixCycles.length > 0
|
|
274
|
+
? state.compileStats.fixCycles.reduce((sum, c) => sum + c.attempts, 0) / state.compileStats.fixCycles.length
|
|
275
|
+
: 0,
|
|
276
|
+
cycles: state.compileStats.fixCycles.map(c => ({
|
|
277
|
+
success: c.successTime > 0,
|
|
278
|
+
attempts: c.attempts,
|
|
279
|
+
codeChanges: c.codeChanges,
|
|
280
|
+
durationMs: c.successTime > 0 ? c.successTime - c.failTime : 0,
|
|
281
|
+
})),
|
|
282
|
+
},
|
|
283
|
+
moduleTimings: [...state.compileStats.moduleTimings.entries()]
|
|
284
|
+
.map(([module, timing]) => ({
|
|
285
|
+
module,
|
|
286
|
+
totalDurationMs: timing.totalDuration,
|
|
287
|
+
taskCount: timing.taskCount,
|
|
288
|
+
slowestTask: timing.slowestTask,
|
|
289
|
+
}))
|
|
290
|
+
.sort((a, b) => b.totalDurationMs - a.totalDurationMs),
|
|
291
|
+
},
|
|
292
|
+
responseLength: [...state.textLengthMap.values()].reduce((sum, len) => sum + len, 0),
|
|
293
|
+
skills: [...state.skillCallMap.values()].map(entry => ({
|
|
294
|
+
skillName: entry.skillName,
|
|
295
|
+
status: entry.status,
|
|
296
|
+
})),
|
|
297
|
+
skillSearches: [...state.skillSearchMap.values()].map(entry => ({
|
|
298
|
+
query: entry.query,
|
|
299
|
+
skillPath: entry.skillPath,
|
|
300
|
+
topK: entry.topK,
|
|
301
|
+
output: entry.output,
|
|
302
|
+
durationMs: entry.endMs && entry.startMs ? entry.endMs - entry.startMs : 0,
|
|
303
|
+
followUpReads: entry.followUpReads.map(r => r.filePath),
|
|
304
|
+
})),
|
|
305
|
+
planning,
|
|
306
|
+
steps: buildSteps(state, {
|
|
307
|
+
textMap: filterByKeys(allContentMaps.textMap, new Set(state.messageMap.keys())),
|
|
308
|
+
reasoningMap: filterByKeys(allContentMaps.reasoningMap, new Set(state.messageMap.keys())),
|
|
309
|
+
toolOutputMap: allContentMaps.toolOutputMap,
|
|
310
|
+
}),
|
|
311
|
+
subagents,
|
|
312
|
+
};
|
|
313
|
+
try {
|
|
314
|
+
const metricsDir = dirs?.metricsDir ?? getMetricsDir();
|
|
315
|
+
fs.mkdirSync(metricsDir, { recursive: true });
|
|
316
|
+
const filePath = path.join(metricsDir, `${sessionId}.json`);
|
|
317
|
+
let merged = { ...output, source: "live" };
|
|
318
|
+
try {
|
|
319
|
+
const existingRaw = fs.readFileSync(filePath, "utf-8");
|
|
320
|
+
const existingOutput = JSON.parse(existingRaw);
|
|
321
|
+
merged = { ...mergeMetricsOutput(existingOutput, output), source: "live" };
|
|
322
|
+
}
|
|
323
|
+
catch {
|
|
324
|
+
// File doesn't exist or is corrupted 鈥?use fresh output
|
|
325
|
+
}
|
|
326
|
+
const tmpPath = filePath + ".tmp";
|
|
327
|
+
fs.writeFileSync(tmpPath, JSON.stringify(merged, null, 2));
|
|
328
|
+
fs.renameSync(tmpPath, filePath);
|
|
329
|
+
// 摘要/详情双表联动入库(force:与文件同拍最新;缺省写全局共享库,显式 summaryFile 可隔离)
|
|
330
|
+
const summaryFile = dirs?.summaryFile ?? getSummaryFile();
|
|
331
|
+
upsertSessionSnapshot(summaryFile, merged, filePath, "force");
|
|
332
|
+
}
|
|
333
|
+
catch (err) {
|
|
334
|
+
log.info("Failed to flush metrics", { sessionId, error: String(err) });
|
|
335
|
+
}
|
|
336
|
+
return output;
|
|
337
|
+
}
|
|
338
|
+
function handleSessionIdle(state, sessionId, dirs) {
|
|
339
|
+
const now = Date.now();
|
|
340
|
+
// Build round snapshot
|
|
341
|
+
const roundDuration = now - state._roundStartTime;
|
|
342
|
+
const firstTokenLatency = state._hasTextPart && state._firstEventTime > 0
|
|
343
|
+
? state._firstTextTime - state._firstEventTime
|
|
344
|
+
: 0;
|
|
345
|
+
// Skip duplicate session.idle 鈥?round was already created
|
|
346
|
+
if (state._roundIdleProcessed) {
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
const isEmptyRound = state._roundTokens.total === 0 && state._roundToolCalls === 0;
|
|
350
|
+
// Only create round snapshot for non-empty rounds
|
|
351
|
+
if (!isEmptyRound) {
|
|
352
|
+
const round = {
|
|
353
|
+
roundIndex: state.rounds.length,
|
|
354
|
+
duration: roundDuration,
|
|
355
|
+
firstTokenLatency,
|
|
356
|
+
tokens: { ...state._roundTokens },
|
|
357
|
+
toolCalls: state._roundToolCalls,
|
|
358
|
+
errors: state._roundErrors,
|
|
359
|
+
userMessage: state._pendingUserMessage.length > 0 ? [...state._pendingUserMessage] : undefined,
|
|
360
|
+
};
|
|
361
|
+
state.rounds.push(round);
|
|
362
|
+
}
|
|
363
|
+
state._roundIdleProcessed = true;
|
|
364
|
+
// Reset per-round state
|
|
365
|
+
state._roundStartTime = now;
|
|
366
|
+
state._firstEventTime = 0;
|
|
367
|
+
state._firstTextTime = 0;
|
|
368
|
+
state._hasFirstEvent = false;
|
|
369
|
+
state._hasTextPart = false;
|
|
370
|
+
state._roundTokens = createTokenUsage();
|
|
371
|
+
state._roundToolCalls = 0;
|
|
372
|
+
state._roundErrors = 0;
|
|
373
|
+
state._pendingUserMessage = [];
|
|
374
|
+
state._roundFirstBuildTracked = false;
|
|
375
|
+
// Finalize current stage
|
|
376
|
+
if (state.currentStage) {
|
|
377
|
+
state.currentStage.endTime = now;
|
|
378
|
+
state.currentStage.tokens = { ...state._stageTokens };
|
|
379
|
+
if (state.currentStage.stage === "build") {
|
|
380
|
+
state.currentStage.hvigorwCalls = state.compileStats.hvigorwCalls;
|
|
381
|
+
}
|
|
382
|
+
state.stages.push(state.currentStage);
|
|
383
|
+
state.currentStage = null;
|
|
384
|
+
}
|
|
385
|
+
state.lastStageEndTime = now;
|
|
386
|
+
state._stageTokens = createTokenUsage();
|
|
387
|
+
// Flush metrics to disk
|
|
388
|
+
state.endTime = now;
|
|
389
|
+
state._idleFlushed = true;
|
|
390
|
+
return flushMetrics(sessionId, state, now, dirs);
|
|
391
|
+
}
|
|
392
|
+
function mergeChildMetrics(parent, child, meta) {
|
|
393
|
+
// Merge tokens
|
|
394
|
+
parent.tokens.input += child.tokens.input;
|
|
395
|
+
parent.tokens.output += child.tokens.output;
|
|
396
|
+
parent.tokens.reasoning += child.tokens.reasoning;
|
|
397
|
+
parent.tokens.cacheRead += child.tokens.cacheRead;
|
|
398
|
+
parent.tokens.cacheWrite += child.tokens.cacheWrite;
|
|
399
|
+
parent.tokens.total += child.tokens.total;
|
|
400
|
+
// Merge tools
|
|
401
|
+
parent.tools.totalCalls += child.tools.totalCalls;
|
|
402
|
+
parent.tools.invalidCalls += child.tools.invalidCalls;
|
|
403
|
+
parent.tools.completedCalls += child.tools.completedCalls;
|
|
404
|
+
parent.tools.errorCalls += child.tools.errorCalls;
|
|
405
|
+
if (child.tools.slowestCall.duration > parent.tools.slowestCall.duration) {
|
|
406
|
+
parent.tools.slowestCall = { ...child.tools.slowestCall };
|
|
407
|
+
}
|
|
408
|
+
for (const [tool, stats] of child.tools.distribution) {
|
|
409
|
+
const existing = parent.tools.distribution.get(tool) ?? { calls: 0, errors: 0, totalDuration: 0, maxDuration: 0 };
|
|
410
|
+
existing.calls += stats.calls;
|
|
411
|
+
existing.errors += stats.errors;
|
|
412
|
+
existing.totalDuration += stats.totalDuration;
|
|
413
|
+
if (stats.maxDuration > existing.maxDuration)
|
|
414
|
+
existing.maxDuration = stats.maxDuration;
|
|
415
|
+
parent.tools.distribution.set(tool, existing);
|
|
416
|
+
}
|
|
417
|
+
// Merge compactions
|
|
418
|
+
parent.compactions += child.compactions;
|
|
419
|
+
// Merge anomaly
|
|
420
|
+
if (child.anomaly.triggered) {
|
|
421
|
+
parent.anomaly.triggered = true;
|
|
422
|
+
parent.anomaly.events.push(...child.anomaly.events);
|
|
423
|
+
}
|
|
424
|
+
// Merge rounds (skip empty rounds)
|
|
425
|
+
for (const round of child.rounds) {
|
|
426
|
+
if (round.tokens.total > 0 || round.toolCalls > 0) {
|
|
427
|
+
parent.rounds.push({ ...round, roundIndex: parent.rounds.length });
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
// Merge stages (skip stages with no tokens 鈥?these correspond to empty rounds)
|
|
431
|
+
for (const stage of child.stages) {
|
|
432
|
+
if (stage.tokens.total > 0) {
|
|
433
|
+
parent.stages.push({ ...stage });
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
// Store child maps in subAgents (not merged into parent)
|
|
437
|
+
parent.subAgents.set(meta.childId, {
|
|
438
|
+
sessionId: meta.childId,
|
|
439
|
+
agentName: meta.agentName,
|
|
440
|
+
title: meta.title,
|
|
441
|
+
messageMap: child.messageMap,
|
|
442
|
+
toolCallMap: child.toolCallMap,
|
|
443
|
+
textLengthMap: child.textLengthMap,
|
|
444
|
+
orderCounter: 0,
|
|
445
|
+
_userMessageIds: new Set(),
|
|
446
|
+
_pendingUserMessage: [],
|
|
447
|
+
});
|
|
448
|
+
// Merge compileStats (additive)
|
|
449
|
+
parent.compileStats.hvigorwCalls += child.compileStats.hvigorwCalls;
|
|
450
|
+
parent.compileStats.hvigorwErrors += child.compileStats.hvigorwErrors;
|
|
451
|
+
parent.compileStats.etsLines += child.compileStats.etsLines;
|
|
452
|
+
// lastBuildSuccess: take child's value if child had any builds
|
|
453
|
+
if (child.compileStats.hvigorwCalls > 0 || child.compileStats.hvigorwErrors > 0) {
|
|
454
|
+
parent.compileStats.lastBuildSuccess = child.compileStats.lastBuildSuccess;
|
|
455
|
+
}
|
|
456
|
+
// Merge firstBuildPerRound
|
|
457
|
+
parent.compileStats.firstBuildPerRound.push(...child.compileStats.firstBuildPerRound);
|
|
458
|
+
// Merge errorCodes
|
|
459
|
+
for (const [code, info] of child.compileStats.errorCodes) {
|
|
460
|
+
const existing = parent.compileStats.errorCodes.get(code);
|
|
461
|
+
if (existing) {
|
|
462
|
+
existing.count += info.count;
|
|
463
|
+
}
|
|
464
|
+
else {
|
|
465
|
+
parent.compileStats.errorCodes.set(code, { ...info });
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
// Merge warnings
|
|
469
|
+
for (const [type, info] of child.compileStats.warnings) {
|
|
470
|
+
const existing = parent.compileStats.warnings.get(type);
|
|
471
|
+
if (existing) {
|
|
472
|
+
existing.count += info.count;
|
|
473
|
+
existing.entries.push(...info.entries);
|
|
474
|
+
}
|
|
475
|
+
else {
|
|
476
|
+
parent.compileStats.warnings.set(type, { count: info.count, entries: [...info.entries] });
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
// Merge moduleTimings
|
|
480
|
+
for (const [module, timing] of child.compileStats.moduleTimings) {
|
|
481
|
+
const existing = parent.compileStats.moduleTimings.get(module);
|
|
482
|
+
if (existing) {
|
|
483
|
+
existing.totalDuration += timing.totalDuration;
|
|
484
|
+
existing.taskCount += timing.taskCount;
|
|
485
|
+
if (timing.slowestTask.duration > existing.slowestTask.duration) {
|
|
486
|
+
existing.slowestTask = { ...timing.slowestTask };
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
else {
|
|
490
|
+
parent.compileStats.moduleTimings.set(module, { ...timing, slowestTask: { ...timing.slowestTask } });
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
// Merge fixCycles
|
|
494
|
+
parent.compileStats.fixCycles.push(...child.compileStats.fixCycles.map(c => ({ ...c })));
|
|
495
|
+
// Merge skillCallMap
|
|
496
|
+
for (const [callID, entry] of child.skillCallMap) {
|
|
497
|
+
if (!parent.skillCallMap.has(callID)) {
|
|
498
|
+
parent.skillCallMap.set(callID, { ...entry });
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
// Merge skillSearchMap
|
|
502
|
+
for (const [callID, entry] of child.skillSearchMap) {
|
|
503
|
+
if (!parent.skillSearchMap.has(callID)) {
|
|
504
|
+
parent.skillSearchMap.set(callID, {
|
|
505
|
+
...entry,
|
|
506
|
+
followUpReads: [...entry.followUpReads],
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
// Merge htmlPreviewMsgId (take first non-null)
|
|
511
|
+
if (!parent.htmlPreviewMsgId && child.htmlPreviewMsgId) {
|
|
512
|
+
parent.htmlPreviewMsgId = child.htmlPreviewMsgId;
|
|
513
|
+
}
|
|
514
|
+
// Merge planning data
|
|
515
|
+
parent.planningCalls.push(...child.planningCalls);
|
|
516
|
+
}
|
|
517
|
+
export function mergeMetricsOutput(existing, fresh) {
|
|
518
|
+
if (!existing)
|
|
519
|
+
return fresh;
|
|
520
|
+
// startTime: min of both
|
|
521
|
+
const startTime = Math.min(existing.startTime, fresh.startTime);
|
|
522
|
+
// duration: fresh.endTime - min startTime
|
|
523
|
+
const duration = fresh.endTime - startTime;
|
|
524
|
+
// Rounds: keep existing, append fresh rounds with new indices only
|
|
525
|
+
// roundIndex resets on restart, so we can't use it as a dedup key with fresh-wins.
|
|
526
|
+
// Strategy: existing rounds are always kept; fresh rounds are added only if their
|
|
527
|
+
// index doesn't already exist in existing. This preserves old session data.
|
|
528
|
+
const existingRoundIndices = new Set(existing.rounds.map(r => r.roundIndex));
|
|
529
|
+
const rounds = [
|
|
530
|
+
...existing.rounds,
|
|
531
|
+
...fresh.rounds.filter(r => !existingRoundIndices.has(r.roundIndex)),
|
|
532
|
+
].sort((a, b) => a.roundIndex - b.roundIndex);
|
|
533
|
+
// Steps: append with dedup by startTime+endTime (fresh wins on collision)
|
|
534
|
+
const stepSeen = new Set();
|
|
535
|
+
const steps = [];
|
|
536
|
+
for (const s of fresh.steps) {
|
|
537
|
+
const k = `${s.startTime}:${s.endTime}`;
|
|
538
|
+
stepSeen.add(k);
|
|
539
|
+
steps.push(s);
|
|
540
|
+
}
|
|
541
|
+
for (const s of existing.steps) {
|
|
542
|
+
if (!stepSeen.has(`${s.startTime}:${s.endTime}`))
|
|
543
|
+
steps.push(s);
|
|
544
|
+
}
|
|
545
|
+
// Subagents: append with dedup by sessionId (fresh wins)
|
|
546
|
+
const subagentMap = new Map();
|
|
547
|
+
for (const s of existing.subagents)
|
|
548
|
+
subagentMap.set(s.sessionId, s);
|
|
549
|
+
for (const s of fresh.subagents)
|
|
550
|
+
subagentMap.set(s.sessionId, s);
|
|
551
|
+
const subagents = [...subagentMap.values()];
|
|
552
|
+
// Planning: append calls with dedup by callID (existing wins on collision)
|
|
553
|
+
const callSeen = new Set();
|
|
554
|
+
const mergedCalls = [];
|
|
555
|
+
for (const c of existing.planning.calls) {
|
|
556
|
+
if (!callSeen.has(c.callID)) {
|
|
557
|
+
callSeen.add(c.callID);
|
|
558
|
+
mergedCalls.push(c);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
for (const c of fresh.planning.calls) {
|
|
562
|
+
if (!callSeen.has(c.callID)) {
|
|
563
|
+
callSeen.add(c.callID);
|
|
564
|
+
mergedCalls.push(c);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
const planning = {
|
|
568
|
+
...fresh.planning,
|
|
569
|
+
calls: mergedCalls,
|
|
570
|
+
};
|
|
571
|
+
// Skills: append with dedup by skillName (fresh wins)
|
|
572
|
+
const skillMap = new Map();
|
|
573
|
+
for (const s of existing.skills)
|
|
574
|
+
skillMap.set(s.skillName, s);
|
|
575
|
+
for (const s of fresh.skills)
|
|
576
|
+
skillMap.set(s.skillName, s);
|
|
577
|
+
const skills = [...skillMap.values()];
|
|
578
|
+
// SkillSearches: append with dedup by query+skillPath (fresh wins)
|
|
579
|
+
const searchMap = new Map();
|
|
580
|
+
for (const s of existing.skillSearches)
|
|
581
|
+
searchMap.set(`${s.query}:${s.skillPath}`, s);
|
|
582
|
+
for (const s of fresh.skillSearches)
|
|
583
|
+
searchMap.set(`${s.query}:${s.skillPath}`, s);
|
|
584
|
+
const skillSearches = [...searchMap.values()];
|
|
585
|
+
// codeStats.errorCodes: merge by code, accumulate count, keep existing type/message
|
|
586
|
+
const errorCodeMap = new Map();
|
|
587
|
+
for (const e of existing.codeStats.errorCodes)
|
|
588
|
+
errorCodeMap.set(e.code, { ...e });
|
|
589
|
+
for (const e of fresh.codeStats.errorCodes) {
|
|
590
|
+
const prev = errorCodeMap.get(e.code);
|
|
591
|
+
if (prev) {
|
|
592
|
+
prev.count += e.count;
|
|
593
|
+
// keep existing type/message
|
|
594
|
+
}
|
|
595
|
+
else {
|
|
596
|
+
errorCodeMap.set(e.code, { ...e });
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
const errorCodes = [...errorCodeMap.values()];
|
|
600
|
+
// codeStats.warnings: merge by type, accumulate count, append entries
|
|
601
|
+
const warnByType = { ...existing.codeStats.warnings.byType };
|
|
602
|
+
const warnEntries = [...existing.codeStats.warnings.entries];
|
|
603
|
+
let warnTotal = existing.codeStats.warnings.total;
|
|
604
|
+
for (const [type, count] of Object.entries(fresh.codeStats.warnings.byType)) {
|
|
605
|
+
warnByType[type] = (warnByType[type] || 0) + count;
|
|
606
|
+
warnTotal += count;
|
|
607
|
+
}
|
|
608
|
+
warnEntries.push(...fresh.codeStats.warnings.entries);
|
|
609
|
+
const warnings = { total: warnTotal, byType: warnByType, entries: warnEntries };
|
|
610
|
+
// codeStats.moduleTimings: merge by module, accumulate totalDuration/taskCount, max slowestTask
|
|
611
|
+
const moduleMap = new Map();
|
|
612
|
+
for (const m of existing.codeStats.moduleTimings)
|
|
613
|
+
moduleMap.set(m.module, { ...m, slowestTask: { ...m.slowestTask } });
|
|
614
|
+
for (const m of fresh.codeStats.moduleTimings) {
|
|
615
|
+
const prev = moduleMap.get(m.module);
|
|
616
|
+
if (prev) {
|
|
617
|
+
prev.totalDurationMs += m.totalDurationMs;
|
|
618
|
+
prev.taskCount += m.taskCount;
|
|
619
|
+
if (m.slowestTask.duration > prev.slowestTask.duration) {
|
|
620
|
+
prev.slowestTask = { ...m.slowestTask };
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
else {
|
|
624
|
+
moduleMap.set(m.module, { ...m, slowestTask: { ...m.slowestTask } });
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
const moduleTimings = [...moduleMap.values()];
|
|
628
|
+
// codeStats.fixCycles: append (skip dedup)
|
|
629
|
+
const fixCycles = {
|
|
630
|
+
...fresh.codeStats.fixCycles,
|
|
631
|
+
cycles: [...existing.codeStats.fixCycles.cycles, ...fresh.codeStats.fixCycles.cycles],
|
|
632
|
+
};
|
|
633
|
+
// codeStats.firstBuildPerRound: fresh accumulates all rounds, use fresh
|
|
634
|
+
const firstBuildPerRound = fresh.codeStats.firstBuildPerRound;
|
|
635
|
+
// systemPrompts: union of keys, fresh values win
|
|
636
|
+
const systemPrompts = { ...existing.systemPrompts };
|
|
637
|
+
for (const [key, val] of Object.entries(fresh.systemPrompts)) {
|
|
638
|
+
systemPrompts[key] = val;
|
|
639
|
+
}
|
|
640
|
+
return {
|
|
641
|
+
sessionId: fresh.sessionId,
|
|
642
|
+
startTime,
|
|
643
|
+
endTime: fresh.endTime,
|
|
644
|
+
duration,
|
|
645
|
+
systemPrompts,
|
|
646
|
+
rounds,
|
|
647
|
+
tokens: fresh.tokens,
|
|
648
|
+
tools: fresh.tools,
|
|
649
|
+
compactions: fresh.compactions,
|
|
650
|
+
anomaly: fresh.anomaly,
|
|
651
|
+
stages: fresh.stages,
|
|
652
|
+
header: fresh.header,
|
|
653
|
+
codeStats: {
|
|
654
|
+
etsLines: fresh.codeStats.etsLines,
|
|
655
|
+
buildSuccess: fresh.codeStats.buildSuccess,
|
|
656
|
+
fixCompileCount: fresh.codeStats.fixCompileCount,
|
|
657
|
+
totalCompileErrors: fresh.codeStats.totalCompileErrors,
|
|
658
|
+
firstBuildPerRound,
|
|
659
|
+
firstBuildPassRate: fresh.codeStats.firstBuildPassRate,
|
|
660
|
+
errorCodes,
|
|
661
|
+
warnings,
|
|
662
|
+
fixCycles,
|
|
663
|
+
moduleTimings,
|
|
664
|
+
},
|
|
665
|
+
responseLength: fresh.responseLength,
|
|
666
|
+
skills,
|
|
667
|
+
skillSearches,
|
|
668
|
+
steps,
|
|
669
|
+
subagents,
|
|
670
|
+
planning,
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
export { flushMetrics, handleSessionIdle, mergeChildMetrics };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { MessageEntry, ToolCallEntry, StepData } from "./metrics-types.js";
|
|
2
|
+
declare function buildSteps(maps: {
|
|
3
|
+
messageMap: Map<string, MessageEntry>;
|
|
4
|
+
toolCallMap: Map<string, ToolCallEntry>;
|
|
5
|
+
textLengthMap: Map<string, number>;
|
|
6
|
+
}, contentMaps?: {
|
|
7
|
+
textMap: Map<string, string>;
|
|
8
|
+
reasoningMap: Map<string, string>;
|
|
9
|
+
toolOutputMap: Map<string, string>;
|
|
10
|
+
}): StepData[];
|
|
11
|
+
declare function extractStepContent(sessionId: string, opts?: {
|
|
12
|
+
eventsDir?: string;
|
|
13
|
+
filterMsgIds?: Set<string>;
|
|
14
|
+
}): {
|
|
15
|
+
textMap: Map<string, string>;
|
|
16
|
+
reasoningMap: Map<string, string>;
|
|
17
|
+
toolOutputMap: Map<string, string>;
|
|
18
|
+
};
|
|
19
|
+
export { buildSteps, extractStepContent };
|