An agent loop with a code-based critic
Run on its own three-runner safety panel (five plans), the critique and revise loop moved the share of plans passing all four safety checks from 1 in 5 to 2 in 5, and one of those two came back 18 weeks long for a runner with 10. Each plan cost $1.19 at API prices and took 172 seconds. In the recorded run below, asked for a 3:05 Boston off 22 miles a week, it recommended 3:20 and its critic caught five problems in the first draft. The agent is a LangGraph state machine with five nodes. It judges whether the goal is realistic, drafts a periodized week-by-week plan, and tailors it to the race and the forecast heat. Then deterministic safety checks in Python run alongside an LLM review, and a revise node fixes whatever they flag and loops back, up to a revision budget.
assess, plan, adjust, critique, revise, with a conditional edge from critique back through revise.
in plain Python: week-over-week ramp above 20%, race week above 75% of peak, long run over 22.5 miles, long run longer than the week.
The goal it was given and the target it recommended for a 16-week build from 22 miles a week.
caught by the critique node in that run's first draft, fixed in one revision, then a clean re-check.
src/eval.py (two aggressive, one realistic); five plans, Boston and New York run twiceWhy a graph
Ask a language model for a sixteen-week marathon plan and the result looks right, even when it raises mileage 30% in a week, skips the taper or prescribes a 26-mile long run. A coach would spot those errors at a glance; in fluent prose they are easy to read past.
The graph adds a place to catch them. The critique node runs
_safety_checks, which reads the plan's numbers and returns a list of problems, and
merges that with issues an LLM reviewer raises. _route sends the plan to
revise when there are issues and revisions remain (two by default), and ends the run
otherwise.
src/knowledge.py: a simulated course penalty against Berlin, about 1 minute per °F above 55°F, and the 30 km wall.critique.A recorded run
From examples/sample_run.md: Boston, goal 3:05, 16 weeks,
22 miles a week now, longest recent run 9 miles, 5 days a week, 74°F
forecast. The assessment it wrote:
A 3:05 marathon (~7:03/mi) typically requires a 40-55 mi/week base with long runs already near 16-18 miles; starting from 22 mi/week and a 9-mile longest run leaves too little runway in 16 weeks to safely build both the mileage and the speed. Targeting 3:20 is an aggressive-but-achievable stretch that lets you peak around 45 mi/week without high injury risk.
The agent trace from the same run:
• assess → feasible=False, target 3:20:00, peak 45 mpw • plan → drafted 16 weeks • adjust → tailored plan to course & weather • critique → 5 issue(s): week 5 jumps mileage >20% (23→28) • revise #1 → addressed 5 issue(s) • critique → clean
Output of src/cli.py, quoted verbatim apart from line breaks and two status symbols left out.
The trace shows only the first of the five issues; the run didn't record the other four or the first draft.
Measured
eval/measure.py runs the three runners in src/eval.py through the
same node sequence the agent uses and scores the raw draft (out of adjust) and the
final plan with the agent's own _safety_checks. It ran each runner twice and stopped
starting new plans after 34 CLI calls, so Boston and New York ran twice and Berlin once. Every
draft and final plan is saved in eval/runs.json.
Issues here are the four deterministic checks only. The critique node also merges in the LLM
review, which raised issues of its own in 11 of 14 rounds; between the two, four of five plans
ran out of revisions. The assessment is not stable either: the same Boston runner was told 3:45 in one
run and 4:15 in the other. Outcomes are hand-coded in eval/error_analysis.json.
total_cost_usd that claude -p --output-format json reports, on Opus 5.5, the CLI's default; it runs on a subscription, so nothing was billedeval/calls.csv)Try the critic
The chart holds the final plan from that run. The four checks below are
_safety_checks from src/graph.py, ported line for line to JavaScript, so
they run in your browser as you change a week. The LLM half of the critique isn't here; it needs a
model call.
Weekly miles as bars, with the long run as the narrow inner bar and the 22.5-mile long-run cap as a dashed line. Click a week, or focus the chart and use the arrow keys, then change it with the sliders.
Plan from examples/sample_run.md. Messages read as _safety_checks returns them, with a colon
where the source has a dash. The three presets are edits for trying the checks, not plans the agent produced.
The plan peaks at 57 miles in week 13, above the 45 the assessment suggested, and no check
compares the two; the ramp rule only looks one week back. The Boston-specific work comes from
adjust: downhill quad conditioning from week 3, Newton-style hill repeats in week 6,
and a 19-mile race simulation in week 12 with the hills placed at miles 15 to 19.
| Wk | Phase | Miles | Long | Focus and key sessions (verbatim) |
|---|---|---|---|---|
| 1 | Base | 28 | 10 | Establish aerobic base and early heat/hydration habits; keep non-long days truly easy, 4x strides on easy day; easy aerobic run + hydration routine practice |
| 2 | Base | 30 | 12 | Build easy volume; begin heat acclimation on warmer runs, 6x100m strides; 5 mi steady on rolling terrain |
| 3 | Base | 32 | 13 | Introduce MP feel and even-effort pacing; prime quads for downhills: 4x1 mi @ marathon pace (~7:38, controlled start); gentle downhill strides |
| 4 | Base | 29 | 11 | Step-back recovery week; light quad conditioning, easy run + strides; short downhill drills |
| 5 | Build | 34 | 14 | Add threshold work; drill controlled, even pacing with easy days between hard efforts, 5x1000m @ 10K pace; 6 mi @ MP with even splits |
| 6 | Build | 38 | 15 | Boston-specific hill strength and quad durability on descents, 4 mi tempo; rolling Newton-style hill repeats (up + down) |
| 7 | Build | 35 | 13 | Step-back recovery week; reinforce fueling/hydration on-the-move, 3 mi tempo; downhill strides + hydration practice |
| 8 | Build | 41 | 16 | Sharpen speed; practice MP under fatigue with an easy day flanking the long run, 6x800m @ 5K pace; 16 mi long run w/ 4 @ MP in final miles |
| 9 | Build | 45 | 17 | Build downhill/quad resilience; carry fuel + fluids on long run, 5 mi tempo; sustained downhill running practice for Newton descents |
| 10 | Peak | 41 | 15 | Step-back before peak; calibrate hot-day pace band vs. Goal effort, 4 mi tempo; hot-day pace-band run (rehearse ~74F effort) |
| 11 | Peak | 48 | 18 | Peak endurance; only one hard quality day plus the long run, with genuine easy running between, 2x2 mi @ threshold (easy days on either side); 18 mi w/ 5 @ MP in miles 12-17 |
| 12 | Peak | 53 | 19 | Simulate Boston profile, fueling, and heat; keep midweek relaxed so the long run stays the week's hardest effort: 5 mi steady (moderate, not hard); 19 mi race sim: downhill start, Newton hills at ~15-19 mi |
| 13 | Peak | 57 | 20 | Peak dress rehearsal at higher aerobic volume; long run ~35% of week, surrounded by easy days, 4 mi tempo (buffered by easy days); 20 mi w/ 6 @ MP over Newton sim + full race-day fueling |
| 14 | Taper | 44 | 16 | Begin taper; hold sharpness and quad readiness, keep heat exposure, 4 mi @ MP; short downhill hill strides |
| 15 | Taper | 33 | 12 | Reduce volume, stay fresh; lock in controlled-start pacing plan, 3 mi @ MP (rehearse conservative early pace); 6x strides + light heat exposure |
| 16 | Taper | 22 | 9 | Race week: rest, fuel, hydrate; even-effort start, respect Newton hills and heat, adjust 3:20 goal to hot-day band, 2 mi @ MP + strides; shakeout jog + hydration/fuel check |
The recorded plan, verbatim. Bars scale to the 57-mile peak.
Interfaces
python -m src.cli takes flags or prompts for them and prints the assessment, the plan
table and the trace. streamlit run app.py wraps the same graph: set the race in the
sidebar and watch the run stream through the nodes, then read the plan table, a weekly-mileage
chart, the safety verdict and the trace. Generation uses the Claude CLI by default or the Anthropic
API with ANTHROPIC_API_KEY; CLAUDE_MODEL can pick a faster model for the
several calls a run makes. There is no hosted demo.
Limitations
The measured panel is five plans from three synthetic runners, so 1 in 5 against 2 in 5 is not a stable rate, but the failures in it are real: revisions that leave a flagged jump in place, add new ones, or change the plan's length. The recorded run in which the critic caught five issues and fixed them in one revision is the good case, not the typical one.
The deterministic checks cover four rules. Anything else, such as too many hard days or a monotonous build, depends on the LLM reviewer. The plan is generated advice for an adult runner and is no substitute for coaching or medical guidance.
The course figures in src/knowledge.py are simulated. They come from an earlier
version of my Majors course-difficulty simulation, which ran on a simulated field with course
penalties planted in code, so they are not measurements of the courses, and they differ from the
current study: the file has London at 28 s and Tokyo at 33 s against Berlin and a Boston index of
1.036, where the study's paired method gives 30.7 s, 21.0 s and a composite of 1.0092. The file
says so in a comment, and the course line it passes to the adjust prompt now calls
the figure a simulated estimate, not a race result. They only steer the wording of
adjust, and none of the safety checks reads them.
2026-10-28: the label was added after the recorded run shown on this page. The values are unchanged.
Stack
StateGraph with a typed state and a conditional edge for the revise loop_safety_checks in src/graph.py; JSON extraction from model output in src/util.pysrc/knowledge.py: course penalties (simulated), heat tax, the 30 km wall, negative-split rate