@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.
- package/README.md +42 -19
- package/dist/adapters/node/metricsSource.cjs +2 -0
- package/dist/adapters/node/metricsSource.cjs.map +1 -0
- package/dist/adapters/node/metricsSource.d.cts +14 -0
- package/dist/adapters/node/metricsSource.d.cts.map +1 -0
- package/dist/adapters/node/metricsSource.d.mts +14 -0
- package/dist/adapters/node/metricsSource.d.mts.map +1 -0
- package/dist/adapters/node/metricsSource.mjs +2 -0
- package/dist/adapters/node/metricsSource.mjs.map +1 -0
- package/dist/adapters/pi/extension.cjs +2 -0
- package/dist/adapters/pi/extension.cjs.map +1 -0
- package/dist/adapters/pi/extension.d.cts +34 -0
- package/dist/adapters/pi/extension.d.cts.map +1 -0
- package/dist/adapters/pi/extension.d.mts +34 -0
- package/dist/adapters/pi/extension.d.mts.map +1 -0
- package/dist/adapters/pi/extension.mjs +2 -0
- package/dist/adapters/pi/extension.mjs.map +1 -0
- package/dist/extension.cjs +1 -2
- package/dist/extension.d.cts +2 -33
- package/dist/extension.d.mts +2 -33
- package/dist/extension.mjs +1 -2
- package/dist/extensions/pi.cjs +1 -2
- package/dist/extensions/pi.d.cts +2 -4
- package/dist/extensions/pi.d.mts +2 -5
- package/dist/extensions/pi.mjs +1 -2
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +5 -4
- package/dist/index.d.mts +5 -4
- package/dist/index.mjs +1 -1
- package/dist/metrics.cjs +1 -2
- package/dist/metrics.d.cts +3 -94
- package/dist/metrics.d.mts +3 -94
- package/dist/metrics.mjs +1 -2
- package/dist/metricsSource.cjs +1 -2
- package/dist/metricsSource.d.cts +3 -25
- package/dist/metricsSource.d.mts +3 -25
- package/dist/metricsSource.mjs +1 -2
- package/dist/services/findings.cjs +2 -0
- package/dist/services/findings.cjs.map +1 -0
- package/dist/services/findings.d.cts +23 -0
- package/dist/services/findings.d.cts.map +1 -0
- package/dist/services/findings.d.mts +23 -0
- package/dist/services/findings.d.mts.map +1 -0
- package/dist/services/findings.mjs +2 -0
- package/dist/services/findings.mjs.map +1 -0
- package/dist/services/metrics.cjs +2 -0
- package/dist/services/metrics.cjs.map +1 -0
- package/dist/services/metrics.d.cts +155 -0
- package/dist/services/metrics.d.cts.map +1 -0
- package/dist/services/metrics.d.mts +155 -0
- package/dist/services/metrics.d.mts.map +1 -0
- package/dist/services/metrics.mjs +2 -0
- package/dist/services/metrics.mjs.map +1 -0
- package/dist/tui/logMetricsOverlay.cjs +2 -0
- package/dist/tui/logMetricsOverlay.cjs.map +1 -0
- package/dist/tui/logMetricsOverlay.d.cts +80 -0
- package/dist/tui/logMetricsOverlay.d.cts.map +1 -0
- package/dist/tui/logMetricsOverlay.d.mts +80 -0
- package/dist/tui/logMetricsOverlay.d.mts.map +1 -0
- package/dist/tui/logMetricsOverlay.mjs +2 -0
- package/dist/tui/logMetricsOverlay.mjs.map +1 -0
- package/dist/tui/metricsOverlay.cjs +1 -2
- package/dist/tui/metricsOverlay.d.cts +2 -71
- package/dist/tui/metricsOverlay.d.mts +2 -71
- package/dist/tui/metricsOverlay.mjs +1 -2
- package/dist/types/metricsSource.d.cts +16 -0
- package/dist/types/metricsSource.d.cts.map +1 -0
- package/dist/types/metricsSource.d.mts +16 -0
- package/dist/types/metricsSource.d.mts.map +1 -0
- package/package.json +16 -9
- package/dist/extension.cjs.map +0 -1
- package/dist/extension.d.cts.map +0 -1
- package/dist/extension.d.mts.map +0 -1
- package/dist/extension.mjs.map +0 -1
- package/dist/extensions/pi.cjs.map +0 -1
- package/dist/extensions/pi.d.cts.map +0 -1
- package/dist/extensions/pi.d.mts.map +0 -1
- package/dist/extensions/pi.mjs.map +0 -1
- package/dist/metrics.cjs.map +0 -1
- package/dist/metrics.d.cts.map +0 -1
- package/dist/metrics.d.mts.map +0 -1
- package/dist/metrics.mjs.map +0 -1
- package/dist/metricsSource.cjs.map +0 -1
- package/dist/metricsSource.d.cts.map +0 -1
- package/dist/metricsSource.d.mts.map +0 -1
- package/dist/metricsSource.mjs.map +0 -1
- package/dist/tui/metricsOverlay.cjs.map +0 -1
- package/dist/tui/metricsOverlay.d.cts.map +0 -1
- package/dist/tui/metricsOverlay.d.mts.map +0 -1
- package/dist/tui/metricsOverlay.mjs.map +0 -1
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
//#region src/services/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
|
+
declare const TURN_FAILED_RECORD = "pi.turn.failed";
|
|
16
|
+
declare const TOOL_TOKEN_SAMPLE_RECORD = "pi.tool_token_sample";
|
|
17
|
+
/** Bucket ceiling per tool: covers 1ms to roughly an hour at 10% resolution. */
|
|
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;
|
|
25
|
+
interface LogMetricsTokenTotals {
|
|
26
|
+
input: number;
|
|
27
|
+
output: number;
|
|
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;
|
|
36
|
+
}
|
|
37
|
+
interface LogMetricsToolLatency {
|
|
38
|
+
name: string;
|
|
39
|
+
calls: number;
|
|
40
|
+
p95Ms?: number;
|
|
41
|
+
/** Buckets currently held for this tool, bounded by MAX_RETAINED_STATE_PER_TOOL. */
|
|
42
|
+
retainedStateSize: number;
|
|
43
|
+
}
|
|
44
|
+
interface LogMetricsEventCount {
|
|
45
|
+
name: string;
|
|
46
|
+
count: number;
|
|
47
|
+
}
|
|
48
|
+
interface LogMetricsOperationDuration {
|
|
49
|
+
name: string;
|
|
50
|
+
calls: number;
|
|
51
|
+
p95Ms?: number;
|
|
52
|
+
retainedStateSize: number;
|
|
53
|
+
}
|
|
54
|
+
interface LogMetricsError {
|
|
55
|
+
at: number;
|
|
56
|
+
event: string;
|
|
57
|
+
message: string;
|
|
58
|
+
code: string;
|
|
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
|
+
}
|
|
77
|
+
interface LogMetricsSnapshot {
|
|
78
|
+
events: number;
|
|
79
|
+
errors: number;
|
|
80
|
+
toolCalls: number;
|
|
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;
|
|
86
|
+
tokens: LogMetricsTokenTotals;
|
|
87
|
+
cost: number;
|
|
88
|
+
toolLatency: LogMetricsToolLatency[];
|
|
89
|
+
toolCost: LogMetricsToolCost[];
|
|
90
|
+
failureCategories: LogMetricsEventCount[];
|
|
91
|
+
eventVolume: LogMetricsEventCount[];
|
|
92
|
+
packageEvents: LogMetricsEventCount[];
|
|
93
|
+
sanitizedFailures: number;
|
|
94
|
+
operationDuration: LogMetricsOperationDuration[];
|
|
95
|
+
recentErrors: LogMetricsError[];
|
|
96
|
+
}
|
|
97
|
+
interface LogMetricsAggregatorOptions {
|
|
98
|
+
now?: () => number;
|
|
99
|
+
maxRecentErrors?: number;
|
|
100
|
+
}
|
|
101
|
+
/** The slice of the aggregator the telemetry extension needs. */
|
|
102
|
+
interface LogMetricsRecorder {
|
|
103
|
+
record(name: string, attributes: Record<string, unknown>): void;
|
|
104
|
+
}
|
|
105
|
+
declare class LogMetricsAggregator implements LogMetricsRecorder {
|
|
106
|
+
private events;
|
|
107
|
+
private errors;
|
|
108
|
+
private toolCalls;
|
|
109
|
+
private failedToolCalls;
|
|
110
|
+
private turns;
|
|
111
|
+
private abortedTurns;
|
|
112
|
+
private failedTurns;
|
|
113
|
+
private cost;
|
|
114
|
+
private readonly tokens;
|
|
115
|
+
private readonly eventCounts;
|
|
116
|
+
private readonly packageCounts;
|
|
117
|
+
private readonly tools;
|
|
118
|
+
private readonly toolTokens;
|
|
119
|
+
private readonly toolFailures;
|
|
120
|
+
private readonly failureCategories;
|
|
121
|
+
private readonly operations;
|
|
122
|
+
private sanitizedFailures;
|
|
123
|
+
private readonly errorLog;
|
|
124
|
+
private readonly now;
|
|
125
|
+
private readonly maxRecentErrors;
|
|
126
|
+
constructor(options?: LogMetricsAggregatorOptions);
|
|
127
|
+
record(name: string, attributes: Record<string, unknown>): void;
|
|
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;
|
|
136
|
+
private recordOperationDuration;
|
|
137
|
+
private recordSanitizedFailure;
|
|
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;
|
|
147
|
+
private recordApiError;
|
|
148
|
+
private recordTurnUsage;
|
|
149
|
+
/** A turn the user has to redo: the most expensive failure mode there is. */
|
|
150
|
+
private recordTurnFailure;
|
|
151
|
+
private pushError;
|
|
152
|
+
}
|
|
153
|
+
//#endregion
|
|
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 };
|
|
155
|
+
//# sourceMappingURL=metrics.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
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"}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
//#region src/services/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
|
+
declare const TURN_FAILED_RECORD = "pi.turn.failed";
|
|
16
|
+
declare const TOOL_TOKEN_SAMPLE_RECORD = "pi.tool_token_sample";
|
|
17
|
+
/** Bucket ceiling per tool: covers 1ms to roughly an hour at 10% resolution. */
|
|
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;
|
|
25
|
+
interface LogMetricsTokenTotals {
|
|
26
|
+
input: number;
|
|
27
|
+
output: number;
|
|
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;
|
|
36
|
+
}
|
|
37
|
+
interface LogMetricsToolLatency {
|
|
38
|
+
name: string;
|
|
39
|
+
calls: number;
|
|
40
|
+
p95Ms?: number;
|
|
41
|
+
/** Buckets currently held for this tool, bounded by MAX_RETAINED_STATE_PER_TOOL. */
|
|
42
|
+
retainedStateSize: number;
|
|
43
|
+
}
|
|
44
|
+
interface LogMetricsEventCount {
|
|
45
|
+
name: string;
|
|
46
|
+
count: number;
|
|
47
|
+
}
|
|
48
|
+
interface LogMetricsOperationDuration {
|
|
49
|
+
name: string;
|
|
50
|
+
calls: number;
|
|
51
|
+
p95Ms?: number;
|
|
52
|
+
retainedStateSize: number;
|
|
53
|
+
}
|
|
54
|
+
interface LogMetricsError {
|
|
55
|
+
at: number;
|
|
56
|
+
event: string;
|
|
57
|
+
message: string;
|
|
58
|
+
code: string;
|
|
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
|
+
}
|
|
77
|
+
interface LogMetricsSnapshot {
|
|
78
|
+
events: number;
|
|
79
|
+
errors: number;
|
|
80
|
+
toolCalls: number;
|
|
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;
|
|
86
|
+
tokens: LogMetricsTokenTotals;
|
|
87
|
+
cost: number;
|
|
88
|
+
toolLatency: LogMetricsToolLatency[];
|
|
89
|
+
toolCost: LogMetricsToolCost[];
|
|
90
|
+
failureCategories: LogMetricsEventCount[];
|
|
91
|
+
eventVolume: LogMetricsEventCount[];
|
|
92
|
+
packageEvents: LogMetricsEventCount[];
|
|
93
|
+
sanitizedFailures: number;
|
|
94
|
+
operationDuration: LogMetricsOperationDuration[];
|
|
95
|
+
recentErrors: LogMetricsError[];
|
|
96
|
+
}
|
|
97
|
+
interface LogMetricsAggregatorOptions {
|
|
98
|
+
now?: () => number;
|
|
99
|
+
maxRecentErrors?: number;
|
|
100
|
+
}
|
|
101
|
+
/** The slice of the aggregator the telemetry extension needs. */
|
|
102
|
+
interface LogMetricsRecorder {
|
|
103
|
+
record(name: string, attributes: Record<string, unknown>): void;
|
|
104
|
+
}
|
|
105
|
+
declare class LogMetricsAggregator implements LogMetricsRecorder {
|
|
106
|
+
private events;
|
|
107
|
+
private errors;
|
|
108
|
+
private toolCalls;
|
|
109
|
+
private failedToolCalls;
|
|
110
|
+
private turns;
|
|
111
|
+
private abortedTurns;
|
|
112
|
+
private failedTurns;
|
|
113
|
+
private cost;
|
|
114
|
+
private readonly tokens;
|
|
115
|
+
private readonly eventCounts;
|
|
116
|
+
private readonly packageCounts;
|
|
117
|
+
private readonly tools;
|
|
118
|
+
private readonly toolTokens;
|
|
119
|
+
private readonly toolFailures;
|
|
120
|
+
private readonly failureCategories;
|
|
121
|
+
private readonly operations;
|
|
122
|
+
private sanitizedFailures;
|
|
123
|
+
private readonly errorLog;
|
|
124
|
+
private readonly now;
|
|
125
|
+
private readonly maxRecentErrors;
|
|
126
|
+
constructor(options?: LogMetricsAggregatorOptions);
|
|
127
|
+
record(name: string, attributes: Record<string, unknown>): void;
|
|
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;
|
|
136
|
+
private recordOperationDuration;
|
|
137
|
+
private recordSanitizedFailure;
|
|
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;
|
|
147
|
+
private recordApiError;
|
|
148
|
+
private recordTurnUsage;
|
|
149
|
+
/** A turn the user has to redo: the most expensive failure mode there is. */
|
|
150
|
+
private recordTurnFailure;
|
|
151
|
+
private pushError;
|
|
152
|
+
}
|
|
153
|
+
//#endregion
|
|
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 };
|
|
155
|
+
//# sourceMappingURL=metrics.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metrics.d.mts","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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=`pi.tool_result`,t=`pi.api_error`,n=`pi.turn.finished`,r=`pi.turn.failed`,i=`pi.tool_token_sample`,a=`unknown`,o=`api_error`,s=1.1,c=Math.log(s),l=160,u=200;function d(){return{samples:0,buckets:new Map}}function f(e){return typeof e==`number`&&Number.isFinite(e)?e:void 0}function p(e){return typeof e==`string`&&e.length>0?e:void 0}function m(e){return e.replace(/[^a-zA-Z0-9@/._:-]/g,`_`).slice(0,128)||`unknown`}function h(e,t){let n=Math.ceil(Math.log(Math.max(e,1))/c);return Math.min(Math.max(n,0),t-1)}function g(e){return s**e}function _(e,t,n){let r=h(t,n);e.buckets.set(r,(e.buckets.get(r)??0)+1),e.samples+=1}function v(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(g(a))}function y(e){return v(e,.95)}function b(e){return e===429?`rate_limited`:e>=500?`provider_unavailable`:o}function x(e){return e.startsWith(`pi.`)?e.slice(3):e}var S=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=m(e);this.events+=1,this.eventCounts.set(n,(this.eventCounts.get(n)??0)+1);let r=p(t[`telemetry.package`]);if(r&&!r.startsWith(`/`)&&!r.includes(`\\`)){let e=m(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=y(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=y(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?v(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`))&&f(t)!==void 0);if(!n)return;let r=m(p(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=f(n[1]);a!==void 0&&_(i,a,160)}recordSanitizedFailure(e,t){let n=p(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=p(t[`error.code`])??n??`error`;this.pushError(e,`${p(t[`error.type`])??`operation`} failed`,m(r))}recordToolResult(e,t){let n=p(t[`tool.name`])??a,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=f(t[`tool.duration_ms`]);i!==void 0&&_(r,i,160),t[`tool.result.error`]===!0&&(this.failedToolCalls+=1,this.toolFailures.set(n,(this.toolFailures.get(n)??0)+1),this.countCategory(`tool_failure`),this.pushError(e,`${n}: tool call failed`,`tool`))}recordToolTokenSample(e){let t=f(e[`gen_ai.usage.total_tokens`]);if(t===void 0||t<=0)return;let n=p(e[`tool.name`])??a,r=this.toolTokens.get(n)??d();this.toolTokens.set(n,r),_(r,t,200)}recordApiError(e,t){this.errors+=1;let n=f(t[`http.response.status_code`]),r=String(t[`http.response.status_code`]);this.countCategory(n===void 0?o:b(n)),this.pushError(e,`provider responded ${r}`,r)}recordTurnUsage(e){this.turns+=1,this.tokens.input+=f(e[`gen_ai.usage.input_tokens`])??0,this.tokens.output+=f(e[`gen_ai.usage.output_tokens`])??0,this.tokens.total+=f(e[`gen_ai.usage.total_tokens`])??0,this.tokens.cacheRead+=f(e[`gen_ai.usage.cache_read_tokens`])??0,this.tokens.cacheWrite+=f(e[`gen_ai.usage.cache_write_tokens`])??0,this.cost+=f(e[`gen_ai.usage.cost`])??0}recordTurnFailure(e){let t=p(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:x(e),message:t,code:n}),this.errorLog.length>this.maxRecentErrors&&(this.errorLog.length=this.maxRecentErrors)}};export{t as API_ERROR_RECORD,S as LogMetricsAggregator,l as MAX_RETAINED_STATE_PER_TOOL,u as MAX_RETAINED_TOKEN_BUCKETS,e as TOOL_RESULT_RECORD,i as TOOL_TOKEN_SAMPLE_RECORD,r as TURN_FAILED_RECORD,n as TURN_FINISHED_RECORD};
|
|
2
|
+
//# sourceMappingURL=metrics.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metrics.mjs","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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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require("../services/findings.cjs"),t=require("./doomOverlay.cjs");let n=require("@earendil-works/pi-tui");const r=[`session`,`agent`,`workflow-name`,`step`],i=[`day`,`week`,`month`,`all`],a=r[0],o=i[0],s=`SINK STATUS`,c=`AGENT_TELEMETRY_DISABLED`,l=`not connected`,u=[0,1,2],d=1e3,f=1e6,p=1e3,m={critical:`▲`,warning:`●`,info:`·`},h={critical:`error`,warning:`warning`,info:`dim`};function g(e,t){let r=(0,n.visibleWidth)(e);return r>=t?(0,n.truncateToWidth)(e,t,`…`):e+` `.repeat(t-r)}function _(e,t){let r=(0,n.visibleWidth)(e);return r>=t?(0,n.truncateToWidth)(e,t):` `.repeat(t-r)+e}function v(e){return e.toLocaleString(`en-US`)}function y(e){return e>=f?`${(e/f).toFixed(1)}M`:e>=p?`${Math.round(e/p)}k`:String(Math.round(e))}function b(e){return`${Math.round(e*100)}%`}function x(e){return e===void 0?`—`:e>=d?`${(e/d).toFixed(2)}s`:`${Math.round(e)}ms`}function S(e){return new Date(e).toTimeString().slice(0,5)}function C(e,t,n){let r=Math.min(n,Math.max(+(e>0),Math.round(e/t*n)));return`█`.repeat(r)+`░`.repeat(n-r)}function w(e,t,r){return(0,n.truncateToWidth)(g(e,14)+t,r)}function T(e,t){return t>0?e/t:0}function E(e,t,n,r){return[g(e,r),g(t,r),g(n,r)]}function D(e,t,n,r,i,a){return[g(e,i),g(a.fg(r?`success`:`warning`,t),i),g(n,i)]}function O(e,t,n){let r=Math.max(1,Math.floor(t/6)),{tokens:i,turns:a}=e,o=T(i.cacheRead,i.cacheRead+i.input),s=T(e.failedToolCalls,e.toolCalls),c=e.errors+e.abortedTurns+e.failedTurns,l=e.failureCategories[0],d=[E(`TOKENS`,y(i.total),`↑${y(i.input)} ↓${y(i.output)}`,r),D(`CACHE`,b(o),`${y(i.cacheRead)} read`,o>=.3,r,n),E(`COST`,`$${e.cost.toFixed(2)}`,a>0?`$${(e.cost/a).toFixed(3)}/turn`:`this session`,r),E(`TURNS`,v(a),a>0?`${y(i.total/a)} tok/turn`:`—`,r),D(`TOOL FAIL`,b(s),`${e.failedToolCalls} of ${e.toolCalls}`,s<=.05,r,n),D(`ISSUES`,v(c),l?l.name:`clean`,c===0,r,n)];return u.map(e=>g(d.map(t=>t[e]??``).join(``),t))}function k(e,t){let n=t>0?e/t:0;return n>=.66?`error`:n>=.33?`warning`:`success`}function A(e,t,r){let i=Math.max(1,t-2-9-25);return e.slice(0,5).map(e=>(0,n.truncateToWidth)(r.fg(h[e.severity],g(m[e.severity],2))+g(e.subject,9)+g(e.detail,25)+r.fg(`dim`,g(e.action,i)),t))}function j(e,t,r,i){let a=e.slice(0,6),o=Math.max(1,r-10-8-6-6-8-1),s=Math.max(...a.map(e=>e.p90Tokens??0),1);return a.map(e=>{let a=e.p90Tokens??0,c=i.fg(k(a,s),C(a,s,o)),l=T(e.failed,e.calls),u=l>0?i.fg(`error`,_(b(l),6)):_(`0%`,6);return(0,n.truncateToWidth)(g(e.name,10)+c+` `+_(e.p90Tokens===void 0?`—`:y(a),8)+_(v(e.calls),6)+u+i.fg(`dim`,_(x(t.get(e.name)),8)),r)})}function M(e,t){let r=Math.max(1,t-6-12-5);return e.recentErrors.slice(0,5).map(e=>(0,n.truncateToWidth)(g(S(e.at),6)+g(e.event,12)+g(e.message,r)+_(e.code,5),t))}function N(e,t,n){let r=n?[w(`diagnostic`,n,t)]:[];return e?[w(`backend`,`${e.backend} · ${e.endpointSource}`,t),w(`endpoint`,e.endpoint,t),...r]:[w(`status`,l,t),w(`metrics`,`aggregated in-process anyway`,t),...r]}function P(e,t){return e?[w(`service`,e.service,t),w(`backend`,e.backend,t),w(`endpoint`,`${e.endpointSource} · ${e.endpoint}`,t),w(`traces`,`${e.traces?`on`:`off`} · AGENT_OTEL_TRACES`,t),w(`redaction`,`${e.redaction?`on`:`off`} · metadata only`,t),w(`file fallback`,e.fileFallback?`allowed`:`disabled`,t)]:[w(`status`,l,t),w(`reason`,`no sink handle this session`,t)]}function F(e,t,r){let i=e?.groups.slice(0,6)??[];if(i.length===0)return[];let a=Math.max(1,t-20-12-8-1),o=Math.max(...i.map(e=>e.totalTokens),1);return i.map(e=>{let i=r.fg(e.issueCount>0?`error`:`accent`,C(e.totalTokens,o,a));return(0,n.truncateToWidth)(`${g(e.key,20)}${g(e.agentName??`—`,12)}${i} ${_(y(e.totalTokens),8)}`,t)})}function I(e,t,r){let i=e?.timeline.slice(-8)??[];if(i.length===0)return[];let a=Math.max(1,t-17-8-1),o=Math.max(...i.map(e=>e.totalTokens),1);return i.map(e=>{let i=r.fg(`success`,C(e.totalTokens,o,a));return(0,n.truncateToWidth)(`${g(e.label,17)}${i} ${_(y(e.totalTokens),8)}`,t)})}function L(e,t){if(t<1)return[e];let n=[],r=``;for(let i of e.split(` `)){let e=r.length===0?i:`${r} ${i}`;e.length>t&&r.length>0?(n.push(r),r=i):r=e}return r.length>0&&n.push(r),n.map(e=>g(e,t))}var R=class extends t.DoomOverlay{getView;done;timer;query;disposed=!1;groupIndex=0;periodIndex=0;requestId=0;loading=!1;showDetail=!1;report;reportError;constructor(e,t,n,r,i){super(e,t),this.getView=n,this.done=r,this.query=i,this.timer=setInterval(()=>{this.disposed||this.tui.requestRender()},1e3),this.timer.unref?.(),this.fetchReport()}handleInput(e){if((0,n.matchesKey)(e,`escape`)||(0,n.matchesKey)(e,`ctrl+c`)||(0,n.matchesKey)(e,`q`)){this.done(void 0);return}let t=e.toLowerCase();if(t===`?`){this.showDetail=!this.showDetail,this.tui.requestRender();return}if(t===`g`){this.groupIndex=(this.groupIndex+1)%r.length,this.refresh();return}if(t===`p`){this.periodIndex=(this.periodIndex+1)%i.length,this.refresh();return}t===`r`&&this.refresh()}refresh(){this.fetchReport(),this.tui.requestRender()}fetchReport(){if(!this.query)return;let e=++this.requestId;this.loading=!0,this.reportError=void 0,this.query({groupBy:this.groupBy(),period:this.period(),limit:6}).then(t=>{this.disposed||e!==this.requestId||(this.report=t,this.loading=!1,this.tui.requestRender())}).catch(t=>{this.disposed||e!==this.requestId||(this.report=void 0,this.reportError=t instanceof Error?t.message:String(t),this.loading=!1,this.tui.requestRender())})}consumerPanel(e){let t=this.theme.fg(`accent`,`TOP CONSUMERS · by ${this.groupBy()} · ${this.period()}`),n=F(this.report,e,this.theme);return[t,...n.length>0?n:[this.theme.fg(`dim`,this.historyStatus())]]}burnPanel(e){let t=this.report?.bucket??``,n=this.theme.fg(`accent`,`TOKEN BURN${t?` · per ${t}`:``}`),r=I(this.report,e,this.theme);return[n,...r.length>0?r:[this.theme.fg(`dim`,this.historyStatus())]]}findingsPanel(t,n){let r=A(e.deriveFindings(t),n,this.theme);return[this.theme.fg(`accent`,`WHAT TO FIX · derived from this session`),...r.length>0?r:[this.theme.fg(`dim`,`no records yet`)]]}toolCostPanel(e,t){let n=new Map(e.toolLatency.flatMap(e=>e.p95Ms===void 0?[]:[[e.name,e.p95Ms]])),r=j(e.toolCost,n,t,this.theme);return[this.theme.fg(`accent`,`TOOL COST · p90 turn tokens · calls · fail% · p95`),...r.length>0?r:[this.theme.fg(`dim`,`no tool calls yet`)]]}errorsPanel(e,t){let n=M(e,t);return[this.theme.fg(`accent`,`RECENT ERRORS`),...n.length>0?n:[this.theme.fg(`dim`,`none`)]]}groupBy(){return r[this.groupIndex]??a}period(){return i[this.periodIndex]??o}historyStatus(){return this.loading?`loading…`:this.reportError?this.reportError:this.report?`${this.transportLabel()} · ${this.report.totals.groupCount} groups`:`no sink history available`}transportLabel(){return this.getView().transport??`sink`}dispose(){this.disposed=!0,clearInterval(this.timer)}getChrome(){return{title:`LOG METRICS`,accent:t.DOOM_OVERLAY_ACCENT,breadcrumb:`SPC › h / help › l / logs`,headerRight:`doom-log · service pi · this session`,footer:`r refresh · g group · p period · ? detail · esc close`,footerHints:[[`r`,`refresh`],[`g`,`group`],[`p`,`period`],[`?`,`detail`],[`esc`,`close`]]}}renderBody(e,t){let n=this.getView();return n.disabled?this.disabledBody(e):this.showDetail?this.detailBody(n,e).slice(0,t):this.metricsBody(n,e,t)}disabledBody(e){return[(0,n.truncateToWidth)(this.theme.fg(`warning`,` Metrics collection is disabled for this session.`),e),(0,n.truncateToWidth)(this.theme.fg(`dim`,` ${c}=1 is set, so no log records are produced.`),e),(0,n.truncateToWidth)(this.theme.fg(`dim`,` Unset ${c} and restart Pi to collect metrics.`),e)]}detailBody(e,t){let n=Math.max(1,t-1);return[this.theme.fg(`accent`,s),...P(e.sink,n),``,this.theme.fg(`accent`,`COLLECTION`),w(`source`,`in-process Pi events`,n),w(`scope`,`current session only`,n),...L(`Headline counters are aggregated in-process from this session. TOP CONSUMERS and TOKEN BURN come from the sink database, so they span every agent and workflow in the selected period, not just this session.`,n),``,this.theme.fg(`accent`,`TOOL COST ATTRIBUTION`),...L(`TOOL COST reports the p90 total tokens of the turns a tool took part in, which is the only attribution Pi emits. A turn running several tools attributes its total to each of them, so the column ranks tools by the weight of the turns they appear in — it is not a per-tool share of spend.`,n)].map(e=>g(` ${e}`,t))}metricsBody(e,t,n){let r=O(e.snapshot,t,this.theme);if(n<=r.length)return r.slice(0,n);if(t<72)return[...r,``,...this.singleColumnPanels(e,t)].slice(0,n);let i=Math.max(24,Math.floor(t*.4)),a=Math.max(1,t-i-1),o=Math.max(1,a-1),c=Math.max(1,i-1),l=[...this.findingsPanel(e.snapshot,o),``,...this.toolCostPanel(e.snapshot,o),``,...this.consumerPanel(o)],u=[...this.burnPanel(c),``,...this.errorsPanel(e.snapshot,c),``,this.theme.fg(`accent`,s),...N(e.sink,c,e.lastDiagnostic)],d=Math.max(0,n-r.length-1),f=[this.theme.fg(`borderMuted`,`${`─`.repeat(a)}┬${`─`.repeat(i)}`)];for(let e=0;e<d;e++)f.push(g(` ${l[e]??``}`,a)+this.theme.fg(`borderMuted`,`│`)+g(` ${u[e]??``}`,i));return[...r,...f].slice(0,n)}singleColumnPanels(e,t){return[...this.findingsPanel(e.snapshot,t),``,...this.toolCostPanel(e.snapshot,t),``,...this.errorsPanel(e.snapshot,t),``,this.theme.fg(`accent`,s),...N(e.sink,t,e.lastDiagnostic)]}};exports.LogMetricsOverlayComponent=R;
|
|
2
|
+
//# sourceMappingURL=logMetricsOverlay.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logMetricsOverlay.cjs","names":["visibleWidth","truncateToWidth","DoomOverlay","matchesKey","deriveFindings","DOOM_OVERLAY_ACCENT"],"sources":["../../src/tui/logMetricsOverlay.ts"],"sourcesContent":["/**\n * The `SPC h l` Log Metrics overlay.\n *\n * The panel answers two questions and skips everything else: what is this\n * session costing, and what should change next. Raw plumbing counters\n * (per-record volume, sink wiring) are either derived into a finding or moved\n * behind `?`, because a number nobody can act on still costs a row.\n *\n * Rendering stays a pure `render(width)` so the layout can be asserted as text\n * without a live terminal. Every headline number comes from the in-process\n * aggregator, so the panels keep working when no sink is connected; only the\n * history panels degrade.\n */\n\nimport type { LogMetricsGroupBy, LogMetricsPeriod, LogMetricsReport } from '@agimon-ai/log-sink-mcp';\nimport type { Theme } from '@earendil-works/pi-coding-agent';\nimport { matchesKey, truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';\nimport { deriveFindings, type LogMetricsFinding, type LogMetricsFindingSeverity } from '../services/findings.ts';\nimport type { LogMetricsSnapshot, LogMetricsToolCost } from '../services/metrics';\nimport type { MetricsQuery, MetricsTransport } from '../types/metricsSource.ts';\nimport { DOOM_OVERLAY_ACCENT, DoomOverlay, type DoomOverlayChrome, type DoomOverlayTui } from './doomOverlay.ts';\n\nexport interface SinkStatus {\n service: string;\n backend: string;\n endpoint: string;\n endpointSource: string;\n traces: boolean;\n redaction: boolean;\n fileFallback: boolean;\n}\n\nexport interface LogMetricsView {\n disabled: boolean;\n snapshot: LogMetricsSnapshot;\n sink: SinkStatus | undefined;\n /** Which transport answered the last history query, for the panel status line. */\n transport?: MetricsTransport;\n /**\n * Most recent telemetry diagnostic. Surfaced here because writing it to stderr\n * would corrupt the TUI frame.\n */\n lastDiagnostic?: string;\n}\n\nconst TITLE = 'LOG METRICS';\nconst BREADCRUMB = 'SPC › h / help › l / logs';\nconst SUBTITLE = 'doom-log · service pi · this session';\nconst FOOTER = 'r refresh · g group · p period · ? detail · esc close';\nconst CONSUMERS_TITLE = 'TOP CONSUMERS';\nconst BURN_TITLE = 'TOKEN BURN';\n/** Dimensions that actually carry attribution today; workflow/step stay for runner-driven work. */\nconst GROUP_CYCLE = ['session', 'agent', 'workflow-name', 'step'] as const satisfies readonly LogMetricsGroupBy[];\nconst PERIOD_CYCLE = ['day', 'week', 'month', 'all'] as const satisfies readonly LogMetricsPeriod[];\n/** Head of each cycle, so the fallback never restates a literal. */\nconst DEFAULT_GROUP = GROUP_CYCLE[0];\nconst DEFAULT_PERIOD = PERIOD_CYCLE[0];\nconst MAX_TOKEN_ROWS = 6;\nconst MAX_BURN_ROWS = 8;\nconst GROUP_KEY_WIDTH = 20;\nconst AGENT_WIDTH = 12;\nconst BURN_LABEL_WIDTH = 17;\nconst LOADING_TEXT = 'loading…';\nconst NO_HISTORY_TEXT = 'no sink history available';\nconst FINDINGS_TITLE = 'WHAT TO FIX · derived from this session';\nconst NO_FINDINGS_TEXT = 'no records yet';\nconst TOOL_COST_TITLE = 'TOOL COST · p90 turn tokens · calls · fail% · p95';\nconst NO_TOOL_COST_TEXT = 'no tool calls yet';\nconst ERRORS_TITLE = 'RECENT ERRORS';\nconst NO_ERRORS_TEXT = 'none';\nconst SINK_TITLE = 'SINK STATUS';\nconst DETAIL_TITLE = 'COLLECTION';\nconst DISABLED_ENV = 'AGENT_TELEMETRY_DISABLED';\nconst NOT_CONNECTED = 'not connected';\nconst EMPTY_VALUE = '—';\nconst ELLIPSIS = '…';\nconst THIS_SESSION = 'this session';\nconst TOGGLE_ON = 'on';\nconst TOGGLE_OFF = 'off';\nconst CLEAN_TEXT = 'clean';\n\nconst MIN_TWO_COLUMN_WIDTH = 72;\nconst RIGHT_COLUMN_RATIO = 0.4;\nconst MIN_RIGHT_COLUMN = 24;\nconst REFRESH_MS = 1000;\n\nconst STAT_CELLS = 6;\nconst STAT_ROW_INDEXES = [0, 1, 2];\nconst TOOL_NAME_WIDTH = 10;\nconst VALUE_WIDTH = 8;\nconst CALLS_WIDTH = 6;\nconst FAIL_WIDTH = 6;\nconst LATENCY_WIDTH = 8;\nconst SINK_LABEL_WIDTH = 14;\nconst ERROR_TIME_WIDTH = 6;\n/** One wider than the longest event name, so the message never abuts it. */\nconst ERROR_EVENT_WIDTH = 12;\nconst ERROR_CODE_WIDTH = 5;\nconst FINDING_GLYPH_WIDTH = 2;\nconst FINDING_SUBJECT_WIDTH = 9;\nconst FINDING_DETAIL_WIDTH = 25;\nconst MAX_TOOL_ROWS = 6;\nconst MAX_FINDING_ROWS = 5;\nconst MAX_ERROR_ROWS = 5;\n/** Bar color thresholds, as a fraction of the panel's most expensive tool. */\nconst COST_WARN_RATIO = 0.33;\nconst COST_ERROR_RATIO = 0.66;\n/** Below this the session is not caching effectively; above it, it is working. */\nconst CACHE_WARN_RATIO = 0.3;\nconst TOOL_FAIL_WARN_RATIO = 0.05;\n\nconst SECOND_MS = 1000;\nconst MILLION = 1_000_000;\nconst THOUSAND = 1000;\nconst PERCENT = 100;\nconst COMPACT_DECIMALS = 1;\nconst DURATION_DECIMALS = 2;\nconst COST_DECIMALS = 2;\nconst PER_TURN_DECIMALS = 3;\nconst TIME_SLICE_END = 5;\n\nconst SEVERITY_GLYPH: Record<LogMetricsFindingSeverity, string> = { critical: '▲', warning: '●', info: '·' };\nconst SEVERITY_COLOR: Record<LogMetricsFindingSeverity, 'error' | 'warning' | 'dim'> = {\n critical: 'error',\n warning: 'warning',\n info: 'dim',\n};\n\nfunction pad(text: string, width: number): string {\n const visible = visibleWidth(text);\n // An explicit ellipsis: the default '...' also emits a colour reset, which\n // leaks escape codes into the middle of a padded column.\n return visible >= width ? truncateToWidth(text, width, ELLIPSIS) : text + ' '.repeat(width - visible);\n}\n\nfunction padStart(text: string, width: number): string {\n const visible = visibleWidth(text);\n return visible >= width ? truncateToWidth(text, width) : ' '.repeat(width - visible) + text;\n}\n\nfunction formatCount(value: number): string {\n return value.toLocaleString('en-US');\n}\n\nfunction formatCompact(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 formatPercent(ratio: number): string {\n return `${Math.round(ratio * PERCENT)}%`;\n}\n\nfunction formatDuration(ms: number | undefined): string {\n if (ms === undefined) return EMPTY_VALUE;\n if (ms >= SECOND_MS) return `${(ms / SECOND_MS).toFixed(DURATION_DECIMALS)}s`;\n return `${Math.round(ms)}ms`;\n}\n\nfunction formatTime(at: number): string {\n return new Date(at).toTimeString().slice(0, TIME_SLICE_END);\n}\n\nfunction bar(value: number, max: number, width: number): string {\n const filled = Math.min(width, Math.max(value > 0 ? 1 : 0, Math.round((value / max) * width)));\n return '█'.repeat(filled) + '░'.repeat(width - filled);\n}\n\nfunction labelled(label: string, value: string, width: number): string {\n return truncateToWidth(pad(label, SINK_LABEL_WIDTH) + value, width);\n}\n\nfunction ratio(numerator: number, denominator: number): number {\n return denominator > 0 ? numerator / denominator : 0;\n}\n\nfunction statCell(label: string, value: string, sublabel: string, width: number): string[] {\n return [pad(label, width), pad(value, width), pad(sublabel, width)];\n}\n\n/**\n * Colours the headline value so the two cells with a defensible threshold —\n * cache hit rate and tool failure rate — read at a glance. The others are\n * workload-dependent, and a colour on them would imply a target that does not\n * exist.\n */\nfunction judgedCell(\n label: string,\n value: string,\n sublabel: string,\n healthy: boolean,\n width: number,\n theme: Theme,\n): string[] {\n return [pad(label, width), pad(theme.fg(healthy ? 'success' : 'warning', value), width), pad(sublabel, width)];\n}\n\n/**\n * The six headline cells. Every one is a lever the user can pull: what the\n * session cost, how much of it was avoidable, and how much was thrown away.\n * Raw event and record counts were dropped — they moved with the workload and\n * nothing followed from them.\n */\nfunction statRows(snapshot: LogMetricsSnapshot, width: number, theme: Theme): string[] {\n const cellWidth = Math.max(1, Math.floor(width / STAT_CELLS));\n const { tokens, turns } = snapshot;\n const cacheHit = ratio(tokens.cacheRead, tokens.cacheRead + tokens.input);\n const failRatio = ratio(snapshot.failedToolCalls, snapshot.toolCalls);\n const issues = snapshot.errors + snapshot.abortedTurns + snapshot.failedTurns;\n const topCategory = snapshot.failureCategories[0];\n const cells = [\n statCell(\n 'TOKENS',\n formatCompact(tokens.total),\n `↑${formatCompact(tokens.input)} ↓${formatCompact(tokens.output)}`,\n cellWidth,\n ),\n judgedCell(\n 'CACHE',\n formatPercent(cacheHit),\n `${formatCompact(tokens.cacheRead)} read`,\n cacheHit >= CACHE_WARN_RATIO,\n cellWidth,\n theme,\n ),\n statCell(\n 'COST',\n `$${snapshot.cost.toFixed(COST_DECIMALS)}`,\n turns > 0 ? `$${(snapshot.cost / turns).toFixed(PER_TURN_DECIMALS)}/turn` : THIS_SESSION,\n cellWidth,\n ),\n statCell(\n 'TURNS',\n formatCount(turns),\n turns > 0 ? `${formatCompact(tokens.total / turns)} tok/turn` : EMPTY_VALUE,\n cellWidth,\n ),\n judgedCell(\n 'TOOL FAIL',\n formatPercent(failRatio),\n `${snapshot.failedToolCalls} of ${snapshot.toolCalls}`,\n failRatio <= TOOL_FAIL_WARN_RATIO,\n cellWidth,\n theme,\n ),\n judgedCell(\n 'ISSUES',\n formatCount(issues),\n topCategory ? topCategory.name : CLEAN_TEXT,\n issues === 0,\n cellWidth,\n theme,\n ),\n ];\n return STAT_ROW_INDEXES.map((row) => pad(cells.map((cell) => cell[row] ?? '').join(''), width));\n}\n\n/** Cheap tools read as `success`, expensive ones as `error`. */\nfunction costColor(value: number, max: number): 'success' | 'warning' | 'error' {\n const share = max > 0 ? value / max : 0;\n if (share >= COST_ERROR_RATIO) return 'error';\n if (share >= COST_WARN_RATIO) return 'warning';\n return 'success';\n}\n\nfunction findingRows(findings: readonly LogMetricsFinding[], width: number, theme: Theme): string[] {\n const actionWidth = Math.max(1, width - FINDING_GLYPH_WIDTH - FINDING_SUBJECT_WIDTH - FINDING_DETAIL_WIDTH);\n return findings\n .slice(0, MAX_FINDING_ROWS)\n .map((finding) =>\n truncateToWidth(\n theme.fg(SEVERITY_COLOR[finding.severity], pad(SEVERITY_GLYPH[finding.severity], FINDING_GLYPH_WIDTH)) +\n pad(finding.subject, FINDING_SUBJECT_WIDTH) +\n pad(finding.detail, FINDING_DETAIL_WIDTH) +\n theme.fg('dim', pad(finding.action, actionWidth)),\n width,\n ),\n );\n}\n\n/**\n * Ranked by cost per call. Latency stays as the trailing column because it is\n * occasionally the answer, but it no longer decides the ordering: a 55ms grep\n * that drags 38k tokens into the turn is the row worth reading.\n */\nfunction toolCostRows(\n tools: readonly LogMetricsToolCost[],\n latency: Map<string, number>,\n width: number,\n theme: Theme,\n): string[] {\n const entries = tools.slice(0, MAX_TOOL_ROWS);\n const barWidth = Math.max(1, width - TOOL_NAME_WIDTH - VALUE_WIDTH - CALLS_WIDTH - FAIL_WIDTH - LATENCY_WIDTH - 1);\n const max = Math.max(...entries.map((entry) => entry.p90Tokens ?? 0), 1);\n return entries.map((entry) => {\n const tokens = entry.p90Tokens ?? 0;\n const barText = theme.fg(costColor(tokens, max), bar(tokens, max, barWidth));\n const failRatio = ratio(entry.failed, entry.calls);\n const fail =\n failRatio > 0 ? theme.fg('error', padStart(formatPercent(failRatio), FAIL_WIDTH)) : padStart('0%', FAIL_WIDTH);\n return truncateToWidth(\n pad(entry.name, TOOL_NAME_WIDTH) +\n barText +\n ' ' +\n padStart(entry.p90Tokens === undefined ? EMPTY_VALUE : formatCompact(tokens), VALUE_WIDTH) +\n padStart(formatCount(entry.calls), CALLS_WIDTH) +\n fail +\n theme.fg('dim', padStart(formatDuration(latency.get(entry.name)), LATENCY_WIDTH)),\n width,\n );\n });\n}\n\nfunction errorRows(snapshot: LogMetricsSnapshot, width: number): string[] {\n const messageWidth = Math.max(1, width - ERROR_TIME_WIDTH - ERROR_EVENT_WIDTH - ERROR_CODE_WIDTH);\n return snapshot.recentErrors\n .slice(0, MAX_ERROR_ROWS)\n .map((entry) =>\n truncateToWidth(\n pad(formatTime(entry.at), ERROR_TIME_WIDTH) +\n pad(entry.event, ERROR_EVENT_WIDTH) +\n pad(entry.message, messageWidth) +\n padStart(entry.code, ERROR_CODE_WIDTH),\n width,\n ),\n );\n}\n\n/**\n * Three lines instead of six. Backend and endpoint answer \"is anything being\n * written\"; the rest of the wiring only matters when it is broken, so it moved\n * behind `?`.\n */\nfunction sinkRows(sink: SinkStatus | undefined, width: number, lastDiagnostic?: string): string[] {\n /** Diagnostics can land before a handle exists, so both branches report them. */\n const diagnosticRows = lastDiagnostic ? [labelled('diagnostic', lastDiagnostic, width)] : [];\n if (!sink) {\n return [\n labelled('status', NOT_CONNECTED, width),\n labelled('metrics', 'aggregated in-process anyway', width),\n ...diagnosticRows,\n ];\n }\n return [\n labelled('backend', `${sink.backend} · ${sink.endpointSource}`, width),\n labelled('endpoint', sink.endpoint, width),\n ...diagnosticRows,\n ];\n}\n\n/** Only fields NodeTelemetryHandle can actually supply; it exposes no queue counters. */\nfunction sinkDetailRows(sink: SinkStatus | undefined, width: number): string[] {\n if (!sink)\n return [labelled('status', NOT_CONNECTED, width), labelled('reason', 'no sink handle this session', width)];\n return [\n labelled('service', sink.service, width),\n labelled('backend', sink.backend, width),\n labelled('endpoint', `${sink.endpointSource} · ${sink.endpoint}`, width),\n labelled('traces', `${sink.traces ? TOGGLE_ON : TOGGLE_OFF} · AGENT_OTEL_TRACES`, width),\n labelled('redaction', `${sink.redaction ? TOGGLE_ON : TOGGLE_OFF} · metadata only`, width),\n labelled('file fallback', sink.fileFallback ? 'allowed' : 'disabled', width),\n ];\n}\n\nconst COLLECTION_NOTE =\n 'Headline counters are aggregated in-process from this session. TOP CONSUMERS and ' +\n 'TOKEN BURN come from the sink database, so they span every agent and workflow in ' +\n 'the selected period, not just this session.';\n\nconst ATTRIBUTION_NOTE =\n 'TOOL COST reports the p90 total tokens of the turns a tool took part in, which is the ' +\n 'only attribution Pi emits. A turn running several tools attributes its total to each of ' +\n 'them, so the column ranks tools by the weight of the turns they appear in — it is not a ' +\n 'per-tool share of spend.';\n\n/** Ranked token consumers for the selected dimension. */\nfunction consumerRows(report: LogMetricsReport | undefined, width: number, theme: Theme): string[] {\n const groups = report?.groups.slice(0, MAX_TOKEN_ROWS) ?? [];\n if (groups.length === 0) return [];\n\n const barWidth = Math.max(1, width - GROUP_KEY_WIDTH - AGENT_WIDTH - VALUE_WIDTH - 1);\n const max = Math.max(...groups.map((group) => group.totalTokens), 1);\n return groups.map((group) => {\n const barText = theme.fg(group.issueCount > 0 ? 'error' : 'accent', bar(group.totalTokens, max, barWidth));\n return truncateToWidth(\n `${pad(group.key, GROUP_KEY_WIDTH)}${pad(group.agentName ?? EMPTY_VALUE, AGENT_WIDTH)}${barText} ${padStart(\n formatCompact(group.totalTokens),\n VALUE_WIDTH,\n )}`,\n width,\n );\n });\n}\n\n/** Token burn per timeline bucket, newest last so the trend reads left to right. */\nfunction burnRows(report: LogMetricsReport | undefined, width: number, theme: Theme): string[] {\n const buckets = report?.timeline.slice(-MAX_BURN_ROWS) ?? [];\n if (buckets.length === 0) return [];\n\n const barWidth = Math.max(1, width - BURN_LABEL_WIDTH - VALUE_WIDTH - 1);\n const max = Math.max(...buckets.map((bucket) => bucket.totalTokens), 1);\n return buckets.map((bucket) => {\n const barText = theme.fg('success', bar(bucket.totalTokens, max, barWidth));\n // `label` is the local-time rendering; bucketStart serialises to UTC.\n return truncateToWidth(\n `${pad(bucket.label, BURN_LABEL_WIDTH)}${barText} ${padStart(formatCompact(bucket.totalTokens), VALUE_WIDTH)}`,\n width,\n );\n });\n}\n\n/** Greedy word wrap: no library dependency for two static paragraphs. */\nfunction wrapText(text: string, width: number): string[] {\n if (width < 1) return [text];\n const lines: string[] = [];\n let current = '';\n for (const word of text.split(' ')) {\n const candidate = current.length === 0 ? word : `${current} ${word}`;\n if (candidate.length > width && current.length > 0) {\n lines.push(current);\n current = word;\n } else {\n current = candidate;\n }\n }\n if (current.length > 0) lines.push(current);\n return lines.map((line) => pad(line, width));\n}\n\nexport class LogMetricsOverlayComponent extends DoomOverlay {\n private readonly getView: () => LogMetricsView;\n private readonly done: (result: undefined) => void;\n private readonly timer: ReturnType<typeof setInterval>;\n private readonly query: MetricsQuery | undefined;\n private disposed = false;\n private groupIndex = 0;\n private periodIndex = 0;\n private requestId = 0;\n private loading = false;\n private showDetail = false;\n private report: LogMetricsReport | undefined;\n private reportError: string | undefined;\n\n constructor(\n tui: DoomOverlayTui,\n theme: Theme,\n getView: () => LogMetricsView,\n done: (result: undefined) => void,\n query?: MetricsQuery,\n ) {\n super(tui, theme);\n this.getView = getView;\n this.done = done;\n this.query = query;\n this.timer = setInterval(() => {\n if (!this.disposed) this.tui.requestRender();\n }, REFRESH_MS);\n this.timer.unref?.();\n this.fetchReport();\n }\n\n handleInput(data: string): void {\n if (matchesKey(data, 'escape') || matchesKey(data, 'ctrl+c') || matchesKey(data, 'q')) {\n this.done(undefined);\n return;\n }\n\n const key = data.toLowerCase();\n if (key === '?') {\n this.showDetail = !this.showDetail;\n this.tui.requestRender();\n return;\n }\n if (key === 'g') {\n this.groupIndex = (this.groupIndex + 1) % GROUP_CYCLE.length;\n this.refresh();\n return;\n }\n if (key === 'p') {\n this.periodIndex = (this.periodIndex + 1) % PERIOD_CYCLE.length;\n this.refresh();\n return;\n }\n if (key === 'r') this.refresh();\n }\n\n /** Re-reads the live session counters, and restarts the history query behind them. */\n private refresh(): void {\n this.fetchReport();\n this.tui.requestRender();\n }\n\n /**\n * Sink history is fetched out of band: the query crosses a process boundary\n * and takes seconds on a busy database, so render never waits on it. Stale\n * responses are dropped by sequence number when the dimension changes mid\n * flight.\n */\n private fetchReport(): void {\n if (!this.query) return;\n\n const requestId = ++this.requestId;\n this.loading = true;\n this.reportError = undefined;\n this.query({ groupBy: this.groupBy(), period: this.period(), limit: MAX_TOKEN_ROWS })\n .then((report) => {\n if (this.disposed || requestId !== this.requestId) return;\n this.report = report;\n this.loading = false;\n this.tui.requestRender();\n })\n .catch((error: unknown) => {\n if (this.disposed || requestId !== this.requestId) return;\n this.report = undefined;\n this.reportError = error instanceof Error ? error.message : String(error);\n this.loading = false;\n this.tui.requestRender();\n });\n }\n\n /** Ranked consumers for the selected dimension, sourced from the sink database. */\n private consumerPanel(width: number): string[] {\n const heading = this.theme.fg('accent', `${CONSUMERS_TITLE} · by ${this.groupBy()} · ${this.period()}`);\n const rows = consumerRows(this.report, width, this.theme);\n return [heading, ...(rows.length > 0 ? rows : [this.theme.fg('dim', this.historyStatus())])];\n }\n\n private burnPanel(width: number): string[] {\n const bucket = this.report?.bucket ?? '';\n const heading = this.theme.fg('accent', `${BURN_TITLE}${bucket ? ` · per ${bucket}` : ''}`);\n const rows = burnRows(this.report, width, this.theme);\n return [heading, ...(rows.length > 0 ? rows : [this.theme.fg('dim', this.historyStatus())])];\n }\n\n private findingsPanel(snapshot: LogMetricsSnapshot, width: number): string[] {\n const rows = findingRows(deriveFindings(snapshot), width, this.theme);\n return [\n this.theme.fg('accent', FINDINGS_TITLE),\n ...(rows.length > 0 ? rows : [this.theme.fg('dim', NO_FINDINGS_TEXT)]),\n ];\n }\n\n private toolCostPanel(snapshot: LogMetricsSnapshot, width: number): string[] {\n const latency = new Map(\n snapshot.toolLatency.flatMap((entry) => (entry.p95Ms === undefined ? [] : [[entry.name, entry.p95Ms] as const])),\n );\n const rows = toolCostRows(snapshot.toolCost, latency, width, this.theme);\n return [\n this.theme.fg('accent', TOOL_COST_TITLE),\n ...(rows.length > 0 ? rows : [this.theme.fg('dim', NO_TOOL_COST_TEXT)]),\n ];\n }\n\n private errorsPanel(snapshot: LogMetricsSnapshot, width: number): string[] {\n const rows = errorRows(snapshot, width);\n return [\n this.theme.fg('accent', ERRORS_TITLE),\n ...(rows.length > 0 ? rows : [this.theme.fg('dim', NO_ERRORS_TEXT)]),\n ];\n }\n\n private groupBy(): LogMetricsGroupBy {\n return GROUP_CYCLE[this.groupIndex] ?? DEFAULT_GROUP;\n }\n\n private period(): LogMetricsPeriod {\n return PERIOD_CYCLE[this.periodIndex] ?? DEFAULT_PERIOD;\n }\n\n /** One status line for the history panels: what is shown, from where. */\n private historyStatus(): string {\n if (this.loading) return LOADING_TEXT;\n if (this.reportError) return this.reportError;\n if (!this.report) return NO_HISTORY_TEXT;\n return `${this.transportLabel()} · ${this.report.totals.groupCount} groups`;\n }\n\n private transportLabel(): MetricsTransport | 'sink' {\n return this.getView().transport ?? 'sink';\n }\n\n dispose(): void {\n this.disposed = true;\n clearInterval(this.timer);\n }\n\n protected getChrome(): DoomOverlayChrome {\n return {\n title: TITLE,\n accent: DOOM_OVERLAY_ACCENT,\n breadcrumb: BREADCRUMB,\n headerRight: SUBTITLE,\n footer: FOOTER,\n footerHints: [\n ['r', 'refresh'],\n ['g', 'group'],\n ['p', 'period'],\n ['?', 'detail'],\n ['esc', 'close'],\n ],\n };\n }\n\n protected renderBody(width: number, height: number): string[] {\n const view = this.getView();\n if (view.disabled) return this.disabledBody(width);\n if (this.showDetail) return this.detailBody(view, width).slice(0, height);\n return this.metricsBody(view, width, height);\n }\n\n /**\n * An explicit disabled state: zeroed stat cells would read as a real session\n * that happened to do nothing.\n */\n private disabledBody(width: number): string[] {\n return [\n truncateToWidth(this.theme.fg('warning', ' Metrics collection is disabled for this session.'), width),\n truncateToWidth(this.theme.fg('dim', ` ${DISABLED_ENV}=1 is set, so no log records are produced.`), width),\n truncateToWidth(this.theme.fg('dim', ` Unset ${DISABLED_ENV} and restart Pi to collect metrics.`), width),\n ];\n }\n\n /**\n * The `?` pane. Sink wiring and the caveats behind each panel live here so the\n * default view can spend its rows on numbers instead of explaining itself.\n */\n private detailBody(view: LogMetricsView, width: number): string[] {\n const content = Math.max(1, width - 1);\n return [\n this.theme.fg('accent', SINK_TITLE),\n ...sinkDetailRows(view.sink, content),\n '',\n this.theme.fg('accent', DETAIL_TITLE),\n labelled('source', 'in-process Pi events', content),\n labelled('scope', 'current session only', content),\n ...wrapText(COLLECTION_NOTE, content),\n '',\n this.theme.fg('accent', 'TOOL COST ATTRIBUTION'),\n ...wrapText(ATTRIBUTION_NOTE, content),\n ].map((line) => pad(` ${line}`, width));\n }\n\n private metricsBody(view: LogMetricsView, width: number, height: number): string[] {\n const stats = statRows(view.snapshot, width, this.theme);\n if (height <= stats.length) return stats.slice(0, height);\n\n if (width < MIN_TWO_COLUMN_WIDTH) {\n return [...stats, '', ...this.singleColumnPanels(view, width)].slice(0, height);\n }\n\n const rightWidth = Math.max(MIN_RIGHT_COLUMN, Math.floor(width * RIGHT_COLUMN_RATIO));\n const leftWidth = Math.max(1, width - rightWidth - 1);\n\n // Each column reserves one leading space, so rows are built one narrower.\n const leftContent = Math.max(1, leftWidth - 1);\n const rightContent = Math.max(1, rightWidth - 1);\n const left = [\n ...this.findingsPanel(view.snapshot, leftContent),\n '',\n ...this.toolCostPanel(view.snapshot, leftContent),\n '',\n ...this.consumerPanel(leftContent),\n ];\n const right = [\n ...this.burnPanel(rightContent),\n '',\n ...this.errorsPanel(view.snapshot, rightContent),\n '',\n this.theme.fg('accent', SINK_TITLE),\n ...sinkRows(view.sink, rightContent, view.lastDiagnostic),\n ];\n\n const bodyHeight = Math.max(0, height - stats.length - 1);\n const body = [this.theme.fg('borderMuted', `${'─'.repeat(leftWidth)}┬${'─'.repeat(rightWidth)}`)];\n for (let index = 0; index < bodyHeight; index++) {\n body.push(\n pad(` ${left[index] ?? ''}`, leftWidth) +\n this.theme.fg('borderMuted', '│') +\n pad(` ${right[index] ?? ''}`, rightWidth),\n );\n }\n return [...stats, ...body].slice(0, height);\n }\n\n private singleColumnPanels(view: LogMetricsView, width: number): string[] {\n return [\n ...this.findingsPanel(view.snapshot, width),\n '',\n ...this.toolCostPanel(view.snapshot, width),\n '',\n ...this.errorsPanel(view.snapshot, width),\n '',\n this.theme.fg('accent', SINK_TITLE),\n ...sinkRows(view.sink, width, view.lastDiagnostic),\n ];\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { LogMetricsSnapshot } from "../services/metrics.cjs";
|
|
2
|
+
import { MetricsQuery, MetricsTransport } from "../types/metricsSource.cjs";
|
|
3
|
+
import { DoomOverlay, DoomOverlayChrome, DoomOverlayTui } from "./doomOverlay.cjs";
|
|
4
|
+
import { Theme } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
//#region src/tui/logMetricsOverlay.d.ts
|
|
6
|
+
interface SinkStatus {
|
|
7
|
+
service: string;
|
|
8
|
+
backend: string;
|
|
9
|
+
endpoint: string;
|
|
10
|
+
endpointSource: string;
|
|
11
|
+
traces: boolean;
|
|
12
|
+
redaction: boolean;
|
|
13
|
+
fileFallback: boolean;
|
|
14
|
+
}
|
|
15
|
+
interface LogMetricsView {
|
|
16
|
+
disabled: boolean;
|
|
17
|
+
snapshot: LogMetricsSnapshot;
|
|
18
|
+
sink: SinkStatus | undefined;
|
|
19
|
+
/** Which transport answered the last history query, for the panel status line. */
|
|
20
|
+
transport?: MetricsTransport;
|
|
21
|
+
/**
|
|
22
|
+
* Most recent telemetry diagnostic. Surfaced here because writing it to stderr
|
|
23
|
+
* would corrupt the TUI frame.
|
|
24
|
+
*/
|
|
25
|
+
lastDiagnostic?: string;
|
|
26
|
+
}
|
|
27
|
+
declare class LogMetricsOverlayComponent extends DoomOverlay {
|
|
28
|
+
private readonly getView;
|
|
29
|
+
private readonly done;
|
|
30
|
+
private readonly timer;
|
|
31
|
+
private readonly query;
|
|
32
|
+
private disposed;
|
|
33
|
+
private groupIndex;
|
|
34
|
+
private periodIndex;
|
|
35
|
+
private requestId;
|
|
36
|
+
private loading;
|
|
37
|
+
private showDetail;
|
|
38
|
+
private report;
|
|
39
|
+
private reportError;
|
|
40
|
+
constructor(tui: DoomOverlayTui, theme: Theme, getView: () => LogMetricsView, done: (result: undefined) => void, query?: MetricsQuery);
|
|
41
|
+
handleInput(data: string): void;
|
|
42
|
+
/** Re-reads the live session counters, and restarts the history query behind them. */
|
|
43
|
+
private refresh;
|
|
44
|
+
/**
|
|
45
|
+
* Sink history is fetched out of band: the query crosses a process boundary
|
|
46
|
+
* and takes seconds on a busy database, so render never waits on it. Stale
|
|
47
|
+
* responses are dropped by sequence number when the dimension changes mid
|
|
48
|
+
* flight.
|
|
49
|
+
*/
|
|
50
|
+
private fetchReport;
|
|
51
|
+
/** Ranked consumers for the selected dimension, sourced from the sink database. */
|
|
52
|
+
private consumerPanel;
|
|
53
|
+
private burnPanel;
|
|
54
|
+
private findingsPanel;
|
|
55
|
+
private toolCostPanel;
|
|
56
|
+
private errorsPanel;
|
|
57
|
+
private groupBy;
|
|
58
|
+
private period;
|
|
59
|
+
/** One status line for the history panels: what is shown, from where. */
|
|
60
|
+
private historyStatus;
|
|
61
|
+
private transportLabel;
|
|
62
|
+
dispose(): void;
|
|
63
|
+
protected getChrome(): DoomOverlayChrome;
|
|
64
|
+
protected renderBody(width: number, height: number): string[];
|
|
65
|
+
/**
|
|
66
|
+
* An explicit disabled state: zeroed stat cells would read as a real session
|
|
67
|
+
* that happened to do nothing.
|
|
68
|
+
*/
|
|
69
|
+
private disabledBody;
|
|
70
|
+
/**
|
|
71
|
+
* The `?` pane. Sink wiring and the caveats behind each panel live here so the
|
|
72
|
+
* default view can spend its rows on numbers instead of explaining itself.
|
|
73
|
+
*/
|
|
74
|
+
private detailBody;
|
|
75
|
+
private metricsBody;
|
|
76
|
+
private singleColumnPanels;
|
|
77
|
+
}
|
|
78
|
+
//#endregion
|
|
79
|
+
export { LogMetricsOverlayComponent, LogMetricsView, SinkStatus };
|
|
80
|
+
//# sourceMappingURL=logMetricsOverlay.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logMetricsOverlay.d.cts","names":[],"sources":["../../src/tui/logMetricsOverlay.ts"],"mappings":";;;;;UAsBiB;EACf;EACA;EACA;EACA;EACA;EACA;EACA;;UAGe;EACf;EACA,UAAU;EACV,MAAM;;EAEN,YAAY;;;;;EAKZ;;cAoYW,mCAAmC;mBAC7B;mBACA;mBACA;mBACA;UACT;UACA;UACA;UACA;UACA;UACA;UACA;UACA;EAGN,YAAA,KAAK,gBACL,OAAO,OACP,eAAe,gBACf,OAAO,4BACP,QAAQ;EAaV,YAAY;;UA0BJ;;;;;;;UAWA;;UAuBA;UAMA;UAOA;UAQA;UAWA;UAQA;UAIA;;UAKA;UAOA;EAIR;YAKU,aAAa;YAiBb,WAAW,eAAe;;;;;UAW5B;;;;;UAYA;UAgBA;UA0CA"}
|