opencode-metrics-plugin 0.1.5 → 0.2.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.
- package/README.md +108 -390
- package/dist/index.d.ts +5 -51
- package/dist/index.js +5 -61
- package/dist/metrics/dirs.d.ts +23 -0
- package/dist/{dirs.js → metrics/dirs.js} +4 -10
- package/dist/metrics/engine/engine.d.ts +15 -0
- package/dist/{metrics-engine.js → metrics/engine/engine.js} +33 -146
- package/dist/{metrics-handlers.d.ts → metrics/engine/handlers.d.ts} +1 -1
- package/dist/{metrics-handlers.js → metrics/engine/handlers.js} +2 -2
- package/dist/metrics/engine/state.d.ts +79 -0
- package/dist/{metrics-types.js → metrics/engine/state.js} +1 -19
- package/dist/{event-logger.d.ts → metrics/eventlog/event-logger.d.ts} +0 -5
- package/dist/{event-logger.js → metrics/eventlog/event-logger.js} +2 -18
- package/dist/metrics/index.d.ts +9 -0
- package/dist/metrics/index.js +6 -0
- package/dist/metrics/runtime.d.ts +25 -0
- package/dist/metrics/runtime.js +28 -0
- package/dist/metrics/snapshot/flush.d.ts +6 -0
- package/dist/{metrics-output.js → metrics/snapshot/flush.js} +6 -290
- package/dist/metrics/snapshot/merge.d.ts +9 -0
- package/dist/metrics/snapshot/merge.js +282 -0
- package/dist/{metrics-steps.d.ts → metrics/snapshot/steps.d.ts} +1 -1
- package/dist/{metrics-steps.js → metrics/snapshot/steps.js} +1 -1
- package/dist/{metrics-types.d.ts → metrics/types.d.ts} +3 -81
- package/dist/metrics/types.js +20 -0
- package/dist/plugin.d.ts +22 -0
- package/dist/plugin.js +49 -0
- package/dist/{logger.d.ts → shared/log.d.ts} +2 -0
- package/dist/{logger.js → shared/log.js} +9 -2
- package/package.json +8 -4
- package/dist/backfill-cli.d.ts +0 -1
- package/dist/backfill-cli.js +0 -74
- package/dist/backfill.d.ts +0 -58
- package/dist/backfill.js +0 -550
- package/dist/dirs.d.ts +0 -34
- package/dist/metrics-engine.d.ts +0 -37
- package/dist/metrics-output.d.ts +0 -11
- package/dist/summary-store.d.ts +0 -150
- package/dist/summary-store.js +0 -560
- /package/dist/{compile-analyzer.d.ts → metrics/analysis/hvigor.d.ts} +0 -0
- /package/dist/{compile-analyzer.js → metrics/analysis/hvigor.js} +0 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** metrics/events 写入目录集。host 可自定义;默认按应用名 env paths。 */
|
|
2
|
+
export interface MetricsDirs {
|
|
3
|
+
/** 会话快照 JSON 目录(<sessionId>.json 直接落这里;多引擎必须隔离,避免多写者冲突) */
|
|
4
|
+
metricsDir: string;
|
|
5
|
+
/** events 逐会话日志目录(steps 全文的数据源) */
|
|
6
|
+
eventsDir: string;
|
|
7
|
+
/** 运行日志文件(可选;logger 无文件权限时回退 console) */
|
|
8
|
+
logFile: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function defaultDirs(appName?: string): MetricsDirs;
|
|
11
|
+
/**
|
|
12
|
+
* 供 host 设定进程级默认目录(未显式传 dirs 的引擎/事件记录器使用)。
|
|
13
|
+
* 多引擎场景请优先在 createMetricsEngine/createEventLogger 实例级传 dirs,
|
|
14
|
+
* 进程级配置仅作默认兜底,避免多宿主互相覆盖。
|
|
15
|
+
*/
|
|
16
|
+
export declare function configureDirs(dirs: Partial<MetricsDirs>): void;
|
|
17
|
+
/** 当前进程级默认目录(defaultDirs 与 configureDirs 的合并结果)。 */
|
|
18
|
+
export declare function getDirs(): MetricsDirs;
|
|
19
|
+
/** 解析实例目录:defaultDirs → 进程级默认(configureDirs)→ 实例传入,三层合并。 */
|
|
20
|
+
export declare function resolveDirs(partial?: Partial<MetricsDirs>): MetricsDirs;
|
|
21
|
+
export declare function getMetricsDir(): string;
|
|
22
|
+
export declare function getEventsDir(): string;
|
|
23
|
+
export declare function getLogFile(): string;
|
|
@@ -1,32 +1,29 @@
|
|
|
1
1
|
import envPaths from 'env-paths';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import { setLogFile } from '../shared/log.js';
|
|
3
4
|
export function defaultDirs(appName = 'opencode-metrics-plugin') {
|
|
4
5
|
const base = envPaths(appName, { suffix: '' });
|
|
5
6
|
return {
|
|
6
7
|
metricsDir: path.join(base.log, 'metrics'),
|
|
7
8
|
eventsDir: path.join(base.log, 'events'),
|
|
8
9
|
logFile: path.join(base.log, 'plugin.log'),
|
|
9
|
-
summaryFile: path.join(base.log, 'summary.db'),
|
|
10
10
|
};
|
|
11
11
|
}
|
|
12
12
|
let _dirs = defaultDirs();
|
|
13
13
|
/**
|
|
14
14
|
* 供 host 设定进程级默认目录(未显式传 dirs 的引擎/事件记录器使用)。
|
|
15
|
-
*
|
|
15
|
+
* 多引擎场景请优先在 createMetricsEngine/createEventLogger 实例级传 dirs,
|
|
16
16
|
* 进程级配置仅作默认兜底,避免多宿主互相覆盖。
|
|
17
|
-
* summaryFile 未显式配置时保持包级全局路径(跨宿主聚合语义)。
|
|
18
17
|
*/
|
|
19
18
|
export function configureDirs(dirs) {
|
|
20
19
|
_dirs = { ...defaultDirs(), ...dirs };
|
|
20
|
+
setLogFile(_dirs.logFile);
|
|
21
21
|
}
|
|
22
22
|
/** 当前进程级默认目录(defaultDirs 与 configureDirs 的合并结果)。 */
|
|
23
23
|
export function getDirs() {
|
|
24
24
|
return _dirs;
|
|
25
25
|
}
|
|
26
|
-
/**
|
|
27
|
-
* 解析实例目录:defaultDirs → 进程级默认(configureDirs)→ 实例传入,三层合并。
|
|
28
|
-
* summaryFile 不随 metricsDir 派生:缺省恒为全局共享库(实例/进程级显式指定才能改写)。
|
|
29
|
-
*/
|
|
26
|
+
/** 解析实例目录:defaultDirs → 进程级默认(configureDirs)→ 实例传入,三层合并。 */
|
|
30
27
|
export function resolveDirs(partial) {
|
|
31
28
|
return partial ? { ...getDirs(), ...partial } : { ...getDirs() };
|
|
32
29
|
}
|
|
@@ -39,6 +36,3 @@ export function getEventsDir() {
|
|
|
39
36
|
export function getLogFile() {
|
|
40
37
|
return _dirs.logFile;
|
|
41
38
|
}
|
|
42
|
-
export function getSummaryFile() {
|
|
43
|
-
return _dirs.summaryFile;
|
|
44
|
-
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { MetricsEngine, MetricsOutput } from "../types.js";
|
|
2
|
+
import type { MetricsDirs } from "../dirs.js";
|
|
3
|
+
export type { TokenUsage, ToolStats, StageType, StageInfo, MessageEntry, ToolCallEntry, SessionMeta, CompileStats, SkillCallEntry, SkillSearchEntry, StepData, RoundSnapshot, SubAgentOutput, MetricsOutput, MetricsEngine, } from "../types.js";
|
|
4
|
+
export interface MetricsEngineOptions {
|
|
5
|
+
enabled?: boolean;
|
|
6
|
+
/** 本引擎输出目录(多引擎各自独立);缺省回退进程级默认(configureDirs),构造时解析固化 */
|
|
7
|
+
dirs?: Partial<MetricsDirs>;
|
|
8
|
+
/** flush 会话快照后回调(宿主可在此把快照上报自有系统) */
|
|
9
|
+
onFlush?: (sessionId: string, snapshot: MetricsOutput) => void;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* 创建指标引擎:订阅事件流,按会话累计状态,idle/flush/dispose 产出快照 JSON。
|
|
13
|
+
* 0.2.0 起移除 scope 门控——记录全部会话;子会话(parentID)自动并入父会话。
|
|
14
|
+
*/
|
|
15
|
+
export declare function createMetricsEngine(opts?: MetricsEngineOptions): MetricsEngine;
|
|
@@ -1,93 +1,23 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
1
|
+
import { TRACKED_EVENT_TYPES } from "../types.js";
|
|
2
|
+
import { createSessionMetrics } from "./state.js";
|
|
3
|
+
import { handlePartUpdated, handleSessionUpdated, handleMessageUpdated, handleSubAgentParts } from "./handlers.js";
|
|
4
|
+
import { handleSessionIdle, flushMetrics } from "../snapshot/flush.js";
|
|
5
|
+
import { mergeChildMetrics } from "../snapshot/merge.js";
|
|
6
|
+
import { resolveDirs } from "../dirs.js";
|
|
7
|
+
import { log } from "../../shared/log.js";
|
|
6
8
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* 统计范围 = 策略 + 宿主声明(claimSession)的交集判定:
|
|
10
|
-
* - mode all —— 全部(配合 agents 即只统计白名单 agent)
|
|
11
|
-
* - mode none —— 默认不统计;宿主 claimSession 才统计
|
|
12
|
-
* - mode declared—— 仅宿主 claimSession 的会话(或 agents 白名单命中)
|
|
13
|
-
* - mode cwd —— 仅工作目录命中 cwdPrefixes
|
|
14
|
-
* 子会话(parentID 归属已统计父会话)自动随父会话统计。
|
|
9
|
+
* 创建指标引擎:订阅事件流,按会话累计状态,idle/flush/dispose 产出快照 JSON。
|
|
10
|
+
* 0.2.0 起移除 scope 门控——记录全部会话;子会话(parentID)自动并入父会话。
|
|
15
11
|
*/
|
|
16
|
-
export function createMetricsEngine(opts = {
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
: opts;
|
|
20
|
-
const enabled = options.enabled !== false;
|
|
21
|
-
const scope = options.scope ?? { mode: "all" };
|
|
22
|
-
// 实例目录:构造时按 defaultDirs → 进程级默认 → 实例传入 三层合并固化;未传则 flush 时动态回退进程级默认
|
|
23
|
-
const instanceDirs = options.dirs ? resolveDirs(options.dirs) : undefined;
|
|
24
|
-
const agents = scope.agents && scope.agents.length > 0 ? new Set(scope.agents) : null;
|
|
25
|
-
const cwdPrefixes = scope.cwdPrefixes ?? [];
|
|
12
|
+
export function createMetricsEngine(opts = {}) {
|
|
13
|
+
const enabled = opts.enabled !== false;
|
|
14
|
+
const instanceDirs = opts.dirs ? resolveDirs(opts.dirs) : undefined;
|
|
26
15
|
const sessions = new Map();
|
|
27
16
|
const childToParent = new Map();
|
|
28
17
|
const childSessionIds = new Set();
|
|
29
|
-
const claimed = new Set();
|
|
30
|
-
// 已定案的会话统计与否(false 不缓存“信息未齐”的暂定,等 session.created 带 agent/dir 再定)
|
|
31
|
-
const resolved = new Map();
|
|
32
|
-
// 工作目录缓存(session.created 记录;cwd 策略与删除时反查用)
|
|
33
|
-
const sessionDirs = new Map();
|
|
34
|
-
const sessionAgents = new Map();
|
|
35
|
-
function decideFresh(id, agent, directory) {
|
|
36
|
-
if (claimed.has(id))
|
|
37
|
-
return true;
|
|
38
|
-
if (agents) {
|
|
39
|
-
if (!agent || !agents.has(agent))
|
|
40
|
-
return false;
|
|
41
|
-
}
|
|
42
|
-
switch (scope.mode) {
|
|
43
|
-
case "all": return true;
|
|
44
|
-
case "none": return false;
|
|
45
|
-
case "declared": return false;
|
|
46
|
-
case "cwd": return directory != null && cwdPrefixes.some((p) => directory.startsWith(p));
|
|
47
|
-
default: return false;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
/** 需等待 agent/directory 才可定案(事件带信息前先跳过,session.created 到达后定案)。 */
|
|
51
|
-
function needsInfo(id, agent, directory) {
|
|
52
|
-
if (agents && !agent)
|
|
53
|
-
return true;
|
|
54
|
-
if (scope.mode === "cwd" && directory == null)
|
|
55
|
-
return true;
|
|
56
|
-
return false;
|
|
57
|
-
}
|
|
58
18
|
function resolveSessionId(rawId) {
|
|
59
19
|
return childToParent.get(rawId) ?? rawId;
|
|
60
20
|
}
|
|
61
|
-
/** 判断事件目标会话是否纳入统计(子会话跟随父会话定案)。 */
|
|
62
|
-
function isTracked(id, eventType, props) {
|
|
63
|
-
let target = id;
|
|
64
|
-
if (id.startsWith("__migrate__:")) {
|
|
65
|
-
target = id.split(":")[2] ?? "";
|
|
66
|
-
}
|
|
67
|
-
else {
|
|
68
|
-
target = childToParent.get(id) ?? id;
|
|
69
|
-
}
|
|
70
|
-
if (!target)
|
|
71
|
-
return false;
|
|
72
|
-
const info = (props?.info ?? {});
|
|
73
|
-
const agent = typeof info.agent === "string" ? info.agent : sessionAgents.get(target);
|
|
74
|
-
const directory = typeof info.directory === "string" ? info.directory : sessionDirs.get(target);
|
|
75
|
-
// 记录目录/agent(含迁移解析出的父子关系场景)
|
|
76
|
-
if (eventType === "session.created" || eventType === "session.updated") {
|
|
77
|
-
if (typeof info.directory === "string")
|
|
78
|
-
sessionDirs.set(target, info.directory);
|
|
79
|
-
if (typeof info.agent === "string")
|
|
80
|
-
sessionAgents.set(target, info.agent);
|
|
81
|
-
}
|
|
82
|
-
const cached = resolved.get(target);
|
|
83
|
-
if (cached !== undefined)
|
|
84
|
-
return cached;
|
|
85
|
-
if (needsInfo(target, agent, directory))
|
|
86
|
-
return false;
|
|
87
|
-
const decision = decideFresh(target, agent, directory);
|
|
88
|
-
resolved.set(target, decision);
|
|
89
|
-
return decision;
|
|
90
|
-
}
|
|
91
21
|
function extractSessionId(event) {
|
|
92
22
|
const props = event.properties;
|
|
93
23
|
if (!props) {
|
|
@@ -104,7 +34,7 @@ export function createMetricsEngine(opts = { enabled: true, scope: { mode: "all"
|
|
|
104
34
|
childSessionIds.add(id);
|
|
105
35
|
const agentName = info.agent || "";
|
|
106
36
|
const title = info.title || "";
|
|
107
|
-
log.info("[Metrics]
|
|
37
|
+
log.info("[Metrics] sub-agent detected", { type: event.type, id, parentID, agentName, title });
|
|
108
38
|
return `__migrate__:${id}:${parentID}:${agentName}:${title}`;
|
|
109
39
|
}
|
|
110
40
|
if (id)
|
|
@@ -117,7 +47,7 @@ export function createMetricsEngine(opts = { enabled: true, scope: { mode: "all"
|
|
|
117
47
|
const info = props.info;
|
|
118
48
|
if (info?.id)
|
|
119
49
|
return resolveSessionId(info.id);
|
|
120
|
-
log.info("[Metrics] extractSessionId: no sessionID found", { type: event.type
|
|
50
|
+
log.info("[Metrics] extractSessionId: no sessionID found", { type: event.type });
|
|
121
51
|
return undefined;
|
|
122
52
|
}
|
|
123
53
|
function getOrCreateSession(sessionId) {
|
|
@@ -128,6 +58,16 @@ export function createMetricsEngine(opts = { enabled: true, scope: { mode: "all"
|
|
|
128
58
|
}
|
|
129
59
|
return state;
|
|
130
60
|
}
|
|
61
|
+
function emitFlush(sessionId, snapshot) {
|
|
62
|
+
if (snapshot && opts.onFlush) {
|
|
63
|
+
try {
|
|
64
|
+
opts.onFlush(sessionId, snapshot);
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
log.error("[Metrics] onFlush failed", { sessionId, error: String(err) });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
131
71
|
return {
|
|
132
72
|
ingest(event) {
|
|
133
73
|
if (!enabled)
|
|
@@ -140,10 +80,7 @@ export function createMetricsEngine(opts = { enabled: true, scope: { mode: "all"
|
|
|
140
80
|
return;
|
|
141
81
|
}
|
|
142
82
|
const props = event.properties;
|
|
143
|
-
//
|
|
144
|
-
if (!isTracked(sessionId, event.type, props))
|
|
145
|
-
return;
|
|
146
|
-
// Handle buffer migration when sub-agent relationship is discovered
|
|
83
|
+
// 子代理关系发现时迁移既有缓冲状态
|
|
147
84
|
if (sessionId.startsWith("__migrate__:")) {
|
|
148
85
|
const parts = sessionId.split(":");
|
|
149
86
|
const childId = parts[1];
|
|
@@ -152,7 +89,6 @@ export function createMetricsEngine(opts = { enabled: true, scope: { mode: "all"
|
|
|
152
89
|
const title = parts[4] || "";
|
|
153
90
|
sessionId = parentId;
|
|
154
91
|
const parentState = getOrCreateSession(parentId);
|
|
155
|
-
// Merge child's existing metrics to parent
|
|
156
92
|
const childState = sessions.get(childId);
|
|
157
93
|
if (childState) {
|
|
158
94
|
log.info("[Metrics] Migration: merging existing child state", { childId, parentId });
|
|
@@ -179,7 +115,7 @@ export function createMetricsEngine(opts = { enabled: true, scope: { mode: "all"
|
|
|
179
115
|
}
|
|
180
116
|
}
|
|
181
117
|
const state = getOrCreateSession(sessionId);
|
|
182
|
-
//
|
|
118
|
+
// 子代理事件路由:原始事件来自子会话时进入对应 subAgents 条目
|
|
183
119
|
const originalSessionId = props?.sessionID || "";
|
|
184
120
|
if (childSessionIds.has(originalSessionId)) {
|
|
185
121
|
const sub = state.subAgents.get(originalSessionId);
|
|
@@ -191,21 +127,18 @@ export function createMetricsEngine(opts = { enabled: true, scope: { mode: "all"
|
|
|
191
127
|
log.info("[Metrics] Sub-agent routing MISS — entry not found", { eventType: event.type, originalSessionId, subAgentsKeys: [...state.subAgents.keys()] });
|
|
192
128
|
}
|
|
193
129
|
}
|
|
194
|
-
//
|
|
130
|
+
// 提取工作目录
|
|
195
131
|
if (event.type === "session.created" && props) {
|
|
196
132
|
const info = props.info;
|
|
197
133
|
const directory = info?.directory;
|
|
198
|
-
if (directory)
|
|
134
|
+
if (directory)
|
|
199
135
|
state.sessionMeta.workingDirectory = directory;
|
|
200
|
-
sessionDirs.set(sessionId, directory);
|
|
201
|
-
}
|
|
202
136
|
}
|
|
203
|
-
//
|
|
137
|
+
// 轮内首个事件时间
|
|
204
138
|
if (!state._hasFirstEvent) {
|
|
205
139
|
state._hasFirstEvent = true;
|
|
206
140
|
state._firstEventTime = Date.now();
|
|
207
141
|
}
|
|
208
|
-
// Route to handler by event type
|
|
209
142
|
if (event.type === "message.part.updated" && props) {
|
|
210
143
|
handlePartUpdated(state, props);
|
|
211
144
|
}
|
|
@@ -213,15 +146,7 @@ export function createMetricsEngine(opts = { enabled: true, scope: { mode: "all"
|
|
|
213
146
|
handleSessionUpdated(state, props);
|
|
214
147
|
}
|
|
215
148
|
if (event.type === "session.idle") {
|
|
216
|
-
|
|
217
|
-
if (options.onFlush && snapshot) {
|
|
218
|
-
try {
|
|
219
|
-
options.onFlush(sessionId, snapshot);
|
|
220
|
-
}
|
|
221
|
-
catch (err) {
|
|
222
|
-
log.error("[Metrics] onFlush failed", { sessionId, error: String(err) });
|
|
223
|
-
}
|
|
224
|
-
}
|
|
149
|
+
emitFlush(sessionId, handleSessionIdle(state, sessionId, instanceDirs));
|
|
225
150
|
}
|
|
226
151
|
if (event.type === "session.compacted") {
|
|
227
152
|
state.compactions++;
|
|
@@ -244,13 +169,7 @@ export function createMetricsEngine(opts = { enabled: true, scope: { mode: "all"
|
|
|
244
169
|
ingestPrompt(sessionId, modelId, system) {
|
|
245
170
|
if (!enabled)
|
|
246
171
|
return;
|
|
247
|
-
const
|
|
248
|
-
const allowed = (scope.mode === "all" && !agents) ||
|
|
249
|
-
claimed.has(resolvedId) ||
|
|
250
|
-
resolved.get(resolvedId) === true;
|
|
251
|
-
if (!allowed)
|
|
252
|
-
return;
|
|
253
|
-
const state = getOrCreateSession(resolvedId);
|
|
172
|
+
const state = getOrCreateSession(resolveSessionId(sessionId));
|
|
254
173
|
state.systemPrompts.set(modelId, system);
|
|
255
174
|
},
|
|
256
175
|
isSubAgent(sessionId) {
|
|
@@ -262,15 +181,7 @@ export function createMetricsEngine(opts = { enabled: true, scope: { mode: "all"
|
|
|
262
181
|
return;
|
|
263
182
|
const now = Date.now();
|
|
264
183
|
state.endTime = now;
|
|
265
|
-
|
|
266
|
-
if (options.onFlush && snapshot) {
|
|
267
|
-
try {
|
|
268
|
-
options.onFlush(sessionId, snapshot);
|
|
269
|
-
}
|
|
270
|
-
catch (err) {
|
|
271
|
-
log.error("[Metrics] onFlush failed", { sessionId, error: String(err) });
|
|
272
|
-
}
|
|
273
|
-
}
|
|
184
|
+
emitFlush(sessionId, flushMetrics(sessionId, state, now, instanceDirs));
|
|
274
185
|
},
|
|
275
186
|
dispose() {
|
|
276
187
|
const now = Date.now();
|
|
@@ -297,35 +208,11 @@ export function createMetricsEngine(opts = { enabled: true, scope: { mode: "all"
|
|
|
297
208
|
state.compileStats._pendingFix = null;
|
|
298
209
|
}
|
|
299
210
|
state.endTime = now;
|
|
300
|
-
|
|
301
|
-
if (options.onFlush && snapshot) {
|
|
302
|
-
try {
|
|
303
|
-
options.onFlush(sessionId, snapshot);
|
|
304
|
-
}
|
|
305
|
-
catch (err) {
|
|
306
|
-
log.error("[Metrics] onFlush failed", { sessionId, error: String(err) });
|
|
307
|
-
}
|
|
308
|
-
}
|
|
211
|
+
emitFlush(sessionId, flushMetrics(sessionId, state, now, instanceDirs));
|
|
309
212
|
}
|
|
310
213
|
sessions.clear();
|
|
311
214
|
childToParent.clear();
|
|
312
215
|
childSessionIds.clear();
|
|
313
216
|
},
|
|
314
|
-
claimSession(sessionId, meta) {
|
|
315
|
-
claimed.add(sessionId);
|
|
316
|
-
if (meta?.agent)
|
|
317
|
-
sessionAgents.set(sessionId, meta.agent);
|
|
318
|
-
// 允许 claim 推翻早前“信息未齐暂不统计”的判定;宿主显式声明即强制纳入
|
|
319
|
-
resolved.set(sessionId, true);
|
|
320
|
-
},
|
|
321
|
-
scopeOf(sessionId) {
|
|
322
|
-
const v = resolved.get(sessionId);
|
|
323
|
-
if (v !== undefined)
|
|
324
|
-
return v;
|
|
325
|
-
return claimed.has(sessionId);
|
|
326
|
-
},
|
|
327
|
-
shouldTrack(sessionId, eventType, props) {
|
|
328
|
-
return isTracked(sessionId, eventType, props);
|
|
329
|
-
},
|
|
330
217
|
};
|
|
331
218
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { SessionMetricsState, SubAgentState } from "./
|
|
1
|
+
import type { SessionMetricsState, SubAgentState } from "./state.js";
|
|
2
2
|
declare function handlePartUpdated(state: SessionMetricsState, props: Record<string, unknown>): void;
|
|
3
3
|
declare function handleSessionUpdated(state: SessionMetricsState, props: Record<string, unknown>): void;
|
|
4
4
|
declare function handleMessageUpdated(state: SessionMetricsState, props: Record<string, unknown>): void;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { createTokenUsage } from "
|
|
2
|
-
import { parseHvigorwOutput } from "
|
|
1
|
+
import { createTokenUsage } from "../types.js";
|
|
2
|
+
import { parseHvigorwOutput } from "../analysis/hvigor.js";
|
|
3
3
|
// ─── Input Simplification ────────────────────────────────────────────────────
|
|
4
4
|
function simplifyInput(raw) {
|
|
5
5
|
if (!raw)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { MessageEntry, PlanningCall, RoundSnapshot, SkillCallEntry, SkillSearchEntry, TokenUsage, ToolCallEntry, SessionMeta, CompileStats, StageInfo, ToolStats } from "../types.js";
|
|
2
|
+
export interface SubAgentState {
|
|
3
|
+
sessionId: string;
|
|
4
|
+
agentName: string;
|
|
5
|
+
title: string;
|
|
6
|
+
messageMap: Map<string, MessageEntry>;
|
|
7
|
+
toolCallMap: Map<string, ToolCallEntry>;
|
|
8
|
+
textLengthMap: Map<string, number>;
|
|
9
|
+
orderCounter: number;
|
|
10
|
+
_userMessageIds: Set<string>;
|
|
11
|
+
_pendingUserMessage: string[];
|
|
12
|
+
}
|
|
13
|
+
export interface SessionMetricsState {
|
|
14
|
+
sessionId: string;
|
|
15
|
+
startTime: number;
|
|
16
|
+
endTime: number;
|
|
17
|
+
rounds: RoundSnapshot[];
|
|
18
|
+
tokens: TokenUsage;
|
|
19
|
+
tools: {
|
|
20
|
+
totalCalls: number;
|
|
21
|
+
invalidCalls: number;
|
|
22
|
+
completedCalls: number;
|
|
23
|
+
errorCalls: number;
|
|
24
|
+
distribution: Map<string, ToolStats>;
|
|
25
|
+
slowestCall: {
|
|
26
|
+
tool: string;
|
|
27
|
+
duration: number;
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
agent: {
|
|
31
|
+
current: string;
|
|
32
|
+
switches: number;
|
|
33
|
+
lastSwitchTime: number;
|
|
34
|
+
usage: Map<string, number>;
|
|
35
|
+
};
|
|
36
|
+
model: {
|
|
37
|
+
current: string;
|
|
38
|
+
switches: number;
|
|
39
|
+
tokenDistribution: Map<string, TokenUsage>;
|
|
40
|
+
};
|
|
41
|
+
compactions: number;
|
|
42
|
+
anomaly: {
|
|
43
|
+
triggered: boolean;
|
|
44
|
+
events: string[];
|
|
45
|
+
};
|
|
46
|
+
currentStage: StageInfo | null;
|
|
47
|
+
stages: StageInfo[];
|
|
48
|
+
lastStageEndTime: number;
|
|
49
|
+
_stageTokens: TokenUsage;
|
|
50
|
+
_roundStartTime: number;
|
|
51
|
+
_firstEventTime: number;
|
|
52
|
+
_firstTextTime: number;
|
|
53
|
+
_hasFirstEvent: boolean;
|
|
54
|
+
_hasTextPart: boolean;
|
|
55
|
+
_roundTokens: TokenUsage;
|
|
56
|
+
_roundToolCalls: number;
|
|
57
|
+
_roundErrors: number;
|
|
58
|
+
_roundFirstBuildTracked: boolean;
|
|
59
|
+
_roundIdleProcessed: boolean;
|
|
60
|
+
_idleFlushed: boolean;
|
|
61
|
+
messageMap: Map<string, MessageEntry>;
|
|
62
|
+
toolCallMap: Map<string, ToolCallEntry>;
|
|
63
|
+
textLengthMap: Map<string, number>;
|
|
64
|
+
sessionMeta: SessionMeta;
|
|
65
|
+
compileStats: CompileStats;
|
|
66
|
+
orderCounter: number;
|
|
67
|
+
skillCallMap: Map<string, SkillCallEntry>;
|
|
68
|
+
htmlPreviewMsgId: string | null;
|
|
69
|
+
skillSearchMap: Map<string, SkillSearchEntry>;
|
|
70
|
+
_pendingReadChain: {
|
|
71
|
+
searchCallID: string;
|
|
72
|
+
} | null;
|
|
73
|
+
_userMessageIds: Set<string>;
|
|
74
|
+
_pendingUserMessage: string[];
|
|
75
|
+
subAgents: Map<string, SubAgentState>;
|
|
76
|
+
planningCalls: PlanningCall[];
|
|
77
|
+
systemPrompts: Map<string, string[]>;
|
|
78
|
+
}
|
|
79
|
+
export declare function createSessionMetrics(sessionId: string): SessionMetricsState;
|
|
@@ -1,23 +1,5 @@
|
|
|
1
|
-
|
|
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
|
-
]);
|
|
1
|
+
import { createTokenUsage } from "../types.js";
|
|
17
2
|
// ─── Factory Functions ───────────────────────────────────────────────────────
|
|
18
|
-
export function createTokenUsage() {
|
|
19
|
-
return { input: 0, output: 0, reasoning: 0, cacheRead: 0, cacheWrite: 0, total: 0 };
|
|
20
|
-
}
|
|
21
3
|
export function createSessionMetrics(sessionId) {
|
|
22
4
|
const now = Date.now();
|
|
23
5
|
return {
|
|
@@ -11,10 +11,5 @@ export interface EventLogger {
|
|
|
11
11
|
export interface EventLoggerOptions {
|
|
12
12
|
/** 本实例事件日志目录(多引擎场景各自独立);缺省回退进程级默认(configureDirs) */
|
|
13
13
|
eventsDir?: string;
|
|
14
|
-
/** 会话归属门控:返回 false 的事件不写入本实例日志(与引擎共用同一 scope 判定) */
|
|
15
|
-
isTracked?: (sessionId: string, event: {
|
|
16
|
-
type: string;
|
|
17
|
-
properties?: Record<string, unknown>;
|
|
18
|
-
}) => boolean;
|
|
19
14
|
}
|
|
20
15
|
export declare function createEventLogger(enabled: boolean, cleanupEnabled?: boolean, opts?: EventLoggerOptions): EventLogger;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as path from "path";
|
|
3
|
-
import { getEventsDir } from "
|
|
4
|
-
import { TRACKED_EVENT_TYPES } from "
|
|
3
|
+
import { getEventsDir } from "../dirs.js";
|
|
4
|
+
import { TRACKED_EVENT_TYPES } from "../types.js";
|
|
5
5
|
// Rotation constants
|
|
6
6
|
const MAX_LOG_SIZE_BYTES = 20 * 1024 * 1024;
|
|
7
7
|
const MAX_LOG_AGE_DAYS = 7;
|
|
@@ -9,7 +9,6 @@ const MAX_ROTATED_FILES = 5;
|
|
|
9
9
|
const FLUSH_INTERVAL_MS = 500;
|
|
10
10
|
const BUFFER_SIZE_LIMIT = 50;
|
|
11
11
|
const SEPARATOR = "=".repeat(72);
|
|
12
|
-
// 鈹€鈹€鈹€ Helpers 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€
|
|
13
12
|
function formatTimestamp(date) {
|
|
14
13
|
const pad = (n) => String(n).padStart(2, "0");
|
|
15
14
|
const ms = String(Math.floor(date.getMilliseconds() / 10)).padStart(2, "0");
|
|
@@ -239,18 +238,6 @@ export function createEventLogger(enabled, cleanupEnabled = false, opts = {}) {
|
|
|
239
238
|
if (sessionId.startsWith("__migrate__:")) {
|
|
240
239
|
const [, childId, parentId] = sessionId.split(":");
|
|
241
240
|
sessionId = parentId;
|
|
242
|
-
// 子会话归属随父:父未纳入统计时丢弃子缓冲,不产生日志文件
|
|
243
|
-
if (opts.isTracked && !opts.isTracked(parentId, event)) {
|
|
244
|
-
const dropBuf = sessionBuffers.get(childId);
|
|
245
|
-
if (dropBuf) {
|
|
246
|
-
if (dropBuf.flushTimer) {
|
|
247
|
-
clearTimeout(dropBuf.flushTimer);
|
|
248
|
-
dropBuf.flushTimer = null;
|
|
249
|
-
}
|
|
250
|
-
sessionBuffers.delete(childId);
|
|
251
|
-
}
|
|
252
|
-
return;
|
|
253
|
-
}
|
|
254
241
|
// Move child's existing buffer to parent
|
|
255
242
|
const childBuf = sessionBuffers.get(childId);
|
|
256
243
|
if (childBuf) {
|
|
@@ -273,9 +260,6 @@ export function createEventLogger(enabled, cleanupEnabled = false, opts = {}) {
|
|
|
273
260
|
subAgent = { childId: rawSessionId };
|
|
274
261
|
}
|
|
275
262
|
}
|
|
276
|
-
// 归属门控:未纳入统计范围的会话不记录事件
|
|
277
|
-
if (opts.isTracked && !opts.isTracked(sessionId, event))
|
|
278
|
-
return;
|
|
279
263
|
const buf = getOrCreateBuffer(sessionId);
|
|
280
264
|
const ts = formatTimestamp(new Date());
|
|
281
265
|
// Write formatted event block
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { createMetricsEngine } from './engine/engine.js';
|
|
2
|
+
export type { MetricsEngineOptions } from './engine/engine.js';
|
|
3
|
+
export { configureDirs, defaultDirs, getDirs, resolveDirs, getMetricsDir, getEventsDir, getLogFile } from './dirs.js';
|
|
4
|
+
export type { MetricsDirs } from './dirs.js';
|
|
5
|
+
export { createEventLogger } from './eventlog/event-logger.js';
|
|
6
|
+
export type { EventLogger, EventLoggerOptions } from './eventlog/event-logger.js';
|
|
7
|
+
export { createMetricsRuntime } from './runtime.js';
|
|
8
|
+
export type { MetricsRuntime, MetricsRuntimeOptions } from './runtime.js';
|
|
9
|
+
export * from './types.js';
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// metrics 域出口:会话指标采集(引擎/快照/事件日志/编译分析/目录管理/运行时组装)
|
|
2
|
+
export { createMetricsEngine } from './engine/engine.js';
|
|
3
|
+
export { configureDirs, defaultDirs, getDirs, resolveDirs, getMetricsDir, getEventsDir, getLogFile } from './dirs.js';
|
|
4
|
+
export { createEventLogger } from './eventlog/event-logger.js';
|
|
5
|
+
export { createMetricsRuntime } from './runtime.js';
|
|
6
|
+
export * from './types.js';
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { MetricsEngineOptions } from './engine/engine.js';
|
|
2
|
+
import type { MetricsDirs } from './dirs.js';
|
|
3
|
+
export interface MetricsRuntimeOptions extends Omit<MetricsEngineOptions, 'enabled'> {
|
|
4
|
+
dirs?: Partial<MetricsDirs>;
|
|
5
|
+
enabled?: boolean;
|
|
6
|
+
/** 事件写盘(events/<sessionId>.log;steps 全文的数据源) */
|
|
7
|
+
eventLogging?: boolean;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* 组装一整套"通用统计插件"的运行时:事件记录 + 指标引擎。
|
|
11
|
+
* 目录为实例级(不产生全局副作用):defaultDirs → 进程级默认(configureDirs)→ opts.dirs 三层合并。
|
|
12
|
+
*/
|
|
13
|
+
export declare function createMetricsRuntime(opts?: MetricsRuntimeOptions): {
|
|
14
|
+
dirs: MetricsDirs;
|
|
15
|
+
eventLogger: import("./eventlog/event-logger.js").EventLogger | null;
|
|
16
|
+
engine: import("./types.js").MetricsEngine;
|
|
17
|
+
/** opencode event hook:`async ({ event }) => { runtime.event({ event }) }` */
|
|
18
|
+
event(input: {
|
|
19
|
+
event: {
|
|
20
|
+
type: string;
|
|
21
|
+
properties?: Record<string, unknown>;
|
|
22
|
+
};
|
|
23
|
+
}): void;
|
|
24
|
+
};
|
|
25
|
+
export type MetricsRuntime = ReturnType<typeof createMetricsRuntime>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { createMetricsEngine } from './engine/engine.js';
|
|
2
|
+
import { createEventLogger } from './eventlog/event-logger.js';
|
|
3
|
+
import { resolveDirs } from './dirs.js';
|
|
4
|
+
/**
|
|
5
|
+
* 组装一整套"通用统计插件"的运行时:事件记录 + 指标引擎。
|
|
6
|
+
* 目录为实例级(不产生全局副作用):defaultDirs → 进程级默认(configureDirs)→ opts.dirs 三层合并。
|
|
7
|
+
*/
|
|
8
|
+
export function createMetricsRuntime(opts = {}) {
|
|
9
|
+
const dirs = resolveDirs(opts.dirs);
|
|
10
|
+
const engine = createMetricsEngine({
|
|
11
|
+
enabled: opts.enabled,
|
|
12
|
+
dirs,
|
|
13
|
+
onFlush: opts.onFlush,
|
|
14
|
+
});
|
|
15
|
+
const eventLogger = (opts.eventLogging ?? true)
|
|
16
|
+
? createEventLogger(true, false, { eventsDir: dirs.eventsDir })
|
|
17
|
+
: null;
|
|
18
|
+
return {
|
|
19
|
+
dirs,
|
|
20
|
+
eventLogger,
|
|
21
|
+
engine,
|
|
22
|
+
/** opencode event hook:`async ({ event }) => { runtime.event({ event }) }` */
|
|
23
|
+
event(input) {
|
|
24
|
+
eventLogger?.log(input.event);
|
|
25
|
+
engine.ingest(input.event);
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { MetricsDirs } from "../dirs.js";
|
|
2
|
+
import type { SessionMetricsState } from "../engine/state.js";
|
|
3
|
+
import type { MetricsOutput } from "../types.js";
|
|
4
|
+
declare function flushMetrics(sessionId: string, state: SessionMetricsState, now: number, dirs?: Partial<MetricsDirs>): MetricsOutput;
|
|
5
|
+
declare function handleSessionIdle(state: SessionMetricsState, sessionId: string, dirs?: Partial<MetricsDirs>): MetricsOutput | undefined;
|
|
6
|
+
export { flushMetrics, handleSessionIdle };
|