tiny-agent-112m / code /scripts /dashboard.html
darioooooo0o's picture
tiny-agent-112m: base + RL weights, tokenizer, code, model card
4397e12 verified
Raw History Blame Contribute Delete
24.6 kB
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Tiny-agent Progress</title>
<style>
:root {
--bg: #f6f7f9; --panel: #ffffff; --ink: #16181d; --muted: #667085; --line: #e4e7ec; --grid: #eef0f3;
--accent: #3b6ef5; --good: #1f9d55; --warn: #c27c0e; --bad: #d64545;
--c1: #3b6ef5; --c2: #e8833a; --c3: #1f9d55; --c4: #d64545; --c5: #8b5cf6; --c6: #0e9f9a;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--bg: #0f1115; --panel: #171a21; --ink: #e8eaf0; --muted: #8b93a7; --line: #262b36; --grid: #1f242e;
--accent: #6d93ff; --good: #3ccf7e; --warn: #f0b13c; --bad: #ff6b6b;
--c1: #6d93ff; --c2: #ff9f5a; --c3: #3ccf7e; --c4: #ff6b6b; --c5: #a78bfa; --c6: #2dd4bf;
}
}
:root[data-theme="dark"] {
--bg: #0f1115; --panel: #171a21; --ink: #e8eaf0; --muted: #8b93a7; --line: #262b36; --grid: #1f242e;
--accent: #6d93ff; --good: #3ccf7e; --warn: #f0b13c; --bad: #ff6b6b;
--c1: #6d93ff; --c2: #ff9f5a; --c3: #3ccf7e; --c4: #ff6b6b; --c5: #a78bfa; --c6: #2dd4bf;
}
* { box-sizing: border-box; }
body { margin: 0; background: var(--bg); color: var(--ink);
font: 14px/1.45 ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; }
.wrap { max-width: 1280px; margin: 0 auto; padding: 20px 16px 48px; }
header { display: flex; flex-wrap: wrap; align-items: baseline; gap: 12px; margin-bottom: 16px; }
h1 { font-size: 20px; margin: 0; letter-spacing: -0.01em; }
.sub { color: var(--muted); font-size: 13px; }
.pill { display: inline-block; padding: 2px 10px; border-radius: 999px; font-size: 12px; font-weight: 600;
background: color-mix(in srgb, var(--accent) 15%, transparent); color: var(--accent); }
.pill.err { background: color-mix(in srgb, var(--bad) 15%, transparent); color: var(--bad); }
.spacer { flex: 1; }
button.theme { background: none; border: 1px solid var(--line); color: var(--muted); border-radius: 8px;
padding: 3px 9px; font: inherit; font-size: 12px; cursor: pointer; }
.pipeline { display: grid; grid-template-columns: repeat(5, 1fr); gap: 8px; margin-bottom: 16px; }
.stage { background: var(--panel); border: 1px solid var(--line); border-radius: 10px; padding: 10px 12px; min-width: 0; }
.stage .name { font-size: 12px; color: var(--muted); display: flex; align-items: center; gap: 6px; }
.stage .val { font-weight: 600; margin-top: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.stage .bar { height: 4px; background: var(--grid); border-radius: 2px; margin-top: 8px; overflow: hidden; }
.stage .bar > i { display: block; height: 100%; background: var(--accent); }
.stage.done .bar > i { background: var(--good); }
.stage.active { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent) inset; }
.dot { width: 8px; height: 8px; border-radius: 50%; background: var(--line); flex: none; }
.stage.done .dot { background: var(--good); }
.stage.active .dot { background: var(--accent); animation: pulse 1.6s infinite; }
@keyframes pulse { 50% { opacity: .35; } }
.kpis { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 8px; margin-bottom: 16px; }
.kpi { background: var(--panel); border: 1px solid var(--line); border-radius: 10px; padding: 10px 12px; }
.kpi .k { font-size: 12px; color: var(--muted); }
.kpi .v { font-size: 22px; font-weight: 650; font-variant-numeric: tabular-nums; letter-spacing: -0.01em; }
.kpi .d { font-size: 12px; color: var(--muted); }
h2 { font-size: 13px; text-transform: uppercase; letter-spacing: .06em; color: var(--muted); margin: 24px 0 8px; }
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(380px, 1fr)); gap: 12px; }
.card { background: var(--panel); border: 1px solid var(--line); border-radius: 10px; padding: 12px 14px; min-width: 0; }
.card h3 { font-size: 14px; margin: 0 0 2px; }
.card .hint { font-size: 12px; color: var(--muted); margin-bottom: 6px; }
.legend { display: flex; flex-wrap: wrap; gap: 4px 12px; font-size: 12px; color: var(--muted); margin-bottom: 4px; }
.legend i { display: inline-block; width: 10px; height: 3px; border-radius: 2px; vertical-align: middle; margin-right: 5px; }
.chart { position: relative; }
.chart svg { display: block; width: 100%; height: 190px; }
.tip { position: absolute; pointer-events: none; background: var(--panel); border: 1px solid var(--line);
border-radius: 8px; padding: 6px 8px; font-size: 12px; box-shadow: 0 4px 14px rgba(0,0,0,.12); white-space: nowrap; display: none; z-index: 2; }
.empty { height: 190px; display: grid; place-items: center; color: var(--muted); font-size: 13px; text-align: center; padding: 0 20px; }
table { width: 100%; border-collapse: collapse; font-size: 13px; font-variant-numeric: tabular-nums; }
th, td { text-align: right; padding: 5px 6px; border-bottom: 1px solid var(--line); }
th:first-child, td:first-child { text-align: left; }
th { color: var(--muted); font-weight: 500; font-size: 12px; }
td .tag { font-size: 11px; color: var(--muted); }
.up { color: var(--good); } .down { color: var(--bad); }
pre { margin: 0; white-space: pre-wrap; word-break: break-word; font: 12px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace;
color: var(--ink); max-height: 320px; overflow: auto; }
.problems { color: var(--bad); }
@media (max-width: 720px) {
.pipeline { grid-template-columns: 1fr 1fr; }
.grid { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<div class="wrap">
<header>
<h1>Tiny-agent</h1>
<span id="stage" class="pill">loading…</span>
<span class="sub" id="updated"></span>
<span class="spacer"></span>
<button class="theme" id="themeBtn" title="Toggle theme">◐ theme</button>
</header>
<div class="pipeline" id="pipeline"></div>
<div class="kpis" id="kpis"></div>
<h2>Reinforcement learning (GRPO)</h2>
<div class="grid" id="rlGrid"></div>
<h2>Per-task accuracy</h2>
<div class="grid"><div class="card" id="kinds"></div></div>
<h2>Pretraining &amp; decay</h2>
<div class="grid" id="ptGrid"></div>
<h2>Log</h2>
<div class="grid">
<div class="card"><h3>Pipeline events</h3><div class="hint">orchestrator and RL waiter</div><pre id="events"></pre></div>
<div class="card"><h3>RL notes</h3><div class="hint">changes made during RL and why (rl/NOTES.md)</div><pre id="notes"></pre></div>
</div>
</div>
<script>
const $ = id => document.getElementById(id);
const css = v => getComputedStyle(document.documentElement).getPropertyValue(v).trim();
const C = () => ['--c1','--c2','--c3','--c4','--c5','--c6'].map(css);
const fmt = (v, d = 2) => v == null || isNaN(v) ? '–' : (+v).toFixed(d);
const pct = (v, d = 0) => v == null ? '–' : (100 * v).toFixed(d) + '%';
const esc = s => String(s).replace(/[&<>]/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;'}[c]));
// theme toggle (per-viewer convenience only)
(function () {
let t = null; try { t = localStorage.getItem('ta-theme'); } catch (e) {}
if (t) document.documentElement.dataset.theme = t;
$('themeBtn').onclick = () => {
const dark = matchMedia('(prefers-color-scheme: dark)').matches;
const cur = document.documentElement.dataset.theme || (dark ? 'dark' : 'light');
const nxt = cur === 'dark' ? 'light' : 'dark';
document.documentElement.dataset.theme = nxt;
try { localStorage.setItem('ta-theme', nxt); } catch (e) {}
if (last) render(last);
};
})();
function ema(pts, a = 0.15) {
let m = null; return pts.map(([x, y]) => (y == null ? [x, null] : (m = m == null ? y : m + a * (y - m), [x, m])));
}
// minimal SVG line chart with hover readout
function chart(card, title, hint, series, o = {}) {
card.innerHTML = `<h3>${title}</h3><div class="hint">${hint}</div>`;
const has = series.some(s => s.pts.some(p => p[1] != null));
if (!has) { card.insertAdjacentHTML('beforeend', `<div class="empty">${o.empty || 'no data yet'}</div>`); return; }
card.insertAdjacentHTML('beforeend', '<div class="legend">' + series.filter(s => !s.noLegend).map(s =>
`<span><i style="background:${s.color};${s.dash ? 'opacity:.45' : ''}"></i>${s.name}</span>`).join('') + '</div>');
const box = document.createElement('div'); box.className = 'chart'; card.appendChild(box);
const W = 600, H = 190, L = 44, R = 10, T = 8, B = 22;
const all = series.flatMap(s => s.pts.filter(p => p[1] != null));
let x0 = Math.min(...all.map(p => p[0])), x1 = Math.max(...all.map(p => p[0]));
if (x1 === x0) { x0 -= 1; x1 += 1; }
let y0 = o.ymin ?? Math.min(...all.map(p => p[1])), y1 = o.ymax ?? Math.max(...all.map(p => p[1]));
if (y1 === y0) { y0 -= 0.5; y1 += 0.5; }
const pad = (y1 - y0) * 0.06; if (o.ymin == null) y0 -= pad; if (o.ymax == null) y1 += pad;
const sx = x => L + (x - x0) / (x1 - x0) * (W - L - R), sy = y => T + (1 - (y - y0) / (y1 - y0)) * (H - T - B);
const yf = o.yfmt || (v => Math.abs(y1 - y0) < 0.1 ? v.toFixed(3) : Math.abs(y1 - y0) < 5 ? v.toFixed(2) : v.toFixed(0));
const xf = o.xfmt || (v => (+v.toFixed(2)).toString());
let g = `<svg viewBox="0 0 ${W} ${H}" preserveAspectRatio="none">`;
for (let i = 0; i <= 4; i++) {
const v = y0 + (y1 - y0) * i / 4, y = sy(v);
g += `<line x1="${L}" x2="${W - R}" y1="${y}" y2="${y}" stroke="${css('--grid')}" />`;
g += `<text x="${L - 6}" y="${y + 4}" text-anchor="end" font-size="10" fill="${css('--muted')}">${yf(v)}</text>`;
}
for (let i = 0; i <= 4; i++) {
const v = x0 + (x1 - x0) * i / 4;
g += `<text x="${sx(v)}" y="${H - 6}" text-anchor="middle" font-size="10" fill="${css('--muted')}">${xf(v)}</text>`;
}
if (o.shade) g += `<rect x="${sx(o.shade[0])}" y="${T}" width="${Math.max(0, sx(Math.min(o.shade[1], x1)) - sx(o.shade[0]))}" height="${H - T - B}" fill="${css('--accent')}" opacity=".07" />`;
for (const s of series) {
let d = '', pen = false;
for (const [x, y] of s.pts) { if (y == null) { pen = false; continue; } d += (pen ? 'L' : 'M') + sx(x).toFixed(1) + ',' + sy(y).toFixed(1); pen = true; }
g += `<path d="${d}" fill="none" stroke="${s.color}" stroke-width="${s.dash ? 1 : 2}" ${s.dash ? 'opacity=".4"' : ''} vector-effect="non-scaling-stroke" stroke-linejoin="round" />`;
if (s.markers) for (const [x, y] of s.pts) if (y != null) g += `<circle cx="${sx(x)}" cy="${sy(y)}" r="3" fill="${s.color}" />`;
}
g += `<line id="hv" x1="0" x2="0" y1="${T}" y2="${H - B}" stroke="${css('--muted')}" stroke-dasharray="3 3" opacity="0" vector-effect="non-scaling-stroke"/></svg><div class="tip"></div>`;
box.innerHTML = g;
const svg = box.querySelector('svg'), tip = box.querySelector('.tip'), hv = box.querySelector('#hv');
svg.addEventListener('mousemove', ev => {
const r = svg.getBoundingClientRect(), x = x0 + ((ev.clientX - r.left) / r.width * W - L) / (W - L - R) * (x1 - x0);
const rows = series.filter(s => !s.noTip).map(s => {
let best = null; for (const p of s.pts) if (p[1] != null && (best == null || Math.abs(p[0] - x) < Math.abs(best[0] - x))) best = p;
return best && `<div><span style="color:${s.color}">●</span> ${s.name}: <b>${(o.tipfmt || yf)(best[1])}</b></div>`;
}).filter(Boolean);
const near = series[0].pts.reduce((b, p) => (b == null || Math.abs(p[0] - x) < Math.abs(b[0] - x) ? p : b), null);
hv.setAttribute('x1', sx(near[0])); hv.setAttribute('x2', sx(near[0])); hv.setAttribute('opacity', 1);
tip.innerHTML = `<div style="color:${css('--muted')}">${o.xname || 'x'} ${xf(near[0])}</div>` + rows.join('');
tip.style.display = 'block';
const px = (ev.clientX - r.left), w = tip.offsetWidth;
tip.style.left = Math.min(Math.max(0, px + 12), r.width - w) + 'px'; tip.style.top = '8px';
});
svg.addEventListener('mouseleave', () => { tip.style.display = 'none'; hv.setAttribute('opacity', 0); });
}
function stageBox(name, val, frac, cls) {
return `<div class="stage ${cls}"><div class="name"><span class="dot"></span>${name}</div><div class="val">${val}</div>` +
`<div class="bar"><i style="width:${Math.round(100 * Math.max(0, Math.min(1, frac || 0)))}%"></i></div></div>`;
}
const kpi = (k, v, d = '') => `<div class="kpi"><div class="k">${k}</div><div class="v">${v}</div><div class="d">${d}</div></div>`;
const mins = m => m == null ? '' : m >= 90 ? `${(m / 60).toFixed(1)} h` : `${Math.round(m)} min`;
let last = null;
function render(s) {
last = s;
const c = C(), order = ['pretrain', 'teacher', 'decay', 'eval', 'rl', 'rl_done'];
const at = order.indexOf(s.stage), state = i => (at > i || (s.stage === 'rl_done' && i <= 4) ? 'done' : at === i ? 'active' : '');
const labels = { sft: 'Warm start (SFT) before RL run 2', pretrain: 'Pretraining', teacher: 'Teacher data', decay: 'LR decay', eval: 'Eval gate', rl: 'GRPO', rl_done: 'GRPO finished', idle: 'Idle' };
$('stage').textContent = labels[s.stage] || s.stage; $('stage').className = 'pill';
$('updated').textContent = 'updated ' + s.now.slice(11) + ' · refreshes every 15 s';
const pt = s.pretrain, dc = pt && pt.decay, tb = s.teacher.build, rl = s.rl, eg = s.eval_gate;
const rlSteps = rl ? rl.steps : [], rlLast = rl && rl.last;
const RL_TOTAL = (() => { const a = (s.procs['grpo.py'] || {}).args || ''; const m = a.match(/--steps (\d+)/); return m ? +m[1] : 300; })();
const RUN = rl ? rl.run : '';
const preTok = dc ? (pt.series.find(r => r.step >= dc.start_step) || pt.last).tokens : pt && pt.last.tokens;
$('pipeline').innerHTML =
stageBox('Pretraining', pt ? `${(preTok / 1e9).toFixed(2)}B tokens` : '–', 1, at > 0 || s.stage === 'rl_done' ? 'done' : state(0)) +
stageBox('Teacher data', tb ? `${tb.kept.toLocaleString()} episodes` : (s.teacher.stats ? `${s.teacher.stats.usable} usable` : '–'), tb ? 1 : 0.5, tb ? 'done' : state(1)) +
stageBox('LR decay', dc ? `${pct(dc.frac)} · ${dc.eta_min != null && dc.frac < 1 ? mins(dc.eta_min) + ' left' : 'done'}` : 'pending', dc ? dc.frac : 0, pt && pt.final ? 'done' : state(2)) +
stageBox('Eval gate', eg ? `pass@1 ${pct(eg._all.pass1, 1)}` : (s.stage === 'eval' ? 'running…' : 'pending'), eg ? 1 : (s.stage === 'eval' ? 0.5 : 0), eg ? 'done' : state(3)) +
stageBox('GRPO' + (RUN ? ' · ' + RUN : ''), rlLast ? `step ${rlLast.step} / ${RL_TOTAL}` : 'pending', rlLast ? rlLast.step / RL_TOTAL : 0, s.stage === 'rl_done' ? 'done' : state(4));
// KPIs follow the active stage
let k = '';
if (rlLast) {
const accE = ema(rlSteps.map(r => [r.step, r.acc]), 0.1).pop()[1];
const ev = rl.evals.length ? rl.evals[rl.evals.length - 1] : null;
const base = eg ? eg._held_out.pass1 : null;
const ho = ev ? ev._held_out.pass1 : null;
const delta = ho != null && base != null ? ho - base : null;
k += kpi('Train accuracy (EMA)', pct(accE, 1), `last step ${pct(rlLast.acc, 1)}`);
k += kpi('Held-out pass@1', pct(ho, 1), delta == null ? 'tasks never trained on' : `<span class="${delta >= 0 ? 'up' : 'down'}">${delta >= 0 ? '+' : ''}${(100 * delta).toFixed(1)} pts</span> vs eval gate`);
k += kpi('Reasoning tokens / episode', fmt(rlLast.gen_tokens, 0), `${fmt(rlLast.turns, 1)} turns`);
k += kpi('Step time', mins(rlLast.step_s / 60), `ETA ${mins((RL_TOTAL - rlLast.step) * rlLast.step_s / 60)}`);
k += kpi('Train/decode mismatch', fmt(rlLast.mismatch, 3), `clipped ${pct(rlLast.clip_frac, 1)}`);
k += kpi('Repeat calls / episode', fmt(rlLast.repeat_calls, 2), `flagged turns ${pct(rlLast.flagged_turns, 1)}`);
} else if (pt) {
const l = pt.last;
k += kpi('Train loss', fmt(l.loss, 3), `step ${l.step.toLocaleString()}`);
k += kpi('Tokens seen', (l.tokens / 1e9).toFixed(2) + 'B', `${(l.tok_s / 1000).toFixed(1)}k tok/s`);
k += kpi('LR multiplier', fmt(l.lr_mult, 3), dc ? 'decaying' : 'stable phase');
if (dc) k += kpi('Decay progress', pct(dc.frac), `${dc.done} / ${dc.steps} steps · ${mins(dc.eta_min)} left`);
if (tb) k += kpi('Teacher episodes kept', tb.kept.toLocaleString(), `of ${tb.total.toLocaleString()} generated`);
if (eg) k += kpi('Eval gate pass@1', pct(eg._all.pass1, 1), `held-out ${pct(eg._held_out.pass1, 1)}`);
}
$('kpis').innerHTML = k;
// RL charts
const g = $('rlGrid'); g.innerHTML = '';
const add = () => { const d = document.createElement('div'); d.className = 'card'; g.appendChild(d); return d; };
const wait = s.stage === 'rl' || s.stage === 'rl_done' ? 'waiting for the first step…' : 'GRPO starts after the decay and the eval gate';
const S = key => rlSteps.map(r => [r.step, r[key]]);
const xo = { xname: 'step', xfmt: v => v.toFixed(0), empty: wait };
chart(add(), 'Reward & accuracy', 'per step (thin) and smoothed (thick); 256 episodes per step', [
{ name: 'accuracy', color: c[0], pts: ema(S('acc'), 0.1) }, { name: 'acc raw', color: c[0], pts: S('acc'), dash: 1, noLegend: 1, noTip: 1 },
{ name: 'reward', color: c[1], pts: ema(S('reward'), 0.1) }, { name: 'reward raw', color: c[1], pts: S('reward'), dash: 1, noLegend: 1, noTip: 1 },
], { ...xo, ymin: 0, ymax: 1, tipfmt: v => v.toFixed(3) });
const evPts = key => [...(eg ? [[0, eg[key].pass1]] : []), ...(rl ? rl.evals.map(e => [e.step, e[key].pass1]) : [])];
chart(add(), 'Eval pass@1', 'every 25 steps on 150 fixed tasks; step 0 = this run\'s pre-RL baseline; faint lines = earlier runs. Held-out = task kinds never trained on', [
{ name: 'held-out', color: c[4], pts: evPts('_held_out'), markers: 1 },
{ name: 'in-distribution', color: c[2], pts: evPts('_in_dist'), markers: 1 },
{ name: 'all', color: c[0], pts: evPts('_all'), markers: 1 },
...Object.entries((rl && rl.refs) || {}).flatMap(([nm, ev]) => [
{ name: nm + ' held-out', color: c[4], dash: 1, pts: ev.map(e => [e.step, e._held_out.pass1]) },
{ name: nm + ' all', color: c[0], dash: 1, pts: ev.map(e => [e.step, e._all.pass1]) }]),
], { ...xo, ymin: 0, ymax: 1, empty: eg ? 'baseline done; first RL eval at step 25' : wait, tipfmt: v => pct(v, 1) });
chart(add(), 'Reasoning length', 'generated tokens per episode (the length penalty should push this down without hurting accuracy)', [
{ name: 'gen tokens', color: c[5], pts: ema(S('gen_tokens'), 0.15) }, { name: 'raw', color: c[5], pts: S('gen_tokens'), dash: 1, noLegend: 1, noTip: 1 },
], xo);
chart(add(), 'Bad behaviour', 'per episode: repeated tool calls, parse errors, truncations; flagged = share of turns penalised', [
{ name: 'repeat calls', color: c[3], pts: S('repeat_calls') }, { name: 'parse errors', color: c[1], pts: S('parse_err') },
{ name: 'truncated', color: c[4], pts: S('truncated') }, { name: 'flagged turns', color: c[0], pts: S('flagged_turns') },
], xo);
chart(add(), 'Answer honesty', 'per episode: invented = wrong answer seen in no tool output (penalised); NOT_FOUND on answerable tasks and wrong-but-copied answers watch for gaming that penalty', [
{ name: 'invented', color: c[3], pts: S('invented') }, { name: 'NOT_FOUND on answerable', color: c[1], pts: S('nf_answerable') },
{ name: 'wrong, copied from output', color: c[5], pts: S('wrong_copied') },
], xo);
chart(add(), 'Learning signal', 'share of sampled groups dropped because all 8 attempts scored the same (too easy or too hard)', [
{ name: 'no-signal share', color: c[1], pts: rlSteps.map(r => [r.step, r.groups ? r.no_signal / r.groups : null]) },
], { ...xo, ymin: 0, ymax: 1, tipfmt: v => pct(v, 0) });
chart(add(), 'Stability', 'train/decode log-prob mismatch and share of tokens outside the importance-ratio clip', [
{ name: 'mismatch', color: c[4], pts: S('mismatch') }, { name: 'clip frac', color: c[2], pts: S('clip_frac') },
], xo);
chart(add(), 'Step time', 'seconds per step (rollouts are most of it)', [
{ name: 'step', color: c[0], pts: S('step_s') }, { name: 'rollout', color: c[5], pts: S('rollout_s') },
], { ...xo, yfmt: v => v.toFixed(0) + 's' });
if (rl && rl.problems.length) {
const d = add(); d.innerHTML = `<h3>Warnings from ${esc(rl.run)}/stdout.log</h3><pre class="problems">${rl.problems.map(esc).join('\n')}</pre>`;
}
// per-kind table
const kinds = new Set([...Object.keys(eg || {}), ...(rl && rl.evals.length ? Object.keys(rl.evals[rl.evals.length - 1]) : [])].filter(x => !x.startsWith('_') && x !== 'step'));
const lastKA = rlLast && rlLast.kind_acc; // not in the trimmed series; fetched from last
const evLast = rl && rl.evals.length ? rl.evals[rl.evals.length - 1] : null;
if (!kinds.size) {
$('kinds').innerHTML = '<h3>Per task kind</h3><div class="empty" style="height:80px">appears after the eval gate</div>';
} else {
let t = `<h3>Per task kind</h3><div class="hint">pass@1 on the fixed eval set; train = last GRPO step's accuracy</div><table><tr><th>kind</th><th>eval gate</th><th>latest eval${evLast ? ' (step ' + evLast.step + ')' : ''}</th><th>change</th><th>train</th></tr>`;
for (const kd of [...kinds].sort()) {
const a = eg && eg[kd] ? eg[kd].pass1 : null, b = evLast && evLast[kd] ? evLast[kd].pass1 : null;
const dlt = a != null && b != null ? b - a : null;
const ho = (s.heldout_kinds || []).includes(kd);
t += `<tr><td>${kd}${ho ? ' <span class="tag">held-out</span>' : ''}</td><td>${pct(a, 1)}</td><td>${pct(b, 1)}</td>` +
`<td class="${dlt == null ? '' : dlt >= 0 ? 'up' : 'down'}">${dlt == null ? '–' : (dlt >= 0 ? '+' : '') + (100 * dlt).toFixed(1)}</td>` +
`<td>${lastKA && lastKA[kd] != null ? pct(lastKA[kd], 0) : '–'}</td></tr>`;
}
$('kinds').innerHTML = t + '</table>';
}
// pretraining charts
const p = $('ptGrid'); p.innerHTML = '';
const addp = () => { const d = document.createElement('div'); d.className = 'card'; p.appendChild(d); return d; };
if (pt) {
// skip the first ~0.1B tokens: the initial loss (~11) would flatten everything after it
const ser = pt.series.filter(r => r.tokens >= 1e8).length > 5 ? pt.series.filter(r => r.tokens >= 1e8) : pt.series;
const T = ser.map(r => r.tokens / 1e9);
const val = pt.val.filter(r => r.tokens >= 1e8).length > 2 ? pt.val.filter(r => r.tokens >= 1e8) : pt.val;
const shade = dc ? [pt.series.find(r => r.step >= dc.start_step).tokens / 1e9, Infinity] : null;
chart(addp(), 'Training loss', 'smoothed; shaded = LR decay (teacher data enters the mix here)', [
{ name: 'loss', color: c[0], pts: ema(ser.map((r, i) => [T[i], r.loss]), 0.08) },
], { xname: 'tokens (B)', xfmt: v => v.toFixed(2) + 'B', shade });
const vk = pt.val.length ? Object.keys(pt.val[pt.val.length - 1]).filter(x => !['step', 'tokens'].includes(x)) : [];
const pick = ['synth_grounded', 'code_python', 'code_shell', 'math', 'english'].filter(x => vk.includes(x));
chart(addp(), 'Validation loss', 'held-out shards per source, every 250 steps', pick.map((key, i) => (
{ name: key, color: c[i % 6], pts: val.map(r => [r.tokens / 1e9, r[key]]), markers: 0 })),
{ xname: 'tokens (B)', xfmt: v => v.toFixed(2) + 'B', shade });
chart(addp(), 'Throughput & LR', 'tokens/s (k) and LR multiplier ×100', [
{ name: 'k tok/s', color: c[2], pts: ser.map((r, i) => [T[i], r.tok_s > 10000 ? r.tok_s / 1000 : null]) },
{ name: 'LR ×100', color: c[1], pts: ser.map((r, i) => [T[i], 100 * r.lr_mult]) },
], { xname: 'tokens (B)', xfmt: v => v.toFixed(2) + 'B', ymin: 0 });
}
if (tb || s.teacher.stats) {
const d = addp(); const st = s.teacher.stats;
d.innerHTML = '<h3>Teacher data (Qwen3.8-27B)</h3><div class="hint">kept = correct, grounded, well-formed, no repeated calls, fits 4k context</div>' +
(tb ? `<table><tr><th>generated</th><th>kept</th><th>wrong</th><th>malformed</th><th>too long</th><th>repeats</th></tr>` +
`<tr><td>${tb.total.toLocaleString()}</td><td>${tb.kept.toLocaleString()} (${pct(tb.kept / tb.total)})</td><td>${tb.wrong}</td><td>${tb.bad}</td><td>${tb.long}</td><td>${tb.repeat}</td></tr></table>` : '') +
(st ? `<div class="hint" style="margin-top:10px">last generator stats: ${st.episodes} episodes, ${st.usable} usable, ${st.tok_per_call} tokens/call, cut ${pct(st.cut_call_rate, 1)}</div>` : '');
}
$('events').textContent = s.events.join('\n') || '–';
$('notes').textContent = (rl && rl.notes) || 'no changes yet';
}
async function tick() {
try {
const r = await fetch('/api/state', { cache: 'no-store' });
const s = await r.json();
if (s.error) throw new Error(s.error);
render(s);
} catch (e) {
$('stage').textContent = 'cannot read state'; $('stage').className = 'pill err';
$('updated').textContent = String(e).slice(0, 120);
}
}
tick(); setInterval(tick, 15000);
</script>
</body>
</html>