ParthKulshreshtha's picture
Upload folder using huggingface_hub
8bccf1e verified
Raw
History Blame Contribute Delete
62.5 kB
/* JE Validation console — configure / live / results screens.
* Vanilla JS, no build step, no external assets. Layout and visual language
* follow docs/je_validation_task_spec_3.html's "Sample run" and "Environment
* console" mockups (frontend-design skill pass, 2026-08-06); every payload
* field consumed below is verified against console/api.py and console/runs.py,
* not assumed from the mockup's illustrative JSON.
*/
// The two baselines always run alongside real models -- they have no
// OpenRouter price and are always launchable (console/api.py BASELINE_ENTRIES).
const BASELINE_IDS = new Set(['baseline:flag_everything', 'baseline:no_evidence']);
// Curated shortlist for the model picker (user decision, 2026-08-06 fixlist):
// these five real OpenRouter models spanning price tiers (opus-class,
// gpt-class, deepseek pro/flash, qwen) are what the dropdown shows before the
// user types; typing searches the full cached catalog instead. Prices shown
// in the UI always come from /api/models -- this list is only ids. The two
// baselines live in their own separate control, not in the dropdown.
const CURATED_MODELS = [
'anthropic/claude-opus-5',
'openai/gpt-5.6-sol',
'deepseek/deepseek-v4-pro',
'deepseek/deepseek-v4-flash',
'qwen/qwen3-30b-a3b-instruct-2507',
];
// Mirrors run_config.py's MIN_SEEDS_FOR_VARIANCE (api.py uses it server-side
// for the 202 response's seed_warning; the UI mirrors it for the inline note
// so the warning appears the moment the slider moves, not after launch).
const MIN_SEEDS_FOR_VARIANCE = 5;
// Mirrors budgets.py's EST_TOKENS_PER_STEP -- calibrated 2026-08-06 from a
// real run's measured token/step p90 (scripts/measure_tokens.py). Used both
// for the token-ceiling prefill and the sweep cost estimate below.
const EST_TOKENS_PER_STEP = 85000;
// Display order for the tool-surface union. Mirrors envir/toolspecs.py's
// ALL_TOOL_NAMES order; purely cosmetic (the rendered set is always the real
// union of whatever /api/tasks returns, not this list) so a tool this array
// doesn't know about still renders, just sorted after the known ones.
const TOOL_ORDER = [
'query_ledger', 'get_entry', 'list_documents', 'open_document', 'get_policy',
'get_master_data', 'recompute', 'aggregate', 'compare_period', 'request_info',
'disposition', 'submit',
];
const Console = {
state: {
task: null, knobs: {}, tools: [],
// default sweep: the cheapest proven LLM preselected so the estimate is
// populated on first paint (launch stays explicit). No baseline by
// default (user, 2026-08-06) -- they're opt-in via the checkboxes.
models: ['deepseek/deepseek-v4-flash'],
seedCount: 5,
stepBudget: 0, tokenCeiling: 0,
estimate: null, // /api/estimate result: totals + key headroom
// runs with different prompt versions are separate scoring conditions;
// recorded in the contract server-side (run_config.py PROMPT_VERSIONS)
promptVersion: 'standard',
},
tasks: [],
modelCatalog: [],
toolUnion: [],
async init() {
checkHealth();
const [tasks, models] = await Promise.all([
fetch('/api/tasks').then((r) => r.json()),
fetch('/api/models').then((r) => r.json()),
]);
this.tasks = tasks;
this.modelCatalog = models;
this.toolUnion = unionTools(tasks);
renderTaskList(tasks);
initModelPicker();
initPromptPreview();
// Mockup parity: a task is always selected, so every section renders
// populated on first paint instead of a page of empty headers.
const firstRow = document.querySelector('#task-list .crow');
if (firstRow) firstRow.click();
const seedCountEl = document.getElementById('seed-count');
seedCountEl.value = this.state.seedCount;
document.getElementById('seed-count-val').textContent = this.state.seedCount;
seedCountEl.addEventListener('input', onSeedCountChange);
document.getElementById('prompt-version').addEventListener('change', (e) => {
this.state.promptVersion = e.target.value;
this.refreshPreview();
refreshPromptPreview();
});
document.getElementById('launch').addEventListener('click', () => this.launch());
for (const btn of document.querySelectorAll('.raw-toggle')) {
btn.addEventListener('click', () => onRawToggle(btn));
}
document.getElementById('history-refresh')
.addEventListener('click', loadRunHistory);
loadRunHistory();
this.refreshPreview();
},
selectTask(task) {
this.state.task = task;
this.state.knobs = { ...task.defaults.knobs };
this.state.tools = [...task.tools]; // all prechecked
this.state.stepBudget = task.defaults.step_budget;
this.state.tokenCeiling = task.defaults.token_ceiling;
clearLaunchError();
renderTaskDetail(task, this.state);
this.refreshPreview();
refreshPromptPreview();
},
buildRunRequest() {
const s = this.state;
return {
run_id: `run-${crypto.randomUUID().slice(0, 8)}`,
task_id: s.task.id,
models: s.models,
seed_count: s.seedCount,
knobs: s.knobs,
tools_enabled: s.tools,
step_budget: s.stepBudget,
token_ceiling: s.tokenCeiling,
prompt_version: s.promptVersion,
};
},
refreshPreview() {
const pre = document.getElementById('config-preview');
const launchBtn = document.getElementById('launch');
if (!this.state.task) {
pre.textContent = '// select a task to build a run config';
launchBtn.disabled = true;
} else {
pre.textContent = JSON.stringify(this.buildRunRequest(), null, 2);
launchBtn.disabled = this.state.models.length === 0;
}
renderSweepEstimate();
},
async launch() {
clearLaunchError();
const body = this.buildRunRequest();
let resp;
try {
resp = await fetch('/api/runs', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
} catch (err) {
showLaunchError({ field: 'request', error: 'network error. Is the backend running?' });
return;
}
if (!resp.ok) {
let err;
try { err = await resp.json(); } catch { err = {}; }
showLaunchError(normalizeError(resp, err));
return;
}
const { contract } = await resp.json();
loadRunHistory();
openLiveView(body.run_id, contract);
},
};
// ---- shared helpers ---------------------------------------------------------
function escapeHtml(s) {
return String(s).replace(/[&<>"']/g, (c) => ({
'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;',
}[c]));
}
function teaser(brief, n) {
return brief.length > n ? `${brief.slice(0, n)}…` : brief;
}
// mirrors runs.py's _slug: model ids ("anthropic/claude-sonnet-4.5",
// "baseline:flag_everything") become the path-safe slugs used in episode
// keys and in the trajectory endpoint's URL segment.
function slugModel(model) {
return model.replace(/\//g, '_').replace(/:/g, '_');
}
// "failed: <reason>" -> "failed"; anything without a colon passes through.
function statusBase(status) {
const i = status.indexOf(':');
return i === -1 ? status : status.slice(0, i);
}
function failureReason(status) {
const i = status.indexOf(':');
return i === -1 ? '' : status.slice(i + 1).trim();
}
const STATUS_GLYPH = {
queued: '○', running: '◐', finished: '✓', failed: '✕', aborted: '■', interrupted: '‼',
};
function statusGlyph(base) {
return STATUS_GLYPH[base] || '?';
}
function fmtPrice(pricePerToken) {
return `$${(pricePerToken * 1e6).toFixed(2)}`;
}
function fmtUsd(x) {
return x < 1 ? x.toFixed(4) : x.toFixed(2);
}
// ---- task list ------------------------------------------------------------
function unionTools(tasks) {
const seen = new Set();
for (const t of tasks) for (const tool of t.tools) seen.add(tool);
const known = TOOL_ORDER.filter((t) => seen.has(t));
const unknown = [...seen].filter((t) => !TOOL_ORDER.includes(t)).sort();
return [...known, ...unknown];
}
function renderTaskList(tasks) {
const root = document.getElementById('task-list');
root.innerHTML = '';
for (const task of tasks) {
const row = document.createElement('button');
row.type = 'button';
row.className = 'crow';
row.dataset.taskId = task.id;
row.title = task.brief;
row.innerHTML = `
<span class="tier ${task.tier}">${task.tier}</span>
<span class="tid mono">${task.id}</span>
<span class="ctitle">${escapeHtml(teaser(task.brief, 70))}</span>
`;
row.addEventListener('click', () => {
for (const el of root.querySelectorAll('.crow')) el.classList.remove('on');
row.classList.add('on');
Console.selectTask(task);
});
root.appendChild(row);
}
}
function renderTaskDetail(task, state) {
document.getElementById('knob-box').hidden = false;
document.getElementById('tool-box').hidden = false;
renderKnobs(task, state);
renderTools(task, state);
syncBudgetInputs(state);
}
// ---- knobs / budgets --------------------------------------------------------
function rangeNumberRow(label, id, min, max, value, disabled, onChange) {
const row = document.createElement('div');
row.className = 'field-row';
row.innerHTML = `
<span class="fieldlabel">${label}</span>
<input type="range" id="${id}-range" min="${min}" max="${max}" step="1"
value="${value}" ${disabled ? 'disabled' : ''}>
<input type="number" id="${id}-num" min="${min}" max="${max}" step="1"
value="${value}" class="mono" ${disabled ? 'disabled' : ''}>
`;
const range = row.querySelector(`#${id}-range`);
const num = row.querySelector(`#${id}-num`);
range.addEventListener('input', () => onChange(Number(range.value), range, num));
num.addEventListener('input', () => onChange(Number(num.value), range, num));
return row;
}
function clampKnob(value, meta) {
if (Number.isNaN(value)) return meta.min;
return Math.min(meta.max, Math.max(meta.min, value));
}
function renderKnobs(task, state) {
const wrap = document.getElementById('knobs');
wrap.innerHTML = '';
for (const [name, meta] of Object.entries(task.knob_meta)) {
const disabled = name === 'population' && !task.population_editable;
const label = name.replace(/_/g, ' ');
const row = rangeNumberRow(label.charAt(0).toUpperCase() + label.slice(1), `knob-${name}`,
meta.min, meta.max, state.knobs[name], disabled, (v, range, num) => {
const clamped = clampKnob(v, meta);
range.value = clamped;
num.value = clamped;
state.knobs[name] = clamped;
if (name === 'population') recalcBudgets(clamped);
Console.refreshPreview();
});
wrap.appendChild(row);
}
}
// Mirrors budgets.py's default_step_budget / default_token_ceiling; the
// server remains authoritative — this is a UI prefill convenience only,
// recomputed whenever the population knob moves.
function recalcBudgets(population) {
const stepBudget = Math.max(60, Math.ceil(4.5 * population));
const tokenCeiling = 10 * stepBudget * EST_TOKENS_PER_STEP;
Console.state.stepBudget = stepBudget;
Console.state.tokenCeiling = tokenCeiling;
syncBudgetInputs(Console.state);
schedulePromptPreviewRefresh();
}
function syncBudgetInputs(state) {
document.getElementById('step-budget-range').value = state.stepBudget;
document.getElementById('step-budget-num').value = state.stepBudget;
}
// The token ceiling has no input of its own (user, 2026-08-06): it is a
// runaway-episode fuse, not a per-run tuning knob, so it silently tracks the
// step budget (same 10x-headroom formula as budgets.py) and only surfaces in
// the run-config JSON.
function onStepBudgetChange(v) {
Console.state.stepBudget = v;
Console.state.tokenCeiling = 10 * v * EST_TOKENS_PER_STEP;
document.getElementById('step-budget-range').value = v;
document.getElementById('step-budget-num').value = v;
Console.refreshPreview();
schedulePromptPreviewRefresh();
}
function onSeedCountChange(e) {
Console.state.seedCount = Number(e.target.value);
document.getElementById('seed-count-val').textContent = Console.state.seedCount;
Console.refreshPreview();
}
// ---- tool surface -----------------------------------------------------------
function renderTools(task, state) {
const wrap = document.getElementById('tools');
wrap.innerHTML = '';
for (const tool of Console.toolUnion) {
const inTask = task.tools.includes(tool);
const on = state.tools.includes(tool);
const label = document.createElement('label');
label.className = `ctool${inTask ? '' : ' off'}`;
label.innerHTML = `<input type="checkbox" value="${escapeHtml(tool)}"
${on ? 'checked' : ''} ${inTask ? '' : 'disabled'}>
<span class="mono">${tool}</span>`;
label.querySelector('input').addEventListener('change', () => {
state.tools = [...wrap.querySelectorAll('input:checked')].map((cb) => cb.value);
Console.refreshPreview();
});
wrap.appendChild(label);
}
}
// ---- model picker: dropdown + full-catalog search + separate baselines ------
//
// One "Models" dropdown button (user decision, 2026-08-06, deliberate spec
// deviation): closed it shows the selection summary; open it lists the five
// curated models, and typing in its search box searches the FULL cached
// catalog (Console.modelCatalog, fetched once from /api/models) -- results
// are instant because the catalog is already in memory. The two baselines
// are plain checkboxes outside the dropdown; they never appear in search.
const SEARCH_RESULT_CAP = 50;
function realModels(ids) {
return ids.filter((id) => !BASELINE_IDS.has(id));
}
function catalogById() {
return new Map(Console.modelCatalog.map((m) => [m.id, m]));
}
function curatedModelList() {
const byId = catalogById();
return CURATED_MODELS.map((id) => byId.get(id)).filter(Boolean);
}
// If OpenRouter is unreachable, /api/models falls back to PINNED_MODELS,
// which contains none of the CURATED_MODELS ids -- curatedModelList()
// quietly filters those out, so without this the dropdown would open empty
// with no explanation. Surface the gap instead.
function renderCuratedNote(searching) {
const note = document.getElementById('curated-note');
const missing = searching ? 0
: CURATED_MODELS.filter((id) => !catalogById().has(id)).length;
note.hidden = missing === 0;
note.textContent = missing > 0
? `${missing} curated model${missing === 1 ? '' : 's'} unavailable ` +
'(model catalog offline); showing what is available.'
: '';
}
function initModelPicker() {
const btn = document.getElementById('model-dd-btn');
const panel = document.getElementById('model-dd');
const search = document.getElementById('model-search');
btn.addEventListener('click', () => {
const open = panel.hidden;
panel.hidden = !open;
btn.setAttribute('aria-expanded', String(open));
if (open) {
search.value = '';
renderModelDropdown('');
search.focus();
}
});
search.addEventListener('input', () => renderModelDropdown(search.value));
document.addEventListener('click', (e) => {
if (panel.hidden) return;
if (!panel.contains(e.target) && e.target !== btn && !btn.contains(e.target)) {
closeModelDropdown();
}
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && !panel.hidden) {
closeModelDropdown();
btn.focus();
}
});
for (const boxId of ['baseline-flag', 'baseline-noev']) {
const cb = document.getElementById(boxId);
cb.checked = Console.state.models.includes(cb.value);
cb.addEventListener('change', () => {
const others = Console.state.models.filter((id) => id !== cb.value);
Console.state.models = cb.checked ? [...others, cb.value] : others;
Console.refreshPreview();
});
}
renderModelSelection();
}
function closeModelDropdown() {
document.getElementById('model-dd').hidden = true;
document.getElementById('model-dd-btn').setAttribute('aria-expanded', 'false');
}
function searchCatalog(q) {
const hits = Console.modelCatalog.filter((m) =>
!BASELINE_IDS.has(m.id) && `${m.id} ${m.name}`.toLowerCase().includes(q));
return { shown: hits.slice(0, SEARCH_RESULT_CAP), total: hits.length };
}
function renderModelDropdown(query) {
const q = query.trim().toLowerCase();
const list = document.getElementById('model-list');
const hint = document.getElementById('model-dd-hint');
renderCuratedNote(q.length > 0);
let models;
if (q.length === 0) {
models = curatedModelList();
hint.textContent = 'Curated shortlist. Type to search the full catalog.';
} else {
const { shown, total } = searchCatalog(q);
models = shown;
hint.textContent = total === 0
? 'No models match.'
: total > shown.length
? `Showing ${shown.length} of ${total} matches. Keep typing to narrow.`
: `${total} match${total === 1 ? '' : 'es'}.`;
}
list.innerHTML = '';
for (const model of models) {
const launchable = model.launchable !== false;
const row = document.createElement('label');
row.className = 'mrow' + (launchable ? '' : ' mrow-disabled');
row.innerHTML = `
<input type="checkbox" value="${escapeHtml(model.id)}"
${Console.state.models.includes(model.id) ? 'checked' : ''}
${launchable ? '' : 'disabled'}>
<span class="mrow-main">
<span class="model-name">${escapeHtml(model.name)}</span>
<span class="model-id mono">${escapeHtml(model.id)}</span>
</span>
<span class="model-price mono">${fmtPrice(model.prompt_price)} / ${fmtPrice(model.completion_price)} /Mtok</span>
`;
row.querySelector('input').addEventListener('change', (e) => {
toggleModel(model.id, e.target.checked);
});
list.appendChild(row);
}
}
function toggleModel(id, on) {
const without = Console.state.models.filter((m) => m !== id);
Console.state.models = on ? [...without, id] : without;
renderModelSelection();
Console.refreshPreview();
}
function renderModelSelection() {
const selected = realModels(Console.state.models);
const label = document.getElementById('model-dd-label');
const byId = catalogById();
if (selected.length === 0) {
label.textContent = 'Select models';
} else if (selected.length === 1) {
const m = byId.get(selected[0]);
label.textContent = m ? m.name : selected[0];
} else {
label.textContent = `${selected.length} models selected`;
}
const tags = document.getElementById('model-tags');
tags.innerHTML = '';
for (const id of selected) {
const m = byId.get(id);
const tag = document.createElement('span');
tag.className = 'mtag';
tag.innerHTML = `
<span>${escapeHtml(m ? m.name : id)}</span>
<button type="button" class="mtag-x" aria-label="Remove ${escapeHtml(id)}">×</button>
`;
tag.querySelector('.mtag-x').addEventListener('click', () => {
toggleModel(id, false);
const panel = document.getElementById('model-dd');
if (!panel.hidden) {
renderModelDropdown(document.getElementById('model-search').value);
}
});
tags.appendChild(tag);
}
}
// ---- prompt preview ---------------------------------------------------------
//
// Shows the literal text the agent receives (GET /api/tasks/{id}/prompt --
// same brief_for() the episode path uses, so the preview cannot drift).
// Re-fetched on task/version change while open; step-budget edits re-fetch
// debounced since the budget is interpolated into the standard/detailed text.
let promptPreviewTimer = null;
function initPromptPreview() {
document.getElementById('prompt-view-btn').addEventListener('click', () => {
const box = document.getElementById('prompt-preview');
const btn = document.getElementById('prompt-view-btn');
box.hidden = !box.hidden;
btn.textContent = box.hidden ? 'view' : 'hide';
btn.setAttribute('aria-pressed', String(!box.hidden));
if (!box.hidden) refreshPromptPreview();
});
}
function schedulePromptPreviewRefresh() {
clearTimeout(promptPreviewTimer);
promptPreviewTimer = setTimeout(refreshPromptPreview, 300);
}
async function refreshPromptPreview() {
const box = document.getElementById('prompt-preview');
if (box.hidden || !Console.state.task) return;
const sys = document.getElementById('prompt-preview-system');
const brief = document.getElementById('prompt-preview-brief');
const params = new URLSearchParams({
version: Console.state.promptVersion,
step_budget: String(Console.state.stepBudget),
});
try {
const resp = await fetch(
`/api/tasks/${Console.state.task.id}/prompt?${params}`);
if (!resp.ok) {
const err = await resp.json().catch(() => ({}));
sys.textContent = '';
brief.textContent = `unavailable: ${err.error || `HTTP ${resp.status}`}`;
return;
}
const data = await resp.json();
sys.textContent = data.system_prompt;
brief.textContent = data.brief;
} catch {
sys.textContent = '';
brief.textContent = 'unavailable: network error';
}
}
// ---- sweep estimate -----------------------------------------------------------
//
// Cost is "rough, input-token dominated, before caching discounts": tokens
// estimated from EST_TOKENS_PER_STEP regardless of model, then bounded by the
// cheapest and priciest SELECTED model's prompt_price -- a range, not a
// blended single number, so the model choice's cost impact stays visible.
// Baseline conditions have $0 pricing and are excluded from the price bound.
// ---- key headroom (cost guard) -------------------------------------------
// Server-computed estimate + remaining key credit (/api/estimate). Warns when
// the run would outspend the key; never blocks the launch. Debounced: knob
// drags fire refresh() per tick and the endpoint calls OpenRouter's /key.
let estimateTimer = null;
function scheduleEstimateRefresh() {
clearTimeout(estimateTimer);
estimateTimer = setTimeout(refreshEstimate, 400);
}
async function refreshEstimate() {
const s = Console.state;
if (!s.models.length || !s.stepBudget) {
Console.state.estimate = null;
renderHeadroom();
return;
}
const params = new URLSearchParams({
models: s.models.join(','),
step_budget: String(s.stepBudget),
seeds: String(s.seedCount),
});
try {
const r = await fetch(`/api/estimate?${params}`);
Console.state.estimate = r.ok ? await r.json() : null;
} catch {
Console.state.estimate = null; // headroom unknown -> no warning
}
renderHeadroom();
}
function renderHeadroom() {
const est = Console.state.estimate;
const line = document.getElementById('headroom-line');
if (line) {
line.hidden = !(est && est.headroom_usd != null);
if (!line.hidden) {
line.textContent = `key headroom $${fmtUsd(est.headroom_usd)} remaining`;
}
}
const warning = document.getElementById('headroom-warning');
warning.hidden = !(est && est.exceeds);
if (!warning.hidden) {
warning.textContent =
`Estimated cost ≈ $${fmtUsd(est.total_usd)} exceeds the key's remaining `
+ `credit $${fmtUsd(est.headroom_usd)} — the run would die partway. `
+ 'Launching stays possible; lower seeds/budget or raise the key limit.';
}
}
function costRangeUsd(state, modelCatalog) {
const byId = new Map(modelCatalog.map((m) => [m.id, m]));
const prices = state.models
.filter((id) => !BASELINE_IDS.has(id))
.map((id) => byId.get(id))
.filter(Boolean)
.map((m) => m.prompt_price);
if (prices.length === 0) return null;
const totalTokens = prices.length * state.seedCount * state.stepBudget * EST_TOKENS_PER_STEP;
return { low: totalTokens * Math.min(...prices), high: totalTokens * Math.max(...prices) };
}
function renderSweepEstimate() {
const s = Console.state;
const box = document.getElementById('sweep-estimate');
const episodes = s.models.length * s.seedCount;
const stepCeiling = episodes * s.stepBudget;
const range = costRangeUsd(s, Console.modelCatalog);
let costLine;
if (!s.models.length) costLine = '-';
else if (!range) costLine = '$0.00 (baselines only)';
else if (range.low === range.high) costLine = `$${fmtUsd(range.low)}`;
else costLine = `$${fmtUsd(range.low)} – $${fmtUsd(range.high)}`;
box.innerHTML = `
<div class="kvline mono"><b>${episodes}</b> episode${episodes === 1 ? '' : 's'} · ${s.models.length} model${s.models.length === 1 ? '' : 's'} × ${s.seedCount} seed${s.seedCount === 1 ? '' : 's'}</div>
<div class="kvline mono">≤ <b>${stepCeiling}</b> agent steps total</div>
<div class="kvline mono">${costLine}</div>
<div class="kvline mono" id="headroom-line" hidden></div>
<div class="fieldnote">rough, input-token dominated, before caching discounts</div>
`;
scheduleEstimateRefresh();
const seedWarning = document.getElementById('seed-warning');
seedWarning.hidden = s.seedCount >= MIN_SEEDS_FOR_VARIANCE;
seedWarning.textContent = 'Fewer than 5 seeds: variance not measurable.';
const withheldCount = s.task ? s.task.tools.length - s.tools.length : 0;
const withheldNote = document.getElementById('withheld-note');
withheldNote.hidden = withheldCount <= 0;
withheldNote.textContent =
`${withheldCount} tool(s) withheld: harder variant, scored as a separate condition.`;
}
// ---- errors --------------------------------------------------------------
const FIELD_TO_BOX = {
knobs: 'knob-box',
tools_enabled: 'tool-box',
models: 'model-box',
};
function normalizeError(resp, body) {
if (resp.status === 409) {
return { field: 'run_id', error: body.detail || 'duplicate run_id' };
}
if (typeof body.field === 'string' && typeof body.error === 'string') return body;
return { field: 'request', error: body.detail || body.error || `launch failed (${resp.status})` };
}
function showLaunchError(err) {
clearLaunchError();
const p = document.getElementById('launch-error');
p.textContent = `${err.field}: ${err.error}`;
p.hidden = false;
const boxId = FIELD_TO_BOX[err.field];
if (boxId) document.getElementById(boxId).classList.add('invalid');
}
function clearLaunchError() {
const p = document.getElementById('launch-error');
p.hidden = true;
p.textContent = '';
for (const id of Object.values(FIELD_TO_BOX)) {
document.getElementById(id).classList.remove('invalid');
}
}
// ---- health dot ------------------------------------------------------------
async function checkHealth() {
const dot = document.getElementById('health-dot');
try {
const r = await fetch('/api/health');
dot.classList.toggle('ok', r.ok);
dot.classList.toggle('down', !r.ok);
dot.title = r.ok ? 'backend online' : 'backend unhealthy';
} catch {
dot.classList.add('down');
dot.title = 'backend unreachable';
}
}
// ---- run history -----------------------------------------------------------
//
// Persisted run.json summaries from GET /api/runs. Clicking a row re-enters
// the normal live-view path (openLiveView + SSE): a finished run's snapshot
// goes terminal immediately, which loads its results; a still-running run
// resumes streaming. This is what makes recorded runs reachable after a page
// reload, a backend restart, or from a browser that never launched them.
async function loadRunHistory() {
let runs;
try {
runs = await fetch('/api/runs').then((r) => r.json());
} catch {
return; // backend unreachable: keep whatever is shown
}
renderRunHistory(runs);
}
function historyModelsLabel(models) {
const real = models.filter((id) => !BASELINE_IDS.has(id));
const parts = real.map((id) => id.split('/').pop());
const baselines = models.length - real.length;
if (baselines) parts.push(baselines === 1 ? '1 baseline' : `${baselines} baselines`);
return parts.join(', ') || '-';
}
function renderRunHistory(runs) {
const list = document.getElementById('history-list');
document.getElementById('history-empty').hidden = runs.length > 0;
list.hidden = runs.length === 0;
list.innerHTML = '';
for (const run of runs) {
const base = statusBase(run.status);
const row = document.createElement('button');
row.type = 'button';
row.className = 'crow history-row';
row.innerHTML = `
<span class="mono history-id">${escapeHtml(run.run_id)}</span>
<span class="history-task">${escapeHtml(run.task_id)}
<span class="tid">${escapeHtml(run.tier || '')}</span></span>
<span class="history-models" title="${escapeHtml(run.models.join(', '))}">${escapeHtml(historyModelsLabel(run.models))}</span>
<span class="mono history-seeds">${run.seed_count} seed${run.seed_count === 1 ? '' : 's'}</span>
<span class="mono history-cost">${run.cost_usd != null ? `$${fmtUsd(run.cost_usd)}` : '-'}</span>
<span class="stat status-${escapeHtml(base)}">${escapeHtml(base)}</span>
`;
row.addEventListener('click', () => openPastRun(run.run_id));
list.appendChild(row);
}
}
async function openPastRun(runId) {
let snap;
try {
snap = await fetch(`/api/runs/${runId}`).then((r) => r.json());
} catch {
return;
}
if (!snap || !snap.contract) return;
openLiveView(runId, snap.contract);
}
// ---- deep links from the Tasks tab -----------------------------------------
//
// tabs.js translates #console/run/<run_id> into a "je:open-run" event (it is
// the only hash reader). Opening the run scrolls to the live view; the pulse
// on the panel and on the run's history row says "this is the one you
// clicked". Unknown run ids degrade to the plain console (openPastRun
// already swallows the failed fetch).
function pulseOnce(el) {
if (!el) return;
el.classList.remove('deep-flash');
void el.offsetWidth; // restart the animation on repeat clicks
el.classList.add('deep-flash');
setTimeout(() => el.classList.remove('deep-flash'), 1600);
}
async function openDeepLinkRun(runId) {
await loadRunHistory(); // ensure the row exists before pulsing it
await openPastRun(runId);
if (!Console.live || Console.live.runId !== runId) return;
pulseOnce(document.querySelector('#live .panel'));
const idEl = [...document.querySelectorAll('#history-list .history-id')]
.find((el) => el.textContent === runId);
if (idEl) pulseOnce(idEl.closest('.history-row'));
}
window.addEventListener('je:open-run', (e) => openDeepLinkRun(e.detail));
// ---- live view: SSE client -------------------------------------------------
//
// A fresh EventSource cannot send Last-Event-ID (the browser only adds it on
// its own auto-reconnects), so the resume point after a page reload travels
// as ?after= -- the server honors either. lastSeq lives in sessionStorage per
// run so a reload in the same tab resumes instead of replaying from zero.
function connectEvents(runId) {
let lastSeq = Number(sessionStorage.getItem(`seq:${runId}`) || 0);
const es = new EventSource(`/api/runs/${runId}/events?after=${lastSeq}`);
es.onopen = async () => {
setHealth('live');
try {
const snap = await fetch(`/api/runs/${runId}`).then((r) => r.json());
await applySnapshot(snap);
} catch {
// transient fetch failure -- the next event or error will recover
}
};
es.onmessage = (ev) => {
const seq = Number(ev.lastEventId);
if (seq <= lastSeq) return; // dedupe: never double-apply
lastSeq = seq;
sessionStorage.setItem(`seq:${runId}`, String(seq));
applyEvent(JSON.parse(ev.data));
};
es.onerror = () => setHealth('reconnecting'); // EventSource retries itself
return es;
}
function setHealth(state) {
const el = document.getElementById('live-conn');
if (!el) return;
el.textContent = state;
el.className = `conn-badge conn-${state}`;
}
// ---- live view: state + rendering ------------------------------------------
function openLiveView(runId, contract) {
if (Console.live && Console.live.es) Console.live.es.close();
document.getElementById('results').hidden = true;
Console.results = null;
const models = contract.sweep.models;
const seedCount = contract.sweep.seeds;
const tiles = {};
for (const model of models) {
for (let seed = 1; seed <= seedCount; seed++) {
const key = `${slugModel(model)}/seed${seed}`;
tiles[key] = {
key, model, seed, status: 'queued', stepsByIdx: {}, detail: {}, cost: 0,
// Incremental counters kept in lockstep with stepsByIdx (see setTileStep)
// so renderScoreboard/renderTile never have to re-walk a tile's full
// step map -- that walk is O(steps-per-tile), and doing it on every
// single incoming step event across a whole sweep is O(total-steps^2).
steps: 0, tokensIn: 0, tokensOut: 0,
};
}
}
Console.live = {
runId, contract, tiles, tileEls: {}, focusedKey: null, rowEls: {},
runStatus: 'queued', runCostFinal: null, terminalHandled: false,
es: null, trajCache: {}, detailTimer: null,
};
renderLiveShell(runId, tiles);
const live = document.getElementById('live');
live.hidden = false;
live.scrollIntoView({ behavior: 'smooth', block: 'start' });
Console.live.es = connectEvents(runId);
const firstKey = Object.keys(tiles)[0];
if (firstKey) selectEpisode(firstKey);
}
function renderLiveShell(runId, tiles) {
document.getElementById('live-run-id').textContent = runId;
setRunStatusChip('queued');
setHealth('connecting');
document.getElementById('live-banner').hidden = true;
document.getElementById('live-terminal-note').hidden = true;
document.getElementById('player-grid').hidden = true;
document.getElementById('step-feed').innerHTML = '';
const grid = document.getElementById('episode-grid');
grid.innerHTML = '';
for (const key of Object.keys(tiles)) {
const tile = tiles[key];
const card = document.createElement('button');
card.type = 'button';
card.className = 'ep-tile';
card.innerHTML = `
<span class="ep-tile-chip status-queued">
<span class="ep-tile-glyph">${statusGlyph('queued')}</span>queued
</span>
<span class="ep-tile-name">${escapeHtml(tile.model)}</span>
<span class="ep-tile-seed mono">seed ${tile.seed}</span>
<span class="ep-tile-counters mono">
<span class="ep-tile-steps">0 steps</span><span class="ep-tile-tokens">0 tok</span>
</span>
<span class="ep-tile-reason"></span>
`;
card.addEventListener('click', () => selectEpisode(key));
grid.appendChild(card);
Console.live.tileEls[key] = card;
}
renderScoreboard();
}
function setRunStatusChip(status) {
const el = document.getElementById('live-status');
const base = statusBase(status);
el.textContent = status;
el.className = `stat status-${base}`;
}
function showLiveBanner(msg) {
const b = document.getElementById('live-banner');
b.textContent = msg;
b.hidden = false;
}
function showLiveTerminalNote(status) {
const note = document.getElementById('live-terminal-note');
note.textContent = `Run ended without results (${status}). ` +
`failed/interrupted runs never produce a results.json.`;
note.hidden = false;
setHealth('closed');
}
function tileStepCount(tile) {
return tile.steps;
}
function tileTokenTotal(tile) {
return tile.tokensIn + tile.tokensOut;
}
// Sets tile.stepsByIdx[idx] and keeps tile.steps/tokensIn/tokensOut (the O(1)
// counters tileStepCount/tileTokenTotal read) in sync with the write --
// diffing against whatever was previously at that idx so a re-write of an
// already-seen step (trajectory backfill overwriting a step-event stub with
// the full record) adjusts the totals by the delta instead of double-adding.
function setTileStep(tile, idx, rec) {
const prev = tile.stepsByIdx[idx];
const prevIn = prev ? (prev.tokens_in || 0) : 0;
const prevOut = prev ? (prev.tokens_out || 0) : 0;
tile.stepsByIdx[idx] = rec;
if (!prev) tile.steps += 1;
tile.tokensIn += (rec.tokens_in || 0) - prevIn;
tile.tokensOut += (rec.tokens_out || 0) - prevOut;
}
function renderTile(key) {
const tile = Console.live.tiles[key];
const card = Console.live.tileEls[key];
if (!tile || !card) return;
const base = statusBase(tile.status);
const chip = card.querySelector('.ep-tile-chip');
chip.className = `ep-tile-chip status-${base}`;
chip.innerHTML = `<span class="ep-tile-glyph">${statusGlyph(base)}</span>${escapeHtml(base)}`;
card.querySelector('.ep-tile-steps').textContent = `${tileStepCount(tile)} steps`;
card.querySelector('.ep-tile-tokens').textContent = `${tileTokenTotal(tile)} tok`;
card.querySelector('.ep-tile-reason').textContent =
base === 'failed' ? failureReason(tile.status) : '';
card.classList.toggle('ep-tile-focused', Console.live.focusedKey === key);
}
function renderAllTiles() {
for (const key of Object.keys(Console.live.tiles)) renderTile(key);
}
// ---- live view: step log (sample-run-player pattern) -----------------------
//
// SSE 'step' events only carry {tool, ok, tokens_in, tokens_out, step_idx}
// (runs.py's _tail) -- no args/observation. Those live in the trajectory
// JSONL only, so the focused episode's full records are backfilled via GET
// .../trajectory: once on selection, then debounced on every further step
// event for that same episode so the log doesn't refetch the whole file on
// every single SSE message.
function selectEpisode(key) {
Console.live.focusedKey = key;
renderAllTiles();
document.getElementById('player-grid').hidden = false;
document.getElementById('step-feed-title').textContent = key;
renderStepLog(key, { forceBottom: true });
refreshFocusedDetail(key);
}
async function refreshFocusedDetail(key) {
const live = Console.live;
const tile = live.tiles[key];
if (!tile) return;
try {
const [modelslug, seedTag] = key.split('/');
const seed = seedTag.replace('seed', '');
const resp = await fetch(`/api/runs/${live.runId}/episodes/${modelslug}/${seed}/trajectory`);
if (!resp.ok) return; // not written yet -- fine
const traj = await resp.json();
for (const rec of traj) {
setTileStep(tile, rec.step_idx, rec);
tile.detail[rec.step_idx] = rec;
}
live.trajCache[key] = traj;
renderTile(key);
if (live.focusedKey === key) renderStepLog(key);
} catch {
// trajectory unreadable/unavailable right now -- leave rows as-is
}
}
function scheduleFocusedDetailRefresh(key) {
const live = Console.live;
if (!live || live.focusedKey !== key) return;
clearTimeout(live.detailTimer);
live.detailTimer = setTimeout(() => refreshFocusedDetail(key), 400);
}
function compactJson(v) {
const s = JSON.stringify(v);
return s.length > 90 ? `${s.slice(0, 90)}…` : s;
}
function stepBorderColor(rec) {
if (!rec.ok) return 'var(--l4)';
if (rec.tool === 'submit') return 'var(--accent)';
return 'var(--line-2)';
}
function stepLogRow(idx, rec) {
const hasDetail = 'args' in rec;
const row = document.createElement('div');
row.className = 'rstep';
row.style.borderLeftColor = stepBorderColor(rec);
row.innerHTML = `
<div class="rhead">
<span class="rn mono">${idx}</span>
<span class="mono rtool">${escapeHtml(rec.tool)}</span>
<span class="mono rargs">${hasDetail ? escapeHtml(compactJson(rec.args)) : '…'}</span>
<span class="mono rtok">${rec.tokens_in || 0}${rec.tokens_out || 0}</span>
</div>
${hasDetail ? `<div class="robs">${escapeHtml(compactJson(rec.observation))}</div>` : ''}
`;
return row;
}
// Autoscroll only if the feed was already at (or very near) the bottom --
// otherwise a user who scrolled up to read an earlier step gets yanked back
// down on every new row. `forceBottom` is for a deliberate view change
// (selecting a different episode), where jumping to the newest step is the
// expected behavior rather than "fighting" anything.
function isNearBottom(el, threshold = 4) {
return el.scrollHeight - el.scrollTop - el.clientHeight <= threshold;
}
// Full rebuild of the step feed from tile.stepsByIdx. Used when focus
// switches episode and after a trajectory backfill (both infrequent --
// backfill is debounced to at most once per 400ms per episode) -- NOT on
// every SSE step event, which instead goes through the incremental
// upsertStepRow below.
function renderStepLog(key, { forceBottom = false } = {}) {
const tile = Console.live.tiles[key];
if (!tile) return;
const feed = document.getElementById('step-feed');
const stayAtBottom = forceBottom || isNearBottom(feed);
feed.innerHTML = '';
const idxs = Object.keys(tile.stepsByIdx).map(Number).sort((a, b) => a - b);
const rowEls = {};
let lastRow = null;
for (const idx of idxs) {
const row = stepLogRow(idx, tile.stepsByIdx[idx]);
feed.appendChild(row);
rowEls[idx] = row;
lastRow = row;
}
Console.live.rowEls = rowEls;
if (stayAtBottom && lastRow) lastRow.scrollIntoView({ block: 'nearest' });
}
// Incremental per-event update for the focused episode's step feed: append
// the new row, or replace the existing row in place if this step_idx was
// already rendered (trajectory backfill re-delivering a step with fuller
// detail). Avoids rebuilding #step-feed's whole innerHTML on every single
// SSE 'step' message.
function upsertStepRow(key, idx, rec) {
const live = Console.live;
if (!live || live.focusedKey !== key) return;
const feed = document.getElementById('step-feed');
const stayAtBottom = isNearBottom(feed);
const row = stepLogRow(idx, rec);
const existing = live.rowEls[idx];
if (existing) existing.replaceWith(row);
else feed.appendChild(row);
live.rowEls[idx] = row;
if (stayAtBottom) row.scrollIntoView({ block: 'nearest' });
}
// ---- live view: run-wide scoreboard -----------------------------------------
//
// "Live scoreboard" mirrors the spec player's sticky panel, scoped to the
// whole run (not just the focused episode) so it stays meaningful across a
// multi-model sweep. Per-step correct/false-positive/missed tags from the
// spec mockup are NOT reproduced here: the answer key stays server-side,
// so nothing in this panel is inferred -- only fields the API actually sends.
function renderScoreboard() {
const live = Console.live;
if (!live) return;
const tiles = Object.values(live.tiles);
const totalSteps = tiles.reduce((s, t) => s + tileStepCount(t), 0);
const totalBudget = tiles.length * (live.contract.step_budget || 0);
const totalTokens = tiles.reduce((s, t) => s + tileTokenTotal(t), 0);
const costSoFar = live.runCostFinal != null
? live.runCostFinal
: tiles.reduce((s, t) => s + (t.cost || 0), 0);
const counts = {};
for (const t of tiles) {
const b = statusBase(t.status);
counts[b] = (counts[b] || 0) + 1;
}
const board = document.getElementById('scoreboard-body');
if (!board) return;
board.innerHTML = `
<table class="kv">
<tr><td>Steps used</td><td class="mono num">${totalSteps} / ${totalBudget}</td></tr>
<tr><td>Cumulative tokens</td><td class="mono num">${totalTokens}</td></tr>
<tr><td>Run cost so far</td><td class="mono num">$${fmtUsd(costSoFar)}</td></tr>
</table>
<div class="lab" style="margin-top:10px">Episodes</div>
<div class="statusline mono">${Object.entries(counts)
.map(([b, n]) => `${statusGlyph(b)} ${n} ${escapeHtml(b)}`).join(' ')}</div>
`;
}
// ---- live view: event + snapshot application -------------------------------
function applyEvent(evt) {
const live = Console.live;
if (!live) return;
if (evt.type === 'step') {
const tile = live.tiles[evt.episode];
if (!tile) return;
const idx = evt.data.step_idx;
const rec = { ...tile.detail[idx], ...evt.data };
setTileStep(tile, idx, rec);
renderTile(evt.episode);
if (live.focusedKey === evt.episode) {
upsertStepRow(evt.episode, idx, rec);
scheduleFocusedDetailRefresh(evt.episode);
}
renderScoreboard();
} else if (evt.type === 'episode') {
const tile = live.tiles[evt.episode];
if (!tile) return;
tile.status = evt.data.status;
if (evt.data.cost_usd != null) tile.cost = evt.data.cost_usd;
renderTile(evt.episode);
renderScoreboard();
} else if (evt.type === 'run') {
if (evt.data.status) {
live.runStatus = evt.data.status;
setRunStatusChip(evt.data.status);
maybeHandleTerminal(evt.data.status);
} else if (evt.data.abort_reason) {
showLiveBanner(`Run aborted: ${evt.data.abort_reason}`);
} else if (evt.data.warning) {
const who = evt.episode ? `${evt.episode}: ` : '';
showLiveBanner(`${who}${evt.data.warning}` +
(evt.data.error ? `: ${evt.data.error}` : ''));
}
}
}
async function applySnapshot(status) {
const live = Console.live;
if (!live) return;
live.runStatus = status.status;
setRunStatusChip(status.status);
if (status.cost_usd != null) live.runCostFinal = status.cost_usd;
if (status.abort_reason) showLiveBanner(`Run aborted: ${status.abort_reason}`);
const episodes = status.episodes || {};
for (const [key, epStatus] of Object.entries(episodes)) {
if (live.tiles[key]) live.tiles[key].status = epStatus;
}
renderAllTiles();
// Ground-truth repair: for any episode that has started, the trajectory
// file on disk is authoritative for step count / token total regardless
// of whether the event stream survived (buffer rollover, backend
// restart). Keyed merge by step_idx keeps this idempotent against any
// live 'step' events arriving around the same time.
await Promise.all(Object.keys(episodes)
.filter((key) => episodes[key] !== 'queued' && live.tiles[key])
.map((key) => refreshFocusedDetail(key)));
renderScoreboard();
maybeHandleTerminal(status.status);
}
function maybeHandleTerminal(status) {
const live = Console.live;
if (!live || live.terminalHandled) return;
const base = statusBase(status);
if (base === 'queued' || base === 'running') return;
live.terminalHandled = true;
if (live.es) { live.es.close(); live.es = null; }
// the stream is deliberately closed now; without this the badge can stick
// at "reconnecting" when a past run's empty SSE stream ends before the
// snapshot goes terminal (EventSource fires onerror on stream end)
setHealth('closed');
finalizeLiveCost(live.runId);
loadRunHistory(); // flip this run's history row to its final state
if (base === 'finished' || base === 'aborted') {
loadResults(live.runId);
} else {
// failed:/interrupted runs never get a results.json -- show the
// terminal state instead of polling results forever
showLiveTerminalNote(status);
}
}
// Episode 'finished' events carry cost_usd, but an episode that instead
// fails mid-run (token ceiling, credit limit) never emits one -- its real
// spend only ever lands in run.json's total (self._spent covers every
// on_usage call regardless of how the episode ended). So the running
// tile-summed total in renderScoreboard can undercount until the run goes
// terminal, at which point this reconciles against the authoritative total.
async function finalizeLiveCost(runId) {
try {
const snap = await fetch(`/api/runs/${runId}`).then((r) => r.json());
const live = Console.live;
if (!live || live.runId !== runId) return;
if (snap.cost_usd != null) live.runCostFinal = snap.cost_usd;
renderScoreboard();
} catch {
// best effort -- the tile-summed running total stays as the estimate
}
}
// ---- results view -----------------------------------------------------------
//
// The model->seed->step drill-down rows are interactive (click to expand) but
// aren't real links or buttons, so they need explicit keyboard support:
// tabindex="0" + role="button" in their markup, and this Enter/Space handler
// wired alongside every click listener. :focus-visible in styles.css already
// renders a visible outline on any focusable element, tr included.
function onActivateKey(handler) {
return (e) => {
if (e.key === 'Enter' || e.key === ' ' || e.key === 'Spacebar') {
e.preventDefault();
handler();
}
};
}
async function loadResults(runId) {
let resp;
try {
resp = await fetch(`/api/runs/${runId}/results`);
} catch {
showLiveTerminalNote('results unreachable: network error');
return;
}
if (!resp.ok) {
// per the invariant this shouldn't happen for finished/aborted runs
// (results.json lands before the status flips terminal) -- surface it
// rather than retry forever
showLiveTerminalNote(`results unavailable (HTTP ${resp.status})`);
return;
}
const data = await resp.json();
const statusSnap = await fetch(`/api/runs/${runId}`).then((r) => r.json())
.catch(() => null);
Console.results = { runId, data, statusSnap, model: null, seed: null, trajCache: {} };
renderResultsModelTable();
document.getElementById('results').hidden = false;
document.getElementById('results-level-2').hidden = true;
document.getElementById('results-level-3').hidden = true;
document.getElementById('results').scrollIntoView({ behavior: 'smooth', block: 'start' });
}
function fmtStat(stat) {
if (!stat || stat.mean == null) return '-';
return `${stat.mean.toFixed(3)} ± ${(stat.std ?? 0).toFixed(3)}`;
}
function fmtMean(stat) {
if (!stat || stat.mean == null) return '-';
return stat.mean.toFixed(3);
}
function fmtPassed(passed) {
if (passed === true) return 'pass';
if (passed === false) return 'fail';
return '-';
}
function renderResultsModelTable() {
const { data } = Console.results;
const wrap = document.getElementById('results-model-table-wrap');
const models = Object.keys(data);
let rows = '';
for (const model of models) {
const agg = data[model].aggregate;
rows += `
<tr class="results-model-row" data-model="${escapeHtml(model)}" tabindex="0" role="button">
<td class="mono">${escapeHtml(model)}</td>
<td class="mono num">${fmtStat(agg.final)}</td>
<td class="mono num">${fmtMean(agg.outcome)}</td>
<td class="mono num">${fmtMean(agg.process)}</td>
<td class="mono num">${(agg.fail_rate * 100).toFixed(0)}%</td>
<td class="mono num">${agg.n}</td>
<td>${agg.variance_measurable ? 'yes' : 'no'}</td>
<td class="rationale-cell">${escapeHtml(agg.rationale)}</td>
</tr>`;
}
wrap.innerHTML = `
<table class="results-table">
<thead><tr>
<th>Model</th><th>Final (mean ± std)</th><th>Outcome</th><th>Process</th>
<th>Fail rate</th><th>n</th><th>Variance measurable</th><th>Rationale</th>
</tr></thead>
<tbody>${rows}</tbody>
</table>`;
for (const row of wrap.querySelectorAll('.results-model-row')) {
row.addEventListener('click', () => selectModel(row.dataset.model));
row.addEventListener('keydown', onActivateKey(() => selectModel(row.dataset.model)));
}
document.getElementById('results-raw-1').textContent = JSON.stringify(data, null, 2);
}
function selectModel(model) {
Console.results.model = model;
Console.results.seed = null;
renderSeedRows(model);
document.getElementById('results-level-2').hidden = false;
document.getElementById('results-level-3').hidden = true;
}
// The reports array in results.json carries no seed number of its own --
// episodes that never finished (failed/aborted) simply have no entry, so a
// naive positional zip against 1..seed_count would misalign after any
// failure. Instead: walk seeds in order and only consume the next report
// when the run snapshot says that seed's episode actually finished. Report
// order is guaranteed to match ascending finished-seed order because
// runs.py submits futures model-major/seed-minor and awaits fut.result()
// in that same submission order.
function zipSeedsToReports(model, seedCount, episodes, reports) {
const slug = slugModel(model);
const queue = [...reports];
const rows = [];
for (let seed = 1; seed <= seedCount; seed++) {
const key = `${slug}/seed${seed}`;
const status = episodes[key] || 'unknown';
const base = statusBase(status);
const report = base === 'finished' ? queue.shift() : null;
rows.push({ seed, key, status, base, report });
}
return rows;
}
function renderSeedRows(model) {
const { data, statusSnap, runId } = Console.results;
const modelData = data[model];
const seedCount = (statusSnap && statusSnap.request && statusSnap.request.seed_count) ||
(Console.live && Console.live.contract.sweep.seeds) || 0;
const episodes = (statusSnap && statusSnap.episodes) || {};
const rows = zipSeedsToReports(model, seedCount, episodes, modelData.reports);
Console.results.seedRows = rows;
document.getElementById('results-level-2-title').textContent = model;
const wrap = document.getElementById('results-seed-table-wrap');
let body = '';
for (const r of rows) {
if (r.report) {
body += `
<tr class="results-seed-row" data-seed="${r.seed}" tabindex="0" role="button">
<td class="mono">${r.seed}</td>
<td class="mono num">${r.report.final.toFixed(3)}</td>
<td class="mono num">${r.report.outcome.toFixed(3)}</td>
<td class="mono num">${r.report.process.toFixed(3)}</td>
<td class="mono num">${r.report.fp_rate.toFixed(3)}</td>
<td>${r.report.auto_fail ? '<span class="stat bad">auto-fail</span>' : ''}</td>
<td>${fmtPassed(r.report.passed)}</td>
<td class="mono" data-seed-forced="${r.seed}">…</td>
</tr>`;
} else {
body += `
<tr class="results-seed-row results-seed-row-noreport" data-seed="${r.seed}">
<td class="mono">${r.seed}</td>
<td colspan="5" class="mono">${escapeHtml(r.status)}</td>
<td></td>
<td class="mono" data-seed-forced="${r.seed}">…</td>
</tr>`;
}
}
wrap.innerHTML = `
<table class="results-table">
<thead><tr>
<th>Seed</th><th>Final</th><th>Outcome</th><th>Process</th><th>FP rate</th>
<th>Auto-fail</th><th>Passed</th><th>Forced submit</th>
</tr></thead>
<tbody>${body}</tbody>
</table>`;
for (const row of wrap.querySelectorAll('.results-seed-row:not(.results-seed-row-noreport)')) {
row.addEventListener('click', () => selectSeed(model, Number(row.dataset.seed)));
row.addEventListener('keydown', onActivateKey(() => selectSeed(model, Number(row.dataset.seed))));
}
document.getElementById('results-raw-2').textContent = JSON.stringify(modelData, null, 2);
document.getElementById('results-breakdown').innerHTML = '';
document.getElementById('results-breakdown').hidden = true;
for (const r of rows) {
if (r.base === 'queued') continue;
fillForcedSubmitMarker(runId, model, r.seed);
}
}
function isForcedSubmit(traj) {
if (!traj.length) return false;
const last = traj[traj.length - 1];
return last.tool === 'submit' && typeof last.reasoning_trace === 'string' &&
last.reasoning_trace.startsWith('FORCED');
}
async function fetchTrajectory(runId, model, seed) {
const slug = slugModel(model);
const key = `${slug}/seed${seed}`;
const cached = (Console.live && Console.live.trajCache[key]) ||
Console.results.trajCache[key];
if (cached) return cached;
const resp = await fetch(`/api/runs/${runId}/episodes/${slug}/${seed}/trajectory`);
if (!resp.ok) throw new Error(`trajectory unavailable (HTTP ${resp.status})`);
const traj = await resp.json();
Console.results.trajCache[key] = traj;
return traj;
}
async function fillForcedSubmitMarker(runId, model, seed) {
const cell = document.querySelector(`[data-seed-forced="${seed}"]`);
try {
const traj = await fetchTrajectory(runId, model, seed);
if (cell) cell.textContent = isForcedSubmit(traj) ? 'forced' : 'no';
} catch {
if (cell) cell.textContent = '-';
}
}
// End-of-run score breakdown, adapted from the spec player's final panel:
// component rows weighted per the task's real weights (from the run's own
// contract, not guessed), fp_rate, auto_fail, and the fabrications list.
// The rationale value is always the literal redaction string the backend
// writes into results.json ("pending judge calibration").
function renderScoreBreakdown(report) {
const wrap = document.getElementById('results-breakdown');
if (!report) {
wrap.hidden = true;
wrap.innerHTML = '';
return;
}
const weights = (Console.results.statusSnap &&
Console.results.statusSnap.contract.scoring.weights) || { outcome: 0, process: 0, rationale: 0 };
const pct = (x) => (x * 100).toFixed(0);
const fabrications = report.fabrications || [];
wrap.hidden = false;
wrap.innerHTML = `
<div class="box breakdown">
<div class="wbar">
<i style="width:${pct(weights.outcome)}%;background:var(--l2)"></i>
<i style="width:${pct(weights.process)}%;background:var(--l3)"></i>
<i style="width:${pct(weights.rationale)}%;background:var(--accent)"></i>
</div>
<table class="results-table">
<thead><tr><th>Component</th><th>Weight</th><th>Score</th></tr></thead>
<tbody>
<tr><td>Outcome</td><td class="mono num">${pct(weights.outcome)}%</td><td class="mono num">${report.outcome.toFixed(3)}</td></tr>
<tr><td>Process</td><td class="mono num">${pct(weights.process)}%</td><td class="mono num">${report.process.toFixed(3)}</td></tr>
<tr><td>Rationale</td><td class="mono num">${pct(weights.rationale)}%</td><td class="rationale-cell">${escapeHtml(report.rationale)}</td></tr>
<tr><td><b>Final</b></td><td></td><td class="mono num"><b>${report.final.toFixed(3)}</b></td></tr>
</tbody>
</table>
<div class="kvline mono">
<span>fp_rate ${report.fp_rate.toFixed(3)}</span>
<span class="stat ${report.auto_fail ? 'bad' : 'ok'}">${report.auto_fail ? 'auto-fail' : 'no auto-fail'}</span>
</div>
<div class="lab" style="margin-top:8px">Fabrications</div>
<div class="dials">${fabrications.length
? fabrications.map((f) => `<span class="dial mono">${escapeHtml(f)}</span>`).join('')
: '<span class="fieldnote">none</span>'}</div>
</div>`;
}
async function selectSeed(model, seed) {
Console.results.seed = seed;
const runId = Console.results.runId;
const row = (Console.results.seedRows || []).find((r) => r.seed === seed);
renderScoreBreakdown(row && row.report);
let traj;
try {
traj = await fetchTrajectory(runId, model, seed);
} catch (e) {
const wrap = document.getElementById('results-step-table-wrap');
wrap.innerHTML = `<p class="note note-error">${escapeHtml(e.message)}</p>`;
document.getElementById('results-level-3-title').textContent = `${model} / seed ${seed}`;
document.getElementById('results-level-3').hidden = false;
return;
}
renderStepTimeline(model, seed, traj);
document.getElementById('results-level-3').hidden = false;
}
function renderStepTimeline(model, seed, traj) {
document.getElementById('results-level-3-title').textContent = `${model} / seed ${seed}`;
const wrap = document.getElementById('results-step-table-wrap');
wrap.innerHTML = '';
const table = document.createElement('table');
table.className = 'results-table';
table.innerHTML = '<thead><tr><th>Step</th><th>Tool</th><th>Args</th><th>Status</th></tr></thead>';
const tbody = document.createElement('tbody');
for (const rec of traj) {
const tr = document.createElement('tr');
tr.className = 'step-timeline-row' + (rec.ok ? '' : ' step-timeline-row-error');
tr.tabIndex = 0;
tr.setAttribute('role', 'button');
const argsStr = JSON.stringify(rec.args);
const summary = argsStr.length > 80 ? `${argsStr.slice(0, 80)}…` : argsStr;
const tdStep = document.createElement('td');
tdStep.className = 'mono';
tdStep.textContent = `#${rec.step_idx}`;
const tdTool = document.createElement('td');
tdTool.textContent = rec.tool;
const tdArgs = document.createElement('td');
tdArgs.className = 'mono';
tdArgs.textContent = summary;
const tdOk = document.createElement('td');
const badge = document.createElement('span');
badge.className = `badge ${rec.ok ? 'badge-ok' : 'badge-error'}`;
badge.textContent = rec.ok ? 'ok' : 'error';
tdOk.appendChild(badge);
tr.append(tdStep, tdTool, tdArgs, tdOk);
tr.addEventListener('click', () => toggleStepDetail(tr, rec));
tr.addEventListener('keydown', onActivateKey(() => toggleStepDetail(tr, rec)));
tbody.appendChild(tr);
}
table.appendChild(tbody);
wrap.appendChild(table);
document.getElementById('results-raw-3').textContent = JSON.stringify(traj, null, 2);
}
function toggleStepDetail(tr, rec) {
const next = tr.nextElementSibling;
if (next && next.classList.contains('step-detail-row')) {
next.remove();
return;
}
const table = tr.closest('table');
const openDetail = table.querySelector('.step-detail-row');
if (openDetail) openDetail.remove();
const detailRow = document.createElement('tr');
detailRow.className = 'step-detail-row';
const td = document.createElement('td');
td.colSpan = 4;
const pre = document.createElement('pre');
pre.textContent = JSON.stringify({ args: rec.args, observation: rec.observation }, null, 2);
td.appendChild(pre);
detailRow.appendChild(td);
tr.after(detailRow);
}
// ---- raw JSON toggles (all three result levels) ----------------------------
const RAW_TOGGLE_WRAP_ID = {
1: 'results-model-table-wrap',
2: 'results-seed-table-wrap',
3: 'results-step-table-wrap',
};
function onRawToggle(btn) {
const level = btn.dataset.level;
const pre = document.getElementById(`results-raw-${level}`);
const wrap = document.getElementById(RAW_TOGGLE_WRAP_ID[level]);
const showingRaw = !pre.hidden;
pre.hidden = showingRaw;
wrap.hidden = !showingRaw;
btn.textContent = showingRaw ? 'Raw JSON' : 'Rendered';
}
// step-budget range/number pair is wired here (not via renderKnobs) since it
// isn't a task knob -- it's a top-level RunRequest field with no server-side
// min/max, just budgets.py's prefill.
document.getElementById('step-budget-range').addEventListener('input', (e) =>
onStepBudgetChange(Number(e.target.value)));
document.getElementById('step-budget-num').addEventListener('input', (e) =>
onStepBudgetChange(Number(e.target.value)));
Console.init();