@agimon-ai/doompi-log 0.0.1-alpha.18 → 0.0.1-alpha.19

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 CHANGED
@@ -24,6 +24,36 @@ Doompi loads the log viewer as core. It is not a layer and does not appear in
24
24
 
25
25
  `SPC h l` opens the Log Metrics overlay for the current session.
26
26
 
27
+ ## What the overlay shows
28
+
29
+ The panel answers two questions — what is this session costing, and what should change
30
+ next — so every row is something you can act on.
31
+
32
+ | Panel | What it tells you |
33
+ | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
34
+ | Headline | Tokens, cache hit rate, cost per turn, tool failure rate, and issues. Cache and tool-failure values are coloured against a threshold; the rest are workload-dependent and stay neutral. |
35
+ | `WHAT TO FIX` | Ranked findings derived from the session, each paired with the change that moves it: a failing tool, redone turns, a cold cache, a tool dragging outsized turns. Critical first. |
36
+ | `TOOL COST` | Tools ranked by cost per call rather than by how often they run, with call count, failure rate, and p95 latency alongside. |
37
+ | `TOP CONSUMERS` | Biggest token consumers from the sink database for the selected dimension and period. Rows with issues render in the error colour. |
38
+ | `TOKEN BURN` | Token spend per time bucket, newest last. |
39
+ | `RECENT ERRORS` | The session's most recent failures, newest first. |
40
+ | `SINK STATUS` | Backend and endpoint. Press `?` for full wiring, collection scope, and the caveats behind each panel. |
41
+
42
+ Findings are computed in-process from records the extension already emits, so the panel
43
+ keeps working with no sink connected — only `TOP CONSUMERS` and `TOKEN BURN` need one.
44
+
45
+ Thresholds are deliberately conservative: a finding that fires on a healthy session
46
+ teaches you to ignore the panel.
47
+
48
+ ### A note on `TOOL COST`
49
+
50
+ The column reports the p90 _turn_ total for turns a tool took part in, which is the only
51
+ attribution Pi emits. A turn running several tools attributes its total to each of them,
52
+ so the number ranks tools by the weight of the turns they appear in — it is not a per-tool
53
+ share of spend. The same caveat is available in the overlay under `?`.
54
+
55
+ Keys: `r` refresh · `g` cycle dimension · `p` cycle period · `?` detail · `esc` close.
56
+
27
57
  ## Public API
28
58
 
29
59
  The package exports the extension, metrics source, overlay, and Pi adapter through declared
package/dist/index.cjs CHANGED
@@ -1 +1 @@
1
- Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./services/metrics.cjs"),t=require("./tui/logMetricsOverlay.cjs"),n=require("./adapters/pi/extension.cjs");exports.API_ERROR_RECORD=e.API_ERROR_RECORD,exports.LogMetricsAggregator=e.LogMetricsAggregator,exports.LogMetricsOverlayComponent=t.LogMetricsOverlayComponent,exports.MAX_RETAINED_STATE_PER_TOOL=e.MAX_RETAINED_STATE_PER_TOOL,exports.TOOL_RESULT_RECORD=e.TOOL_RESULT_RECORD,exports.TURN_FINISHED_RECORD=e.TURN_FINISHED_RECORD,exports.createDoomLogExtension=n.createDoomLogExtension,exports.createPiTelemetryExtension=n.createPiTelemetryExtension,exports.openLogMetricsOverlay=n.openLogMetricsOverlay,exports.registerLogMetricsLeaderBinding=n.registerLogMetricsLeaderBinding;
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./services/metrics.cjs"),t=require("./services/findings.cjs"),n=require("./tui/logMetricsOverlay.cjs"),r=require("./adapters/pi/extension.cjs");exports.API_ERROR_RECORD=e.API_ERROR_RECORD,exports.LogMetricsAggregator=e.LogMetricsAggregator,exports.LogMetricsOverlayComponent=n.LogMetricsOverlayComponent,exports.MAX_RETAINED_STATE_PER_TOOL=e.MAX_RETAINED_STATE_PER_TOOL,exports.MAX_RETAINED_TOKEN_BUCKETS=e.MAX_RETAINED_TOKEN_BUCKETS,exports.TOOL_RESULT_RECORD=e.TOOL_RESULT_RECORD,exports.TOOL_TOKEN_SAMPLE_RECORD=e.TOOL_TOKEN_SAMPLE_RECORD,exports.TURN_FAILED_RECORD=e.TURN_FAILED_RECORD,exports.TURN_FINISHED_RECORD=e.TURN_FINISHED_RECORD,exports.createDoomLogExtension=r.createDoomLogExtension,exports.createPiTelemetryExtension=r.createPiTelemetryExtension,exports.deriveFindings=t.deriveFindings,exports.openLogMetricsOverlay=r.openLogMetricsOverlay,exports.registerLogMetricsLeaderBinding=r.registerLogMetricsLeaderBinding;
package/dist/index.d.cts CHANGED
@@ -1,4 +1,5 @@
1
- import { API_ERROR_RECORD, LogMetricsAggregator, LogMetricsAggregatorOptions, LogMetricsError, LogMetricsEventCount, LogMetricsOperationDuration, LogMetricsRecorder, LogMetricsSnapshot, LogMetricsTokenTotals, LogMetricsToolLatency, MAX_RETAINED_STATE_PER_TOOL, TOOL_RESULT_RECORD, TURN_FINISHED_RECORD } from "./services/metrics.cjs";
1
+ import { API_ERROR_RECORD, LogMetricsAggregator, LogMetricsAggregatorOptions, LogMetricsError, LogMetricsEventCount, LogMetricsOperationDuration, LogMetricsRecorder, LogMetricsSnapshot, LogMetricsTokenTotals, LogMetricsToolCost, LogMetricsToolLatency, MAX_RETAINED_STATE_PER_TOOL, MAX_RETAINED_TOKEN_BUCKETS, TOOL_RESULT_RECORD, TOOL_TOKEN_SAMPLE_RECORD, TURN_FAILED_RECORD, TURN_FINISHED_RECORD } from "./services/metrics.cjs";
2
2
  import { LogMetricsOverlayComponent, LogMetricsView, SinkStatus } from "./tui/logMetricsOverlay.cjs";
3
3
  import { PiTelemetryExtensionOptions, createDoomLogExtension, createPiTelemetryExtension, openLogMetricsOverlay, registerLogMetricsLeaderBinding } from "./adapters/pi/extension.cjs";
4
- export { API_ERROR_RECORD, LogMetricsAggregator, type LogMetricsAggregatorOptions, type LogMetricsError, type LogMetricsEventCount, type LogMetricsOperationDuration, LogMetricsOverlayComponent, type LogMetricsRecorder, type LogMetricsSnapshot, type LogMetricsTokenTotals, type LogMetricsToolLatency, type LogMetricsView, MAX_RETAINED_STATE_PER_TOOL, type PiTelemetryExtensionOptions, type SinkStatus, TOOL_RESULT_RECORD, TURN_FINISHED_RECORD, createDoomLogExtension, createPiTelemetryExtension, openLogMetricsOverlay, registerLogMetricsLeaderBinding };
4
+ import { LogMetricsFinding, LogMetricsFindingSeverity, deriveFindings } from "./services/findings.cjs";
5
+ export { API_ERROR_RECORD, LogMetricsAggregator, type LogMetricsAggregatorOptions, type LogMetricsError, type LogMetricsEventCount, type LogMetricsFinding, type LogMetricsFindingSeverity, type LogMetricsOperationDuration, LogMetricsOverlayComponent, type LogMetricsRecorder, type LogMetricsSnapshot, type LogMetricsTokenTotals, type LogMetricsToolCost, type LogMetricsToolLatency, type LogMetricsView, MAX_RETAINED_STATE_PER_TOOL, MAX_RETAINED_TOKEN_BUCKETS, type PiTelemetryExtensionOptions, type SinkStatus, TOOL_RESULT_RECORD, TOOL_TOKEN_SAMPLE_RECORD, TURN_FAILED_RECORD, TURN_FINISHED_RECORD, createDoomLogExtension, createPiTelemetryExtension, deriveFindings, openLogMetricsOverlay, registerLogMetricsLeaderBinding };
package/dist/index.d.mts CHANGED
@@ -1,4 +1,5 @@
1
- import { API_ERROR_RECORD, LogMetricsAggregator, LogMetricsAggregatorOptions, LogMetricsError, LogMetricsEventCount, LogMetricsOperationDuration, LogMetricsRecorder, LogMetricsSnapshot, LogMetricsTokenTotals, LogMetricsToolLatency, MAX_RETAINED_STATE_PER_TOOL, TOOL_RESULT_RECORD, TURN_FINISHED_RECORD } from "./services/metrics.mjs";
1
+ import { API_ERROR_RECORD, LogMetricsAggregator, LogMetricsAggregatorOptions, LogMetricsError, LogMetricsEventCount, LogMetricsOperationDuration, LogMetricsRecorder, LogMetricsSnapshot, LogMetricsTokenTotals, LogMetricsToolCost, LogMetricsToolLatency, MAX_RETAINED_STATE_PER_TOOL, MAX_RETAINED_TOKEN_BUCKETS, TOOL_RESULT_RECORD, TOOL_TOKEN_SAMPLE_RECORD, TURN_FAILED_RECORD, TURN_FINISHED_RECORD } from "./services/metrics.mjs";
2
2
  import { LogMetricsOverlayComponent, LogMetricsView, SinkStatus } from "./tui/logMetricsOverlay.mjs";
3
3
  import { PiTelemetryExtensionOptions, createDoomLogExtension, createPiTelemetryExtension, openLogMetricsOverlay, registerLogMetricsLeaderBinding } from "./adapters/pi/extension.mjs";
4
- export { API_ERROR_RECORD, LogMetricsAggregator, type LogMetricsAggregatorOptions, type LogMetricsError, type LogMetricsEventCount, type LogMetricsOperationDuration, LogMetricsOverlayComponent, type LogMetricsRecorder, type LogMetricsSnapshot, type LogMetricsTokenTotals, type LogMetricsToolLatency, type LogMetricsView, MAX_RETAINED_STATE_PER_TOOL, type PiTelemetryExtensionOptions, type SinkStatus, TOOL_RESULT_RECORD, TURN_FINISHED_RECORD, createDoomLogExtension, createPiTelemetryExtension, openLogMetricsOverlay, registerLogMetricsLeaderBinding };
4
+ import { LogMetricsFinding, LogMetricsFindingSeverity, deriveFindings } from "./services/findings.mjs";
5
+ export { API_ERROR_RECORD, LogMetricsAggregator, type LogMetricsAggregatorOptions, type LogMetricsError, type LogMetricsEventCount, type LogMetricsFinding, type LogMetricsFindingSeverity, type LogMetricsOperationDuration, LogMetricsOverlayComponent, type LogMetricsRecorder, type LogMetricsSnapshot, type LogMetricsTokenTotals, type LogMetricsToolCost, type LogMetricsToolLatency, type LogMetricsView, MAX_RETAINED_STATE_PER_TOOL, MAX_RETAINED_TOKEN_BUCKETS, type PiTelemetryExtensionOptions, type SinkStatus, TOOL_RESULT_RECORD, TOOL_TOKEN_SAMPLE_RECORD, TURN_FAILED_RECORD, TURN_FINISHED_RECORD, createDoomLogExtension, createPiTelemetryExtension, deriveFindings, openLogMetricsOverlay, registerLogMetricsLeaderBinding };
package/dist/index.mjs CHANGED
@@ -1 +1 @@
1
- import{API_ERROR_RECORD as e,LogMetricsAggregator as t,MAX_RETAINED_STATE_PER_TOOL as n,TOOL_RESULT_RECORD as r,TURN_FINISHED_RECORD as i}from"./services/metrics.mjs";import{LogMetricsOverlayComponent as a}from"./tui/logMetricsOverlay.mjs";import{createDoomLogExtension as o,createPiTelemetryExtension as s,openLogMetricsOverlay as c,registerLogMetricsLeaderBinding as l}from"./adapters/pi/extension.mjs";export{e as API_ERROR_RECORD,t as LogMetricsAggregator,a as LogMetricsOverlayComponent,n as MAX_RETAINED_STATE_PER_TOOL,r as TOOL_RESULT_RECORD,i as TURN_FINISHED_RECORD,o as createDoomLogExtension,s as createPiTelemetryExtension,c as openLogMetricsOverlay,l as registerLogMetricsLeaderBinding};
1
+ import{API_ERROR_RECORD as e,LogMetricsAggregator as t,MAX_RETAINED_STATE_PER_TOOL as n,MAX_RETAINED_TOKEN_BUCKETS as r,TOOL_RESULT_RECORD as i,TOOL_TOKEN_SAMPLE_RECORD as a,TURN_FAILED_RECORD as o,TURN_FINISHED_RECORD as s}from"./services/metrics.mjs";import{deriveFindings as c}from"./services/findings.mjs";import{LogMetricsOverlayComponent as l}from"./tui/logMetricsOverlay.mjs";import{createDoomLogExtension as u,createPiTelemetryExtension as d,openLogMetricsOverlay as f,registerLogMetricsLeaderBinding as p}from"./adapters/pi/extension.mjs";export{e as API_ERROR_RECORD,t as LogMetricsAggregator,l as LogMetricsOverlayComponent,n as MAX_RETAINED_STATE_PER_TOOL,r as MAX_RETAINED_TOKEN_BUCKETS,i as TOOL_RESULT_RECORD,a as TOOL_TOKEN_SAMPLE_RECORD,o as TURN_FAILED_RECORD,s as TURN_FINISHED_RECORD,u as createDoomLogExtension,d as createPiTelemetryExtension,c as deriveFindings,f as openLogMetricsOverlay,p as registerLogMetricsLeaderBinding};
package/dist/metrics.cjs CHANGED
@@ -1 +1 @@
1
- Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./services/metrics.cjs");exports.API_ERROR_RECORD=e.API_ERROR_RECORD,exports.LogMetricsAggregator=e.LogMetricsAggregator,exports.MAX_RETAINED_STATE_PER_TOOL=e.MAX_RETAINED_STATE_PER_TOOL,exports.TOOL_RESULT_RECORD=e.TOOL_RESULT_RECORD,exports.TURN_FINISHED_RECORD=e.TURN_FINISHED_RECORD;
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./services/metrics.cjs"),t=require("./services/findings.cjs");exports.API_ERROR_RECORD=e.API_ERROR_RECORD,exports.LogMetricsAggregator=e.LogMetricsAggregator,exports.MAX_RETAINED_STATE_PER_TOOL=e.MAX_RETAINED_STATE_PER_TOOL,exports.MAX_RETAINED_TOKEN_BUCKETS=e.MAX_RETAINED_TOKEN_BUCKETS,exports.TOOL_RESULT_RECORD=e.TOOL_RESULT_RECORD,exports.TOOL_TOKEN_SAMPLE_RECORD=e.TOOL_TOKEN_SAMPLE_RECORD,exports.TURN_FAILED_RECORD=e.TURN_FAILED_RECORD,exports.TURN_FINISHED_RECORD=e.TURN_FINISHED_RECORD,exports.deriveFindings=t.deriveFindings;
@@ -1,2 +1,3 @@
1
- import { API_ERROR_RECORD, LogMetricsAggregator, LogMetricsAggregatorOptions, LogMetricsError, LogMetricsEventCount, LogMetricsOperationDuration, LogMetricsRecorder, LogMetricsSnapshot, LogMetricsTokenTotals, LogMetricsToolLatency, MAX_RETAINED_STATE_PER_TOOL, TOOL_RESULT_RECORD, TURN_FINISHED_RECORD } from "./services/metrics.cjs";
2
- export { API_ERROR_RECORD, LogMetricsAggregator, type LogMetricsAggregatorOptions, type LogMetricsError, type LogMetricsEventCount, type LogMetricsOperationDuration, type LogMetricsRecorder, type LogMetricsSnapshot, type LogMetricsTokenTotals, type LogMetricsToolLatency, MAX_RETAINED_STATE_PER_TOOL, TOOL_RESULT_RECORD, TURN_FINISHED_RECORD };
1
+ import { API_ERROR_RECORD, LogMetricsAggregator, LogMetricsAggregatorOptions, LogMetricsError, LogMetricsEventCount, LogMetricsOperationDuration, LogMetricsRecorder, LogMetricsSnapshot, LogMetricsTokenTotals, LogMetricsToolCost, LogMetricsToolLatency, MAX_RETAINED_STATE_PER_TOOL, MAX_RETAINED_TOKEN_BUCKETS, TOOL_RESULT_RECORD, TOOL_TOKEN_SAMPLE_RECORD, TURN_FAILED_RECORD, TURN_FINISHED_RECORD } from "./services/metrics.cjs";
2
+ import { LogMetricsFinding, LogMetricsFindingSeverity, deriveFindings } from "./services/findings.cjs";
3
+ export { API_ERROR_RECORD, LogMetricsAggregator, type LogMetricsAggregatorOptions, type LogMetricsError, type LogMetricsEventCount, type LogMetricsFinding, type LogMetricsFindingSeverity, type LogMetricsOperationDuration, type LogMetricsRecorder, type LogMetricsSnapshot, type LogMetricsTokenTotals, type LogMetricsToolCost, type LogMetricsToolLatency, MAX_RETAINED_STATE_PER_TOOL, MAX_RETAINED_TOKEN_BUCKETS, TOOL_RESULT_RECORD, TOOL_TOKEN_SAMPLE_RECORD, TURN_FAILED_RECORD, TURN_FINISHED_RECORD, deriveFindings };
@@ -1,2 +1,3 @@
1
- import { API_ERROR_RECORD, LogMetricsAggregator, LogMetricsAggregatorOptions, LogMetricsError, LogMetricsEventCount, LogMetricsOperationDuration, LogMetricsRecorder, LogMetricsSnapshot, LogMetricsTokenTotals, LogMetricsToolLatency, MAX_RETAINED_STATE_PER_TOOL, TOOL_RESULT_RECORD, TURN_FINISHED_RECORD } from "./services/metrics.mjs";
2
- export { API_ERROR_RECORD, LogMetricsAggregator, type LogMetricsAggregatorOptions, type LogMetricsError, type LogMetricsEventCount, type LogMetricsOperationDuration, type LogMetricsRecorder, type LogMetricsSnapshot, type LogMetricsTokenTotals, type LogMetricsToolLatency, MAX_RETAINED_STATE_PER_TOOL, TOOL_RESULT_RECORD, TURN_FINISHED_RECORD };
1
+ import { API_ERROR_RECORD, LogMetricsAggregator, LogMetricsAggregatorOptions, LogMetricsError, LogMetricsEventCount, LogMetricsOperationDuration, LogMetricsRecorder, LogMetricsSnapshot, LogMetricsTokenTotals, LogMetricsToolCost, LogMetricsToolLatency, MAX_RETAINED_STATE_PER_TOOL, MAX_RETAINED_TOKEN_BUCKETS, TOOL_RESULT_RECORD, TOOL_TOKEN_SAMPLE_RECORD, TURN_FAILED_RECORD, TURN_FINISHED_RECORD } from "./services/metrics.mjs";
2
+ import { LogMetricsFinding, LogMetricsFindingSeverity, deriveFindings } from "./services/findings.mjs";
3
+ export { API_ERROR_RECORD, LogMetricsAggregator, type LogMetricsAggregatorOptions, type LogMetricsError, type LogMetricsEventCount, type LogMetricsFinding, type LogMetricsFindingSeverity, type LogMetricsOperationDuration, type LogMetricsRecorder, type LogMetricsSnapshot, type LogMetricsTokenTotals, type LogMetricsToolCost, type LogMetricsToolLatency, MAX_RETAINED_STATE_PER_TOOL, MAX_RETAINED_TOKEN_BUCKETS, TOOL_RESULT_RECORD, TOOL_TOKEN_SAMPLE_RECORD, TURN_FAILED_RECORD, TURN_FINISHED_RECORD, deriveFindings };
package/dist/metrics.mjs CHANGED
@@ -1 +1 @@
1
- import{API_ERROR_RECORD as e,LogMetricsAggregator as t,MAX_RETAINED_STATE_PER_TOOL as n,TOOL_RESULT_RECORD as r,TURN_FINISHED_RECORD as i}from"./services/metrics.mjs";export{e as API_ERROR_RECORD,t as LogMetricsAggregator,n as MAX_RETAINED_STATE_PER_TOOL,r as TOOL_RESULT_RECORD,i as TURN_FINISHED_RECORD};
1
+ import{API_ERROR_RECORD as e,LogMetricsAggregator as t,MAX_RETAINED_STATE_PER_TOOL as n,MAX_RETAINED_TOKEN_BUCKETS as r,TOOL_RESULT_RECORD as i,TOOL_TOKEN_SAMPLE_RECORD as a,TURN_FAILED_RECORD as o,TURN_FINISHED_RECORD as s}from"./services/metrics.mjs";import{deriveFindings as c}from"./services/findings.mjs";export{e as API_ERROR_RECORD,t as LogMetricsAggregator,n as MAX_RETAINED_STATE_PER_TOOL,r as MAX_RETAINED_TOKEN_BUCKETS,i as TOOL_RESULT_RECORD,a as TOOL_TOKEN_SAMPLE_RECORD,o as TURN_FAILED_RECORD,s as TURN_FINISHED_RECORD,c as deriveFindings};
@@ -0,0 +1,2 @@
1
+ const e=`rate_limited`,t={[e]:`rate limited`,provider_unavailable:`unavailable`},n={critical:0,warning:1,info:2},r=1e6,i=1e3,a={id:`healthy`,severity:`info`,subject:`session`,detail:`no failures, no outliers`,action:`nothing to act on yet`};function o(e){return e>=r?`${(e/r).toFixed(1)}M`:e>=i?`${Math.round(e/i)}k`:String(Math.round(e))}function s(e){return`${Math.round(e*100)}%`}function c(e){let t=e.map(e=>e.p90Tokens).filter(e=>e!==void 0).sort((e,t)=>e-t);if(t.length===0)return 0;let n=Math.floor(t.length/2),r=t[n-1]??0,i=t[n]??0;return t.length%2==0?(r+i)/2:i}function l(e){let{cacheRead:t,cacheWrite:n,input:r}=e.tokens,i=t+r;if(i<5e4)return;let a=t/i;if(!(a>=.3))return{id:`low-cache-hit`,severity:`warning`,subject:`cache`,detail:`${s(a)} hit · ${o(n)} written`,action:`pin stable context first`}}function u(e){let t=c(e.toolCost);if(t<=0)return;let n=e.toolCost.find(e=>e.samples>=3&&(e.p90Tokens??0)>=t*1.5);if(n?.p90Tokens)return{id:`expensive-tool`,severity:`warning`,subject:n.name,detail:`${o(n.p90Tokens)} p90 · ${Math.round(n.p90Tokens/t)}× median`,action:`narrow what it pulls in`}}function d(e){return e.toolCost.filter(e=>e.calls>=3&&e.failed/e.calls>=.25).sort((e,t)=>t.failed-e.failed).map(e=>({id:`failing-tool:${e.name}`,severity:`critical`,subject:e.name,detail:`${e.failed} of ${e.calls} failed`,action:`retries burn tokens`}))}function f(e){if(e.abortedTurns+e.failedTurns!==0)return{id:`redone-turns`,severity:`critical`,subject:`turns`,detail:`${e.abortedTurns} aborted · ${e.failedTurns} failed`,action:`paid for, then thrown away`}}function p(n){let r=n.failureCategories.find(e=>e.name in t);if(r)return{id:`provider:${r.name}`,severity:`warning`,subject:`provider`,detail:`${r.count} × ${t[r.name]??r.name}`,action:r.name===e?`back off or drop concurrency`:`retry or switch provider`}}function m(e){if(e.turns<3)return;let t=e.tokens.input/e.turns;if(!(t<12e4))return{id:`heavy-context`,severity:`warning`,subject:`context`,detail:`${o(t)} input tok/turn`,action:`compact or split the task`}}function h(e){if(e.turns<3)return;let t=e.toolCalls/e.turns;if(!(t<8))return{id:`tool-churn`,severity:`info`,subject:`tools`,detail:`${t.toFixed(1)} calls/turn`,action:`give it a narrower target`}}function g(e){let t=[...d(e),f(e),p(e),l(e),u(e),m(e),h(e)].filter(e=>e!==void 0);return t.length===0?e.events>0?[a]:[]:t.sort((e,t)=>n[e.severity]-n[t.severity])}exports.deriveFindings=g;
2
+ //# sourceMappingURL=findings.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"findings.cjs","names":[],"sources":["../../src/services/findings.ts"],"sourcesContent":["/**\n * Ranked, actionable findings derived from a session snapshot.\n *\n * Every finding restates numbers the aggregator already holds and pairs them\n * with the one change that moves them, because a counter the user cannot act on\n * is screen space spent for nothing. Nothing here queries the sink: the panel\n * has to stay useful when no sink is connected, which is exactly the situation\n * where a user has no other way to see any of this.\n *\n * Thresholds are deliberately conservative. A finding that fires on a healthy\n * session teaches the user to ignore the panel, which is worse than an empty\n * one.\n */\n\nimport type { LogMetricsSnapshot, LogMetricsToolCost } from './metrics.ts';\n\nexport type LogMetricsFindingSeverity = 'critical' | 'warning' | 'info';\n\nexport interface LogMetricsFinding {\n /** Stable identifier, so a finding can be asserted without matching prose. */\n id: string;\n severity: LogMetricsFindingSeverity;\n /** What the finding is about: a tool name, or a facet like `cache`. */\n subject: string;\n /** The evidence, already formatted. */\n detail: string;\n /** The change that moves the number. */\n action: string;\n}\n\n/** Below this, the prompt prefix is churning and cache writes are being wasted. */\nconst LOW_CACHE_HIT_RATIO = 0.3;\n/** Cache hit rate is meaningless on a session that has barely sent anything. */\nconst MIN_CACHE_SAMPLE_TOKENS = 50_000;\n/** A tool is \"expensive\" only well clear of the session's typical turn. */\nconst EXPENSIVE_TOOL_RATIO = 1.5;\nconst MIN_TOOL_SAMPLES = 3;\nconst FAILING_TOOL_RATIO = 0.25;\nconst MIN_TOOL_CALLS = 3;\n/** Sustained input this large per turn means context is carried, not scoped. */\nconst HEAVY_TURN_INPUT_TOKENS = 120_000;\nconst MIN_TURNS_FOR_AVERAGE = 3;\n/** Tool calls per turn above this reads as search thrash rather than progress. */\nconst BUSY_TURN_TOOL_CALLS = 8;\n\nconst RATE_LIMITED = 'rate_limited';\n/** Kept short because the finding's subject column already says \"provider\". */\nconst SHORT_CATEGORY: Record<string, string> = {\n [RATE_LIMITED]: 'rate limited',\n provider_unavailable: 'unavailable',\n};\n\nconst SEVERITY_RANK: Record<LogMetricsFindingSeverity, number> = { critical: 0, warning: 1, info: 2 };\nconst PERCENT = 100;\nconst MILLION = 1_000_000;\nconst THOUSAND = 1000;\nconst COMPACT_DECIMALS = 1;\nconst HEALTHY_FINDING: LogMetricsFinding = {\n id: 'healthy',\n severity: 'info',\n subject: 'session',\n detail: 'no failures, no outliers',\n action: 'nothing to act on yet',\n};\n\nfunction compact(value: number): string {\n if (value >= MILLION) return `${(value / MILLION).toFixed(COMPACT_DECIMALS)}M`;\n if (value >= THOUSAND) return `${Math.round(value / THOUSAND)}k`;\n return String(Math.round(value));\n}\n\nfunction percent(value: number): string {\n return `${Math.round(value * PERCENT)}%`;\n}\n\n/**\n * Median rather than mean: one pathological tool would drag a mean up far\n * enough to hide itself from the comparison that is meant to catch it.\n */\nfunction medianTokens(tools: readonly LogMetricsToolCost[]): number {\n const sampled = tools\n .map((tool) => tool.p90Tokens)\n .filter((tokens): tokens is number => tokens !== undefined)\n .sort((left, right) => left - right);\n if (sampled.length === 0) return 0;\n const middle = Math.floor(sampled.length / 2);\n const lower = sampled[middle - 1] ?? 0;\n const upper = sampled[middle] ?? 0;\n return sampled.length % 2 === 0 ? (lower + upper) / 2 : upper;\n}\n\nfunction cacheFinding(snapshot: LogMetricsSnapshot): LogMetricsFinding | undefined {\n const { cacheRead, cacheWrite, input } = snapshot.tokens;\n const cacheable = cacheRead + input;\n if (cacheable < MIN_CACHE_SAMPLE_TOKENS) return undefined;\n\n const hitRatio = cacheRead / cacheable;\n if (hitRatio >= LOW_CACHE_HIT_RATIO) return undefined;\n return {\n id: 'low-cache-hit',\n severity: 'warning',\n subject: 'cache',\n detail: `${percent(hitRatio)} hit · ${compact(cacheWrite)} written`,\n action: 'pin stable context first',\n };\n}\n\nfunction expensiveToolFinding(snapshot: LogMetricsSnapshot): LogMetricsFinding | undefined {\n const median = medianTokens(snapshot.toolCost);\n if (median <= 0) return undefined;\n\n const worst = snapshot.toolCost.find(\n (tool) => tool.samples >= MIN_TOOL_SAMPLES && (tool.p90Tokens ?? 0) >= median * EXPENSIVE_TOOL_RATIO,\n );\n if (!worst?.p90Tokens) return undefined;\n return {\n id: 'expensive-tool',\n severity: 'warning',\n subject: worst.name,\n detail: `${compact(worst.p90Tokens)} p90 · ${Math.round(worst.p90Tokens / median)}× median`,\n action: 'narrow what it pulls in',\n };\n}\n\nfunction failingToolFindings(snapshot: LogMetricsSnapshot): LogMetricsFinding[] {\n return snapshot.toolCost\n .filter((tool) => tool.calls >= MIN_TOOL_CALLS && tool.failed / tool.calls >= FAILING_TOOL_RATIO)\n .sort((left, right) => right.failed - left.failed)\n .map((tool) => ({\n id: `failing-tool:${tool.name}`,\n severity: 'critical' as const,\n subject: tool.name,\n detail: `${tool.failed} of ${tool.calls} failed`,\n action: 'retries burn tokens',\n }));\n}\n\nfunction abortedTurnFinding(snapshot: LogMetricsSnapshot): LogMetricsFinding | undefined {\n const redone = snapshot.abortedTurns + snapshot.failedTurns;\n if (redone === 0) return undefined;\n return {\n id: 'redone-turns',\n severity: 'critical',\n subject: 'turns',\n detail: `${snapshot.abortedTurns} aborted · ${snapshot.failedTurns} failed`,\n action: 'paid for, then thrown away',\n };\n}\n\nfunction providerFinding(snapshot: LogMetricsSnapshot): LogMetricsFinding | undefined {\n const category = snapshot.failureCategories.find((entry) => entry.name in SHORT_CATEGORY);\n if (!category) return undefined;\n return {\n id: `provider:${category.name}`,\n severity: 'warning',\n subject: 'provider',\n detail: `${category.count} × ${SHORT_CATEGORY[category.name] ?? category.name}`,\n action: category.name === RATE_LIMITED ? 'back off or drop concurrency' : 'retry or switch provider',\n };\n}\n\nfunction contextFinding(snapshot: LogMetricsSnapshot): LogMetricsFinding | undefined {\n if (snapshot.turns < MIN_TURNS_FOR_AVERAGE) return undefined;\n\n const perTurn = snapshot.tokens.input / snapshot.turns;\n if (perTurn < HEAVY_TURN_INPUT_TOKENS) return undefined;\n return {\n id: 'heavy-context',\n severity: 'warning',\n subject: 'context',\n detail: `${compact(perTurn)} input tok/turn`,\n action: 'compact or split the task',\n };\n}\n\nfunction toolChurnFinding(snapshot: LogMetricsSnapshot): LogMetricsFinding | undefined {\n if (snapshot.turns < MIN_TURNS_FOR_AVERAGE) return undefined;\n\n const perTurn = snapshot.toolCalls / snapshot.turns;\n if (perTurn < BUSY_TURN_TOOL_CALLS) return undefined;\n return {\n id: 'tool-churn',\n severity: 'info',\n subject: 'tools',\n detail: `${perTurn.toFixed(1)} calls/turn`,\n action: 'give it a narrower target',\n };\n}\n\n/**\n * Ranked most-actionable first. Severity leads because a failing tool costs\n * more than an inefficient one, and the panel is read top-down under a\n * height limit.\n */\nexport function deriveFindings(snapshot: LogMetricsSnapshot): LogMetricsFinding[] {\n const findings = [\n ...failingToolFindings(snapshot),\n abortedTurnFinding(snapshot),\n providerFinding(snapshot),\n cacheFinding(snapshot),\n expensiveToolFinding(snapshot),\n contextFinding(snapshot),\n toolChurnFinding(snapshot),\n ].filter((finding): finding is LogMetricsFinding => finding !== undefined);\n\n if (findings.length === 0) return snapshot.events > 0 ? [HEALTHY_FINDING] : [];\n return findings.sort((left, right) => SEVERITY_RANK[left.severity] - SEVERITY_RANK[right.severity]);\n}\n"],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ import { LogMetricsSnapshot } from "./metrics.cjs";
2
+ //#region src/services/findings.d.ts
3
+ type LogMetricsFindingSeverity = 'critical' | 'warning' | 'info';
4
+ interface LogMetricsFinding {
5
+ /** Stable identifier, so a finding can be asserted without matching prose. */
6
+ id: string;
7
+ severity: LogMetricsFindingSeverity;
8
+ /** What the finding is about: a tool name, or a facet like `cache`. */
9
+ subject: string;
10
+ /** The evidence, already formatted. */
11
+ detail: string;
12
+ /** The change that moves the number. */
13
+ action: string;
14
+ }
15
+ /**
16
+ * Ranked most-actionable first. Severity leads because a failing tool costs
17
+ * more than an inefficient one, and the panel is read top-down under a
18
+ * height limit.
19
+ */
20
+ declare function deriveFindings(snapshot: LogMetricsSnapshot): LogMetricsFinding[];
21
+ //#endregion
22
+ export { LogMetricsFinding, LogMetricsFindingSeverity, deriveFindings };
23
+ //# sourceMappingURL=findings.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"findings.d.cts","names":[],"sources":["../../src/services/findings.ts"],"mappings":";;KAgBY;UAEK;;EAEf;EACA,UAAU;;EAEV;;EAEA;;EAEA;;;;;;;iBAuKc,eAAe,UAAU,qBAAqB"}
@@ -0,0 +1,23 @@
1
+ import { LogMetricsSnapshot } from "./metrics.mjs";
2
+ //#region src/services/findings.d.ts
3
+ type LogMetricsFindingSeverity = 'critical' | 'warning' | 'info';
4
+ interface LogMetricsFinding {
5
+ /** Stable identifier, so a finding can be asserted without matching prose. */
6
+ id: string;
7
+ severity: LogMetricsFindingSeverity;
8
+ /** What the finding is about: a tool name, or a facet like `cache`. */
9
+ subject: string;
10
+ /** The evidence, already formatted. */
11
+ detail: string;
12
+ /** The change that moves the number. */
13
+ action: string;
14
+ }
15
+ /**
16
+ * Ranked most-actionable first. Severity leads because a failing tool costs
17
+ * more than an inefficient one, and the panel is read top-down under a
18
+ * height limit.
19
+ */
20
+ declare function deriveFindings(snapshot: LogMetricsSnapshot): LogMetricsFinding[];
21
+ //#endregion
22
+ export { LogMetricsFinding, LogMetricsFindingSeverity, deriveFindings };
23
+ //# sourceMappingURL=findings.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"findings.d.mts","names":[],"sources":["../../src/services/findings.ts"],"mappings":";;KAgBY;UAEK;;EAEf;EACA,UAAU;;EAEV;;EAEA;;EAEA;;;;;;;iBAuKc,eAAe,UAAU,qBAAqB"}
@@ -0,0 +1,2 @@
1
+ const e=`rate_limited`,t={[e]:`rate limited`,provider_unavailable:`unavailable`},n={critical:0,warning:1,info:2},r=1e6,i=1e3,a={id:`healthy`,severity:`info`,subject:`session`,detail:`no failures, no outliers`,action:`nothing to act on yet`};function o(e){return e>=r?`${(e/r).toFixed(1)}M`:e>=i?`${Math.round(e/i)}k`:String(Math.round(e))}function s(e){return`${Math.round(e*100)}%`}function c(e){let t=e.map(e=>e.p90Tokens).filter(e=>e!==void 0).sort((e,t)=>e-t);if(t.length===0)return 0;let n=Math.floor(t.length/2),r=t[n-1]??0,i=t[n]??0;return t.length%2==0?(r+i)/2:i}function l(e){let{cacheRead:t,cacheWrite:n,input:r}=e.tokens,i=t+r;if(i<5e4)return;let a=t/i;if(!(a>=.3))return{id:`low-cache-hit`,severity:`warning`,subject:`cache`,detail:`${s(a)} hit · ${o(n)} written`,action:`pin stable context first`}}function u(e){let t=c(e.toolCost);if(t<=0)return;let n=e.toolCost.find(e=>e.samples>=3&&(e.p90Tokens??0)>=t*1.5);if(n?.p90Tokens)return{id:`expensive-tool`,severity:`warning`,subject:n.name,detail:`${o(n.p90Tokens)} p90 · ${Math.round(n.p90Tokens/t)}× median`,action:`narrow what it pulls in`}}function d(e){return e.toolCost.filter(e=>e.calls>=3&&e.failed/e.calls>=.25).sort((e,t)=>t.failed-e.failed).map(e=>({id:`failing-tool:${e.name}`,severity:`critical`,subject:e.name,detail:`${e.failed} of ${e.calls} failed`,action:`retries burn tokens`}))}function f(e){if(e.abortedTurns+e.failedTurns!==0)return{id:`redone-turns`,severity:`critical`,subject:`turns`,detail:`${e.abortedTurns} aborted · ${e.failedTurns} failed`,action:`paid for, then thrown away`}}function p(n){let r=n.failureCategories.find(e=>e.name in t);if(r)return{id:`provider:${r.name}`,severity:`warning`,subject:`provider`,detail:`${r.count} × ${t[r.name]??r.name}`,action:r.name===e?`back off or drop concurrency`:`retry or switch provider`}}function m(e){if(e.turns<3)return;let t=e.tokens.input/e.turns;if(!(t<12e4))return{id:`heavy-context`,severity:`warning`,subject:`context`,detail:`${o(t)} input tok/turn`,action:`compact or split the task`}}function h(e){if(e.turns<3)return;let t=e.toolCalls/e.turns;if(!(t<8))return{id:`tool-churn`,severity:`info`,subject:`tools`,detail:`${t.toFixed(1)} calls/turn`,action:`give it a narrower target`}}function g(e){let t=[...d(e),f(e),p(e),l(e),u(e),m(e),h(e)].filter(e=>e!==void 0);return t.length===0?e.events>0?[a]:[]:t.sort((e,t)=>n[e.severity]-n[t.severity])}export{g as deriveFindings};
2
+ //# sourceMappingURL=findings.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"findings.mjs","names":[],"sources":["../../src/services/findings.ts"],"sourcesContent":["/**\n * Ranked, actionable findings derived from a session snapshot.\n *\n * Every finding restates numbers the aggregator already holds and pairs them\n * with the one change that moves them, because a counter the user cannot act on\n * is screen space spent for nothing. Nothing here queries the sink: the panel\n * has to stay useful when no sink is connected, which is exactly the situation\n * where a user has no other way to see any of this.\n *\n * Thresholds are deliberately conservative. A finding that fires on a healthy\n * session teaches the user to ignore the panel, which is worse than an empty\n * one.\n */\n\nimport type { LogMetricsSnapshot, LogMetricsToolCost } from './metrics.ts';\n\nexport type LogMetricsFindingSeverity = 'critical' | 'warning' | 'info';\n\nexport interface LogMetricsFinding {\n /** Stable identifier, so a finding can be asserted without matching prose. */\n id: string;\n severity: LogMetricsFindingSeverity;\n /** What the finding is about: a tool name, or a facet like `cache`. */\n subject: string;\n /** The evidence, already formatted. */\n detail: string;\n /** The change that moves the number. */\n action: string;\n}\n\n/** Below this, the prompt prefix is churning and cache writes are being wasted. */\nconst LOW_CACHE_HIT_RATIO = 0.3;\n/** Cache hit rate is meaningless on a session that has barely sent anything. */\nconst MIN_CACHE_SAMPLE_TOKENS = 50_000;\n/** A tool is \"expensive\" only well clear of the session's typical turn. */\nconst EXPENSIVE_TOOL_RATIO = 1.5;\nconst MIN_TOOL_SAMPLES = 3;\nconst FAILING_TOOL_RATIO = 0.25;\nconst MIN_TOOL_CALLS = 3;\n/** Sustained input this large per turn means context is carried, not scoped. */\nconst HEAVY_TURN_INPUT_TOKENS = 120_000;\nconst MIN_TURNS_FOR_AVERAGE = 3;\n/** Tool calls per turn above this reads as search thrash rather than progress. */\nconst BUSY_TURN_TOOL_CALLS = 8;\n\nconst RATE_LIMITED = 'rate_limited';\n/** Kept short because the finding's subject column already says \"provider\". */\nconst SHORT_CATEGORY: Record<string, string> = {\n [RATE_LIMITED]: 'rate limited',\n provider_unavailable: 'unavailable',\n};\n\nconst SEVERITY_RANK: Record<LogMetricsFindingSeverity, number> = { critical: 0, warning: 1, info: 2 };\nconst PERCENT = 100;\nconst MILLION = 1_000_000;\nconst THOUSAND = 1000;\nconst COMPACT_DECIMALS = 1;\nconst HEALTHY_FINDING: LogMetricsFinding = {\n id: 'healthy',\n severity: 'info',\n subject: 'session',\n detail: 'no failures, no outliers',\n action: 'nothing to act on yet',\n};\n\nfunction compact(value: number): string {\n if (value >= MILLION) return `${(value / MILLION).toFixed(COMPACT_DECIMALS)}M`;\n if (value >= THOUSAND) return `${Math.round(value / THOUSAND)}k`;\n return String(Math.round(value));\n}\n\nfunction percent(value: number): string {\n return `${Math.round(value * PERCENT)}%`;\n}\n\n/**\n * Median rather than mean: one pathological tool would drag a mean up far\n * enough to hide itself from the comparison that is meant to catch it.\n */\nfunction medianTokens(tools: readonly LogMetricsToolCost[]): number {\n const sampled = tools\n .map((tool) => tool.p90Tokens)\n .filter((tokens): tokens is number => tokens !== undefined)\n .sort((left, right) => left - right);\n if (sampled.length === 0) return 0;\n const middle = Math.floor(sampled.length / 2);\n const lower = sampled[middle - 1] ?? 0;\n const upper = sampled[middle] ?? 0;\n return sampled.length % 2 === 0 ? (lower + upper) / 2 : upper;\n}\n\nfunction cacheFinding(snapshot: LogMetricsSnapshot): LogMetricsFinding | undefined {\n const { cacheRead, cacheWrite, input } = snapshot.tokens;\n const cacheable = cacheRead + input;\n if (cacheable < MIN_CACHE_SAMPLE_TOKENS) return undefined;\n\n const hitRatio = cacheRead / cacheable;\n if (hitRatio >= LOW_CACHE_HIT_RATIO) return undefined;\n return {\n id: 'low-cache-hit',\n severity: 'warning',\n subject: 'cache',\n detail: `${percent(hitRatio)} hit · ${compact(cacheWrite)} written`,\n action: 'pin stable context first',\n };\n}\n\nfunction expensiveToolFinding(snapshot: LogMetricsSnapshot): LogMetricsFinding | undefined {\n const median = medianTokens(snapshot.toolCost);\n if (median <= 0) return undefined;\n\n const worst = snapshot.toolCost.find(\n (tool) => tool.samples >= MIN_TOOL_SAMPLES && (tool.p90Tokens ?? 0) >= median * EXPENSIVE_TOOL_RATIO,\n );\n if (!worst?.p90Tokens) return undefined;\n return {\n id: 'expensive-tool',\n severity: 'warning',\n subject: worst.name,\n detail: `${compact(worst.p90Tokens)} p90 · ${Math.round(worst.p90Tokens / median)}× median`,\n action: 'narrow what it pulls in',\n };\n}\n\nfunction failingToolFindings(snapshot: LogMetricsSnapshot): LogMetricsFinding[] {\n return snapshot.toolCost\n .filter((tool) => tool.calls >= MIN_TOOL_CALLS && tool.failed / tool.calls >= FAILING_TOOL_RATIO)\n .sort((left, right) => right.failed - left.failed)\n .map((tool) => ({\n id: `failing-tool:${tool.name}`,\n severity: 'critical' as const,\n subject: tool.name,\n detail: `${tool.failed} of ${tool.calls} failed`,\n action: 'retries burn tokens',\n }));\n}\n\nfunction abortedTurnFinding(snapshot: LogMetricsSnapshot): LogMetricsFinding | undefined {\n const redone = snapshot.abortedTurns + snapshot.failedTurns;\n if (redone === 0) return undefined;\n return {\n id: 'redone-turns',\n severity: 'critical',\n subject: 'turns',\n detail: `${snapshot.abortedTurns} aborted · ${snapshot.failedTurns} failed`,\n action: 'paid for, then thrown away',\n };\n}\n\nfunction providerFinding(snapshot: LogMetricsSnapshot): LogMetricsFinding | undefined {\n const category = snapshot.failureCategories.find((entry) => entry.name in SHORT_CATEGORY);\n if (!category) return undefined;\n return {\n id: `provider:${category.name}`,\n severity: 'warning',\n subject: 'provider',\n detail: `${category.count} × ${SHORT_CATEGORY[category.name] ?? category.name}`,\n action: category.name === RATE_LIMITED ? 'back off or drop concurrency' : 'retry or switch provider',\n };\n}\n\nfunction contextFinding(snapshot: LogMetricsSnapshot): LogMetricsFinding | undefined {\n if (snapshot.turns < MIN_TURNS_FOR_AVERAGE) return undefined;\n\n const perTurn = snapshot.tokens.input / snapshot.turns;\n if (perTurn < HEAVY_TURN_INPUT_TOKENS) return undefined;\n return {\n id: 'heavy-context',\n severity: 'warning',\n subject: 'context',\n detail: `${compact(perTurn)} input tok/turn`,\n action: 'compact or split the task',\n };\n}\n\nfunction toolChurnFinding(snapshot: LogMetricsSnapshot): LogMetricsFinding | undefined {\n if (snapshot.turns < MIN_TURNS_FOR_AVERAGE) return undefined;\n\n const perTurn = snapshot.toolCalls / snapshot.turns;\n if (perTurn < BUSY_TURN_TOOL_CALLS) return undefined;\n return {\n id: 'tool-churn',\n severity: 'info',\n subject: 'tools',\n detail: `${perTurn.toFixed(1)} calls/turn`,\n action: 'give it a narrower target',\n };\n}\n\n/**\n * Ranked most-actionable first. Severity leads because a failing tool costs\n * more than an inefficient one, and the panel is read top-down under a\n * height limit.\n */\nexport function deriveFindings(snapshot: LogMetricsSnapshot): LogMetricsFinding[] {\n const findings = [\n ...failingToolFindings(snapshot),\n abortedTurnFinding(snapshot),\n providerFinding(snapshot),\n cacheFinding(snapshot),\n expensiveToolFinding(snapshot),\n contextFinding(snapshot),\n toolChurnFinding(snapshot),\n ].filter((finding): finding is LogMetricsFinding => finding !== undefined);\n\n if (findings.length === 0) return snapshot.events > 0 ? [HEALTHY_FINDING] : [];\n return findings.sort((left, right) => SEVERITY_RANK[left.severity] - SEVERITY_RANK[right.severity]);\n}\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- const e=1.1,t=Math.log(e);function n(e){return typeof e==`number`&&Number.isFinite(e)?e:void 0}function r(e){return typeof e==`string`&&e.length>0?e:void 0}function i(e){return e.replace(/[^a-zA-Z0-9@/._:-]/g,`_`).slice(0,128)||`unknown`}function a(e){let n=Math.ceil(Math.log(Math.max(e,1))/t);return Math.min(Math.max(n,0),159)}function o(t){return e**t}function s(e){if(e.samples===0)return;let t=e.samples*.95,n=[...e.buckets.keys()].sort((e,t)=>e-t),r=0,i=n[0];for(let a of n)if(r+=e.buckets.get(a)??0,i=a,r>=t)break;return Math.round(o(i))}function c(e){return e.startsWith(`pi.`)?e.slice(3):e}var l=class{events=0;errors=0;toolCalls=0;failedToolCalls=0;cost=0;tokens={input:0,output:0,total:0};eventCounts=new Map;packageCounts=new Map;tools=new Map;operations=new Map;sanitizedFailures=0;errorLog=[];now;maxRecentErrors;constructor(e={}){this.now=e.now??Date.now,this.maxRecentErrors=e.maxRecentErrors??8}record(e,t){let n=i(e);this.events+=1,this.eventCounts.set(n,(this.eventCounts.get(n)??0)+1);let a=r(t[`telemetry.package`]);if(a&&!a.startsWith(`/`)&&!a.includes(`\\`)){let e=i(a);this.packageCounts.set(e,(this.packageCounts.get(e)??0)+1)}this.recordOperationDuration(n,t),this.recordSanitizedFailure(n,t),n===`pi.tool_result`?this.recordToolResult(n,t):n===`pi.api_error`?this.recordApiError(n,t):n===`pi.turn.finished`&&this.recordTurnUsage(t)}snapshot(){let e=[...this.tools.entries()].map(([e,t])=>{let n=s(t);return{name:e,calls:t.calls,retainedStateSize:t.buckets.size,...n===void 0?{}:{p95Ms:n}}}).sort((e,t)=>(t.p95Ms??-1)-(e.p95Ms??-1)),t=[...this.eventCounts.entries()].map(([e,t])=>({name:e,count:t})).sort((e,t)=>t.count-e.count),n=[...this.packageCounts.entries()].map(([e,t])=>({name:e,count:t})).sort((e,t)=>t.count-e.count),r=[...this.operations.entries()].map(([e,t])=>{let n=s(t);return{name:e,calls:t.calls,retainedStateSize:t.buckets.size,...n===void 0?{}:{p95Ms:n}}}).sort((e,t)=>(t.p95Ms??-1)-(e.p95Ms??-1));return{events:this.events,errors:this.errors,toolCalls:this.toolCalls,failedToolCalls:this.failedToolCalls,tokens:{...this.tokens},cost:this.cost,toolLatency:e,eventVolume:t,packageEvents:n,sanitizedFailures:this.sanitizedFailures,operationDuration:r,recentErrors:this.errorLog.map(e=>({...e}))}}recordOperationDuration(e,t){let o=Object.entries(t).find(([e,t])=>e!==`tool.duration_ms`&&(e===`duration_ms`||e.endsWith(`.duration_ms`))&&n(t)!==void 0);if(!o)return;let s=i(r(t[`operation.name`])??e),c=this.operations.get(s)??{calls:0,samples:0,buckets:new Map};this.operations.set(s,c),c.calls+=1;let l=n(o[1]);if(l===void 0)return;let u=a(l);c.buckets.set(u,(c.buckets.get(u)??0)+1),c.samples+=1}recordSanitizedFailure(e,t){let n=r(t.outcome);if(t[`tool.result.error`]!==!0&&e!==`pi.api_error`&&n!==`error`&&n!==`failed`&&n!==`failure`||(this.sanitizedFailures+=1,e===`pi.tool_result`||e===`pi.api_error`))return;let a=r(t[`error.code`])??n??`error`;this.pushError(e,`${r(t[`error.type`])??`operation`} failed`,i(a))}recordToolResult(e,t){let i=r(t[`tool.name`])??`unknown`,o=this.tools.get(i)??{calls:0,samples:0,buckets:new Map};this.tools.set(i,o),o.calls+=1,this.toolCalls+=1;let s=n(t[`tool.duration_ms`]);if(s!==void 0){let e=a(s);o.buckets.set(e,(o.buckets.get(e)??0)+1),o.samples+=1}t[`tool.result.error`]===!0&&(this.failedToolCalls+=1,this.pushError(e,`${i}: tool call failed`,`tool`))}recordApiError(e,t){this.errors+=1;let n=String(t[`http.response.status_code`]);this.pushError(e,`provider responded ${n}`,n)}recordTurnUsage(e){this.tokens.input+=n(e[`gen_ai.usage.input_tokens`])??0,this.tokens.output+=n(e[`gen_ai.usage.output_tokens`])??0,this.tokens.total+=n(e[`gen_ai.usage.total_tokens`])??0,this.cost+=n(e[`gen_ai.usage.cost`])??0}pushError(e,t,n){this.errorLog.unshift({at:this.now(),event:c(e),message:t,code:n}),this.errorLog.length>this.maxRecentErrors&&(this.errorLog.length=this.maxRecentErrors)}};exports.API_ERROR_RECORD=`pi.api_error`,exports.LogMetricsAggregator=l,exports.MAX_RETAINED_STATE_PER_TOOL=160,exports.TOOL_RESULT_RECORD=`pi.tool_result`,exports.TURN_FINISHED_RECORD=`pi.turn.finished`;
1
+ const e=`unknown`,t=`api_error`,n=1.1,r=Math.log(n);function i(){return{samples:0,buckets:new Map}}function a(e){return typeof e==`number`&&Number.isFinite(e)?e:void 0}function o(e){return typeof e==`string`&&e.length>0?e:void 0}function s(e){return e.replace(/[^a-zA-Z0-9@/._:-]/g,`_`).slice(0,128)||`unknown`}function c(e,t){let n=Math.ceil(Math.log(Math.max(e,1))/r);return Math.min(Math.max(n,0),t-1)}function l(e){return n**e}function u(e,t,n){let r=c(t,n);e.buckets.set(r,(e.buckets.get(r)??0)+1),e.samples+=1}function d(e,t){if(e.samples===0)return;let n=e.samples*t,r=[...e.buckets.keys()].sort((e,t)=>e-t),i=0,a=r[0];for(let t of r)if(i+=e.buckets.get(t)??0,a=t,i>=n)break;return Math.round(l(a))}function f(e){return d(e,.95)}function p(e){return e===429?`rate_limited`:e>=500?`provider_unavailable`:t}function m(e){return e.startsWith(`pi.`)?e.slice(3):e}var h=class{events=0;errors=0;toolCalls=0;failedToolCalls=0;turns=0;abortedTurns=0;failedTurns=0;cost=0;tokens={input:0,output:0,total:0,cacheRead:0,cacheWrite:0};eventCounts=new Map;packageCounts=new Map;tools=new Map;toolTokens=new Map;toolFailures=new Map;failureCategories=new Map;operations=new Map;sanitizedFailures=0;errorLog=[];now;maxRecentErrors;constructor(e={}){this.now=e.now??Date.now,this.maxRecentErrors=e.maxRecentErrors??8}record(e,t){let n=s(e);this.events+=1,this.eventCounts.set(n,(this.eventCounts.get(n)??0)+1);let r=o(t[`telemetry.package`]);if(r&&!r.startsWith(`/`)&&!r.includes(`\\`)){let e=s(r);this.packageCounts.set(e,(this.packageCounts.get(e)??0)+1)}this.recordOperationDuration(n,t),this.recordSanitizedFailure(n,t),n===`pi.tool_result`?this.recordToolResult(n,t):n===`pi.api_error`?this.recordApiError(n,t):n===`pi.turn.finished`?this.recordTurnUsage(t):n===`pi.turn.failed`?this.recordTurnFailure(t):n===`pi.tool_token_sample`&&this.recordToolTokenSample(t)}snapshot(){let e=[...this.tools.entries()].map(([e,t])=>{let n=f(t);return{name:e,calls:t.calls,retainedStateSize:t.buckets.size,...n===void 0?{}:{p95Ms:n}}}).sort((e,t)=>(t.p95Ms??-1)-(e.p95Ms??-1)),t=[...this.eventCounts.entries()].map(([e,t])=>({name:e,count:t})).sort((e,t)=>t.count-e.count),n=[...this.packageCounts.entries()].map(([e,t])=>({name:e,count:t})).sort((e,t)=>t.count-e.count),r=[...this.operations.entries()].map(([e,t])=>{let n=f(t);return{name:e,calls:t.calls,retainedStateSize:t.buckets.size,...n===void 0?{}:{p95Ms:n}}}).sort((e,t)=>(t.p95Ms??-1)-(e.p95Ms??-1));return{events:this.events,errors:this.errors,toolCalls:this.toolCalls,failedToolCalls:this.failedToolCalls,turns:this.turns,abortedTurns:this.abortedTurns,failedTurns:this.failedTurns,tokens:{...this.tokens},cost:this.cost,toolLatency:e,toolCost:this.toolCostRows(),failureCategories:[...this.failureCategories.entries()].map(([e,t])=>({name:e,count:t})).sort((e,t)=>t.count-e.count),eventVolume:t,packageEvents:n,sanitizedFailures:this.sanitizedFailures,operationDuration:r,recentErrors:this.errorLog.map(e=>({...e}))}}toolCostRows(){return[...new Set([...this.tools.keys(),...this.toolTokens.keys()])].map(e=>{let t=this.toolTokens.get(e),n=t?d(t,.9):void 0;return{name:e,calls:this.tools.get(e)?.calls??0,failed:this.toolFailures.get(e)??0,samples:t?.samples??0,...n===void 0?{}:{p90Tokens:n}}}).sort((e,t)=>(t.p90Tokens??-1)-(e.p90Tokens??-1))}countCategory(e){this.failureCategories.set(e,(this.failureCategories.get(e)??0)+1)}recordOperationDuration(e,t){let n=Object.entries(t).find(([e,t])=>e!==`tool.duration_ms`&&(e===`duration_ms`||e.endsWith(`.duration_ms`))&&a(t)!==void 0);if(!n)return;let r=s(o(t[`operation.name`])??e),i=this.operations.get(r)??{calls:0,samples:0,buckets:new Map};this.operations.set(r,i),i.calls+=1;let c=a(n[1]);c!==void 0&&u(i,c,160)}recordSanitizedFailure(e,t){let n=o(t.outcome);if(t[`tool.result.error`]!==!0&&e!==`pi.api_error`&&n!==`error`&&n!==`failed`&&n!==`failure`||(this.sanitizedFailures+=1,e===`pi.tool_result`||e===`pi.api_error`))return;e!==`pi.turn.failed`&&this.countCategory(`log_error`);let r=o(t[`error.code`])??n??`error`;this.pushError(e,`${o(t[`error.type`])??`operation`} failed`,s(r))}recordToolResult(t,n){let r=o(n[`tool.name`])??e,i=this.tools.get(r)??{calls:0,samples:0,buckets:new Map};this.tools.set(r,i),i.calls+=1,this.toolCalls+=1;let s=a(n[`tool.duration_ms`]);s!==void 0&&u(i,s,160),n[`tool.result.error`]===!0&&(this.failedToolCalls+=1,this.toolFailures.set(r,(this.toolFailures.get(r)??0)+1),this.countCategory(`tool_failure`),this.pushError(t,`${r}: tool call failed`,`tool`))}recordToolTokenSample(t){let n=a(t[`gen_ai.usage.total_tokens`]);if(n===void 0||n<=0)return;let r=o(t[`tool.name`])??e,s=this.toolTokens.get(r)??i();this.toolTokens.set(r,s),u(s,n,200)}recordApiError(e,n){this.errors+=1;let r=a(n[`http.response.status_code`]),i=String(n[`http.response.status_code`]);this.countCategory(r===void 0?t:p(r)),this.pushError(e,`provider responded ${i}`,i)}recordTurnUsage(e){this.turns+=1,this.tokens.input+=a(e[`gen_ai.usage.input_tokens`])??0,this.tokens.output+=a(e[`gen_ai.usage.output_tokens`])??0,this.tokens.total+=a(e[`gen_ai.usage.total_tokens`])??0,this.tokens.cacheRead+=a(e[`gen_ai.usage.cache_read_tokens`])??0,this.tokens.cacheWrite+=a(e[`gen_ai.usage.cache_write_tokens`])??0,this.cost+=a(e[`gen_ai.usage.cost`])??0}recordTurnFailure(e){let t=o(e.outcome)===`aborted`;t?this.abortedTurns+=1:this.failedTurns+=1,this.countCategory(t?`aborted`:`turn_failed`)}pushError(e,t,n){this.errorLog.unshift({at:this.now(),event:m(e),message:t,code:n}),this.errorLog.length>this.maxRecentErrors&&(this.errorLog.length=this.maxRecentErrors)}};exports.API_ERROR_RECORD=`pi.api_error`,exports.LogMetricsAggregator=h,exports.MAX_RETAINED_STATE_PER_TOOL=160,exports.MAX_RETAINED_TOKEN_BUCKETS=200,exports.TOOL_RESULT_RECORD=`pi.tool_result`,exports.TOOL_TOKEN_SAMPLE_RECORD=`pi.tool_token_sample`,exports.TURN_FAILED_RECORD=`pi.turn.failed`,exports.TURN_FINISHED_RECORD=`pi.turn.finished`;
2
2
  //# sourceMappingURL=metrics.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"metrics.cjs","names":[],"sources":["../../src/services/metrics.ts"],"sourcesContent":["/**\n * Session-local aggregation of the log records the Pi telemetry extension\n * already emits.\n *\n * The aggregator is fed `(recordName, attributes)` pairs rather than Pi events\n * so it can only ever see what the sink sees: no extra instrumentation is added\n * for metrics, and nothing here depends on a live sink. Everything is scoped to\n * the current process, so there is no persistence and no query API.\n */\n\n/** Record names the aggregator derives numbers from. */\nexport const TOOL_RESULT_RECORD = 'pi.tool_result';\nexport const API_ERROR_RECORD = 'pi.api_error';\nexport const TURN_FINISHED_RECORD = 'pi.turn.finished';\n\nconst UNKNOWN_TOOL = 'unknown';\nconst TOOL_ERROR_CODE = 'tool';\nconst RECORD_PREFIX = 'pi.';\nconst DEFAULT_MAX_RECENT_ERRORS = 8;\nconst P95_QUANTILE = 0.95;\n\n/**\n * Latency is kept as a fixed logarithmic histogram rather than a sample array,\n * so a long session cannot grow the retained state without bound. Each bucket\n * is 10% wider than the last, which keeps the reported p95 within about 10% of\n * the true value while capping the state at one small map per tool.\n */\nconst LATENCY_BUCKET_GROWTH = 1.1;\nconst LATENCY_BUCKET_LOG_BASE = Math.log(LATENCY_BUCKET_GROWTH);\nconst MINIMUM_SAMPLE_MS = 1;\n\n/** Bucket ceiling per tool: covers 1ms to roughly an hour at 10% resolution. */\nexport const MAX_RETAINED_STATE_PER_TOOL = 160;\n\nexport interface LogMetricsTokenTotals {\n input: number;\n output: number;\n total: number;\n}\n\nexport interface LogMetricsToolLatency {\n name: string;\n calls: number;\n p95Ms?: number;\n /** Buckets currently held for this tool, bounded by MAX_RETAINED_STATE_PER_TOOL. */\n retainedStateSize: number;\n}\n\nexport interface LogMetricsEventCount {\n name: string;\n count: number;\n}\n\nexport interface LogMetricsOperationDuration {\n name: string;\n calls: number;\n p95Ms?: number;\n retainedStateSize: number;\n}\n\nexport interface LogMetricsError {\n at: number;\n event: string;\n message: string;\n code: string;\n}\n\nexport interface LogMetricsSnapshot {\n events: number;\n errors: number;\n toolCalls: number;\n failedToolCalls: number;\n tokens: LogMetricsTokenTotals;\n cost: number;\n toolLatency: LogMetricsToolLatency[];\n eventVolume: LogMetricsEventCount[];\n packageEvents: LogMetricsEventCount[];\n sanitizedFailures: number;\n operationDuration: LogMetricsOperationDuration[];\n recentErrors: LogMetricsError[];\n}\n\nexport interface LogMetricsAggregatorOptions {\n now?: () => number;\n maxRecentErrors?: number;\n}\n\n/** The slice of the aggregator the telemetry extension needs. */\nexport interface LogMetricsRecorder {\n record(name: string, attributes: Record<string, unknown>): void;\n}\n\ninterface ToolLatencyState {\n calls: number;\n samples: number;\n buckets: Map<number, number>;\n}\n\nfunction readNumber(value: unknown): number | undefined {\n return typeof value === 'number' && Number.isFinite(value) ? value : undefined;\n}\n\nfunction readString(value: unknown): string | undefined {\n return typeof value === 'string' && value.length > 0 ? value : undefined;\n}\n\nfunction safeMetricName(value: string): string {\n const normalized = value.replace(/[^a-zA-Z0-9@/._:-]/g, '_').slice(0, 128);\n return normalized || 'unknown';\n}\n\nfunction latencyBucket(durationMs: number): number {\n const index = Math.ceil(Math.log(Math.max(durationMs, MINIMUM_SAMPLE_MS)) / LATENCY_BUCKET_LOG_BASE);\n return Math.min(Math.max(index, 0), MAX_RETAINED_STATE_PER_TOOL - 1);\n}\n\nfunction bucketUpperBoundMs(index: number): number {\n return LATENCY_BUCKET_GROWTH ** index;\n}\n\n/**\n * The upper edge of the first bucket whose cumulative count crosses the\n * quantile. Reporting the edge rather than a bucket midpoint keeps the answer\n * an upper bound, so a single 250ms call never reports faster than 250ms.\n */\nfunction bucketedP95(state: ToolLatencyState): number | undefined {\n if (state.samples === 0) return undefined;\n\n const target = state.samples * P95_QUANTILE;\n const indexes = [...state.buckets.keys()].sort((left, right) => left - right);\n let cumulative = 0;\n let selected = indexes[0] as number;\n for (const index of indexes) {\n cumulative += state.buckets.get(index) ?? 0;\n selected = index;\n if (cumulative >= target) break;\n }\n return Math.round(bucketUpperBoundMs(selected));\n}\n\n/** Strips the `pi.` prefix so an error entry reads as the event it came from. */\nfunction errorEventName(recordName: string): string {\n return recordName.startsWith(RECORD_PREFIX) ? recordName.slice(RECORD_PREFIX.length) : recordName;\n}\n\nexport class LogMetricsAggregator implements LogMetricsRecorder {\n private events = 0;\n private errors = 0;\n private toolCalls = 0;\n private failedToolCalls = 0;\n private cost = 0;\n private readonly tokens: LogMetricsTokenTotals = { input: 0, output: 0, total: 0 };\n private readonly eventCounts = new Map<string, number>();\n private readonly packageCounts = new Map<string, number>();\n private readonly tools = new Map<string, ToolLatencyState>();\n private readonly operations = new Map<string, ToolLatencyState>();\n private sanitizedFailures = 0;\n private readonly errorLog: LogMetricsError[] = [];\n private readonly now: () => number;\n private readonly maxRecentErrors: number;\n\n constructor(options: LogMetricsAggregatorOptions = {}) {\n this.now = options.now ?? Date.now;\n this.maxRecentErrors = options.maxRecentErrors ?? DEFAULT_MAX_RECENT_ERRORS;\n }\n\n record(name: string, attributes: Record<string, unknown>): void {\n const metricName = safeMetricName(name);\n this.events += 1;\n this.eventCounts.set(metricName, (this.eventCounts.get(metricName) ?? 0) + 1);\n const packageName = readString(attributes['telemetry.package']);\n if (packageName && !packageName.startsWith('/') && !packageName.includes('\\\\')) {\n const safePackageName = safeMetricName(packageName);\n this.packageCounts.set(safePackageName, (this.packageCounts.get(safePackageName) ?? 0) + 1);\n }\n this.recordOperationDuration(metricName, attributes);\n this.recordSanitizedFailure(metricName, attributes);\n\n if (metricName === TOOL_RESULT_RECORD) this.recordToolResult(metricName, attributes);\n else if (metricName === API_ERROR_RECORD) this.recordApiError(metricName, attributes);\n else if (metricName === TURN_FINISHED_RECORD) this.recordTurnUsage(attributes);\n }\n\n snapshot(): LogMetricsSnapshot {\n const toolLatency = [...this.tools.entries()]\n .map(([name, state]) => {\n const p95Ms = bucketedP95(state);\n return {\n name,\n calls: state.calls,\n retainedStateSize: state.buckets.size,\n ...(p95Ms === undefined ? {} : { p95Ms }),\n };\n })\n .sort((left, right) => (right.p95Ms ?? -1) - (left.p95Ms ?? -1));\n\n const eventVolume = [...this.eventCounts.entries()]\n .map(([name, count]) => ({ name, count }))\n .sort((left, right) => right.count - left.count);\n const packageEvents = [...this.packageCounts.entries()]\n .map(([name, count]) => ({ name, count }))\n .sort((left, right) => right.count - left.count);\n const operationDuration = [...this.operations.entries()]\n .map(([name, state]) => {\n const p95Ms = bucketedP95(state);\n return {\n name,\n calls: state.calls,\n retainedStateSize: state.buckets.size,\n ...(p95Ms === undefined ? {} : { p95Ms }),\n };\n })\n .sort((left, right) => (right.p95Ms ?? -1) - (left.p95Ms ?? -1));\n\n return {\n events: this.events,\n errors: this.errors,\n toolCalls: this.toolCalls,\n failedToolCalls: this.failedToolCalls,\n tokens: { ...this.tokens },\n cost: this.cost,\n toolLatency,\n eventVolume,\n packageEvents,\n sanitizedFailures: this.sanitizedFailures,\n operationDuration,\n recentErrors: this.errorLog.map((entry) => ({ ...entry })),\n };\n }\n\n private recordOperationDuration(recordName: string, attributes: Record<string, unknown>): void {\n const duration = Object.entries(attributes).find(\n ([key, value]) =>\n key !== 'tool.duration_ms' &&\n (key === 'duration_ms' || key.endsWith('.duration_ms')) &&\n readNumber(value) !== undefined,\n );\n if (!duration) return;\n const operationName = safeMetricName(readString(attributes['operation.name']) ?? recordName);\n const state = this.operations.get(operationName) ?? { calls: 0, samples: 0, buckets: new Map<number, number>() };\n this.operations.set(operationName, state);\n state.calls += 1;\n const durationMs = readNumber(duration[1]);\n if (durationMs === undefined) return;\n const bucket = latencyBucket(durationMs);\n state.buckets.set(bucket, (state.buckets.get(bucket) ?? 0) + 1);\n state.samples += 1;\n }\n\n private recordSanitizedFailure(recordName: string, attributes: Record<string, unknown>): void {\n const outcome = readString(attributes.outcome);\n const failed =\n attributes['tool.result.error'] === true ||\n recordName === API_ERROR_RECORD ||\n outcome === 'error' ||\n outcome === 'failed' ||\n outcome === 'failure';\n if (!failed) return;\n this.sanitizedFailures += 1;\n if (recordName === TOOL_RESULT_RECORD || recordName === API_ERROR_RECORD) return;\n const code = readString(attributes['error.code']) ?? outcome ?? 'error';\n this.pushError(recordName, `${readString(attributes['error.type']) ?? 'operation'} failed`, safeMetricName(code));\n }\n\n private recordToolResult(recordName: string, attributes: Record<string, unknown>): void {\n const toolName = readString(attributes['tool.name']) ?? UNKNOWN_TOOL;\n const state = this.tools.get(toolName) ?? { calls: 0, samples: 0, buckets: new Map<number, number>() };\n this.tools.set(toolName, state);\n state.calls += 1;\n this.toolCalls += 1;\n\n // A call whose start was never seen carries no duration, so it counts as a\n // call but contributes no latency sample.\n const durationMs = readNumber(attributes['tool.duration_ms']);\n if (durationMs !== undefined) {\n const bucket = latencyBucket(durationMs);\n state.buckets.set(bucket, (state.buckets.get(bucket) ?? 0) + 1);\n state.samples += 1;\n }\n\n if (attributes['tool.result.error'] === true) {\n this.failedToolCalls += 1;\n // Redaction is on by default, so the tool's own output is unavailable and\n // the tool name is the only detail this entry can carry.\n this.pushError(recordName, `${toolName}: tool call failed`, TOOL_ERROR_CODE);\n }\n }\n\n private recordApiError(recordName: string, attributes: Record<string, unknown>): void {\n this.errors += 1;\n const status = String(attributes['http.response.status_code']);\n this.pushError(recordName, `provider responded ${status}`, status);\n }\n\n private recordTurnUsage(attributes: Record<string, unknown>): void {\n this.tokens.input += readNumber(attributes['gen_ai.usage.input_tokens']) ?? 0;\n this.tokens.output += readNumber(attributes['gen_ai.usage.output_tokens']) ?? 0;\n this.tokens.total += readNumber(attributes['gen_ai.usage.total_tokens']) ?? 0;\n this.cost += readNumber(attributes['gen_ai.usage.cost']) ?? 0;\n }\n\n private pushError(recordName: string, message: string, code: string): void {\n this.errorLog.unshift({ at: this.now(), event: errorEventName(recordName), message, code });\n if (this.errorLog.length > this.maxRecentErrors) this.errorLog.length = this.maxRecentErrors;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"metrics.cjs","names":[],"sources":["../../src/services/metrics.ts"],"sourcesContent":["/**\n * Session-local aggregation of the log records the Pi telemetry extension\n * already emits.\n *\n * The aggregator is fed `(recordName, attributes)` pairs rather than Pi events\n * so it can only ever see what the sink sees: no extra instrumentation is added\n * for metrics, and nothing here depends on a live sink. Everything is scoped to\n * the current process, so there is no persistence and no query API.\n */\n\n/** Record names the aggregator derives numbers from. */\nexport const TOOL_RESULT_RECORD = 'pi.tool_result';\nexport const API_ERROR_RECORD = 'pi.api_error';\nexport const TURN_FINISHED_RECORD = 'pi.turn.finished';\nexport const TURN_FAILED_RECORD = 'pi.turn.failed';\nexport const TOOL_TOKEN_SAMPLE_RECORD = 'pi.tool_token_sample';\n\nconst UNKNOWN_TOOL = 'unknown';\nconst TOOL_ERROR_CODE = 'tool';\nconst RECORD_PREFIX = 'pi.';\nconst DEFAULT_MAX_RECENT_ERRORS = 8;\nconst P95_QUANTILE = 0.95;\nconst P90_QUANTILE = 0.9;\nconst ABORTED_OUTCOME = 'aborted';\n\n/**\n * Failure taxonomy, mirroring log-sink's AgentIssueCategory closely enough that\n * a session-local category reads the same as the one the sink would report for\n * the same record. Rate limiting and provider outages are split out because\n * they call for different action: back off versus retry elsewhere.\n */\nconst RATE_LIMIT_STATUS = 429;\nconst SERVER_ERROR_STATUS = 500;\nconst CATEGORY_TOOL_FAILURE = 'tool_failure';\nconst CATEGORY_RATE_LIMITED = 'rate_limited';\nconst CATEGORY_PROVIDER_UNAVAILABLE = 'provider_unavailable';\nconst CATEGORY_API_ERROR = 'api_error';\nconst CATEGORY_ABORTED = 'aborted';\nconst CATEGORY_TURN_FAILED = 'turn_failed';\nconst CATEGORY_LOG_ERROR = 'log_error';\n\n/**\n * Latency is kept as a fixed logarithmic histogram rather than a sample array,\n * so a long session cannot grow the retained state without bound. Each bucket\n * is 10% wider than the last, which keeps the reported p95 within about 10% of\n * the true value while capping the state at one small map per tool.\n */\nconst LATENCY_BUCKET_GROWTH = 1.1;\nconst LATENCY_BUCKET_LOG_BASE = Math.log(LATENCY_BUCKET_GROWTH);\nconst MINIMUM_SAMPLE_MS = 1;\n\n/** Bucket ceiling per tool: covers 1ms to roughly an hour at 10% resolution. */\nexport const MAX_RETAINED_STATE_PER_TOOL = 160;\n\n/**\n * Token distributions reuse the same histogram at a higher ceiling: a turn's\n * total can run to millions once cache reads are counted, which the latency\n * ceiling would saturate and report as a flat maximum.\n */\nexport const MAX_RETAINED_TOKEN_BUCKETS = 200;\n\nexport interface LogMetricsTokenTotals {\n input: number;\n output: number;\n total: number;\n /**\n * Cached prompt tokens billed at the read rate. Held separately from `input`\n * because the ratio between them is the session's cache hit rate, which is\n * the single largest cost lever a user controls.\n */\n cacheRead: number;\n cacheWrite: number;\n}\n\nexport interface LogMetricsToolLatency {\n name: string;\n calls: number;\n p95Ms?: number;\n /** Buckets currently held for this tool, bounded by MAX_RETAINED_STATE_PER_TOOL. */\n retainedStateSize: number;\n}\n\nexport interface LogMetricsEventCount {\n name: string;\n count: number;\n}\n\nexport interface LogMetricsOperationDuration {\n name: string;\n calls: number;\n p95Ms?: number;\n retainedStateSize: number;\n}\n\nexport interface LogMetricsError {\n at: number;\n event: string;\n message: string;\n code: string;\n}\n\n/**\n * What a tool costs, rather than how long it takes.\n *\n * `p90Tokens` is the 90th percentile of the *turn* total for turns this tool\n * took part in, which is the only attribution the Pi extension emits\n * (`token_attribution: 'toolCallTurn'`). It answers \"when this tool runs, how\n * expensive is the turn\", not \"this tool consumed N tokens\" — a turn with\n * several tool calls attributes its total to each of them.\n */\nexport interface LogMetricsToolCost {\n name: string;\n calls: number;\n failed: number;\n /** Turns carrying usage that this tool took part in, i.e. the p90 sample count. */\n samples: number;\n p90Tokens?: number;\n}\n\nexport interface LogMetricsSnapshot {\n events: number;\n errors: number;\n toolCalls: number;\n failedToolCalls: number;\n /** Turns that reported usage, i.e. the denominator for per-turn averages. */\n turns: number;\n abortedTurns: number;\n failedTurns: number;\n tokens: LogMetricsTokenTotals;\n cost: number;\n toolLatency: LogMetricsToolLatency[];\n toolCost: LogMetricsToolCost[];\n failureCategories: LogMetricsEventCount[];\n eventVolume: LogMetricsEventCount[];\n packageEvents: LogMetricsEventCount[];\n sanitizedFailures: number;\n operationDuration: LogMetricsOperationDuration[];\n recentErrors: LogMetricsError[];\n}\n\nexport interface LogMetricsAggregatorOptions {\n now?: () => number;\n maxRecentErrors?: number;\n}\n\n/** The slice of the aggregator the telemetry extension needs. */\nexport interface LogMetricsRecorder {\n record(name: string, attributes: Record<string, unknown>): void;\n}\n\ninterface ToolLatencyState {\n calls: number;\n samples: number;\n buckets: Map<number, number>;\n}\n\ninterface SampleState {\n samples: number;\n buckets: Map<number, number>;\n}\n\nfunction emptySampleState(): SampleState {\n return { samples: 0, buckets: new Map<number, number>() };\n}\n\nfunction readNumber(value: unknown): number | undefined {\n return typeof value === 'number' && Number.isFinite(value) ? value : undefined;\n}\n\nfunction readString(value: unknown): string | undefined {\n return typeof value === 'string' && value.length > 0 ? value : undefined;\n}\n\nfunction safeMetricName(value: string): string {\n const normalized = value.replace(/[^a-zA-Z0-9@/._:-]/g, '_').slice(0, 128);\n return normalized || 'unknown';\n}\n\nfunction bucketIndex(value: number, cap: number): number {\n const index = Math.ceil(Math.log(Math.max(value, MINIMUM_SAMPLE_MS)) / LATENCY_BUCKET_LOG_BASE);\n return Math.min(Math.max(index, 0), cap - 1);\n}\n\nfunction bucketUpperBound(index: number): number {\n return LATENCY_BUCKET_GROWTH ** index;\n}\n\nfunction observe(state: SampleState, value: number, cap: number): void {\n const bucket = bucketIndex(value, cap);\n state.buckets.set(bucket, (state.buckets.get(bucket) ?? 0) + 1);\n state.samples += 1;\n}\n\n/**\n * The upper edge of the first bucket whose cumulative count crosses the\n * quantile. Reporting the edge rather than a bucket midpoint keeps the answer\n * an upper bound, so a single 250ms call never reports faster than 250ms.\n */\nfunction bucketedQuantile(state: SampleState, quantile: number): number | undefined {\n if (state.samples === 0) return undefined;\n\n const target = state.samples * quantile;\n const indexes = [...state.buckets.keys()].sort((left, right) => left - right);\n let cumulative = 0;\n let selected = indexes[0] as number;\n for (const index of indexes) {\n cumulative += state.buckets.get(index) ?? 0;\n selected = index;\n if (cumulative >= target) break;\n }\n return Math.round(bucketUpperBound(selected));\n}\n\nfunction bucketedP95(state: SampleState): number | undefined {\n return bucketedQuantile(state, P95_QUANTILE);\n}\n\n/** Categorises an HTTP failure so the action it implies is distinguishable. */\nfunction apiErrorCategory(status: number): string {\n if (status === RATE_LIMIT_STATUS) return CATEGORY_RATE_LIMITED;\n if (status >= SERVER_ERROR_STATUS) return CATEGORY_PROVIDER_UNAVAILABLE;\n return CATEGORY_API_ERROR;\n}\n\n/** Strips the `pi.` prefix so an error entry reads as the event it came from. */\nfunction errorEventName(recordName: string): string {\n return recordName.startsWith(RECORD_PREFIX) ? recordName.slice(RECORD_PREFIX.length) : recordName;\n}\n\nexport class LogMetricsAggregator implements LogMetricsRecorder {\n private events = 0;\n private errors = 0;\n private toolCalls = 0;\n private failedToolCalls = 0;\n private turns = 0;\n private abortedTurns = 0;\n private failedTurns = 0;\n private cost = 0;\n private readonly tokens: LogMetricsTokenTotals = { input: 0, output: 0, total: 0, cacheRead: 0, cacheWrite: 0 };\n private readonly eventCounts = new Map<string, number>();\n private readonly packageCounts = new Map<string, number>();\n private readonly tools = new Map<string, ToolLatencyState>();\n private readonly toolTokens = new Map<string, SampleState>();\n private readonly toolFailures = new Map<string, number>();\n private readonly failureCategories = new Map<string, number>();\n private readonly operations = new Map<string, ToolLatencyState>();\n private sanitizedFailures = 0;\n private readonly errorLog: LogMetricsError[] = [];\n private readonly now: () => number;\n private readonly maxRecentErrors: number;\n\n constructor(options: LogMetricsAggregatorOptions = {}) {\n this.now = options.now ?? Date.now;\n this.maxRecentErrors = options.maxRecentErrors ?? DEFAULT_MAX_RECENT_ERRORS;\n }\n\n record(name: string, attributes: Record<string, unknown>): void {\n const metricName = safeMetricName(name);\n this.events += 1;\n this.eventCounts.set(metricName, (this.eventCounts.get(metricName) ?? 0) + 1);\n const packageName = readString(attributes['telemetry.package']);\n if (packageName && !packageName.startsWith('/') && !packageName.includes('\\\\')) {\n const safePackageName = safeMetricName(packageName);\n this.packageCounts.set(safePackageName, (this.packageCounts.get(safePackageName) ?? 0) + 1);\n }\n this.recordOperationDuration(metricName, attributes);\n this.recordSanitizedFailure(metricName, attributes);\n\n if (metricName === TOOL_RESULT_RECORD) this.recordToolResult(metricName, attributes);\n else if (metricName === API_ERROR_RECORD) this.recordApiError(metricName, attributes);\n else if (metricName === TURN_FINISHED_RECORD) this.recordTurnUsage(attributes);\n else if (metricName === TURN_FAILED_RECORD) this.recordTurnFailure(attributes);\n else if (metricName === TOOL_TOKEN_SAMPLE_RECORD) this.recordToolTokenSample(attributes);\n }\n\n snapshot(): LogMetricsSnapshot {\n const toolLatency = [...this.tools.entries()]\n .map(([name, state]) => {\n const p95Ms = bucketedP95(state);\n return {\n name,\n calls: state.calls,\n retainedStateSize: state.buckets.size,\n ...(p95Ms === undefined ? {} : { p95Ms }),\n };\n })\n .sort((left, right) => (right.p95Ms ?? -1) - (left.p95Ms ?? -1));\n\n const eventVolume = [...this.eventCounts.entries()]\n .map(([name, count]) => ({ name, count }))\n .sort((left, right) => right.count - left.count);\n const packageEvents = [...this.packageCounts.entries()]\n .map(([name, count]) => ({ name, count }))\n .sort((left, right) => right.count - left.count);\n const operationDuration = [...this.operations.entries()]\n .map(([name, state]) => {\n const p95Ms = bucketedP95(state);\n return {\n name,\n calls: state.calls,\n retainedStateSize: state.buckets.size,\n ...(p95Ms === undefined ? {} : { p95Ms }),\n };\n })\n .sort((left, right) => (right.p95Ms ?? -1) - (left.p95Ms ?? -1));\n\n return {\n events: this.events,\n errors: this.errors,\n toolCalls: this.toolCalls,\n failedToolCalls: this.failedToolCalls,\n turns: this.turns,\n abortedTurns: this.abortedTurns,\n failedTurns: this.failedTurns,\n tokens: { ...this.tokens },\n cost: this.cost,\n toolLatency,\n toolCost: this.toolCostRows(),\n failureCategories: [...this.failureCategories.entries()]\n .map(([name, count]) => ({ name, count }))\n .sort((left, right) => right.count - left.count),\n eventVolume,\n packageEvents,\n sanitizedFailures: this.sanitizedFailures,\n operationDuration,\n recentErrors: this.errorLog.map((entry) => ({ ...entry })),\n };\n }\n\n /**\n * Ranked by cost per call rather than call count: a tool invoked twice into a\n * 300k-token turn is the one worth changing, and a frequency ranking buries\n * it under the cheap tools that dominate any transcript.\n */\n private toolCostRows(): LogMetricsToolCost[] {\n const names = new Set([...this.tools.keys(), ...this.toolTokens.keys()]);\n return [...names]\n .map((name) => {\n const tokens = this.toolTokens.get(name);\n const p90Tokens = tokens ? bucketedQuantile(tokens, P90_QUANTILE) : undefined;\n return {\n name,\n calls: this.tools.get(name)?.calls ?? 0,\n failed: this.toolFailures.get(name) ?? 0,\n samples: tokens?.samples ?? 0,\n ...(p90Tokens === undefined ? {} : { p90Tokens }),\n };\n })\n .sort((left, right) => (right.p90Tokens ?? -1) - (left.p90Tokens ?? -1));\n }\n\n private countCategory(category: string): void {\n this.failureCategories.set(category, (this.failureCategories.get(category) ?? 0) + 1);\n }\n\n private recordOperationDuration(recordName: string, attributes: Record<string, unknown>): void {\n const duration = Object.entries(attributes).find(\n ([key, value]) =>\n key !== 'tool.duration_ms' &&\n (key === 'duration_ms' || key.endsWith('.duration_ms')) &&\n readNumber(value) !== undefined,\n );\n if (!duration) return;\n const operationName = safeMetricName(readString(attributes['operation.name']) ?? recordName);\n const state = this.operations.get(operationName) ?? { calls: 0, samples: 0, buckets: new Map<number, number>() };\n this.operations.set(operationName, state);\n state.calls += 1;\n const durationMs = readNumber(duration[1]);\n if (durationMs === undefined) return;\n observe(state, durationMs, MAX_RETAINED_STATE_PER_TOOL);\n }\n\n private recordSanitizedFailure(recordName: string, attributes: Record<string, unknown>): void {\n const outcome = readString(attributes.outcome);\n const failed =\n attributes['tool.result.error'] === true ||\n recordName === API_ERROR_RECORD ||\n outcome === 'error' ||\n outcome === 'failed' ||\n outcome === 'failure';\n if (!failed) return;\n this.sanitizedFailures += 1;\n if (recordName === TOOL_RESULT_RECORD || recordName === API_ERROR_RECORD) return;\n // Turn failures carry their own category, applied by recordTurnFailure.\n if (recordName !== TURN_FAILED_RECORD) this.countCategory(CATEGORY_LOG_ERROR);\n const code = readString(attributes['error.code']) ?? outcome ?? 'error';\n this.pushError(recordName, `${readString(attributes['error.type']) ?? 'operation'} failed`, safeMetricName(code));\n }\n\n private recordToolResult(recordName: string, attributes: Record<string, unknown>): void {\n const toolName = readString(attributes['tool.name']) ?? UNKNOWN_TOOL;\n const state = this.tools.get(toolName) ?? { calls: 0, samples: 0, buckets: new Map<number, number>() };\n this.tools.set(toolName, state);\n state.calls += 1;\n this.toolCalls += 1;\n\n // A call whose start was never seen carries no duration, so it counts as a\n // call but contributes no latency sample.\n const durationMs = readNumber(attributes['tool.duration_ms']);\n if (durationMs !== undefined) observe(state, durationMs, MAX_RETAINED_STATE_PER_TOOL);\n\n if (attributes['tool.result.error'] === true) {\n this.failedToolCalls += 1;\n this.toolFailures.set(toolName, (this.toolFailures.get(toolName) ?? 0) + 1);\n this.countCategory(CATEGORY_TOOL_FAILURE);\n // Redaction is on by default, so the tool's own output is unavailable and\n // the tool name is the only detail this entry can carry.\n this.pushError(recordName, `${toolName}: tool call failed`, TOOL_ERROR_CODE);\n }\n }\n\n /**\n * Attributes a turn's token total to every tool that ran in it. The Pi\n * extension emits one sample per tool result carrying the whole turn's usage,\n * so this builds a per-tool distribution of \"how expensive is a turn that\n * uses this tool\" — see LogMetricsToolCost for what that does and does not\n * claim.\n */\n private recordToolTokenSample(attributes: Record<string, unknown>): void {\n const totalTokens = readNumber(attributes['gen_ai.usage.total_tokens']);\n if (totalTokens === undefined || totalTokens <= 0) return;\n const toolName = readString(attributes['tool.name']) ?? UNKNOWN_TOOL;\n const state = this.toolTokens.get(toolName) ?? emptySampleState();\n this.toolTokens.set(toolName, state);\n observe(state, totalTokens, MAX_RETAINED_TOKEN_BUCKETS);\n }\n\n private recordApiError(recordName: string, attributes: Record<string, unknown>): void {\n this.errors += 1;\n const statusCode = readNumber(attributes['http.response.status_code']);\n const status = String(attributes['http.response.status_code']);\n this.countCategory(statusCode === undefined ? CATEGORY_API_ERROR : apiErrorCategory(statusCode));\n this.pushError(recordName, `provider responded ${status}`, status);\n }\n\n private recordTurnUsage(attributes: Record<string, unknown>): void {\n this.turns += 1;\n this.tokens.input += readNumber(attributes['gen_ai.usage.input_tokens']) ?? 0;\n this.tokens.output += readNumber(attributes['gen_ai.usage.output_tokens']) ?? 0;\n this.tokens.total += readNumber(attributes['gen_ai.usage.total_tokens']) ?? 0;\n this.tokens.cacheRead += readNumber(attributes['gen_ai.usage.cache_read_tokens']) ?? 0;\n this.tokens.cacheWrite += readNumber(attributes['gen_ai.usage.cache_write_tokens']) ?? 0;\n this.cost += readNumber(attributes['gen_ai.usage.cost']) ?? 0;\n }\n\n /** A turn the user has to redo: the most expensive failure mode there is. */\n private recordTurnFailure(attributes: Record<string, unknown>): void {\n const aborted = readString(attributes.outcome) === ABORTED_OUTCOME;\n if (aborted) this.abortedTurns += 1;\n else this.failedTurns += 1;\n this.countCategory(aborted ? CATEGORY_ABORTED : CATEGORY_TURN_FAILED);\n }\n\n private pushError(recordName: string, message: string, code: string): void {\n this.errorLog.unshift({ at: this.now(), event: errorEventName(recordName), message, code });\n if (this.errorLog.length > this.maxRecentErrors) this.errorLog.length = this.maxRecentErrors;\n }\n}\n"],"mappings":"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"}
@@ -12,12 +12,27 @@
12
12
  declare const TOOL_RESULT_RECORD = "pi.tool_result";
13
13
  declare const API_ERROR_RECORD = "pi.api_error";
14
14
  declare const TURN_FINISHED_RECORD = "pi.turn.finished";
15
+ declare const TURN_FAILED_RECORD = "pi.turn.failed";
16
+ declare const TOOL_TOKEN_SAMPLE_RECORD = "pi.tool_token_sample";
15
17
  /** Bucket ceiling per tool: covers 1ms to roughly an hour at 10% resolution. */
16
18
  declare const MAX_RETAINED_STATE_PER_TOOL = 160;
19
+ /**
20
+ * Token distributions reuse the same histogram at a higher ceiling: a turn's
21
+ * total can run to millions once cache reads are counted, which the latency
22
+ * ceiling would saturate and report as a flat maximum.
23
+ */
24
+ declare const MAX_RETAINED_TOKEN_BUCKETS = 200;
17
25
  interface LogMetricsTokenTotals {
18
26
  input: number;
19
27
  output: number;
20
28
  total: number;
29
+ /**
30
+ * Cached prompt tokens billed at the read rate. Held separately from `input`
31
+ * because the ratio between them is the session's cache hit rate, which is
32
+ * the single largest cost lever a user controls.
33
+ */
34
+ cacheRead: number;
35
+ cacheWrite: number;
21
36
  }
22
37
  interface LogMetricsToolLatency {
23
38
  name: string;
@@ -42,14 +57,37 @@ interface LogMetricsError {
42
57
  message: string;
43
58
  code: string;
44
59
  }
60
+ /**
61
+ * What a tool costs, rather than how long it takes.
62
+ *
63
+ * `p90Tokens` is the 90th percentile of the *turn* total for turns this tool
64
+ * took part in, which is the only attribution the Pi extension emits
65
+ * (`token_attribution: 'toolCallTurn'`). It answers "when this tool runs, how
66
+ * expensive is the turn", not "this tool consumed N tokens" — a turn with
67
+ * several tool calls attributes its total to each of them.
68
+ */
69
+ interface LogMetricsToolCost {
70
+ name: string;
71
+ calls: number;
72
+ failed: number;
73
+ /** Turns carrying usage that this tool took part in, i.e. the p90 sample count. */
74
+ samples: number;
75
+ p90Tokens?: number;
76
+ }
45
77
  interface LogMetricsSnapshot {
46
78
  events: number;
47
79
  errors: number;
48
80
  toolCalls: number;
49
81
  failedToolCalls: number;
82
+ /** Turns that reported usage, i.e. the denominator for per-turn averages. */
83
+ turns: number;
84
+ abortedTurns: number;
85
+ failedTurns: number;
50
86
  tokens: LogMetricsTokenTotals;
51
87
  cost: number;
52
88
  toolLatency: LogMetricsToolLatency[];
89
+ toolCost: LogMetricsToolCost[];
90
+ failureCategories: LogMetricsEventCount[];
53
91
  eventVolume: LogMetricsEventCount[];
54
92
  packageEvents: LogMetricsEventCount[];
55
93
  sanitizedFailures: number;
@@ -69,11 +107,17 @@ declare class LogMetricsAggregator implements LogMetricsRecorder {
69
107
  private errors;
70
108
  private toolCalls;
71
109
  private failedToolCalls;
110
+ private turns;
111
+ private abortedTurns;
112
+ private failedTurns;
72
113
  private cost;
73
114
  private readonly tokens;
74
115
  private readonly eventCounts;
75
116
  private readonly packageCounts;
76
117
  private readonly tools;
118
+ private readonly toolTokens;
119
+ private readonly toolFailures;
120
+ private readonly failureCategories;
77
121
  private readonly operations;
78
122
  private sanitizedFailures;
79
123
  private readonly errorLog;
@@ -82,13 +126,30 @@ declare class LogMetricsAggregator implements LogMetricsRecorder {
82
126
  constructor(options?: LogMetricsAggregatorOptions);
83
127
  record(name: string, attributes: Record<string, unknown>): void;
84
128
  snapshot(): LogMetricsSnapshot;
129
+ /**
130
+ * Ranked by cost per call rather than call count: a tool invoked twice into a
131
+ * 300k-token turn is the one worth changing, and a frequency ranking buries
132
+ * it under the cheap tools that dominate any transcript.
133
+ */
134
+ private toolCostRows;
135
+ private countCategory;
85
136
  private recordOperationDuration;
86
137
  private recordSanitizedFailure;
87
138
  private recordToolResult;
139
+ /**
140
+ * Attributes a turn's token total to every tool that ran in it. The Pi
141
+ * extension emits one sample per tool result carrying the whole turn's usage,
142
+ * so this builds a per-tool distribution of "how expensive is a turn that
143
+ * uses this tool" — see LogMetricsToolCost for what that does and does not
144
+ * claim.
145
+ */
146
+ private recordToolTokenSample;
88
147
  private recordApiError;
89
148
  private recordTurnUsage;
149
+ /** A turn the user has to redo: the most expensive failure mode there is. */
150
+ private recordTurnFailure;
90
151
  private pushError;
91
152
  }
92
153
  //#endregion
93
- export { API_ERROR_RECORD, LogMetricsAggregator, LogMetricsAggregatorOptions, LogMetricsError, LogMetricsEventCount, LogMetricsOperationDuration, LogMetricsRecorder, LogMetricsSnapshot, LogMetricsTokenTotals, LogMetricsToolLatency, MAX_RETAINED_STATE_PER_TOOL, TOOL_RESULT_RECORD, TURN_FINISHED_RECORD };
154
+ export { API_ERROR_RECORD, LogMetricsAggregator, LogMetricsAggregatorOptions, LogMetricsError, LogMetricsEventCount, LogMetricsOperationDuration, LogMetricsRecorder, LogMetricsSnapshot, LogMetricsTokenTotals, LogMetricsToolCost, LogMetricsToolLatency, MAX_RETAINED_STATE_PER_TOOL, MAX_RETAINED_TOKEN_BUCKETS, TOOL_RESULT_RECORD, TOOL_TOKEN_SAMPLE_RECORD, TURN_FAILED_RECORD, TURN_FINISHED_RECORD };
94
155
  //# sourceMappingURL=metrics.d.cts.map
@@ -1 +1 @@
1
- {"version":3,"file":"metrics.d.cts","names":[],"sources":["../../src/services/metrics.ts"],"mappings":";;;;;;;;;;;cAWa;cACA;cACA;;cAmBA;UAEI;EACf;EACA;EACA;;UAGe;EACf;EACA;EACA;;EAEA;;UAGe;EACf;EACA;;UAGe;EACf;EACA;EACA;EACA;;UAGe;EACf;EACA;EACA;EACA;;UAGe;EACf;EACA;EACA;EACA;EACA,QAAQ;EACR;EACA,aAAa;EACb,aAAa;EACb,eAAe;EACf;EACA,mBAAmB;EACnB,cAAc;;UAGC;EACf;EACA;;;UAIe;EACf,OAAO,cAAc,YAAY;;cAwDtB,gCAAgC;UACnC;UACA;UACA;UACA;UACA;mBACS;mBACA;mBACA;mBACA;mBACA;UACT;mBACS;mBACA;mBACA;EAEL,YAAA,UAAS;EAKrB,OAAO,cAAc,YAAY;EAiBjC,YAAY;UA+CJ;UAmBA;UAeA;UAwBA;UAMA;UAOA"}
1
+ {"version":3,"file":"metrics.d.cts","names":[],"sources":["../../src/services/metrics.ts"],"mappings":";;;;;;;;;;;cAWa;cACA;cACA;cACA;cACA;;cAqCA;;;;;;cAOA;UAEI;EACf;EACA;EACA;;;;;;EAMA;EACA;;UAGe;EACf;EACA;EACA;;EAEA;;UAGe;EACf;EACA;;UAGe;EACf;EACA;EACA;EACA;;UAGe;EACf;EACA;EACA;EACA;;;;;;;;;;;UAYe;EACf;EACA;EACA;;EAEA;EACA;;UAGe;EACf;EACA;EACA;EACA;;EAEA;EACA;EACA;EACA,QAAQ;EACR;EACA,aAAa;EACb,UAAU;EACV,mBAAmB;EACnB,aAAa;EACb,eAAe;EACf;EACA,mBAAmB;EACnB,cAAc;;UAGC;EACf;EACA;;;UAIe;EACf,OAAO,cAAc,YAAY;;cAkFtB,gCAAgC;UACnC;UACA;UACA;UACA;UACA;UACA;UACA;UACA;mBACS;mBACA;mBACA;mBACA;mBACA;mBACA;mBACA;mBACA;UACT;mBACS;mBACA;mBACA;EAEL,YAAA,UAAS;EAKrB,OAAO,cAAc,YAAY;EAmBjC,YAAY;;;;;;UA2DJ;UAiBA;UAIA;UAiBA;UAiBA;;;;;;;;UA6BA;UASA;UAQA;;UAWA;UAOA"}
@@ -12,12 +12,27 @@
12
12
  declare const TOOL_RESULT_RECORD = "pi.tool_result";
13
13
  declare const API_ERROR_RECORD = "pi.api_error";
14
14
  declare const TURN_FINISHED_RECORD = "pi.turn.finished";
15
+ declare const TURN_FAILED_RECORD = "pi.turn.failed";
16
+ declare const TOOL_TOKEN_SAMPLE_RECORD = "pi.tool_token_sample";
15
17
  /** Bucket ceiling per tool: covers 1ms to roughly an hour at 10% resolution. */
16
18
  declare const MAX_RETAINED_STATE_PER_TOOL = 160;
19
+ /**
20
+ * Token distributions reuse the same histogram at a higher ceiling: a turn's
21
+ * total can run to millions once cache reads are counted, which the latency
22
+ * ceiling would saturate and report as a flat maximum.
23
+ */
24
+ declare const MAX_RETAINED_TOKEN_BUCKETS = 200;
17
25
  interface LogMetricsTokenTotals {
18
26
  input: number;
19
27
  output: number;
20
28
  total: number;
29
+ /**
30
+ * Cached prompt tokens billed at the read rate. Held separately from `input`
31
+ * because the ratio between them is the session's cache hit rate, which is
32
+ * the single largest cost lever a user controls.
33
+ */
34
+ cacheRead: number;
35
+ cacheWrite: number;
21
36
  }
22
37
  interface LogMetricsToolLatency {
23
38
  name: string;
@@ -42,14 +57,37 @@ interface LogMetricsError {
42
57
  message: string;
43
58
  code: string;
44
59
  }
60
+ /**
61
+ * What a tool costs, rather than how long it takes.
62
+ *
63
+ * `p90Tokens` is the 90th percentile of the *turn* total for turns this tool
64
+ * took part in, which is the only attribution the Pi extension emits
65
+ * (`token_attribution: 'toolCallTurn'`). It answers "when this tool runs, how
66
+ * expensive is the turn", not "this tool consumed N tokens" — a turn with
67
+ * several tool calls attributes its total to each of them.
68
+ */
69
+ interface LogMetricsToolCost {
70
+ name: string;
71
+ calls: number;
72
+ failed: number;
73
+ /** Turns carrying usage that this tool took part in, i.e. the p90 sample count. */
74
+ samples: number;
75
+ p90Tokens?: number;
76
+ }
45
77
  interface LogMetricsSnapshot {
46
78
  events: number;
47
79
  errors: number;
48
80
  toolCalls: number;
49
81
  failedToolCalls: number;
82
+ /** Turns that reported usage, i.e. the denominator for per-turn averages. */
83
+ turns: number;
84
+ abortedTurns: number;
85
+ failedTurns: number;
50
86
  tokens: LogMetricsTokenTotals;
51
87
  cost: number;
52
88
  toolLatency: LogMetricsToolLatency[];
89
+ toolCost: LogMetricsToolCost[];
90
+ failureCategories: LogMetricsEventCount[];
53
91
  eventVolume: LogMetricsEventCount[];
54
92
  packageEvents: LogMetricsEventCount[];
55
93
  sanitizedFailures: number;
@@ -69,11 +107,17 @@ declare class LogMetricsAggregator implements LogMetricsRecorder {
69
107
  private errors;
70
108
  private toolCalls;
71
109
  private failedToolCalls;
110
+ private turns;
111
+ private abortedTurns;
112
+ private failedTurns;
72
113
  private cost;
73
114
  private readonly tokens;
74
115
  private readonly eventCounts;
75
116
  private readonly packageCounts;
76
117
  private readonly tools;
118
+ private readonly toolTokens;
119
+ private readonly toolFailures;
120
+ private readonly failureCategories;
77
121
  private readonly operations;
78
122
  private sanitizedFailures;
79
123
  private readonly errorLog;
@@ -82,13 +126,30 @@ declare class LogMetricsAggregator implements LogMetricsRecorder {
82
126
  constructor(options?: LogMetricsAggregatorOptions);
83
127
  record(name: string, attributes: Record<string, unknown>): void;
84
128
  snapshot(): LogMetricsSnapshot;
129
+ /**
130
+ * Ranked by cost per call rather than call count: a tool invoked twice into a
131
+ * 300k-token turn is the one worth changing, and a frequency ranking buries
132
+ * it under the cheap tools that dominate any transcript.
133
+ */
134
+ private toolCostRows;
135
+ private countCategory;
85
136
  private recordOperationDuration;
86
137
  private recordSanitizedFailure;
87
138
  private recordToolResult;
139
+ /**
140
+ * Attributes a turn's token total to every tool that ran in it. The Pi
141
+ * extension emits one sample per tool result carrying the whole turn's usage,
142
+ * so this builds a per-tool distribution of "how expensive is a turn that
143
+ * uses this tool" — see LogMetricsToolCost for what that does and does not
144
+ * claim.
145
+ */
146
+ private recordToolTokenSample;
88
147
  private recordApiError;
89
148
  private recordTurnUsage;
149
+ /** A turn the user has to redo: the most expensive failure mode there is. */
150
+ private recordTurnFailure;
90
151
  private pushError;
91
152
  }
92
153
  //#endregion
93
- export { API_ERROR_RECORD, LogMetricsAggregator, LogMetricsAggregatorOptions, LogMetricsError, LogMetricsEventCount, LogMetricsOperationDuration, LogMetricsRecorder, LogMetricsSnapshot, LogMetricsTokenTotals, LogMetricsToolLatency, MAX_RETAINED_STATE_PER_TOOL, TOOL_RESULT_RECORD, TURN_FINISHED_RECORD };
154
+ export { API_ERROR_RECORD, LogMetricsAggregator, LogMetricsAggregatorOptions, LogMetricsError, LogMetricsEventCount, LogMetricsOperationDuration, LogMetricsRecorder, LogMetricsSnapshot, LogMetricsTokenTotals, LogMetricsToolCost, LogMetricsToolLatency, MAX_RETAINED_STATE_PER_TOOL, MAX_RETAINED_TOKEN_BUCKETS, TOOL_RESULT_RECORD, TOOL_TOKEN_SAMPLE_RECORD, TURN_FAILED_RECORD, TURN_FINISHED_RECORD };
94
155
  //# sourceMappingURL=metrics.d.mts.map