pi-exchange-stats 0.1.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/LICENSE +19 -0
- package/README.md +46 -0
- package/exchange-stats.js +382 -0
- package/package.json +38 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ezoushen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE.
|
package/README.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# pi-exchange-stats
|
|
2
|
+
|
|
3
|
+
Show timing, token, cache, cost, and tool-use statistics for each Pi exchange.
|
|
4
|
+
|
|
5
|
+
An **exchange** is one uninterrupted work span from a submitted prompt until Pi has
|
|
6
|
+
nothing left to do automatically. A **turn** is one model response plus the tools it
|
|
7
|
+
invokes, so an exchange can contain several turns. The status line shows the current
|
|
8
|
+
or most recent exchange, and a transcript card records the settled breakdown.
|
|
9
|
+
|
|
10
|
+
Tool time is the union of tool spans, not their sum, so parallel calls are not counted
|
|
11
|
+
twice. Model time is estimated as turn wall time minus tool time. Output throughput is
|
|
12
|
+
calculated per turn, while cumulative cost and token fields use Pi's reported usage.
|
|
13
|
+
Run `/exstats` to append a cumulative session card.
|
|
14
|
+
|
|
15
|
+
## External contract
|
|
16
|
+
|
|
17
|
+
Pi must emit its documented session, agent, turn, tool, and UI-prompt lifecycle events.
|
|
18
|
+
Assistant messages should include usage and cost fields when the provider supports
|
|
19
|
+
them. No network service or machine-local file is required.
|
|
20
|
+
|
|
21
|
+
## If the contract is unmet
|
|
22
|
+
|
|
23
|
+
Missing usage fields are reported as zero; the extension does not invent token or cost
|
|
24
|
+
data. Missing lifecycle events produce an incomplete or absent span. When UI status is
|
|
25
|
+
unavailable, status updates are skipped. If custom-entry persistence is unavailable,
|
|
26
|
+
the live status can still update and the agent turn continues.
|
|
27
|
+
|
|
28
|
+
The measurements are process-local. They do not claim provider-side queue time,
|
|
29
|
+
exclusive model compute time, or billing beyond the usage object Pi received.
|
|
30
|
+
|
|
31
|
+
## Settings
|
|
32
|
+
|
|
33
|
+
There are no settings and no environment variables. The package operates entirely
|
|
34
|
+
from Pi-provided events and context.
|
|
35
|
+
|
|
36
|
+
## Install and verify
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
pi install npm:pi-exchange-stats
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Submit a prompt that makes at least one tool call and wait for Pi to settle. Expand the
|
|
43
|
+
exchange card and verify that its turn count, output tokens, tool names, and wall-time
|
|
44
|
+
breakdown match the transcript. Run `/exstats` and confirm that the session card equals
|
|
45
|
+
the sum of completed exchanges. To check parallel-tool accounting, run two overlapping
|
|
46
|
+
tools and confirm their union is not larger than the exchange wall time.
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
// extensions/exchange-stats/exchange-stats.ts
|
|
2
|
+
import { keyHint } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { Box, Text } from "@earendil-works/pi-tui";
|
|
4
|
+
var ENTRY_TYPE = "exchange-stats";
|
|
5
|
+
var STATUS_KEY = "exchange";
|
|
6
|
+
var TICK_MS = 1e3;
|
|
7
|
+
var TOOL_SHARE_NOTE = 0.25;
|
|
8
|
+
var emptyTotals = () => ({
|
|
9
|
+
input: 0,
|
|
10
|
+
output: 0,
|
|
11
|
+
reasoning: 0,
|
|
12
|
+
cacheRead: 0,
|
|
13
|
+
cacheWrite: 0,
|
|
14
|
+
totalTokens: 0,
|
|
15
|
+
cost: 0
|
|
16
|
+
});
|
|
17
|
+
function fmtDuration(ms) {
|
|
18
|
+
if (!Number.isFinite(ms) || ms < 0) return "\u2014";
|
|
19
|
+
if (ms < 1e3) return `${Math.round(ms)}ms`;
|
|
20
|
+
const s = ms / 1e3;
|
|
21
|
+
if (s < 60) return `${s.toFixed(1)}s`;
|
|
22
|
+
const m = Math.floor(s / 60);
|
|
23
|
+
const rem = Math.round(s % 60);
|
|
24
|
+
return rem > 0 ? `${m}m${rem}s` : `${m}m`;
|
|
25
|
+
}
|
|
26
|
+
function fmtTokens(n) {
|
|
27
|
+
if (!Number.isFinite(n) || n <= 0) return "0";
|
|
28
|
+
if (n < 1e3) return `${Math.round(n)}`;
|
|
29
|
+
if (n < 1e6) return `${(n / 1e3).toFixed(1)}k`;
|
|
30
|
+
return `${(n / 1e6).toFixed(2)}M`;
|
|
31
|
+
}
|
|
32
|
+
function fmtCost(cost) {
|
|
33
|
+
if (!Number.isFinite(cost) || cost <= 0) return "$0";
|
|
34
|
+
return cost < 0.01 ? `$${cost.toFixed(5)}` : `$${cost.toFixed(4)}`;
|
|
35
|
+
}
|
|
36
|
+
function fmtRate(tokensPerSec) {
|
|
37
|
+
if (!Number.isFinite(tokensPerSec) || tokensPerSec <= 0) return "\u2014";
|
|
38
|
+
return tokensPerSec < 1e3 ? `${Math.round(tokensPerSec)} tok/s` : `${(tokensPerSec / 1e3).toFixed(1)}k tok/s`;
|
|
39
|
+
}
|
|
40
|
+
function plural(n, one, many = `${one}s`) {
|
|
41
|
+
return n === 1 ? `${n} ${one}` : `${n} ${many}`;
|
|
42
|
+
}
|
|
43
|
+
function openToolText(spans, now) {
|
|
44
|
+
let longest;
|
|
45
|
+
for (const span of spans.values()) {
|
|
46
|
+
if (!longest || now - span.start > now - longest.start) longest = span;
|
|
47
|
+
}
|
|
48
|
+
if (!longest) return void 0;
|
|
49
|
+
const extra = spans.size > 1 ? ` (+${spans.size - 1})` : "";
|
|
50
|
+
return `${longest.name} ${fmtDuration(now - longest.start)}${extra}`;
|
|
51
|
+
}
|
|
52
|
+
function expandHint() {
|
|
53
|
+
try {
|
|
54
|
+
return ` (${keyHint("app.tools.expand", "to expand")})`;
|
|
55
|
+
} catch {
|
|
56
|
+
return " (expand for per-turn detail)";
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function toolBreakdown(turn) {
|
|
60
|
+
return turn.tools.map((tool) => `${tool.name} ${fmtDuration(tool.ms)}${tool.isError ? " (failed)" : ""}`).join(", ");
|
|
61
|
+
}
|
|
62
|
+
function unionMs(runs) {
|
|
63
|
+
if (runs.length === 0) return 0;
|
|
64
|
+
const sorted = [...runs].sort((a, b) => a.start - b.start);
|
|
65
|
+
let total = 0;
|
|
66
|
+
let start = sorted[0].start;
|
|
67
|
+
let end = sorted[0].end;
|
|
68
|
+
for (const run of sorted.slice(1)) {
|
|
69
|
+
if (run.start > end) {
|
|
70
|
+
total += end - start;
|
|
71
|
+
start = run.start;
|
|
72
|
+
end = run.end;
|
|
73
|
+
} else if (run.end > end) {
|
|
74
|
+
end = run.end;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return total + (end - start);
|
|
78
|
+
}
|
|
79
|
+
function exchange_stats_default(pi) {
|
|
80
|
+
const sessionTotals = {
|
|
81
|
+
...emptyTotals(),
|
|
82
|
+
exchanges: 0,
|
|
83
|
+
turnCount: 0,
|
|
84
|
+
durationMs: 0,
|
|
85
|
+
toolMs: 0,
|
|
86
|
+
waitingMs: 0
|
|
87
|
+
};
|
|
88
|
+
let sessionStartedAt = 0;
|
|
89
|
+
let running = false;
|
|
90
|
+
let startedAt = 0;
|
|
91
|
+
let promptCount = 0;
|
|
92
|
+
let exchangeIndex = 0;
|
|
93
|
+
let model = "unknown";
|
|
94
|
+
let stopReason = "stop";
|
|
95
|
+
let turns = [];
|
|
96
|
+
let totals = emptyTotals();
|
|
97
|
+
let waitingMs = 0;
|
|
98
|
+
let waitingStart;
|
|
99
|
+
let liveTimer;
|
|
100
|
+
let liveRefresh;
|
|
101
|
+
let activeTurn;
|
|
102
|
+
let activeToolRuns = [];
|
|
103
|
+
function setStatus(text, ctx) {
|
|
104
|
+
if (!ctx.hasUI) return;
|
|
105
|
+
ctx.ui.setStatus(STATUS_KEY, text);
|
|
106
|
+
}
|
|
107
|
+
function liveStatusText() {
|
|
108
|
+
const now = Date.now();
|
|
109
|
+
const parts = [`\u23F1 ${fmtDuration(now - startedAt)}`];
|
|
110
|
+
if (turns.length > 0) parts.push(`${plural(turns.length, "turn")} done`);
|
|
111
|
+
const toolText = activeTurn ? openToolText(activeTurn.spans, now) : void 0;
|
|
112
|
+
if (toolText) parts.push(toolText);
|
|
113
|
+
else if (activeTurn) parts.push(`turn ${activeTurn.index}`);
|
|
114
|
+
return parts.join(" \xB7 ");
|
|
115
|
+
}
|
|
116
|
+
function stopLiveTimer() {
|
|
117
|
+
if (liveTimer) clearInterval(liveTimer);
|
|
118
|
+
liveTimer = void 0;
|
|
119
|
+
liveRefresh = void 0;
|
|
120
|
+
}
|
|
121
|
+
function resetExchangeState() {
|
|
122
|
+
running = false;
|
|
123
|
+
startedAt = 0;
|
|
124
|
+
promptCount = 0;
|
|
125
|
+
turns = [];
|
|
126
|
+
totals = emptyTotals();
|
|
127
|
+
waitingMs = 0;
|
|
128
|
+
waitingStart = void 0;
|
|
129
|
+
activeTurn = void 0;
|
|
130
|
+
activeToolRuns = [];
|
|
131
|
+
stopLiveTimer();
|
|
132
|
+
}
|
|
133
|
+
pi.registerEntryRenderer(ENTRY_TYPE, (entry, { expanded }, theme) => {
|
|
134
|
+
const data = entry.data;
|
|
135
|
+
const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
|
|
136
|
+
if (!data) {
|
|
137
|
+
box.addChild(new Text(theme.fg("dim", "(no stats)"), 0, 0));
|
|
138
|
+
return box;
|
|
139
|
+
}
|
|
140
|
+
const isSession = data.kind === "session";
|
|
141
|
+
const headline = isSession ? `\u{1F4CA} Session \xB7 ${plural(data.turnCount, "turn")} across ${plural(data.index, "exchange")}` : `\u23F1 Exchange ${data.index} \xB7 ${fmtDuration(data.durationMs)}`;
|
|
142
|
+
box.addChild(
|
|
143
|
+
new Text(
|
|
144
|
+
theme.fg("accent", theme.bold(headline)) + theme.fg("dim", ` ${data.model}`) + (!expanded && !isSession ? theme.fg("dim", expandHint()) : ""),
|
|
145
|
+
0,
|
|
146
|
+
0
|
|
147
|
+
)
|
|
148
|
+
);
|
|
149
|
+
const summary = [];
|
|
150
|
+
if (isSession) {
|
|
151
|
+
summary.push(`active ${fmtDuration(data.durationMs)}`);
|
|
152
|
+
} else {
|
|
153
|
+
summary.push(plural(data.turnCount, "turn"), plural(data.promptCount, "prompt"));
|
|
154
|
+
}
|
|
155
|
+
if (data.toolMs > 0) summary.push(`tools ${fmtDuration(data.toolMs)}`);
|
|
156
|
+
summary.push(`out ${fmtTokens(data.output)}`, fmtCost(data.cost));
|
|
157
|
+
if (data.waitingMs > 0) summary.push(`waiting ${fmtDuration(data.waitingMs)}`);
|
|
158
|
+
box.addChild(new Text(theme.fg("dim", summary.join(" \xB7 ")), 0, 0));
|
|
159
|
+
if (expanded) {
|
|
160
|
+
for (const turn of data.turns) {
|
|
161
|
+
const detail = [`model ${fmtDuration(turn.modelMs)}`];
|
|
162
|
+
if (turn.toolMs > 0) detail.push(`tools ${fmtDuration(turn.toolMs)} (${toolBreakdown(turn)})`);
|
|
163
|
+
detail.push(`out ${fmtTokens(turn.output)}`, fmtRate(turn.outputPerSec));
|
|
164
|
+
box.addChild(
|
|
165
|
+
new Text(
|
|
166
|
+
` ${theme.fg("dim", `#${String(turn.index).padStart(2, " ")}`)} ` + theme.fg("text", fmtDuration(turn.durationMs).padStart(8, " ")) + theme.fg("dim", ` ${detail.join(" \xB7 ")}`),
|
|
167
|
+
0,
|
|
168
|
+
0
|
|
169
|
+
)
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
if (isSession && data.turnCount > 0) {
|
|
173
|
+
box.addChild(
|
|
174
|
+
new Text(
|
|
175
|
+
theme.fg("dim", `avg ${fmtDuration(data.durationMs / data.turnCount)} per turn`),
|
|
176
|
+
0,
|
|
177
|
+
0
|
|
178
|
+
)
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
const tokenParts = [
|
|
182
|
+
`in ${fmtTokens(data.input)}`,
|
|
183
|
+
`out ${fmtTokens(data.output)}`,
|
|
184
|
+
`cache r ${fmtTokens(data.cacheRead)} / w ${fmtTokens(data.cacheWrite)}`,
|
|
185
|
+
`total ${fmtTokens(data.totalTokens)}`,
|
|
186
|
+
fmtCost(data.cost)
|
|
187
|
+
];
|
|
188
|
+
if (data.reasoning > 0) tokenParts.splice(2, 0, `thinking ${fmtTokens(data.reasoning)}`);
|
|
189
|
+
box.addChild(new Text(theme.fg("dim", tokenParts.join(" \xB7 ")), 0, 0));
|
|
190
|
+
if (data.startedAt > 0) {
|
|
191
|
+
const from = new Date(data.startedAt).toLocaleTimeString();
|
|
192
|
+
const to = new Date(data.endedAt).toLocaleTimeString();
|
|
193
|
+
box.addChild(new Text(theme.fg("dim", `${from} \u2192 ${to} stop: ${data.stopReason}`), 0, 0));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return box;
|
|
197
|
+
});
|
|
198
|
+
pi.on("session_start", (_event, ctx) => {
|
|
199
|
+
resetExchangeState();
|
|
200
|
+
sessionStartedAt = Date.now();
|
|
201
|
+
setStatus("\u23F1 ready", ctx);
|
|
202
|
+
});
|
|
203
|
+
pi.on("session_shutdown", () => {
|
|
204
|
+
resetExchangeState();
|
|
205
|
+
});
|
|
206
|
+
pi.on("before_agent_start", (_event, ctx) => {
|
|
207
|
+
if (running) {
|
|
208
|
+
promptCount++;
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
running = true;
|
|
212
|
+
startedAt = Date.now();
|
|
213
|
+
promptCount = 1;
|
|
214
|
+
exchangeIndex++;
|
|
215
|
+
turns = [];
|
|
216
|
+
totals = emptyTotals();
|
|
217
|
+
waitingMs = 0;
|
|
218
|
+
waitingStart = void 0;
|
|
219
|
+
activeTurn = void 0;
|
|
220
|
+
activeToolRuns = [];
|
|
221
|
+
model = ctx.model?.id ?? "unknown";
|
|
222
|
+
stopReason = "stop";
|
|
223
|
+
stopLiveTimer();
|
|
224
|
+
setStatus(liveStatusText(), ctx);
|
|
225
|
+
liveRefresh = () => setStatus(liveStatusText(), ctx);
|
|
226
|
+
liveTimer = setInterval(() => {
|
|
227
|
+
try {
|
|
228
|
+
liveRefresh?.();
|
|
229
|
+
} catch {
|
|
230
|
+
stopLiveTimer();
|
|
231
|
+
}
|
|
232
|
+
}, TICK_MS);
|
|
233
|
+
});
|
|
234
|
+
pi.on("turn_start", (event, _ctx) => {
|
|
235
|
+
if (!running) return;
|
|
236
|
+
activeTurn = { index: event.turnIndex ?? turns.length + 1, startedAt: Date.now(), spans: /* @__PURE__ */ new Map() };
|
|
237
|
+
activeToolRuns = [];
|
|
238
|
+
});
|
|
239
|
+
pi.on("tool_execution_start", (event, _ctx) => {
|
|
240
|
+
if (!running || !activeTurn) return;
|
|
241
|
+
activeTurn.spans.set(event.toolCallId, { name: event.toolName, start: Date.now() });
|
|
242
|
+
});
|
|
243
|
+
pi.on("tool_execution_end", (event, _ctx) => {
|
|
244
|
+
if (!running || !activeTurn) return;
|
|
245
|
+
const open = activeTurn.spans.get(event.toolCallId);
|
|
246
|
+
if (open) {
|
|
247
|
+
activeTurn.spans.delete(event.toolCallId);
|
|
248
|
+
activeToolRuns.push({
|
|
249
|
+
name: event.toolName ?? open.name,
|
|
250
|
+
start: open.start,
|
|
251
|
+
end: Date.now(),
|
|
252
|
+
isError: Boolean(event.isError)
|
|
253
|
+
});
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
const at = Date.now();
|
|
257
|
+
activeToolRuns.push({ name: event.toolName, start: at, end: at, isError: Boolean(event.isError) });
|
|
258
|
+
});
|
|
259
|
+
pi.on("turn_end", (event, _ctx) => {
|
|
260
|
+
if (!running || !activeTurn) return;
|
|
261
|
+
const endedAt = Date.now();
|
|
262
|
+
for (const [id, open] of activeTurn.spans) {
|
|
263
|
+
activeToolRuns.push({ name: open.name, start: open.start, end: endedAt, isError: false });
|
|
264
|
+
activeTurn.spans.delete(id);
|
|
265
|
+
}
|
|
266
|
+
const usage = event.message?.role === "assistant" ? event.message.usage : void 0;
|
|
267
|
+
const durationMs = endedAt - activeTurn.startedAt;
|
|
268
|
+
const toolMs = unionMs(activeToolRuns);
|
|
269
|
+
const output = usage?.output ?? 0;
|
|
270
|
+
turns.push({
|
|
271
|
+
index: activeTurn.index,
|
|
272
|
+
durationMs,
|
|
273
|
+
toolMs,
|
|
274
|
+
modelMs: Math.max(0, durationMs - toolMs),
|
|
275
|
+
outputPerSec: durationMs > 0 ? output / (durationMs / 1e3) : 0,
|
|
276
|
+
tools: activeToolRuns.map((run) => ({ name: run.name, ms: run.end - run.start, isError: run.isError })),
|
|
277
|
+
input: usage?.input ?? 0,
|
|
278
|
+
output,
|
|
279
|
+
reasoning: usage?.reasoning ?? 0,
|
|
280
|
+
cacheRead: usage?.cacheRead ?? 0,
|
|
281
|
+
cacheWrite: usage?.cacheWrite ?? 0,
|
|
282
|
+
totalTokens: usage?.totalTokens ?? 0,
|
|
283
|
+
cost: usage?.cost?.total ?? 0
|
|
284
|
+
});
|
|
285
|
+
totals.input += usage?.input ?? 0;
|
|
286
|
+
totals.output += output;
|
|
287
|
+
totals.reasoning += usage?.reasoning ?? 0;
|
|
288
|
+
totals.cacheRead += usage?.cacheRead ?? 0;
|
|
289
|
+
totals.cacheWrite += usage?.cacheWrite ?? 0;
|
|
290
|
+
totals.totalTokens += usage?.totalTokens ?? 0;
|
|
291
|
+
totals.cost += usage?.cost?.total ?? 0;
|
|
292
|
+
if (event.message?.stopReason) stopReason = event.message.stopReason;
|
|
293
|
+
activeTurn = void 0;
|
|
294
|
+
activeToolRuns = [];
|
|
295
|
+
});
|
|
296
|
+
pi.on("ui_prompt_start", (_event, _ctx) => {
|
|
297
|
+
if (!running || waitingStart !== void 0) return;
|
|
298
|
+
waitingStart = Date.now();
|
|
299
|
+
});
|
|
300
|
+
pi.on("ui_prompt_end", (_event, _ctx) => {
|
|
301
|
+
if (waitingStart === void 0) return;
|
|
302
|
+
waitingMs += Date.now() - waitingStart;
|
|
303
|
+
waitingStart = void 0;
|
|
304
|
+
});
|
|
305
|
+
pi.on("agent_settled", (_event, ctx) => {
|
|
306
|
+
if (!running) return;
|
|
307
|
+
const endedAt = Date.now();
|
|
308
|
+
if (waitingStart !== void 0) {
|
|
309
|
+
waitingMs += endedAt - waitingStart;
|
|
310
|
+
waitingStart = void 0;
|
|
311
|
+
}
|
|
312
|
+
const durationMs = endedAt - startedAt;
|
|
313
|
+
const toolMs = turns.reduce((sum, turn) => sum + turn.toolMs, 0);
|
|
314
|
+
const record = {
|
|
315
|
+
...totals,
|
|
316
|
+
kind: "exchange",
|
|
317
|
+
index: exchangeIndex,
|
|
318
|
+
promptCount,
|
|
319
|
+
turnCount: turns.length,
|
|
320
|
+
turns,
|
|
321
|
+
startedAt,
|
|
322
|
+
endedAt,
|
|
323
|
+
durationMs,
|
|
324
|
+
waitingMs,
|
|
325
|
+
toolMs,
|
|
326
|
+
model,
|
|
327
|
+
stopReason
|
|
328
|
+
};
|
|
329
|
+
stopLiveTimer();
|
|
330
|
+
sessionTotals.input += totals.input;
|
|
331
|
+
sessionTotals.output += totals.output;
|
|
332
|
+
sessionTotals.reasoning += totals.reasoning;
|
|
333
|
+
sessionTotals.cacheRead += totals.cacheRead;
|
|
334
|
+
sessionTotals.cacheWrite += totals.cacheWrite;
|
|
335
|
+
sessionTotals.totalTokens += totals.totalTokens;
|
|
336
|
+
sessionTotals.cost += totals.cost;
|
|
337
|
+
sessionTotals.exchanges++;
|
|
338
|
+
sessionTotals.turnCount += turns.length;
|
|
339
|
+
sessionTotals.durationMs += durationMs;
|
|
340
|
+
sessionTotals.toolMs += toolMs;
|
|
341
|
+
sessionTotals.waitingMs += waitingMs;
|
|
342
|
+
try {
|
|
343
|
+
pi.appendEntry(ENTRY_TYPE, record);
|
|
344
|
+
} catch {
|
|
345
|
+
}
|
|
346
|
+
const parts = [`\u23F1 ${fmtDuration(durationMs)}`, plural(turns.length, "turn")];
|
|
347
|
+
if (toolMs > 0 && durationMs > 0 && toolMs / durationMs >= TOOL_SHARE_NOTE) {
|
|
348
|
+
parts.push(`tools ${fmtDuration(toolMs)}`);
|
|
349
|
+
}
|
|
350
|
+
parts.push(`out ${fmtTokens(totals.output)}`, fmtCost(totals.cost));
|
|
351
|
+
if (waitingMs > 0) parts.push(`waiting ${fmtDuration(waitingMs)}`);
|
|
352
|
+
setStatus(parts.join(" \xB7 "), ctx);
|
|
353
|
+
resetExchangeState();
|
|
354
|
+
});
|
|
355
|
+
pi.registerCommand("exstats", {
|
|
356
|
+
description: "Append a cumulative session timing card",
|
|
357
|
+
handler: () => {
|
|
358
|
+
const record = {
|
|
359
|
+
...sessionTotals,
|
|
360
|
+
kind: "session",
|
|
361
|
+
index: sessionTotals.exchanges,
|
|
362
|
+
promptCount: sessionTotals.exchanges,
|
|
363
|
+
turnCount: sessionTotals.turnCount,
|
|
364
|
+
turns: [],
|
|
365
|
+
startedAt: sessionStartedAt,
|
|
366
|
+
endedAt: Date.now(),
|
|
367
|
+
durationMs: sessionTotals.durationMs,
|
|
368
|
+
waitingMs: sessionTotals.waitingMs,
|
|
369
|
+
toolMs: sessionTotals.toolMs,
|
|
370
|
+
model: `${plural(sessionTotals.turnCount, "turn")} tracked`,
|
|
371
|
+
stopReason: "session"
|
|
372
|
+
};
|
|
373
|
+
try {
|
|
374
|
+
pi.appendEntry(ENTRY_TYPE, record);
|
|
375
|
+
} catch {
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
export {
|
|
381
|
+
exchange_stats_default as default
|
|
382
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-exchange-stats",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Show timing and cost for each Pi exchange.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./exchange-stats.js",
|
|
7
|
+
"exports": "./exchange-stats.js",
|
|
8
|
+
"files": [
|
|
9
|
+
"exchange-stats.js",
|
|
10
|
+
"README.md",
|
|
11
|
+
"LICENSE"
|
|
12
|
+
],
|
|
13
|
+
"keywords": [
|
|
14
|
+
"pi-package"
|
|
15
|
+
],
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/ezoushen/pi-extensions.git",
|
|
20
|
+
"directory": "extensions/exchange-stats"
|
|
21
|
+
},
|
|
22
|
+
"homepage": "https://github.com/ezoushen/pi-extensions/tree/main/extensions/exchange-stats#readme",
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://github.com/ezoushen/pi-extensions/issues"
|
|
25
|
+
},
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
28
|
+
"@earendil-works/pi-tui": "*"
|
|
29
|
+
},
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
},
|
|
33
|
+
"pi": {
|
|
34
|
+
"extensions": [
|
|
35
|
+
"./exchange-stats.js"
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
}
|