# Pi Metrics

[English](README.md) | 简体中文

为 Pi 提供可观测的活动延迟、包含推理用量但**排除 TTFT 的解码速度估计**，以及快速、实时变化的近似已观测流速率。显式接入的 bridge 可为子 SDK 会话及其他 agent 提供独立归属的请求 TTFT／TPS；兼容的 `subagent` 工具 details 在没有计时时提供仅用量报告。数值测量保存在 Pi 会话中。存在兼容的逐次尝试收据时，`/metrics` 和详情还会独立显示 **Translator reported usage（Translator 已报告用量）**；原生总量仍保持原生口径。扩展不修改提示词、不包装 provider、不额外调用模型，也不尝试恢复模型的私有推理内容。

**版本：** `0.0.7`；父会话测量 schema 为 **2**，agent 事件契约为 **1**。已为以 `@orbit-labs-ai/pi-metrics` 名称进行**首次公开 npm 分发**及 GitHub 发布做好准备。尚未确认发布完成；下文 npm 命令仅在维护者验证 `0.0.7` 已可获取后适用。参见[更新日志](CHANGELOG.zh-CN.md)。

## 环境要求

- **Pi 1.1.0+**、**Node.js 22.19+**。
- Pi 以宿主 peer 依赖提供 `@earendil-works/pi-coding-agent`、`@earendil-works/pi-ai` 和 `@earendil-works/pi-tui`，不将其打包。无需单独执行 `npm install` 或构建。发布候选版使用未经修改的官方 Pi **1.4.0** SDK 检查。
- 页脚需要交互式终端 UI。print、JSON、RPC 和 SDK 会话仍可记录数据。`/metrics` 通知需要支持 UI 的宿主，例如 TUI 或支持相应协议的 RPC 客户端。

## 快速开始

确认 npm 发布后，加载固定版本的包，仅本次使用，不注册持久安装：

```sh
pi -e npm:@orbit-labs-ai/pi-metrics@0.0.7
```

如需只记录、不替换页脚：

```sh
pi -e npm:@orbit-labs-ai/pi-metrics@0.0.7 --metrics-no-footer
```

发布前，或进行源码开发时，改在**仓库根目录**加载：

```sh
pi -e ./Pi/pi-metrics/index.ts
```

源码命令也接受 `--metrics-no-footer`。不要与现有安装同时加载第二份。加载本身不发送模型请求；解析 npm 来源会从注册表下载包代码。日常提示词及 Pi 自身的后台工作仍遵循你的 provider 配置和计费规则。

## 持久 npm 安装

维护者确认 npm 可获取后：

```sh
pi install npm:@orbit-labs-ai/pi-metrics@0.0.7
```

Pi 将包安装到其托管 npm 目录，并注册到个人 Pi 设置。新建会话，或在空闲时执行 `/reload`。精确版本已**固定**：`pi update --extensions` 会核对安装状态，但不会将其提升到新版本。更新需要明确指定新版本；不要替换活跃会话正在使用的文件，也不要加载两份副本。

### 从 0.0.6 升级

已有本地副本不会自动更新。先检查 `pi list`，等待空闲后，使用旧 Metrics 注册项的精确来源／路径移除注册，再安装固定 npm 版本（确认发布之后），或注册一个全新的稳定副本。保留旧本地目录，直到没有会话使用它。不要移除无关包或改写会话历史。父会话 schema 2 和 agent 契约 1 不变。Translator 用量报告需要单独升级到支持收据的 Translator；无法回填早期缺失收据，也不会自动转发子会话收据。

### 包含文件

公开 tarball 使用明确的文件白名单：六个运行时 TypeScript 模块（`index.ts`、`metrics.ts`、`footer.ts`、`agents.ts`、`agent-bridge.ts`、**`translator.ts`**）、`package.json`、中英文 README、中英文更新日志，以及从仓库复制的本地 **MIT `LICENSE`**。不包含测试、开发脚本、日志、个人运行数据、生成的宿主类型声明或打包依赖。`translator.ts` 仅读取兼容的用量收据，不是内置的 Translator 工具副本。Pi 仍是运行宿主，而不是与 Metrics 一同安装的包依赖。

## 持久本地安装

**不要从经常切换工具分支的 checkout 安装。** Pi 会直接加载本地包路径。请将工具复制到 Git 仓库外全新的稳定目录：

```sh
mkdir -p "$HOME/.local/share/orbit-tools" &&
mkdir "$HOME/.local/share/orbit-tools/pi-metrics" &&
cp -R ./Pi/pi-metrics/. "$HOME/.local/share/orbit-tools/pi-metrics/" &&
pi install "$HOME/.local/share/orbit-tools/pi-metrics"
```

从仓库根目录运行。目标已存在时会故意失败。最后一步将本地目录注册到个人 Pi 设置，并非 npm 安装。新建会话，或在空闲时执行 `/reload`。不要再用 `-e` 同时加载第二份。

**更新：** 先检查现有安装，将新源码复制到另一个全新目录，再把旧包注册项替换为新路径。不要覆盖运行中会话使用的副本，也不要同时注册两版。旧副本在没有会话使用前应保留。更新是手动操作，不会自动从本分支拉取。若采用其他路径，移除时也应使用实际注册的路径。

## 命令与默认值

| 命令或参数 | 行为 |
|---|---|
| `/metrics` | 当前分支的活动／请求延迟均值、有效吞吐量、请求覆盖率、原生全会话用量、独立的 Translator 已报告用量，以及其他 agent 的观测／明细。 |
| `/metrics details` | 在 Pi 原生编辑器中查看／复制所有当前页脚指标和扩展状态，包括隐藏的空闲状态。即使提交，编辑也会被丢弃，不发送任何内容；RPC 改用通知。 |
| `/metrics footer on` | 在 TUI 中启用／重新取得 Metrics 页脚。 |
| `/metrics footer off` | 关闭页脚，继续记录。 |
| `/metrics footer collapse` / `expand` | 选择单行或三行页脚，不停止记录。 |
| `/metrics footer toggle` / `Alt+M` | Metrics 拥有页脚时切换折叠／展开。 |
| `--metrics-no-footer` | 启动时不安装自定义页脚，默认 `false`；仍记录数据。 |

右侧的 **`[▾ Collapse]`／`[▸ Expand]`** 按钮可在支持鼠标的 Pi fullscreen 模式点击（已在 1.3.0 验证）；窄窗口显示 `[▾]`／`[▸]`。regular 模式保留终端原生鼠标选择，请用 `Alt+M` 或命令。如果终端不将 Alt 作为 Meta 发送，请使用命令。点击不会从提示词编辑器夺取键盘焦点。折叠／展开不会抢回已被其他扩展替换的页脚；需要时明确执行 `footer on`。

开关仅保存在内存中。新会话和重载默认**展开**，是否启用按 CLI 参数初始化；同一 runtime 内 off/on 保留所选布局。没有重置／删除记录命令，也没有单独的偏好设置文件。

### 页脚兼容性

Pi **只有一个自定义页脚槽位**。启用 Metrics 可以替换其他扩展的页脚。关闭 Metrics 时，仅在它仍拥有该槽位的情况下恢复 Pi 内置页脚，不恢复先前其他扩展的布局。如果已被其他扩展替换，关闭 Metrics 不会清除那个替代页脚。

其他扩展通过 `ctx.ui.setStatus()` 发布的文字会去除控制序列，不会从那些扩展中删除。默认只隐藏精确匹配的已知空闲键值对：`lsp` → `LSP: idle`，以及 `pi-background-task` → `0 bg tasks`。活跃及未知状态仍可显示，警告／错误文字优先；空闲状态仍可在 `/metrics details` 查看。Metrics 未拥有页脚时，详情会明确标明 Git 分支／扩展状态不可用。

无法组合多个自定义页脚布局。其他扩展应占用页脚时使用 `--metrics-no-footer`。不要为处理页脚冲突重置无关扩展。

## 阅读页脚

展开页脚**最多三行**，折叠后为**一行**。保留已有符号。当前目录采用**加粗的主题强调色**，作为视觉锚点；指标数值加粗，标签及模型／分支等信息较淡，组间间距统一。活跃扩展状态与末行共用空间，不额外增加行。宽终端展开示意：

```text
~/project · ⎇ feature/demo                                                provider/model · high  [▾ Collapse]
◷ TTFT 840ms obs   ↯ EFF TPS 38.4 tok/s · last   ⋯ OBS TPS ≈42.6 tok/s   ↗ IN 12k   ↙ OUT 2.1k
◫ CTX ━╌╌╌╌╌ 24.0% · 48k/200k   ◇ COST $0.012   ◈ CACHE R 8.2k · W 0 · HIT 40%
```

无活跃扩展状态时的折叠示意：

```text
TTFT 840ms  TPS 38.4 · last   ◫ CTX 24%   provider/model · high   … +1  [▸ Expand]
```

较窄终端以 `… +N` 标明省略的次要指标组数量，长值用省略号截断，可选标题信息直接隐藏，不换行。长目录的末尾仍保持可见。`/metrics details` 保留**所有**数值及完整状态文字；显示不全时可用它查看。折叠状态**优先显示 TTFT 和 TPS**，与展开视图使用相同的父活动数值及 `last`／`pending`／`so far` 标记。简写 `TPS` 指以 token／秒为单位的 EFF TPS，不是实时 OBS TPS。高上下文占用（70% 警告／90% 错误色）、警告／活跃状态、父活动状态及带归属的 agent 性能优先于普通上下文、模型信息和目录；可选组按宽度显示，长路径／模型不再挤掉性能指标。极窄终端仍不可避免地截断文字。全部布局按 Unicode 终端列宽计算。

无需 Nerd Font。字体家族和字号由终端而非 Pi 控制；可选带有真实粗体字重的等宽字体，如 **JetBrains Mono** 或 **SF Mono**。本扩展只调整主题颜色、字重和间距，不修改终端配置，也不安装字体。

| 字段 | 含义 |
|---|---|
| 标题行 | 突出的当前目录、按空间显示的 Git 分支、会话名、选定 provider／model 和 thinking level，以及折叠按钮。最近实际响应模型不同时，完整详情始终另行标出。 |
| `TTFT … obs` | 本次活动首次**观测到**内容的时间。跨工具轮次和自动续接循环保持不变，不按每次 API 请求重新开始。 |
| `EFF TPS` | 已报告输出量／从首次观测到 token 至响应完成的时间，按活动内的请求汇总。用于估计解码速度；包含已报告推理 token，但排除每个请求的 TTFT、工具执行及请求间空隙。 |
| `last`／`pending`／`so far` | EFF TPS 保留上次估计，直到新的有效估计产生。当前活动覆盖不足时，保留值标为 `last`；只有尚无估计时才使用 `pending`。活动仍在进行时，已完成且符合条件的请求汇总标为 `so far`，而非最终值。 |
| `OBS TPS ≈` | 仅在实时滚动速率为正时显示：已暴露文本、思考及工具参数字符的近似到达速率。不可用、归零或响应结束时隐藏，不是隐藏推理速率。 |
| `AGENT …`／`Agents: … active requests` | 显式接入的子／其他 agent 请求观测，独立展示。空间允许时显示最近开始的已记录 agent 请求的 TTFT／EFF TPS／状态。活跃数量指已观测请求，不是全部进程或任务数。`/metrics details` 保留每个 source／agent／request、模型、覆盖率及仅用量工具报告。不会用 agent 的速率替代父会话速率。 |
| `IN`／`OUT` | **整个会话文件**的 Pi 原生输入／输出用量，包括废弃分支及通过顶层工具用量报告的子 agent 用量。输出已包含标准化后的推理用量。不加入 Translator 自定义收据。 |
| `CTX` | Pi 当前估计的上下文 token／容量及百分比；未知时用 `?`，包括压缩之后。 |
| `CACHE` | 整个会话的读／写缓存 token。`HIT` 为最近已定稿请求的 `cacheRead / (input + cacheRead + cacheWrite)`。 |
| `COST` | 按 Pi 保存的用量及配置价格累计的美元费用，不是账单或订阅余额。缺少费用不代表免费。 |

**原生总量是动态更新的已存储计量，不是实时流估算。** Pi 记录定稿的 assistant／工具用量及辅助用量后，总量随之更新。当前 Workbench 为子 agent 返回顶层工具用量，因此这些工作**已包含**在原生 `IN`／`OUT`／`CACHE`／`COST` 中。Agent／details 记录只提供观测或明细；Metrics 不会再次相加。若工具只提供 details 而未提供原生用量，原生总量可能不完整。`CTX` 和缓存 `HIT` 跟随当前分支，不采用全会话计量口径。`/metrics details` 将这些口径与当前活动性能分开标注。

不可用的延迟／速率显示 `—`，不伪造零值；但 EFF TPS 有历史估计时会保留并标为 `last`，不活跃的 OBS TPS 则隐藏。原生用量／费用保留 Pi 的标准化数值，包括其零值默认值。失败、中止及 deferred 响应会标注。恢复的部分活动标为 incomplete，不显示最终有效速率。保存失败时显示警告／状态，并在事件处理边界通过 Pi 扩展错误机制报告，不向 JSON／RPC stdout 写入破坏协议的文字。

## 测量定义

### 可观测 TTFT 与活动边界

一次**活动**从首个 `agent_start` 开始，到 `agent_settled` 结束，包含其中的工具轮次、自动重试、压缩后的续接及队列工作。排队的 steering／follow-up 消息可能属于同一次活动，因此不一定对应一条键入提示词或 Pi 底层的一个 `turn`。

首个非空标准化 `thinking_delta`、`text_delta` 或 `toolcall_delta` 固定活动 TTFT。空 delta、开始标记及传输元数据不计入。首个思考、首个文本及首个工具参数时间分别记录。工具执行后，或同一未结束活动内再次触发 `agent_start` 续接时，不重置该时钟。

每个对话请求**也保留自身的时间信息**：从本扩展 `context_with_system` 处理器运行，到首次观测 delta 及 `message_end`。响应计时在工具执行前冻结；最终用量在 `turn_end` 获取，此时消息变换及工具结果已持久化。活动和请求使用同一单调时钟，保存的时长保留完整数值精度。ISO 墙钟起止时间另行保存，不能替代单调时钟计算时长。

这些是**客户端观测边界**，不是按下 Enter 后的延迟、首个 HTTP 字节时间或服务端首次生成 token 的时间。对应 hook 之前的工作不计入；区间内的认证、后续准备、传输、缓冲、provider 重试及观测开销均计入。若在活动／请求已开始后才接入，缺失的时间保持未知。

### 隐藏思考：能测量什么，不能测量什么

- 仅被 Pi 界面隐藏的思考仍产生标准化事件，可正常观测。
- provider 可能只暴露推理摘要，而非完整内部推理；这些摘要仅属于已观测流。
- 从未流式暴露的推理无法实时计数，也无法由本扩展确定其精确首 token／生成时间。
- Pi 标准化后的 **`usage.output` 包含 `usage.reasoning`**。推理属于子集，不能再次相加。即使没有暴露 delta，也保存最终输出总量，但 EFF TPS 要求观测到首个 token。
- 保留 Pi 提供的标准化推理计数，但缺失或零值不能证明没有思考：部分 Pi adapter 会把未提供的细分默认置零。`reasoningBreakdown` 明确区分 `positive` 和 `zero-or-unreported`。
- 无法将其中专属的**隐藏**推理 token 数和时长从已暴露摘要、prefill、排队等延迟中分离。因此 `hiddenReasoningTokens` 和 `hiddenReasoningMs` 明确为 `null`，不进行猜测。

计量依赖 provider 报告及 Pi／其他扩展的标准化结果。客户端指标无法恢复未报告的用量、未公开的推理文本或精确服务端解码时间。

### 有效 TPS

```text
streamMs = response completion time - first observed token time
request effective TPS = normalized output tokens / (streamMs / 1000)
activity effective TPS = sum(output tokens) / (sum(request streamMs) / 1000)
```

分母从**首次观测到非空思考、文本或工具参数 delta** 开始，到工具执行前的 `message_end` 结束。**每个请求**的全部初始等待（TTFT）都被排除，而非每次活动只排除一次。工具执行、请求间空隙及独立辅助模型工作也不计入。完整 `responseMs` 仍保存用于延迟分析，但不再作为 EFF TPS 的分母。

例如，初始等待 20 秒，随后在 5 秒流式阶段内报告 200 个输出 token，则为 **200 / 5 = 40 tok/s**，不是 200 / 25 = 8。只延长初始等待，不会改变 EFF TPS。

这是**基于客户端观测的解码速度估计**，不是精确的服务端解码速度或单 token 延迟。已报告输出仍包含推理；若 provider 在首次观测到 token 之前生成了隐藏推理，这些 token 仍在分子中，但对应初始时间被排除。例如，在上述 5 秒已观测区间内，用 1,200 个已报告 token（含 1,000 个隐藏推理 token）计算会得到 240 tok/s。因此隐藏推理、缓冲和完成阶段开销可能影响估算；不会推测或补回无法观测的隐藏时长。

请求只有在以 `stop`、`length` 或 `toolUse` 结束、输出用量为有效正数、首 token 至完成的时长已知且为正时才符合条件。推理／输出计数矛盾、缺少用量、错误、中止和 deferred 响应均不符合条件。若单个缓冲块到达后才完成响应，即使没有有意义的滚动 OBS 速率，也可计算 EFF TPS。只有用量而没有已观测 delta 的响应，或零长度流，无法提供新的 EFF TPS，不退回使用完整请求时间。零输出保守地视为不可用，因为 Pi 也用零表示缺失用量。

任何已开始请求尚未完成或不符合条件时，记录中的活动 EFF TPS 都不可用；覆盖计算不会悄悄排除失败请求。页脚会跨工具调用、新内容、续接请求及新活动保留**上次有效估计**，直到新的有效估计将其替换。当前覆盖不足时，`last` 表示这只是显示层保留值，并非当前活动的最终速率。若从未有过估计，EFF TPS 仍显示 `—`。

重载和分支导航仅从当前分支已定稿的请求记录重建保留值，包括后来变为不完整的活动中较早已完成且符合条件的请求前缀。不会将临时响应 checkpoint 当作已定稿历史；新会话不携带保留值。临时 `so far` 估计仍可能因最终消息变换而修正或撤回；只有有效的权威定稿估计才会替换保留值。该显示行为不改变保存的测量、有效性判定或计算公式。

`/metrics` 另外报告被覆盖请求的总输出量／已观测流总时长（即按流时长加权的速率），并明确给出**已覆盖／已记录**数量，不使用各请求速率的简单平均。历史或辅助原生用量不混入分子。Provider `durationMs` 另行保存，不替代该公式的分母。

### 近似已观测流 TPS

已观测文本、思考及工具参数的 **Unicode code point 数／4**（包括空白），除以最长为滚动 **2 秒**的已观测时间。窗口在请求开始处截断，而非在首个到达块处截断，避免夸大启动阶段速率。计数放入 **100 ms 桶**，边界桶会产生近似误差。至少需要两个非空 delta，且距离首个 delta 已过 250 ms。即使模型仍在隐藏推理，没有已暴露输出到达时，该速率也可能降为零。在获得正值估计前，页脚隐藏 OBS TPS；速率归零或响应结束时再次隐藏；已暴露输出恢复且滚动速率为正时重新显示。静默期间仍继续采样，零值样本仍保存在会话记录中。

采样／重绘间隔为 **250 ms**，受事件循环延迟影响。这不是真实 tokenization；语言、代码、emoji、JSON、摘要及批量到达都可能显著影响估算。`≈` 和独立的 `OBS` 标签是有意保留的，不据此外推隐藏 token 速率。

## 子 agent 与其他 agent

### 可用接入与边界

Pi 父会话的 `message_update` 事件描述的是**父会话**，不是所有子进程。兼容的 `subagent` single／parallel／chain details 结构提供 `details.results[].usage`，不提供子请求开始或非空 delta 时间戳；Pi **1.3.0** 随包附带的 `examples/extensions/subagent/index.ts` 也使用此结构。这些聚合用量不足以计算 TTFT 或解码 TPS。工具墙钟时长包含启动、排队、工具及多个请求，**绝不作为子请求流时长**。

Metrics 在 `turn_end` 自动从该 `subagent` details 结构复制白名单中的最终用量：agent／model、结果位置、模式、退出码、轮次、输入／输出／缓存 token 及费用。不读取任务／消息／stderr 文本，不猜测计时，也不回填旧工具结果。只有顶层已定稿工具结果具有此仅用量适配所需的持久 entry ID；缺少该边界的原始嵌套 details 不导入。这些是**仅供拆分查看的明细报告**，不是原生总量排除的金额。当前 Workbench 通过顶层 `result.usage` 返回子 agent 用量；Pi 已经计入，Metrics 不会再加一次 details。旧版／仅 details 工具可能没有报告原生用量，修改版或第三方工具的结构也可能不同。Metrics 不从 details 推算缺失的原生总量。

要获得真正的已观测计时，需要显式为子 agent 或调度器接入：

- **Pi SDK 子会话：** 将导出的 [`createAgentMetricsBridge`](agent-bridge.ts) 内联扩展加入子会话的 `DefaultResourceLoader.extensionFactories`。它观测子会话的 `context_with_system`、首个非空标准化 thinking／text／tool-argument delta、`message_end` 及权威 `turn_end` 用量。响应计时在子工具执行前冻结，每个子请求使用独立 ID。发布开始、首次观测、最终快照；已知请求缺少完成边界时发布中断快照。不启动 agent、不包装 provider、不改提示词、不创建定时器，也不调用模型。
- **其他 agent／子进程：** 使用生产者自身的单调请求／delta／完成边界实现下文数值契约，并显式转发至父会话事件总线或工具 details。总线**只在同一进程内有效**。不会安装 IPC 服务、stdout 解析器、文件监视器、私有配置发现机制或自动子进程注入。
- 计时属于观测者本地。通过 IPC 测量到达事件的调度器须标注 `external_request_start`，其传输／缓冲开销包含在内。不可将不同进程的时钟相减。两种接入都不测量服务端生成时间或未暴露的推理。

在现有可信调度器内部，为**每次子调用**在首个提示词前创建新的 bridge（调度器已提供 `pi`、`ctx`、`toolCallId`；非工具 agent 使用 `null`）：

```ts
import { randomUUID } from "node:crypto";
import { createAgentMetricsBridge } from "/stable/pi-metrics/agent-bridge.ts";
import { AGENT_EVENT } from "/stable/pi-metrics/agents.ts";

const anchorId = ctx.sessionManager.getLeafId();
if (!anchorId) throw new Error("A parent session entry is required for attribution");
const childMetrics = createAgentMetricsBridge({
  source: "my-dispatcher",
  agentId: randomUUID(),
  sessionId: ctx.sessionManager.getSessionId(),
  anchorId,
  toolCallId,
  emit: observation => pi.events.emit(AGENT_EVENT, observation),
});
// Add childMetrics to the CHILD resource loader's extensionFactories.
// The existing dispatcher still owns child creation, cancellation and disposal.
```

使用已审查的稳定源码路径，而非照抄占位路径。此片段本身既不启动进程，也不为已有进程自动接入。不要用 `pi -e` 加载 `agent-bridge.ts`：它是需要显式归属／传输参数的 factory，不是独立扩展。子会话单独加载 Metrics 时只记录自身会话，不会自动转发到父会话。取消时先等待子会话 abort，再 dispose。崩溃／直接 dispose 可能留下 running 快照；重载／导航后显示为 interrupted，不伪造最终用量。父会话被替换或导航时应停止转发，新工作须创建新的子调用 ID／anchor。没有实时子 agent 字符速率采样器：运行中可提供 TTFT，EFF TPS 要等用量定稿。

### 数值型外部 agent 契约 v1

在**父 runtime** 中执行 `pi.events.emit("orbit.pi-metrics.agent.v1", snapshot)`，或把相同快照组成的数组放在工具结果／更新的 **`details.piMetrics`**。Metrics 观测 `tool_execution_update`、`tool_execution_end` 和 `turn_end` 中已定稿的工具结果；details 快照的 `toolCallId` 必须等于该事件的工具 ID，包括嵌套 ID。总线不会推断工具归属：须传正确 ID 或 `null`。这是可信接入协议，不是带认证的遥测端点。

以下字段全部必需，不可用时明确用 `null`。**仅数值测量**指不含提示词、回答、推理文本、参数、结果、任意标签或原始事件；仅允许这些白名单标识符／枚举及数值。出现未知字段会拒绝整条快照，而非将其保存。

| 字段 | 契约 |
|---|---|
| `version` | 固定为 `1`。 |
| `source`、`agentId`、`requestId` | 生产者命名空间、唯一子调用、唯一请求／尝试。跨传输和重放使用同一三元组。不要用可复用角色名充当调用 ID，也不要通过此通道再次报告父会话自己的请求。 |
| `sessionId`、`anchorId`、`toolCallId` | 目标**父会话** ID、调度时捕获的现存父分支 entry；`toolCallId` 是已知父工具 ID 或 `null`。会话不匹配或 anchor 不在当前分支时拒绝。helper 固定这些 ID，不会重新指向新目标。 |
| `provider`、`model` | 实际响应模型的标识符或 `null`；helper 从权威 assistant 消息定稿。 |
| 标识符格式 | 1–256 个 Unicode 字母／数字或 `._:/@+-`，首字符必须是字母／数字。不含空格或控制序列。只有 `toolCallId`、`provider`、`model` 可以为 `null`。 |
| `sequence` | 非负安全整数，同一请求的新快照严格递增。允许仅发送完成快照；流式发送快照时从 `0` 开始。 |
| `state` | `running`、`complete` 或 `interrupted`。`complete` 包括具有最终用量的失败／中止响应，不仅是成功响应。终态快照不可修改。 |
| `timingBasis` | `context_with_system`（子 Pi hook）、`external_request_start`（生产者注明的开始边界）或 `unavailable`。 |
| `elapsedMs`、`ttftMs`、`responseMs` | 从**同一个本地单调请求开始时刻**计算的有限非负毫秒数，不超过 `Number.MAX_SAFE_INTEGER`；也可为 `null`。`ttftMs` 是首次非空已观测 delta，已知后不可改变。`responseMs` 在响应完成时冻结，排除工具。已知时间满足 `ttftMs ≤ responseMs ≤ elapsedMs`，允许 TTFT 缺失。`unavailable` 要求三项均为 `null`；其他口径要求 `elapsedMs`。有计时的 `complete` 要求 `responseMs`。 |
| `outputTokens`、`reasoningTokens` | 非负安全整数或 `null`。输出**已包含推理**，推理是子集，不得超过输出。不重复相加，也不推测隐藏 token。零输出不能建立可用吞吐量。 |
| `stopReason` | `stop`、`length`、`toolUse`、`error`、`aborted`、`deferred` 或 `null`。`complete` 必须非空；`running` 要求 `responseMs`、两个 token 数和 `stopReason` 全为 `null`。 |

例如，完成快照的测量字段为 `elapsedMs: 30000`、`ttftMs: 20000`、`responseMs: 25000`、`outputTokens: 200`、`reasoningTokens: 150`、`stopReason: "stop"`，则 EFF TPS 为 **200 / 5 = 40**，不是 200 / 30，也不是 350 / 5。不接收速率字段，由 Metrics 计算。错误、中止、deferred／不完整请求、未知／零流时长和未知／零输出均不产生 EFF TPS。逐 agent 汇总按被覆盖输出／被覆盖流秒数计算，并列出**已覆盖／已记录请求数**，不累加并行速率或取简单均值。覆盖率不能说明生产者从未报告的请求。

保存前先校验并复制标量字段。完全重复及较低序号的过期重放被忽略；冲突修订、归属／计时口径／首次观测改变、耗时倒退及终态后改变被拒绝。保存的 `(source, agentId, requestId)` 标识在整个父会话文件（含废弃分支）内，对总线／details／重载投递去重。显示和汇总只使用当前分支 entry。未完成历史恢复为 interrupted，恢复后的迟到回调不能继续推进它。即使 anchor 是共享祖先，之前出现过的子调用在恢复后也不能再附加新的请求 ID；新工作需要新的调用 ID。重放不会把定稿记录复制到缺少它的分支。生产者须协调身份：不同 ID 声称代表同一物理请求时，无法自动识别。

只有成功保存的快照推进去重状态，保存失败可重试。有 UI 时通过警告／状态报告保存失败，不回显载荷。总线发送后不等待结果，不提供投递确认或自动重试；传输可靠性由调度器负责。拒绝次数显示在页脚／详情中，会话恢复／导航时清零；被拒绝的载荷不保存。runtime 结束时取消总线订阅。Metrics 不会把外部记录加到父活动／请求覆盖或原生 `IN`／`OUT`／`CACHE`／`COST`；底层的子 agent 工作可能已通过工具原生用量计入。Details 聚合用量也绝不加入已接入请求的吞吐量分子。

## Translator 已报告用量

`/metrics` 和 `/metrics details` 提供独立的 **Translator reported usage** 区域。只读取 `customType` 为 **`orbit.translator.usage.v1`** 的 `custom` entry，范围是包含废弃分支的**整个会话文件**。显示已记录尝试数、已报告／未报告数量、**仅已报告部分的小计**，以及按 provider／model、方向和状态拆分的明细。失败或重试的尝试只要有数值用量仍计入；输出已包含推理子集。

这**不是合并的会话账单**。页脚原生 `IN`／`OUT`／`CACHE`／`COST` 保持不变。子会话的 Translator 收据可能只留在子会话中，永远不到达父会话，因此父会话收据小计或“原生用量加 Translator 用量”都不能代表完整花费。不显示合并总量。收据没有请求时钟，不影响 TTFT、EFF TPS、OBS TPS、提示词处理或性能覆盖率。

- **没有有效收据：** 显示 `Unavailable`；报告从升级到支持收据的 Translator 后开始，不回填早期用量。这不能证明未使用翻译或翻译免费。
- **`usage: null`：** 已记录的尝试，但用量**未报告**，不是零费用尝试。混合报告只显示已知小计，未报告尝试单独计数。Pi 标准化后的数值零费用按报告显示，不证明请求免费，也不是账单。
- **去重：** 每个 `requestId` 在整个会话中只对应一份不可变收据，不按分支、provider 或方向另行计数。完全重复的收据忽略。同一 ID 的有效收据发生冲突时，整个 ID 从金额／尝试计数中排除，不任意选择某个金额；显示格式错误、重复收据数量及冲突 ID 数量。格式错误收据不占用 ID。旧 Translator outcome 聚合记录即使包含 usage，也**绝不累加**。
- **只读：** 每次命令读取当前会话 entry；不复制收据、不迁移会话、不扫描其他会话历史或个人配置、不调用模型或发送网络请求。恢复／导航不改写记录。不安装收据传输或子会话自动转发机制。

### 收据契约 v1

这些自定义 entry 由兼容的 Translator 生产者负责；Metrics 仅读取。`data` 必须且只能包含以下字段（不含提示词、回答、错误文本或 provider 原始载荷）：

| 字段 | 契约 |
|---|---|
| `version` | 固定为 `1`。 |
| `requestId` | **一次实际尝试**的唯一字符串，重放／转发时保持不变；不同重试使用不同 ID。 |
| `provider`、`model` | Provider 和模型标识字符串。 |
| `usage` | 完整 Pi `Usage`，未报告时明确为 `null`。`input`、`output`、`cacheRead`、`cacheWrite`、`totalTokens` 为必需的非负安全整数；可选 `reasoning` 和 `cacheWrite1h` 分别是 `output` 和 `cacheWrite` 的子集。`cost` 必须包含有限非负数 `input`、`output`、`cacheRead`、`cacheWrite`、`total`，每项不超过 `Number.MAX_SAFE_INTEGER`。不转换字符串，不把缺失字段默认填零。 |
| `status` | 非空状态字符串；无论结果如何，数值用量均计入。 |
| `direction` | `input`、`output` 或 `subagent`。 |
| `nativeAccounting` | 固定为 `false`；这些收据不是原生 usage entry。 |

字符串字段不得为空白，最多 256 个 UTF-16 code unit，不得包含控制／格式字符或行／段落分隔符。未知字段、无效版本／类型／数值及矛盾的子集会导致拒绝该收据，不回显载荷。小计超出安全数值范围时显示不可用，不显示不安全的数值。标准化后的 provider 数据也无法补出 provider 从未报告的用量。若不同 ID 声称代表同一次物理尝试，无法自动识别。

## 会话记录与隐私

所有数据以 Pi **custom entry** 保存，不进入模型上下文。默认位置为 `~/.pi/agent/sessions/` 或你配置的会话目录；`/session` 可定位当前文件。不创建额外数据库、指标文件、网络端点或遥测上传。使用 `--no-session` 或内存 SDK 会话时，数据仅留在内存中。

父会话 schema 2 保持原有三种 entry，独立的 agent 契约 1 entry 不改写它们：

| `customType` | 保存内容 |
|---|---|
| `orbit.pi-metrics.v2` | 权威定稿的请求，以 `messageEntryId`、`requestId`、`runId` 和请求序号关联。 |
| `orbit.pi-metrics.run.v2` | 活动开始／进度／最终汇总，`state` 区分 `running`、`settled`、`interrupted`。 |
| `orbit.pi-metrics.trace.v2` | `request_start`、`first_observation`、`samples`、临时 `response_checkpoint`、`request_interrupted` 及 `tool_start`／`tool_end` 事件。 |
| `orbit.pi-metrics.agent.v1` | 经校验的其他 agent 请求标量快照，含 source／agent／request 和父分支／工具归属。每个标识使用最近接收的快照，不可把连续快照相加。 |
| `orbit.pi-metrics.delegation.v1` | 最终 `subagent` details 用量，关联持久 `toolResultEntryId`，保留并行／chain 的独立结果位置；仅供明细查看，不伪造计时，不额外计入原生总量。 |
| `orbit.translator.usage.v1` | 由兼容的 Translator 写入，Metrics **只读**：逐次尝试的数值用量或 `null`、请求 ID、provider／model、方向／状态及 `nativeAccounting: false`。不保存重复副本。 |

新的请求／活动记录用 `throughputBasis: "reported_output_tokens/observed_stream_elapsed_seconds"` 标识 EFF TPS 公式。活动的 `coveredStreamMs` 是作为分母的流时长之和。`measuredRequestMs` 和 `coveredRequestMs` 仍保留完整请求时长用于分析，不用于吞吐量计算。采用完整请求时间口径的旧 schema 2 记录仍可读取。

记录保留所有可获得的数值测量：

- 活动／请求的可观测 TTFT、首文本／思考／工具参数、完整响应时间、已观测流区间、ISO 时间戳、计时／吞吐量定义及速率不可用原因。
- 请求／实际响应模型、provider／API、停止原因、provider 时长、开始时的 thinking level、白名单中的最终 Pi 用量／费用、标准化推理细分及明确未知的隐藏思考字段。
- 分类已观测字符数、delta 数、估算值及采样的时间戳／速率／累计字符数，包括静默等待期间的采样。
- 请求起止上下文快照、整个会话用量／费用、请求覆盖率及加权活动吞吐量。
- 每个工具的标识、名称、父 ID、请求 ID、起止偏移、时长及错误／完成状态。**并行／嵌套工具的墙钟时长按区间并集计算**，而非直接相加。Provider 重复使用的工具 ID 按请求区分。

**不降采样、不设历史上限：** 保留每一个实际产生的采样点。通常按 **128** 个一批（约 32 秒）追加，响应结束或有序中断时再刷新。`chunkIndex`、`firstSampleIndex`、`sampleCount`、`sampleChunks` 和 `samplesComplete` 使重建可以核验。按 `requestId` 查找匹配的 `samples` trace entry，以 `chunkIndex` 排序后连接，再追加定稿请求中的内联 `samples` 后备数据；不要重复连接临时 checkpoint 中的副本。实际采样时间戳会暴露定时器延迟／错过的触发机会；这不是逐 token 或每个 delta 的轨迹。

首次观测 checkpoint 在响应完成前保存精确的已观测延迟。响应 checkpoint 在工具完成前保留冻结的计时，但属于**临时结果**：`turn_end` 变换后的最终请求 entry 替代其用量。活动进度采用紧凑快照；`listsComplete: false` 表示省略完整 ID／工具列表，并非实际没有这些项。计数仍然可用，各工具的 trace entry 单独保存，活动结束／中断汇总则包含完整列表，避免反复复制不断增长的列表。

完整保留会使会话文件及 Pi 的内存历史逐渐增长。崩溃可能丢失当前未刷新的采样批次、最终用量或最终活动汇总；已有 checkpoint 仍可用，但不能补造缺失数据。中断或缺失边界会标为部分记录。存储失败会停止周期采样，避免未保存的缓冲无限增长，并标记捕获不完整。SDK runtime 被释放后无法再保存最终汇总；SDK 调用者应先 `await session.abort()`，再 `session.dispose()`。Pi 1.1 自身也可能在直接中途释放时报告缺少 assistant 条目的边界错误。

指标中不重复保存提示词、回复／思考文本、工具参数／结果、凭据或 provider 原始载荷。Pi 原生转录仍保留正常的对话数据。模型 ID、工具名、时间戳、token 和费用可能敏感，分享前应检查会话。“完整”指全部已捕获测量和 provider 提供的标准化用量，**不包括无法获得的模型内部推理**。

### 历史、分支与 schema 1

最近性能及汇总跟随当前分支；token／费用按 Pi 的整个会话口径累计，包括废弃分支、压缩、分支摘要、缓存保温及已报告的嵌套工具用量。这些辅助工作不获得父会话对话请求计时，也不混入父会话 EFF TPS。显式接入的 agent 使用独立的分支内记录和覆盖率；仅用量的 subagent details 是可能与原生用量重叠的信息明细，不重复相加。Translator 收据小计采用整个会话范围，与原生总量分开，只覆盖该会话内实际存在的收据。

现有 schema 1 和 schema 2 entry 原样保留。读取时使用保存的 **`streamMs`** 重算请求速率，不使用旧的缓存 `outputTps` 或完整 `responseMs`。旧 schema 2 活动速率从关联请求的流时长重建；若请求缺失或不符合条件，活动速率保持不可用，不沿用完整请求时间口径的旧速率。无法重建 schema 1 活动分组／TTFT，或被旧版降采样丢弃的点。不为安装前的请求回填时间。恢复／导航不会重复创建请求记录；选到 assistant 条目但不包含其后的指标 entry 时，不包含该定稿测量。

## 开发与测试

在仓库根目录运行：

```sh
npm --prefix Pi/pi-metrics test
npm --prefix Pi/pi-metrics run typecheck
npm --prefix Pi/pi-metrics run test:cli
npm --prefix Pi/pi-metrics run test:package
(cd Pi/pi-metrics && npm pack --dry-run --ignore-scripts)
```

请在仓库检出目录中运行：npm tarball 有意不包含测试和开发脚本。`test:package` 验证 dry-run 及实际 tarball 的精确文件清单、公开发布元数据、仅由宿主提供的 peer 依赖及复制的 MIT 许可证，然后在检出目录之外解包，使用真实 SDK 和干净的临时配置加载包清单。通过合成 Translator 收据验证 `/metrics` 和 `/metrics details`，不执行安装，不调用 provider 或网络。打包使用离线 npm 和空的临时 npm 配置，不读取注册表凭据或真实 provider 设置。解包需要 `tar`（受支持的 macOS／Linux 测试宿主已提供）。

测试使用合成时钟、隔离的本地 HTTP provider、真实 Pi SDK 及已安装包声明的 `bin.pi` 可执行文件。完整测试包含**仅在临时配置目录中**的安装／移除。如果禁止执行安装命令，可运行不安装的子集；Node 参数必须位于文件列表**之前**（通过 `npm test --` 追加参数不会过滤此脚本）：

```sh
cd Pi/pi-metrics
node --test --test-skip-pattern='^real (CLI|fullscreen terminal|regular terminal)' test/*.test.mjs
```

这会排除四项原有的依赖安装操作的 CLI 测试，不代表完整套件通过。两项 `one-run` fullscreen／regular 测试仍通过 `-e` 验证加载、折叠 TTFT／TPS、鼠标／命令开关、缩放及正常退出，不执行安装。两种测试路径都不会安装依赖。

终端检查使用独立的 tmux server，不加载用户配置，并使用临时 HOME／agent／workspace／session 目录、环境变量白名单及离线模式。不使用付费模型、个人提示词或个人配置。配置隔离不是操作系统沙箱。

新增 agent 回归覆盖严格数值校验、并行／串行归属、未知／失败计时、推理子集、重复／冲突／过期快照、保存失败重试、按分支恢复／中断、没有计时的 details 用量、不重复计算原生用量及载荷隐私。真实 SDK 测试使用隔离的合成父会话／并行子会话和 localhost provider，包含子工具延迟、最终用量变换、数值总线投递及磁盘恢复。不运行随包附带的子进程 subagent 工具，也不对真实 provider 进行 benchmark。折叠页脚回归验证 TTFT／TPS 优先级、保留速率标记及按宽度显示上下文／模型／状态。Translator 回归使用合成收据，覆盖严格校验、三个方向、失败／重试、null 与零值、格式错误／重复／冲突数据、不安全求和、忽略旧聚合记录、全会话跨分支去重、只读命令、RPC／无页脚显示，以及原生用量／性能不变。真实 SDK fixture 在完全不调用 provider 的情况下检查磁盘恢复和命令显示，并将原生子 agent 总量与 Pi 自身会话统计比较。CLI fixture 用更高的视口检查加长的计量报告，再恢复原有 32 行页脚／缩放检查。

既有覆盖还包括跨工具／内容／新活动保留 EFF TPS、新估计替换、按分支恢复且不改变覆盖判定、静默窗口内 OBS TPS 隐藏及恢复、不受 TTFT 影响的 EFF TPS、缺失／零长度流、历史完整请求速率转换、隐藏推理计算、UI 隐藏思考、跨工具／续接保持活动 TTFT、并行／嵌套工具、无损分块、临时／最终用量、旧版历史、错误、中止、SDK 释放和会话恢复。页脚回归还覆盖三行／单行高度上限、Unicode 与长路径、已知空闲过滤且不改变其他扩展状态、警告优先级、高上下文提示、折叠的所有权／释放及详情隔离。真实 CLI 检查覆盖稳定副本安装／移除、JSON、fullscreen／dark 与 regular／light 终端、鼠标折叠／展开、regular 模式键盘切换、原生详情取消／提交且不发送编辑内容、缩放、页脚共存／开关、禁用参数、嵌套源码重载及正常退出。

Pi 不可用时，宿主测试明确跳过。终端测试还需要 **macOS／Linux 上的 tmux**。应报告跳过，不能宣称完整通过。SDK 查找支持托管安装、npm 嵌套依赖及 pnpm 风格的 SDK／依赖符号链接。这些布局使用合成文件系统 fixture 验证，不代表分别测试了不同安装发行形式。

类型检查需要宿主 SDK 和现有 TypeScript 编译器；缺少时会失败，不会静默跳过。可选路径覆盖：

```sh
PI_SDK_PATH="/absolute/path/to/@earendil-works/pi-coding-agent/dist/index.js" \
PI_TSC_PATH="/absolute/path/to/typescript/lib/tsc.js" \
npm --prefix Pi/pi-metrics run typecheck
```

`PI_SDK_PATH` 指向文件而非目录；`PI_TSC_PATH` 指向编译器 JavaScript 入口，而非 shell wrapper。运行时不需要这两个设置。类型检查不生成构建产物。

**0.0.7 发布候选版验证（2026-10-28）：** 在 macOS、**未经修改的官方 Pi 1.4.0** SDK、Node.js 26.12.0 和 tmux 3.8 下，最终不加过滤的 `npm test` **通过 95 项测试，零失败／跳过**，包括全部四项安装／移除 CLI 测试，以及两项无需安装的单次加载终端测试。使用现有 TypeScript **5.9.3** 编译器，通过严格 SDK 类型检查。较早的委派检查通过 91 项选定测试，把四项安装测试留给维护者；父任务的完整运行取代该部分结果。打包检查 **2/2 通过**，包含 `npm pack --dry-run`、无打包依赖的实际 **12 文件** tarball，以及无 provider／网络调用的隔离清单加载和两个命令。npm／GitHub 发布和活跃安装启用是独立操作。本套件的 Translator 收据是合成 fixture，不是真实 provider 或生产者到父会话转发测试。

**本机发布验证：** 0.0.6 通过完整测试集 **84 项测试，无跳过**，包括在临时配置中执行的四项依赖安装操作的 CLI 测试、真实 SDK 子会话接入／工具 details 计量，以及隔离 tmux 的 fullscreen／regular 检查；环境为 macOS、Pi 1.4.0 SDK、Node.js 26.12.0、tmux 3.8。使用已存在的 TypeScript 5.9.3 编译器及文档中的 `PI_TSC_PATH` 覆盖参数，通过严格 SDK 类型检查。较早一次完整运行在本机同时运行其他测试集时，fullscreen 终端测试等待折叠页脚超时；随后的完整重跑及另外两次 `test:cli` 运行均通过。0.0.5 在 macOS、Pi 1.3.0、Node.js 26.12.0 下通过 **69 项测试，无跳过**，严格 TypeScript 检查通过，包含隔离 tmux 的鼠标／键盘／详情检查。0.0.4 在 Pi 1.2.0、Node.js 26.8.2、tmux 3.7c 下通过 62 项测试，无跳过，严格 TypeScript 检查通过。0.0.3 及以前的版本在 Pi 1.1.0 上验证。

**验证边界：** 本机检查不是 CI 或真实 provider benchmark。桌面字体／配色、任意第三方页脚组合及其他宿主版本／平台仍需单独验收。Fixture 验证的是计算和生命周期行为，不是 provider 报告的准确性或服务端解码速度。

## 卸载

对于固定 npm 版本示例：

```sh
pi remove npm:@orbit-labs-ai/pi-metrics@0.0.7
```

对于本地副本示例：

```sh
pi remove "$HOME/.local/share/orbit-tools/pi-metrics"
```

若来源／路径不同，使用实际注册项，再在空闲时重载或重启 Pi。单次加载则退出后不再传入 `-e`。npm 移除会取消注册并删除托管包；本地移除只取消路径注册，不删除工具副本目录。确认没有会话使用后才能删除该目录。**会话历史及指标保留**：卸载不改写会话、不清除用量／费用。删除会话是需要单独明确执行的操作。

## 许可证

[MIT](LICENSE) © 2026 Orbit Labs contributors。npm tarball 和稳定副本安装示例均在工具目录内包含仓库的 MIT 许可证。
