Skill Tracer
Skills › Scientific Agent Skills › pufferlib › environments.md

environments.md

286 lines · skills/pufferlib/references/environments.md at 5f2bc03

Open on GitHub

Most marks come from matching words in the file, so treat them as pointers.

1# Environment Contracts and Native Environments
2
3Review: **2026-10-01**. Native 5.0 guidance is in [native-5.md](native-5.md);
43.0 and the pinned 4.0 snapshot below are historical profiles.
5

Environment Contracts and Native Environments

Review: 2026-10-01. Native 5.0 guidance is in native-5.md; 3.0 and the pinned 4.0 snapshot below are historical profiles.

Raw lines 1 to 5
6## Start from the Gymnasium contract
7
8A current single-agent Gymnasium environment defines `observation_space` and
9`action_space`, then implements:
11```python
12def reset(self, *, seed=None, options=None):
13 super().reset(seed=seed)
14 return observation, info
16def step(self, action):
17 return observation, reward, terminated, truncated, info
18```
20Contract requirements:
22- `observation` must be contained in `observation_space` after reset and every
23 step, with the documented shape and dtype.
24- `action` must be contained in `action_space`.
25- `reward` is a finite scalar for ordinary single-agent tasks.
26- `terminated` means the task's MDP reached a terminal state.
27- `truncated` means an external limit ended the episode, commonly a time limit.
28- `info` is a dictionary; never hide the only termination signal in it.
29- Call `reset()` after either `terminated` or `truncated`.
30- Seed the environment through `reset(seed=...)`. Seed the action space
31 separately when sampled actions must be reproducible.
32- Always call `close()`.
34Both flags can be true in Gymnasium (for example a terminal at a time limit).
35The synthetic helper deliberately makes its own flags mutually exclusive; that
36is not a universal Gymnasium requirement.
38Do not collapse `terminated` and `truncated` during learning. A time-limit
39truncation can still permit value bootstrapping; a true terminal state does not.
41Run the local contract tool before involving PufferLib:
43```bash
44python3 scripts/env_contract_validator.py
45```
47It validates only the bundled synthetic environment. It intentionally has no
48module-path option, so it cannot dynamically import an untrusted package.

Start from the Gymnasium contract

A current single-agent Gymnasium environment defines observation_space and action_space, then implements:

def reset(self, *, seed=None, options=None):
    super().reset(seed=seed)
    return observation, info

def step(self, action):
    return observation, reward, terminated, truncated, info

Contract requirements:

  • observation must be contained in observation_space after reset and every step, with the documented shape and dtype.
  • action must be contained in action_space.
  • reward is a finite scalar for ordinary single-agent tasks.
  • terminated means the task's MDP reached a terminal state.
  • truncated means an external limit ended the episode, commonly a time limit.
  • info is a dictionary; never hide the only termination signal in it.
  • Call reset() after either terminated or truncated.
  • Seed the environment through reset(seed=...). Seed the action space separately when sampled actions must be reproducible.
  • Always call close().

Both flags can be true in Gymnasium (for example a terminal at a time limit). The synthetic helper deliberately makes its own flags mutually exclusive; that is not a universal Gymnasium requirement.

Do not collapse terminated and truncated during learning. A time-limit truncation can still permit value bootstrapping; a true terminal state does not.

Run the local contract tool before involving PufferLib:

python3 scripts/env_contract_validator.py

It validates only the bundled synthetic environment. It intentionally has no module-path option, so it cannot dynamically import an untrusted package.

Raw lines 6 to 49
Runs code python3 scripts/env_contract_validator.py :44
50## Autoreset and final observations
52Gymnasium 1.3's built-in vector APIs default to `NEXT_STEP`. A reset-only call
53ignores that slot's action and must be excluded from learning transitions.
54`SAME_STEP` returns the reset observation; the terminal observation is in
55`infos["final_obs"]` with the `_final_obs` validity mask. `DISABLED` requires an
56explicit boolean `reset_mask`. Record `metadata["autoreset_mode"]`, wrapper
57compatibility and the resolved version. Bootstrap truncations from the final
58pre-reset observation, not the next episode; terminals suppress bootstrapping.
60PufferLib 3.0 pins Gymnasium `<=0.29.1`, and its own `Serial`/worker machinery
61resets an already-done adapter on the next send. This is not Gymnasium 1.3's
62VectorEnv contract. Do not inject a current Gymnasium vectorizer or assume its
63`final_obs` key exists in PufferLib. Inspect returned arrays and exclude
64reset-only transitions. Native PufferEnv implementations own their autoresets.
65The bundled validator checks only the synthetic single-environment contract.

Autoreset and final observations

Gymnasium 1.3's built-in vector APIs default to NEXT_STEP. A reset-only call ignores that slot's action and must be excluded from learning transitions. SAME_STEP returns the reset observation; the terminal observation is in infos["final_obs"] with the _final_obs validity mask. DISABLED requires an explicit boolean reset_mask. Record metadata["autoreset_mode"], wrapper compatibility and the resolved version. Bootstrap truncations from the final pre-reset observation, not the next episode; terminals suppress bootstrapping.

PufferLib 3.0 pins Gymnasium <=0.29.1, and its own Serial/worker machinery resets an already-done adapter on the next send. This is not Gymnasium 1.3's VectorEnv contract. Do not inject a current Gymnasium vectorizer or assume its final_obs key exists in PufferLib. Inspect returned arrays and exclude reset-only transitions. Native PufferEnv implementations own their autoresets. The bundled validator checks only the synthetic single-environment contract.

Raw lines 50 to 66
67## Published PufferLib 3.0.0 native contract
69For a native Python `PufferEnv`, assign these attributes **before** calling
70`super().__init__(buf)`:
72```python
73import gymnasium
74import numpy as np
75import pufferlib
78class ReviewedEnv(pufferlib.PufferEnv):
79 def __init__(self, buf=None, seed=0):
80 self.single_observation_space = gymnasium.spaces.Box(
81 low=-1.0, high=1.0, shape=(4,), dtype=np.float32
82 )
83 self.single_action_space = gymnasium.spaces.Discrete(3)
84 self.num_agents = 2
85 super().__init__(buf)
86```
88The stable base accepts a Box observation space and Discrete, MultiDiscrete, or
89Box action space. It allocates or attaches:
91- `observations`
92- `actions`
93- `rewards`
94- `terminals`
95- `truncations`
96- `masks`
98Native methods operate on those buffers:
100```python
101def reset(self, seed=None):
102 # update self.observations in place
103 return self.observations, []
105def step(self, actions):
106 # update all buffers in place
107 return (
108 self.observations,
109 self.rewards,
110 self.terminals,
111 self.truncations,
112 [],
113 )
114```
116The `infos` value for native Puffer environments is a list of dictionaries.
117PufferLib's native interface expects vector rows for agents, even when there is
118one agent. Native environments handle their own resets; clear rewards,
119terminals, truncations, masks, and partially written observations explicitly.
120Never leave a previous step's buffer values in place.

Published PufferLib 3.0.0 native contract

For a native Python PufferEnv, assign these attributes before calling super().__init__(buf):

import gymnasium
import numpy as np
import pufferlib


class ReviewedEnv(pufferlib.PufferEnv):
    def __init__(self, buf=None, seed=0):
        self.single_observation_space = gymnasium.spaces.Box(
            low=-1.0, high=1.0, shape=(4,), dtype=np.float32
        )
        self.single_action_space = gymnasium.spaces.Discrete(3)
        self.num_agents = 2
        super().__init__(buf)

The stable base accepts a Box observation space and Discrete, MultiDiscrete, or Box action space. It allocates or attaches:

  • observations
  • actions
  • rewards
  • terminals
  • truncations
  • masks

Native methods operate on those buffers:

def reset(self, seed=None):
    # update self.observations in place
    return self.observations, []

def step(self, actions):
    # update all buffers in place
    return (
        self.observations,
        self.rewards,
        self.terminals,
        self.truncations,
        [],
    )

The infos value for native Puffer environments is a list of dictionaries. PufferLib's native interface expects vector rows for agents, even when there is one agent. Native environments handle their own resets; clear rewards, terminals, truncations, masks, and partially written observations explicitly. Never leave a previous step's buffer values in place.

Raw lines 67 to 121
122### Native shape checklist
124For `A = num_agents` and single observation shape `S`:
126- observations: `(A, *S)`
127- rewards: `(A,)`
128- terminals: `(A,)`
129- truncations: `(A,)`
130- masks: `(A,)`
131- actions: joint shape derived from the single action space and `A`
133Validate the exact allocated action shape rather than assuming `(A,)`, especially
134for MultiDiscrete and Box actions.

Native shape checklist

For A = num_agents and single observation shape S:

  • observations: (A, *S)
  • rewards: (A,)
  • terminals: (A,)
  • truncations: (A,)
  • masks: (A,)
  • actions: joint shape derived from the single action space and A

Validate the exact allocated action shape rather than assuming (A,), especially for MultiDiscrete and Box actions.

Raw lines 122 to 135
136## Stable Gymnasium and PettingZoo adaptation
138PufferLib 3.0 uses explicit adapters:
140```python
141import pufferlib.emulation
143wrapped = pufferlib.emulation.GymnasiumPufferEnv(reviewed_gymnasium_instance)
144```
146or:
148```python
149wrapped = pufferlib.emulation.PettingZooPufferEnv(reviewed_parallel_instance)
150```
152There is no supported 3.0 `pufferlib.emulate(...)` convenience function matching
153the old skill examples. Pass either an `env` instance or an `env_creator`
154callable according to the class signature; do not pass both.
156The Gymnasium adapter:
158- maps structured observation/action spaces to flat arrays;
159- checks the first observation and action against the original spaces;
160- returns separate terminal and truncation values;
161- requires reset before step and reset after episode end.
163The PettingZoo adapter:
165- targets the Parallel API;
166- uses `possible_agents` as the fixed slot set;
167- pads missing agents and exposes masks;
168- canonicalizes per-agent spaces and flattened buffers.
170Validate heterogeneous-agent spaces before use. The adapter derives its single
171spaces from the first possible agent, so environments with incompatible spaces
172need an explicit reviewed transformation.

Stable Gymnasium and PettingZoo adaptation

PufferLib 3.0 uses explicit adapters:

import pufferlib.emulation

wrapped = pufferlib.emulation.GymnasiumPufferEnv(reviewed_gymnasium_instance)

or:

wrapped = pufferlib.emulation.PettingZooPufferEnv(reviewed_parallel_instance)

There is no supported 3.0 pufferlib.emulate(...) convenience function matching the old skill examples. Pass either an env instance or an env_creator callable according to the class signature; do not pass both.

The Gymnasium adapter:

  • maps structured observation/action spaces to flat arrays;
  • checks the first observation and action against the original spaces;
  • returns separate terminal and truncation values;
  • requires reset before step and reset after episode end.

The PettingZoo adapter:

  • targets the Parallel API;
  • uses possible_agents as the fixed slot set;
  • pads missing agents and exposes masks;
  • canonicalizes per-agent spaces and flattened buffers.

Validate heterogeneous-agent spaces before use. The adapter derives its single spaces from the first possible agent, so environments with incompatible spaces need an explicit reviewed transformation.

Raw lines 136 to 173
174### Structured spaces
176Stable emulation supports Box, Discrete, MultiDiscrete, Tuple, and Dict patterns
177through a packed NumPy dtype. This is byte-layout conversion, not semantic
178feature engineering. Check:
180- deterministic Dict key order;
181- leaf shape and dtype;
182- finite numeric values;
183- lossless action reconstruction;
184- policy-side unflattening;
185- padding/mask handling for variable populations.

Structured spaces

Stable emulation supports Box, Discrete, MultiDiscrete, Tuple, and Dict patterns through a packed NumPy dtype. This is byte-layout conversion, not semantic feature engineering. Check:

  • deterministic Dict key order;
  • leaf shape and dtype;
  • finite numeric values;
  • lossless action reconstruction;
  • policy-side unflattening;
  • padding/mask handling for variable populations.
Raw lines 174 to 186
187## Historical 4.0 Ocean contract
189The reviewed 4.0 source focuses on first-party C environments. It no longer
190provides the 3.0 Python emulation/vector modules. The official starting points
191are:
193- `ocean/squared`: commented single-agent template
194- `ocean/target`: commented multi-agent template
196A binding defines compile-time metadata such as:
198```c
199#define OBS_SIZE 121
200#define NUM_ATNS 1
201#define ACT_SIZES {5}
202#define OBS_TENSOR_T ByteTensor
204#define Env Squared
205#include "vecenv.h"
206```
208The environment struct must include pointers for observations, actions,
209rewards, and terminals, plus `num_agents` and a log struct. It implements
210`c_reset`, `c_step`, `c_render`, and `c_close`; `binding.c` supplies `my_init`
211and `my_log`.
213Security and correctness rules:
2151. Treat the C environment and every linked library as native code.
2162. Verify repository/commit, license, asset rights, and checksums before build.
2173. Build only the selected environment in a disposable container or VM.
2184. Start with the local/address-sanitizer build described by upstream.
2195. Match `OBS_SIZE`, tensor dtype, action branch count/sizes, and actual writes.
2206. Bounds-check every index and allocation; use checked arithmetic for sizes.
2217. Initialize every output element each step. Reset reward/terminal buffers
222 before early returns.
2238. Use an environment-owned RNG seeded per instance; do not use global RNG
224 state for reproducibility.
2259. Free only memory owned by the environment. Do not free framework buffers.
22610. Fuzz reset/step/action boundaries before optimization.
228`c_step` may reset immediately after marking a terminal. Record this autoreset
229behavior when interpreting terminal observations.

Historical 4.0 Ocean contract

The reviewed 4.0 source focuses on first-party C environments. It no longer provides the 3.0 Python emulation/vector modules. The official starting points are:

  • ocean/squared: commented single-agent template
  • ocean/target: commented multi-agent template

A binding defines compile-time metadata such as:

#define OBS_SIZE 121
#define NUM_ATNS 1
#define ACT_SIZES {5}
#define OBS_TENSOR_T ByteTensor

#define Env Squared
#include "vecenv.h"

The environment struct must include pointers for observations, actions, rewards, and terminals, plus num_agents and a log struct. It implements c_reset, c_step, c_render, and c_close; binding.c supplies my_init and my_log.

Security and correctness rules:

  1. Treat the C environment and every linked library as native code.
  2. Verify repository/commit, license, asset rights, and checksums before build.
  3. Build only the selected environment in a disposable container or VM.
  4. Start with the local/address-sanitizer build described by upstream.
  5. Match OBS_SIZE, tensor dtype, action branch count/sizes, and actual writes.
  6. Bounds-check every index and allocation; use checked arithmetic for sizes.
  7. Initialize every output element each step. Reset reward/terminal buffers before early returns.
  8. Use an environment-owned RNG seeded per instance; do not use global RNG state for reproducibility.
  9. Free only memory owned by the environment. Do not free framework buffers.
  10. Fuzz reset/step/action boundaries before optimization.

c_step may reset immediately after marking a terminal. Record this autoreset behavior when interpreting terminal observations.

Raw lines 187 to 230
231## Environment provenance
233An environment package may execute arbitrary Python/native code and may fetch
234assets at import, build, reset, or render time. Before execution:
236- use the official repository and immutable revision;
237- inspect package/build scripts and transitive dependencies;
238- verify artifact hashes or attestations;
239- review license compatibility for code, datasets, media, ROMs, maps, and model
240 opponents separately;
241- reject unlicensed ROMs or “accept ROM license” automation without proof of
242 rights;
243- disable network and credentials in the first-run sandbox;
244- cap disk, memory, processes, threads, episode length, agents, and render size;
245- do not load bundled checkpoints or pickle files during environment import.
247An entry in Ocean/config is not a blanket security, quality, or licensing
248approval.

Environment provenance

An environment package may execute arbitrary Python/native code and may fetch assets at import, build, reset, or render time. Before execution:

  • use the official repository and immutable revision;
  • inspect package/build scripts and transitive dependencies;
  • verify artifact hashes or attestations;
  • review license compatibility for code, datasets, media, ROMs, maps, and model opponents separately;
  • reject unlicensed ROMs or “accept ROM license” automation without proof of rights;
  • disable network and credentials in the first-run sandbox;
  • cap disk, memory, processes, threads, episode length, agents, and render size;
  • do not load bundled checkpoints or pickle files during environment import.

An entry in Ocean/config is not a blanket security, quality, or licensing approval.

Raw lines 231 to 249
250## Testing ladder
2521. Built-in synthetic contract validator.
2532. One environment, one seed, serial, tens of steps.
2543. Boundary actions and intentionally invalid actions.
2554. Termination and time-limit truncation tests.
2565. Same-seed trace comparison.
2576. Independent-seed diversity check.
2587. Structured-space round trip.
2598. Multi-agent join/leave and mask tests.
2609. Serial versus vectorized trace equivalence where ordering permits.
26110. Bounded throughput benchmark only after correctness passes.

Testing ladder

  1. Built-in synthetic contract validator.
  2. One environment, one seed, serial, tens of steps.
  3. Boundary actions and intentionally invalid actions.
  4. Termination and time-limit truncation tests.
  5. Same-seed trace comparison.
  6. Independent-seed diversity check.
  7. Structured-space round trip.
  8. Multi-agent join/leave and mask tests.
  9. Serial versus vectorized trace equivalence where ordering permits.
  10. Bounded throughput benchmark only after correctness passes.
Raw lines 250 to 262
263## Sources
265- [Gymnasium vector API](https://gymnasium.farama.org/api/vector/) and
266 [autoreset modes](https://farama.org/Vector-Autoreset-Mode) — checked 2026-10-01.
269- [Gymnasium Env API](https://gymnasium.farama.org/api/env/) — current reset,
270 step, spaces, and seeding contract; accessed 2026-07-23.
271- [Gymnasium terminated/truncated explanation](https://farama.org/Gymnasium-Terminated-Truncated-Step-API)
272 — published 2023-10-27; accessed 2026-07-23.
273- [PufferLib 3.0 core environment source](https://github.com/PufferAI/PufferLib/blob/3.0/pufferlib/pufferlib.py)
274 — stable native contract; accessed 2026-07-23.
275- [PufferLib 3.0 emulation source](https://github.com/PufferAI/PufferLib/blob/3.0/pufferlib/emulation.py)
276 — stable adapters; accessed 2026-07-23.
277- [PufferLib 3.0 Gymnasium example](https://github.com/PufferAI/PufferLib/blob/3.0/examples/gymnasium_env.py)
278 — stable example; accessed 2026-07-23.
279- [PufferLib 3.0 PettingZoo example](https://github.com/PufferAI/PufferLib/blob/3.0/examples/pettingzoo_env.py)
280 — stable example; accessed 2026-07-23.
281- [PufferLib 4.0 Squared template](https://github.com/PufferAI/PufferLib/tree/25647630e1b15330bb3153a5a0d3ff8d234c3acf/ocean/squared)
282 — historical single-agent native template; accessed 2026-07-23.
283- [PufferLib 4.0 Target template](https://github.com/PufferAI/PufferLib/tree/25647630e1b15330bb3153a5a0d3ff8d234c3acf/ocean/target)
284 — historical multi-agent native template; accessed 2026-07-23.
285- [PufferLib Ocean](https://puffer.ai/ocean.html) — current first-party
286 collection; accessed 2026-07-23.

Sources

Raw lines 263 to 286