agent-profiler 1.0.0 → 1.0.1
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 +33 -3
- package/assets/dashboard.png +0 -0
- package/dist/adapters/cursor.js +5 -1
- package/dist/cli.js +23 -0
- package/dist/commands/dashboard.js +17 -0
- package/dist/commands/init.js +26 -0
- package/dist/commands/last.js +3 -191
- package/dist/core/dashboardServer.js +294 -0
- package/dist/core/db.js +164 -54
- package/dist/core/eventMetadata.js +7 -2
- package/dist/core/gitWorkspace.js +16 -4
- package/dist/core/sessionAnalytics.js +204 -0
- package/dist/dashboard/app.js +329 -0
- package/dist/dashboard/index.html +89 -0
- package/dist/dashboard/styles.css +389 -0
- package/package.json +5 -3
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
function fmt(n) {
|
|
2
|
+
if (n == null || Number.isNaN(n)) return "—";
|
|
3
|
+
return new Intl.NumberFormat("en-US").format(Math.round(n));
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
function el(id) {
|
|
7
|
+
const n = document.getElementById(id);
|
|
8
|
+
if (!n) throw new Error(`missing #${id}`);
|
|
9
|
+
return n;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function encodeQuery(obj) {
|
|
13
|
+
const p = new URLSearchParams();
|
|
14
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
15
|
+
if (v !== undefined && v !== null && v !== "") p.set(k, String(v));
|
|
16
|
+
}
|
|
17
|
+
return p.toString();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function roleClass(role) {
|
|
21
|
+
if (role === "user_prompt") return "user";
|
|
22
|
+
if (
|
|
23
|
+
role === "assistant_message" ||
|
|
24
|
+
role === "assistant_delta" ||
|
|
25
|
+
role === "assistant"
|
|
26
|
+
)
|
|
27
|
+
return "assistant";
|
|
28
|
+
if (role.startsWith("tool_")) return "tool";
|
|
29
|
+
if (role.startsWith("shell_")) return "shell";
|
|
30
|
+
return "other";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function renderUsageBars(container, usage) {
|
|
34
|
+
container.replaceChildren();
|
|
35
|
+
if (!usage) {
|
|
36
|
+
container.textContent = "No data.";
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const max = Math.max(
|
|
40
|
+
1,
|
|
41
|
+
usage.input + usage.output + usage.toolResults + usage.shellOutput,
|
|
42
|
+
);
|
|
43
|
+
const rows = [
|
|
44
|
+
["Input", usage.input, "input"],
|
|
45
|
+
["Output", usage.output, "output"],
|
|
46
|
+
["Tool / MCP", usage.toolResults, "tool"],
|
|
47
|
+
["Shell", usage.shellOutput, "shell"],
|
|
48
|
+
];
|
|
49
|
+
for (const [label, value, cls] of rows) {
|
|
50
|
+
const row = document.createElement("div");
|
|
51
|
+
row.className = "bar-row";
|
|
52
|
+
const lab = document.createElement("span");
|
|
53
|
+
lab.textContent = label;
|
|
54
|
+
const track = document.createElement("div");
|
|
55
|
+
track.className = "bar-track";
|
|
56
|
+
const fill = document.createElement("div");
|
|
57
|
+
fill.className = `bar-fill ${cls}`;
|
|
58
|
+
fill.style.width = `${(value / max) * 100}%`;
|
|
59
|
+
track.appendChild(fill);
|
|
60
|
+
const pct = document.createElement("span");
|
|
61
|
+
pct.className = "muted small";
|
|
62
|
+
pct.style.minWidth = "4.5rem";
|
|
63
|
+
pct.style.textAlign = "right";
|
|
64
|
+
pct.textContent = fmt(value);
|
|
65
|
+
row.append(lab, track, pct);
|
|
66
|
+
container.appendChild(row);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function renderVerticalBars(container, items, opts = {}) {
|
|
71
|
+
const { maxHeight = 100, valueKey = "value", labelKey = "label" } = opts;
|
|
72
|
+
container.replaceChildren();
|
|
73
|
+
if (!items || items.length === 0) {
|
|
74
|
+
container.textContent = "No data.";
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const maxVal = Math.max(1, ...items.map((i) => i[valueKey]));
|
|
78
|
+
for (const item of items) {
|
|
79
|
+
const col = document.createElement("div");
|
|
80
|
+
col.className = "vbar-col";
|
|
81
|
+
const track = document.createElement("div");
|
|
82
|
+
track.className = "vbar-track";
|
|
83
|
+
const fill = document.createElement("div");
|
|
84
|
+
fill.className = "vbar-fill";
|
|
85
|
+
fill.style.height = `${(item[valueKey] / maxVal) * maxHeight}px`;
|
|
86
|
+
track.appendChild(fill);
|
|
87
|
+
const lab = document.createElement("span");
|
|
88
|
+
lab.className = "vbar-label";
|
|
89
|
+
lab.textContent = item[labelKey];
|
|
90
|
+
const val = document.createElement("span");
|
|
91
|
+
val.textContent = fmt(item[valueKey]);
|
|
92
|
+
col.append(track, val, lab);
|
|
93
|
+
container.appendChild(col);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function renderTimeline(container, timeline) {
|
|
98
|
+
container.replaceChildren();
|
|
99
|
+
if (!timeline || timeline.length === 0) {
|
|
100
|
+
container.textContent = "No events.";
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const t0 = new Date(timeline[0].createdAt).getTime();
|
|
104
|
+
const t1 = new Date(timeline[timeline.length - 1].createdAt).getTime();
|
|
105
|
+
const span = Math.max(1, t1 - t0);
|
|
106
|
+
for (let i = 0; i < timeline.length; i++) {
|
|
107
|
+
const ev = timeline[i];
|
|
108
|
+
const seg = document.createElement("div");
|
|
109
|
+
seg.className = `timeline-seg ${roleClass(ev.role)}`;
|
|
110
|
+
const start = new Date(ev.createdAt).getTime();
|
|
111
|
+
const end =
|
|
112
|
+
i + 1 < timeline.length
|
|
113
|
+
? new Date(timeline[i + 1].createdAt).getTime()
|
|
114
|
+
: start + 1;
|
|
115
|
+
const w = Math.max(0.15, ((end - start) / span) * 100);
|
|
116
|
+
seg.style.width = `${w}%`;
|
|
117
|
+
seg.title = `${ev.role} · ${fmt(ev.estimatedTotalTokens)} tok`;
|
|
118
|
+
container.appendChild(seg);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function renderSparkline(svg, points) {
|
|
123
|
+
svg.replaceChildren();
|
|
124
|
+
if (!points || points.length === 0) return;
|
|
125
|
+
const w = 200;
|
|
126
|
+
const h = 48;
|
|
127
|
+
const pad = 4;
|
|
128
|
+
const ys = points.map((p) => p.efficiencyScore);
|
|
129
|
+
const minY = Math.min(...ys);
|
|
130
|
+
const maxY = Math.max(...ys, 1);
|
|
131
|
+
const coords = points.map((p, i) => {
|
|
132
|
+
const x = pad + (i / Math.max(1, points.length - 1)) * (w - pad * 2);
|
|
133
|
+
const y =
|
|
134
|
+
h -
|
|
135
|
+
pad -
|
|
136
|
+
((p.efficiencyScore - minY) / Math.max(1e-6, maxY - minY)) *
|
|
137
|
+
(h - pad * 2);
|
|
138
|
+
return `${x},${y}`;
|
|
139
|
+
});
|
|
140
|
+
const poly = document.createElementNS(
|
|
141
|
+
"http://www.w3.org/2000/svg",
|
|
142
|
+
"polyline",
|
|
143
|
+
);
|
|
144
|
+
poly.setAttribute("points", coords.join(" "));
|
|
145
|
+
svg.appendChild(poly);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function fetchJson(path) {
|
|
149
|
+
const res = await fetch(path);
|
|
150
|
+
if (!res.ok) throw new Error(`${path} ${res.status}`);
|
|
151
|
+
return res.json();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
let selectedSessionKey = "";
|
|
155
|
+
|
|
156
|
+
function sessionOptionKey(row) {
|
|
157
|
+
return `${row.source}\t${row.sessionId}`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function loadSessions(selectEl, overviewDesc, preferredValue) {
|
|
161
|
+
const data = await fetchJson(`/api/sessions?limit=30`);
|
|
162
|
+
const keep = preferredValue ?? selectEl.value;
|
|
163
|
+
selectEl.replaceChildren();
|
|
164
|
+
const optLatest = document.createElement("option");
|
|
165
|
+
optLatest.value = "";
|
|
166
|
+
optLatest.textContent = "Latest activity";
|
|
167
|
+
selectEl.appendChild(optLatest);
|
|
168
|
+
|
|
169
|
+
for (const row of data.sessions) {
|
|
170
|
+
const opt = document.createElement("option");
|
|
171
|
+
opt.value = sessionOptionKey(row);
|
|
172
|
+
const shortId =
|
|
173
|
+
row.sessionId.length > 12
|
|
174
|
+
? `${row.sessionId.slice(0, 12)}…`
|
|
175
|
+
: row.sessionId;
|
|
176
|
+
opt.textContent = `${row.source} · ${shortId} · ${row.eventCount} events`;
|
|
177
|
+
selectEl.appendChild(opt);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (keep && [...selectEl.options].some((o) => o.value === keep)) {
|
|
181
|
+
selectEl.value = keep;
|
|
182
|
+
selectedSessionKey = keep;
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (
|
|
187
|
+
overviewDesc &&
|
|
188
|
+
overviewDesc.sessionId &&
|
|
189
|
+
overviewDesc.sessionId.trim().length > 0
|
|
190
|
+
) {
|
|
191
|
+
const key = sessionOptionKey({
|
|
192
|
+
source: overviewDesc.source,
|
|
193
|
+
sessionId: overviewDesc.sessionId,
|
|
194
|
+
repoPath: overviewDesc.repoPath,
|
|
195
|
+
startedAt: "",
|
|
196
|
+
endedAt: "",
|
|
197
|
+
eventCount: 0,
|
|
198
|
+
});
|
|
199
|
+
if ([...selectEl.options].some((o) => o.value === key)) {
|
|
200
|
+
selectEl.value = key;
|
|
201
|
+
selectedSessionKey = key;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function parseSelectedSession(selectEl) {
|
|
207
|
+
const v = selectEl.value;
|
|
208
|
+
if (!v) return null;
|
|
209
|
+
const [source, sessionId] = v.split("\t");
|
|
210
|
+
return { source, sessionId };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function refreshAll() {
|
|
214
|
+
const metaLine = el("meta-line");
|
|
215
|
+
const sessionSelect = el("session-select");
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
const overview = await fetchJson("/api/overview");
|
|
219
|
+
metaLine.textContent = overview.databasePath
|
|
220
|
+
? `Database: ${overview.databasePath}`
|
|
221
|
+
: "";
|
|
222
|
+
|
|
223
|
+
await loadSessions(sessionSelect, overview.descriptor, selectedSessionKey);
|
|
224
|
+
|
|
225
|
+
const sel = parseSelectedSession(sessionSelect);
|
|
226
|
+
let report = overview.report;
|
|
227
|
+
let timelineQs;
|
|
228
|
+
let histQs;
|
|
229
|
+
|
|
230
|
+
if (sel) {
|
|
231
|
+
report = (await fetchJson(`/api/session/report?${encodeQuery(sel)}`))
|
|
232
|
+
.report;
|
|
233
|
+
timelineQs = encodeQuery(sel);
|
|
234
|
+
histQs = encodeQuery(sel);
|
|
235
|
+
} else if (overview.descriptor) {
|
|
236
|
+
const d = overview.descriptor;
|
|
237
|
+
if (d.sessionId && d.sessionId.trim().length > 0) {
|
|
238
|
+
timelineQs = encodeQuery({ source: d.source, sessionId: d.sessionId });
|
|
239
|
+
histQs = timelineQs;
|
|
240
|
+
} else {
|
|
241
|
+
timelineQs = encodeQuery({
|
|
242
|
+
source: d.source,
|
|
243
|
+
legacy: "1",
|
|
244
|
+
repoPath: d.repoPath ?? "",
|
|
245
|
+
});
|
|
246
|
+
histQs = timelineQs;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
el("total-tokens").textContent =
|
|
251
|
+
report && report.usage ? fmt(report.usage.total) : "—";
|
|
252
|
+
|
|
253
|
+
renderUsageBars(el("usage-bars"), report?.usage ?? null);
|
|
254
|
+
|
|
255
|
+
el("efficiency-score").textContent =
|
|
256
|
+
report != null ? String(report.efficiencyScore) : "—";
|
|
257
|
+
|
|
258
|
+
const histData = timelineQs
|
|
259
|
+
? await fetchJson(`/api/tool-histogram?${histQs}`)
|
|
260
|
+
: { histogram: [] };
|
|
261
|
+
renderVerticalBars(
|
|
262
|
+
el("tool-histogram"),
|
|
263
|
+
(histData.histogram ?? []).map((h) => ({
|
|
264
|
+
label: h.bucket,
|
|
265
|
+
value: h.count,
|
|
266
|
+
})),
|
|
267
|
+
{ maxHeight: 90 },
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
const audit = await fetchJson("/api/context-audit");
|
|
271
|
+
renderVerticalBars(
|
|
272
|
+
el("context-bars"),
|
|
273
|
+
(audit.files ?? []).map((f) => ({
|
|
274
|
+
label: f.path.replace(/^.*\//, ""),
|
|
275
|
+
value: f.estimatedTokens,
|
|
276
|
+
})),
|
|
277
|
+
{ maxHeight: 140 },
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
const timelineData = timelineQs
|
|
281
|
+
? await fetchJson(`/api/session/timeline?${timelineQs}`)
|
|
282
|
+
: { timeline: [] };
|
|
283
|
+
el("timeline-meta").textContent =
|
|
284
|
+
report != null
|
|
285
|
+
? `${report.sessionShape.turns} turns · ${report.sessionShape.fileEdits} edits · ${report.sessionShape.shellCalls} shell · ${report.sessionShape.toolCalls} tool calls · ${report.durationMinutes} min`
|
|
286
|
+
: "";
|
|
287
|
+
|
|
288
|
+
renderTimeline(el("timeline-track"), timelineData.timeline);
|
|
289
|
+
|
|
290
|
+
const scores = await fetchJson("/api/score-history?limit=15");
|
|
291
|
+
renderSparkline(el("score-sparkline"), scores.points ?? []);
|
|
292
|
+
|
|
293
|
+
const flagsUl = el("red-flags");
|
|
294
|
+
flagsUl.replaceChildren();
|
|
295
|
+
if (report?.redFlags?.length) {
|
|
296
|
+
for (const f of report.redFlags) {
|
|
297
|
+
const li = document.createElement("li");
|
|
298
|
+
const sev = document.createElement("span");
|
|
299
|
+
sev.className = `sev ${f.severity === "MEDIUM" ? "medium" : ""}`;
|
|
300
|
+
sev.textContent = f.severity;
|
|
301
|
+
li.append(sev, document.createTextNode(`${f.title}: ${f.detail}`));
|
|
302
|
+
flagsUl.appendChild(li);
|
|
303
|
+
}
|
|
304
|
+
} else {
|
|
305
|
+
flagsUl.innerHTML = `<li class="muted">None</li>`;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const recOl = el("recommendations");
|
|
309
|
+
recOl.replaceChildren();
|
|
310
|
+
if (report?.recommendations?.length) {
|
|
311
|
+
for (const r of report.recommendations) {
|
|
312
|
+
const li = document.createElement("li");
|
|
313
|
+
li.textContent = r;
|
|
314
|
+
recOl.appendChild(li);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
} catch (e) {
|
|
318
|
+
metaLine.textContent = String(e.message ?? e);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
el("refresh-btn").addEventListener("click", () => refreshAll());
|
|
323
|
+
|
|
324
|
+
el("session-select").addEventListener("change", () => {
|
|
325
|
+
selectedSessionKey = el("session-select").value;
|
|
326
|
+
void refreshAll();
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
void refreshAll();
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>Agent Profiler Dashboard</title>
|
|
7
|
+
<link rel="stylesheet" href="./styles.css" />
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div class="layout">
|
|
11
|
+
<header class="header">
|
|
12
|
+
<div>
|
|
13
|
+
<h1>Agent Profiler Dashboard</h1>
|
|
14
|
+
<p class="muted" id="meta-line"></p>
|
|
15
|
+
</div>
|
|
16
|
+
<div class="header-actions">
|
|
17
|
+
<label class="session-label">
|
|
18
|
+
Session
|
|
19
|
+
<select id="session-select" aria-label="Session"></select>
|
|
20
|
+
</label>
|
|
21
|
+
<button type="button" id="refresh-btn">Refresh</button>
|
|
22
|
+
</div>
|
|
23
|
+
</header>
|
|
24
|
+
|
|
25
|
+
<main class="grid">
|
|
26
|
+
<section class="card span-2">
|
|
27
|
+
<h2>Observable usage</h2>
|
|
28
|
+
<div class="gauge-row">
|
|
29
|
+
<div class="gauge">
|
|
30
|
+
<span class="gauge-value" id="total-tokens">—</span>
|
|
31
|
+
<span class="muted small">estimated total tokens</span>
|
|
32
|
+
</div>
|
|
33
|
+
<div class="bars" id="usage-bars"></div>
|
|
34
|
+
</div>
|
|
35
|
+
</section>
|
|
36
|
+
|
|
37
|
+
<section class="card">
|
|
38
|
+
<h2>Efficiency</h2>
|
|
39
|
+
<div class="score-block">
|
|
40
|
+
<span class="score" id="efficiency-score">—</span>
|
|
41
|
+
<span class="muted small">/ 100</span>
|
|
42
|
+
</div>
|
|
43
|
+
<div class="sparkline-wrap">
|
|
44
|
+
<svg
|
|
45
|
+
id="score-sparkline"
|
|
46
|
+
viewBox="0 0 200 48"
|
|
47
|
+
aria-hidden="true"
|
|
48
|
+
></svg>
|
|
49
|
+
</div>
|
|
50
|
+
</section>
|
|
51
|
+
|
|
52
|
+
<section class="card span-3">
|
|
53
|
+
<h2>Session timeline</h2>
|
|
54
|
+
<div class="timeline-meta muted small" id="timeline-meta"></div>
|
|
55
|
+
<div class="timeline-track" id="timeline-track"></div>
|
|
56
|
+
<div class="timeline-legend">
|
|
57
|
+
<span><i class="dot user"></i> user</span>
|
|
58
|
+
<span><i class="dot assistant"></i> assistant</span>
|
|
59
|
+
<span><i class="dot tool"></i> tool</span>
|
|
60
|
+
<span><i class="dot shell"></i> shell</span>
|
|
61
|
+
<span><i class="dot other"></i> other</span>
|
|
62
|
+
</div>
|
|
63
|
+
</section>
|
|
64
|
+
|
|
65
|
+
<section class="card">
|
|
66
|
+
<h2>Tool result sizes</h2>
|
|
67
|
+
<div class="bars vertical" id="tool-histogram"></div>
|
|
68
|
+
</section>
|
|
69
|
+
|
|
70
|
+
<section class="card span-2">
|
|
71
|
+
<h2>Context audit</h2>
|
|
72
|
+
<p class="muted small">Estimated tokens for always-on repo files</p>
|
|
73
|
+
<div class="bars vertical tall" id="context-bars"></div>
|
|
74
|
+
</section>
|
|
75
|
+
|
|
76
|
+
<section class="card">
|
|
77
|
+
<h2>Red flags</h2>
|
|
78
|
+
<ul class="flag-list" id="red-flags"></ul>
|
|
79
|
+
</section>
|
|
80
|
+
|
|
81
|
+
<section class="card span-2">
|
|
82
|
+
<h2>Recommendations</h2>
|
|
83
|
+
<ol class="rec-list" id="recommendations"></ol>
|
|
84
|
+
</section>
|
|
85
|
+
</main>
|
|
86
|
+
</div>
|
|
87
|
+
<script src="./app.js" defer></script>
|
|
88
|
+
</body>
|
|
89
|
+
</html>
|