pi-metrics 0.3.0 → 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/README.md +12 -2
- package/README.zh-CN.md +13 -1
- package/SKILL.md +1 -1
- package/locales/index.json +48 -0
- package/package.json +1 -1
- package/src/config.ts +52 -0
- package/src/index.ts +100 -1
package/README.md
CHANGED
|
@@ -11,9 +11,19 @@ Session metrics for the [Pi coding agent](https://github.com/earendil-works/pi):
|
|
|
11
11
|
- When the agent fully settles (`agent_settled` — including auto-retries, compaction continuations, or Esc interruption), a final line shows the **total elapsed time from message send to stop** (`⏱ Total elapsed 18.9s`).
|
|
12
12
|
- After each LLM turn, a notification reports TPS, TTFT, token counts, generation time, stalls, and blended cost when available.
|
|
13
13
|
- Telemetry is persisted as `tps` custom session entries and restored after session resume or `/tree` navigation.
|
|
14
|
-
- Metrics are exposed through session entries and notifications
|
|
14
|
+
- Metrics are exposed through session entries and notifications. Use `/config:metrics` to open the TUI settings menu, or use `/config:metrics enable|disable` to change the setting directly.
|
|
15
15
|
|
|
16
|
-
##
|
|
16
|
+
## Configuration
|
|
17
|
+
|
|
18
|
+
The configuration file is `<pi-agent-dir>/extensions/pi-metrics/config.json`:
|
|
19
|
+
|
|
20
|
+
```json
|
|
21
|
+
{
|
|
22
|
+
"enabled": true
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Use `/config:metrics enable` or `/config:metrics disable` to update it, or open the normal TUI menu with `/config:metrics`. Run `/reload` after manually editing the file.
|
|
17
27
|
|
|
18
28
|
The TPS implementation is maintained in this package. Remove the standalone `npm:@monotykamary/pi-tps` entry from Pi settings before enabling this package, otherwise both extensions will record duplicate `tps` entries and notifications.
|
|
19
29
|
|
package/README.zh-CN.md
CHANGED
|
@@ -11,7 +11,19 @@
|
|
|
11
11
|
- AI 完全停止时(`agent_settled`,覆盖自动重试、compaction 续跑以及 Esc 中断)追加一行**从发出消息到停止的总耗时**(`⏱ 总耗时 18.9s`)。
|
|
12
12
|
- 每轮 LLM 调用结束后显示 TPS、TTFT、token 数、生成耗时、stall 和可用的综合成本。
|
|
13
13
|
- Telemetry 以 `tps` custom session entry 持久化,并在恢复 session 或 `/tree` 后恢复显示。
|
|
14
|
-
- Metrics 通过 session entry
|
|
14
|
+
- Metrics 通过 session entry 和通知提供。使用 `/config:metrics` 打开 TUI 配置菜单,也可以使用 `/config:metrics enable|disable` 直接切换。
|
|
15
|
+
|
|
16
|
+
## 配置
|
|
17
|
+
|
|
18
|
+
配置文件为 `<Pi agent 目录>/extensions/pi-metrics/config.json`:
|
|
19
|
+
|
|
20
|
+
```json
|
|
21
|
+
{
|
|
22
|
+
"enabled": true
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
使用 `/config:metrics enable` 或 `/config:metrics disable` 修改配置,也可以使用 `/config:metrics` 打开常规 TUI 菜单;手动修改文件后执行 `/reload`。可参考 [`config.example.json`](./config.example.json)。
|
|
15
27
|
|
|
16
28
|
## 从 pi-tps 迁移
|
|
17
29
|
|
package/SKILL.md
CHANGED
|
@@ -5,7 +5,7 @@ description: "启用与排查 pi-metrics 的耗时、TPS、TTFT、token 和成
|
|
|
5
5
|
|
|
6
6
|
# 配置 pi-metrics
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
配置文件为 `<Pi agent 目录>/extensions/pi-metrics/config.json`,字段只有 `enabled`,默认是 `true`。可以使用 `/config:metrics` 打开 TUI 菜单,或使用 `/config:metrics enable|disable` 直接修改并持久化。手动修改配置文件后执行 `/reload`。
|
|
9
9
|
|
|
10
10
|
## 诊断与修改
|
|
11
11
|
|
package/locales/index.json
CHANGED
|
@@ -38,5 +38,53 @@
|
|
|
38
38
|
"tpsRate": {
|
|
39
39
|
"zh-CN": "${value}/M",
|
|
40
40
|
"en-US": "${value}/M"
|
|
41
|
+
},
|
|
42
|
+
"configCommandDescription": {
|
|
43
|
+
"zh-CN": "配置指标显示",
|
|
44
|
+
"en-US": "Configure metrics display"
|
|
45
|
+
},
|
|
46
|
+
"configCommandUsage": {
|
|
47
|
+
"zh-CN": "用法:/config:metrics(打开配置菜单)、enable、disable 或 reset",
|
|
48
|
+
"en-US": "Usage: /config:metrics (open settings), enable, disable, or reset"
|
|
49
|
+
},
|
|
50
|
+
"configMenuTitle": {
|
|
51
|
+
"zh-CN": "指标配置",
|
|
52
|
+
"en-US": "Metrics settings"
|
|
53
|
+
},
|
|
54
|
+
"configEnabled": {
|
|
55
|
+
"zh-CN": "启用指标:{value}",
|
|
56
|
+
"en-US": "Metrics enabled: {value}"
|
|
57
|
+
},
|
|
58
|
+
"configDisabled": {
|
|
59
|
+
"zh-CN": "禁用指标:{value}",
|
|
60
|
+
"en-US": "Metrics disabled: {value}"
|
|
61
|
+
},
|
|
62
|
+
"configOn": {
|
|
63
|
+
"zh-CN": "开",
|
|
64
|
+
"en-US": "on"
|
|
65
|
+
},
|
|
66
|
+
"configOff": {
|
|
67
|
+
"zh-CN": "关",
|
|
68
|
+
"en-US": "off"
|
|
69
|
+
},
|
|
70
|
+
"configDone": {
|
|
71
|
+
"zh-CN": "完成",
|
|
72
|
+
"en-US": "Done"
|
|
73
|
+
},
|
|
74
|
+
"configCommandInteractiveOnly": {
|
|
75
|
+
"zh-CN": "指标配置需要在 TUI 中修改;请在交互式 Pi 会话中执行命令。",
|
|
76
|
+
"en-US": "Metrics configuration requires the TUI; run this command in an interactive Pi session."
|
|
77
|
+
},
|
|
78
|
+
"configCommandSaved": {
|
|
79
|
+
"zh-CN": "指标配置已保存:{path}。请执行 /reload 使配置生效。",
|
|
80
|
+
"en-US": "Metrics configuration saved to {path}. Run /reload to apply it."
|
|
81
|
+
},
|
|
82
|
+
"configCommandInvalid": {
|
|
83
|
+
"zh-CN": "指标配置无效:{error}",
|
|
84
|
+
"en-US": "Invalid metrics configuration: {error}"
|
|
85
|
+
},
|
|
86
|
+
"configLoadFailed": {
|
|
87
|
+
"zh-CN": "指标配置读取失败:{path}({error})。当前继续启用指标。",
|
|
88
|
+
"en-US": "Failed to read metrics configuration: {path} ({error}). Metrics remain enabled."
|
|
41
89
|
}
|
|
42
90
|
}
|
package/package.json
CHANGED
package/src/config.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
|
|
5
|
+
const EXTENSIONS_DIRECTORY = "extensions";
|
|
6
|
+
const PACKAGE_NAME = "pi-metrics";
|
|
7
|
+
const CONFIG_FILE_NAME = "config.json";
|
|
8
|
+
const UTF8_ENCODING = "utf8";
|
|
9
|
+
const FILE_NOT_FOUND_CODE = "ENOENT";
|
|
10
|
+
|
|
11
|
+
export interface MetricsConfig {
|
|
12
|
+
enabled: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const DEFAULT_METRICS_CONFIG: MetricsConfig = { enabled: true };
|
|
16
|
+
|
|
17
|
+
/** 返回 pi-metrics 配置文件路径。 */
|
|
18
|
+
export function configPath(agentDir = getAgentDir()): string {
|
|
19
|
+
return join(agentDir, EXTENSIONS_DIRECTORY, PACKAGE_NAME, CONFIG_FILE_NAME);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** 解析并校验 pi-metrics 配置。 */
|
|
23
|
+
export function parseConfig(value: unknown): MetricsConfig {
|
|
24
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
25
|
+
throw new Error("configuration must be an object");
|
|
26
|
+
}
|
|
27
|
+
const raw = value as Record<string, unknown>;
|
|
28
|
+
for (const key of Object.keys(raw)) {
|
|
29
|
+
if (key !== "enabled") throw new Error(`unknown configuration field: ${key}`);
|
|
30
|
+
}
|
|
31
|
+
if (raw.enabled !== undefined && typeof raw.enabled !== "boolean") {
|
|
32
|
+
throw new Error("enabled must be a boolean");
|
|
33
|
+
}
|
|
34
|
+
return { enabled: (raw.enabled as boolean | undefined) ?? DEFAULT_METRICS_CONFIG.enabled };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** 读取配置文件;缺少文件时使用默认启用状态。 */
|
|
38
|
+
export function loadConfig(path = configPath()): MetricsConfig {
|
|
39
|
+
try {
|
|
40
|
+
return parseConfig(JSON.parse(readFileSync(path, UTF8_ENCODING)));
|
|
41
|
+
} catch (error) {
|
|
42
|
+
if ((error as NodeJS.ErrnoException).code === FILE_NOT_FOUND_CODE) return { ...DEFAULT_METRICS_CONFIG };
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** 将经过校验的配置写入配置文件。 */
|
|
48
|
+
export function saveConfig(config: MetricsConfig, path = configPath()): string {
|
|
49
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
50
|
+
writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`, UTF8_ENCODING);
|
|
51
|
+
return path;
|
|
52
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,12 +1,111 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionAPI,
|
|
3
|
+
ExtensionCommandContext,
|
|
4
|
+
ExtensionContext,
|
|
5
|
+
} from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { createTranslator, loadCatalog } from "pi-extensions-i18n";
|
|
2
7
|
import turnElapsed from "./turn-elapsed.ts";
|
|
3
8
|
import tps from "./tps.ts";
|
|
9
|
+
import { configPath, loadConfig, parseConfig, saveConfig, type MetricsConfig } from "./config.ts";
|
|
4
10
|
|
|
11
|
+
const messages = loadCatalog(new URL("../locales/index.json", import.meta.url));
|
|
12
|
+
const i18n = createTranslator(messages);
|
|
13
|
+
const CONFIG_COMMAND_ALIASES = ["config:metrics", "metrics-config", "pi-metrics-config"] as const;
|
|
14
|
+
const CONFIG_RESET_COMMAND = "reset";
|
|
15
|
+
const CONFIG_CHOICE = { enabled: 0, disabled: 1 } as const;
|
|
16
|
+
const ENABLE_COMMAND = "enable";
|
|
17
|
+
const DISABLE_COMMAND = "disable";
|
|
18
|
+
const NOTICE_WARNING = "warning" as const;
|
|
19
|
+
const NOTICE_INFO = "info" as const;
|
|
20
|
+
const NOTICE_ERROR = "error" as const;
|
|
21
|
+
|
|
22
|
+
/** 注册配置命令,通过 TUI 选择是否启用指标。 */
|
|
23
|
+
function registerConfigCommand(pi: ExtensionAPI): void {
|
|
24
|
+
const command = {
|
|
25
|
+
description: i18n.t("configCommandDescription"),
|
|
26
|
+
/** Completes the supported reset action. */
|
|
27
|
+
getArgumentCompletions: () => [{ value: CONFIG_RESET_COMMAND, label: CONFIG_RESET_COMMAND }],
|
|
28
|
+
/** Selects and persists the enabled state. */
|
|
29
|
+
handler: async (args: string, ctx: ExtensionCommandContext): Promise<void> => {
|
|
30
|
+
const value = args.trim();
|
|
31
|
+
if (value && value !== CONFIG_RESET_COMMAND && value !== ENABLE_COMMAND && value !== DISABLE_COMMAND) {
|
|
32
|
+
ctx.ui.notify(i18n.t("configCommandUsage"), NOTICE_WARNING);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (value === CONFIG_RESET_COMMAND || value === ENABLE_COMMAND || value === DISABLE_COMMAND) {
|
|
36
|
+
try {
|
|
37
|
+
const config = parseConfig(value === CONFIG_RESET_COMMAND ? {} : { enabled: value === ENABLE_COMMAND });
|
|
38
|
+
const path = saveConfig(config);
|
|
39
|
+
ctx.ui.notify(i18n.t("configCommandSaved", { path }), NOTICE_INFO);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
ctx.ui.notify(i18n.t("configCommandInvalid", {
|
|
42
|
+
error: error instanceof Error ? error.message : String(error),
|
|
43
|
+
}), NOTICE_ERROR);
|
|
44
|
+
}
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (!ctx.hasUI) {
|
|
48
|
+
ctx.ui.notify(i18n.t("configCommandInteractiveOnly"), NOTICE_WARNING);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
let current: MetricsConfig;
|
|
52
|
+
try {
|
|
53
|
+
current = loadConfig();
|
|
54
|
+
} catch (error) {
|
|
55
|
+
ctx.ui.notify(i18n.t("configCommandInvalid", {
|
|
56
|
+
error: error instanceof Error ? error.message : String(error),
|
|
57
|
+
}), NOTICE_ERROR);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const enabledChoice = i18n.t("configEnabled", { value: i18n.t(current.enabled ? "configOn" : "configOff") });
|
|
61
|
+
const disabledChoice = i18n.t("configDisabled", { value: i18n.t(current.enabled ? "configOff" : "configOn") });
|
|
62
|
+
const doneChoice = i18n.t("configDone");
|
|
63
|
+
const selected = await ctx.ui.select(i18n.t("configMenuTitle"), [
|
|
64
|
+
enabledChoice,
|
|
65
|
+
disabledChoice,
|
|
66
|
+
doneChoice,
|
|
67
|
+
]);
|
|
68
|
+
if (selected === undefined || selected === doneChoice) return;
|
|
69
|
+
const choice = selected === enabledChoice ? CONFIG_CHOICE.enabled : CONFIG_CHOICE.disabled;
|
|
70
|
+
try {
|
|
71
|
+
const path = saveConfig({ enabled: choice === CONFIG_CHOICE.enabled });
|
|
72
|
+
ctx.ui.notify(i18n.t("configCommandSaved", { path }), NOTICE_INFO);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
ctx.ui.notify(i18n.t("configCommandInvalid", {
|
|
75
|
+
error: error instanceof Error ? error.message : String(error),
|
|
76
|
+
}), NOTICE_ERROR);
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
for (const name of CONFIG_COMMAND_ALIASES) pi.registerCommand(name, command);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** 注册耗时和 TPS 指标事件;配置关闭时不注册指标处理器。 */
|
|
5
84
|
export default function piHud(pi: ExtensionAPI): void {
|
|
85
|
+
registerConfigCommand(pi);
|
|
86
|
+
let config: MetricsConfig;
|
|
87
|
+
let configError: unknown;
|
|
88
|
+
try {
|
|
89
|
+
config = loadConfig();
|
|
90
|
+
} catch (error) {
|
|
91
|
+
config = { enabled: true };
|
|
92
|
+
configError = error;
|
|
93
|
+
}
|
|
94
|
+
if (configError !== undefined) {
|
|
95
|
+
pi.on("session_start", (_event, ctx: ExtensionContext) => {
|
|
96
|
+
ctx.ui.notify(i18n.t("configLoadFailed", {
|
|
97
|
+
path: configPath(),
|
|
98
|
+
error: configError instanceof Error ? configError.message : String(configError),
|
|
99
|
+
}), NOTICE_WARNING);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
if (!config.enabled) return;
|
|
6
103
|
turnElapsed(pi);
|
|
7
104
|
tps(pi);
|
|
8
105
|
}
|
|
9
106
|
|
|
107
|
+
export { configPath, loadConfig, parseConfig, saveConfig } from "./config.ts";
|
|
108
|
+
export type { MetricsConfig } from "./config.ts";
|
|
10
109
|
export { default as turnElapsed } from "./turn-elapsed.ts";
|
|
11
110
|
export { default as tps } from "./tps.ts";
|
|
12
111
|
export * from "./format-utils.ts";
|