@@ -7,14 +7,14 @@ Examples should feel like readable experiment notes, not framework plugins.
77
88Each user-facing example should provide:
99
10- - a short module docstring explaining the loop
10+ - a short module docstring that names the loop and declares success / failure
1111- a tiny world or environment
1212- a tiny agent or policy
1313- a ` run(...) ` function that returns ` Trace `
1414- a ` main() ` function for script execution
1515- a ` --no-render ` flag for headless tests
16- - a focused smoke test
17- - a README section explaining what is simplified
16+ - a focused smoke test in ` tests/test_examples_smoke.py `
17+ - a README section in the matching ` examples/<category>/README.md `
1818
1919The core loop should be visible:
2020
@@ -37,6 +37,34 @@ for _ in range(max_steps):
3737 break
3838```
3939
40+ ## Docstring Header
41+
42+ The first thing a reader sees should be enough to understand the loop without
43+ reading code. Every example module docstring should answer three questions:
44+
45+ 1 . ** What is the loop?** One paragraph naming the perception-action cycle.
46+ 2 . ** What counts as success?** Concrete condition that flips
47+ ` info["success"] ` to ` True ` .
48+ 3 . ** What counts as failure?** Each ` Failure.kind ` the example can emit,
49+ labelled ` (recoverable) ` or ` (terminal) ` .
50+
51+ Example:
52+
53+ ``` python
54+ """ Reactive obstacle avoidance with a fake lidar.
55+
56+ The agent reads a discrete lidar reading at every step, picks a direction
57+ that keeps the closest obstacle outside the safety radius, and moves.
58+
59+ Success: robot reaches the goal cell before max_steps.
60+ Failure: collision (recoverable - the agent retries from a safe cell) or
61+ timeout (terminal).
62+ """
63+ ```
64+
65+ A short success/failure block lets ` Trace.summary() ` and the README captions
66+ stay aligned with the runnable example.
67+
4068## Failure Contract
4169
4270Failures are part of the API. Do not hide them in print output.
@@ -61,12 +89,47 @@ Common failure kinds:
6189- ` blocked_grasp `
6290- ` suction_miss `
6391- ` container_closed `
92+ - ` precondition_blocked `
6493- ` model_error `
6594- ` timeout `
95+ - ` invalid_action `
96+
97+ Recoverable failures should leave ` done=False ` . Terminal failures should
98+ leave ` done=True ` and have ` recoverable=False ` . Use ` timeout ` as the
99+ canonical terminal failure when the agent runs out of steps without
100+ success.
66101
67102The agent should usually update memory, belief, state, or plan after a
68- recoverable failure. A retry should be different from repeating the same action
69- blindly.
103+ recoverable failure. A retry should be different from repeating the same
104+ action blindly.
105+
106+ ### New Failure Kind Checklist
107+
108+ When introducing a new ` Failure.kind ` :
109+
110+ - pick a snake_case name that names the * cause* , not the symptom
111+ - check it does not duplicate an existing kind in the list above
112+ - decide ` recoverable=True ` or ` recoverable=False ` and stay consistent
113+ across the example
114+ - add a regression assertion in
115+ ` tests/test_failure_contracts.py ` if the kind has nontrivial recovery
116+ semantics
117+ - document the kind in the example's docstring header and category README
118+
119+ ## Loop Counters
120+
121+ Counters surfaced through ` info ` should:
122+
123+ - end with ` _count ` so ` Trace.summary() ` picks them up automatically
124+ - be monotonic over the run
125+ - represent a teaching event (retries, replans, recoveries, persistences),
126+ not raw step ticks
127+
128+ Conventional names already in the repo: ` retry_count ` , ` replan_count ` ,
129+ ` recovery_count ` , ` miss_count ` , ` servo_steps ` , ` belief_updates ` ,
130+ ` avoidance_count ` , ` target_switches ` , ` info_gain_step_count ` ,
131+ ` recovery_count ` , ` memory_persistence_count ` . Reuse these when the concept
132+ matches.
70133
71134## Visualization Checklist
72135
@@ -86,19 +149,26 @@ Useful visual elements:
86149- state machine state
87150- reward, cost, entropy, or model error when relevant
88151
89- Every major README GIF should be generated by ` scripts/make_gifs.py ` from the
90- same runnable example.
152+ Every major README GIF should be generated by ` scripts/make_gifs.py ` from
153+ the same runnable example. The GIF caption in the root README should name
154+ the * internal* state that is being shown (belief, novelty, predicted
155+ rollout, ...), not just the visible motion.
91156
92157## Example README Section
93158
94- Use this shape in ` examples/<category>/README.md ` :
159+ Use this shape in ` examples/<category>/README.md ` . The section headings
160+ are fixed so contributors can scan the file consistently:
95161
96162```` markdown
97163## ` NN_example_name.py `
98164
99165### What this teaches
100166
101- One short paragraph.
167+ One short paragraph naming the loop and the lesson. Mention the closest
168+ existing example and how this one differs.
169+
170+ Success: ...
171+ Failure: kind_a (recoverable), kind_b (terminal).
102172
103173### Run
104174
@@ -121,6 +191,10 @@ observe -> update belief -> act -> observe failure -> retry
121191- concrete modification ideas
122192````
123193
194+ The ` Success: ` and ` Failure: ` lines under "What this teaches" mirror the
195+ docstring header so the README block stays in sync with the runnable
196+ example.
197+
124198## Smoke Test Pattern
125199
126200Add a focused test in ` tests/test_examples_smoke.py ` :
@@ -131,29 +205,65 @@ def test_new_example_runs_headless() -> None:
131205
132206 trace = module.run(seed = 0 , render = False , max_steps = 40 )
133207
134- assert trace.infos[- 1 ][" success" ] is True
135- assert len (trace.actions) > 0
208+ final = trace.infos[- 1 ]
209+ assert final[" success" ] is True
210+ assert final[" replan_count" ] >= 1
211+ assert any (failure.kind == " blocked_path" for failure in trace.failures())
136212```
137213
138- Add assertions for the concept being taught. For example, if the example is
139- about recovery, assert that a relevant ` Failure ` appears in ` trace.failures() ` .
140- Use ` trace.summary() ` when a test or README needs compact run-level statistics
141- such as total reward, failure counts, or maximum ` *_count ` loop counters.
214+ Smoke tests should assert the concept being taught, not the implementation.
215+ Useful patterns:
216+
217+ - assert ` info["success"] is True ` for the golden seed
218+ - assert a counter is at least the expected lower bound (use ` >= ` , not ` == ` )
219+ - assert a recoverable ` Failure.kind ` appears in ` trace.failures() `
220+ - assert the loop terminates (` len(trace.actions) <= max_steps ` )
221+ - use ` trace.summary() ` when a test or README needs compact run-level
222+ statistics such as total reward, failure counts, or maximum ` *_count `
223+ loop counters
142224
143225## GIF Addition Pattern
144226
145227If the example should appear in the root README:
146228
147- 1 . Add a ` make_<name>() ` function to ` scripts/make_gifs.py ` .
148- 2 . Register it in ` MAKERS ` .
149- 3 . Add the generated GIF to ` README.md ` .
229+ 1 . Add a ` make_<name>() ` function to ` scripts/make_gifs.py ` that imports
230+ the example via ` load_example(...) ` and renders frames using the same
231+ draw code the example uses.
232+ 2 . Register the maker in ` MAKERS ` with a short key.
233+ 3 . Add the generated GIF to the matching category README ** and** the root
234+ ` README.md ` .
1502354 . Install contributor dependencies and run:
151236
152237``` bash
153238pip install -e " .[dev]"
154239python scripts/run_all_smoke_tests.py --gifs --check-gifs
155240```
156241
242+ The check verifies frame count and nonblank pixels for every generated
243+ GIF, and the Markdown asset check verifies that every linked image
244+ exists.
245+
246+ ## Package Boundary
247+
248+ Keep agent code in the example. Keep environment code in the example * or*
249+ in ` pir/worlds/ ` . The package boundary should make examples easier to
250+ read, not turn the repo into a framework.
251+
252+ Move a world from an example into ` pir/worlds/ ` when:
253+
254+ - another example or adapter needs to import it
255+ - the world has a clean ` reset() ` / ` observe() ` / ` step() ` surface
256+ - moving it does not require lifting agent code
257+
258+ Patterns to follow: ` pir/worlds/blocked_path.py ` and
259+ ` pir/worlds/moving_obstacle.py ` . Both expose ` reset() ` , ` observe() ` , and
260+ a ` step() ` that returns ` StepResult ` , and both keep their ` draw_*_scene `
261+ helper next to the world so example modules stay focused on the agent.
262+
263+ ` step() ` should return ` StepResult ` so the unwrap pattern
264+ ` obs, reward, done, info = result.as_tuple() ` works the same way across
265+ the codebase.
266+
157267## When To Add Shared Code
158268
159269Keep logic local to the example first. Move code into ` pir/ ` only when:
@@ -162,4 +272,22 @@ Keep logic local to the example first. Move code into `pir/` only when:
162272- the shared code makes examples easier to read
163273- the abstraction does not hide the interaction loop
164274
165- Small duplication is acceptable when it keeps examples readable.
275+ Small duplication is acceptable when it keeps examples readable. Three
276+ examples that each inline an A* routine is fine; refactoring them into a
277+ shared planner that hides the heuristic is not.
278+
279+ ## Contributor Checklist
280+
281+ Before opening a PR with a new example:
282+
283+ - [ ] docstring header declares loop, success condition, and each failure
284+ kind
285+ - [ ] ` run(seed, render, max_steps) ` returns ` Trace `
286+ - [ ] ` --no-render ` flag exists on the CLI entry point
287+ - [ ] ` env.step(...) ` returns ` StepResult `
288+ - [ ] new ` Failure.kind ` s (if any) follow the snake_case cause-naming rule
289+ - [ ] new counters end with ` _count `
290+ - [ ] smoke test asserts the concept being taught
291+ - [ ] category README block uses the fixed headings above
292+ - [ ] root README and category README link the GIF if one was added
293+ - [ ] ` python scripts/run_all_smoke_tests.py --check-gifs ` is green
0 commit comments