pi-editor-footer 0.5.0 → 0.6.0
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/CHANGELOG.md +8 -0
- package/dist/color-policy.js +13 -5
- package/dist/live-border.js +34 -11
- package/dist/telemetry.js +78 -2
- package/package.json +1 -1
- package/src/color-policy.ts +13 -5
- package/src/live-border.ts +51 -12
- package/src/telemetry.ts +80 -3
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.6.0] - 2026-08-24
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- Context usage colors now `12.5%`/`25%`/`50%` quotas — `dim` 0-12.5 → `accent` 12.5-25 → `warning` 25-50 → `error` 50-100 via `color-policy:contextUsageColor` (was `25%`/`50%`/`75%`), respects theme semantic tokens
|
|
12
|
+
- Live `↑`/`↓` tokens now per agent run (option B) — cumulative across turns in this agent via `telemetry:peekAgentLive()` + `live-border` top `↑`/`↓` (was per-turn via `getLastTurnTelemetry`), `getLastTelemetry` stays agent sum
|
|
13
|
+
- Stall relocated from bottom telemetry to top right of tool use with `dim |` pipe — `run-activity` now `tools | !2×3.3s` top, bottom `telemetry:formatTurnTelemetry` now `TPS · TTFT` only (suppressed `stalls:false`)
|
|
14
|
+
|
|
7
15
|
## [0.5.0] - 2026-08-22
|
|
8
16
|
|
|
9
17
|
### Changed
|
package/dist/color-policy.js
CHANGED
|
@@ -6,13 +6,21 @@ export function stressColor(value, warn = 70, danger = 90) {
|
|
|
6
6
|
return "accent";
|
|
7
7
|
}
|
|
8
8
|
export function contextUsageColor(pct) {
|
|
9
|
-
// 25
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
// 12.5 / 25 / 50 quotas — four urgency tiers, increasingly aggressive as context fills.
|
|
10
|
+
// 0 – 12.5% dim — plenty of headroom, visually quiet.
|
|
11
|
+
// 12.5 – 25% accent — first nudge, noticeable but calm.
|
|
12
|
+
// 25 – 50% warning — half consumed, needs attention.
|
|
13
|
+
// 50 – 100% error — critical, about to run out.
|
|
14
|
+
// Uses theme semantic tokens (dim/accent/warning/error) so the progression
|
|
15
|
+
// respects the active theme and remains legible on light/dark/custom palettes.
|
|
16
|
+
// A fixed hex palette (e.g. grey→sky→amber→red) would be more vivid but
|
|
17
|
+
// would ignore the user's theme and can clash with light backgrounds —
|
|
18
|
+
// semantic tokens keep the "aggressive" ordering while staying theme-coherent.
|
|
13
19
|
if (pct >= 50)
|
|
14
|
-
return "
|
|
20
|
+
return "error";
|
|
15
21
|
if (pct >= 25)
|
|
22
|
+
return "warning";
|
|
23
|
+
if (pct >= 12.5)
|
|
16
24
|
return "accent";
|
|
17
25
|
return "dim";
|
|
18
26
|
}
|
package/dist/live-border.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import { createChromeSnapshot, formatTopContextFromSnapshot, } from "./chrome-state.js";
|
|
14
14
|
import { resolveGlyphs, resolveIconMode } from "./icons.js";
|
|
15
15
|
import { formatRunActivityTopRight } from "./run-activity.js";
|
|
16
|
-
import { formatTelemetryTokens, formatTurnTelemetry } from "./telemetry.js";
|
|
16
|
+
import { formatTelemetryTokens, formatTurnDuration, formatTurnTelemetry, } from "./telemetry.js";
|
|
17
17
|
export const REFRESH_MS = 1000;
|
|
18
18
|
export class LiveBorder {
|
|
19
19
|
deps;
|
|
@@ -92,13 +92,33 @@ export class LiveBorder {
|
|
|
92
92
|
refreshTopBorder() {
|
|
93
93
|
const editor = this.deps.getEditor();
|
|
94
94
|
const ctx = this.deps.getCtx();
|
|
95
|
+
const cfg = this.deps.getConfig();
|
|
95
96
|
if (!editor || !ctx)
|
|
96
97
|
return;
|
|
97
98
|
try {
|
|
98
|
-
// SAFETY: theme is live pi TUI theme read at render time
|
|
99
|
-
const theme = ctx.ui.theme; // SAFETY: pi seam — intentional unsafe cast, validated at runtime
|
|
99
|
+
// SAFETY: theme is live pi TUI theme read at render time — intentional unsafe cast, validated at runtime
|
|
100
|
+
const theme = ctx.ui.theme; // SAFETY: pi seam — intentional unsafe cast, validated at runtime
|
|
100
101
|
const snap = this.deps.runActivityTracker.getSnapshot();
|
|
101
|
-
|
|
102
|
+
let text = formatRunActivityTopRight(snap, theme);
|
|
103
|
+
// Relocate stall to the right of tool use with pipe separator — agent-run live (option B), not bottom telemetry
|
|
104
|
+
if (cfg.telemetry.enabled && cfg.telemetry.stalls) {
|
|
105
|
+
// SAFETY: pi seam — intentional unsafe cast, validated at runtime — telemetry tracker for stall (agent run, option B)
|
|
106
|
+
const tracker = this.deps.telemetryTracker;
|
|
107
|
+
const tel = tracker.peekAgentLive() ?? tracker.getLastTelemetry();
|
|
108
|
+
if (tel && tel.stallMs > 0) {
|
|
109
|
+
const glyphs = resolveGlyphs(cfg.icons.mode);
|
|
110
|
+
// SAFETY: pi seam — intentional unsafe cast, validated at runtime — theme fg
|
|
111
|
+
const stallText = theme.fg("warning", `${glyphs.stall}${tel.stallCount}×${formatTurnDuration(tel.stallMs).trim()}`);
|
|
112
|
+
if (text) {
|
|
113
|
+
// SAFETY: pi seam — intentional unsafe cast, validated at runtime — theme fg for pipe
|
|
114
|
+
const pipe = theme.fg("dim", " | ");
|
|
115
|
+
text = `${text}${pipe}${stallText}`;
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
text = stallText;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
102
122
|
editor.setTopRightText(text);
|
|
103
123
|
}
|
|
104
124
|
catch {
|
|
@@ -122,15 +142,17 @@ export class LiveBorder {
|
|
|
122
142
|
return;
|
|
123
143
|
}
|
|
124
144
|
try {
|
|
125
|
-
// SAFETY: peekLive ?? getLastTelemetry preserves cost after toggle (AGENTS.md gotcha)
|
|
145
|
+
// SAFETY: peekLive ?? getLastTelemetry preserves cost after toggle (AGENTS.md gotcha) — intentional unsafe cast, validated at runtime
|
|
126
146
|
const live = this.deps.telemetryTracker.peekLive() ??
|
|
127
147
|
this.deps.telemetryTracker.getLastTelemetry();
|
|
128
148
|
if (!live)
|
|
129
149
|
return;
|
|
130
|
-
// SAFETY: pi
|
|
131
|
-
const theme = ctx?.ui?.theme;
|
|
150
|
+
// SAFETY: pi seam — intentional unsafe cast, validated at runtime — theme from extension context
|
|
151
|
+
const theme = ctx?.ui?.theme;
|
|
132
152
|
const glyphs = resolveGlyphs(cfg.icons.mode);
|
|
133
|
-
|
|
153
|
+
// Stall relocated to top right of tool use with pipe — suppress in bottom telemetry
|
|
154
|
+
const bottomCfg = { ...cfg.telemetry, stalls: false };
|
|
155
|
+
const right = formatTurnTelemetry(live, theme, bottomCfg, glyphs);
|
|
134
156
|
if (live.totalMs > 0) {
|
|
135
157
|
editor.setTelemetryText(right);
|
|
136
158
|
editor.setBottomLeftText("");
|
|
@@ -169,9 +191,10 @@ export class LiveBorder {
|
|
|
169
191
|
// Tokens line above model info — left aligned, no border; hidden at startup per user request
|
|
170
192
|
let tokensText = "";
|
|
171
193
|
if (cfg.telemetry.enabled && cfg.telemetry.tokens) {
|
|
172
|
-
//
|
|
173
|
-
|
|
174
|
-
|
|
194
|
+
// Live input/output per agent run (option B) — cumulative across turns in this agent, like live output
|
|
195
|
+
// SAFETY: pi seam — intentional unsafe cast, validated at runtime — telemetry tracker for top tokens line (agent run)
|
|
196
|
+
const tracker = this.deps.telemetryTracker;
|
|
197
|
+
const live = tracker.peekAgentLive() ?? tracker.getLastTelemetry();
|
|
175
198
|
if (live) {
|
|
176
199
|
tokensText = formatTelemetryTokens(live, theme, cfg.telemetry, glyphs);
|
|
177
200
|
}
|
package/dist/telemetry.js
CHANGED
|
@@ -50,6 +50,7 @@ export class TurnTelemetryTracker {
|
|
|
50
50
|
agentStartMs = null;
|
|
51
51
|
agentTurns = [];
|
|
52
52
|
lastTelemetry = null;
|
|
53
|
+
lastTurnTelemetry = null;
|
|
53
54
|
decayBaseTps = null;
|
|
54
55
|
decayStartMs = null;
|
|
55
56
|
constructor(now = () => performance.now()) {
|
|
@@ -58,6 +59,78 @@ export class TurnTelemetryTracker {
|
|
|
58
59
|
getLastTelemetry() {
|
|
59
60
|
return this.lastTelemetry;
|
|
60
61
|
}
|
|
62
|
+
getLastTurnTelemetry() {
|
|
63
|
+
return this.lastTurnTelemetry;
|
|
64
|
+
}
|
|
65
|
+
/** Agent-run live: cumulative across turns in current agent, including current live turn. Option B. */
|
|
66
|
+
peekAgentLive() {
|
|
67
|
+
// No agent active -> show last agent sum (option B) or current live turn if any
|
|
68
|
+
if (this.agentStartMs === null) {
|
|
69
|
+
const live = this.peekLive();
|
|
70
|
+
if (live)
|
|
71
|
+
return live;
|
|
72
|
+
return this.lastTelemetry ?? this.lastTurnTelemetry;
|
|
73
|
+
}
|
|
74
|
+
const live = this.peekLive();
|
|
75
|
+
const hasCompleted = this.agentTurns.length > 0;
|
|
76
|
+
if (!hasCompleted && !live)
|
|
77
|
+
return null;
|
|
78
|
+
let inputTokens = 0;
|
|
79
|
+
let outputTokens = 0;
|
|
80
|
+
let totalTokens = 0;
|
|
81
|
+
let costUsd = 0;
|
|
82
|
+
let stallMs = 0;
|
|
83
|
+
let stallCount = 0;
|
|
84
|
+
let generationMs = 0;
|
|
85
|
+
let ttftMs = 0;
|
|
86
|
+
// sum completed turns
|
|
87
|
+
for (const t of this.agentTurns) {
|
|
88
|
+
inputTokens += t.inputTokens;
|
|
89
|
+
outputTokens += t.outputTokens;
|
|
90
|
+
totalTokens += t.totalTokens;
|
|
91
|
+
costUsd += t.costUsd;
|
|
92
|
+
stallMs += t.stallMs;
|
|
93
|
+
stallCount += t.stallCount;
|
|
94
|
+
generationMs += t.generationMs;
|
|
95
|
+
}
|
|
96
|
+
if (this.agentTurns.length > 0)
|
|
97
|
+
ttftMs = this.agentTurns[0].ttftMs;
|
|
98
|
+
if (live) {
|
|
99
|
+
inputTokens += live.inputTokens;
|
|
100
|
+
outputTokens += live.outputTokens;
|
|
101
|
+
totalTokens += live.totalTokens;
|
|
102
|
+
costUsd += live.costUsd;
|
|
103
|
+
stallMs += live.stallMs;
|
|
104
|
+
stallCount += live.stallCount;
|
|
105
|
+
generationMs += live.generationMs;
|
|
106
|
+
if (ttftMs === 0)
|
|
107
|
+
ttftMs = live.ttftMs;
|
|
108
|
+
}
|
|
109
|
+
const now = this.now();
|
|
110
|
+
const totalMs = Math.max(0, now - this.agentStartMs);
|
|
111
|
+
const measurementMs = outputTokens > 0 && generationMs > 0 ? generationMs : null;
|
|
112
|
+
const tps = measurementMs === null
|
|
113
|
+
? null
|
|
114
|
+
: round(outputTokens / (measurementMs / 1000), 1);
|
|
115
|
+
const validCost = Number.isFinite(costUsd) && costUsd > 0;
|
|
116
|
+
const validTokens = Number.isFinite(totalTokens) && totalTokens > 0;
|
|
117
|
+
return {
|
|
118
|
+
tps,
|
|
119
|
+
ttftMs,
|
|
120
|
+
totalMs,
|
|
121
|
+
inputTokens,
|
|
122
|
+
outputTokens,
|
|
123
|
+
stallMs,
|
|
124
|
+
stallCount,
|
|
125
|
+
rateUsdPerMTokens: validCost && validTokens
|
|
126
|
+
? round(costUsd / (totalTokens / 1_000_000), 2)
|
|
127
|
+
: null,
|
|
128
|
+
generationMs,
|
|
129
|
+
totalTokens,
|
|
130
|
+
costUsd: validCost ? costUsd : 0,
|
|
131
|
+
measurementMs,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
61
134
|
/** Live snapshot while a turn is running — for real-time border refresh. Returns null when idle. */
|
|
62
135
|
peekLive() {
|
|
63
136
|
const turn = this.turn;
|
|
@@ -269,8 +342,10 @@ export class TurnTelemetryTracker {
|
|
|
269
342
|
const telemetry = this.endTurn();
|
|
270
343
|
if (telemetry && this.agentStartMs !== null)
|
|
271
344
|
this.agentTurns.push(telemetry);
|
|
272
|
-
if (telemetry)
|
|
345
|
+
if (telemetry) {
|
|
346
|
+
this.lastTurnTelemetry = telemetry;
|
|
273
347
|
this.lastTelemetry = telemetry;
|
|
348
|
+
}
|
|
274
349
|
return telemetry;
|
|
275
350
|
}
|
|
276
351
|
endTurn() {
|
|
@@ -351,10 +426,11 @@ export class TurnTelemetryTracker {
|
|
|
351
426
|
measurementMs,
|
|
352
427
|
};
|
|
353
428
|
this.lastTelemetry = result;
|
|
429
|
+
// keep lastTurnTelemetry as last turn's per-turn telemetry (live input stays per-turn, not agent total)
|
|
354
430
|
return result;
|
|
355
431
|
}
|
|
356
432
|
}
|
|
357
|
-
function formatTurnDuration(ms) {
|
|
433
|
+
export function formatTurnDuration(ms) {
|
|
358
434
|
if (ms < 60_000) {
|
|
359
435
|
// TTFT fixed width: 2-digit integer + one decimal -> padded 4 + "s" =5, then overall duration field padded to 7 for telemetry totalMs
|
|
360
436
|
// For TTFT we want 5, for duration we want 7 — caller will pad accordingly, so here return 5 for <60s case
|
package/package.json
CHANGED
package/src/color-policy.ts
CHANGED
|
@@ -8,11 +8,19 @@ export function stressColor(value: number, warn = 70, danger = 90): ThemeColor {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
export function contextUsageColor(pct: number): ThemeColor {
|
|
11
|
-
// 25
|
|
12
|
-
//
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
// 12.5 / 25 / 50 quotas — four urgency tiers, increasingly aggressive as context fills.
|
|
12
|
+
// 0 – 12.5% dim — plenty of headroom, visually quiet.
|
|
13
|
+
// 12.5 – 25% accent — first nudge, noticeable but calm.
|
|
14
|
+
// 25 – 50% warning — half consumed, needs attention.
|
|
15
|
+
// 50 – 100% error — critical, about to run out.
|
|
16
|
+
// Uses theme semantic tokens (dim/accent/warning/error) so the progression
|
|
17
|
+
// respects the active theme and remains legible on light/dark/custom palettes.
|
|
18
|
+
// A fixed hex palette (e.g. grey→sky→amber→red) would be more vivid but
|
|
19
|
+
// would ignore the user's theme and can clash with light backgrounds —
|
|
20
|
+
// semantic tokens keep the "aggressive" ordering while staying theme-coherent.
|
|
21
|
+
if (pct >= 50) return "error";
|
|
22
|
+
if (pct >= 25) return "warning";
|
|
23
|
+
if (pct >= 12.5) return "accent";
|
|
16
24
|
return "dim";
|
|
17
25
|
}
|
|
18
26
|
|
package/src/live-border.ts
CHANGED
|
@@ -18,7 +18,11 @@ import {
|
|
|
18
18
|
import { resolveGlyphs, resolveIconMode } from "./icons.js";
|
|
19
19
|
import { formatRunActivityTopRight } from "./run-activity.js";
|
|
20
20
|
import type { RunActivityTracker } from "./run-activity.js";
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
formatTelemetryTokens,
|
|
23
|
+
formatTurnDuration,
|
|
24
|
+
formatTurnTelemetry,
|
|
25
|
+
} from "./telemetry.js";
|
|
22
26
|
import type { TurnTelemetry } from "./telemetry.js";
|
|
23
27
|
import type { TurnTelemetryTracker } from "./telemetry.js";
|
|
24
28
|
import type { ThemeConfig } from "./config.js";
|
|
@@ -113,14 +117,43 @@ export class LiveBorder {
|
|
|
113
117
|
private refreshTopBorder(): void {
|
|
114
118
|
const editor = this.deps.getEditor();
|
|
115
119
|
const ctx = this.deps.getCtx();
|
|
120
|
+
const cfg = this.deps.getConfig();
|
|
116
121
|
if (!editor || !ctx) return;
|
|
117
122
|
try {
|
|
118
|
-
// SAFETY: theme is live pi TUI theme read at render time
|
|
123
|
+
// SAFETY: theme is live pi TUI theme read at render time — intentional unsafe cast, validated at runtime
|
|
119
124
|
const theme = (
|
|
120
125
|
ctx.ui as unknown as { theme: { fg(s: string, t: string): string } }
|
|
121
|
-
).theme; // SAFETY: pi seam — intentional unsafe cast, validated at runtime
|
|
126
|
+
).theme; // SAFETY: pi seam — intentional unsafe cast, validated at runtime
|
|
122
127
|
const snap = this.deps.runActivityTracker.getSnapshot();
|
|
123
|
-
|
|
128
|
+
let text = formatRunActivityTopRight(snap, theme as never);
|
|
129
|
+
// Relocate stall to the right of tool use with pipe separator — agent-run live (option B), not bottom telemetry
|
|
130
|
+
if (cfg.telemetry.enabled && cfg.telemetry.stalls) {
|
|
131
|
+
// SAFETY: pi seam — intentional unsafe cast, validated at runtime — telemetry tracker for stall (agent run, option B)
|
|
132
|
+
const tracker = this.deps.telemetryTracker as unknown as {
|
|
133
|
+
peekAgentLive(): import("./telemetry.js").TurnTelemetry | null;
|
|
134
|
+
getLastTelemetry(): import("./telemetry.js").TurnTelemetry | null;
|
|
135
|
+
};
|
|
136
|
+
const tel = tracker.peekAgentLive() ?? tracker.getLastTelemetry();
|
|
137
|
+
if (tel && tel.stallMs > 0) {
|
|
138
|
+
const glyphs = resolveGlyphs(cfg.icons.mode);
|
|
139
|
+
// SAFETY: pi seam — intentional unsafe cast, validated at runtime — theme fg
|
|
140
|
+
const stallText = (
|
|
141
|
+
theme as unknown as { fg: (s: string, t: string) => string }
|
|
142
|
+
).fg(
|
|
143
|
+
"warning",
|
|
144
|
+
`${glyphs.stall}${tel.stallCount}×${formatTurnDuration(tel.stallMs).trim()}`,
|
|
145
|
+
);
|
|
146
|
+
if (text) {
|
|
147
|
+
// SAFETY: pi seam — intentional unsafe cast, validated at runtime — theme fg for pipe
|
|
148
|
+
const pipe = (
|
|
149
|
+
theme as unknown as { fg: (s: string, t: string) => string }
|
|
150
|
+
).fg("dim", " | ");
|
|
151
|
+
text = `${text}${pipe}${stallText}`;
|
|
152
|
+
} else {
|
|
153
|
+
text = stallText;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
124
157
|
editor.setTopRightText(text);
|
|
125
158
|
} catch {
|
|
126
159
|
// SAFETY: best-effort UI, ignore recoverable error
|
|
@@ -142,18 +175,20 @@ export class LiveBorder {
|
|
|
142
175
|
return;
|
|
143
176
|
}
|
|
144
177
|
try {
|
|
145
|
-
// SAFETY: peekLive ?? getLastTelemetry preserves cost after toggle (AGENTS.md gotcha)
|
|
178
|
+
// SAFETY: peekLive ?? getLastTelemetry preserves cost after toggle (AGENTS.md gotcha) — intentional unsafe cast, validated at runtime
|
|
146
179
|
const live =
|
|
147
180
|
this.deps.telemetryTracker.peekLive() ??
|
|
148
181
|
this.deps.telemetryTracker.getLastTelemetry();
|
|
149
182
|
if (!live) return;
|
|
150
|
-
// SAFETY: pi
|
|
151
|
-
const theme = (ctx as unknown as { ui?: { theme?: unknown } })?.ui?.theme;
|
|
183
|
+
// SAFETY: pi seam — intentional unsafe cast, validated at runtime — theme from extension context
|
|
184
|
+
const theme = (ctx as unknown as { ui?: { theme?: unknown } })?.ui?.theme;
|
|
152
185
|
const glyphs = resolveGlyphs(cfg.icons.mode);
|
|
186
|
+
// Stall relocated to top right of tool use with pipe — suppress in bottom telemetry
|
|
187
|
+
const bottomCfg = { ...cfg.telemetry, stalls: false };
|
|
153
188
|
const right = formatTurnTelemetry(
|
|
154
189
|
live,
|
|
155
190
|
theme as never,
|
|
156
|
-
|
|
191
|
+
bottomCfg as never,
|
|
157
192
|
glyphs as never,
|
|
158
193
|
);
|
|
159
194
|
if (live.totalMs > 0) {
|
|
@@ -202,10 +237,14 @@ export class LiveBorder {
|
|
|
202
237
|
// Tokens line above model info — left aligned, no border; hidden at startup per user request
|
|
203
238
|
let tokensText = "";
|
|
204
239
|
if (cfg.telemetry.enabled && cfg.telemetry.tokens) {
|
|
205
|
-
//
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
240
|
+
// Live input/output per agent run (option B) — cumulative across turns in this agent, like live output
|
|
241
|
+
// SAFETY: pi seam — intentional unsafe cast, validated at runtime — telemetry tracker for top tokens line (agent run)
|
|
242
|
+
const tracker = this.deps.telemetryTracker as unknown as {
|
|
243
|
+
// SAFETY: pi seam — intentional unsafe cast, validated at runtime
|
|
244
|
+
peekAgentLive(): import("./telemetry.js").TurnTelemetry | null;
|
|
245
|
+
getLastTelemetry(): import("./telemetry.js").TurnTelemetry | null;
|
|
246
|
+
};
|
|
247
|
+
const live = tracker.peekAgentLive() ?? tracker.getLastTelemetry();
|
|
209
248
|
if (live) {
|
|
210
249
|
tokensText = formatTelemetryTokens(
|
|
211
250
|
live,
|
package/src/telemetry.ts
CHANGED
|
@@ -150,9 +150,9 @@ export class TurnTelemetryTracker {
|
|
|
150
150
|
private agentStartMs: number | null = null;
|
|
151
151
|
private agentTurns: TurnTelemetry[] = [];
|
|
152
152
|
private lastTelemetry: TurnTelemetry | null = null;
|
|
153
|
+
private lastTurnTelemetry: TurnTelemetry | null = null;
|
|
153
154
|
private decayBaseTps: number | null = null;
|
|
154
155
|
private decayStartMs: number | null = null;
|
|
155
|
-
|
|
156
156
|
constructor(now: () => number = () => performance.now()) {
|
|
157
157
|
this.now = now;
|
|
158
158
|
}
|
|
@@ -160,6 +160,79 @@ export class TurnTelemetryTracker {
|
|
|
160
160
|
getLastTelemetry(): TurnTelemetry | null {
|
|
161
161
|
return this.lastTelemetry;
|
|
162
162
|
}
|
|
163
|
+
|
|
164
|
+
getLastTurnTelemetry(): TurnTelemetry | null {
|
|
165
|
+
return this.lastTurnTelemetry;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Agent-run live: cumulative across turns in current agent, including current live turn. Option B. */
|
|
169
|
+
peekAgentLive(): TurnTelemetry | null {
|
|
170
|
+
// No agent active -> show last agent sum (option B) or current live turn if any
|
|
171
|
+
if (this.agentStartMs === null) {
|
|
172
|
+
const live = this.peekLive();
|
|
173
|
+
if (live) return live;
|
|
174
|
+
return this.lastTelemetry ?? this.lastTurnTelemetry;
|
|
175
|
+
}
|
|
176
|
+
const live = this.peekLive();
|
|
177
|
+
const hasCompleted = this.agentTurns.length > 0;
|
|
178
|
+
if (!hasCompleted && !live) return null;
|
|
179
|
+
let inputTokens = 0;
|
|
180
|
+
let outputTokens = 0;
|
|
181
|
+
let totalTokens = 0;
|
|
182
|
+
let costUsd = 0;
|
|
183
|
+
let stallMs = 0;
|
|
184
|
+
let stallCount = 0;
|
|
185
|
+
let generationMs = 0;
|
|
186
|
+
let ttftMs = 0;
|
|
187
|
+
// sum completed turns
|
|
188
|
+
for (const t of this.agentTurns) {
|
|
189
|
+
inputTokens += t.inputTokens;
|
|
190
|
+
outputTokens += t.outputTokens;
|
|
191
|
+
totalTokens += t.totalTokens;
|
|
192
|
+
costUsd += t.costUsd;
|
|
193
|
+
stallMs += t.stallMs;
|
|
194
|
+
stallCount += t.stallCount;
|
|
195
|
+
generationMs += t.generationMs;
|
|
196
|
+
}
|
|
197
|
+
if (this.agentTurns.length > 0) ttftMs = this.agentTurns[0]!.ttftMs;
|
|
198
|
+
if (live) {
|
|
199
|
+
inputTokens += live.inputTokens;
|
|
200
|
+
outputTokens += live.outputTokens;
|
|
201
|
+
totalTokens += live.totalTokens;
|
|
202
|
+
costUsd += live.costUsd;
|
|
203
|
+
stallMs += live.stallMs;
|
|
204
|
+
stallCount += live.stallCount;
|
|
205
|
+
generationMs += live.generationMs;
|
|
206
|
+
if (ttftMs === 0) ttftMs = live.ttftMs;
|
|
207
|
+
}
|
|
208
|
+
const now = this.now();
|
|
209
|
+
const totalMs = Math.max(0, now - this.agentStartMs);
|
|
210
|
+
const measurementMs =
|
|
211
|
+
outputTokens > 0 && generationMs > 0 ? generationMs : null;
|
|
212
|
+
const tps =
|
|
213
|
+
measurementMs === null
|
|
214
|
+
? null
|
|
215
|
+
: round(outputTokens / (measurementMs / 1000), 1);
|
|
216
|
+
const validCost = Number.isFinite(costUsd) && costUsd > 0;
|
|
217
|
+
const validTokens = Number.isFinite(totalTokens) && totalTokens > 0;
|
|
218
|
+
return {
|
|
219
|
+
tps,
|
|
220
|
+
ttftMs,
|
|
221
|
+
totalMs,
|
|
222
|
+
inputTokens,
|
|
223
|
+
outputTokens,
|
|
224
|
+
stallMs,
|
|
225
|
+
stallCount,
|
|
226
|
+
rateUsdPerMTokens:
|
|
227
|
+
validCost && validTokens
|
|
228
|
+
? round(costUsd / (totalTokens / 1_000_000), 2)
|
|
229
|
+
: null,
|
|
230
|
+
generationMs,
|
|
231
|
+
totalTokens,
|
|
232
|
+
costUsd: validCost ? costUsd : 0,
|
|
233
|
+
measurementMs,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
163
236
|
/** Live snapshot while a turn is running — for real-time border refresh. Returns null when idle. */
|
|
164
237
|
peekLive(): TurnTelemetry | null {
|
|
165
238
|
const turn = this.turn;
|
|
@@ -386,7 +459,10 @@ export class TurnTelemetryTracker {
|
|
|
386
459
|
const telemetry = this.endTurn();
|
|
387
460
|
if (telemetry && this.agentStartMs !== null)
|
|
388
461
|
this.agentTurns.push(telemetry);
|
|
389
|
-
if (telemetry)
|
|
462
|
+
if (telemetry) {
|
|
463
|
+
this.lastTurnTelemetry = telemetry;
|
|
464
|
+
this.lastTelemetry = telemetry;
|
|
465
|
+
}
|
|
390
466
|
return telemetry;
|
|
391
467
|
}
|
|
392
468
|
|
|
@@ -478,11 +554,12 @@ export class TurnTelemetryTracker {
|
|
|
478
554
|
measurementMs,
|
|
479
555
|
};
|
|
480
556
|
this.lastTelemetry = result;
|
|
557
|
+
// keep lastTurnTelemetry as last turn's per-turn telemetry (live input stays per-turn, not agent total)
|
|
481
558
|
return result;
|
|
482
559
|
}
|
|
483
560
|
}
|
|
484
561
|
|
|
485
|
-
function formatTurnDuration(ms: number): string {
|
|
562
|
+
export function formatTurnDuration(ms: number): string {
|
|
486
563
|
if (ms < 60_000) {
|
|
487
564
|
// TTFT fixed width: 2-digit integer + one decimal -> padded 4 + "s" =5, then overall duration field padded to 7 for telemetry totalMs
|
|
488
565
|
// For TTFT we want 5, for duration we want 7 — caller will pad accordingly, so here return 5 for <60s case
|