Files
distribution/.agents/scripts/observe.mjs

220 lines
6.0 KiB
JavaScript

#!/usr/bin/env node
import { createServer } from "node:http";
import {
existsSync,
readdirSync,
readFileSync,
statSync,
} from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
const workspace = path.resolve(scriptDir, "../..");
const agentsDir = path.join(workspace, ".agents");
const runsDir = path.join(agentsDir, "runs");
const checkpointPath = path.join(agentsDir, "checkpoint.json");
const staticDir = path.join(agentsDir, "observability");
const port = parsePort(process.argv) || 4317;
const server = createServer((req, res) => {
const url = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`);
if (url.pathname === "/api/status") {
const tail = parseTail(url.searchParams.get("tail"));
sendJson(res, buildStatus(tail));
return;
}
serveStatic(res, url.pathname);
});
server.listen(port, "127.0.0.1", () => {
console.log(`Agent observability dashboard: http://127.0.0.1:${port}`);
});
server.on("error", (error) => {
console.error(`Could not start observability dashboard on 127.0.0.1:${port}`);
console.error(error.message);
process.exit(1);
});
function parsePort(argv) {
const index = argv.findIndex((arg) => arg === "--port" || arg === "-p");
if (index === -1) return null;
const value = Number(argv[index + 1]);
if (!Number.isInteger(value) || value < 1 || value > 65535) return null;
return value;
}
function parseTail(value) {
const parsed = Number(value);
if (!Number.isInteger(parsed) || parsed < 0) return 80;
return Math.min(parsed, 500);
}
function buildStatus(tailLines) {
const checkpoint = readJsonIfExists(checkpointPath);
const runs = listRuns();
const activeRunId = checkpoint?.active_run_id || findLatestRunId(runs);
const activeRun = runs.find((run) => run.id === activeRunId) || null;
return {
generated_at: new Date().toISOString(),
workspace,
checkpoint,
summary: {
run_count: runs.length,
active_run_id: activeRunId,
current_phase: checkpoint?.current_phase || null,
active_branch: checkpoint?.active_branch || null,
blocker_count: Array.isArray(checkpoint?.blockers) ? checkpoint.blockers.length : 0,
next_action: checkpoint?.next_action || null,
},
runs,
active_progress: activeRun ? readProgress(activeRun, tailLines) : null,
};
}
function listRuns() {
if (!existsSync(runsDir)) return [];
return readdirSync(runsDir)
.map((name) => path.join(runsDir, name))
.filter((runPath) => statSync(runPath).isDirectory())
.map((runPath) => {
const id = path.basename(runPath);
const state = readJsonIfExists(path.join(runPath, "state.json"));
const agents = normalizeAgents(readJsonIfExists(path.join(runPath, "agents.json")));
return {
id,
path: path.relative(workspace, runPath),
state,
agents: agents.map((agent) => ({
...agent,
lease_expired: isExpired(agent.lease_expires_at),
})),
mtime_ms: statSync(runPath).mtimeMs,
};
})
.sort((a, b) => b.mtime_ms - a.mtime_ms);
}
function normalizeAgents(value) {
if (!value) return [];
if (Array.isArray(value)) return value;
if (Array.isArray(value.agents)) return value.agents;
return Object.entries(value).map(([role, agent]) => ({
role,
...(typeof agent === "object" && agent ? agent : { status: String(agent) }),
}));
}
function findLatestRunId(runs) {
if (runs.length === 0) return null;
return runs.reduce((latest, run) => (run.mtime_ms > latest.mtime_ms ? run : latest)).id;
}
function readProgress(run, tailLines) {
const runPath = path.join(workspace, run.path);
const candidateNames = [
"progress.log",
"implementation-log.md",
"validation.md",
"opus-proposal.md",
"snarky-review.md",
];
const filePath = candidateNames
.map((name) => path.join(runPath, name))
.find((candidate) => existsSync(candidate) && statSync(candidate).isFile());
if (!filePath) {
return {
run_id: run.id,
path: null,
lines: [],
};
}
const lines = readFileSync(filePath, "utf8").split(/\r?\n/);
if (lines.length > 0 && lines[lines.length - 1] === "") {
lines.pop();
}
return {
run_id: run.id,
path: path.relative(workspace, filePath),
lines: lines.slice(Math.max(0, lines.length - tailLines)),
};
}
function isExpired(value) {
if (!value) return false;
const time = Date.parse(value);
if (!Number.isFinite(time)) return false;
return time < Date.now();
}
function readJsonIfExists(filePath) {
if (!existsSync(filePath)) return null;
try {
return JSON.parse(readFileSync(filePath, "utf8"));
} catch (error) {
return {
parse_error: error.message,
};
}
}
function sendJson(res, value) {
const body = JSON.stringify(value);
res.writeHead(200, {
"Content-Type": "application/json",
"Content-Length": Buffer.byteLength(body),
});
res.end(body);
}
function serveStatic(res, pathname) {
const relative = pathname === "/" ? "index.html" : pathname.slice(1);
const filePath = path.resolve(staticDir, relative);
if (filePath !== staticDir && !filePath.startsWith(`${staticDir}${path.sep}`)) {
sendNotFound(res);
return;
}
if (!existsSync(filePath) || !statSync(filePath).isFile()) {
sendNotFound(res);
return;
}
const body = readFileSync(filePath);
res.writeHead(200, {
"Content-Type": contentType(filePath),
"Content-Length": body.length,
});
res.end(body);
}
function sendNotFound(res) {
res.writeHead(404, { "Content-Type": "text/plain" });
res.end("Not found");
}
function contentType(filePath) {
const extension = path.extname(filePath);
switch (extension) {
case ".html":
return "text/html; charset=utf-8";
case ".css":
return "text/css; charset=utf-8";
case ".js":
return "text/javascript; charset=utf-8";
case ".json":
return "application/json";
default:
return "application/octet-stream";
}
}