trantor 0.17.5 → 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 +51 -29
- package/package.json +1 -1
- package/ui.html +10 -8
|
@@ -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
|
@@ -250,7 +250,8 @@ const server = http.createServer(async (req, res) => {
|
|
|
250
250
|
// ts must not throw (new Date(NaN).toISOString() does) and 500 the whole endpoint — return null
|
|
251
251
|
// and let callers skip that day-bucket.
|
|
252
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
|
|
253
|
+
const ALL = "*"; // the cross-project ("All projects") bucket
|
|
254
|
+
const out = { totals: {}, lessons: { global: [], byAgent: {}, byProject: {}, projects: [] }, agents: [], agentsByProject: {}, models: [], modelsByProject: {} };
|
|
254
255
|
|
|
255
256
|
// relay lessons → global / by-agent / by-project (project derived from the recorder's session id)
|
|
256
257
|
const projSet = new Set();
|
|
@@ -259,55 +260,76 @@ const server = http.createServer(async (req, res) => {
|
|
|
259
260
|
if (l.scope === "global") out.lessons.global.push(rec); else (out.lessons.byAgent[l.scope] ||= []).push(rec);
|
|
260
261
|
if (rec.project) { (out.lessons.byProject[rec.project] ||= []).push(rec); projSet.add(rec.project); }
|
|
261
262
|
}
|
|
262
|
-
out.lessons.projects = [...projSet].sort();
|
|
263
263
|
|
|
264
|
-
// per-LLM reliability from turn telemetry
|
|
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.
|
|
265
266
|
const turns = scanTelemetry();
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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); }
|
|
271
274
|
const dk = dayOf(t.ts); const d = dk ? (a.days[dk] ||= { turns: 0, failures: 0 }) : null; if (d) d.turns++;
|
|
272
|
-
if (t.exit && t.exit !== 0) { a.failures++;
|
|
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); }
|
|
273
282
|
}
|
|
274
|
-
// lessons-accumulated-over-time per
|
|
275
|
-
const
|
|
276
|
-
for (const l of state.lessons) {
|
|
277
|
-
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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,
|
|
282
293
|
lastFailure: a.lastFailure, models: [...a.models],
|
|
283
294
|
series: {
|
|
284
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 })),
|
|
285
296
|
lessons: Object.keys(ld).sort().map(d => ({ day: d, count: (cum += ld[d]) })),
|
|
286
|
-
}
|
|
287
|
-
};
|
|
297
|
+
} };
|
|
288
298
|
});
|
|
289
299
|
|
|
290
|
-
// Scrooge guardrails (per model
|
|
300
|
+
// Scrooge guardrails (global per model) + per-model economics from the ledger, bucketed by project
|
|
291
301
|
let guard = {}; try { guard = JSON.parse(readFileSync(join(homedir(), ".token-scrooge", "lessons.json"), "utf8")) || {}; } catch {}
|
|
292
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 {}
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
const m = (
|
|
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: {} });
|
|
296
306
|
m.calls++; m.ti += c.tokens_in || 0; m.to += c.tokens_out || 0; m.cost += c.cost_usd || 0;
|
|
297
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; }
|
|
298
|
-
|
|
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
|
+
|
|
299
312
|
const savedOf = (ti, to, cost) => +Math.max(0, ti * 15 / 1e6 + to * 75 / 1e6 - cost).toFixed(2);
|
|
300
313
|
let totalGuardrails = 0;
|
|
301
|
-
const mkModel = (model, g) => {
|
|
314
|
+
const mkModel = (scope, model, g) => {
|
|
302
315
|
const gcount = Object.values(g || {}).reduce((s, arr) => s + (Array.isArray(arr) ? arr.length : 0), 0);
|
|
303
|
-
totalGuardrails += gcount;
|
|
316
|
+
if (scope === ALL) totalGuardrails += gcount; // guardrails are global — count once
|
|
317
|
+
const lm = (ledAgg[scope] || {})[model];
|
|
304
318
|
return { model, guardrails: g || {}, guardrailCount: gcount, calls: lm ? lm.calls : 0, cost_usd: lm ? +lm.cost.toFixed(4) : 0,
|
|
305
319
|
saved_usd: lm ? savedOf(lm.ti, lm.to, lm.cost) : 0,
|
|
306
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) })) : [] } };
|
|
307
321
|
};
|
|
308
|
-
const
|
|
309
|
-
|
|
310
|
-
|
|
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); }
|
|
311
333
|
|
|
312
334
|
out.totals = { lessons: state.lessons.length, guardrails: totalGuardrails, turns: totalTurns, failures: totalFails, failRate: totalTurns ? +(totalFails / totalTurns).toFixed(3) : 0, models: out.models.length };
|
|
313
335
|
return json(res, 200, out);
|
package/package.json
CHANGED
package/ui.html
CHANGED
|
@@ -557,20 +557,22 @@ function renderLearnBody(){const el=$('#learnBody');if(!el)return;if(!LEARN){el.
|
|
|
557
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
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
559
|
h+=`</div>`;
|
|
560
|
-
// Per-LLM reliability + progression charts
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
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);
|
|
564
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>`;
|
|
565
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>`:''}`;
|
|
566
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>`;
|
|
567
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>`;
|
|
568
570
|
h+=`</div>`;}
|
|
569
571
|
h+=`</div>`;
|
|
570
|
-
// Baked-in guardrails per model + savings chart
|
|
571
|
-
h+=`<div class="lsec"><h3>Baked-in improvements <span class="lbadge">auto-injected into prompts</span></h3>`;
|
|
572
|
-
const gm=(L.models||[]).filter(m=>m.guardrailCount>0);
|
|
573
|
-
if(!gm.length)h+=`<div class="lempty">no
|
|
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>`;
|
|
574
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>`;
|
|
575
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>`;}
|
|
576
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>`;
|