Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 25 additions & 5 deletions metainfer/tasks/evolve_kernel/static/ok-detail.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { html } from "htm/preact";
import { useCallback, useEffect, useState } from "preact/hooks";
import { StateGraph } from "app/ok-state-graph";
import { KernelLibrary, formatMs } from "app/ok-kernel-library";
import { RetrospectiveModal } from "app/ok-retrospective-modal";
import { AgentsPanel } from "app/agents-panel";
import { Timeline } from "app/timeline";
import { fmtDur } from "app/utils";
Expand Down Expand Up @@ -144,7 +145,7 @@ const ITER_STATUS_CLASS = {
running: "yellow", // in flight — no result yet, but nothing broke either
};

function IterationProgress({ iterations }) {
function IterationProgress({ iterations, selectedN, onSelect }) {
const rows = iterations || [];
if (rows.length === 0) {
return html`<p class="muted">No iterations recorded yet.</p>`;
Expand All @@ -167,7 +168,7 @@ function IterationProgress({ iterations }) {
<thead>
<tr>
<th>#</th><th>Status</th><th>Exec Time</th><th>Speedup</th>
<th>Library</th><th>Duration</th><th>Goal</th>
<th>Library</th><th>Duration</th><th>Goal</th><th>Retro</th>
</tr>
</thead>
<tbody>
Expand All @@ -178,7 +179,10 @@ function IterationProgress({ iterations }) {
// coerce rather than trusting the type (a string "1.02" is fine).
const speedup = perf.speedup == null ? null : Number(perf.speedup);
return html`
<tr key=${it.iteration}>
<tr key=${it.iteration}
class=${it.iteration === selectedN ? "selected" : ""}
style=${{ cursor: onSelect ? "pointer" : "default" }}
onClick=${() => onSelect && onSelect(it.iteration)}>
<td><strong>${it.iteration}</strong></td>
<td><span class=${"ok-badge " + (ITER_STATUS_CLASS[it.status] || "")}>
${it.status || "—"}</span></td>
Expand All @@ -192,6 +196,11 @@ function IterationProgress({ iterations }) {
<td class="ok-goal" title=${it.goal || ""}>
${it.goal || (it.failure_reason ? "failed: " + it.failure_reason : "—")}
</td>
<td class="ok-retro-cell">
${it.retrospective_path
? html`<span class="ok-retro-dot" title="retrospective available">●</span>`
: html`<span class="muted">—</span>`}
</td>
</tr>`;
})}
</tbody>
Expand Down Expand Up @@ -316,6 +325,7 @@ export default function OptKernelDetailView({
}) {
const [selectedKernel, setSelectedKernel] = useState(null);
const [showHarness, setShowHarness] = useState(null); // "correctness" | "perf" | null
const [selectedIter, setSelectedIter] = useState(null); // iteration whose retrospective is open
const { timeline, agents, loadState, lastErr } = data;
const rt = useRuntimeData(taskId);

Expand All @@ -336,10 +346,13 @@ export default function OptKernelDetailView({
<section class="panel ok-panel-full">
<h2>Iteration Progress
<span class="muted" style="font-size:0.75rem;margin-left:0.5rem;">
(▲ = new best exec time)
(▲ = new best exec time · click a row for its retrospective)
</span>
</h2>
<${IterationProgress} iterations=${rt.iterations} />
<${IterationProgress}
iterations=${rt.iterations}
selectedN=${selectedIter}
onSelect=${(n) => setSelectedIter(n)} />
</section>

<div class="ok-grid">
Expand Down Expand Up @@ -402,5 +415,12 @@ export default function OptKernelDetailView({
<h2>Event Timeline</h2>
<${Timeline} events=${timeline.events} />
</section>

${selectedIter != null ? html`
<${RetrospectiveModal}
taskId=${taskId}
iteration=${selectedIter}
onClose=${() => setSelectedIter(null)} />
` : null}
`;
}
66 changes: 66 additions & 0 deletions metainfer/tasks/evolve_kernel/static/ok-retrospective-modal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// Retrospective modal for evolve-kernel.
//
// Runs are driven by an LLM loop, and each iteration writes a short prose
// retrospective (what it tried, what it measured, what to do next). That is
// the one artifact a human can read to judge *why* a run is where it is, but
// until now the detail page never surfaced it. Rows in the Iteration Progress
// table open this modal; the backend returns a placeholder when an iteration
// has no retro file, so the modal is always informative.

import { html } from "htm/preact";
import { useEffect, useState } from "preact/hooks";
import { marked } from "marked";
import { getRetrospective } from "app/ok-runtime-api";

export function RetrospectiveModal({ taskId, iteration, onClose }) {
const [data, setData] = useState(null);
const [err, setErr] = useState(null);

useEffect(() => {
if (taskId == null || iteration == null) return;
let alive = true;
setData(null);
setErr(null);
getRetrospective(taskId, iteration)
.then((d) => { if (alive) setData(d); })
.catch((e) => { if (alive) setErr(e); });
return () => { alive = false; };
}, [taskId, iteration]);

useEffect(() => {
const onKey = (e) => { if (e.key === "Escape") onClose && onClose(); };
if (iteration != null) document.addEventListener("keydown", onKey);
return () => document.removeEventListener("keydown", onKey);
}, [iteration, onClose]);

if (iteration == null) return null;

let body;
if (err) {
body = html`<p style="color:var(--err)">failed to load retrospective: ${String(err)}</p>`;
} else if (!data) {
body = html`<p class="muted">loading…</p>`;
} else {
const rendered = marked.parse(data.markdown || "");
body = html`<div class="retro-md" dangerouslySetInnerHTML=${{ __html: rendered }}></div>`;
}

return html`
<div class="modal-overlay open" onClick=${(e) => {
if (e.target === e.currentTarget) onClose && onClose();
}}>
<div class="modal">
<div class="modal-header">
<h3>
Iteration ${iteration} retrospective
${data && !data.has_retrospective
? html`<span class="ok-badge yellow">no retro file</span>`
: null}
</h3>
<button class="close" onClick=${onClose} title="close (Esc)">×</button>
</div>
<div class="modal-body">${body}</div>
</div>
</div>
`;
}
10 changes: 10 additions & 0 deletions metainfer/tasks/evolve_kernel/static/ok.css
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,16 @@
overflow: auto;
}

/* "Retro" column: a dot marks iterations that wrote a retrospective, which is
also the affordance that a row opens the retrospective modal. */
.ok-retro-cell {
text-align: center;
}

.ok-retro-dot {
color: var(--accent, #58a6ff);
}

.ok-flow-indicator {
display: flex;
align-items: center;
Expand Down
82 changes: 82 additions & 0 deletions metainfer/tasks/evolve_kernel/tests/test_retrospective.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
"""Contract for the reader behind the WebUI retrospective modal.

``static/ok-detail.js`` makes each Iteration Progress row open a modal that
fetches ``/iterations/{n}/retrospective`` (``read_retrospective``). Two things
have to hold for that to be safe to click:

* the reader must return the markdown of the iteration's ``retrospective.md``,
together with that iteration's perf and the previous one's — the modal shows
the prose, and the two perf maps are what make it a *retro*spective rather
than a bare file dump; and
* when an iteration has no retro file (a failed or still-running iteration) the
reader must still return a non-empty, informative payload instead of raising,
so clicking any row is harmless.

The "Retro" column marker is derived from ``read_iterations``, which must carry
the row's ``retrospective_path`` through unchanged.
"""

from __future__ import annotations

import json

from metainfer.tasks.evolve_kernel.server import _state_readers as sr


def _write_iteration(state_dir, n, **fields):
# Mirrors the writer: IterationRecord.to_dict() always drops every field on
# disk, so fixtures pass the ones the UI reads explicitly.
iters = state_dir / "iterations"
iters.mkdir(parents=True, exist_ok=True)
(iters / f"{n:03d}.json").write_text(
json.dumps({"iteration": n, **fields}), encoding="utf-8")


def test_read_iterations_exposes_retrospective_path(tmp_path):
_write_iteration(tmp_path, 1, status="success",
retrospective_path="/tmp/retro_001.md")
_write_iteration(tmp_path, 2, status="success", perf={"exec_time_ms": 0.12})

rows = sr.read_iterations(tmp_path)

assert [r["iteration"] for r in rows] == [1, 2]
# Marker column: iteration 1 has a retro, iteration 2 does not.
assert rows[0]["retrospective_path"] == "/tmp/retro_001.md"
assert rows[1].get("retrospective_path") is None


def test_read_retrospective_reads_markdown_and_perf_delta(tmp_path):
retro = tmp_path / "retro_002.md"
retro.write_text("# Iteration 2\n\nDouble-buffering won.", encoding="utf-8")
_write_iteration(tmp_path, 1, perf={"exec_time_ms": 0.20, "speedup": 1.00})
_write_iteration(tmp_path, 2, status="success",
perf={"exec_time_ms": 0.12, "speedup": 1.67},
retrospective_path=str(retro))

out = sr.read_retrospective(tmp_path, 2)

assert out["has_retrospective"] is True
assert "Double-buffering won." in out["markdown"]
assert out["this_perf"]["speedup"] == 1.67
assert out["prev_perf"]["speedup"] == 1.00
assert out["iteration"] == 2


def test_read_retrospective_placeholder_when_no_file(tmp_path):
# A failed iteration never writes a retro file; the modal must still have
# something to show rather than an empty body or an error.
_write_iteration(tmp_path, 1, status="failed", failure_reason="harness crashed")

out = sr.read_retrospective(tmp_path, 1)

assert out["has_retrospective"] is False
assert out["markdown"]
assert out["iteration"] == 1


def test_read_retrospective_missing_iteration(tmp_path):
out = sr.read_retrospective(tmp_path, 7)

assert out["has_retrospective"] is False
assert out["markdown"]
assert out["iteration"] == 7
Loading