@orbit-labs-ai/pi-metrics
Observed latency, TTFT-excluded decode-speed estimates, live stream rates, and full numerical session measurements for Pi.
Package details
Install @orbit-labs-ai/pi-metrics from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:@orbit-labs-ai/pi-metrics- Package
@orbit-labs-ai/pi-metrics- Version
0.0.7- Published
- Oct 27, 2026
- Downloads
- 230/mo · 230/wk
- Author
- fhxgs
- License
- MIT
- Types
- extension
- Size
- 208.4 KB
- Dependencies
- 0 dependencies · 3 peers
Pi manifest JSON
{
"extensions": [
"./index.ts"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
Pi Metrics
English | 简体中文
Observed activity latency, a reasoning-inclusive decode-speed estimate excluding TTFT, and a quick, real-time approximate observed-stream rate for Pi. An opt-in bridge adds separately attributed request TTFT/TPS for child SDK sessions and other agents; compatible subagent tool details provide usage-only reports when timing is unavailable. Numerical measurements stay in Pi's session storage. /metrics and details also show Translator reported usage separately when compatible per-attempt receipts are present; native totals remain native. The extension does not change prompts, wrap providers, make extra model calls, or attempt to recover private model reasoning.
Version: 0.0.7; parent measurement schema 2, agent event contract 1. Prepared for the first public npm distribution as @orbit-labs-ai/pi-metrics, alongside the GitHub release. Publication is not yet confirmed; the npm commands below apply only after the maintainer verifies that 0.0.7 is available. See the changelog.
Requirements
- Pi 1.1.0+, Node.js 22.19+.
- Pi supplies
@earendil-works/pi-coding-agent,@earendil-works/pi-ai, and@earendil-works/pi-tuias host peers; they are not bundled. No separatenpm installor build is needed. The release candidate is checked against the unmodified official Pi 1.4.0 SDK. - The footer requires the interactive terminal UI. Recording also works in print, JSON, RPC and SDK sessions.
/metricsnotifications require a UI-capable host, such as the TUI or a supporting RPC client.
Quick start
After npm publication is confirmed, load the pinned package for one invocation without registering an installation:
pi -e npm:@orbit-labs-ai/pi-metrics@0.0.7
To record without replacing the footer:
pi -e npm:@orbit-labs-ai/pi-metrics@0.0.7 --metrics-no-footer
Before publication, or for source development, load from the repository root instead:
pi -e ./Pi/pi-metrics/index.ts
The source command also accepts --metrics-no-footer. Do not load a second copy alongside an existing installation. Loading does not send a model request; resolving an npm source downloads package code from the registry. Ordinary prompts and Pi's own background work still follow your provider configuration and billing.
Persistent npm installation
After the maintainer confirms npm availability:
pi install npm:@orbit-labs-ai/pi-metrics@0.0.7
Pi installs the package in its managed npm directory and registers it in your personal Pi settings. Start a new session or use /reload when idle. The exact version is pinned: pi update --extensions reconciles it but does not advance it to a newer version. Updating requires an explicit new version specification; do not replace files used by active sessions or load two copies.
Upgrading from 0.0.6
Existing local copies do not update automatically. Inspect pi list, wait until idle, remove the old Metrics registration using its exact source/path, then install the pinned npm version (after publication is confirmed) or register a fresh stable copy. Keep the old local directory until no session uses it. Do not remove unrelated packages or rewrite session history. Parent schema 2 and agent contract 1 remain unchanged. Translator usage reporting needs a separately upgraded, receipt-capable Translator; missing earlier receipts cannot be backfilled, and child receipts are not automatically forwarded.
Package contents
The public tarball has an explicit file allowlist: six runtime TypeScript modules (index.ts, metrics.ts, footer.ts, agents.ts, agent-bridge.ts, translator.ts), package.json, both READMEs, both changelogs and a local MIT LICENSE copied from the repository. It excludes tests, development scripts, logs, personal runtime data, generated host declarations and bundled dependencies. translator.ts only reads compatible usage receipts; it is not a bundled copy of the Translator tool. Pi remains the runtime host, not a package dependency installed alongside Metrics.
Persistent local installation
Do not install from a checkout that switches tool branches. Pi loads a local package in place. Copy it to a fresh, stable directory outside Git:
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"
Run this from the repository root. It deliberately fails if the destination exists. The last command registers a local directory in your personal Pi settings; it is not an npm installation. Start a new session or use /reload when idle. Do not also load another copy with -e.
Updates: inspect the existing installation, copy the new source into another fresh directory, and replace the old package registration with the new path. Do not overwrite a copy used by running sessions or register both versions simultaneously. Retain the old copy until no session uses it. Updates are manual, not automatic pulls from this branch. If another path was chosen, use that exact path for removal too.
Commands and defaults
| Command or flag | Behavior |
|---|---|
/metrics |
Active-branch activity/request latency means, effective throughput, request coverage, native whole-session usage, separate Translator reported usage, and other-agent observations/breakdowns. |
/metrics details |
All current footer values and extension statuses, including hidden idle states, in Pi's native editor for inspection/copy. Edits are discarded even on submit; nothing is sent. RPC uses a notification instead. |
/metrics footer on |
Enable/reclaim the Metrics footer in the TUI. |
/metrics footer off |
Disable the footer; recording continues. |
/metrics footer collapse / expand |
Select a one-row or three-row footer without disabling recording. |
/metrics footer toggle / Alt+M |
Switch between collapsed and expanded while Metrics owns the footer. |
--metrics-no-footer |
Start without a custom footer. Default false. Recording continues. |
The right-aligned [▾ Collapse] / [▸ Expand] button is clickable in mouse-capable fullscreen Pi (tested on 1.3.0). Narrow windows use [▾] / [▸]. Regular terminals keep their native mouse selection; use Alt+M or the commands there. If your terminal does not send Alt as Meta, use the command. Clicking never takes keyboard focus from the prompt editor. Collapse/expand does not reclaim a footer replaced by another extension; explicitly use footer on for that.
Toggles are in-memory only. New sessions and reloads start expanded, with footer ownership initialized from the CLI flag; off/on preserves the chosen layout within that runtime. There is no reset/delete-records command or separate preferences file.
Footer compatibility
Pi has one custom footer slot. Enabling Metrics can replace another extension's footer. Turning Metrics off restores Pi's built-in footer only if Metrics still owns the slot, not another extension's earlier layout. If another extension has displaced Metrics, disabling Metrics leaves that replacement alone.
Text from other extensions' ctx.ui.setStatus() is sanitized, not removed from those extensions. Only exact known idle pairs—lsp → LSP: idle and pi-background-task → 0 bg tasks—are hidden by default. Active and unfamiliar statuses remain eligible for display, with warning/error text prioritized. Idle states remain in /metrics details. If Metrics does not own a footer, that view explicitly reports Git branch/status information as unavailable.
Custom footer layouts cannot be composed. Use --metrics-no-footer when another extension should own the slot. Do not reset unrelated extensions to solve a footer conflict.
Reading the footer
The expanded footer is at most three rows, and collapsed is one row. Existing symbols are retained. The current directory is a bold, accent-colored anchor; metric values are bold, labels and model/branch metadata quieter, and groups have consistent spacing. Active extension statuses share the last row rather than adding rows. Illustrative expanded display at a wide terminal:
~/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%
Collapsed, with no active extension status:
TTFT 840ms TPS 38.4 · last ◫ CTX 24% provider/model · high … +1 [▸ Expand]
On narrower terminals, less-important groups are omitted with … +N (the number of omitted groups), long values are ellipsized, and optional header metadata is dropped instead of wrapping. The end of a long directory remains visible. /metrics details retains all values and full status text; use it whenever a group does not fit. Collapsed mode puts TTFT and TPS first, using the same parent activity values and last/pending/so far qualifiers as the expanded view. Its short TPS label means EFF TPS in tokens/second, not live OBS TPS. High context (70% warning / 90% error), warning/active statuses, parent activity state and attributed agent performance take precedence over ordinary context, model metadata and the directory. Those optional groups appear as width allows; long paths/models can no longer displace performance. Extremely narrow terminals necessarily clip text. All layout uses Unicode terminal-column widths.
No Nerd Font is required. Font family and size belong to the terminal, not Pi: a monospaced font such as JetBrains Mono or SF Mono with a real bold face is a suitable choice. This extension changes only theme colors, weight and spacing; it never changes terminal configuration or installs fonts.
| Field | Meaning |
|---|---|
| Header | Prominent directory, Git branch, session name, selected provider/model and thinking level as space allows, plus the collapse button. The full details view always identifies the latest concrete responder when it differs. |
TTFT … obs |
Time to first observed content in the activity. Fixed across tool rounds and automatic continuation loops, not restarted with each API request. |
EFF TPS |
Reported output / time from the first observed token to response completion, aggregated across the activity's requests. Estimates decode speed; includes reported reasoning tokens but excludes every request's TTFT, tool execution and gaps between requests. |
last / pending / so far |
EFF TPS retains the previous estimate until a new valid estimate arrives. last marks that retained value when current activity coverage is unavailable; pending is used only before any estimate exists. An eligible completed-request aggregate within a running activity is marked so far, not final. |
OBS TPS ≈ |
Shown only with a positive live rolling rate: approximate arrival rate of exposed text, thinking and tool-argument characters. Hidden while unavailable, at zero, or after the response ends. Not a rate for hidden reasoning. |
AGENT … / Agents: … active requests |
Separate, opt-in child/other-agent request observations. Shows the most recently started recorded agent request's TTFT/EFF TPS/state as space allows. Active counts mean observed requests, not all processes or tasks. /metrics details retains each source/agent/request, model, coverage and usage-only tool reports. Never substitutes an agent's rate for the parent's. |
IN / OUT |
Pi-native input/output totals for the whole session file, including abandoned branches and child usage reported through top-level tool usage. Output already includes normalized reasoning usage. Translator custom receipts are not added. |
CTX |
Pi's current estimate of context tokens/window and percentage; ? when unknown, including after compaction. |
CACHE |
Whole-session read/write tokens. HIT is the latest finalized request's cacheRead / (input + cacheRead + cacheWrite). |
COST |
Whole-session dollars from Pi's stored usage and configured prices, not an invoice or subscription balance. Missing cost does not prove a request was free. |
Native totals are dynamic stored accounting, not live stream estimates. They update as Pi records finalized assistant/tool usage and auxiliary usage. Current Workbench returns top-level tool usage for children, so that work is already included in native IN/OUT/CACHE/COST. Agent/detail records only provide observations or breakdowns; Metrics never adds them again. A tool that supplies details but omits native usage may leave native totals incomplete. CTX and cache HIT follow the current branch, not the whole-session accounting scope. /metrics details labels these scopes separately from current-activity performance.
Unavailable latency/rates use —, not invented zeroes, except that EFF TPS keeps a previous estimate labeled last when available and inactive OBS TPS is hidden; native usage/cost retain Pi's normalized values, including its zero defaults. Failed, aborted and deferred responses are labeled. Resumed partial activities are marked incomplete and do not show a final effective rate. Save failures produce a warning/status and are reported through Pi's extension error handler at handler boundaries, without writing protocol-breaking text to JSON/RPC stdout.
Measurement definitions
Observed TTFT and activity boundaries
An activity begins at the first agent_start and ends at agent_settled. This includes tool rounds, automatic retries, compaction-related continuation and queued work inside that activity. A queued steering/follow-up message can therefore belong to the same activity; this is not necessarily one typed prompt or one low-level Pi turn.
The first nonempty normalized thinking_delta, text_delta or toolcall_delta fixes activity TTFT. Empty deltas, start markers and transport metadata do not count. First thinking, first text and first tool-argument times are recorded separately. The clock does not reset after tools or a later agent_start continuation within the same unsettled activity.
Each conversational request also retains its own timing, from this extension's context_with_system handler to its first observed delta and message_end. The response clock freezes before tools execute; final usage is taken at turn_end, after message transformations and tool results have been persisted. Activity timestamps and request timestamps use the same monotonic clock; persisted durations retain full numeric precision. Wall-clock ISO start/end timestamps are separate and must not be used instead of monotonic durations.
These are client-observed boundaries, not pressing-Enter latency, first HTTP byte time, or server first-generated-token time. Work before the corresponding hook is excluded; authentication, later preparation, transport, buffering, provider retries and observer overhead inside its interval are included. Attaching after activity/request start leaves missing timing unknown.
Hidden thinking: what can and cannot be measured
- Thinking hidden only by Pi's UI still generates normalized events and is observed normally.
- A provider may expose reasoning summaries, not its full internal reasoning. Those summaries are just the observed stream.
- Reasoning never streamed cannot be counted live or assigned an exact first-token/generation timestamp by this extension.
- Pi's normalized
usage.outputincludesusage.reasoning. Reasoning is a subset: never add it again. The final output total is stored even if no deltas were exposed, but EFF TPS requires an observed first token. - Preserve Pi's normalized reasoning count when available, but do not interpret absent or zero breakdown as proof of no thinking: some Pi adapters default an omitted breakdown to zero.
reasoningBreakdownexplicitly distinguishespositivefromzero-or-unreported. - The number of specifically hidden reasoning tokens and their duration cannot be separated from exposed summaries, prefill, queues or other delays.
hiddenReasoningTokensandhiddenReasoningMsare explicitlynull, not guessed.
Accounting depends on what the provider reports and Pi/other extensions normalize. No client-only metric can recover unreported token usage, undisclosed reasoning text, or exact server decode timing.
Effective TPS
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)
The denominator begins at the first nonempty observed thinking, text or tool-argument delta and ends at message_end, before tools execute. The entire initial wait (TTFT) is excluded for each request, not just once per activity. Tool time, between-request gaps, and separate auxiliary model work are also excluded. Full responseMs is still recorded for latency analysis but is never the EFF TPS divisor.
For example, a 20-second initial wait followed by 200 reported output tokens over a 5-second stream gives 200 / 5 = 40 tok/s, not 200 / 25 = 8. Increasing only the initial wait does not change EFF TPS.
This is a client-observed estimate of decode speed, not exact server decode speed or per-token latency. Reported output still includes reasoning; if a provider generated hidden reasoning before the first observed token, those tokens remain in the numerator even though that initial time is excluded. For example, 1,200 reported tokens (including 1,000 hidden reasoning tokens) over that 5-second observed span give 240 tok/s. Hidden reasoning, buffering and completion overhead can therefore skew the estimate; no unavailable hidden duration is inferred or added back.
A request is eligible only when it ends with stop, length or toolUse, has positive valid output usage, and has a known positive first-token-to-completion duration. Inconsistent reasoning/output counts, missing usage, errors, aborts and deferred responses are ineligible. A single buffered chunk can have EFF TPS if completion is later than its arrival, without a meaningful rolling OBS rate. A usage-only response with no observed delta, or a zero-length stream, cannot supply a new EFF TPS; there is no fallback to full request time. Zero output is treated conservatively as unavailable because Pi also uses zero for missing usage.
Recorded activity EFF TPS is unavailable if any started request is pending or ineligible; failures are not silently excluded from its coverage. The footer retains the last valid estimate across tool calls, incoming content, continuation requests and new activities until a new valid estimate replaces it. When current coverage is unavailable, last identifies this display-only fallback, not a final rate for the current activity. Without any previous estimate, EFF TPS remains —.
Reload and branch navigation rebuild the retained estimate only from finalized request records on the active branch, including eligible completed prefixes of a later-incomplete activity. Provisional response checkpoints are not promoted to finalized history; a new session starts without a retained estimate. Provisional so far estimates can still be corrected or withdrawn by final message transformations; only a valid authoritative estimate replaces the retained fallback. Retention does not change stored measurements, eligibility or the formula.
/metrics separately reports aggregate output / aggregate observed stream duration for covered requests (a stream-duration-weighted rate), with an explicit covered/recorded count, never an unweighted mean of individual rates. Historical or auxiliary native usage is not mixed into its numerator. Provider durationMs is stored separately, not substituted into the formula.
Approximate observed-stream TPS
Observed text, thinking and tool-argument Unicode code points / 4, including whitespace, divided by the observed elapsed time up to a rolling 2-second window. The window is clipped at request start, not the first arriving chunk, to avoid inflating startup rates. Counts use 100 ms buckets; boundary buckets make this approximate. At least two nonempty deltas and 250 ms after the first are needed. The rate can decay to zero when no exposed output arrives, even while hidden reasoning continues. The footer hides OBS TPS until a positive estimate is available, hides it again at zero or response completion, and shows it again when exposed output resumes with a positive rolling rate. Sampling continues during silence and zero samples remain in session records.
Sampling/redraw uses 250 ms intervals, subject to event-loop delays. This is not tokenization; language, code, emoji, JSON, summaries and batching can substantially change the estimate. The ≈ and separate OBS label are intentional. No hidden-token rate is extrapolated from it.
Subagents and other agents
Available integration and limits
Pi's parent message_update events describe the parent, not every child process. The compatible subagent single/parallel/chain details shape, also used by Pi 1.3.0's shipped examples/extensions/subagent/index.ts, exposes details.results[].usage, not child request-start or nonempty delta timestamps. This aggregate usage is not enough to calculate TTFT or decode TPS. The tool's wall time includes startup, queues, tools and multiple requests and is never used as child stream time.
Metrics automatically copies allowlisted final usage from that subagent details shape at turn_end: agent/model, result slot, mode, exit code, turns, input/output/cache tokens and cost. It does not read task/message/stderr text, guess timing, or backfill old tool results. Only top-level finalized tool results have the durable entry IDs needed for this usage-only adapter; raw nested details without that boundary are not imported. These are breakdown-only reports, not amounts excluded from native totals. Current Workbench returns child usage through top-level result.usage; Pi already counts it, and Metrics never re-adds the details. Older/details-only tools may not report that native usage, and modified/third-party tools may use a different shape. Metrics does not infer missing native totals from details.
For actual observed timing, explicitly instrument the child or dispatcher:
- Pi SDK child: include the exported
createAgentMetricsBridgeinline extension in the child'sDefaultResourceLoader.extensionFactories. It observes childcontext_with_system, first nonempty normalized thinking/text/tool-argument delta,message_end, and authoritativeturn_endusage. Response time freezes before child tools; every child request gets its own ID. It publishes start, first observation and final snapshots, and an interrupted snapshot if a known request loses its boundary. It does not start agents, wrap providers, modify prompts, create timers, or make model calls. - Other agents/subprocesses: implement the same numeric contract below using the producer's own monotonic request/delta/completion boundaries. Forward it explicitly to the parent bus or tool details. The bus is in-process only. No IPC server, stdout parser, file watcher, discovery of private configurations, or automatic subprocess injection is installed.
- Timing is observer-local. A dispatcher measuring arrivals over IPC must label
external_request_start; its transport/buffering overhead is included. Never subtract clocks from different processes. Neither integration measures server generation time or unexposed reasoning.
Inside an existing trusted dispatcher, create a fresh bridge for each child invocation, before its first prompt (the dispatcher already supplies pi, ctx, and toolCallId, or null for a non-tool agent):
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.
Use your reviewed stable source path, not the literal placeholder. This snippet alone neither starts nor instruments an existing process. Do not load agent-bridge.ts with pi -e; it is a factory requiring explicit attribution/transport, not a standalone extension. A child running Metrics independently records its own session; that does not automatically forward records to the parent. Await child abort before disposal when cancelling. A crash/direct disposal can leave a running snapshot; reload/navigation shows it as interrupted, without manufacturing final usage. Stop forwarding when the parent is replaced or navigates, and create fresh child IDs/anchors for new work. There is no live child character-rate sampler: TTFT can arrive while running, EFF TPS only after finalized usage.
Numeric external-agent contract v1
Emit pi.events.emit("orbit.pi-metrics.agent.v1", snapshot) in the parent runtime, or put an array of identical snapshots at a tool result/update's details.piMetrics. Metrics observes tool_execution_update, tool_execution_end, and finalized turn_end tool results; details snapshots must have toolCallId equal to that event's tool ID, including nested IDs. Bus delivery has no inferred tool association: pass the correct ID or null. This is a trusted integration protocol, not an authenticated telemetry endpoint.
All fields below are required; use explicit null for unavailable values. Numeric-only measurements means no prompts, answers, reasoning text, arguments, results, arbitrary tags or raw events; only these allowlisted identifiers/enums and numeric measurements are accepted. Unknown fields reject the entire snapshot, rather than persisting them.
| Fields | Contract |
|---|---|
version |
Exactly 1. |
source, agentId, requestId |
Producer namespace, unique child invocation, unique request/attempt. Keep the same triple across transports and replays. Never reuse a role name as an invocation ID, or report the parent's own requests through this channel. |
sessionId, anchorId, toolCallId |
Target parent session ID and existing parent branch entry captured at dispatch; toolCallId is a known parent tool ID or null. Reject mismatched sessions and anchors absent from the current branch. The helper snapshots these IDs, never retargets them. |
provider, model |
Concrete responder identifiers, or null; the helper finalizes these from the authoritative assistant message. |
| Identifier syntax | 1–256 Unicode letters/numbers or ._:/@+-, starting with a letter/number. No spaces or control sequences. null is allowed only for toolCallId, provider, model. |
sequence |
Nonnegative safe integer, strictly increasing for new snapshots of a request. Complete-only delivery is supported; start at 0 when streaming snapshots. |
state |
running, complete or interrupted. complete includes failed/aborted responses with final accounting, not just success. Terminal snapshots are immutable. |
timingBasis |
context_with_system (child Pi hook), external_request_start (producer's documented start), or unavailable. |
elapsedMs, ttftMs, responseMs |
Finite, nonnegative milliseconds from the same local monotonic request start, at most Number.MAX_SAFE_INTEGER; or null. ttftMs is the first nonempty observed delta, immutable once known. responseMs freezes at response completion, excluding tools. Known times satisfy ttftMs ≤ responseMs ≤ elapsedMs; missing TTFT is allowed. unavailable requires all three null; other bases require elapsedMs. A timed complete requires responseMs. |
outputTokens, reasoningTokens |
Nonnegative safe integer counts, or null. Output already includes reasoning; reasoning is a subset and cannot exceed output. No addition or inference of hidden tokens. Zero output does not establish usable throughput. |
stopReason |
stop, length, toolUse, error, aborted, deferred, or null. Required non-null for complete. A running snapshot requires responseMs, both token counts and stopReason to be null. |
For example, a complete snapshot's measurement fields can be elapsedMs: 30000, ttftMs: 20000, responseMs: 25000, outputTokens: 200, reasoningTokens: 150, stopReason: "stop". Its EFF TPS is 200 / 5 = 40, not 200 / 30 and not 350 / 5. No rate field is accepted; Metrics derives it. Errors, aborts, deferred/incomplete requests, unknown/zero stream duration and unknown/zero output do not produce EFF TPS. Per-agent summaries use covered output / covered stream seconds with covered/recorded request counts, not sums of parallel rates or unweighted means. Coverage says nothing about requests a producer never reported.
Snapshots are validated and scalar-copied before saving. Exact duplicates and lower-sequence stale replays are ignored; conflicting revisions, changed attribution/basis/first observation, backwards elapsed time and post-terminal changes are rejected. The saved (source, agentId, requestId) identity deduplicates bus/details/reload delivery across the whole parent session file, including abandoned branches. Display and summaries use only active-branch entries. Pending history restores as interrupted and cannot be advanced by late callbacks after restore. Previously seen child invocations cannot attach new request IDs after restore either, even if their anchor is a shared ancestor; new work needs fresh invocation IDs. A replay does not copy a finalized record into a branch that lacks it. Producers must coordinate identities: two different IDs claiming the same physical request cannot be detected automatically.
Only successfully saved snapshots advance dedup state; a failed save can be retried. A UI warning/status, when available, reports save failure without echoing the payload. The bus is fire-and-forget: it provides no delivery acknowledgment or automatic retry; the dispatcher owns transport reliability. Rejection counts appear in the footer/details and reset on session restore/navigation; rejected payloads are not saved. Ended runtimes unsubscribe from the bus. Metrics never adds external records to parent activity/request coverage or native IN/OUT/CACHE/COST; the underlying child work may already be included through the tool's native usage. Details aggregate usage is never added to the instrumented request numerator either.
Translator reported usage
/metrics and /metrics details have a separate Translator reported usage section. It reads only custom entries whose customType is orbit.translator.usage.v1, across the whole session file, including abandoned branches. It shows the recorded attempt count, reported/unreported counts, a reported subtotal only, and breakdowns by provider/model, direction and status. Failed or retried attempts with numeric usage still count; output already includes the reasoning subset.
This is not a combined session bill. Native footer IN/OUT/CACHE/COST stay unchanged. A child Translator receipt may remain in the child's session and never reach the parent, so neither a parent receipt subtotal nor native-plus-Translator arithmetic establishes complete spend. No combined total is displayed. Receipts have no request clocks and never affect TTFT, EFF TPS, OBS TPS, prompt handling or performance coverage.
- No valid receipts:
Unavailable; reporting starts after a receipt-capable Translator upgrade. Earlier usage is not backfilled. This does not prove translation was unused or free. usage: null: a recorded attempt with unreported usage, not a zero-cost attempt. Mixed reports show only the known subtotal, with unreported attempts counted separately. Pi-normalized numeric zero costs are displayed as reported, not proof of free requests or an invoice.- Deduplication: one immutable receipt per
requestIdacross the whole session, not per branch, provider or direction. Exact duplicate receipts are ignored. Conflicting valid receipts with the same ID exclude that entire ID from amounts/counts rather than selecting an arbitrary amount; malformed and duplicate receipt counts and conflicting-ID counts are shown. Malformed receipts do not reserve an ID. Legacy Translator outcome aggregates are never summed, even if they contain usage. - Read-only: each command reads current session entries; no extra receipt copy, session migration, scan of other session history or personal configuration, model call or network request is made. Resume/navigation leaves records untouched. No receipt transport or automatic forwarding from children is installed.
Receipt contract v1
The compatible Translator producer owns these custom entries; Metrics only reads them. data has exactly these fields (no prompt, answer, error text or raw provider payload):
| Field | Contract |
|---|---|
version |
Exactly 1. |
requestId |
Unique string for one actual attempt, retained across replay/forwarding; distinct retries have distinct IDs. |
provider, model |
Provider and model identifier strings. |
usage |
Complete Pi Usage or explicit null when unreported. Required nonnegative safe-integer input, output, cacheRead, cacheWrite, totalTokens; optional reasoning and cacheWrite1h are subsets of output and cacheWrite. cost requires finite nonnegative input, output, cacheRead, cacheWrite, total, each at most Number.MAX_SAFE_INTEGER. No string coercion or missing-field zero defaults. |
status |
Nonempty status string; numeric usage counts regardless of outcome. |
direction |
input, output or subagent. |
nativeAccounting |
Exactly false; these receipts are not native usage entries. |
String fields must be nonblank, at most 256 UTF-16 code units, and contain no control/format characters or line/paragraph separators. Unknown fields, invalid versions/types/numbers and inconsistent subsets reject the receipt without echoing its payload. If a subtotal exceeds the safe numeric range it is shown as unavailable, not an unsafe number. No normalized provider data can establish usage that the provider never reported. Different IDs claiming the same physical attempt cannot be detected automatically.
Session records and privacy
All data is stored as Pi custom entries, excluded from model context, normally in ~/.pi/agent/sessions/ or your configured session directory. /session locates the active file. No extra database, metrics file, network endpoint or telemetry upload is created. With --no-session or an in-memory SDK session, storage remains in memory.
Parent schema 2 keeps its three entry types; separate agent contract 1 entries do not rewrite them:
customType |
Stored data |
|---|---|
orbit.pi-metrics.v2 |
Authoritative finalized request, linked by messageEntryId, requestId, runId and request index. |
orbit.pi-metrics.run.v2 |
Activity start/progress/final summary. state distinguishes running, settled, interrupted. |
orbit.pi-metrics.trace.v2 |
request_start, first_observation, samples, provisional response_checkpoint, request_interrupted, and tool_start/tool_end events. |
orbit.pi-metrics.agent.v1 |
Validated scalar other-agent request snapshots, with source/agent/request and parent branch/tool attribution. Latest accepted snapshot per identity wins; do not sum successive snapshots. |
orbit.pi-metrics.delegation.v1 |
Final subagent details usage, linked to the durable toolResultEntryId, keeping separate parallel/chain result slots; breakdown only, no invented timing or extra native accounting. |
orbit.translator.usage.v1 |
Written by a compatible Translator, read only by Metrics: per-attempt numeric usage or null, request ID, provider/model, direction/status and nativeAccounting: false. No duplicate copy is saved. |
New request/activity records identify the EFF TPS formula with throughputBasis: "reported_output_tokens/observed_stream_elapsed_seconds". Activity coveredStreamMs is the denominator's summed stream time. measuredRequestMs and coveredRequestMs retain full request durations for analysis, not throughput calculation. Older schema 2 records with the full-request basis are still readable.
Records preserve all available numerical measurements:
- Activity/request observed TTFT, first text/thinking/tool arguments, full response time, observed stream span, ISO timestamps, timing/throughput definitions and unavailable-rate reasons.
- Requested/response model, provider/API, stop reason, provider duration, starting thinking level, allowlisted final Pi usage/cost, normalized reasoning breakdown and explicit unknown hidden-thinking fields.
- Per-kind observed character counts, delta counts, estimates and sample timestamps/rates/cumulative character counts, including samples during silent waits.
- Starting/ending context snapshots, whole-session usage/cost, request coverage and weighted activity throughput.
- Individual tool identifiers, names, parent IDs, request IDs, start/end offsets, elapsed time and error/finished status. Parallel/nested tool wall time is the union of intervals, not their sum. Reused provider tool IDs are scoped to their request.
No downsampling or history cap: every produced sample is retained. Normally samples are appended in ordered batches of 128 (about 32 seconds), then flushed at response end or orderly interruption. chunkIndex, firstSampleIndex, sampleCount, sampleChunks and samplesComplete make reconstruction auditable. Join matching samples trace entries by requestId, ordered by chunkIndex, then append the finalized request's inline samples fallback. Do not concatenate provisional checkpoint copies a second time. Actual sample timestamps expose delayed/missed timer opportunities; this is not a per-token or every-delta trace.
First-observation checkpoints preserve exact observed latencies before response completion. Response checkpoints preserve frozen timing before tools finish, but are provisional: final request entries supersede their usage after turn_end transformations. Activity progress uses compact snapshots; listsComplete: false means full ID/tool lists are omitted, not empty in reality. Counts remain available, individual tool trace entries persist, and settlement/interruption summaries include full lists. This avoids repeatedly copying growing lists.
Full retention grows the session file and Pi's in-memory history over time. Crashes can lose the current unflushed sample batch, final usage, or final activity summary; prior checkpoints remain available but cannot manufacture missing data. Interrupted or missing boundaries are marked partial. A storage failure stops periodic sampling to avoid unbounded unsaved buffering and marks incomplete capture. Once an SDK runtime is disposed, it cannot save a final summary; SDK callers should await session.abort() before session.dispose(). Pi 1.1 can itself report a missing assistant-entry boundary on abrupt disposal.
No duplicate prompts, response/thinking text, tool arguments/results, credentials or raw provider payloads are copied into metrics. Pi's normal transcript still contains its usual conversation data. Model IDs, tool names, timestamps, tokens and costs can be sensitive: review sessions before sharing them. "Full" means all captured measurements and provider-supplied normalized accounting, not unavailable internal model reasoning.
History, branches and schema 1
Latest performance and summaries follow the active branch; token/cost totals follow Pi's whole-session convention, including abandoned branches, compaction, branch summaries, cache warming and reported nested tool usage. Such auxiliary work does not receive parent conversational timing records and is not mixed into parent EFF TPS. Explicitly instrumented agents have separate branch-local records and coverage; usage-only subagent details are informational breakdowns that can overlap native usage, never added again. Translator receipt subtotals follow the whole session, remain separate from native totals, and cover only receipts present there.
Existing schema 1 and schema 2 entries are retained unchanged. Request rates are recomputed from stored streamMs, never an old cached outputTps or full responseMs. Older schema 2 activity rates are rebuilt from their linked requests' stream durations; missing or ineligible requests leave the activity rate unavailable, not a reused full-request rate. Schema 1 activity grouping/TTFT and samples discarded by the old downsampling cannot be reconstructed. Records from before installation are not backfilled. Resume/navigation never creates duplicate requests; selecting an assistant entry without its later metric entry excludes that finalized measurement.
Development and tests
From the repository root:
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)
Run these from the repository checkout: tests and development scripts are intentionally absent from the npm tarball. test:package verifies the exact dry-run and actual tarball contents, public metadata, host-only peers and the copied MIT license, then extracts the tarball outside the checkout and loads its package manifest with the real SDK in a clean temporary profile. It exercises /metrics and /metrics details with a synthetic Translator receipt, without installation or any provider/network call. Packing uses offline npm with empty temporary npm configuration; no registry credentials or real provider settings are read. Tarball extraction requires tar (available on the supported macOS/Linux test hosts).
Tests use synthetic clocks, isolated local HTTP providers, the real Pi SDK and the installed package's declared bin.pi executable. The full suite includes installation/removal only in temporary profiles. If installation commands are prohibited, run the non-installation subset with the Node option before the file list (appending it through npm test -- does not filter this script):
cd Pi/pi-metrics
node --test --test-skip-pattern='^real (CLI|fullscreen terminal|regular terminal)' test/*.test.mjs
This excludes four original install-dependent CLI cases, not a complete suite pass. The two one-run fullscreen/regular tests still exercise -e loading, collapsed TTFT/TPS, mouse/command toggles, resize and clean exit without installation. No dependencies are installed by either test path.
Terminal checks use their own tmux servers without user configuration, temporary HOME/agent/workspace/session directories, an environment allowlist and offline mode. No paid completions or personal prompts/configuration are used. Configuration isolation is not an OS sandbox.
New agent regressions cover strict numeric validation, parallel/sequential attribution, unknown/failed timing, reasoning subsets, duplicate/conflicting/stale snapshots, failed-save retry, branch-correct resume/interruption, details usage without timing, native accounting without double counting, and payload privacy. Real SDK tests run isolated synthetic parent/parallel child sessions against localhost, including a child tool delay, final usage transformations, numeric bus delivery and disk resume. They do not run the shipped subprocess subagent tool or benchmark a real provider. Collapsed-footer regressions verify TTFT/TPS precedence, retained-rate qualifiers and width-dependent context/model/status visibility. Translator regressions use synthetic receipts to cover strict validation, all three directions, failures/retries, null versus zero, malformed/duplicate/conflicting data, unsafe sums, ignored legacy aggregates, whole-session branch deduplication, read-only commands, RPC/footer-disabled display, and unchanged native usage/performance. A real-SDK fixture checks disk resume and command rendering without any provider calls and compares native child totals against Pi's own session statistics. CLI fixtures inspect the longer accounting report in a taller viewport, then restore the original 32-row footer/resize checks.
Coverage also includes retained EFF TPS across tools/content/new activities, replacement by new estimates, branch-correct restoration without changing coverage, OBS TPS hiding/reappearing across silent windows, TTFT-independent EFF TPS, missing/zero stream durations, historical full-request rate conversion, hidden-reasoning arithmetic, UI-hidden thinking, fixed activity TTFT across tools/continuations, parallel/nested tools, lossless chunking, provisional/final usage, legacy history, errors, cancellation, SDK disposal, and session resume. Footer regressions also cover the three-/one-row height bounds, Unicode and long paths, known-idle filtering without altering other extensions' statuses, warning priority, high-context visibility, collapse ownership/disposal and details isolation. Real CLI checks cover stable-copy installation/removal, JSON, fullscreen/dark and regular/light terminals, mouse collapse/expand, regular-mode keyboard toggles, native details cancel/submit without sending edits, resize, footer coexistence/toggles, opt-out, nested-source reload and clean shutdown.
Host tests explicitly skip when Pi is unavailable. Terminal tests also require tmux on macOS/Linux. Report skips, not a complete pass. SDK lookup supports managed installs, nested npm dependencies and pnpm-style SDK/dependency symlinks. Synthetic filesystem fixtures cover those layouts; they are not separate installed distributions.
Type checking requires the host SDK and an existing TypeScript compiler; it fails rather than silently skipping. Optional overrides:
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 points to a file, not a directory. PI_TSC_PATH points to the compiler's JavaScript entry, not a shell wrapper. Neither setting is required at runtime. Type checking emits no build output.
0.0.7 release-candidate verification (2026-10-28): on macOS with the unmodified official Pi 1.4.0 SDK, Node.js 26.12.0 and tmux 3.8, the final unfiltered npm test passed 95 tests, zero failures/skips, including all four install/remove CLI cases and both no-install one-run terminal cases. Strict SDK type checking passed with the existing TypeScript 5.9.3 compiler. The earlier delegated run passed 91 selected tests while reserving four installation cases for the maintainer; the full parent run supersedes that partial result. Package checks passed 2/2, including npm pack --dry-run, an actual 12-file tarball with no bundled dependencies, and isolated manifest loading plus both commands with no provider/network calls. npm/GitHub publication and live activation are separate operations. Translator receipts in this suite are synthetic fixtures, not a live-provider or producer-to-parent forwarding test.
Local release verification: version 0.0.6 passed the full suite, 84 tests with zero skips, including the four install-dependent CLI cases in temporary profiles, real-SDK child instrumentation/tool-details accounting and isolated tmux fullscreen/regular checks, on macOS with the Pi 1.4.0 SDK, Node.js 26.12.0 and tmux 3.8. Strict SDK type checking passed using the documented PI_TSC_PATH override with an existing TypeScript 5.9.3 compiler. One earlier full run, made while other test suites shared the machine, timed out in the fullscreen terminal test waiting for the collapsed footer; the full rerun and two further test:cli runs passed. Version 0.0.5 passed 69 tests with zero skips and strict TypeScript checking on macOS with Pi 1.3.0 and Node.js 26.12.0, including isolated tmux mouse/keyboard/details checks. Version 0.0.4 passed 62 tests with zero skips and strict TypeScript checking on Pi 1.2.0, Node.js 26.8.2 and tmux 3.7c. Releases up to 0.0.3 were checked on Pi 1.1.0.
Validation limits: local checks are not CI or a live-provider benchmark. Desktop fonts/colors, arbitrary third-party footer combinations and other host versions/platforms require separate acceptance. Fixtures establish arithmetic/lifecycle behavior, not provider reporting accuracy or server decode speed.
Uninstall
For the pinned npm example:
pi remove npm:@orbit-labs-ai/pi-metrics@0.0.7
For the local-copy example:
pi remove "$HOME/.local/share/orbit-tools/pi-metrics"
Use your actual registered source/path if different, then reload when idle or restart Pi. For a one-run load, exit and omit -e next time. npm removal unregisters and removes the managed package; local removal unregisters the path but does not delete the copied directory. Delete that directory only after no session uses it. Session history and metrics are retained: uninstall does not rewrite sessions or clear usage/cost data. Session deletion is a separate explicit action.
License
MIT © 2026 Orbit Labs contributors. The npm tarball and stable-copy example include the repository's MIT license alongside the tool.