trantor 0.17.4 → 0.17.6
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/hub.mjs +117 -1
- package/package.json +1 -1
- package/ui.html +93 -1
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
},
|
|
7
7
|
"metadata": {
|
|
8
8
|
"description": "Trantor — the hub-world for AI agent crews: live message bus, presence, project Kanban/flow board + context-handoff for independent AI coding agents (Claude, Codex, Gemini, …)",
|
|
9
|
-
"version": "0.17.
|
|
9
|
+
"version": "0.17.6"
|
|
10
10
|
},
|
|
11
11
|
"plugins": [
|
|
12
12
|
{
|
|
13
13
|
"name": "trantor",
|
|
14
14
|
"source": "./",
|
|
15
15
|
"description": "The hub-world for AI agent crews. Say \"fire up the crew\" and Claude becomes the architect: a plan-aware Advisor routes the work (solo / cheap inline calls / live crew of Codex, Gemini, Kimi & DeepSeek in their own terminal windows), a Kanban/flow command center with a testing gate tracks it, and an economics brain (Scrooge) keeps the receipts. Includes the relay MCP, a SessionStart auto-discovery hook, and a PreCompact context-handoff so a fresh session can take over a full window instead of compacting.",
|
|
16
|
-
"version": "0.17.
|
|
16
|
+
"version": "0.17.6",
|
|
17
17
|
"author": {
|
|
18
18
|
"name": "Sasha Bogojevic"
|
|
19
19
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trantor",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.6",
|
|
4
4
|
"description": "Trantor — the hub-world for AI agent crews: live message bus, presence, project Kanban/flow board + crew orchestration for independent AI coding agents (Claude, Codex, Gemini, Kimi, DeepSeek)",
|
|
5
5
|
"mcpServers": {
|
|
6
6
|
"relay": {
|
package/hub.mjs
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// machines reach it (e.g. over a Tailscale tailnet), set RELAY_HOST=0.0.0.0 — but only on a
|
|
6
6
|
// private network, or add auth first. See "Always-on / remote hub" in the README (roadmap).
|
|
7
7
|
import http from "node:http";
|
|
8
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync, statSync } from "node:fs";
|
|
8
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, statSync, readdirSync } from "node:fs";
|
|
9
9
|
import { homedir } from "node:os";
|
|
10
10
|
import { join } from "node:path";
|
|
11
11
|
|
|
@@ -22,6 +22,29 @@ if (!existsSync(DATA_DIR)) mkdirSync(DATA_DIR, { recursive: true });
|
|
|
22
22
|
// total cheap to serve no matter how big the ledger grows.
|
|
23
23
|
let _ledgerCache = { mtimeMs: -1, rows: [] };
|
|
24
24
|
|
|
25
|
+
// Per-turn failure telemetry lives across many ~/.agent-bus/logs/<agent>-<project>.jsonl files
|
|
26
|
+
// (written by crew-runner.mjs). Scanning them all every /learning poll would be wasteful, so cache
|
|
27
|
+
// the aggregate and only rescan when a log file changes (tracked by the dir's newest mtime).
|
|
28
|
+
const LOGDIR = join(homedir(), ".agent-bus", "logs");
|
|
29
|
+
let _telemetryCache = { maxMtimeMs: -1, turns: [] };
|
|
30
|
+
function scanTelemetry() {
|
|
31
|
+
let files = [];
|
|
32
|
+
try { files = readdirSync(LOGDIR).filter(f => f.endsWith(".jsonl")); } catch { return _telemetryCache.turns; }
|
|
33
|
+
let maxMtime = 0;
|
|
34
|
+
for (const f of files) { try { const m = statSync(join(LOGDIR, f)).mtimeMs; if (m > maxMtime) maxMtime = m; } catch {} }
|
|
35
|
+
if (maxMtime === _telemetryCache.maxMtimeMs) return _telemetryCache.turns; // nothing changed
|
|
36
|
+
const turns = [];
|
|
37
|
+
for (const f of files) {
|
|
38
|
+
let txt = ""; try { txt = readFileSync(join(LOGDIR, f), "utf8"); } catch { continue; }
|
|
39
|
+
for (const line of txt.trim().split("\n")) {
|
|
40
|
+
if (!line) continue;
|
|
41
|
+
try { const r = JSON.parse(line); if (r && r.agent) turns.push(r); } catch {}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
_telemetryCache = { maxMtimeMs: maxMtime, turns };
|
|
45
|
+
return turns;
|
|
46
|
+
}
|
|
47
|
+
|
|
25
48
|
// peers: { session: { lastSeen, status, project } } ; tasks: kanban cards
|
|
26
49
|
// projectMeta: { project: { brief, by, updated } } — the "what & why" blurb per project
|
|
27
50
|
let state = { messages: [], peers: {}, seq: 0, tasks: [], taskSeq: 0, projectMeta: {}, lessons: [] };
|
|
@@ -218,6 +241,99 @@ const server = http.createServer(async (req, res) => {
|
|
|
218
241
|
const ls = state.lessons.filter(l => l.scope === "global" || (agent && l.scope === agent));
|
|
219
242
|
return json(res, 200, { lessons: ls });
|
|
220
243
|
}
|
|
244
|
+
// The self-learning loop, surfaced for the dashboard "Learning" sidebar: relay lessons grouped
|
|
245
|
+
// (global / per-agent / per-project), per-LLM reliability from turn telemetry (+ daily series for
|
|
246
|
+
// charts), and the Scrooge guardrails baked into each model's prompt (+ per-model economics).
|
|
247
|
+
if (req.method === "GET" && P === "/learning") {
|
|
248
|
+
const projOf = by => (by && by.includes(":")) ? by.split(":").pop() : "";
|
|
249
|
+
// ts is ms (lessons/telemetry) or s (ledger). Null-safe: a malformed record with a missing/bad
|
|
250
|
+
// ts must not throw (new Date(NaN).toISOString() does) and 500 the whole endpoint — return null
|
|
251
|
+
// and let callers skip that day-bucket.
|
|
252
|
+
const dayOf = ts => { const n = Number(ts); if (!n) return null; const d = new Date(n > 2e10 ? n : n * 1000); return Number.isNaN(d.getTime()) ? null : d.toISOString().slice(0, 10); };
|
|
253
|
+
const ALL = "*"; // the cross-project ("All projects") bucket
|
|
254
|
+
const out = { totals: {}, lessons: { global: [], byAgent: {}, byProject: {}, projects: [] }, agents: [], agentsByProject: {}, models: [], modelsByProject: {} };
|
|
255
|
+
|
|
256
|
+
// relay lessons → global / by-agent / by-project (project derived from the recorder's session id)
|
|
257
|
+
const projSet = new Set();
|
|
258
|
+
for (const l of state.lessons) {
|
|
259
|
+
const rec = { text: l.text, scope: l.scope, by: l.by || "", project: projOf(l.by), ts: l.ts || 0 };
|
|
260
|
+
if (l.scope === "global") out.lessons.global.push(rec); else (out.lessons.byAgent[l.scope] ||= []).push(rec);
|
|
261
|
+
if (rec.project) { (out.lessons.byProject[rec.project] ||= []).push(rec); projSet.add(rec.project); }
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// per-LLM reliability from turn telemetry, bucketed BY PROJECT (+ a global ALL bucket) so the
|
|
265
|
+
// sidebar's project filter scopes the charts. Each turn carries its own project.
|
|
266
|
+
const turns = scanTelemetry();
|
|
267
|
+
const relAgg = {}; // scope -> agent -> {turns,failures,models:Set,lastFailure,days}
|
|
268
|
+
const scopeModels = {}; // scope -> Set(model used)
|
|
269
|
+
let totalTurns = 0, totalFails = 0;
|
|
270
|
+
const bumpRel = (scope, t) => {
|
|
271
|
+
const a = ((relAgg[scope] ||= {})[t.agent] ||= { agent: t.agent, turns: 0, failures: 0, models: new Set(), lastFailure: null, days: {} });
|
|
272
|
+
a.turns++;
|
|
273
|
+
if (t.model) { a.models.add(t.model); if (t.model !== "default") (scopeModels[scope] ||= new Set()).add(t.model); }
|
|
274
|
+
const dk = dayOf(t.ts); const d = dk ? (a.days[dk] ||= { turns: 0, failures: 0 }) : null; if (d) d.turns++;
|
|
275
|
+
if (t.exit && t.exit !== 0) { a.failures++; if (d) d.failures++; if (!a.lastFailure || t.ts > a.lastFailure.ts) a.lastFailure = { ts: t.ts, exit: t.exit, project: t.project || "" }; }
|
|
276
|
+
};
|
|
277
|
+
for (const t of turns) {
|
|
278
|
+
if (!t.agent) continue;
|
|
279
|
+
totalTurns++; if (t.exit && t.exit !== 0) totalFails++;
|
|
280
|
+
bumpRel(ALL, t);
|
|
281
|
+
if (t.project) { bumpRel(t.project, t); projSet.add(t.project); }
|
|
282
|
+
}
|
|
283
|
+
// lessons-accumulated-over-time per scope -> agent brand -> day (agent-scoped lessons only)
|
|
284
|
+
const lessonAgg = {};
|
|
285
|
+
for (const l of state.lessons) {
|
|
286
|
+
if (l.scope === "global") continue; const d = dayOf(l.ts); if (!d) continue;
|
|
287
|
+
const bump = scope => { (((lessonAgg[scope] ||= {})[l.scope] ||= {})[d]) = (lessonAgg[scope][l.scope][d] || 0) + 1; };
|
|
288
|
+
bump(ALL); const p = projOf(l.by); if (p) bump(p);
|
|
289
|
+
}
|
|
290
|
+
const buildAgents = scope => Object.values(relAgg[scope] || {}).sort((a, b) => b.turns - a.turns).map(a => {
|
|
291
|
+
const days = Object.keys(a.days).sort(); let cum = 0; const ld = (lessonAgg[scope] || {})[a.agent] || {};
|
|
292
|
+
return { agent: a.agent, turns: a.turns, failures: a.failures, failRate: a.turns ? +(a.failures / a.turns).toFixed(3) : 0,
|
|
293
|
+
lastFailure: a.lastFailure, models: [...a.models],
|
|
294
|
+
series: {
|
|
295
|
+
failRate: days.map(d => ({ day: d, turns: a.days[d].turns, failures: a.days[d].failures, rate: a.days[d].turns ? +(a.days[d].failures / a.days[d].turns).toFixed(3) : 0 })),
|
|
296
|
+
lessons: Object.keys(ld).sort().map(d => ({ day: d, count: (cum += ld[d]) })),
|
|
297
|
+
} };
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
// Scrooge guardrails (global per model) + per-model economics from the ledger, bucketed by project
|
|
301
|
+
let guard = {}; try { guard = JSON.parse(readFileSync(join(homedir(), ".token-scrooge", "lessons.json"), "utf8")) || {}; } catch {}
|
|
302
|
+
try { const lp = join(homedir(), ".token-scrooge", "calls.jsonl"); const st = statSync(lp); if (st.mtimeMs !== _ledgerCache.mtimeMs) { const rows = readFileSync(lp, "utf8").trim().split("\n").map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(c => c && c.ok); _ledgerCache = { mtimeMs: st.mtimeMs, rows }; } } catch {}
|
|
303
|
+
const ledAgg = {}; // scope -> model -> {calls,ti,to,cost,days}
|
|
304
|
+
const bumpLed = (scope, c) => {
|
|
305
|
+
const m = ((ledAgg[scope] ||= {})[c.model] ||= { calls: 0, ti: 0, to: 0, cost: 0, days: {} });
|
|
306
|
+
m.calls++; m.ti += c.tokens_in || 0; m.to += c.tokens_out || 0; m.cost += c.cost_usd || 0;
|
|
307
|
+
const dk = dayOf(c.ts); if (dk) { const d = (m.days[dk] ||= { cost: 0, ti: 0, to: 0 }); d.cost += c.cost_usd || 0; d.ti += c.tokens_in || 0; d.to += c.tokens_out || 0; }
|
|
308
|
+
(scopeModels[scope] ||= new Set()).add(c.model);
|
|
309
|
+
};
|
|
310
|
+
for (const c of _ledgerCache.rows) { if (!c.model) continue; bumpLed(ALL, c); if (c.project) { bumpLed(c.project, c); projSet.add(c.project); } }
|
|
311
|
+
|
|
312
|
+
const savedOf = (ti, to, cost) => +Math.max(0, ti * 15 / 1e6 + to * 75 / 1e6 - cost).toFixed(2);
|
|
313
|
+
let totalGuardrails = 0;
|
|
314
|
+
const mkModel = (scope, model, g) => {
|
|
315
|
+
const gcount = Object.values(g || {}).reduce((s, arr) => s + (Array.isArray(arr) ? arr.length : 0), 0);
|
|
316
|
+
if (scope === ALL) totalGuardrails += gcount; // guardrails are global — count once
|
|
317
|
+
const lm = (ledAgg[scope] || {})[model];
|
|
318
|
+
return { model, guardrails: g || {}, guardrailCount: gcount, calls: lm ? lm.calls : 0, cost_usd: lm ? +lm.cost.toFixed(4) : 0,
|
|
319
|
+
saved_usd: lm ? savedOf(lm.ti, lm.to, lm.cost) : 0,
|
|
320
|
+
series: { saved: lm ? Object.keys(lm.days).sort().map(d => ({ day: d, saved: savedOf(lm.days[d].ti, lm.days[d].to, lm.days[d].cost) })) : [] } };
|
|
321
|
+
};
|
|
322
|
+
const buildModels = scope => {
|
|
323
|
+
const keys = new Set(scopeModels[scope] || []); // models used in this scope
|
|
324
|
+
if (scope === ALL) for (const k of Object.keys(guard)) if (k !== "*") keys.add(k); // global view also lists every guardrailed model
|
|
325
|
+
const arr = [...keys].sort().map(m => mkModel(scope, m, guard[m]));
|
|
326
|
+
if (guard["*"]) arr.unshift(mkModel(scope, "∗ all models", guard["*"])); // guardrails that apply to every model
|
|
327
|
+
return arr;
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
out.lessons.projects = [...projSet].sort();
|
|
331
|
+
out.agents = buildAgents(ALL); out.models = buildModels(ALL);
|
|
332
|
+
for (const p of out.lessons.projects) { out.agentsByProject[p] = buildAgents(p); out.modelsByProject[p] = buildModels(p); }
|
|
333
|
+
|
|
334
|
+
out.totals = { lessons: state.lessons.length, guardrails: totalGuardrails, turns: totalTurns, failures: totalFails, failRate: totalTurns ? +(totalFails / totalTurns).toFixed(3) : 0, models: out.models.length };
|
|
335
|
+
return json(res, 200, out);
|
|
336
|
+
}
|
|
221
337
|
if (req.method === "POST" && P === "/send") {
|
|
222
338
|
const b = await body(req);
|
|
223
339
|
if (!b.from || !String(b.text ?? "").trim()) return json(res, 400, { error: "from and non-empty text required" });
|
package/package.json
CHANGED
package/ui.html
CHANGED
|
@@ -11,8 +11,38 @@ header{display:flex;align-items:center;gap:13px;padding:11px 18px;border-bottom:
|
|
|
11
11
|
.dot.off{background:var(--dim);box-shadow:none}
|
|
12
12
|
.muted{color:var(--mut)}.dim{color:var(--dim)}.spacer{flex:1}
|
|
13
13
|
.pill{background:var(--card);border:1px solid var(--line);border-radius:20px;padding:3px 11px;font-size:12px;color:var(--mut)}
|
|
14
|
-
main{flex:1;display:grid;grid-template-columns:1fr 330px;min-height:0}
|
|
14
|
+
main{flex:1;display:grid;grid-template-columns:var(--lw,44px) 1fr 330px;min-height:0}
|
|
15
|
+
main.learn-open{--lw:372px}
|
|
15
16
|
.boards{overflow-y:auto;padding:16px 18px}
|
|
17
|
+
/* Learning sidebar (collapsible left rail) — surfaces the self-learning loop */
|
|
18
|
+
.learn{background:var(--panel);border-right:1px solid var(--line);display:flex;flex-direction:column;min-height:0;overflow:hidden}
|
|
19
|
+
.learn-head{display:flex;align-items:center;gap:8px;padding:11px 12px;border-bottom:1px solid var(--line);cursor:pointer;white-space:nowrap;user-select:none}
|
|
20
|
+
.learn-head .lt{font-weight:700;font-size:13px;color:var(--tx)}.learn-head .lt b{color:var(--grn)}
|
|
21
|
+
.learn-head .chev{margin-left:auto;color:var(--mut);font-size:14px}
|
|
22
|
+
main:not(.learn-open) .learn-head{flex-direction:column;gap:12px;padding:14px 0;height:100%;border-bottom:none;justify-content:flex-start}
|
|
23
|
+
main:not(.learn-open) .learn-head .lt{writing-mode:vertical-rl;transform:rotate(180deg);letter-spacing:.6px}
|
|
24
|
+
main:not(.learn-open) .learn-head .chev{margin:0}
|
|
25
|
+
main:not(.learn-open) .learn-body{display:none}
|
|
26
|
+
.learn-body{overflow-y:auto;padding:10px 12px 26px;flex:1}
|
|
27
|
+
.lsum{font-size:11.5px;color:var(--mut);line-height:1.5;margin-bottom:13px}.lsum b{color:var(--grn)}
|
|
28
|
+
.lsec{margin-bottom:17px}
|
|
29
|
+
.lsec>h3{font-size:10.5px;text-transform:uppercase;letter-spacing:.6px;color:var(--dim);margin:0 0 7px;display:flex;align-items:center;gap:6px}
|
|
30
|
+
.lsel{background:var(--card);color:var(--tx);border:1px solid var(--line);border-radius:6px;font:inherit;font-size:11px;padding:2px 4px;margin-left:auto;cursor:pointer;outline:none}
|
|
31
|
+
.lcard{background:var(--card);border:1px solid var(--line);border-radius:9px;padding:8px 10px;margin-bottom:7px}
|
|
32
|
+
.lcard .lc-h{display:flex;align-items:center;gap:7px;font-size:12px;margin-bottom:5px}
|
|
33
|
+
.lcard .lc-h .nm{font-weight:600;color:var(--tx)}
|
|
34
|
+
.lcard .lc-h .mut{color:var(--mut);font-size:10.5px;margin-left:auto}
|
|
35
|
+
.lles{font-size:11.5px;color:var(--mut);line-height:1.45;padding:5px 0;border-top:1px solid var(--line)}
|
|
36
|
+
.lles:first-child{border-top:none}
|
|
37
|
+
.lles .sc{display:inline-block;font-size:9px;color:var(--dim);border:1px solid var(--line);border-radius:8px;padding:0 5px;margin-right:5px;vertical-align:1px;text-transform:uppercase;letter-spacing:.4px}
|
|
38
|
+
.lguard{font-size:11px;color:var(--mut);line-height:1.42;padding:3px 0 3px 13px;position:relative}
|
|
39
|
+
.lguard:before{content:"";position:absolute;left:3px;top:9px;width:4px;height:4px;border-radius:50%;background:var(--grn)}
|
|
40
|
+
.ltask{font-size:9px;color:var(--dim);text-transform:uppercase;letter-spacing:.5px;margin:5px 0 2px}
|
|
41
|
+
.lstat{display:inline-block;font-size:10.5px;color:var(--mut);margin-right:10px}
|
|
42
|
+
.lstat b{color:var(--tx)}.lstat.bad b{color:var(--red)}.lstat.good b{color:var(--grn)}
|
|
43
|
+
.lchart{margin-top:4px}.lchart svg{display:block;width:100%;height:42px}
|
|
44
|
+
.lbadge{font-size:8.5px;color:var(--grn);border:1px solid #1d4a44;border-radius:8px;padding:1px 6px;text-transform:none;letter-spacing:0}
|
|
45
|
+
.lempty{font-size:11px;color:var(--dim);font-style:italic;padding:4px 0}
|
|
16
46
|
.proj{background:var(--panel);border:1px solid var(--line);border-radius:14px;margin-bottom:16px;overflow:hidden}
|
|
17
47
|
.proj-h{display:flex;align-items:center;gap:10px;padding:12px 16px;border-bottom:1px solid var(--line);background:#0e1421}
|
|
18
48
|
.proj-h .pname{font-family:'Sora',ui-sans-serif,sans-serif;font-weight:700;font-size:15px}
|
|
@@ -145,6 +175,12 @@ aside h2{font-size:10.5px;text-transform:uppercase;letter-spacing:.09em;color:va
|
|
|
145
175
|
<span class="pill"><span id="nproj">0</span> projects · <span id="nsess">0</span> live · <span id="ntask">0</span> cards</span>
|
|
146
176
|
</header>
|
|
147
177
|
<main>
|
|
178
|
+
<section class="learn" id="learn">
|
|
179
|
+
<div class="learn-head" id="learnToggle" title="Learning — lessons, per-LLM reliability, baked-in guardrails">
|
|
180
|
+
<span class="lt">🧠 <b>Learning</b></span><span class="chev" id="learnChev">›</span>
|
|
181
|
+
</div>
|
|
182
|
+
<div class="learn-body" id="learnBody"></div>
|
|
183
|
+
</section>
|
|
148
184
|
<div class="boards" id="boards"><div class="empty big">no projects yet — agents register a project on connect</div></div>
|
|
149
185
|
<aside>
|
|
150
186
|
<h2>Live feed</h2>
|
|
@@ -492,6 +528,62 @@ $('#send').onclick=async()=>{const t=$('#text').value.trim();if(!t)return;
|
|
|
492
528
|
$('#text').addEventListener('keydown',e=>{if(e.key==='Enter')$('#send').click();});
|
|
493
529
|
fetch('/recent?limit=40').then(r=>r.json()).then(d=>(d.messages||[]).forEach(m=>addMsg(m,false))).catch(()=>{});
|
|
494
530
|
$('#hub').textContent=location.host;
|
|
531
|
+
/* ---- Learning sidebar: the self-learning loop, surfaced ---- */
|
|
532
|
+
const LGRN='#2dd4bf',LRED='#ef6a6a';
|
|
533
|
+
// tiny inline-SVG charts (no library) — match the flow view's hand-rolled SVG approach
|
|
534
|
+
function lcLine(pts,key,opt){opt=opt||{};const w=300,h=42,pad=4,color=opt.color||LGRN;if(!pts||!pts.length)return '';
|
|
535
|
+
const ys=pts.map(p=>+p[key]||0),max=Math.max(1e-6,...ys),n=pts.length;
|
|
536
|
+
const X=i=>pad+(n<=1?(w-2*pad)/2:i*(w-2*pad)/(n-1)),Y=v=>h-pad-(v/max)*(h-2*pad);
|
|
537
|
+
const pl=pts.map((p,i)=>`${X(i).toFixed(1)},${Y(+p[key]||0).toFixed(1)}`).join(' ');
|
|
538
|
+
return `<svg viewBox="0 0 ${w} ${h}" preserveAspectRatio="none">`+
|
|
539
|
+
(opt.fill?`<polygon points="${pad},${h-pad} ${pl} ${X(n-1).toFixed(1)},${h-pad}" fill="${color}" opacity=".13"/>`:'')+
|
|
540
|
+
`<polyline points="${pl}" fill="none" stroke="${color}" stroke-width="1.5" stroke-linejoin="round"/>`+
|
|
541
|
+
(n===1?`<circle cx="${X(0).toFixed(1)}" cy="${Y(ys[0]).toFixed(1)}" r="2.2" fill="${color}"/>`:'')+`</svg>`;}
|
|
542
|
+
function lcBars(pts,key,opt){opt=opt||{};const w=300,h=42,pad=4,color=opt.color||LGRN;if(!pts||!pts.length)return '';
|
|
543
|
+
const ys=pts.map(p=>+p[key]||0),max=Math.max(1e-6,...ys),n=pts.length,gap=(w-2*pad)/n,bw=gap*0.68;
|
|
544
|
+
return `<svg viewBox="0 0 ${w} ${h}" preserveAspectRatio="none">`+pts.map((p,i)=>{const v=+p[key]||0,bh=(v/max)*(h-2*pad),x=pad+i*gap+(gap-bw)/2;
|
|
545
|
+
return `<rect x="${x.toFixed(1)}" y="${(h-pad-bh).toFixed(1)}" width="${bw.toFixed(1)}" height="${Math.max(0.5,bh).toFixed(1)}" rx="1" fill="${color}" opacity=".75"/>`;}).join('')+`</svg>`;}
|
|
546
|
+
let LEARN=null,learnProj=localStorage.getItem('abLearnProj')||'';
|
|
547
|
+
let learnOpen=localStorage.getItem('abLearnOpen')==='1';
|
|
548
|
+
function applyLearnState(){document.querySelector('main').classList.toggle('learn-open',learnOpen);$('#learnChev').textContent=learnOpen?'‹':'›';}
|
|
549
|
+
function renderLearnBody(){const el=$('#learnBody');if(!el)return;if(!LEARN){el.innerHTML='<div class="lempty">loading…</div>';return;}
|
|
550
|
+
const L=LEARN,t=L.totals||{};
|
|
551
|
+
let h=`<div class="lsum">Trantor has recorded <b>${t.lessons||0}</b> lessons and baked <b>${t.guardrails||0}</b> guardrails into model prompts. Across <b>${t.turns||0}</b> crew turns the failure rate is <b>${((t.failRate||0)*100).toFixed(1)}%</b> — self-healing as fixes compound.</div>`;
|
|
552
|
+
// Lessons (project filter; lifetime also shows per-agent lessons)
|
|
553
|
+
const projOpts=['<option value="">All projects (lifetime)</option>'].concat((L.lessons.projects||[]).map(p=>`<option value="${esc(p)}"${p===learnProj?' selected':''}>${esc(p)}</option>`)).join('');
|
|
554
|
+
h+=`<div class="lsec"><h3>Lessons <select class="lsel" id="lprojSel">${projOpts}</select></h3>`;
|
|
555
|
+
const lessons=learnProj?(L.lessons.byProject[learnProj]||[]):(L.lessons.global||[]);
|
|
556
|
+
if(!lessons.length)h+=`<div class="lempty">no lessons recorded for this scope yet</div>`;
|
|
557
|
+
else h+=lessons.slice(0,40).map(l=>`<div class="lles"><span class="sc">${esc(l.scope)}</span>${esc(l.text)}</div>`).join('');
|
|
558
|
+
if(!learnProj)for(const[ag,arr]of Object.entries(L.lessons.byAgent||{}))h+=`<div class="lles"><span class="sc">${esc(ag)}</span>${esc(arr[0].text)}${arr.length>1?` <span class="dim">+${arr.length-1} more</span>`:''}</div>`;
|
|
559
|
+
h+=`</div>`;
|
|
560
|
+
// Per-LLM reliability + progression charts — scoped to the selected project (or all)
|
|
561
|
+
const scopeLbl=learnProj?` <span class="dim" style="text-transform:none;letter-spacing:0">· ${esc(learnProj)}</span>`:'';
|
|
562
|
+
const ags=learnProj?(L.agentsByProject[learnProj]||[]):(L.agents||[]);
|
|
563
|
+
h+=`<div class="lsec"><h3>Per-LLM reliability${scopeLbl}</h3>`;
|
|
564
|
+
if(!ags.length)h+=`<div class="lempty">no turn telemetry${learnProj?' for this project':''} yet</div>`;
|
|
565
|
+
for(const a of ags){const fr=(a.failRate*100).toFixed(1);
|
|
566
|
+
h+=`<div class="lcard"><div class="lc-h">${iconFor(a.agent,14)}<span class="nm">${esc(a.agent)}</span><span class="mut">${a.models.filter(m=>m!=='default').join(', ')||'default'}</span></div>`;
|
|
567
|
+
h+=`<span class="lstat"><b>${a.turns}</b> turns</span><span class="lstat ${a.failures?'bad':'good'}"><b>${fr}%</b> fail</span>${a.lastFailure?`<span class="lstat">last exit ${a.lastFailure.exit}</span>`:''}`;
|
|
568
|
+
if((a.series.failRate||[]).length>1)h+=`<div class="lchart"><div class="ltask">fail-rate / day</div>${lcLine(a.series.failRate,'rate',{color:LRED,fill:true})}</div>`;
|
|
569
|
+
if((a.series.lessons||[]).length>1)h+=`<div class="lchart"><div class="ltask">lessons learned (cumulative)</div>${lcLine(a.series.lessons,'count',{color:LGRN,fill:true})}</div>`;
|
|
570
|
+
h+=`</div>`;}
|
|
571
|
+
h+=`</div>`;
|
|
572
|
+
// Baked-in guardrails per model + savings chart — guardrails are global; savings scoped to the project
|
|
573
|
+
h+=`<div class="lsec"><h3>Baked-in improvements${scopeLbl} <span class="lbadge">auto-injected into prompts</span></h3>`;
|
|
574
|
+
const gm=(learnProj?(L.modelsByProject[learnProj]||[]):(L.models||[])).filter(m=>m.guardrailCount>0);
|
|
575
|
+
if(!gm.length)h+=`<div class="lempty">no guardrailed models${learnProj?' used in this project':''} yet</div>`;
|
|
576
|
+
for(const m of gm){h+=`<div class="lcard"><div class="lc-h"><span class="nm">${esc(m.model)}</span>${m.saved_usd?`<span class="mut">$${m.saved_usd} saved · ${m.calls} calls</span>`:''}</div>`;
|
|
577
|
+
for(const[task,arr]of Object.entries(m.guardrails||{})){h+=`<div class="ltask">${esc(task)}</div>`;for(const g of arr)h+=`<div class="lguard">${esc(g)}</div>`;}
|
|
578
|
+
if((m.series.saved||[]).length>1)h+=`<div class="lchart"><div class="ltask">savings / day</div>${lcBars(m.series.saved,'saved',{color:LGRN})}</div>`;
|
|
579
|
+
h+=`</div>`;}
|
|
580
|
+
h+=`</div>`;
|
|
581
|
+
el.innerHTML=h;
|
|
582
|
+
const ps=$('#lprojSel');if(ps)ps.onchange=()=>{learnProj=ps.value;localStorage.setItem('abLearnProj',learnProj);renderLearnBody();};}
|
|
583
|
+
async function learn(){try{LEARN=await (await fetch('/learning')).json();if(learnOpen)renderLearnBody();}catch(_){}}
|
|
584
|
+
$('#learnToggle').onclick=()=>{learnOpen=!learnOpen;localStorage.setItem('abLearnOpen',learnOpen?'1':'0');applyLearnState();if(learnOpen)renderLearnBody();};
|
|
585
|
+
applyLearnState();learn();setInterval(learn,20000);
|
|
586
|
+
|
|
495
587
|
render();setInterval(render,2500);stream();
|
|
496
588
|
// Self-heal after the laptop sleeps / the tab is backgrounded: browser timers and the SSE
|
|
497
589
|
// stream get suspended and don't reliably resume, so the board freezes on stale data. The
|