# SPDX-License-Identifier: BSD-3-Clause """Render a rollout trace as a single HTML page you can scroll through. A JSONL trace answers "what was the reward"; it does not show you *why*. This lays each episode out as a filmstrip: the view the model was looking at, what it said, the action it chose, what the environment replied, and the running cost — ending with the reveal and the score. Usage: python scripts/render_trace.py rollouts/anthropic_agentic python scripts/render_trace.py rollouts/hf_agentic --out qwen.html """ from __future__ import annotations import argparse import base64 import html import json import pathlib def _data_uri(path: pathlib.Path) -> str: """Inline an image so the report is a single portable file.""" if not path.exists(): return "" mime = "image/png" if path.suffix == ".png" else "image/jpeg" return f"data:{mime};base64,{base64.b64encode(path.read_bytes()).decode()}" def _fmt_distance(km: float | None) -> str: if km is None: return "—" return f"{km * 1000:.0f} m" if km < 10 else f"{km:,.0f} km" def _verdict(km: float | None) -> str: if km is None: return "no guess" if km < 0.025: return "perfect" if km < 25: return "pinpoint" if km < 200: return "close" if km < 1500: return "right region" return "wrong continent" def render(trace_dir: pathlib.Path, out: pathlib.Path) -> None: """Write one HTML report for a trace directory.""" records = [ json.loads(line) for line in (trace_dir / "trace.jsonl").read_text().splitlines() if line.strip() ] summary_path = trace_dir / "summary.json" summary = json.loads(summary_path.read_text()) if summary_path.exists() else {} episodes = [] for record in records: turns = [] for row in record.get("trace", []): image = "" if row.get("image"): image = _data_uri(trace_dir / row["image"]) action = row.get("action") or {} kind = ( action.get("action", "reset") if isinstance(action, dict) else str(action) ) args = ( ", ".join( f"{key}={value}" for key, value in action.items() if key != "action" ) if isinstance(action, dict) else "" ) turns.append( { "turn": row.get("turn"), "kind": kind, "args": args, "reply": row.get("reply") or "", "feedback": row.get("feedback") or "", "steps": row.get("steps_remaining"), "cost": row.get("action_cost"), "image": image, "image_kind": row.get("image_kind", "none"), } ) episodes.append({"record": record, "turns": turns}) parts = [ _HEAD.replace( "__TITLE__", html.escape(str(summary.get("model", trace_dir.name))) ) ] parts.append(_summary_block(summary, records)) for episode in episodes: parts.append(_episode_block(episode)) parts.append("") out.write_text("".join(parts)) print(f"wrote {out} ({out.stat().st_size / 1e6:.1f} MB, {len(episodes)} episodes)") def _summary_block(summary: dict, records: list[dict]) -> str: rows = "".join( f"
{html.escape(label)}{html.escape(str(value))}
" for label, value in [ ("model", summary.get("model", "—")), ("mode", summary.get("mode", "—")), ("episodes", summary.get("episodes", len(records))), ("mean reward", f"{summary.get('mean_reward', 0):.3f}"), ("median distance", _fmt_distance(summary.get("median_distance_km"))), ("within 200 km", f"{summary.get('within_200km', 0)}/{len(records)}"), ("parsed", f"{summary.get('parsed', 0)}/{len(records)}"), ("wall clock", f"{summary.get('wall_clock_s', 0):.1f}s"), ] ) return f"

rollout trace

{rows}
" def _episode_block(episode: dict) -> str: record = episode["record"] km = record.get("distance_km") reward = record.get("reward") or 0.0 head = ( f"

episode {record.get('task_index')} " f"{html.escape(str(record.get('country', '')))}" f"{_verdict(km)}" f"{_fmt_distance(km)}" f"reward {reward:.3f}" f"{record.get('turns', 0)} turns" f"{record.get('latency_s', 0):.1f}s

" ) cards = [] for turn in episode["turns"]: image = ( f"observation at turn {turn[" if turn["image"] else "
no image
" ) reply = html.escape(turn["reply"][:1400]) meta = [] if turn["steps"] is not None: meta.append(f"{turn['steps']} steps left") if turn["cost"] is not None: meta.append(f"cost {turn['cost']:.2f}") cards.append( f"""
{image}
{html.escape(str(turn["kind"]))} {html.escape(turn["args"])} {html.escape(" · ".join(meta))}
{html.escape(turn["feedback"])}
{f"
model reply
{reply}
" if reply else ""}
""" ) return f"
{head}
{''.join(cards)}
" _HEAD = """ __TITLE__ — rollout trace """ def main() -> None: """Command-line entry point.""" parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("trace_dir", type=pathlib.Path) parser.add_argument("--out", type=pathlib.Path) args = parser.parse_args() out = args.out or (args.trace_dir / "trace.html") render(args.trace_dir, out) if __name__ == "__main__": main()