Generate markup, render it, compare the render to the target, correct, repeat. Every screenshot-to-code demo has that loop. Three things decide whether it is worth running, and none of them are the model.
Repo: https://github.com/dev48v/render-loop — PUBLIC, MIT, 25 tests, standard library only, no browser.
The whole loop runs in your browser: https://dev48.infy.uk/agentlab/vol2-01-render-loop.html
1. A pixel diff cannot converge
Two renders can be structurally identical and differ on thousands of pixels — a font advanced differently, an edge landed on a half-pixel. So the score never reaches zero.
Worse than the floor is the saturation. Once a candidate is wrong enough that its boxes no longer overlap the target's, further error changes the pixel score by almost nothing:
| error | structural | pixel | pixel step |
|---|---|---|---|
| +16 | 3.20 | 0.2946 | +0.0179 |
| +24 | 6.70 | 0.3125 | +0.0179 |
| +34 | 10.20 | 0.3214 | +0.0089 |
| +48 | 13.70 | 0.3293 | +0.0079 |
Structural keeps climbing. Pixel has flattened to steps of 0.008 — a 441× difference in remaining signal. A loop driven by that number is taking random walks with extra steps.
The structural comparison matches boxes to boxes and reaches exactly zero when the layouts agree, which is the property that lets a loop terminate at all.
assert structural_diff(boxes, boxes).value == 0.0
total += (missing + extra) * 10.0 # a whole missing element is not a nudge
That * 10 matters: a score that ranks "wrong size" and "absent entirely" the same will happily delete things to reduce error.
2. Best-so-far, not last
Every correction can make things worse. A loop that returns its final attempt returns whatever its last mistake produced.
report = run_loop(TARGET, scripted([good, awful, awful]), patience=5)
assert report.best_source == good
assert report.attempts[-1].score > report.best_score # 51.13 vs 0.25
One variable. It is the cheapest correctness property in the project, and it is missing from most implementations because on a good run the two are the same.
3. The generator never sees the answer
Easier to get wrong by accident than it sounds — one shared object is enough. The proposer receives the previous source, its rendered boxes, and a score. Never the target markup.
def test_the_generator_never_receives_the_target_source():
run_loop(TARGET, spy, max_attempts=2)
assert TARGET not in seen # only the previous source
assert seen[0] == "" # and the first call starts from nothing
There is a peeking() generator in the repo that is handed the answer. It exists purely as a control: it matches on the first attempt, and nothing else in the suite is allowed to. A test that the honest loop is honest means very little without one showing what cheating looks like.
Five outcomes, and it always names one
matched · no_improvement · budget · gave_up · unrenderable
An unrenderable target stops before a single candidate is generated — no signal to work against means any output is a guess. Unrenderable output is a failed attempt rather than a crash, because a generator emitting broken markup is the normal case.
And the shipped default generator cannot solve a general target, on purpose, so python -m render_loop ends in an honest no_improvement rather than a demo tuned to its own example.
Agent Lab Vol 2 opens here. Vol 1 was five tools that knew when to stop; Vol 2 starts with one that knows when its own feedback signal is lying to it: https://dev48.infy.uk/agentlab.php
Top comments (0)