Baseline: distribution workspace before observability redesign
This commit is contained in:
183
.agents/observability/app.js
Normal file
183
.agents/observability/app.js
Normal file
@@ -0,0 +1,183 @@
|
||||
const refreshButton = document.getElementById("refresh-button");
|
||||
const refreshStatus = document.getElementById("refresh-status");
|
||||
const tailSelect = document.getElementById("tail-select");
|
||||
|
||||
const fields = {
|
||||
activeRun: document.getElementById("metric-active-run"),
|
||||
phase: document.getElementById("metric-phase"),
|
||||
branch: document.getElementById("metric-branch"),
|
||||
blockers: document.getElementById("metric-blockers"),
|
||||
runCount: document.getElementById("run-count"),
|
||||
runsList: document.getElementById("runs-list"),
|
||||
generatedAt: document.getElementById("generated-at"),
|
||||
nextAction: document.getElementById("next-action"),
|
||||
workspace: document.getElementById("workspace"),
|
||||
blockersList: document.getElementById("blockers"),
|
||||
progressPath: document.getElementById("progress-path"),
|
||||
progressTail: document.getElementById("progress-tail"),
|
||||
};
|
||||
|
||||
let refreshTimer = null;
|
||||
|
||||
refreshButton.addEventListener("click", () => loadStatus());
|
||||
tailSelect.addEventListener("change", () => loadStatus());
|
||||
|
||||
loadStatus();
|
||||
refreshTimer = setInterval(loadStatus, 5000);
|
||||
window.addEventListener("beforeunload", () => clearInterval(refreshTimer));
|
||||
|
||||
async function loadStatus() {
|
||||
setRefreshState("Loading", "");
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/status?tail=${encodeURIComponent(tailSelect.value)}`);
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
const data = await response.json();
|
||||
render(data);
|
||||
setRefreshState("Live", "status-ok");
|
||||
} catch (error) {
|
||||
setRefreshState("Error", "status-dead");
|
||||
fields.progressTail.textContent = `Failed to load status: ${error.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
function render(data) {
|
||||
const summary = data.summary || {};
|
||||
const checkpoint = data.checkpoint || {};
|
||||
const blockers = Array.isArray(checkpoint.blockers) ? checkpoint.blockers : [];
|
||||
|
||||
fields.activeRun.textContent = summary.active_run_id || "none";
|
||||
fields.phase.textContent = summary.current_phase || "unknown";
|
||||
fields.branch.textContent = summary.active_branch || "none";
|
||||
fields.blockers.textContent = String(blockers.length);
|
||||
fields.runCount.textContent = `${data.runs.length} total`;
|
||||
fields.generatedAt.textContent = formatTime(data.generated_at);
|
||||
fields.nextAction.textContent = summary.next_action || "No next action recorded.";
|
||||
fields.workspace.textContent = data.workspace || "-";
|
||||
|
||||
renderBlockers(blockers);
|
||||
renderRuns(data.runs, summary.active_run_id);
|
||||
renderProgress(data.active_progress);
|
||||
}
|
||||
|
||||
function renderBlockers(blockers) {
|
||||
fields.blockersList.innerHTML = "";
|
||||
|
||||
if (blockers.length === 0) {
|
||||
fields.blockersList.innerHTML = '<p class="muted">No blockers recorded.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
for (const blocker of blockers) {
|
||||
const item = document.createElement("div");
|
||||
item.className = "blocker";
|
||||
item.textContent = blocker;
|
||||
fields.blockersList.appendChild(item);
|
||||
}
|
||||
}
|
||||
|
||||
function renderRuns(runs, activeRunId) {
|
||||
fields.runsList.innerHTML = "";
|
||||
|
||||
if (runs.length === 0) {
|
||||
fields.runsList.innerHTML =
|
||||
'<div class="empty">No runs yet. Create .agents/runs/<run-id>/ with state.json and agents.json.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
for (const run of runs) {
|
||||
fields.runsList.appendChild(renderRun(run, run.id === activeRunId));
|
||||
}
|
||||
}
|
||||
|
||||
function renderRun(run, active) {
|
||||
const state = run.state || {};
|
||||
const card = document.createElement("article");
|
||||
card.className = "run-card";
|
||||
|
||||
const title = document.createElement("div");
|
||||
title.className = "run-title";
|
||||
title.innerHTML = `
|
||||
<strong>${escapeHtml(run.id)}</strong>
|
||||
<span class="chip ${active ? "status-ok" : ""}">${active ? "active" : "recorded"}</span>
|
||||
`;
|
||||
card.appendChild(title);
|
||||
|
||||
const meta = document.createElement("div");
|
||||
meta.className = "run-meta";
|
||||
meta.innerHTML = `
|
||||
<span>Phase: ${escapeHtml(state.current_phase || state.phase || "unknown")}</span>
|
||||
<span>Branch: ${escapeHtml(state.current_branch || state.branch || "none")}</span>
|
||||
<span>Expected: ${escapeHtml(state.expected_output_artifact || state.expected_output || "none")}</span>
|
||||
<span>Retries: ${escapeHtml(String(state.retry_count ?? 0))}</span>
|
||||
`;
|
||||
card.appendChild(meta);
|
||||
|
||||
const agents = document.createElement("div");
|
||||
agents.className = "agent-list";
|
||||
if (!run.agents || run.agents.length === 0) {
|
||||
agents.innerHTML = '<small class="muted">No agents recorded.</small>';
|
||||
} else {
|
||||
for (const agent of run.agents) {
|
||||
agents.appendChild(renderAgent(agent));
|
||||
}
|
||||
}
|
||||
card.appendChild(agents);
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
function renderAgent(agent) {
|
||||
const row = document.createElement("div");
|
||||
row.className = "agent-row";
|
||||
const statusClass = agent.lease_expired
|
||||
? "status-dead"
|
||||
: agent.status === "completed"
|
||||
? "status-ok"
|
||||
: "status-warn";
|
||||
const id = agent.thread_id || agent.process_id || agent.tool_call_id || "none";
|
||||
|
||||
row.innerHTML = `
|
||||
<div>
|
||||
<strong>${escapeHtml(agent.role || "unknown-role")}</strong>
|
||||
<small>heartbeat ${escapeHtml(agent.heartbeat_at || "none")} / id ${escapeHtml(String(id))}</small>
|
||||
${agent.last_error ? `<small>${escapeHtml(agent.last_error)}</small>` : ""}
|
||||
</div>
|
||||
<span class="chip ${statusClass}">${escapeHtml(agent.lease_expired ? "expired" : agent.status || "unknown")}</span>
|
||||
`;
|
||||
return row;
|
||||
}
|
||||
|
||||
function renderProgress(progress) {
|
||||
if (!progress || !progress.path) {
|
||||
fields.progressPath.textContent = "no progress file";
|
||||
fields.progressTail.textContent = "No progress file found for the active run.";
|
||||
return;
|
||||
}
|
||||
|
||||
fields.progressPath.textContent = progress.path;
|
||||
fields.progressTail.textContent = progress.lines.length > 0
|
||||
? progress.lines.join("\n")
|
||||
: "Progress file is empty.";
|
||||
}
|
||||
|
||||
function setRefreshState(label, extraClass) {
|
||||
refreshStatus.className = `status-pill ${extraClass}`;
|
||||
refreshStatus.textContent = label;
|
||||
}
|
||||
|
||||
function formatTime(value) {
|
||||
if (!value) return "-";
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return value;
|
||||
return date.toLocaleTimeString();
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value)
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """)
|
||||
.replaceAll("'", "'");
|
||||
}
|
||||
88
.agents/observability/index.html
Normal file
88
.agents/observability/index.html
Normal file
@@ -0,0 +1,88 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Agent Observability</title>
|
||||
<link rel="stylesheet" href="styles.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="shell">
|
||||
<header class="topbar">
|
||||
<div>
|
||||
<p class="eyebrow">Distribution orchestration</p>
|
||||
<h1>Agent Observability</h1>
|
||||
</div>
|
||||
<div class="toolbar">
|
||||
<label>
|
||||
Tail
|
||||
<select id="tail-select">
|
||||
<option value="40">40 lines</option>
|
||||
<option value="80" selected>80 lines</option>
|
||||
<option value="160">160 lines</option>
|
||||
</select>
|
||||
</label>
|
||||
<button id="refresh-button">Refresh</button>
|
||||
<span id="refresh-status" class="status-pill">Loading</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<section class="summary-grid" aria-label="Summary">
|
||||
<article class="metric">
|
||||
<span>Active run</span>
|
||||
<strong id="metric-active-run">-</strong>
|
||||
</article>
|
||||
<article class="metric">
|
||||
<span>Phase</span>
|
||||
<strong id="metric-phase">-</strong>
|
||||
</article>
|
||||
<article class="metric">
|
||||
<span>Branch</span>
|
||||
<strong id="metric-branch">-</strong>
|
||||
</article>
|
||||
<article class="metric">
|
||||
<span>Blockers</span>
|
||||
<strong id="metric-blockers">-</strong>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="layout">
|
||||
<section class="panel runs-panel">
|
||||
<div class="panel-head">
|
||||
<h2>Runs</h2>
|
||||
<span id="run-count" class="muted">0</span>
|
||||
</div>
|
||||
<div id="runs-list" class="runs-list"></div>
|
||||
</section>
|
||||
|
||||
<section class="panel detail-panel">
|
||||
<div class="panel-head">
|
||||
<h2>Checkpoint</h2>
|
||||
<span id="generated-at" class="muted">-</span>
|
||||
</div>
|
||||
<div class="checkpoint-grid">
|
||||
<div>
|
||||
<span>Next action</span>
|
||||
<p id="next-action">-</p>
|
||||
</div>
|
||||
<div>
|
||||
<span>Workspace</span>
|
||||
<p id="workspace">-</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="blockers" class="blockers"></div>
|
||||
|
||||
<div class="panel-head lower">
|
||||
<h2>Progress Tail</h2>
|
||||
<span id="progress-path" class="muted">-</span>
|
||||
</div>
|
||||
<pre id="progress-tail" class="progress-tail">No progress loaded.</pre>
|
||||
</section>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
363
.agents/observability/styles.css
Normal file
363
.agents/observability/styles.css
Normal file
@@ -0,0 +1,363 @@
|
||||
:root {
|
||||
--bg: #f5f4ef;
|
||||
--panel: #ffffff;
|
||||
--ink: #202428;
|
||||
--muted: #68707a;
|
||||
--line: #d9d4c8;
|
||||
--soft: #faf8f2;
|
||||
--blue: #235f8f;
|
||||
--green: #237457;
|
||||
--amber: #9a6818;
|
||||
--red: #a43d34;
|
||||
--shadow: 0 14px 34px rgba(35, 32, 25, 0.08);
|
||||
font-family:
|
||||
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
|
||||
sans-serif;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background: var(--bg);
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
button,
|
||||
select {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
button {
|
||||
min-height: 36px;
|
||||
border: 1px solid #1d4f76;
|
||||
border-radius: 8px;
|
||||
background: var(--blue);
|
||||
color: #ffffff;
|
||||
font-weight: 800;
|
||||
padding: 0 14px;
|
||||
}
|
||||
|
||||
select {
|
||||
min-height: 36px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
color: var(--ink);
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.shell {
|
||||
width: min(1440px, 100%);
|
||||
margin: 0 auto;
|
||||
padding: 22px;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
margin: 0 0 4px;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 28px;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 16px;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.toolbar label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.status-pill,
|
||||
.chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 26px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.status-pill {
|
||||
border: 1px solid var(--line);
|
||||
background: #ffffff;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.status-ok {
|
||||
background: #e5f2eb;
|
||||
color: var(--green);
|
||||
}
|
||||
|
||||
.status-warn {
|
||||
background: #f7ead1;
|
||||
color: var(--amber);
|
||||
}
|
||||
|
||||
.status-dead {
|
||||
background: #f5dddd;
|
||||
color: var(--red);
|
||||
}
|
||||
|
||||
.summary-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.metric,
|
||||
.panel {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: var(--panel);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.metric {
|
||||
display: grid;
|
||||
gap: 7px;
|
||||
min-height: 94px;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.metric span,
|
||||
.checkpoint-grid span {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.metric strong {
|
||||
overflow: hidden;
|
||||
color: var(--ink);
|
||||
font-size: 23px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(320px, 430px) minmax(0, 1fr);
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.panel {
|
||||
min-width: 0;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.panel-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.panel-head.lower {
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.muted {
|
||||
overflow: hidden;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.runs-list {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.empty {
|
||||
border: 1px dashed var(--line);
|
||||
border-radius: 8px;
|
||||
background: var(--soft);
|
||||
color: var(--muted);
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.run-card {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: var(--soft);
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.run-title {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.run-title strong {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.run-meta {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.agent-list {
|
||||
display: grid;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.agent-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
border-top: 1px solid var(--line);
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.agent-row small {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
color: var(--muted);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.checkpoint-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.checkpoint-grid div,
|
||||
.blockers {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: var(--soft);
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.checkpoint-grid p {
|
||||
overflow-wrap: anywhere;
|
||||
margin-top: 5px;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.blockers {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.blocker {
|
||||
color: var(--red);
|
||||
font-size: 14px;
|
||||
font-weight: 750;
|
||||
}
|
||||
|
||||
.progress-tail {
|
||||
overflow: auto;
|
||||
min-height: 360px;
|
||||
max-height: 620px;
|
||||
margin: 0;
|
||||
border: 1px solid #2d3339;
|
||||
border-radius: 8px;
|
||||
background: #202428;
|
||||
color: #f6f3ea;
|
||||
padding: 14px;
|
||||
font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.55;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
@media (max-width: 980px) {
|
||||
.summary-grid,
|
||||
.layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 560px) {
|
||||
.shell {
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.toolbar,
|
||||
.toolbar label,
|
||||
button,
|
||||
select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.checkpoint-grid,
|
||||
.run-meta {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user