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,301 @@
|
|
|
1
|
+
// Local observability webUI: a tiny read-only HTTP server over the same
|
|
2
|
+
// telemetry store the static dashboard reads. The static `--dashboard` file
|
|
3
|
+
// is untouched — this is a convenience live view for the browser.
|
|
4
|
+
//
|
|
5
|
+
// Design (production-quality, minimal blast radius):
|
|
6
|
+
// - `node:http` builtin only. No new dependencies.
|
|
7
|
+
// - Loopback-only by default (`127.0.0.1`). Traces hold scrubbed previews,
|
|
8
|
+
// never keys — but they still describe your machine, so the server never
|
|
9
|
+
// binds a LAN interface unless explicitly asked (`host` option).
|
|
10
|
+
// - Read-only: GET only (anything else is 405), no request body is ever
|
|
11
|
+
// read, nothing is ever written. Every handler is guarded — a bad request
|
|
12
|
+
// or a corrupt store yields a status code, never a crash.
|
|
13
|
+
// - Live data: every request re-reads the store, so the page and the JSON
|
|
14
|
+
// API always reflect the latest flushed turn. Responses carry
|
|
15
|
+
// `Cache-Control: no-store`.
|
|
16
|
+
// - Same honesty rules as the static page: the HTML comes from the shared
|
|
17
|
+
// `buildDashboardHtml` builder (plus a meta refresh), and the JSON API
|
|
18
|
+
// only carries reported values with `usageReported`-style flags — absent
|
|
19
|
+
// stays absent, never zero-filled.
|
|
20
|
+
//
|
|
21
|
+
// Routes:
|
|
22
|
+
// - `GET /` → live dashboard HTML (auto-refreshes every 5s)
|
|
23
|
+
// - `GET /api/health` → `{ok, version, sessions, turns, service}`
|
|
24
|
+
// - `GET /api/aggregates` → `summarizeTelemetry(sessions)` as JSON
|
|
25
|
+
// - `GET /api/sessions` → per-session summaries (counts + reported usage)
|
|
26
|
+
// - `GET /api/sessions/:id` → one full stored session, or 404
|
|
27
|
+
// - anything else → 404; non-GET → 405.
|
|
28
|
+
import { createServer } from "node:http";
|
|
29
|
+
import { TELEMETRY_VERSION, loadTelemetrySessions, summarizeTelemetry, telemetryDir, } from "./telemetry.js";
|
|
30
|
+
import { buildDashboardHtml } from "./telemetry-dashboard.js";
|
|
31
|
+
export const TELEMETRY_SERVER_DEFAULT_HOST = "127.0.0.1";
|
|
32
|
+
// Ephemeral by default (port 0): zero collision failures, the real URL is
|
|
33
|
+
// printed on start. Pin with --port / ATOM_TELEMETRY_PORT when bookmarkable
|
|
34
|
+
// matters.
|
|
35
|
+
export const TELEMETRY_SERVER_DEFAULT_PORT = 0;
|
|
36
|
+
export const TELEMETRY_SERVER_REFRESH_SECONDS = 5;
|
|
37
|
+
export const TELEMETRY_SERVER_PORT_ENV = "ATOM_TELEMETRY_PORT";
|
|
38
|
+
// Parse a port candidate (CLI flag or env). Returns the port when it is an
|
|
39
|
+
// integer in range, else null (caller falls through to the next source).
|
|
40
|
+
export function parseTelemetryPort(value) {
|
|
41
|
+
if (value === undefined || value === null)
|
|
42
|
+
return null;
|
|
43
|
+
const text = String(value).trim();
|
|
44
|
+
if (!/^\d+$/.test(text))
|
|
45
|
+
return null;
|
|
46
|
+
const n = Number(text);
|
|
47
|
+
if (!Number.isSafeInteger(n) || n < 1 || n > 65535)
|
|
48
|
+
return null;
|
|
49
|
+
return n;
|
|
50
|
+
}
|
|
51
|
+
// Precedence: explicit CLI value > ATOM_TELEMETRY_PORT > ephemeral (0).
|
|
52
|
+
export function resolveTelemetryPort(env = process.env, cliValue) {
|
|
53
|
+
return (parseTelemetryPort(cliValue) ??
|
|
54
|
+
parseTelemetryPort(env[TELEMETRY_SERVER_PORT_ENV]) ??
|
|
55
|
+
TELEMETRY_SERVER_DEFAULT_PORT);
|
|
56
|
+
}
|
|
57
|
+
function emptyOutcomes() {
|
|
58
|
+
return {
|
|
59
|
+
completed: 0,
|
|
60
|
+
blocked: 0,
|
|
61
|
+
unverified: 0,
|
|
62
|
+
"budget-exceeded": 0,
|
|
63
|
+
failed: 0,
|
|
64
|
+
cancelled: 0,
|
|
65
|
+
pending: 0,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
export function summarizeSession(s) {
|
|
69
|
+
const summary = {
|
|
70
|
+
sessionId: s.sessionId,
|
|
71
|
+
startedAt: s.startedAt,
|
|
72
|
+
endedAt: s.endedAt,
|
|
73
|
+
provider: s.provider,
|
|
74
|
+
model: s.model,
|
|
75
|
+
project: s.project,
|
|
76
|
+
turns: s.turns.length,
|
|
77
|
+
modelCalls: 0,
|
|
78
|
+
toolCalls: 0,
|
|
79
|
+
succeededToolCalls: 0,
|
|
80
|
+
failedToolCalls: 0,
|
|
81
|
+
usageReported: false,
|
|
82
|
+
usage: {},
|
|
83
|
+
outcomes: emptyOutcomes(),
|
|
84
|
+
retries: 0,
|
|
85
|
+
};
|
|
86
|
+
try {
|
|
87
|
+
for (const t of s.turns) {
|
|
88
|
+
if (t.outcome in summary.outcomes)
|
|
89
|
+
summary.outcomes[t.outcome] += 1;
|
|
90
|
+
summary.retries += typeof t.retryCount === "number" ? t.retryCount : 0;
|
|
91
|
+
summary.modelCalls += Array.isArray(t.modelCalls) ? t.modelCalls.length : 0;
|
|
92
|
+
if (t.usageReported) {
|
|
93
|
+
["prompt_tokens", "completion_tokens", "total_tokens", "cacheReadTokens", "cacheWriteTokens"].forEach((k) => {
|
|
94
|
+
// Reported-only accumulation: a turn that reported nothing
|
|
95
|
+
// contributes nothing (never zero-filled).
|
|
96
|
+
if (t.usage[k] !== undefined) {
|
|
97
|
+
summary.usageReported = true;
|
|
98
|
+
summary.usage[k] = (summary.usage[k] ?? 0) + t.usage[k];
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
if (Array.isArray(t.toolCalls)) {
|
|
103
|
+
for (const c of t.toolCalls) {
|
|
104
|
+
summary.toolCalls += 1;
|
|
105
|
+
if (c.success)
|
|
106
|
+
summary.succeededToolCalls += 1;
|
|
107
|
+
else
|
|
108
|
+
summary.failedToolCalls += 1;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// summaries are best-effort; return what accumulated
|
|
115
|
+
}
|
|
116
|
+
return summary;
|
|
117
|
+
}
|
|
118
|
+
function sendJson(res, status, body) {
|
|
119
|
+
try {
|
|
120
|
+
const text = JSON.stringify(body);
|
|
121
|
+
res.writeHead(status, {
|
|
122
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
123
|
+
"Cache-Control": "no-store",
|
|
124
|
+
"X-Content-Type-Options": "nosniff",
|
|
125
|
+
"Content-Length": Buffer.byteLength(text),
|
|
126
|
+
});
|
|
127
|
+
res.end(text);
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
try {
|
|
131
|
+
res.end();
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
// never throw out of a handler
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function sendHtml(res, status, html) {
|
|
139
|
+
try {
|
|
140
|
+
res.writeHead(status, {
|
|
141
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
142
|
+
"Cache-Control": "no-store",
|
|
143
|
+
"X-Content-Type-Options": "nosniff",
|
|
144
|
+
"Content-Length": Buffer.byteLength(html),
|
|
145
|
+
});
|
|
146
|
+
res.end(html);
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
try {
|
|
150
|
+
res.end();
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
// never throw out of a handler
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const SESSION_ID_RE = /^[A-Za-z0-9_.-]+$/;
|
|
158
|
+
function handleRequest(req, res, home, refreshSeconds) {
|
|
159
|
+
try {
|
|
160
|
+
if (req.method !== "GET") {
|
|
161
|
+
sendJson(res, 405, { error: "method not allowed (read-only GET server)" });
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
let pathname = "/";
|
|
165
|
+
try {
|
|
166
|
+
pathname = new URL(req.url ?? "/", "http://localhost").pathname;
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
sendJson(res, 400, { error: "bad request" });
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (pathname === "/") {
|
|
173
|
+
const { sessions, corrupt } = loadTelemetrySessions(home);
|
|
174
|
+
const html = buildDashboardHtml(sessions, {
|
|
175
|
+
sourceDir: telemetryDir(home),
|
|
176
|
+
corruptFiles: corrupt,
|
|
177
|
+
refreshSeconds,
|
|
178
|
+
});
|
|
179
|
+
sendHtml(res, 200, html);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (pathname === "/api/health") {
|
|
183
|
+
const { sessions } = loadTelemetrySessions(home);
|
|
184
|
+
sendJson(res, 200, {
|
|
185
|
+
ok: true,
|
|
186
|
+
service: "atom-observability",
|
|
187
|
+
version: TELEMETRY_VERSION,
|
|
188
|
+
sessions: sessions.length,
|
|
189
|
+
turns: sessions.reduce((n, s) => n + s.turns.length, 0),
|
|
190
|
+
});
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (pathname === "/api/aggregates") {
|
|
194
|
+
const { sessions } = loadTelemetrySessions(home);
|
|
195
|
+
sendJson(res, 200, summarizeTelemetry(sessions));
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (pathname === "/api/sessions") {
|
|
199
|
+
const { sessions } = loadTelemetrySessions(home);
|
|
200
|
+
sendJson(res, 200, sessions.map(summarizeSession));
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (pathname.startsWith("/api/sessions/")) {
|
|
204
|
+
const raw = pathname.slice("/api/sessions/".length);
|
|
205
|
+
let id = "";
|
|
206
|
+
try {
|
|
207
|
+
id = decodeURIComponent(raw);
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
sendJson(res, 400, { error: "bad session id" });
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
// Sessions are looked up in the loaded list (never by path), but an
|
|
214
|
+
// invalid id is still a 404 without touching the store twice.
|
|
215
|
+
if (!SESSION_ID_RE.test(id)) {
|
|
216
|
+
sendJson(res, 404, { error: "session not found" });
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const { sessions } = loadTelemetrySessions(home);
|
|
220
|
+
const found = sessions.find((s) => s.sessionId === id);
|
|
221
|
+
if (!found) {
|
|
222
|
+
sendJson(res, 404, { error: "session not found" });
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
sendJson(res, 200, found);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
sendJson(res, 404, { error: "not found" });
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
sendJson(res, 500, { error: "internal error" });
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
export function startTelemetryServer(opts = {}) {
|
|
235
|
+
const host = typeof opts.host === "string" && opts.host.length > 0 ? opts.host : TELEMETRY_SERVER_DEFAULT_HOST;
|
|
236
|
+
const port = typeof opts.port === "number" && Number.isSafeInteger(opts.port) && opts.port >= 0 && opts.port <= 65535
|
|
237
|
+
? opts.port
|
|
238
|
+
: TELEMETRY_SERVER_DEFAULT_PORT;
|
|
239
|
+
const refreshSeconds = typeof opts.refreshSeconds === "number" && Number.isFinite(opts.refreshSeconds) && opts.refreshSeconds > 0
|
|
240
|
+
? Math.floor(opts.refreshSeconds)
|
|
241
|
+
: TELEMETRY_SERVER_REFRESH_SECONDS;
|
|
242
|
+
const home = opts.home;
|
|
243
|
+
return new Promise((resolve, reject) => {
|
|
244
|
+
let server;
|
|
245
|
+
try {
|
|
246
|
+
server = createServer((req, res) => handleRequest(req, res, home, refreshSeconds));
|
|
247
|
+
}
|
|
248
|
+
catch (e) {
|
|
249
|
+
reject(e instanceof Error ? e : new Error(String(e)));
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
// Track sockets so close() never hangs on keep-alive connections.
|
|
253
|
+
const sockets = new Set();
|
|
254
|
+
try {
|
|
255
|
+
server.on("connection", (s) => {
|
|
256
|
+
sockets.add(s);
|
|
257
|
+
s.on("close", () => sockets.delete(s));
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
// observer wiring must never break startup
|
|
262
|
+
}
|
|
263
|
+
const onError = (e) => {
|
|
264
|
+
reject(e instanceof Error ? e : new Error(String(e)));
|
|
265
|
+
};
|
|
266
|
+
server.once("error", onError);
|
|
267
|
+
server.listen(port, host, () => {
|
|
268
|
+
server.off("error", onError);
|
|
269
|
+
let actual = port;
|
|
270
|
+
try {
|
|
271
|
+
const addr = server.address();
|
|
272
|
+
if (addr !== null && typeof addr === "object")
|
|
273
|
+
actual = addr.port;
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
// keep the requested port in the URL on introspection failure
|
|
277
|
+
}
|
|
278
|
+
resolve({
|
|
279
|
+
url: `http://${host}:${actual}/`,
|
|
280
|
+
host,
|
|
281
|
+
port: actual,
|
|
282
|
+
close: () => new Promise((done) => {
|
|
283
|
+
try {
|
|
284
|
+
for (const s of sockets) {
|
|
285
|
+
try {
|
|
286
|
+
s.destroy();
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
// ignore per-socket failures
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
server.close(() => done());
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
done();
|
|
296
|
+
}
|
|
297
|
+
}),
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
}
|