Skip to content

Commit 5a1cddc

Browse files
committed
tighten example authoring guide
Expand the contract for new examples so contributors do not need to read three existing examples to learn the conventions: - pin the docstring header shape: loop, success condition, and each Failure kind (recoverable or terminal) - add a 'New Failure Kind Checklist' and an expanded list of canonical kinds, including invalid_action and precondition_blocked - add a 'Loop Counters' section that documents the _count suffix convention so Trace.summary() picks them up - pin the README section headings (already consistent across the five category READMEs) - add a 'Package Boundary' section pointing at pir/worlds/blocked_path.py and pir/worlds/moving_obstacle.py as the reference for environment extraction - add a final 'Contributor Checklist' that maps each rule to a PR-level acceptance criterion No code or example was changed; existing examples are grandfathered.
1 parent e752499 commit 5a1cddc

1 file changed

Lines changed: 147 additions & 19 deletions

File tree

docs/example_authoring.md

Lines changed: 147 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@ Examples should feel like readable experiment notes, not framework plugins.
77

88
Each 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

1919
The 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

4270
Failures 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

67102
The 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

126200
Add 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

145227
If 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`.
150235
4. Install contributor dependencies and run:
151236

152237
```bash
153238
pip install -e ".[dev]"
154239
python 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

159269
Keep 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

Comments
 (0)