@agimon-ai/doompi-log 0.0.1-alpha.2 → 0.0.1-alpha.21

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.
Files changed (90) hide show
  1. package/README.md +42 -19
  2. package/dist/adapters/node/metricsSource.cjs +2 -0
  3. package/dist/adapters/node/metricsSource.cjs.map +1 -0
  4. package/dist/adapters/node/metricsSource.d.cts +14 -0
  5. package/dist/adapters/node/metricsSource.d.cts.map +1 -0
  6. package/dist/adapters/node/metricsSource.d.mts +14 -0
  7. package/dist/adapters/node/metricsSource.d.mts.map +1 -0
  8. package/dist/adapters/node/metricsSource.mjs +2 -0
  9. package/dist/adapters/node/metricsSource.mjs.map +1 -0
  10. package/dist/adapters/pi/extension.cjs +2 -0
  11. package/dist/adapters/pi/extension.cjs.map +1 -0
  12. package/dist/adapters/pi/extension.d.cts +34 -0
  13. package/dist/adapters/pi/extension.d.cts.map +1 -0
  14. package/dist/adapters/pi/extension.d.mts +34 -0
  15. package/dist/adapters/pi/extension.d.mts.map +1 -0
  16. package/dist/adapters/pi/extension.mjs +2 -0
  17. package/dist/adapters/pi/extension.mjs.map +1 -0
  18. package/dist/extension.cjs +1 -2
  19. package/dist/extension.d.cts +2 -33
  20. package/dist/extension.d.mts +2 -33
  21. package/dist/extension.mjs +1 -2
  22. package/dist/extensions/pi.cjs +1 -2
  23. package/dist/extensions/pi.d.cts +2 -4
  24. package/dist/extensions/pi.d.mts +2 -5
  25. package/dist/extensions/pi.mjs +1 -2
  26. package/dist/index.cjs +1 -1
  27. package/dist/index.d.cts +5 -4
  28. package/dist/index.d.mts +5 -4
  29. package/dist/index.mjs +1 -1
  30. package/dist/metrics.cjs +1 -2
  31. package/dist/metrics.d.cts +3 -94
  32. package/dist/metrics.d.mts +3 -94
  33. package/dist/metrics.mjs +1 -2
  34. package/dist/metricsSource.cjs +1 -2
  35. package/dist/metricsSource.d.cts +3 -25
  36. package/dist/metricsSource.d.mts +3 -25
  37. package/dist/metricsSource.mjs +1 -2
  38. package/dist/services/findings.cjs +2 -0
  39. package/dist/services/findings.cjs.map +1 -0
  40. package/dist/services/findings.d.cts +23 -0
  41. package/dist/services/findings.d.cts.map +1 -0
  42. package/dist/services/findings.d.mts +23 -0
  43. package/dist/services/findings.d.mts.map +1 -0
  44. package/dist/services/findings.mjs +2 -0
  45. package/dist/services/findings.mjs.map +1 -0
  46. package/dist/services/metrics.cjs +2 -0
  47. package/dist/services/metrics.cjs.map +1 -0
  48. package/dist/services/metrics.d.cts +155 -0
  49. package/dist/services/metrics.d.cts.map +1 -0
  50. package/dist/services/metrics.d.mts +155 -0
  51. package/dist/services/metrics.d.mts.map +1 -0
  52. package/dist/services/metrics.mjs +2 -0
  53. package/dist/services/metrics.mjs.map +1 -0
  54. package/dist/tui/logMetricsOverlay.cjs +2 -0
  55. package/dist/tui/logMetricsOverlay.cjs.map +1 -0
  56. package/dist/tui/logMetricsOverlay.d.cts +80 -0
  57. package/dist/tui/logMetricsOverlay.d.cts.map +1 -0
  58. package/dist/tui/logMetricsOverlay.d.mts +80 -0
  59. package/dist/tui/logMetricsOverlay.d.mts.map +1 -0
  60. package/dist/tui/logMetricsOverlay.mjs +2 -0
  61. package/dist/tui/logMetricsOverlay.mjs.map +1 -0
  62. package/dist/tui/metricsOverlay.cjs +1 -2
  63. package/dist/tui/metricsOverlay.d.cts +2 -71
  64. package/dist/tui/metricsOverlay.d.mts +2 -71
  65. package/dist/tui/metricsOverlay.mjs +1 -2
  66. package/dist/types/metricsSource.d.cts +16 -0
  67. package/dist/types/metricsSource.d.cts.map +1 -0
  68. package/dist/types/metricsSource.d.mts +16 -0
  69. package/dist/types/metricsSource.d.mts.map +1 -0
  70. package/package.json +16 -9
  71. package/dist/extension.cjs.map +0 -1
  72. package/dist/extension.d.cts.map +0 -1
  73. package/dist/extension.d.mts.map +0 -1
  74. package/dist/extension.mjs.map +0 -1
  75. package/dist/extensions/pi.cjs.map +0 -1
  76. package/dist/extensions/pi.d.cts.map +0 -1
  77. package/dist/extensions/pi.d.mts.map +0 -1
  78. package/dist/extensions/pi.mjs.map +0 -1
  79. package/dist/metrics.cjs.map +0 -1
  80. package/dist/metrics.d.cts.map +0 -1
  81. package/dist/metrics.d.mts.map +0 -1
  82. package/dist/metrics.mjs.map +0 -1
  83. package/dist/metricsSource.cjs.map +0 -1
  84. package/dist/metricsSource.d.cts.map +0 -1
  85. package/dist/metricsSource.d.mts.map +0 -1
  86. package/dist/metricsSource.mjs.map +0 -1
  87. package/dist/tui/metricsOverlay.cjs.map +0 -1
  88. package/dist/tui/metricsOverlay.d.cts.map +0 -1
  89. package/dist/tui/metricsOverlay.d.mts.map +0 -1
  90. package/dist/tui/metricsOverlay.mjs.map +0 -1
@@ -1,94 +1,3 @@
1
- //#region src/metrics.d.ts
2
- /**
3
- * Session-local aggregation of the log records the Pi telemetry extension
4
- * already emits.
5
- *
6
- * The aggregator is fed `(recordName, attributes)` pairs rather than Pi events
7
- * so it can only ever see what the sink sees: no extra instrumentation is added
8
- * for metrics, and nothing here depends on a live sink. Everything is scoped to
9
- * the current process, so there is no persistence and no query API.
10
- */
11
- /** Record names the aggregator derives numbers from. */
12
- declare const TOOL_RESULT_RECORD = "pi.tool_result";
13
- declare const API_ERROR_RECORD = "pi.api_error";
14
- declare const TURN_FINISHED_RECORD = "pi.turn.finished";
15
- /** Bucket ceiling per tool: covers 1ms to roughly an hour at 10% resolution. */
16
- declare const MAX_RETAINED_STATE_PER_TOOL = 160;
17
- interface LogMetricsTokenTotals {
18
- input: number;
19
- output: number;
20
- total: number;
21
- }
22
- interface LogMetricsToolLatency {
23
- name: string;
24
- calls: number;
25
- p95Ms?: number;
26
- /** Buckets currently held for this tool, bounded by MAX_RETAINED_STATE_PER_TOOL. */
27
- retainedStateSize: number;
28
- }
29
- interface LogMetricsEventCount {
30
- name: string;
31
- count: number;
32
- }
33
- interface LogMetricsOperationDuration {
34
- name: string;
35
- calls: number;
36
- p95Ms?: number;
37
- retainedStateSize: number;
38
- }
39
- interface LogMetricsError {
40
- at: number;
41
- event: string;
42
- message: string;
43
- code: string;
44
- }
45
- interface LogMetricsSnapshot {
46
- events: number;
47
- errors: number;
48
- toolCalls: number;
49
- failedToolCalls: number;
50
- tokens: LogMetricsTokenTotals;
51
- cost: number;
52
- toolLatency: LogMetricsToolLatency[];
53
- eventVolume: LogMetricsEventCount[];
54
- packageEvents: LogMetricsEventCount[];
55
- sanitizedFailures: number;
56
- operationDuration: LogMetricsOperationDuration[];
57
- recentErrors: LogMetricsError[];
58
- }
59
- interface LogMetricsAggregatorOptions {
60
- now?: () => number;
61
- maxRecentErrors?: number;
62
- }
63
- /** The slice of the aggregator the telemetry extension needs. */
64
- interface LogMetricsRecorder {
65
- record(name: string, attributes: Record<string, unknown>): void;
66
- }
67
- declare class LogMetricsAggregator implements LogMetricsRecorder {
68
- private events;
69
- private errors;
70
- private toolCalls;
71
- private failedToolCalls;
72
- private cost;
73
- private readonly tokens;
74
- private readonly eventCounts;
75
- private readonly packageCounts;
76
- private readonly tools;
77
- private readonly operations;
78
- private sanitizedFailures;
79
- private readonly errorLog;
80
- private readonly now;
81
- private readonly maxRecentErrors;
82
- constructor(options?: LogMetricsAggregatorOptions);
83
- record(name: string, attributes: Record<string, unknown>): void;
84
- snapshot(): LogMetricsSnapshot;
85
- private recordOperationDuration;
86
- private recordSanitizedFailure;
87
- private recordToolResult;
88
- private recordApiError;
89
- private recordTurnUsage;
90
- private pushError;
91
- }
92
- //#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 };
94
- //# sourceMappingURL=metrics.d.cts.map
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,94 +1,3 @@
1
- //#region src/metrics.d.ts
2
- /**
3
- * Session-local aggregation of the log records the Pi telemetry extension
4
- * already emits.
5
- *
6
- * The aggregator is fed `(recordName, attributes)` pairs rather than Pi events
7
- * so it can only ever see what the sink sees: no extra instrumentation is added
8
- * for metrics, and nothing here depends on a live sink. Everything is scoped to
9
- * the current process, so there is no persistence and no query API.
10
- */
11
- /** Record names the aggregator derives numbers from. */
12
- declare const TOOL_RESULT_RECORD = "pi.tool_result";
13
- declare const API_ERROR_RECORD = "pi.api_error";
14
- declare const TURN_FINISHED_RECORD = "pi.turn.finished";
15
- /** Bucket ceiling per tool: covers 1ms to roughly an hour at 10% resolution. */
16
- declare const MAX_RETAINED_STATE_PER_TOOL = 160;
17
- interface LogMetricsTokenTotals {
18
- input: number;
19
- output: number;
20
- total: number;
21
- }
22
- interface LogMetricsToolLatency {
23
- name: string;
24
- calls: number;
25
- p95Ms?: number;
26
- /** Buckets currently held for this tool, bounded by MAX_RETAINED_STATE_PER_TOOL. */
27
- retainedStateSize: number;
28
- }
29
- interface LogMetricsEventCount {
30
- name: string;
31
- count: number;
32
- }
33
- interface LogMetricsOperationDuration {
34
- name: string;
35
- calls: number;
36
- p95Ms?: number;
37
- retainedStateSize: number;
38
- }
39
- interface LogMetricsError {
40
- at: number;
41
- event: string;
42
- message: string;
43
- code: string;
44
- }
45
- interface LogMetricsSnapshot {
46
- events: number;
47
- errors: number;
48
- toolCalls: number;
49
- failedToolCalls: number;
50
- tokens: LogMetricsTokenTotals;
51
- cost: number;
52
- toolLatency: LogMetricsToolLatency[];
53
- eventVolume: LogMetricsEventCount[];
54
- packageEvents: LogMetricsEventCount[];
55
- sanitizedFailures: number;
56
- operationDuration: LogMetricsOperationDuration[];
57
- recentErrors: LogMetricsError[];
58
- }
59
- interface LogMetricsAggregatorOptions {
60
- now?: () => number;
61
- maxRecentErrors?: number;
62
- }
63
- /** The slice of the aggregator the telemetry extension needs. */
64
- interface LogMetricsRecorder {
65
- record(name: string, attributes: Record<string, unknown>): void;
66
- }
67
- declare class LogMetricsAggregator implements LogMetricsRecorder {
68
- private events;
69
- private errors;
70
- private toolCalls;
71
- private failedToolCalls;
72
- private cost;
73
- private readonly tokens;
74
- private readonly eventCounts;
75
- private readonly packageCounts;
76
- private readonly tools;
77
- private readonly operations;
78
- private sanitizedFailures;
79
- private readonly errorLog;
80
- private readonly now;
81
- private readonly maxRecentErrors;
82
- constructor(options?: LogMetricsAggregatorOptions);
83
- record(name: string, attributes: Record<string, unknown>): void;
84
- snapshot(): LogMetricsSnapshot;
85
- private recordOperationDuration;
86
- private recordSanitizedFailure;
87
- private recordToolResult;
88
- private recordApiError;
89
- private recordTurnUsage;
90
- private pushError;
91
- }
92
- //#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 };
94
- //# sourceMappingURL=metrics.d.mts.map
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,2 +1 @@
1
- const e=`pi.tool_result`,t=`pi.api_error`,n=`pi.turn.finished`,r=1.1,i=Math.log(r),a=160;function o(e){return typeof e==`number`&&Number.isFinite(e)?e:void 0}function s(e){return typeof e==`string`&&e.length>0?e:void 0}function c(e){return e.replace(/[^a-zA-Z0-9@/._:-]/g,`_`).slice(0,128)||`unknown`}function l(e){let t=Math.ceil(Math.log(Math.max(e,1))/i);return Math.min(Math.max(t,0),159)}function u(e){return r**e}function d(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(u(i))}function f(e){return e.startsWith(`pi.`)?e.slice(3):e}var p=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=c(e);this.events+=1,this.eventCounts.set(n,(this.eventCounts.get(n)??0)+1);let r=s(t[`telemetry.package`]);if(r&&!r.startsWith(`/`)&&!r.includes(`\\`)){let e=c(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)}snapshot(){let e=[...this.tools.entries()].map(([e,t])=>{let n=d(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=d(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 n=Object.entries(t).find(([e,t])=>e!==`tool.duration_ms`&&(e===`duration_ms`||e.endsWith(`.duration_ms`))&&o(t)!==void 0);if(!n)return;let r=c(s(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 a=o(n[1]);if(a===void 0)return;let u=l(a);i.buckets.set(u,(i.buckets.get(u)??0)+1),i.samples+=1}recordSanitizedFailure(e,t){let n=s(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 r=s(t[`error.code`])??n??`error`;this.pushError(e,`${s(t[`error.type`])??`operation`} failed`,c(r))}recordToolResult(e,t){let n=s(t[`tool.name`])??`unknown`,r=this.tools.get(n)??{calls:0,samples:0,buckets:new Map};this.tools.set(n,r),r.calls+=1,this.toolCalls+=1;let i=o(t[`tool.duration_ms`]);if(i!==void 0){let e=l(i);r.buckets.set(e,(r.buckets.get(e)??0)+1),r.samples+=1}t[`tool.result.error`]===!0&&(this.failedToolCalls+=1,this.pushError(e,`${n}: 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+=o(e[`gen_ai.usage.input_tokens`])??0,this.tokens.output+=o(e[`gen_ai.usage.output_tokens`])??0,this.tokens.total+=o(e[`gen_ai.usage.total_tokens`])??0,this.cost+=o(e[`gen_ai.usage.cost`])??0}pushError(e,t,n){this.errorLog.unshift({at:this.now(),event:f(e),message:t,code:n}),this.errorLog.length>this.maxRecentErrors&&(this.errorLog.length=this.maxRecentErrors)}};export{t as API_ERROR_RECORD,p as LogMetricsAggregator,a as MAX_RETAINED_STATE_PER_TOOL,e as TOOL_RESULT_RECORD,n as TURN_FINISHED_RECORD};
2
- //# sourceMappingURL=metrics.mjs.map
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};
@@ -1,2 +1 @@
1
- Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./_virtual/_rolldown/runtime.cjs");let t=require("node:child_process"),n=require("node:module"),r=require("node:path");r=e.__toESM(r,1);let i=require("@agimon-ai/log-sink-mcp");const a=`total-tokens`;function o(){let e=(0,n.createRequire)(require("url").pathToFileURL(__filename).href).resolve(`@agimon-ai/log-sink-mcp/package.json`);return r.default.join(r.default.dirname(e),`dist`,`cli.mjs`)}function s(e){return new URLSearchParams({groupBy:e.groupBy,period:e.period,sort:a,limit:String(e.limit),toolLimit:`1`}).toString()}async function c(e,t,n){let r=await n(`${e}/api/metrics?${s(t)}`,{signal:AbortSignal.timeout(5e3)});if(!r.ok)throw Error(`sink responded ${r.status}`);let i=await r.json();if(i.error||!Array.isArray(i.timeline))throw Error(i.error??`sink is an older version`);return i}function l(e){return new Promise((n,r)=>{(0,t.execFile)(process.execPath,[o(),`logs`,`metrics`,`--group-by`,e.groupBy,`--period`,e.period,`--sort`,a,`--limit`,String(e.limit),`--tool-limit`,`1`],{timeout:3e4,maxBuffer:33554432},(e,t,i)=>{if(e){r(Error(i.trim()||e.message));return}try{n(JSON.parse(t))}catch{r(Error(`metrics CLI returned unparsable output`))}})})}function u(e={}){let t=e.fetchImpl??globalThis.fetch,n=e.cliRunner??l,r,a,o=async()=>{if(a!==void 0)return a??void 0;try{a=(await(0,i.resolveLogSinkPort)({env:e.env,cwd:e.cwd,healthCheck:!0}))?.endpoint??null}catch{a=null}return a??void 0};return{lastTransport:()=>r,async query(e){let i=await o();if(i&&t)try{let n=await c(i,e,t);return r=`http`,n}catch{a=null}let s=await n(e);return r=`cli`,s}}}exports.createMetricsSource=u;
2
- //# sourceMappingURL=metricsSource.cjs.map
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./adapters/node/metricsSource.cjs");exports.createMetricsSource=e.createMetricsSource;
@@ -1,25 +1,3 @@
1
- import { LogMetricsGroupBy, LogMetricsPeriod, LogMetricsReport } from "@agimon-ai/log-sink-mcp";
2
- //#region src/metricsSource.d.ts
3
- interface MetricsQueryParams {
4
- groupBy: LogMetricsGroupBy;
5
- period: LogMetricsPeriod;
6
- limit: number;
7
- }
8
- type MetricsQuery = (params: MetricsQueryParams) => Promise<LogMetricsReport>;
9
- /** Where the last successful answer came from, so the overlay can say so. */
10
- type MetricsTransport = 'http' | 'cli';
11
- interface MetricsSourceOptions {
12
- env?: NodeJS.ProcessEnv;
13
- cwd?: string;
14
- /** Overrides transport discovery in tests. */
15
- fetchImpl?: typeof fetch;
16
- cliRunner?: (params: MetricsQueryParams) => Promise<LogMetricsReport>;
17
- }
18
- interface MetricsSource {
19
- query: MetricsQuery;
20
- lastTransport(): MetricsTransport | undefined;
21
- }
22
- declare function createMetricsSource(options?: MetricsSourceOptions): MetricsSource;
23
- //#endregion
24
- export { MetricsQuery, MetricsQueryParams, MetricsSource, MetricsSourceOptions, MetricsTransport, createMetricsSource };
25
- //# sourceMappingURL=metricsSource.d.cts.map
1
+ import { MetricsQuery, MetricsQueryParams, MetricsSource, MetricsTransport } from "./types/metricsSource.cjs";
2
+ import { MetricsSourceOptions, createMetricsSource } from "./adapters/node/metricsSource.cjs";
3
+ export { type MetricsQuery, type MetricsQueryParams, type MetricsSource, type MetricsSourceOptions, type MetricsTransport, createMetricsSource };
@@ -1,25 +1,3 @@
1
- import { LogMetricsGroupBy, LogMetricsPeriod, LogMetricsReport } from "@agimon-ai/log-sink-mcp";
2
- //#region src/metricsSource.d.ts
3
- interface MetricsQueryParams {
4
- groupBy: LogMetricsGroupBy;
5
- period: LogMetricsPeriod;
6
- limit: number;
7
- }
8
- type MetricsQuery = (params: MetricsQueryParams) => Promise<LogMetricsReport>;
9
- /** Where the last successful answer came from, so the overlay can say so. */
10
- type MetricsTransport = 'http' | 'cli';
11
- interface MetricsSourceOptions {
12
- env?: NodeJS.ProcessEnv;
13
- cwd?: string;
14
- /** Overrides transport discovery in tests. */
15
- fetchImpl?: typeof fetch;
16
- cliRunner?: (params: MetricsQueryParams) => Promise<LogMetricsReport>;
17
- }
18
- interface MetricsSource {
19
- query: MetricsQuery;
20
- lastTransport(): MetricsTransport | undefined;
21
- }
22
- declare function createMetricsSource(options?: MetricsSourceOptions): MetricsSource;
23
- //#endregion
24
- export { MetricsQuery, MetricsQueryParams, MetricsSource, MetricsSourceOptions, MetricsTransport, createMetricsSource };
25
- //# sourceMappingURL=metricsSource.d.mts.map
1
+ import { MetricsQuery, MetricsQueryParams, MetricsSource, MetricsTransport } from "./types/metricsSource.mjs";
2
+ import { MetricsSourceOptions, createMetricsSource } from "./adapters/node/metricsSource.mjs";
3
+ export { type MetricsQuery, type MetricsQueryParams, type MetricsSource, type MetricsSourceOptions, type MetricsTransport, createMetricsSource };
@@ -1,2 +1 @@
1
- import{createRequire as e}from"node:module";import{execFile as t}from"node:child_process";import n from"node:path";import{resolveLogSinkPort as r}from"@agimon-ai/log-sink-mcp";const i=`total-tokens`;function a(){let t=e(import.meta.url).resolve(`@agimon-ai/log-sink-mcp/package.json`);return n.join(n.dirname(t),`dist`,`cli.mjs`)}function o(e){return new URLSearchParams({groupBy:e.groupBy,period:e.period,sort:i,limit:String(e.limit),toolLimit:`1`}).toString()}async function s(e,t,n){let r=await n(`${e}/api/metrics?${o(t)}`,{signal:AbortSignal.timeout(5e3)});if(!r.ok)throw Error(`sink responded ${r.status}`);let i=await r.json();if(i.error||!Array.isArray(i.timeline))throw Error(i.error??`sink is an older version`);return i}function c(e){return new Promise((n,r)=>{t(process.execPath,[a(),`logs`,`metrics`,`--group-by`,e.groupBy,`--period`,e.period,`--sort`,i,`--limit`,String(e.limit),`--tool-limit`,`1`],{timeout:3e4,maxBuffer:33554432},(e,t,i)=>{if(e){r(Error(i.trim()||e.message));return}try{n(JSON.parse(t))}catch{r(Error(`metrics CLI returned unparsable output`))}})})}function l(e={}){let t=e.fetchImpl??globalThis.fetch,n=e.cliRunner??c,i,a,o=async()=>{if(a!==void 0)return a??void 0;try{a=(await r({env:e.env,cwd:e.cwd,healthCheck:!0}))?.endpoint??null}catch{a=null}return a??void 0};return{lastTransport:()=>i,async query(e){let r=await o();if(r&&t)try{let n=await s(r,e,t);return i=`http`,n}catch{a=null}let c=await n(e);return i=`cli`,c}}}export{l as createMetricsSource};
2
- //# sourceMappingURL=metricsSource.mjs.map
1
+ import{createMetricsSource as e}from"./adapters/node/metricsSource.mjs";export{e as createMetricsSource};
@@ -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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"}
@@ -0,0 +1,2 @@
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
+ //# sourceMappingURL=metrics.cjs.map
@@ -0,0 +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';\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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"}