atom-agent 0.3.0 → 1.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/CHANGELOG.md +82 -0
- package/README.md +83 -32
- package/dist/App.js +2178 -318
- package/dist/adapters.js +146 -15
- package/dist/agent/gates.js +153 -0
- package/dist/agent/loop-guard.js +184 -0
- package/dist/agent/loop.js +908 -0
- package/dist/agent/normalize.js +144 -0
- package/dist/agent/types.js +1 -0
- package/dist/auth.js +2 -1
- package/dist/cli.js +68 -6
- package/dist/compact.js +6 -48
- package/dist/config.js +171 -0
- package/dist/context-manager.js +564 -0
- package/dist/kilo.js +343 -0
- package/dist/local-discovery.js +308 -0
- package/dist/policy.js +286 -0
- package/dist/prompt-cache.js +99 -0
- package/dist/providers.js +183 -2
- package/dist/rollback.js +21 -0
- package/dist/scheduler.js +247 -0
- package/dist/session.js +35 -3
- package/dist/skills.js +214 -43
- package/dist/snapshots.js +57 -2
- package/dist/system.js +8 -1
- package/dist/telemetry-dashboard.js +589 -0
- package/dist/telemetry-server.js +301 -0
- package/dist/telemetry.js +1056 -0
- package/dist/tools/dir-cache.js +207 -0
- package/dist/tools/filesystem.js +149 -0
- package/dist/tools/fingerprints.js +33 -0
- package/dist/tools/overflow.js +76 -0
- package/dist/tools/read-cache.js +160 -0
- package/dist/tools/registry.js +802 -0
- package/dist/tools/search.js +242 -0
- package/dist/tools/shared.js +31 -0
- package/dist/tools/shell.js +273 -0
- package/dist/tools/todo.js +191 -0
- package/dist/tools/web.js +454 -0
- package/dist/tools.js +17 -1863
- package/dist/ui/activity.js +51 -0
- package/dist/ui/diff-panel.js +55 -0
- package/dist/ui/diff-view.js +112 -0
- package/dist/ui/diff.js +422 -0
- package/dist/ui/errors.js +129 -0
- package/dist/ui/highlight.js +120 -0
- package/dist/ui/input-model.js +115 -0
- package/dist/ui/input.js +40 -0
- package/dist/ui/live-tail.js +15 -0
- package/dist/ui/markdown.js +525 -0
- package/dist/ui/modals.js +47 -0
- package/dist/ui/palette.js +70 -0
- package/dist/ui/pickers.js +32 -0
- package/dist/ui/side-by-side.js +144 -0
- package/dist/ui/status-bar.js +75 -0
- package/dist/ui/theme.js +128 -0
- package/dist/ui/todo-panel.js +30 -0
- package/dist/ui/tool-inspector.js +59 -0
- package/dist/ui/transcript.js +128 -0
- package/dist/zen.js +145 -666
- package/package.json +1 -1
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
// Local observability dashboard renderer (pure: sessions in, HTML out).
|
|
2
|
+
//
|
|
3
|
+
// Produces a single self-contained HTML file (inline CSS + vanilla JS, no CDN,
|
|
4
|
+
// no external requests — it works over file:// with no network). Open it from
|
|
5
|
+
// disk; nothing is uploaded anywhere.
|
|
6
|
+
//
|
|
7
|
+
// Honesty rules enforced here, not just documented:
|
|
8
|
+
// - Every metric that can be absent renders as "n/a" with a title explaining
|
|
9
|
+
// why (e.g. "not reported by provider"). Zero is only shown when zero was
|
|
10
|
+
// measured (e.g. zero failed calls out of N recorded calls).
|
|
11
|
+
// - Cost is always n/a until a provider reports it (no pricing tables, no
|
|
12
|
+
// token × price synthesis — see TELEMETRY_COST_NOTE).
|
|
13
|
+
// - Tool rows never show token counts (tools don't consume model tokens);
|
|
14
|
+
// usage lives on model calls and turn/session aggregates, labeled as
|
|
15
|
+
// API-reported only.
|
|
16
|
+
// - An empty store renders an honest empty state, never placeholder charts.
|
|
17
|
+
//
|
|
18
|
+
// Drill-down: session → turn → iteration → model call / tool call → result
|
|
19
|
+
// preview. All dynamic strings are HTML-escaped.
|
|
20
|
+
import { mkdirSync, renameSync, writeFileSync } from "node:fs";
|
|
21
|
+
import * as path from "node:path";
|
|
22
|
+
import { TELEMETRY_COST_NOTE, loadTelemetrySessions, summarizeTelemetry, telemetryDashboardFilePath, telemetryDir, } from "./telemetry.js";
|
|
23
|
+
export function escapeHtml(value) {
|
|
24
|
+
const s = typeof value === "string" ? value : String(value ?? "");
|
|
25
|
+
return s
|
|
26
|
+
.replace(/&/g, "&")
|
|
27
|
+
.replace(/</g, "<")
|
|
28
|
+
.replace(/>/g, ">")
|
|
29
|
+
.replace(/"/g, """)
|
|
30
|
+
.replace(/'/g, "'");
|
|
31
|
+
}
|
|
32
|
+
function fmtCount(n) {
|
|
33
|
+
try {
|
|
34
|
+
return Math.floor(n).toLocaleString("en-US");
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return String(Math.floor(n));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function fmtMs(ms) {
|
|
41
|
+
if (ms === null || ms === undefined || !Number.isFinite(ms)) {
|
|
42
|
+
return `<span class="na" title="Not measured — no timed calls were recorded.">n/a</span>`;
|
|
43
|
+
}
|
|
44
|
+
if (ms < 1000)
|
|
45
|
+
return `${Math.round(ms)} ms`;
|
|
46
|
+
if (ms < 60000)
|
|
47
|
+
return `${(ms / 1000).toFixed(1)} s`;
|
|
48
|
+
return `${(ms / 60000).toFixed(1)} min`;
|
|
49
|
+
}
|
|
50
|
+
function fmtTokens(value, reported, label) {
|
|
51
|
+
if (!reported || value === undefined) {
|
|
52
|
+
return `<span class="na" title="${escapeHtml(label)}">n/a</span>`;
|
|
53
|
+
}
|
|
54
|
+
return fmtCount(value);
|
|
55
|
+
}
|
|
56
|
+
function usageCell(usage, reported) {
|
|
57
|
+
if (!reported) {
|
|
58
|
+
return `<span class="na" title="No model call in scope reported token usage. The provider did not send a usage payload.">n/a</span>`;
|
|
59
|
+
}
|
|
60
|
+
const parts = [];
|
|
61
|
+
if (usage.prompt_tokens !== undefined)
|
|
62
|
+
parts.push(`in ${fmtCount(usage.prompt_tokens)}`);
|
|
63
|
+
if (usage.completion_tokens !== undefined)
|
|
64
|
+
parts.push(`out ${fmtCount(usage.completion_tokens)}`);
|
|
65
|
+
if (usage.cacheReadTokens !== undefined)
|
|
66
|
+
parts.push(`cache-read ${fmtCount(usage.cacheReadTokens)}`);
|
|
67
|
+
if (usage.cacheWriteTokens !== undefined)
|
|
68
|
+
parts.push(`cache-write ${fmtCount(usage.cacheWriteTokens)}`);
|
|
69
|
+
if (usage.total_tokens !== undefined)
|
|
70
|
+
parts.push(`total ${fmtCount(usage.total_tokens)}`);
|
|
71
|
+
return escapeHtml(parts.length > 0 ? parts.join(" · ") : "reported (empty)");
|
|
72
|
+
}
|
|
73
|
+
function sessionTokens(s) {
|
|
74
|
+
const usage = {};
|
|
75
|
+
let reported = false;
|
|
76
|
+
for (const t of s.turns) {
|
|
77
|
+
if (!t.usageReported)
|
|
78
|
+
continue;
|
|
79
|
+
reported = true;
|
|
80
|
+
["prompt_tokens", "completion_tokens", "total_tokens", "cacheReadTokens", "cacheWriteTokens"].forEach((k) => {
|
|
81
|
+
if (t.usage[k] !== undefined)
|
|
82
|
+
usage[k] = (usage[k] ?? 0) + t.usage[k];
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return { usage, reported };
|
|
86
|
+
}
|
|
87
|
+
function turnTokensTotal(t) {
|
|
88
|
+
if (!t.usageReported)
|
|
89
|
+
return null;
|
|
90
|
+
if (t.usage.total_tokens !== undefined)
|
|
91
|
+
return t.usage.total_tokens;
|
|
92
|
+
const sum = (t.usage.prompt_tokens ?? 0) + (t.usage.completion_tokens ?? 0);
|
|
93
|
+
return sum > 0 || t.usage.prompt_tokens !== undefined || t.usage.completion_tokens !== undefined ? sum : null;
|
|
94
|
+
}
|
|
95
|
+
function shortId(id) {
|
|
96
|
+
return id.length > 13 ? `${id.slice(0, 8)}…${id.slice(-4)}` : id;
|
|
97
|
+
}
|
|
98
|
+
function fmtTime(iso) {
|
|
99
|
+
if (!iso)
|
|
100
|
+
return `<span class="na">n/a</span>`;
|
|
101
|
+
try {
|
|
102
|
+
const d = new Date(iso);
|
|
103
|
+
if (Number.isNaN(d.getTime()))
|
|
104
|
+
return escapeHtml(iso);
|
|
105
|
+
return escapeHtml(d.toLocaleString());
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return escapeHtml(iso);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// --- SVG charts (measured data only; empty states handled by callers) ---
|
|
112
|
+
function barRow(label, value, max, extra, okWidth, failWidth) {
|
|
113
|
+
const pct = max > 0 ? Math.max(2, Math.round((value / max) * 100)) : 0;
|
|
114
|
+
const ok = okWidth !== undefined && failWidth !== undefined;
|
|
115
|
+
const bar = ok
|
|
116
|
+
? `<span class="bar"><span class="seg-ok" style="width:${okWidth}%"></span><span class="seg-fail" style="width:${failWidth}%"></span></span>`
|
|
117
|
+
: `<span class="bar"><span class="fill" style="width:${pct}%"></span></span>`;
|
|
118
|
+
return `<div class="brow"><span class="blabel" title="${escapeHtml(label)}">${escapeHtml(label)}</span>${bar}<span class="bval">${extra}</span></div>`;
|
|
119
|
+
}
|
|
120
|
+
function toolChart(agg) {
|
|
121
|
+
if (agg.byTool.length === 0) {
|
|
122
|
+
return `<p class="empty">No tool calls recorded yet.</p>`;
|
|
123
|
+
}
|
|
124
|
+
const max = Math.max(...agg.byTool.map((t) => t.calls));
|
|
125
|
+
const rows = agg.byTool.slice(0, 15).map((t) => {
|
|
126
|
+
const okPct = t.calls > 0 ? Math.round((t.succeeded / t.calls) * 100) : 0;
|
|
127
|
+
const failPct = t.calls > 0 ? Math.max(t.failed > 0 ? 2 : 0, Math.round((t.failed / t.calls) * 100)) : 0;
|
|
128
|
+
return barRow(t.name, t.calls, max, `${fmtCount(t.calls)} · ok ${fmtCount(t.succeeded)} · fail ${fmtCount(t.failed)} · avg ${fmtMs(t.avgDurationMs)}`, okPct, failPct);
|
|
129
|
+
});
|
|
130
|
+
const more = agg.byTool.length > 15 ? `<p class="empty">+ ${agg.byTool.length - 15} more tool(s) in the table below.</p>` : "";
|
|
131
|
+
return `<div class="bars legend-ok-fail">${rows.join("")}</div>${more}
|
|
132
|
+
<p class="legend"><span class="sw-ok"></span> succeeded <span class="sw-fail"></span> failed</p>`;
|
|
133
|
+
}
|
|
134
|
+
function tokensChart(sessions) {
|
|
135
|
+
const withUsage = sessions
|
|
136
|
+
.map((s) => ({ s, total: turnTokensTotal({ usage: sessionTokens(s).usage, usageReported: sessionTokens(s).reported }) }))
|
|
137
|
+
.filter((e) => e.total !== null);
|
|
138
|
+
if (withUsage.length === 0) {
|
|
139
|
+
return `<p class="empty">No token usage reported yet — providers send usage per model call; nothing is estimated.</p>`;
|
|
140
|
+
}
|
|
141
|
+
const top = [...withUsage].sort((a, b) => b.total - a.total).slice(0, 20);
|
|
142
|
+
const max = Math.max(...top.map((e) => e.total));
|
|
143
|
+
const rows = top.map((e) => barRow(`${shortId(e.s.sessionId)} · ${e.s.model}`, e.total, max, fmtCount(e.total)));
|
|
144
|
+
const skipped = withUsage.length > 20 ? `<p class="empty">Top 20 of ${withUsage.length} sessions with reported usage.</p>` : "";
|
|
145
|
+
const unreported = sessions.length - withUsage.length;
|
|
146
|
+
const note = unreported > 0
|
|
147
|
+
? `<p class="empty">${unreported} session(s) reported no usage and are excluded (not shown as zero).</p>`
|
|
148
|
+
: "";
|
|
149
|
+
return `<div class="bars">${rows.join("")}</div>${skipped}${note}`;
|
|
150
|
+
}
|
|
151
|
+
function outcomesChart(agg) {
|
|
152
|
+
const total = agg.turns;
|
|
153
|
+
if (total === 0)
|
|
154
|
+
return `<p class="empty">No turns recorded yet.</p>`;
|
|
155
|
+
const order = [
|
|
156
|
+
["completed", agg.outcomes.completed, "seg-ok"],
|
|
157
|
+
["blocked", agg.outcomes.blocked, "seg-warn"],
|
|
158
|
+
["unverified", agg.outcomes.unverified, "seg-warn"],
|
|
159
|
+
["budget-exceeded", agg.outcomes["budget-exceeded"], "seg-warn"],
|
|
160
|
+
["failed", agg.outcomes.failed, "seg-fail"],
|
|
161
|
+
["cancelled", agg.outcomes.cancelled, "seg-mute"],
|
|
162
|
+
["pending", agg.outcomes.pending, "seg-mute"],
|
|
163
|
+
];
|
|
164
|
+
const segs = order
|
|
165
|
+
.filter(([, n]) => n > 0)
|
|
166
|
+
.map(([label, n, cls]) => {
|
|
167
|
+
const pct = Math.max(n > 0 ? 3 : 0, Math.round((n / total) * 100));
|
|
168
|
+
return `<span class="${cls}" style="width:${pct}%" title="${escapeHtml(label)}: ${n}"></span>`;
|
|
169
|
+
})
|
|
170
|
+
.join("");
|
|
171
|
+
const legend = order
|
|
172
|
+
.filter(([, n]) => n > 0)
|
|
173
|
+
.map(([label, n]) => `<span class="pill">${escapeHtml(label)} ${n}</span>`)
|
|
174
|
+
.join(" ");
|
|
175
|
+
return `<div class="stacked">${segs}</div><p class="legend">${legend}</p>`;
|
|
176
|
+
}
|
|
177
|
+
function latencyChart(agg) {
|
|
178
|
+
const rows = agg.byTool.filter((t) => t.avgDurationMs !== null).sort((a, b) => b.avgDurationMs - a.avgDurationMs).slice(0, 12);
|
|
179
|
+
if (rows.length === 0)
|
|
180
|
+
return `<p class="empty">No timed tool calls recorded yet.</p>`;
|
|
181
|
+
const max = Math.max(...rows.map((t) => t.avgDurationMs));
|
|
182
|
+
return `<div class="bars">${rows
|
|
183
|
+
.map((t) => barRow(t.name, t.avgDurationMs, max, `avg ${fmtMs(t.avgDurationMs)} · ${fmtCount(t.calls)} call(s)`))
|
|
184
|
+
.join("")}</div>`;
|
|
185
|
+
}
|
|
186
|
+
// --- Session detail ---
|
|
187
|
+
function modelCallRows(turnId, s) {
|
|
188
|
+
if (s.modelCalls.length === 0)
|
|
189
|
+
return `<p class="empty">No model calls recorded for this turn.</p>`;
|
|
190
|
+
const rows = s.modelCalls.map((m) => {
|
|
191
|
+
const retries = m.retries.length > 0
|
|
192
|
+
? `<ul class="retries">${m.retries
|
|
193
|
+
.map((r) => `<li>retry${r.attempt !== null ? ` #${r.attempt}` : ""}${r.status !== null ? ` · HTTP ${r.status}` : ""}${r.delayMs !== null ? ` · after ${r.delayMs} ms` : ""} · <span class="mono">${escapeHtml(r.detail)}</span></li>`)
|
|
194
|
+
.join("")}</ul>`
|
|
195
|
+
: `<span class="na" title="No transport retries preceded this call.">none</span>`;
|
|
196
|
+
return `<tr>
|
|
197
|
+
<td class="mono">${escapeHtml(m.id)}</td>
|
|
198
|
+
<td>iter ${m.iteration + 1}</td>
|
|
199
|
+
<td>${escapeHtml(m.provider)} · ${escapeHtml(m.model)}</td>
|
|
200
|
+
<td>${fmtMs(m.durationMs)}</td>
|
|
201
|
+
<td>${usageCell(m.usage ?? {}, m.usageReported)}</td>
|
|
202
|
+
<td>${m.reasoningLabel ? escapeHtml(m.reasoningLabel) : `<span class="na" title="The response carried no reasoning metadata.">n/a</span>`}</td>
|
|
203
|
+
<td>${m.toolCallCount}</td>
|
|
204
|
+
<td>${escapeHtml(m.finishReason)}${m.error ? `<br><span class="err">${escapeHtml(m.error)}</span>` : ""}</td>
|
|
205
|
+
<td>${retries}</td>
|
|
206
|
+
</tr>`;
|
|
207
|
+
});
|
|
208
|
+
void turnId;
|
|
209
|
+
return `<table class="detail"><thead><tr><th>Call</th><th>Iteration</th><th>Provider · model</th><th>Latency</th><th>Tokens (reported)</th><th>Reasoning</th><th>Tool calls</th><th>Finish</th><th>Retries</th></tr></thead><tbody>${rows.join("")}</tbody></table>`;
|
|
210
|
+
}
|
|
211
|
+
function toolCallRows(s) {
|
|
212
|
+
if (s.toolCalls.length === 0)
|
|
213
|
+
return `<p class="empty">No tool calls in this turn (final answer with no tool use).</p>`;
|
|
214
|
+
const rows = s.toolCalls.map((c) => {
|
|
215
|
+
const status = c.success
|
|
216
|
+
? `<span class="ok">ok</span>`
|
|
217
|
+
: `<span class="err">fail · ${escapeHtml(c.errorKind ?? "tool-error")}</span>`;
|
|
218
|
+
const batch = c.batchSize > 1 ? `<br><span class="mute">batch ${c.batchIndex + 1}/${c.batchSize}</span>` : "";
|
|
219
|
+
const argsNote = c.argsTruncated ? `<br><span class="mute">truncated · ${fmtCount(c.argsChars)} chars total</span>` : "";
|
|
220
|
+
const resultNote = c.resultTruncated
|
|
221
|
+
? `<br><span class="mute">truncated · ${fmtCount(c.resultChars)} chars total</span>`
|
|
222
|
+
: `<br><span class="mute">${fmtCount(c.resultChars)} chars</span>`;
|
|
223
|
+
return `<tr>
|
|
224
|
+
<td class="mono">${escapeHtml(c.id)}</td>
|
|
225
|
+
<td>iter ${c.iteration + 1}</td>
|
|
226
|
+
<td><span class="mono">${escapeHtml(c.name)}</span>${batch}<br>${status}</td>
|
|
227
|
+
<td>${fmtMs(c.durationMs)}</td>
|
|
228
|
+
<td><span class="na" title="Tools do not consume model tokens. Token usage is reported per model call above — never attributed to tools.">n/a</span></td>
|
|
229
|
+
<td><details><summary>args</summary><pre>${escapeHtml(c.argsPreview)}</pre>${argsNote}</details></td>
|
|
230
|
+
<td><details><summary>result</summary><pre>${escapeHtml(c.resultPreview)}</pre>${resultNote}</details></td>
|
|
231
|
+
</tr>`;
|
|
232
|
+
});
|
|
233
|
+
return `<table class="detail"><thead><tr><th>Call</th><th>Iteration</th><th>Tool · status</th><th>Duration</th><th>Tokens</th><th>Args</th><th>Result</th></tr></thead><tbody>${rows.join("")}</tbody></table>`;
|
|
234
|
+
}
|
|
235
|
+
function iterationTimeline(s) {
|
|
236
|
+
if (s.iterations.length === 0)
|
|
237
|
+
return `<p class="empty">No iterations recorded.</p>`;
|
|
238
|
+
const max = Math.max(1, ...s.iterations.map((i) => i.durationMs));
|
|
239
|
+
const rows = s.iterations.map((i) => {
|
|
240
|
+
const pct = Math.max(3, Math.round((i.durationMs / max) * 100));
|
|
241
|
+
const tools = i.toolCallIds.length > 0 ? ` · ${i.toolCallIds.length} tool(s)` : " · final";
|
|
242
|
+
return `<div class="trow"><span class="tlabel">iter ${i.step + 1}</span><span class="tbar"><span class="tfill" style="width:${pct}%"></span></span><span class="tval">${fmtMs(i.durationMs)}${escapeHtml(tools)}</span></div>`;
|
|
243
|
+
});
|
|
244
|
+
return `<div class="timeline">${rows.join("")}</div>`;
|
|
245
|
+
}
|
|
246
|
+
function turnBlock(t) {
|
|
247
|
+
const outcomeCls = t.outcome === "completed" ? "ok" : t.outcome === "failed" ? "err" : "warn";
|
|
248
|
+
const total = turnTokensTotal(t);
|
|
249
|
+
// Loop-harness rollup (present only when the loop reported stats for this
|
|
250
|
+
// turn — older sessions render exactly as before).
|
|
251
|
+
const loopBits = [];
|
|
252
|
+
if (t.loop) {
|
|
253
|
+
if (t.loop.cacheHits > 0)
|
|
254
|
+
loopBits.push(`read-cache hits ${fmtCount(t.loop.cacheHits)}`);
|
|
255
|
+
if (t.loop.repetitionHits > 0)
|
|
256
|
+
loopBits.push(`loop-guard hits ${fmtCount(t.loop.repetitionHits)}`);
|
|
257
|
+
if (t.loop.bottleneckName) {
|
|
258
|
+
loopBits.push(`bottleneck ${escapeHtml(t.loop.bottleneckName)}` +
|
|
259
|
+
(t.loop.bottleneckMs !== undefined ? ` ${fmtMs(t.loop.bottleneckMs)}` : ""));
|
|
260
|
+
}
|
|
261
|
+
if (t.loop.truncations > 0)
|
|
262
|
+
loopBits.push(`truncated ${fmtCount(t.loop.truncations)} turn(s)`);
|
|
263
|
+
}
|
|
264
|
+
const head = `<span class="${outcomeCls}">${escapeHtml(t.outcome)}</span>` +
|
|
265
|
+
` · ${fmtMs(t.durationMs)}` +
|
|
266
|
+
` · ${t.modelCalls.length} model call(s)` +
|
|
267
|
+
` · ${t.toolCalls.length} tool call(s)` +
|
|
268
|
+
` · retries ${t.retryCount}` +
|
|
269
|
+
` · tokens ${total !== null ? fmtCount(total) : `<span class="na" title="No model call in this turn reported usage.">n/a</span>`}` +
|
|
270
|
+
(loopBits.length > 0 ? ` · ${loopBits.join(" · ")}` : "");
|
|
271
|
+
return `<details class="turn" data-outcome="${escapeHtml(t.outcome)}">
|
|
272
|
+
<summary><span class="mono">${escapeHtml(t.id)}</span> · ${fmtTime(t.startedAt)} · ${escapeHtml(t.provider)} · ${escapeHtml(t.model)} · ${head}</summary>
|
|
273
|
+
<div class="turnbody">
|
|
274
|
+
<p><strong>Input:</strong> <span class="mono">${escapeHtml(t.inputPreview)}</span> <span class="mute">(${fmtCount(t.inputChars)} chars)</span></p>
|
|
275
|
+
${t.error ? `<p><strong>Error:</strong> <span class="err">${escapeHtml(t.error)}</span></p>` : ""}
|
|
276
|
+
${t.replyPreview ? `<p><strong>Reply:</strong> <span class="mono">${escapeHtml(t.replyPreview)}</span></p>` : ""}
|
|
277
|
+
<h5>Timeline</h5>
|
|
278
|
+
${iterationTimeline(t)}
|
|
279
|
+
<h5>Model calls</h5>
|
|
280
|
+
${modelCallRows(t.id, t)}
|
|
281
|
+
<h5>Tool calls</h5>
|
|
282
|
+
${toolCallRows(t)}
|
|
283
|
+
</div>
|
|
284
|
+
</details>`;
|
|
285
|
+
}
|
|
286
|
+
function sessionBlock(s) {
|
|
287
|
+
const { usage, reported } = sessionTokens(s);
|
|
288
|
+
const total = turnTokensTotal({ usage, usageReported: reported });
|
|
289
|
+
const ok = s.turns.reduce((n, t) => n + t.toolCalls.filter((c) => c.success).length, 0);
|
|
290
|
+
const tools = s.turns.reduce((n, t) => n + t.toolCalls.length, 0);
|
|
291
|
+
const outcomes = s.turns.map((t) => t.outcome).join(" ");
|
|
292
|
+
const subagents = s.subagents.length === 0
|
|
293
|
+
? `<p class="empty">No subagents recorded — this session ran the single-agent loop (depth 1). Delegated workers will appear here when a delegate tool is used.</p>`
|
|
294
|
+
: `<ul>${s.subagents
|
|
295
|
+
.map((sub) => `<li><span class="mono">${escapeHtml(sub.name)}</span> · ${escapeHtml(sub.status)} · ${fmtMs(sub.durationMs)}${sub.summaryPreview ? ` · <span class="mono">${escapeHtml(sub.summaryPreview)}</span>` : ""}</li>`)
|
|
296
|
+
.join("")}</ul>`;
|
|
297
|
+
const events = s.events.length === 0
|
|
298
|
+
? `<p class="empty">No session events.</p>`
|
|
299
|
+
: `<ul>${s.events.map((e) => `<li>${fmtTime(e.at)} · <span class="mono">${escapeHtml(e.kind)}</span> · ${escapeHtml(e.detail)}</li>`).join("")}</ul>`;
|
|
300
|
+
const turns = s.turns.length === 0
|
|
301
|
+
? `<p class="empty">No turns recorded in this session yet.</p>`
|
|
302
|
+
: s.turns.map((t) => turnBlock(t)).join("");
|
|
303
|
+
return `<details class="session" data-session data-provider="${escapeHtml(s.provider)}" data-model="${escapeHtml(s.model)}" data-outcomes="${escapeHtml(outcomes)}" data-search="${escapeHtml(`${s.sessionId} ${s.provider} ${s.model} ${s.project ?? ""}`.toLowerCase())}">
|
|
304
|
+
<summary>
|
|
305
|
+
<span class="mono">${escapeHtml(shortId(s.sessionId))}</span>
|
|
306
|
+
<span class="mute">${fmtTime(s.startedAt)}</span>
|
|
307
|
+
<span class="pill">${escapeHtml(s.provider)}</span>
|
|
308
|
+
<span class="pill">${escapeHtml(s.model)}</span>
|
|
309
|
+
<span>${s.turns.length} turn(s)</span>
|
|
310
|
+
<span>tokens ${total !== null ? fmtCount(total) : `<span class="na" title="No turn in this session reported usage.">n/a</span>`}</span>
|
|
311
|
+
<span>tools ${ok}/${tools}</span>
|
|
312
|
+
</summary>
|
|
313
|
+
<div class="sessbody">
|
|
314
|
+
<table class="meta"><tbody>
|
|
315
|
+
<tr><th>Session</th><td class="mono">${escapeHtml(s.sessionId)}</td></tr>
|
|
316
|
+
<tr><th>Started / ended</th><td>${fmtTime(s.startedAt)} → ${s.endedAt ? fmtTime(s.endedAt) : "running"}</td></tr>
|
|
317
|
+
<tr><th>Project</th><td>${s.project ? escapeHtml(s.project) : `<span class="na">n/a</span>`}</td></tr>
|
|
318
|
+
<tr><th>Compaction spend</th><td>${usageCell(s.compactionUsage, s.compactionReported)} <span class="mute">(summary POSTs, kept separate from turn usage)</span></td></tr>
|
|
319
|
+
</tbody></table>
|
|
320
|
+
<h4>Subagents</h4>
|
|
321
|
+
${subagents}
|
|
322
|
+
<h4>Events</h4>
|
|
323
|
+
${events}
|
|
324
|
+
<h4>Turns</h4>
|
|
325
|
+
${turns}
|
|
326
|
+
</div>
|
|
327
|
+
</details>`;
|
|
328
|
+
}
|
|
329
|
+
// --- Page ---
|
|
330
|
+
// Render the dashboard for every stored session and write it atomically next
|
|
331
|
+
// to the store (telemetry/dashboard.html). Best-effort: returns the written
|
|
332
|
+
// path, or null (never throws) when the store is unreadable or the disk fails.
|
|
333
|
+
export function writeTelemetryDashboard(home, opts = {}) {
|
|
334
|
+
try {
|
|
335
|
+
const { sessions, corrupt } = loadTelemetrySessions(home);
|
|
336
|
+
const html = buildDashboardHtml(sessions, {
|
|
337
|
+
...opts,
|
|
338
|
+
sourceDir: telemetryDir(home),
|
|
339
|
+
corruptFiles: corrupt,
|
|
340
|
+
});
|
|
341
|
+
const dir = telemetryDir(home);
|
|
342
|
+
mkdirSync(dir, { recursive: true });
|
|
343
|
+
const finalPath = telemetryDashboardFilePath(home);
|
|
344
|
+
const tmpPath = path.join(dir, `.dashboard.tmp.${process.pid}`);
|
|
345
|
+
writeFileSync(tmpPath, html, "utf8");
|
|
346
|
+
renameSync(tmpPath, finalPath);
|
|
347
|
+
return finalPath;
|
|
348
|
+
}
|
|
349
|
+
catch {
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
export function buildDashboardHtml(sessions, opts = {}) {
|
|
354
|
+
const agg = summarizeTelemetry(sessions);
|
|
355
|
+
const generatedAt = opts.generatedAt ?? new Date().toISOString();
|
|
356
|
+
const providers = [...new Set(sessions.flatMap((s) => s.turns.map((t) => t.provider)).concat(sessions.map((s) => s.provider)))].sort();
|
|
357
|
+
const providerOptions = providers.map((p) => `<option value="${escapeHtml(p)}">${escapeHtml(p)}</option>`).join("");
|
|
358
|
+
const corruptNote = opts.corruptFiles && opts.corruptFiles > 0
|
|
359
|
+
? `<p class="warn">${opts.corruptFiles} corrupt session file(s) were skipped (never crash the dashboard — see storage notes).</p>`
|
|
360
|
+
: "";
|
|
361
|
+
const successRate = agg.toolSuccessRate !== null
|
|
362
|
+
? `${(agg.toolSuccessRate * 100).toFixed(1)}%`
|
|
363
|
+
: `<span class="na" title="No tool calls recorded — a rate over zero calls would be fake.">n/a</span>`;
|
|
364
|
+
const refreshSeconds = typeof opts.refreshSeconds === "number" && Number.isFinite(opts.refreshSeconds)
|
|
365
|
+
? Math.floor(opts.refreshSeconds)
|
|
366
|
+
: 0;
|
|
367
|
+
const refreshMeta = refreshSeconds > 0 ? `\n<meta http-equiv="refresh" content="${refreshSeconds}">` : "";
|
|
368
|
+
const livePill = refreshSeconds > 0
|
|
369
|
+
? ` · <span class="pill">live · refreshes every ${refreshSeconds}s</span>`
|
|
370
|
+
: "";
|
|
371
|
+
return `<!DOCTYPE html>
|
|
372
|
+
<html lang="en">
|
|
373
|
+
<head>
|
|
374
|
+
<meta charset="utf-8">
|
|
375
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">${refreshMeta}
|
|
376
|
+
<title>ATOM Observability</title>
|
|
377
|
+
<style>
|
|
378
|
+
:root {
|
|
379
|
+
color-scheme: dark;
|
|
380
|
+
--bg: #0b0e14; --bg-soft: #0e131b; --panel: #131926; --panel-2: #0f1520;
|
|
381
|
+
--line: #263042; --line-soft: #1c2433;
|
|
382
|
+
--txt: #e8eef7; --mute: #93a0b4; --faint: #5f6b80;
|
|
383
|
+
--ok: #3fb950; --fail: #f85149; --warn: #d29922; --acc: #58a6ff; --acc-soft: rgba(88, 166, 255, 0.12);
|
|
384
|
+
--radius: 12px; --shadow: 0 8px 28px rgba(0, 0, 0, 0.35);
|
|
385
|
+
}
|
|
386
|
+
* { box-sizing: border-box; }
|
|
387
|
+
html { scroll-behavior: smooth; scroll-padding-top: 76px; }
|
|
388
|
+
body {
|
|
389
|
+
background:
|
|
390
|
+
radial-gradient(1100px 320px at 15% -80px, rgba(88, 166, 255, 0.10), transparent 60%),
|
|
391
|
+
radial-gradient(900px 300px at 90% -60px, rgba(63, 185, 80, 0.07), transparent 60%),
|
|
392
|
+
var(--bg);
|
|
393
|
+
background-attachment: fixed;
|
|
394
|
+
color: var(--txt);
|
|
395
|
+
font: 14px/1.6 -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, Roboto, sans-serif;
|
|
396
|
+
margin: 0 auto; max-width: 1120px; padding: 28px 24px 40px;
|
|
397
|
+
-webkit-font-smoothing: antialiased;
|
|
398
|
+
}
|
|
399
|
+
::selection { background: rgba(88, 166, 255, 0.35); }
|
|
400
|
+
a { color: var(--acc); }
|
|
401
|
+
:focus-visible { outline: 2px solid var(--acc); outline-offset: 2px; border-radius: 4px; }
|
|
402
|
+
/* Hero */
|
|
403
|
+
.hero { padding: 10px 0 4px; }
|
|
404
|
+
.kicker { display: inline-flex; align-items: center; gap: 8px; font-size: 11px; font-weight: 700; letter-spacing: 0.14em; text-transform: uppercase; color: var(--acc); background: var(--acc-soft); border: 1px solid rgba(88, 166, 255, 0.35); border-radius: 999px; padding: 3px 12px; margin-bottom: 12px; }
|
|
405
|
+
.kicker .dot { width: 7px; height: 7px; border-radius: 50%; background: var(--ok); box-shadow: 0 0 8px var(--ok); }
|
|
406
|
+
h1 { font-size: 30px; line-height: 1.2; letter-spacing: -0.02em; margin: 0 0 6px; }
|
|
407
|
+
h2 { font-size: 15px; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; color: var(--mute); border-bottom: 1px solid var(--line-soft); padding-bottom: 8px; margin: 36px 0 4px; }
|
|
408
|
+
h2 .hcount { color: var(--faint); font-weight: 600; }
|
|
409
|
+
h4 { margin: 18px 0 6px; font-size: 14px; } h5 { margin: 14px 0 6px; color: var(--mute); font-size: 12px; text-transform: uppercase; letter-spacing: 0.08em; }
|
|
410
|
+
/* Sticky section nav */
|
|
411
|
+
.toc { position: sticky; top: 0; z-index: 20; display: flex; gap: 4px; flex-wrap: wrap; margin: 14px -24px 0; padding: 10px 24px; background: rgba(11, 14, 20, 0.86); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); border-bottom: 1px solid var(--line-soft); }
|
|
412
|
+
.toc a { color: var(--mute); text-decoration: none; font-size: 12.5px; font-weight: 600; padding: 5px 12px; border-radius: 999px; border: 1px solid transparent; }
|
|
413
|
+
.toc a:hover { color: var(--txt); background: rgba(255, 255, 255, 0.05); border-color: var(--line); }
|
|
414
|
+
.sub, .mute { color: var(--mute); } .mono, pre, code { font-family: ui-monospace, SFMono-Regular, "Cascadia Mono", Consolas, monospace; font-size: 0.92em; }
|
|
415
|
+
pre { background: #080b11; border: 1px solid var(--line-soft); border-radius: 8px; padding: 10px 12px; overflow: auto; max-height: 300px; white-space: pre-wrap; word-break: break-word; }
|
|
416
|
+
.na { color: var(--mute); font-style: italic; border-bottom: 1px dotted var(--mute); }
|
|
417
|
+
.ok { color: var(--ok); font-weight: 600; } .err { color: var(--fail); } .warn { color: var(--warn); }
|
|
418
|
+
/* Overview cards */
|
|
419
|
+
.cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(168px, 1fr)); gap: 12px; margin: 16px 0; }
|
|
420
|
+
.card { position: relative; background: linear-gradient(180deg, rgba(255,255,255,0.025), transparent 40%), var(--panel); border: 1px solid var(--line-soft); border-radius: var(--radius); padding: 12px 14px; box-shadow: var(--shadow); transition: transform 120ms ease, border-color 120ms ease; overflow: hidden; }
|
|
421
|
+
.card::before { content: ""; position: absolute; inset: 0 0 auto 0; height: 2px; background: linear-gradient(90deg, var(--acc), transparent 70%); opacity: 0.7; }
|
|
422
|
+
.card:hover { transform: translateY(-2px); border-color: var(--line); }
|
|
423
|
+
.card .k { color: var(--mute); font-size: 11px; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; }
|
|
424
|
+
.card .v { font-size: 22px; font-weight: 700; letter-spacing: -0.01em; font-variant-numeric: tabular-nums; margin-top: 2px; }
|
|
425
|
+
.grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; } @media (max-width: 800px) { .grid2 { grid-template-columns: 1fr; } }
|
|
426
|
+
.panel { background: var(--panel); border: 1px solid var(--line-soft); border-radius: var(--radius); padding: 14px 16px; box-shadow: var(--shadow); }
|
|
427
|
+
/* Bar rows */
|
|
428
|
+
.brow { display: flex; align-items: center; gap: 10px; margin: 6px 0; }
|
|
429
|
+
.blabel { flex: 0 0 auto; width: min(220px, 28vw); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; }
|
|
430
|
+
.bar { flex: 1 1 auto; min-width: 60px; background: #080b11; border: 1px solid var(--line-soft); border-radius: 999px; height: 14px; display: flex; overflow: hidden; }
|
|
431
|
+
.fill { background: linear-gradient(90deg, #2f7de1, var(--acc)); display: block; height: 100%; border-radius: 999px; }
|
|
432
|
+
.seg-ok { background: linear-gradient(90deg, #2ea043, var(--ok)); display: block; height: 100%; } .seg-fail { background: linear-gradient(90deg, #da3633, var(--fail)); display: block; height: 100%; }
|
|
433
|
+
.seg-warn { background: linear-gradient(90deg, #bb8009, var(--warn)); display: block; height: 100%; } .seg-mute { background: var(--faint); display: block; height: 100%; }
|
|
434
|
+
.bval { flex: 0 1 300px; min-width: 120px; color: var(--mute); font-size: 12px; font-variant-numeric: tabular-nums; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-align: right; }
|
|
435
|
+
.stacked { display: flex; height: 20px; border-radius: 999px; overflow: hidden; background: #080b11; border: 1px solid var(--line-soft); margin: 10px 0; }
|
|
436
|
+
.pill { background: rgba(255,255,255,0.04); border: 1px solid var(--line); border-radius: 999px; padding: 1px 10px; font-size: 12px; margin: 2px; display: inline-block; font-variant-numeric: tabular-nums; }
|
|
437
|
+
.legend { color: var(--mute); font-size: 12px; } .sw-ok, .sw-fail { display: inline-block; width: 10px; height: 10px; border-radius: 3px; } .sw-ok { background: var(--ok); box-shadow: 0 0 6px rgba(63,185,80,0.6); } .sw-fail { background: var(--fail); box-shadow: 0 0 6px rgba(248,81,73,0.6); }
|
|
438
|
+
.empty { color: var(--mute); font-style: italic; background: rgba(255,255,255,0.015); border: 1px dashed var(--line); border-radius: 8px; padding: 10px 12px; }
|
|
439
|
+
/* Tables */
|
|
440
|
+
table { border-collapse: collapse; width: 100%; margin: 8px 0; font-size: 13px; }
|
|
441
|
+
th, td { border-bottom: 1px solid var(--line-soft); padding: 8px 10px; text-align: left; vertical-align: top; }
|
|
442
|
+
thead th { background: rgba(255,255,255,0.03); color: var(--mute); font-size: 11px; text-transform: uppercase; letter-spacing: 0.07em; border-bottom: 1px solid var(--line); position: sticky; top: 0; z-index: 1; }
|
|
443
|
+
tbody tr:hover td { background: rgba(88, 166, 255, 0.05); }
|
|
444
|
+
td { font-variant-numeric: tabular-nums; }
|
|
445
|
+
table.meta { width: auto; }
|
|
446
|
+
table.meta th { border: 1px solid var(--line-soft); }
|
|
447
|
+
table.meta td { border: 1px solid var(--line-soft); }
|
|
448
|
+
/* Drill-down */
|
|
449
|
+
details { background: var(--panel); border: 1px solid var(--line-soft); border-radius: var(--radius); margin: 10px 0; box-shadow: var(--shadow); transition: border-color 120ms ease; }
|
|
450
|
+
details:hover { border-color: var(--line); }
|
|
451
|
+
details[open] { border-color: rgba(88, 166, 255, 0.4); }
|
|
452
|
+
details summary { cursor: pointer; padding: 12px 14px; display: flex; gap: 10px; flex-wrap: wrap; align-items: center; list-style: none; border-radius: var(--radius); }
|
|
453
|
+
details summary::-webkit-details-marker { display: none; }
|
|
454
|
+
details summary::before { content: "▸"; color: var(--acc); font-weight: 700; transition: transform 120ms ease; }
|
|
455
|
+
details[open] > summary::before { transform: rotate(90deg); }
|
|
456
|
+
details .sessbody, details .turnbody { padding: 2px 14px 14px; border-top: 1px solid var(--line-soft); }
|
|
457
|
+
details.turn { background: var(--panel-2); }
|
|
458
|
+
.turnbody { overflow-x: auto; }
|
|
459
|
+
/* Filters */
|
|
460
|
+
.filters { display: flex; gap: 10px; flex-wrap: wrap; margin: 14px 0; align-items: center; }
|
|
461
|
+
.filters input, .filters select { background: #080b11; color: var(--txt); border: 1px solid var(--line); border-radius: 8px; padding: 7px 10px; font: inherit; }
|
|
462
|
+
.filters input:focus, .filters select:focus { border-color: var(--acc); outline: none; box-shadow: 0 0 0 3px var(--acc-soft); }
|
|
463
|
+
.filters input[type="search"] { flex: 1 1 220px; }
|
|
464
|
+
/* Timelines */
|
|
465
|
+
.trow { display: flex; align-items: center; gap: 10px; margin: 4px 0; }
|
|
466
|
+
.tlabel { flex: 0 0 64px; color: var(--mute); font-variant-numeric: tabular-nums; } .tbar { flex: 1; background: #080b11; border: 1px solid var(--line-soft); border-radius: 999px; height: 12px; overflow: hidden; } .tfill { background: linear-gradient(90deg, #2f7de1, var(--acc)); display: block; height: 100%; border-radius: 999px; } .tval { flex: 0 1 auto; color: var(--mute); font-size: 12px; font-variant-numeric: tabular-nums; }
|
|
467
|
+
ul.retries { margin: 4px 0; padding-left: 18px; color: var(--mute); font-size: 12px; }
|
|
468
|
+
.foot { color: var(--faint); font-size: 12px; margin-top: 32px; border-top: 1px solid var(--line-soft); padding-top: 12px; }
|
|
469
|
+
@media (max-width: 640px) {
|
|
470
|
+
body { padding: 18px 14px 32px; }
|
|
471
|
+
h1 { font-size: 24px; }
|
|
472
|
+
.toc { margin: 12px -14px 0; padding: 8px 14px; }
|
|
473
|
+
.brow { flex-wrap: wrap; }
|
|
474
|
+
.blabel { width: auto; }
|
|
475
|
+
.bar { flex: 1 1 100%; order: 3; }
|
|
476
|
+
.bval { flex: 1 1 auto; text-align: left; }
|
|
477
|
+
}
|
|
478
|
+
@media (prefers-reduced-motion: reduce) {
|
|
479
|
+
html { scroll-behavior: auto; }
|
|
480
|
+
* { transition: none !important; }
|
|
481
|
+
}
|
|
482
|
+
</style>
|
|
483
|
+
</head>
|
|
484
|
+
<body>
|
|
485
|
+
<header class="hero">
|
|
486
|
+
<div class="kicker"><span class="dot"></span>Local-only agent telemetry</div>
|
|
487
|
+
<h1>ATOM Observability</h1>
|
|
488
|
+
<p class="sub">Generated ${escapeHtml(generatedAt)}${opts.sourceDir ? ` · source <span class="mono">${escapeHtml(opts.sourceDir)}</span>` : ""}${opts.atomVersion ? ` · atom ${escapeHtml(opts.atomVersion)}` : ""}${livePill}</p>
|
|
489
|
+
<p class="sub">Private by design: this file was rendered on your machine from <span class="mono">~/.atom/telemetry/</span> and never leaves it. Previews are truncated and scrubbed of known provider secrets; API keys are never stored. Disable recording with <code>ATOM_TELEMETRY=0</code> or <code>"telemetry": {"enabled": false}</code> in <code>atom.json</code>.</p>
|
|
490
|
+
</header>
|
|
491
|
+
<nav class="toc" aria-label="Dashboard sections">
|
|
492
|
+
<a href="#overview">Overview</a><a href="#outcomes">Outcomes</a><a href="#tokens">Tokens</a><a href="#sessions">Sessions</a><a href="#honesty">Reading honestly</a>
|
|
493
|
+
</nav>
|
|
494
|
+
${corruptNote}
|
|
495
|
+
|
|
496
|
+
<h2 id="overview">Overview</h2>
|
|
497
|
+
<div class="cards">
|
|
498
|
+
<div class="card"><div class="k">Sessions</div><div class="v">${fmtCount(agg.sessions)}</div></div>
|
|
499
|
+
<div class="card"><div class="k">Turns</div><div class="v">${fmtCount(agg.turns)}</div></div>
|
|
500
|
+
<div class="card"><div class="k">Model calls</div><div class="v">${fmtCount(agg.modelCalls)}</div></div>
|
|
501
|
+
<div class="card"><div class="k">Tool calls</div><div class="v">${fmtCount(agg.toolCalls)}</div></div>
|
|
502
|
+
<div class="card"><div class="k">Tool success rate</div><div class="v">${successRate}</div></div>
|
|
503
|
+
<div class="card"><div class="k">Prompt tokens (reported)</div><div class="v">${fmtTokens(agg.usage.prompt_tokens, agg.usageReported, "No model call reported prompt_tokens.")}</div></div>
|
|
504
|
+
<div class="card"><div class="k">Completion tokens (reported)</div><div class="v">${fmtTokens(agg.usage.completion_tokens, agg.usageReported, "No model call reported completion_tokens.")}</div></div>
|
|
505
|
+
<div class="card"><div class="k">Cache read / write (reported)</div><div class="v">${fmtTokens(agg.usage.cacheReadTokens, agg.usageReported, "No provider reported cache-read counters.")} / ${fmtTokens(agg.usage.cacheWriteTokens, agg.usageReported, "No provider reported cache-write counters.")}</div></div>
|
|
506
|
+
<div class="card"><div class="k">Retries</div><div class="v">${fmtCount(agg.retries)}</div></div>
|
|
507
|
+
<div class="card"><div class="k">Read-cache hits (local)</div><div class="v">${fmtCount(agg.cacheHits)}</div></div>
|
|
508
|
+
<div class="card"><div class="k">Loop-guard hits</div><div class="v">${fmtCount(agg.repetitionHits)}</div></div>
|
|
509
|
+
<div class="card"><div class="k">Avg model latency</div><div class="v">${fmtMs(agg.avgModelLatencyMs)}</div></div>
|
|
510
|
+
<div class="card"><div class="k">Avg tool duration</div><div class="v">${fmtMs(agg.avgToolDurationMs)}</div></div>
|
|
511
|
+
<div class="card"><div class="k">Total cost</div><div class="v"><span class="na" title="${escapeHtml(agg.costNote)}">n/a</span></div></div>
|
|
512
|
+
</div>
|
|
513
|
+
|
|
514
|
+
<h2 id="outcomes">Turn outcomes</h2>
|
|
515
|
+
<div class="panel">${outcomesChart(agg)}</div>
|
|
516
|
+
|
|
517
|
+
<div class="grid2">
|
|
518
|
+
<div><h2>Tool calls by tool</h2><div class="panel">${toolChart(agg)}</div></div>
|
|
519
|
+
<div><h2>Avg tool duration</h2><div class="panel">${latencyChart(agg)}</div></div>
|
|
520
|
+
</div>
|
|
521
|
+
|
|
522
|
+
<h2 id="tokens">Tokens per session <span class="hcount">(reported only)</span></h2>
|
|
523
|
+
<div class="panel">${tokensChart(sessions)}</div>
|
|
524
|
+
|
|
525
|
+
<h2 id="sessions">Sessions <span class="hcount">(${sessions.length})</span></h2>
|
|
526
|
+
<div class="filters">
|
|
527
|
+
<input id="q" type="search" placeholder="Filter sessions…" aria-label="Filter sessions">
|
|
528
|
+
<select id="fprov" aria-label="Filter by provider"><option value="">All providers</option>${providerOptions}</select>
|
|
529
|
+
<select id="fout" aria-label="Filter by turn outcome">
|
|
530
|
+
<option value="">Any outcome</option>
|
|
531
|
+
<option value="completed">completed</option><option value="blocked">blocked</option>
|
|
532
|
+
<option value="unverified">unverified</option><option value="budget-exceeded">budget-exceeded</option>
|
|
533
|
+
<option value="failed">failed</option><option value="cancelled">cancelled</option><option value="pending">pending</option>
|
|
534
|
+
</select>
|
|
535
|
+
<span class="mute" id="count"></span>
|
|
536
|
+
</div>
|
|
537
|
+
<div id="sessions">
|
|
538
|
+
${sessions.length === 0 ? `<p class="empty">No sessions recorded yet. Use the agent — one small file per session lands in <span class="mono">~/.atom/telemetry/sessions/</span> on every completed turn — then regenerate this page.</p>` : sessions.map((s) => sessionBlock(s)).join("")}
|
|
539
|
+
</div>
|
|
540
|
+
|
|
541
|
+
<h2 id="honesty">Reading this page honestly</h2>
|
|
542
|
+
<div class="panel"><ul>
|
|
543
|
+
<li><strong>n/a</strong> means <em>not measured or not reported</em> — never zero. Hover any n/a for the exact reason.</li>
|
|
544
|
+
<li><strong>Tokens</strong> come only from <code>usage</code> payloads the provider sent with a model call. Sessions without payloads are excluded from token charts (not plotted as zero).</li>
|
|
545
|
+
<li><strong>Cost</strong> is ${escapeHtml(TELEMETRY_COST_NOTE)}</li>
|
|
546
|
+
<li><strong>Tool tokens:</strong> tools do not consume model tokens, so tool rows show n/a by design; per-call usage lives on model calls.</li>
|
|
547
|
+
<li><strong>Retries</strong> are transport retries inside one model call (HTTP 429/5xx or network, up to 2 retries) and attach to the call they precede.</li>
|
|
548
|
+
<li><strong>Iterations</strong> are loop tool-round steps: one model call plus the tool calls it requested. Displayed 1-based.</li>
|
|
549
|
+
<li><strong>Subagents:</strong> ATOM v1 runs a single-agent loop, so sessions normally show none — that is the honest state, not missing data.</li>
|
|
550
|
+
<li><strong>Durations</strong> are measured wall-clock (<code>Date.now()</code> deltas around the awaited call), including parallel-batch members timed individually.</li>
|
|
551
|
+
</ul></div>
|
|
552
|
+
|
|
553
|
+
<p class="foot">ATOM local observability · session → turn → iteration → model/tool call → result · stored under <span class="mono">~/.atom/telemetry/sessions/</span> (one JSON file per session, 0600 POSIX).</p>
|
|
554
|
+
|
|
555
|
+
<script>
|
|
556
|
+
(function () {
|
|
557
|
+
var q = document.getElementById("q");
|
|
558
|
+
var fp = document.getElementById("fprov");
|
|
559
|
+
var fo = document.getElementById("fout");
|
|
560
|
+
var count = document.getElementById("count");
|
|
561
|
+
function apply() {
|
|
562
|
+
var needle = (q.value || "").toLowerCase();
|
|
563
|
+
var prov = fp.value || "";
|
|
564
|
+
var out = fo.value || "";
|
|
565
|
+
var shown = 0, total = 0;
|
|
566
|
+
var nodes = document.querySelectorAll("#sessions details[data-session]");
|
|
567
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
568
|
+
total++;
|
|
569
|
+
var el = nodes[i];
|
|
570
|
+
var hay = el.getAttribute("data-search") || "";
|
|
571
|
+
var okQ = !needle || hay.indexOf(needle) !== -1;
|
|
572
|
+
var okP = !prov || el.getAttribute("data-provider") === prov;
|
|
573
|
+
var outs = (el.getAttribute("data-outcomes") || "").split(" ");
|
|
574
|
+
var okO = !out || outs.indexOf(out) !== -1;
|
|
575
|
+
var show = okQ && okP && okO;
|
|
576
|
+
el.style.display = show ? "" : "none";
|
|
577
|
+
if (show) shown++;
|
|
578
|
+
}
|
|
579
|
+
count.textContent = "showing " + shown + " of " + total;
|
|
580
|
+
}
|
|
581
|
+
q.addEventListener("input", apply);
|
|
582
|
+
fp.addEventListener("change", apply);
|
|
583
|
+
fo.addEventListener("change", apply);
|
|
584
|
+
apply();
|
|
585
|
+
})();
|
|
586
|
+
</script>
|
|
587
|
+
</body>
|
|
588
|
+
</html>`;
|
|
589
|
+
}
|