pi-open-tui 0.2.13 → 0.2.15
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 +141 -135
- package/README.zh-CN.md +141 -135
- package/extensions/open-tui/config.ts +16 -0
- package/extensions/open-tui/editor.ts +11 -0
- package/extensions/open-tui/fullscreen-scroll.ts +34 -0
- package/extensions/open-tui/index.ts +5 -1
- package/extensions/open-tui/settings-command.ts +83 -7
- package/extensions/open-tui/state.ts +11 -8
- package/extensions/open-tui/telemetry.ts +295 -298
- package/extensions/open-tui/utils.ts +365 -361
- package/package.json +2 -2
|
@@ -1,298 +1,295 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
AgentSettledEvent,
|
|
3
|
-
AgentStartEvent,
|
|
4
|
-
MessageEndEvent,
|
|
5
|
-
MessageStartEvent,
|
|
6
|
-
MessageUpdateEvent,
|
|
7
|
-
ToolExecutionStartEvent,
|
|
8
|
-
TurnEndEvent,
|
|
9
|
-
TurnStartEvent,
|
|
10
|
-
Theme,
|
|
11
|
-
} from "@earendil-works/pi-coding-agent";
|
|
12
|
-
import type { IconMode, TelemetryConfig } from "./config.ts";
|
|
13
|
-
import { resolveGlyphs } from "./icons.ts";
|
|
14
|
-
import { fmtTokens, formatDuration } from "./utils.ts";
|
|
15
|
-
|
|
16
|
-
const STALL_THRESHOLD_MS = 1000;
|
|
17
|
-
|
|
18
|
-
type TelemetryEvent =
|
|
19
|
-
| AgentStartEvent
|
|
20
|
-
| AgentSettledEvent
|
|
21
|
-
| TurnStartEvent
|
|
22
|
-
| MessageStartEvent
|
|
23
|
-
| MessageUpdateEvent
|
|
24
|
-
| MessageEndEvent
|
|
25
|
-
| ToolExecutionStartEvent
|
|
26
|
-
| TurnEndEvent;
|
|
27
|
-
type AgentMessage = MessageStartEvent["message"];
|
|
28
|
-
type AssistantMessage = Extract<AgentMessage, { role: "assistant" }>;
|
|
29
|
-
|
|
30
|
-
interface MessageTiming {
|
|
31
|
-
lastUpdateMs: number;
|
|
32
|
-
firstOutputMs: number | null;
|
|
33
|
-
inStall: boolean;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
interface TurnTiming {
|
|
37
|
-
startMs: number;
|
|
38
|
-
firstTokenMs: number | null;
|
|
39
|
-
currentMessage: MessageTiming | null;
|
|
40
|
-
messages: AssistantMessage[];
|
|
41
|
-
generationMs: number;
|
|
42
|
-
stallMs: number;
|
|
43
|
-
stallCount: number;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export interface TurnTelemetry {
|
|
47
|
-
tps: number | null;
|
|
48
|
-
ttftMs: number;
|
|
49
|
-
totalMs: number;
|
|
50
|
-
inputTokens: number;
|
|
51
|
-
outputTokens: number;
|
|
52
|
-
stallMs: number;
|
|
53
|
-
stallCount: number;
|
|
54
|
-
rateUsdPerMTokens: number | null;
|
|
55
|
-
generationMs: number;
|
|
56
|
-
totalTokens: number;
|
|
57
|
-
costUsd: number;
|
|
58
|
-
measurementMs: number | null;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function isAssistantMessage(message: AgentMessage): message is AssistantMessage {
|
|
62
|
-
return message.role === "assistant";
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function round(value: number, decimals: number): number {
|
|
66
|
-
const factor = 10 ** decimals;
|
|
67
|
-
return Math.round(value * factor) / factor;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export class TurnTelemetryTracker {
|
|
71
|
-
private readonly now: () => number;
|
|
72
|
-
private turn: TurnTiming | undefined;
|
|
73
|
-
private agentStartMs: number | null = null;
|
|
74
|
-
private agentTurns: TurnTelemetry[] = [];
|
|
75
|
-
|
|
76
|
-
constructor(now: () => number = () => performance.now()) {
|
|
77
|
-
this.now = now;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
handle(event: TelemetryEvent): TurnTelemetry | undefined {
|
|
81
|
-
switch (event.type) {
|
|
82
|
-
case "agent_start":
|
|
83
|
-
if (this.agentStartMs === null) {
|
|
84
|
-
this.agentStartMs = this.now();
|
|
85
|
-
this.agentTurns = [];
|
|
86
|
-
}
|
|
87
|
-
return;
|
|
88
|
-
case "agent_settled":
|
|
89
|
-
return this.endAgent();
|
|
90
|
-
case "turn_start":
|
|
91
|
-
this.startTurn();
|
|
92
|
-
return;
|
|
93
|
-
case "message_start":
|
|
94
|
-
this.startMessage(event.message);
|
|
95
|
-
return;
|
|
96
|
-
case "message_update":
|
|
97
|
-
this.updateMessage(event);
|
|
98
|
-
return;
|
|
99
|
-
case "message_end":
|
|
100
|
-
this.endMessage(event.message);
|
|
101
|
-
return;
|
|
102
|
-
case "tool_execution_start":
|
|
103
|
-
return;
|
|
104
|
-
case "turn_end":
|
|
105
|
-
return this.endTurnAndCollect();
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
private startTurn(): void {
|
|
110
|
-
this.turn = {
|
|
111
|
-
startMs: this.now(),
|
|
112
|
-
firstTokenMs: null,
|
|
113
|
-
currentMessage: null,
|
|
114
|
-
messages: [],
|
|
115
|
-
generationMs: 0,
|
|
116
|
-
stallMs: 0,
|
|
117
|
-
stallCount: 0,
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
private startMessage(message: AgentMessage): void {
|
|
122
|
-
if (!this.turn || !isAssistantMessage(message)) return;
|
|
123
|
-
const now = this.now();
|
|
124
|
-
this.turn.currentMessage = {
|
|
125
|
-
lastUpdateMs: now,
|
|
126
|
-
firstOutputMs: null,
|
|
127
|
-
inStall: false,
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
private updateMessage(event: MessageUpdateEvent): void {
|
|
132
|
-
const turn = this.turn;
|
|
133
|
-
const current = turn?.currentMessage;
|
|
134
|
-
const streamEvent = event.assistantMessageEvent;
|
|
135
|
-
if (
|
|
136
|
-
streamEvent.type !== "text_delta" &&
|
|
137
|
-
streamEvent.type !== "thinking_delta" &&
|
|
138
|
-
streamEvent.type !== "toolcall_delta"
|
|
139
|
-
) return;
|
|
140
|
-
if (streamEvent.delta.length === 0) return;
|
|
141
|
-
const message = event.message;
|
|
142
|
-
if (!turn || !current || !isAssistantMessage(message)) return;
|
|
143
|
-
|
|
144
|
-
const now = this.now();
|
|
145
|
-
if (current.firstOutputMs === null) {
|
|
146
|
-
current.firstOutputMs = now;
|
|
147
|
-
turn.firstTokenMs ??= now;
|
|
148
|
-
current.lastUpdateMs = now;
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const gap = now - current.lastUpdateMs;
|
|
153
|
-
if (gap >= STALL_THRESHOLD_MS) {
|
|
154
|
-
if (!current.inStall) turn.stallCount++;
|
|
155
|
-
current.inStall = true;
|
|
156
|
-
turn.stallMs += gap;
|
|
157
|
-
} else {
|
|
158
|
-
current.inStall = false;
|
|
159
|
-
}
|
|
160
|
-
current.lastUpdateMs = now;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
private endMessage(message: AgentMessage): void {
|
|
164
|
-
const turn = this.turn;
|
|
165
|
-
if (!turn || !isAssistantMessage(message)) return;
|
|
166
|
-
|
|
167
|
-
const current = turn.currentMessage;
|
|
168
|
-
if (current) {
|
|
169
|
-
const endMs = this.now();
|
|
170
|
-
turn.generationMs = endMs - turn.startMs;
|
|
171
|
-
if (current.firstOutputMs === null && message.usage
|
|
172
|
-
turn.firstTokenMs ??= endMs;
|
|
173
|
-
}
|
|
174
|
-
turn.currentMessage = null;
|
|
175
|
-
}
|
|
176
|
-
turn.messages.push(message);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
private endTurnAndCollect(): TurnTelemetry | undefined {
|
|
180
|
-
const telemetry = this.endTurn();
|
|
181
|
-
if (telemetry && this.agentStartMs !== null) this.agentTurns.push(telemetry);
|
|
182
|
-
return telemetry;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
private endTurn(): TurnTelemetry | undefined {
|
|
186
|
-
const turn = this.turn;
|
|
187
|
-
this.turn = undefined;
|
|
188
|
-
if (!turn || turn.firstTokenMs === null || turn.messages.length === 0) return;
|
|
189
|
-
|
|
190
|
-
const endMs = this.now();
|
|
191
|
-
let inputTokens = 0;
|
|
192
|
-
let outputTokens = 0;
|
|
193
|
-
let totalTokens = 0;
|
|
194
|
-
let costUsd = 0;
|
|
195
|
-
for (const message of turn.messages) {
|
|
196
|
-
inputTokens += message.usage
|
|
197
|
-
outputTokens += message.usage
|
|
198
|
-
totalTokens += message.usage
|
|
199
|
-
costUsd += message.usage
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
const
|
|
237
|
-
const
|
|
238
|
-
const
|
|
239
|
-
const
|
|
240
|
-
const
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
)
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
parts.push(theme.fg(
|
|
280
|
-
}
|
|
281
|
-
if (config.
|
|
282
|
-
parts.push(theme.fg("
|
|
283
|
-
}
|
|
284
|
-
if (config.
|
|
285
|
-
parts.push(theme.fg("
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
parts.push(theme.fg("
|
|
290
|
-
}
|
|
291
|
-
if (config.
|
|
292
|
-
parts.push(theme.fg("warning", `${glyphs.
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
}
|
|
297
|
-
return parts.join(` ${theme.fg("dim", "|")} `);
|
|
298
|
-
}
|
|
1
|
+
import type {
|
|
2
|
+
AgentSettledEvent,
|
|
3
|
+
AgentStartEvent,
|
|
4
|
+
MessageEndEvent,
|
|
5
|
+
MessageStartEvent,
|
|
6
|
+
MessageUpdateEvent,
|
|
7
|
+
ToolExecutionStartEvent,
|
|
8
|
+
TurnEndEvent,
|
|
9
|
+
TurnStartEvent,
|
|
10
|
+
Theme,
|
|
11
|
+
} from "@earendil-works/pi-coding-agent";
|
|
12
|
+
import type { IconMode, TelemetryConfig } from "./config.ts";
|
|
13
|
+
import { resolveGlyphs } from "./icons.ts";
|
|
14
|
+
import { finiteOrZero, fmtTokens, formatDuration } from "./utils.ts";
|
|
15
|
+
|
|
16
|
+
const STALL_THRESHOLD_MS = 1000;
|
|
17
|
+
|
|
18
|
+
type TelemetryEvent =
|
|
19
|
+
| AgentStartEvent
|
|
20
|
+
| AgentSettledEvent
|
|
21
|
+
| TurnStartEvent
|
|
22
|
+
| MessageStartEvent
|
|
23
|
+
| MessageUpdateEvent
|
|
24
|
+
| MessageEndEvent
|
|
25
|
+
| ToolExecutionStartEvent
|
|
26
|
+
| TurnEndEvent;
|
|
27
|
+
type AgentMessage = MessageStartEvent["message"];
|
|
28
|
+
type AssistantMessage = Extract<AgentMessage, { role: "assistant" }>;
|
|
29
|
+
|
|
30
|
+
interface MessageTiming {
|
|
31
|
+
lastUpdateMs: number;
|
|
32
|
+
firstOutputMs: number | null;
|
|
33
|
+
inStall: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface TurnTiming {
|
|
37
|
+
startMs: number;
|
|
38
|
+
firstTokenMs: number | null;
|
|
39
|
+
currentMessage: MessageTiming | null;
|
|
40
|
+
messages: AssistantMessage[];
|
|
41
|
+
generationMs: number;
|
|
42
|
+
stallMs: number;
|
|
43
|
+
stallCount: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface TurnTelemetry {
|
|
47
|
+
tps: number | null;
|
|
48
|
+
ttftMs: number;
|
|
49
|
+
totalMs: number;
|
|
50
|
+
inputTokens: number;
|
|
51
|
+
outputTokens: number;
|
|
52
|
+
stallMs: number;
|
|
53
|
+
stallCount: number;
|
|
54
|
+
rateUsdPerMTokens: number | null;
|
|
55
|
+
generationMs: number;
|
|
56
|
+
totalTokens: number;
|
|
57
|
+
costUsd: number;
|
|
58
|
+
measurementMs: number | null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function isAssistantMessage(message: AgentMessage): message is AssistantMessage {
|
|
62
|
+
return message.role === "assistant";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function round(value: number, decimals: number): number {
|
|
66
|
+
const factor = 10 ** decimals;
|
|
67
|
+
return Math.round(value * factor) / factor;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export class TurnTelemetryTracker {
|
|
71
|
+
private readonly now: () => number;
|
|
72
|
+
private turn: TurnTiming | undefined;
|
|
73
|
+
private agentStartMs: number | null = null;
|
|
74
|
+
private agentTurns: TurnTelemetry[] = [];
|
|
75
|
+
|
|
76
|
+
constructor(now: () => number = () => performance.now()) {
|
|
77
|
+
this.now = now;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
handle(event: TelemetryEvent): TurnTelemetry | undefined {
|
|
81
|
+
switch (event.type) {
|
|
82
|
+
case "agent_start":
|
|
83
|
+
if (this.agentStartMs === null) {
|
|
84
|
+
this.agentStartMs = this.now();
|
|
85
|
+
this.agentTurns = [];
|
|
86
|
+
}
|
|
87
|
+
return;
|
|
88
|
+
case "agent_settled":
|
|
89
|
+
return this.endAgent();
|
|
90
|
+
case "turn_start":
|
|
91
|
+
this.startTurn();
|
|
92
|
+
return;
|
|
93
|
+
case "message_start":
|
|
94
|
+
this.startMessage(event.message);
|
|
95
|
+
return;
|
|
96
|
+
case "message_update":
|
|
97
|
+
this.updateMessage(event);
|
|
98
|
+
return;
|
|
99
|
+
case "message_end":
|
|
100
|
+
this.endMessage(event.message);
|
|
101
|
+
return;
|
|
102
|
+
case "tool_execution_start":
|
|
103
|
+
return;
|
|
104
|
+
case "turn_end":
|
|
105
|
+
return this.endTurnAndCollect();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private startTurn(): void {
|
|
110
|
+
this.turn = {
|
|
111
|
+
startMs: this.now(),
|
|
112
|
+
firstTokenMs: null,
|
|
113
|
+
currentMessage: null,
|
|
114
|
+
messages: [],
|
|
115
|
+
generationMs: 0,
|
|
116
|
+
stallMs: 0,
|
|
117
|
+
stallCount: 0,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private startMessage(message: AgentMessage): void {
|
|
122
|
+
if (!this.turn || !isAssistantMessage(message)) return;
|
|
123
|
+
const now = this.now();
|
|
124
|
+
this.turn.currentMessage = {
|
|
125
|
+
lastUpdateMs: now,
|
|
126
|
+
firstOutputMs: null,
|
|
127
|
+
inStall: false,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private updateMessage(event: MessageUpdateEvent): void {
|
|
132
|
+
const turn = this.turn;
|
|
133
|
+
const current = turn?.currentMessage;
|
|
134
|
+
const streamEvent = event.assistantMessageEvent;
|
|
135
|
+
if (
|
|
136
|
+
streamEvent.type !== "text_delta" &&
|
|
137
|
+
streamEvent.type !== "thinking_delta" &&
|
|
138
|
+
streamEvent.type !== "toolcall_delta"
|
|
139
|
+
) return;
|
|
140
|
+
if (streamEvent.delta.length === 0) return;
|
|
141
|
+
const message = event.message;
|
|
142
|
+
if (!turn || !current || !isAssistantMessage(message)) return;
|
|
143
|
+
|
|
144
|
+
const now = this.now();
|
|
145
|
+
if (current.firstOutputMs === null) {
|
|
146
|
+
current.firstOutputMs = now;
|
|
147
|
+
turn.firstTokenMs ??= now;
|
|
148
|
+
current.lastUpdateMs = now;
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const gap = now - current.lastUpdateMs;
|
|
153
|
+
if (gap >= STALL_THRESHOLD_MS) {
|
|
154
|
+
if (!current.inStall) turn.stallCount++;
|
|
155
|
+
current.inStall = true;
|
|
156
|
+
turn.stallMs += gap;
|
|
157
|
+
} else {
|
|
158
|
+
current.inStall = false;
|
|
159
|
+
}
|
|
160
|
+
current.lastUpdateMs = now;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private endMessage(message: AgentMessage): void {
|
|
164
|
+
const turn = this.turn;
|
|
165
|
+
if (!turn || !isAssistantMessage(message)) return;
|
|
166
|
+
|
|
167
|
+
const current = turn.currentMessage;
|
|
168
|
+
if (current) {
|
|
169
|
+
const endMs = this.now();
|
|
170
|
+
turn.generationMs = endMs - turn.startMs;
|
|
171
|
+
if (current.firstOutputMs === null && finiteOrZero(message.usage?.output) > 0) {
|
|
172
|
+
turn.firstTokenMs ??= endMs;
|
|
173
|
+
}
|
|
174
|
+
turn.currentMessage = null;
|
|
175
|
+
}
|
|
176
|
+
turn.messages.push(message);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private endTurnAndCollect(): TurnTelemetry | undefined {
|
|
180
|
+
const telemetry = this.endTurn();
|
|
181
|
+
if (telemetry && this.agentStartMs !== null) this.agentTurns.push(telemetry);
|
|
182
|
+
return telemetry;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private endTurn(): TurnTelemetry | undefined {
|
|
186
|
+
const turn = this.turn;
|
|
187
|
+
this.turn = undefined;
|
|
188
|
+
if (!turn || turn.firstTokenMs === null || turn.messages.length === 0) return;
|
|
189
|
+
|
|
190
|
+
const endMs = this.now();
|
|
191
|
+
let inputTokens = 0;
|
|
192
|
+
let outputTokens = 0;
|
|
193
|
+
let totalTokens = 0;
|
|
194
|
+
let costUsd = 0;
|
|
195
|
+
for (const message of turn.messages) {
|
|
196
|
+
inputTokens += finiteOrZero(message.usage?.input);
|
|
197
|
+
outputTokens += finiteOrZero(message.usage?.output);
|
|
198
|
+
totalTokens += finiteOrZero(message.usage?.totalTokens);
|
|
199
|
+
costUsd += finiteOrZero(message.usage?.cost?.total);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const measurementMs = outputTokens > 0 && turn.generationMs > 0 ? turn.generationMs : null;
|
|
203
|
+
const tps = measurementMs === null
|
|
204
|
+
? null
|
|
205
|
+
: round(outputTokens / (measurementMs / 1000), 1);
|
|
206
|
+
const validCost = Number.isFinite(costUsd) && costUsd > 0;
|
|
207
|
+
const validTokens = Number.isFinite(totalTokens) && totalTokens > 0;
|
|
208
|
+
return {
|
|
209
|
+
tps,
|
|
210
|
+
ttftMs: turn.firstTokenMs - turn.startMs,
|
|
211
|
+
totalMs: endMs - turn.startMs,
|
|
212
|
+
inputTokens,
|
|
213
|
+
outputTokens,
|
|
214
|
+
stallMs: turn.stallMs,
|
|
215
|
+
stallCount: turn.stallCount,
|
|
216
|
+
rateUsdPerMTokens: validCost && validTokens
|
|
217
|
+
? round(costUsd / (totalTokens / 1_000_000), 2)
|
|
218
|
+
: null,
|
|
219
|
+
generationMs: turn.generationMs,
|
|
220
|
+
totalTokens,
|
|
221
|
+
costUsd: validCost ? costUsd : 0,
|
|
222
|
+
measurementMs,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
private endAgent(): TurnTelemetry | undefined {
|
|
227
|
+
const startMs = this.agentStartMs;
|
|
228
|
+
const turns = this.agentTurns;
|
|
229
|
+
this.agentStartMs = null;
|
|
230
|
+
this.agentTurns = [];
|
|
231
|
+
if (startMs === null || turns.length === 0) return;
|
|
232
|
+
|
|
233
|
+
const outputTokens = turns.reduce((sum, turn) => sum + turn.outputTokens, 0);
|
|
234
|
+
const inputTokens = turns.reduce((sum, turn) => sum + turn.inputTokens, 0);
|
|
235
|
+
const totalTokens = turns.reduce((sum, turn) => sum + turn.totalTokens, 0);
|
|
236
|
+
const costUsd = turns.reduce((sum, turn) => sum + turn.costUsd, 0);
|
|
237
|
+
const stallMs = turns.reduce((sum, turn) => sum + turn.stallMs, 0);
|
|
238
|
+
const stallCount = turns.reduce((sum, turn) => sum + turn.stallCount, 0);
|
|
239
|
+
const generationMs = turns.reduce((sum, turn) => sum + turn.generationMs, 0);
|
|
240
|
+
const measurementMs = outputTokens > 0 && generationMs > 0 ? generationMs : null;
|
|
241
|
+
const tps = measurementMs === null
|
|
242
|
+
? null
|
|
243
|
+
: round(outputTokens / (measurementMs / 1000), 1);
|
|
244
|
+
const validRate = costUsd > 0 && totalTokens > 0;
|
|
245
|
+
return {
|
|
246
|
+
tps,
|
|
247
|
+
ttftMs: turns[0]!.ttftMs,
|
|
248
|
+
totalMs: this.now() - startMs,
|
|
249
|
+
inputTokens,
|
|
250
|
+
outputTokens,
|
|
251
|
+
stallMs,
|
|
252
|
+
stallCount,
|
|
253
|
+
rateUsdPerMTokens: validRate ? round(costUsd / (totalTokens / 1_000_000), 2) : null,
|
|
254
|
+
generationMs,
|
|
255
|
+
totalTokens,
|
|
256
|
+
costUsd,
|
|
257
|
+
measurementMs,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function formatTurnDuration(ms: number): string {
|
|
263
|
+
return ms < 60_000 ? `${(ms / 1000).toFixed(1)}s` : formatDuration(ms);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export function formatTurnTelemetry(
|
|
267
|
+
telemetry: TurnTelemetry,
|
|
268
|
+
theme: Theme,
|
|
269
|
+
config: TelemetryConfig,
|
|
270
|
+
iconMode: IconMode,
|
|
271
|
+
): string {
|
|
272
|
+
const glyphs = resolveGlyphs(iconMode);
|
|
273
|
+
const parts: string[] = [];
|
|
274
|
+
if (config.tps) {
|
|
275
|
+
const value = telemetry.tps === null ? "—" : `${telemetry.tps.toFixed(1)} tok/s`;
|
|
276
|
+
parts.push(theme.fg(telemetry.tps === null ? "muted" : "accent", `${glyphs.speed} TPS ${value}`));
|
|
277
|
+
}
|
|
278
|
+
if (config.ttft) {
|
|
279
|
+
parts.push(theme.fg("text", `${glyphs.latency} TTFT ${formatTurnDuration(telemetry.ttftMs)}`));
|
|
280
|
+
}
|
|
281
|
+
if (config.duration) {
|
|
282
|
+
parts.push(theme.fg("success", `${glyphs.done} ${formatTurnDuration(telemetry.totalMs)}`));
|
|
283
|
+
}
|
|
284
|
+
if (config.tokens) {
|
|
285
|
+
parts.push(theme.fg("accent", `${glyphs.input} ${fmtTokens(telemetry.inputTokens)}`));
|
|
286
|
+
parts.push(theme.fg("success", `${glyphs.output} ${fmtTokens(telemetry.outputTokens)}`));
|
|
287
|
+
}
|
|
288
|
+
if (config.stalls && telemetry.stallMs > 0) {
|
|
289
|
+
parts.push(theme.fg("warning", `${glyphs.stall} stall ${telemetry.stallCount}x / ${formatTurnDuration(telemetry.stallMs)}`));
|
|
290
|
+
}
|
|
291
|
+
if (config.cost && telemetry.rateUsdPerMTokens !== null) {
|
|
292
|
+
parts.push(theme.fg("warning", `${glyphs.cost} $${telemetry.rateUsdPerMTokens.toFixed(2)}/M`));
|
|
293
|
+
}
|
|
294
|
+
return parts.join(` ${theme.fg("dim", "|")} `);
|
|
295
|
+
}
|