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.
@@ -0,0 +1,90 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import { getEventsDir } from "./dirs.js";
4
+ function buildSteps(maps, contentMaps) {
5
+ const sorted = [...maps.messageMap.values()]
6
+ .filter(m => m.finish && m.tokens.total > 0)
7
+ .sort((a, b) => a.order - b.order);
8
+ return sorted.map((msg, idx) => {
9
+ const tools = [...maps.toolCallMap.values()]
10
+ .filter(tc => tc.parentMsgId === msg.msgId && tc.status !== "running")
11
+ .map(tc => ({
12
+ tool: tc.tool,
13
+ callID: tc.callID,
14
+ status: tc.status,
15
+ input: tc.input,
16
+ output: contentMaps?.toolOutputMap.get(tc.callID) ?? tc.outputPreview,
17
+ durationMs: tc.endMs && tc.startMs ? tc.endMs - tc.startMs : 0,
18
+ }));
19
+ return {
20
+ index: idx,
21
+ startTime: msg.startTime,
22
+ endTime: msg.endTime,
23
+ durationMs: msg.endTime && msg.startTime ? msg.endTime - msg.startTime : 0,
24
+ tokens: msg.tokens,
25
+ cost: msg.cost,
26
+ tools,
27
+ textLength: maps.textLengthMap.get(msg.msgId) || 0,
28
+ text: contentMaps?.textMap.get(msg.msgId) || "",
29
+ reasoning: contentMaps?.reasoningMap.get(msg.msgId) || "",
30
+ };
31
+ });
32
+ }
33
+ function extractStepContent(sessionId, opts) {
34
+ const textMap = new Map();
35
+ const reasoningMap = new Map();
36
+ const toolOutputMap = new Map();
37
+ const filterMsgIds = opts?.filterMsgIds;
38
+ try {
39
+ const filePath = path.join(opts?.eventsDir ?? getEventsDir(), `${sessionId}.log`);
40
+ const content = fs.readFileSync(filePath, "utf-8");
41
+ // Split by [timestamp] prefix (zero-width lookahead)
42
+ const blocks = content.split(/^(?=\[\d{4}-\d{2}-\d{2} )/m);
43
+ for (const block of blocks) {
44
+ const dataIdx = block.indexOf(" DATA: ");
45
+ if (dataIdx < 0)
46
+ continue;
47
+ const jsonStr = block.slice(dataIdx + 8);
48
+ if (!jsonStr.trim())
49
+ continue;
50
+ try {
51
+ const event = JSON.parse(jsonStr);
52
+ const props = event;
53
+ const part = props.part;
54
+ if (!part)
55
+ continue;
56
+ const partType = part.type;
57
+ const msgId = part.messageID;
58
+ if (partType === "text" && msgId) {
59
+ if (filterMsgIds && !filterMsgIds.has(msgId))
60
+ continue;
61
+ const text = part.text || "";
62
+ textMap.set(msgId, (textMap.get(msgId) || "") + text);
63
+ }
64
+ if (partType === "reasoning" && msgId) {
65
+ if (filterMsgIds && !filterMsgIds.has(msgId))
66
+ continue;
67
+ const text = part.text || "";
68
+ reasoningMap.set(msgId, (reasoningMap.get(msgId) || "") + text);
69
+ }
70
+ if (partType === "tool") {
71
+ const callID = part.callID;
72
+ const toolState = part.state;
73
+ const status = toolState?.status;
74
+ if (callID && (status === "completed" || status === "error")) {
75
+ const output = toolState?.output || toolState?.raw || "";
76
+ toolOutputMap.set(callID, output);
77
+ }
78
+ }
79
+ }
80
+ catch {
81
+ // Skip unparseable blocks
82
+ }
83
+ }
84
+ }
85
+ catch {
86
+ // File doesn't exist or read error 鈥?return empty maps
87
+ }
88
+ return { textMap, reasoningMap, toolOutputMap };
89
+ }
90
+ export { buildSteps, extractStepContent };
@@ -0,0 +1,381 @@
1
+ export declare const TRACKED_EVENT_TYPES: Set<string>;
2
+ export interface TokenUsage {
3
+ input: number;
4
+ output: number;
5
+ reasoning: number;
6
+ cacheRead: number;
7
+ cacheWrite: number;
8
+ total: number;
9
+ }
10
+ export interface ToolStats {
11
+ calls: number;
12
+ errors: number;
13
+ totalDuration: number;
14
+ maxDuration: number;
15
+ }
16
+ export type StageType = "design" | "dev" | "build";
17
+ export interface StageInfo {
18
+ stage: StageType;
19
+ startTime: number;
20
+ endTime: number;
21
+ hvigorwCalls: number;
22
+ tokens: TokenUsage;
23
+ }
24
+ export interface MessageEntry {
25
+ msgId: string;
26
+ tokens: TokenUsage;
27
+ cost: number;
28
+ startTime: number;
29
+ endTime: number;
30
+ finish: string;
31
+ order: number;
32
+ }
33
+ export interface ToolCallEntry {
34
+ callID: string;
35
+ tool: string;
36
+ status: "running" | "completed" | "error";
37
+ input: Record<string, unknown>;
38
+ outputPreview: string;
39
+ startMs: number;
40
+ endMs: number;
41
+ parentMsgId: string;
42
+ }
43
+ export interface SessionMeta {
44
+ sessionId: string;
45
+ workingDirectory: string;
46
+ startTime: string;
47
+ model: string;
48
+ agent: string;
49
+ agentSwitches: number;
50
+ agentUsage: Record<string, number>;
51
+ modelSwitches: number;
52
+ modelTokenDistribution: Record<string, TokenUsage>;
53
+ /** opencode session.title(实时事件不产出,采集/回填场景可选填充) */
54
+ title?: string;
55
+ }
56
+ export interface FixCycle {
57
+ failTime: number;
58
+ successTime: number;
59
+ attempts: number;
60
+ codeChanges: number;
61
+ }
62
+ export interface PendingFix {
63
+ failTime: number;
64
+ attempts: number;
65
+ codeChanges: number;
66
+ }
67
+ export interface WarningInfo {
68
+ count: number;
69
+ entries: Array<{
70
+ type: string;
71
+ message: string;
72
+ file?: string;
73
+ line?: number;
74
+ }>;
75
+ }
76
+ export interface ModuleTimingInfo {
77
+ totalDuration: number;
78
+ taskCount: number;
79
+ slowestTask: {
80
+ name: string;
81
+ duration: number;
82
+ };
83
+ }
84
+ export interface CompileStats {
85
+ hvigorwCalls: number;
86
+ hvigorwErrors: number;
87
+ lastBuildSuccess: boolean;
88
+ etsLines: number;
89
+ firstBuildPerRound: boolean[];
90
+ errorCodes: Map<string, {
91
+ count: number;
92
+ type: string;
93
+ message: string;
94
+ }>;
95
+ fixCycles: FixCycle[];
96
+ _pendingFix: PendingFix | null;
97
+ warnings: Map<string, WarningInfo>;
98
+ moduleTimings: Map<string, ModuleTimingInfo>;
99
+ }
100
+ export interface SkillCallEntry {
101
+ skillName: string;
102
+ parentMsgId: string;
103
+ status: string;
104
+ }
105
+ export interface TodoSnapshot {
106
+ content: string;
107
+ status: string;
108
+ priority: string;
109
+ }
110
+ export interface PlanningCall {
111
+ callID: string;
112
+ startTime: number;
113
+ endTime: number;
114
+ durationMs: number;
115
+ todos: TodoSnapshot[];
116
+ }
117
+ export interface PlanningMetrics {
118
+ planningRounds: number;
119
+ totalPlanningDuration: number;
120
+ avgPlanningDuration: number;
121
+ totalTasks: number;
122
+ completedTasks: number;
123
+ inProgressTasks: number;
124
+ pendingTasks: number;
125
+ completionRate: number;
126
+ priorityDistribution: Record<string, number>;
127
+ calls: PlanningCall[];
128
+ }
129
+ export interface SkillSearchEntry {
130
+ callID: string;
131
+ skillPath: string;
132
+ query: string;
133
+ topK: number;
134
+ output: string;
135
+ startMs: number;
136
+ endMs: number;
137
+ followUpReads: Array<{
138
+ filePath: string;
139
+ callID: string;
140
+ }>;
141
+ }
142
+ export interface StepData {
143
+ index: number;
144
+ startTime: number;
145
+ endTime: number;
146
+ durationMs: number;
147
+ tokens: TokenUsage;
148
+ cost: number;
149
+ tools: Array<{
150
+ tool: string;
151
+ callID: string;
152
+ status: string;
153
+ input: Record<string, unknown>;
154
+ output: string;
155
+ durationMs: number;
156
+ }>;
157
+ textLength: number;
158
+ text: string;
159
+ reasoning: string;
160
+ }
161
+ export interface RoundSnapshot {
162
+ roundIndex: number;
163
+ duration: number;
164
+ firstTokenLatency: number;
165
+ tokens: TokenUsage;
166
+ toolCalls: number;
167
+ errors: number;
168
+ userMessage?: string[];
169
+ }
170
+ export interface SubAgentOutput {
171
+ sessionId: string;
172
+ agent: string;
173
+ title: string;
174
+ steps: StepData[];
175
+ tokens: TokenUsage;
176
+ tools: {
177
+ totalCalls: number;
178
+ invalidCalls: number;
179
+ successRate: number;
180
+ distribution: Record<string, {
181
+ calls: number;
182
+ errors: number;
183
+ avgDuration: number;
184
+ maxDuration: number;
185
+ }>;
186
+ slowestCall: {
187
+ tool: string;
188
+ duration: number;
189
+ };
190
+ };
191
+ userMessages?: string[];
192
+ }
193
+ export interface MetricsOutput {
194
+ sessionId: string;
195
+ /** 快照来源标记:live 进程写入 / opencode.db 回填生成(force 重建仅保护 live 源)。 */
196
+ source?: "live" | "backfill";
197
+ startTime: number;
198
+ endTime: number;
199
+ duration: number;
200
+ systemPrompts: Record<string, string[]>;
201
+ rounds: RoundSnapshot[];
202
+ tokens: TokenUsage & {
203
+ cacheHitRate: number;
204
+ avgTokensPerStep: number;
205
+ };
206
+ tools: {
207
+ totalCalls: number;
208
+ invalidCalls: number;
209
+ successRate: number;
210
+ distribution: Record<string, {
211
+ calls: number;
212
+ errors: number;
213
+ avgDuration: number;
214
+ maxDuration: number;
215
+ }>;
216
+ slowestCall: {
217
+ tool: string;
218
+ duration: number;
219
+ };
220
+ };
221
+ compactions: number;
222
+ anomaly: {
223
+ triggered: boolean;
224
+ events: string[];
225
+ };
226
+ stages: Array<{
227
+ stage: StageType;
228
+ duration: number;
229
+ hvigorwCalls: number;
230
+ tokens: TokenUsage;
231
+ }>;
232
+ header: SessionMeta;
233
+ codeStats: {
234
+ etsLines: number;
235
+ buildSuccess: boolean;
236
+ fixCompileCount: number;
237
+ totalCompileErrors: number;
238
+ firstBuildPerRound: boolean[];
239
+ firstBuildPassRate: number;
240
+ errorCodes: Array<{
241
+ code: string;
242
+ count: number;
243
+ type: string;
244
+ message: string;
245
+ }>;
246
+ warnings: {
247
+ total: number;
248
+ byType: Record<string, number>;
249
+ entries: Array<{
250
+ type: string;
251
+ message: string;
252
+ file?: string;
253
+ line?: number;
254
+ }>;
255
+ };
256
+ fixCycles: {
257
+ successRate: number;
258
+ avgAttempts: number;
259
+ cycles: Array<{
260
+ success: boolean;
261
+ attempts: number;
262
+ codeChanges: number;
263
+ durationMs: number;
264
+ }>;
265
+ };
266
+ moduleTimings: Array<{
267
+ module: string;
268
+ totalDurationMs: number;
269
+ taskCount: number;
270
+ slowestTask: {
271
+ name: string;
272
+ duration: number;
273
+ };
274
+ }>;
275
+ };
276
+ responseLength: number;
277
+ skills: Array<{
278
+ skillName: string;
279
+ status: string;
280
+ }>;
281
+ skillSearches: Array<{
282
+ query: string;
283
+ skillPath: string;
284
+ topK: number;
285
+ output: string;
286
+ durationMs: number;
287
+ followUpReads: string[];
288
+ }>;
289
+ steps: StepData[];
290
+ subagents: SubAgentOutput[];
291
+ planning: PlanningMetrics;
292
+ }
293
+ export interface MetricsEngine {
294
+ ingest(event: {
295
+ type: string;
296
+ properties?: Record<string, unknown>;
297
+ }): void;
298
+ ingestPrompt(sessionId: string, modelId: string, system: string[]): void;
299
+ isSubAgent(sessionId: string): boolean;
300
+ flush(sessionId: string): void;
301
+ dispose(): void;
302
+ }
303
+ export interface SubAgentState {
304
+ sessionId: string;
305
+ agentName: string;
306
+ title: string;
307
+ messageMap: Map<string, MessageEntry>;
308
+ toolCallMap: Map<string, ToolCallEntry>;
309
+ textLengthMap: Map<string, number>;
310
+ orderCounter: number;
311
+ _userMessageIds: Set<string>;
312
+ _pendingUserMessage: string[];
313
+ }
314
+ export interface SessionMetricsState {
315
+ sessionId: string;
316
+ startTime: number;
317
+ endTime: number;
318
+ rounds: RoundSnapshot[];
319
+ tokens: TokenUsage;
320
+ tools: {
321
+ totalCalls: number;
322
+ invalidCalls: number;
323
+ completedCalls: number;
324
+ errorCalls: number;
325
+ distribution: Map<string, ToolStats>;
326
+ slowestCall: {
327
+ tool: string;
328
+ duration: number;
329
+ };
330
+ };
331
+ agent: {
332
+ current: string;
333
+ switches: number;
334
+ lastSwitchTime: number;
335
+ usage: Map<string, number>;
336
+ };
337
+ model: {
338
+ current: string;
339
+ switches: number;
340
+ tokenDistribution: Map<string, TokenUsage>;
341
+ };
342
+ compactions: number;
343
+ anomaly: {
344
+ triggered: boolean;
345
+ events: string[];
346
+ };
347
+ currentStage: StageInfo | null;
348
+ stages: StageInfo[];
349
+ lastStageEndTime: number;
350
+ _stageTokens: TokenUsage;
351
+ _roundStartTime: number;
352
+ _firstEventTime: number;
353
+ _firstTextTime: number;
354
+ _hasFirstEvent: boolean;
355
+ _hasTextPart: boolean;
356
+ _roundTokens: TokenUsage;
357
+ _roundToolCalls: number;
358
+ _roundErrors: number;
359
+ _roundFirstBuildTracked: boolean;
360
+ _roundIdleProcessed: boolean;
361
+ _idleFlushed: boolean;
362
+ messageMap: Map<string, MessageEntry>;
363
+ toolCallMap: Map<string, ToolCallEntry>;
364
+ textLengthMap: Map<string, number>;
365
+ sessionMeta: SessionMeta;
366
+ compileStats: CompileStats;
367
+ orderCounter: number;
368
+ skillCallMap: Map<string, SkillCallEntry>;
369
+ htmlPreviewMsgId: string | null;
370
+ skillSearchMap: Map<string, SkillSearchEntry>;
371
+ _pendingReadChain: {
372
+ searchCallID: string;
373
+ } | null;
374
+ _userMessageIds: Set<string>;
375
+ _pendingUserMessage: string[];
376
+ subAgents: Map<string, SubAgentState>;
377
+ planningCalls: PlanningCall[];
378
+ systemPrompts: Map<string, string[]>;
379
+ }
380
+ export declare function createTokenUsage(): TokenUsage;
381
+ export declare function createSessionMetrics(sessionId: string): SessionMetricsState;
@@ -0,0 +1,102 @@
1
+ export const TRACKED_EVENT_TYPES = new Set([
2
+ "session.created",
3
+ "session.updated",
4
+ "session.idle",
5
+ "session.deleted",
6
+ "session.compacted",
7
+ "session.status",
8
+ "session.error",
9
+ "message.updated",
10
+ "message.removed",
11
+ "message.part.updated",
12
+ "message.part.removed",
13
+ "permission.asked",
14
+ "permission.replied",
15
+ "command.executed",
16
+ ]);
17
+ // ─── Factory Functions ───────────────────────────────────────────────────────
18
+ export function createTokenUsage() {
19
+ return { input: 0, output: 0, reasoning: 0, cacheRead: 0, cacheWrite: 0, total: 0 };
20
+ }
21
+ export function createSessionMetrics(sessionId) {
22
+ const now = Date.now();
23
+ return {
24
+ sessionId,
25
+ startTime: now,
26
+ endTime: now,
27
+ rounds: [],
28
+ tokens: createTokenUsage(),
29
+ tools: {
30
+ totalCalls: 0,
31
+ invalidCalls: 0,
32
+ completedCalls: 0,
33
+ errorCalls: 0,
34
+ distribution: new Map(),
35
+ slowestCall: { tool: "", duration: 0 },
36
+ },
37
+ agent: {
38
+ current: "",
39
+ switches: 0,
40
+ lastSwitchTime: now,
41
+ usage: new Map(),
42
+ },
43
+ model: {
44
+ current: "",
45
+ switches: 0,
46
+ tokenDistribution: new Map(),
47
+ },
48
+ compactions: 0,
49
+ anomaly: { triggered: false, events: [] },
50
+ currentStage: null,
51
+ stages: [],
52
+ lastStageEndTime: now,
53
+ messageMap: new Map(),
54
+ toolCallMap: new Map(),
55
+ textLengthMap: new Map(),
56
+ sessionMeta: {
57
+ sessionId,
58
+ workingDirectory: '',
59
+ startTime: '',
60
+ model: '',
61
+ agent: '',
62
+ agentSwitches: 0,
63
+ agentUsage: {},
64
+ modelSwitches: 0,
65
+ modelTokenDistribution: {},
66
+ },
67
+ compileStats: {
68
+ hvigorwCalls: 0,
69
+ hvigorwErrors: 0,
70
+ lastBuildSuccess: false,
71
+ etsLines: 0,
72
+ firstBuildPerRound: [],
73
+ errorCodes: new Map(),
74
+ fixCycles: [],
75
+ _pendingFix: null,
76
+ warnings: new Map(),
77
+ moduleTimings: new Map(),
78
+ },
79
+ orderCounter: 0,
80
+ skillCallMap: new Map(),
81
+ htmlPreviewMsgId: null,
82
+ skillSearchMap: new Map(),
83
+ _pendingReadChain: null,
84
+ _userMessageIds: new Set(),
85
+ _pendingUserMessage: [],
86
+ subAgents: new Map(),
87
+ _stageTokens: createTokenUsage(),
88
+ _roundStartTime: now,
89
+ _firstEventTime: 0,
90
+ _firstTextTime: 0,
91
+ _hasFirstEvent: false,
92
+ _hasTextPart: false,
93
+ _roundTokens: createTokenUsage(),
94
+ _roundToolCalls: 0,
95
+ _roundErrors: 0,
96
+ _roundFirstBuildTracked: false,
97
+ _roundIdleProcessed: false,
98
+ _idleFlushed: false,
99
+ planningCalls: [],
100
+ systemPrompts: new Map(),
101
+ };
102
+ }