Back to Publications
Human LearningSeptember 3, 2026Implementation Report

Explained at Five Levels: Generating Verified Interactive Explainers with a Research Agent

AI LearningKnowledge SystemsExplainersResearch Agents

Model Version

Explained at Five Levels: Generating Verified Interactive Explainers with a Research Agent

Executive summary

Our deep research agent turns a question into a citation-backed, verifier-audited paper built on an append-only evidence ledger. --format five-levels turns the same research into a different artifact: one standalone interactive HTML page, "topic, explained at five levels," in the spirit of WIRED's "5 Levels" video series. One expert persona explains one topic to a child, a teenager, an undergraduate, a PhD student, and a peer. Every level carries prose verified against the ledger — levels 3 to 5 must cite it — and a bespoke D3 or canvas panel the reader can operate.

The pipeline researches the question into a ledger, designs a running example, a palette, a shared data model, and five level plans in one structured call, writes the five levels in parallel, assembles them into a fixed HTML shell, syntax-checks each panel with node --check, exercises the assembled page in headless Chrome, and sends failing levels back to their writer for at most two rounds. The prose of all five levels is then verified claim by claim against the ledger. A publish gate blocks import if any of that did not happen, and opens the pull request as a draft.

Three pages ship with this report: knowledge graphs, human memory, and sorting and searching. Across the three runs the verifier checked 60 claims — 52 supported, 8 partial, none unsupported or contradicted — from 88 evidence records over 22 distinct sources, and the browser exercised 90 controls across 15 panels with no failures. End to end, a page cost between 293 and 576 wall-clock seconds and roughly 180,000 input and 57,000 output tokens.

The format

WIRED's "5 Levels" series puts one expert in front of five people of increasing expertise to explain the same idea to each. What makes it work is not the expert getting more technical; it is watching the same idea get rebuilt. The child's version is not wrong; it is soft.

We kept that structure. The audience ladder in explainer/schemas.py is fixed: a child (age seven), a teenager, an undergraduate (knows the standard tools), a PhD student (wants the open problems), and a peer ("no analogies survive this level"). The model names the rungs for the topic, but as the comment above them puts it, "the rungs never move: that is what makes the format a format."

The pedagogical core is one running example, five rebuilds. The design prompt requires "ONE running example threads through all five levels — a single concrete thing a child can hold and an expert can still argue about," and that "Each level REBUILDS what the previous one simplified. Level 1 is honest but soft; level 5 admits which parts of level 4 are folklore." The design also writes shared_data_js, the example's data model as JavaScript constants every panel uses, "so the child's toy and the expert's model are the same objects."

The panel ladder escalates by mechanism, not by decoration. Level 1 is a tactile toy; level 2 shows structure and vocabulary; level 3 is precise mechanics, a small engine with real notation; level 4 is the research problem, run or measured live with a number that moves; level 5 is the frontier — a geometry, a bound, a proof sketch, a competing-models comparison, or a failure case the reader can construct.

An interactive panel per level is why this format belongs to the Human Learning and Knowledge Systems pillar, whose premise is plain: knowledge that nobody consumes is wasted. A paper is read once, by the fraction of people who read papers. A passage that answers a question the reader poses, by letting them change an input and watch the output, is consumed. The shell contract makes that a rule: "Interactions must teach the level's idea. Decoration is not interaction." A dead button is a defect, and the validator treats it as one.

The prompts describe a format, never a subject. They carry worked examples that deliberately span unrelated fields — memory, sorting, encryption, photosynthesis, supply chains, prompt injection — and three tests hold that line: test_prompts_are_topic_neutral and test_schema_descriptions_are_topic_neutral fail if any discipline reads as the default, and test_rail_icons_are_not_graph_glyphs keeps the page furniture from belonging to one kind of topic. The forceGraph helper is available and explicitly optional: "do not bend a topic into a graph to use it."

Anatomy of a page

A produced page is a single HTML file with no build step and no server. It contains:

  • A masthead — title, deck, and, when a persona was requested, "Explained by …".
  • A rail of five tabs, one per level, each with an audience label and detail ("A PhD student" / "cognitive neuroscience").
  • Five level sections, one visible at a time. Each is two columns: prose on the left, an interactive panel on the right. The panel is a title bar, a stage of at least 340 px in a 6:5 aspect, optional body controls, and an optional legend.
  • Footnote citations. Sources are numbered by URL in order of first appearance from level 1 to level 5, so one paper keeps one number across the whole page; each [E:id] in the prose becomes a superscript linking to the Sources list.
  • A sources and verification appendix — every URL the page drew on, and every extracted claim with its verdict and the verifier's note.

The shell is code and the levels are content. explainer/shell.py owns the page: masthead, rail, tabs, CSS, panel chrome, the forceGraph helper, footnotes, the appendix, iframe messaging, and the sanitizer for model markup. The model writes, per level, a heading, prose_html, the panel's control and readout markup, and panel_js, which becomes the body of function initLevelN(). Each level's code is emitted in its own <script> block, so a failure in one level is contained to that level's stage and reported there, rather than silencing the page.

The writer knows the shell only through SHELL_CONTRACT in prompts.py: the globals it may use, the CSS classes already styled, and hard rules — ids prefixed l{n}-, wire every control, a rendered <svg> or <canvas> in the stage, no network, nothing may throw, no blocking loops.

The pipeline

question
  → scope → plan ─(Send×N)→ researcher → coverage ─(Send×M | design)   ← shared with papers
  → design            (reason lane)   running example, palette, shared_data_js, five LevelPlans
  → write_level ×5    (reason lane)   prose citing [E:id] + panel markup + panel_js, in parallel
  → assemble                          render into the shell
  → validate_panels                   node --check per level, then headless Chrome
        ─(Send×k, ≤ MAX_REPAIR_ROUNDS=2)→ repair_level → assemble → validate_panels
  → verify            (verifier lane) claims across all five levels vs the ledger
  → finalize                          page + appendix + publish gate

Research is the paper graph's own. add_research_phase in deep_research/graph.py wires scope, plan, the parallel researchers, and the coverage gate; the paper graph continues to synthesize, the explainer graph to design. URL policy, untrusted-content handling, budgets, and the ledger apply unchanged. The research prompt fingerprint (4b9087b9e9c6) is identical across the three published runs and the paper runs from 2026-08-03: the explainer format changes what is done with the evidence, not how it is gathered.

Design is one structured call. The reason lane returns an ExplainerDesign: title, persona, deck, running example, palette, shared_data_js, and exactly five LevelPlans, each with an audience, heading, what this level rebuilds, key ideas, a PanelConcept, and the evidence ids it will lean on. The node syntax-checks shared_data_js and re-asks once if it does not parse, drops evidence ids absent from the ledger, and overrides the model's persona with the requested one.

Levels are written in parallel, and one cannot kill the page. Five write_level tasks each make one structured call. A writer that raises is stubbed with a visible failure paragraph and routed to repair rather than aborting the run. The plan's heading overrides the writer's, so the ladder stays intact.

Repair is targeted. Only failing levels go back to the reason lane, with the specific error, for at most two rounds. Levels that passed are carried through byte-identical.

Two schemas face the model: ExplainerDesign (one call) and LevelContent (one per level, plus one per repaired level). Size guidance lives in the prompts — the design in about 6,000 tokens with shared_data_js at most about 60 lines and 2,500 characters; a level in about 9,000 tokens.

Two offline tools operate on a finished run. python -m deep_research.explainer.repair runs/<id> [--rounds 2] [--export] reloads a run's artifacts, re-runs the validate-and-repair loop, and recomputes validation and the gate. rerender rebuilds the page through the current shell.py with no model calls, carrying gate and manifest over unchanged: a presentation re-render, not a re-verification.

Verification and the publish gate

combined_draft flattens the five levels' prose, citation tokens intact, into one markdown document, and the paper graph's own verify node returns a verdict per claim against the ledger's quotes. Levels 3 to 5 must cite every material claim; levels 1 and 2 need not, because an analogy is not an assertion about the literature.

observability/manifest.py::publish_gate blocks when verification did not run, when it extracted no claims, when any claim was judged unsupported or contradicted, or when the draft cited evidence ids absent from the ledger. partial is deliberately not blocking: it marks a wording problem for the reviewer to see, not an unsupported assertion.

explainer/graph.py::validation_blockers adds the format's own blockers: panels not validated because validation did not run, because the validator was unavailable, or because one or more panels failed — each reported as a sentence naming the level and its first error.

A blocked gate has three consequences. cli.py --import-paper refuses with exit 3 unless --allow-unverified; the pull request opens as a draft; and the page itself prints "Publication blocked pending human review."

Browser validation

scripts/validate-explainer.mjs drives the installed Google Chrome through playwright-core on the chrome channel. Before the browser opens, validate.py::syntax_check wraps each level's panel_js in a function, runs node --check, and on failure returns the error with the line renumbered relative to the panel code — a bracket error is caught in milliseconds rather than as a page that renders nothing.

In the browser, the validator checks that window.d3 loaded and that exactly five rail tabs exist, then per level: clicks the tab; checks #l{n}-stage is laid out and contains an <svg> with at least three descendants or a <canvas> with nonzero width; operates up to 10 buttons and selects inside the panel, each with a 6,000 ms action timeout; flags a static control whose id initLevelN's source never mentions; and screenshots the level. Errors are attributed to the active level, so repair knows who to send them to.

Each level runs under a 35,000 ms budget; on overrun the level fails with a message naming a blocking main thread, and a fresh page is opened for the remaining levels, so one hung panel does not cost the report on the other four. explainer/validate.py wraps the script with a 240-second timeout and never raises: a crash maps to available=False, a timeout to available=True, ok=False. Both block the gate.

Sandbox

Model-written JavaScript ships in the page, so it is contained at three levels.

sanitize_fragment drops script, style, iframe, form, and other active elements, all on* attributes, and javascript: URLs from model markup; safe_script escapes </script so a level cannot end its own block early. The page's own CSP is:

default-src 'none'; script-src 'unsafe-inline' https://cdnjs.cloudflare.com;
style-src 'unsafe-inline' https://fonts.googleapis.com; font-src https://fonts.gstatic.com;
img-src data:; connect-src 'none'; base-uri 'none'; form-action 'none'

And ExplainedPage.jsx embeds the page in an iframe with sandbox="allow-scripts allow-popups allow-popups-to-escape-sandbox allow-top-navigation-by-user-activation".

None of that replaces the review before merge. A sandbox bounds what a panel can reach, not what it can claim.

From run to site

--export writes pipeline/imports/explained/<slug>/ with the page, a sidecar JSON, and the five screenshots. npm run import:explainer -- --file pipeline/imports/explained/<slug>/<slug>.json validates the bundle, copies the page to public/explainers/ — served at /explainers/<slug>, a path no site route uses, because the host's clean-URL rewriting would otherwise let the static file shadow the site route — and the sidecar and screenshots to public/explained/, then upserts public/explained/manifest.json with validated = available && ok. pages/explained/@slug/+Page.jsx renders the site page, prerendered from that manifest, with the standalone page embedded and sized from the height it reports.

From /admin/research, api/research/run.js dispatches research.yml with format=five-levels. The workflow runs the preflight — which for this format proves a browser can launch before any research budget is spent — then the agent, then import-explainer, and opens a pull request labeled deep-research,five-levels with the level screenshots in its body, as a draft if any gate failed. No artifact cluster is generated: the page is the artifact.

The three pages

Knowledge graphsHuman memorySorting and searching
Persona(chosen by the design)a neuroscientist who studies memorya Harvard professor of computer science
Running example"Santiago is the capital of Chile" — one edge in a tiny graph of cities, countries, people and the sources that vouch for themA song heard at a terrific party, during a great philosophical conversation with a friend — and what happens to that memory over twenty yearsEight numbered cards dealt 5, 2, 9, 1, 5♦, 6, 3, 8 — two fives so stability shows, and a target value 6 to search for
L1 panelDraw a fact (force graph)The party in a jar 🫙 (tactile toy)Tidy the hand (tactile toy)
L2 panelTriples and types (svg diagram)Three ways to study one word (structure walkthrough)Count the work (step-through with tally)
L3 panelQuery engine (mini engine)Encode–cue overlap engine (step-through experiment)Drive the algorithm (step-through interpreter)
L4 panelTrain a link predictor (live training)Measuring the gradient (simulated experiment)Measure it live (live experiment + fit)
L5 panelPoison the pipeline (mini engine)Standard model vs transformation (competing-models arena)Break the model (competing-models comparison)
Prose2,817 words3,173 words2,972 words
Panel code627 lines659 lines755 lines

The sorting page is the clearest demonstration of the ladder doing its job. Level 1 is eight cards a child drags into order. Level 2 counts the comparisons as they happen. Level 3 drives the algorithms in notation. Level 4 fits a growth curve to timings measured in the reader's own browser. Level 5 argues about branch mispredictions and skewed pivots — the same eight cards, five times, each level rebuilding what the last one smoothed over.

Read them at /explained/learning-knowledge-graphs, /explained/learning-how-human-memory-works, and /explained/learning-sorting-and-searching.

What a run costs

All three runs used the quick preset (3 subquestions, 1 round, a 900-second wall budget that was never exhausted), Tavily search, claude-sonnet-5 on the fast lane, and claude-opus-5 on the reason and verifier lanes. Every preflight passed. These are the costs of producing a page, measured from the runs that produced the three published pages.

Knowledge graphsHuman memorySorting and searchingTotal
Wall-clock seconds576.2293.4352.31,221.9
Model calls26252879
Input tokens184,248157,394189,865531,507
Output tokens59,75951,33660,966172,061
Evidence records32243288
Distinct sources86822
Claims supported / partial17 / 319 / 116 / 452 / 8
Repair rounds100—
Browser validation15,858 ms14,811 ms19,103 ms49,772 ms
Controls exercised28253790

claude-opus-5 made 67 of the 79 calls and consumed 514,601 of the 531,507 input tokens; the fast lane made 4 calls per run, 12 in total, for 2,932 output tokens. Output is where this format is heavy: five writers each return prose plus a panel of 100 to 170 lines, and output tokens per run ran from 51,336 to 60,966 — several times what a paper of comparable prose length costs, because the panels are code.

No claim in any of the three runs was judged unsupported or contradicted, and no page cited an evidence id absent from its ledger, so all three cleared the publish gate. Prices are deliberately absent: the repository records model ids and token counts, and the observability backend prices them.

One provenance note. The knowledge-graphs page was produced under an earlier revision of the explainer prompts (fingerprint ae487a6201b7) and needed one repair round; the other two ran under the current prompts (874deb875128) and passed browser validation on the first pass. The research prompt fingerprint is the same for all three.

Limitations and open problems

Verification checks prose, not panels. The verifier sees the five levels' prose flattened to text, never panel_js, shared_data_js, or the numbers a panel computes. A level-4 simulation can converge to the wrong value, and the page will render, pass validation, score full marks on its claims, and be wrong. This is the gap most likely to publish a confidently incorrect interactive.

The repair loop fixes code, not pedagogy. A level goes back only for a syntax error, a browser error, an empty stage, an unwired control, a timeout, or an unknown citation. Nothing sends a level back because its panel is dull or its running example drifted; that is caught, if at all, by the human reading the pull request.

The validator proves absence of failure, not presence of function. A control passes if clicking it does not throw or time out. A button whose handler changes nothing passes, and an empty axis satisfies the stage check.

Research limits are inherited. The research phase is the paper agent's, with the quick preset's caps of three subquestions, three fetches per subquestion, and 16,000 characters per page; PDFs are read from their text layer only, page-capped, with no OCR. Source diversity has no representation in the design: six to eight distinct sources carried a whole page in every run here.

Persona is prompt-level. The persona reaches the design and every writer, and one test follows it end to end, but nothing checks that the voice is consistent across levels, or that a "Harvard professor" is not asserting something a professor would not.

Single language, one library, one CDN. The prompts are English and nothing selects a language. Every page loads D3 7.9.0 from cdnjs.cloudflare.com; the validator checks that d3 loaded, so a CDN outage fails validation rather than shipping a blank page, but a standalone page should not need a third party at runtime.

Three pages is not an evaluation. Nothing here measures whether a reader learns more from a five-levels page than from prose, whether the ladder lands for its stated audiences, or how the format holds up on topics with no good visual — which is the question the panel ladder is most exposed to.

Reproduce it

The standalone implementation is public at github.com/binaryninja/five-levels (commit d7ece8d3be6a89120ce74a5e98d92ff3adf995eb, MIT license, Copyright (c) 2026 Jeremy Richards). It holds the agent package, the validator, three example pages under examples/, and a CI workflow running the Python tests and the validator's --check. It needs uv, Node, an installed Google Chrome, and provider keys in .env.

In this repository the same code lives in research-agent/src/deep_research/explainer/ — 2,022 lines across nine files, with shell.py (569) and graph.py (547) the largest — plus scripts/validate-explainer.mjs (308), scripts/import-explainer.mjs (179), src/pages/ExplainedPage.jsx (316), and research-agent/tests/test_explainer.py (622). The agent's 155 tests run in well under a second.

Install:

git clone https://github.com/binaryninja/five-levels
cd five-levels
uv sync --project research-agent
npm install
cp .env.example .env

Run — the memory page's exact invocation:

uv run --project research-agent deep-research "How does human memory work: how are memories encoded, consolidated, retrieved, and forgotten?" \
    --pillar learning --preset quick --format five-levels \
    --persona "a neuroscientist who studies memory" --export

Add --preflight-only to check the environment, including that a browser launches, before spending research budget. Validate any page without the agent:

node scripts/validate-explainer.mjs --check
npm run explainer:validate -- --file examples/how-human-memory-works.html --out /tmp/shots

Tests: uv run --project research-agent pytest -q.

On this site the exported bundle takes the import:explainer path described above, by hand or from /admin/research. The pipeline this format extends is described in /papers/automation-what-building-a-model-agnostic-research-pipeline-actually-took.

Provenance

This report was written from the implementation and the run artifacts, not produced by the research agent. It carries no evidence ledger or verification appendix and none of the guarantees of an agent-produced paper on this site. The three pages it describes do carry both.