opencode-metrics-plugin 0.3.5 → 0.4.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/CHANGELOG.md +5 -0
- package/README.md +1 -0
- package/dist/metrics/runtime.d.ts +11 -0
- package/dist/metrics/runtime.js +6 -0
- package/dist/metrics/sessionFilter.d.ts +12 -0
- package/dist/metrics/sessionFilter.js +104 -0
- package/dist/plugin.js +4 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.0(2026-09-15)
|
|
4
|
+
|
|
5
|
+
- 新增会话 agent 白名单 `agents`(支持 `*` 通配,如 `["android-to-hmos-*", "build"]`):配置后只有根会话 agent 命中任一模式才记录——快照、事件日志、system prompt、上传全不产生;子会话(task/explore 等)随父会话判定正常并入;agent 未知的会话默认记录(fail-open)。缺省/空数组 = 不过滤全记(向后兼容)。
|
|
6
|
+
- 典型用法:全局 `~/.config/opencode/opencode.json` 注册 `["opencode-metrics-plugin", { "agents": ["android-to-hmos-*"] }]`,实现「插件处处加载、只统计迁移 agent 的会话」,不受 opencode 项目级配置发现的 git 边界限制。注意项目 `.opencode/opencode-metrics.json(c)` 存在时仍会完全接管(含 `agents`),接管文件需同步配置。
|
|
7
|
+
|
|
3
8
|
## 0.3.5(2026-09-15)
|
|
4
9
|
|
|
5
10
|
- 修复「重启 + resume 续跑」系列数据损坏:steps 重复乱序、planning 清零、tokens/工具统计丢段、agent/model 归因丢失;累加字段改差量并入,连发消息即切轮、退出兜底补轮。
|
package/README.md
CHANGED
|
@@ -90,6 +90,7 @@
|
|
|
90
90
|
| 选项 | 类型 | 默认 | 说明 |
|
|
91
91
|
|---|---|---|---|
|
|
92
92
|
| `enabled` | boolean | `true` | 总开关,`false` 时不记录任何会话 |
|
|
93
|
+
| `agents` | string[] | — | 会话 agent 白名单(`*` 通配,如 `["android-to-hmos-*", "build"]`):根会话 agent 命中才记录(快照/事件日志/上传全不产生),子会话随父判定;缺省全记 |
|
|
93
94
|
| `eventLogging` | boolean | `true` | 原始事件写盘;关闭后快照仍生成,但 `steps[].text/reasoning` 为空 |
|
|
94
95
|
| `checkpointFlushIntervalMs` | number | `2000` | 实时落盘冷却窗口(ms),`0` 关闭退回只在会话空闲时落盘 |
|
|
95
96
|
| `dirs` | object | env-paths | `metricsDir` / `eventsDir` / `logFile`(基准,实际写 `<所在目录>/log/plugin/YYYYMMDD.log`)/ `uploadStagingDir` |
|
|
@@ -6,6 +6,12 @@ export interface MetricsRuntimeOptions extends Omit<MetricsEngineOptions, 'enabl
|
|
|
6
6
|
enabled?: boolean;
|
|
7
7
|
/** 事件写盘(events/<sessionId>.log;steps 全文的数据源) */
|
|
8
8
|
eventLogging?: boolean;
|
|
9
|
+
/**
|
|
10
|
+
* 会话 agent 白名单(通配符 `*`,如 ["android-to-hmos-*", "build"]):
|
|
11
|
+
* 根会话的 agent 命中任一模式才记录(快照/事件日志/上传全不产生),子会话随父判定;
|
|
12
|
+
* 缺省/空数组 = 不过滤,记录全部会话(向后兼容)。
|
|
13
|
+
*/
|
|
14
|
+
agents?: string[];
|
|
9
15
|
/** 上传子系统配置(DESIGN.md §13.8);enabled:true 才会创建管理器 */
|
|
10
16
|
upload?: UploadConfig;
|
|
11
17
|
}
|
|
@@ -18,6 +24,11 @@ export declare function createMetricsRuntime(opts?: MetricsRuntimeOptions): {
|
|
|
18
24
|
eventLogger: import("./eventlog/event-logger.js").EventLogger | null;
|
|
19
25
|
engine: import("./types.js").MetricsEngine;
|
|
20
26
|
uploadManager: import("./upload/uploadManager.js").UploadManager | null;
|
|
27
|
+
sessionFilter: {
|
|
28
|
+
active: boolean;
|
|
29
|
+
shouldTrackEvent: (event: import("./sessionFilter.js").SessionFilterEvent) => boolean;
|
|
30
|
+
shouldTrackSession: (sessionId: string) => boolean;
|
|
31
|
+
};
|
|
21
32
|
/** opencode event hook:`async ({ event }) => { runtime.event({ event }) }` */
|
|
22
33
|
event(input: {
|
|
23
34
|
event: {
|
package/dist/metrics/runtime.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createMetricsEngine } from './engine/engine.js';
|
|
2
2
|
import { createEventLogger } from './eventlog/event-logger.js';
|
|
3
|
+
import { createSessionFilter } from './sessionFilter.js';
|
|
3
4
|
import { resolveDirs } from './dirs.js';
|
|
4
5
|
import { createUploadManager } from './upload/uploadManager.js';
|
|
5
6
|
/**
|
|
@@ -17,6 +18,8 @@ export function createMetricsRuntime(opts = {}) {
|
|
|
17
18
|
const eventLogger = (opts.eventLogging ?? true)
|
|
18
19
|
? createEventLogger(true, false, { eventsDir: dirs.eventsDir })
|
|
19
20
|
: null;
|
|
21
|
+
// 会话 agent 过滤:event 入口统一拦截,事件日志/引擎/上传三个子系统一次挡住
|
|
22
|
+
const sessionFilter = createSessionFilter(opts.agents);
|
|
20
23
|
// 上传管理器旁路挂在 runtime.event 上(在 engine.ingest 之后,保证触发时快照已落盘)
|
|
21
24
|
const uploadManager = createUploadManager({
|
|
22
25
|
config: opts.upload,
|
|
@@ -32,8 +35,11 @@ export function createMetricsRuntime(opts = {}) {
|
|
|
32
35
|
eventLogger,
|
|
33
36
|
engine,
|
|
34
37
|
uploadManager,
|
|
38
|
+
sessionFilter,
|
|
35
39
|
/** opencode event hook:`async ({ event }) => { runtime.event({ event }) }` */
|
|
36
40
|
event(input) {
|
|
41
|
+
if (!sessionFilter.shouldTrackEvent(input.event))
|
|
42
|
+
return;
|
|
37
43
|
eventLogger?.log(input.event);
|
|
38
44
|
engine.ingest(input.event);
|
|
39
45
|
uploadManager?.observe(input.event);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface SessionFilterEvent {
|
|
2
|
+
type: string;
|
|
3
|
+
properties?: Record<string, unknown>;
|
|
4
|
+
}
|
|
5
|
+
/** 通配符转正则:* → 任意串,其余字符按字面匹配;整串匹配,大小写敏感 */
|
|
6
|
+
export declare function compileAgentPatterns(patterns: readonly string[]): RegExp[];
|
|
7
|
+
export declare function createSessionFilter(agents?: readonly string[]): {
|
|
8
|
+
active: boolean;
|
|
9
|
+
shouldTrackEvent: (event: SessionFilterEvent) => boolean;
|
|
10
|
+
shouldTrackSession: (sessionId: string) => boolean;
|
|
11
|
+
};
|
|
12
|
+
export type SessionFilter = ReturnType<typeof createSessionFilter>;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// 会话 agent 过滤器:按根会话的 agent 通配匹配决定是否记录,子会话随父判定。
|
|
2
|
+
//
|
|
3
|
+
// 设计要点:
|
|
4
|
+
// - 判定信号:session.created / session.updated 的 properties.info.agent(实测会话首个事件即携带);
|
|
5
|
+
// message.updated 的 info.agent 作兜底(session 事件缺 agent 时)。agent 首次非空即终判,不再翻案。
|
|
6
|
+
// - fail-open:agent 始终未知的会话默认记录,防止 opencode 事件结构变化导致漏采。
|
|
7
|
+
// - 子会话(info.parentID)不独立判定,解析到根会话后跟随其判定;父未判定时同样放行。
|
|
8
|
+
// - 未配置 agents(缺省/空数组/全空串)时过滤器不激活,所有事件直通(向后兼容)。
|
|
9
|
+
/** 通配符转正则:* → 任意串,其余字符按字面匹配;整串匹配,大小写敏感 */
|
|
10
|
+
export function compileAgentPatterns(patterns) {
|
|
11
|
+
const escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
12
|
+
return patterns.map((p) => new RegExp(`^${p.split("*").map(escape).join(".*")}$`));
|
|
13
|
+
}
|
|
14
|
+
export function createSessionFilter(agents) {
|
|
15
|
+
const patterns = (agents ?? []).filter((p) => typeof p === "string" && p.length > 0);
|
|
16
|
+
const active = patterns.length > 0;
|
|
17
|
+
const regexes = active ? compileAgentPatterns(patterns) : [];
|
|
18
|
+
const decisions = new Map();
|
|
19
|
+
const childToParent = new Map();
|
|
20
|
+
function matchesAgent(agent) {
|
|
21
|
+
return regexes.some((re) => re.test(agent));
|
|
22
|
+
}
|
|
23
|
+
/** 沿 parentID 链向上找根会话(防环,最多 10 层) */
|
|
24
|
+
function resolveRoot(sessionId) {
|
|
25
|
+
let root = sessionId;
|
|
26
|
+
for (let i = 0; i < 10; i++) {
|
|
27
|
+
const parent = childToParent.get(root);
|
|
28
|
+
if (!parent)
|
|
29
|
+
break;
|
|
30
|
+
root = parent;
|
|
31
|
+
}
|
|
32
|
+
return root;
|
|
33
|
+
}
|
|
34
|
+
function decide(rootId, agent) {
|
|
35
|
+
const existing = decisions.get(rootId);
|
|
36
|
+
if (existing === "in" || existing === "out")
|
|
37
|
+
return;
|
|
38
|
+
decisions.set(rootId, matchesAgent(agent) ? "in" : "out");
|
|
39
|
+
}
|
|
40
|
+
/** 会话级判定:供 system.transform 等按 sessionId 查询的入口 */
|
|
41
|
+
function shouldTrackSession(sessionId) {
|
|
42
|
+
if (!active)
|
|
43
|
+
return true;
|
|
44
|
+
return decisions.get(resolveRoot(sessionId)) !== "out";
|
|
45
|
+
}
|
|
46
|
+
function extractSessionId(props) {
|
|
47
|
+
const sessionID = props.sessionID;
|
|
48
|
+
if (typeof sessionID === "string" && sessionID)
|
|
49
|
+
return sessionID;
|
|
50
|
+
const info = props.info;
|
|
51
|
+
const id = info?.id;
|
|
52
|
+
if (typeof id === "string" && id)
|
|
53
|
+
return id;
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* 事件级判定 + 状态机推进;返回 false 表示该事件所属会话不在记录范围。
|
|
58
|
+
* 无法归属会话的事件放行(引擎侧自会丢弃)。
|
|
59
|
+
*/
|
|
60
|
+
function shouldTrackEvent(event) {
|
|
61
|
+
if (!active)
|
|
62
|
+
return true;
|
|
63
|
+
const props = event.properties;
|
|
64
|
+
if (!props)
|
|
65
|
+
return true;
|
|
66
|
+
if (event.type === "session.created" || event.type === "session.updated") {
|
|
67
|
+
const info = props.info;
|
|
68
|
+
const id = info?.id;
|
|
69
|
+
if (typeof id === "string" && id) {
|
|
70
|
+
const parentID = info.parentID;
|
|
71
|
+
if (typeof parentID === "string" && parentID) {
|
|
72
|
+
childToParent.set(id, parentID);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
const agent = info.agent;
|
|
76
|
+
if (typeof agent === "string" && agent)
|
|
77
|
+
decide(id, agent);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
else if (event.type === "message.updated") {
|
|
82
|
+
// 兜底:session 事件一直没带 agent 时,用消息的 agent 定案(仅根会话)
|
|
83
|
+
const sessionID = props.sessionID;
|
|
84
|
+
const info = props.info;
|
|
85
|
+
const agent = info?.agent;
|
|
86
|
+
if (typeof sessionID === "string" && sessionID && typeof agent === "string" && agent && !childToParent.has(sessionID)) {
|
|
87
|
+
decide(resolveRoot(sessionID), agent);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
else if (event.type === "session.deleted") {
|
|
91
|
+
// 清理:根会话删除即移除判定;子会话删除移除映射
|
|
92
|
+
const sid = extractSessionId(props);
|
|
93
|
+
if (sid) {
|
|
94
|
+
childToParent.delete(sid);
|
|
95
|
+
decisions.delete(sid);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const sid = extractSessionId(props);
|
|
99
|
+
if (!sid)
|
|
100
|
+
return true;
|
|
101
|
+
return shouldTrackSession(sid);
|
|
102
|
+
}
|
|
103
|
+
return { active, shouldTrackEvent, shouldTrackSession };
|
|
104
|
+
}
|
package/dist/plugin.js
CHANGED
|
@@ -23,6 +23,7 @@ export const MetricsPlugin = async (input, options) => {
|
|
|
23
23
|
eventsDir: runtime.dirs.eventsDir,
|
|
24
24
|
enabled: config.enabled !== false,
|
|
25
25
|
upload: Boolean(runtime.uploadManager),
|
|
26
|
+
agents: runtime.sessionFilter.active ? config.agents : null,
|
|
26
27
|
configSource: source,
|
|
27
28
|
});
|
|
28
29
|
const hooks = {
|
|
@@ -31,11 +32,13 @@ export const MetricsPlugin = async (input, options) => {
|
|
|
31
32
|
event: event,
|
|
32
33
|
});
|
|
33
34
|
},
|
|
34
|
-
// 记录各模型 system prompt
|
|
35
|
+
// 记录各模型 system prompt(只读不改写、不返回;子代理跳过;agents 过滤外的会话跳过)
|
|
35
36
|
"experimental.chat.system.transform": async (input, output) => {
|
|
36
37
|
const sessionId = input.sessionID;
|
|
37
38
|
if (!sessionId)
|
|
38
39
|
return;
|
|
40
|
+
if (!runtime.sessionFilter.shouldTrackSession(sessionId))
|
|
41
|
+
return;
|
|
39
42
|
if (runtime.eventLogger?.isSubAgent(sessionId))
|
|
40
43
|
return;
|
|
41
44
|
const modelId = input.model?.id ?? "unknown";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-metrics-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "opencode plugin: auto-records every session's metrics (tokens/rounds/steps/sub-agents/hvigor builds) to snapshot JSON. Zero DB, zero config. Pair with session-viewer for visualization.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|