relayrl_env_trait 1.3.1

Traits for training and testing environments in the RelayRL framework.
Documentation
# RelayRL Environment Traits

The environment abstraction layer for RelayRL. This crate is intentionally tiny and
dependency-light (`thiserror` + `uuid` only): it defines *how* the runtime talks to an
environment, but contains no simulator, no tensor backend, and no runtime logic. The
`relayrl_framework` client drives any type that implements these traits.

## System layout

All public items live in the `traits` module and are re-exported at the crate root:

- `Environment`: the base contract every environment shares (observation/mask building,
  dtypes and dimensions, flat-bytes accessors, discreteness, and conversion into an
  `EnvironmentHandle`).
- `ScalarEnvironment`: one object per logical environment, stepped with a single action.
- `VectorEnvironment`: one object that owns a *batch* of logical environments, stepped
  with a batch of actions in a single call.
- `EnvironmentHandle`: a runtime-facing enum unifying boxed scalar and vector
  environments, with `DynScalarEnvironment` providing object-safe, clonable scalar envs.
- Supporting types: `EnvironmentUuid` (stable per-env identity), `EnvDType` /
  `EnvironmentKind`, `ScalarEnvReset` / `VectorEnvReset`, the byte aliases
  `Observation` / `Mask` / `Reward` / `Done` / `Truncated`, and
  `TrainingPerformanceReturnFn` for custom training signals.

## Scalar vs. vector execution

The framework may run **many logical environments in parallel** (one
`ScalarEnvironment` per worker) or a single **batched** simulator that implements
`VectorEnvironment`:

- Use `ScalarEnvironment` when each sub-environment is its own object with a scalar
  step. A parallel runner holds many handles, assigns one stable `EnvironmentUuid` per
  sub-env, and steps each worker independently.
- Use `VectorEnvironment` when one implementation can apply a batch of actions keyed by
  `EnvironmentUuid` in a single call (GPU batching, vectorized physics, a remote batched
  service, etc.).

## Design notes and implementor contracts

- **`Send + Sync` everywhere.** All traits require `Send + Sync`, so mutable simulation
  state should live behind interior mutability (e.g. `Mutex`, atomics) rather than `&mut
  self` — every method takes `&self`.
- **Opaque identity.** Treat `EnvironmentUuid` as opaque; the same uuid must refer to
  one logical env across `reset`/`step` and any runtime routing.
- **Ordering.** Unless your concrete type documents otherwise, callers should not assume
  `VectorEnvironment::step_bytes` output order matches input order; key results by
  `EnvironmentUuid`.
- **Errors are whole-operation.** `EnvironmentError` describes the entire call; partial
  success is not expressed in the type system. Surface per-env failures inside your info
  payloads if you need them.
- **Type-erased observations.** `Environment::build_observation` returns
  `std::any::Any` for framework integration; pair it with a documented downcasting
  convention. The `flat_*_bytes` accessors provide the byte-oriented path the runtime uses.

## Quick start

A minimal scalar environment skeleton. Note that every method takes `&self`, so any
mutable state must use interior mutability:

```rust,no_run
use relayrl_env_trait::*;
use std::any::Any;

#[derive(Clone)]
struct MyEnv;

impl Environment for MyEnv {
    fn run_environment(&self) -> Result<(), EnvironmentError> { Ok(()) }
    fn build_observation(&self) -> Result<Box<dyn Any>, EnvironmentError> {
        Ok(Box::new(vec![0u8; self.observation_dim()]))
    }
    fn build_mask(&self) -> Result<Box<dyn Any>, EnvironmentError> { Ok(Box::new(())) }
    fn observation_dtype(&self) -> EnvDType { EnvDType::NdArray(EnvNdArrayDType::F32) }
    fn action_dtype(&self) -> EnvDType { EnvDType::NdArray(EnvNdArrayDType::I64) }
    fn observation_dim(&self) -> usize { 8 }
    fn action_dim(&self) -> usize { 4 }
    fn flat_observation_bytes(&self) -> Observation { vec![0u8; self.observation_dim()] }
    fn flat_mask_bytes(&self) -> Mask { None }
    fn action_is_discrete(&self) -> bool { true }
    fn kind(&self) -> EnvironmentKind { EnvironmentKind::Scalar }
    fn into_handle(self: Box<Self>) -> EnvironmentHandle {
        EnvironmentHandle::Scalar(Box::new(*self))
    }
}

impl ScalarEnvironment for MyEnv {
    fn reset(&self) -> Result<ScalarEnvReset, EnvironmentError> {
        Ok(ScalarEnvReset { observation: self.flat_observation_bytes(), info: None })
    }
    fn step_bytes(
        &self,
        _action: &[u8],
    ) -> Option<(Observation, Mask, Reward, Done, Truncated)> {
        Some((self.flat_observation_bytes(), None, 0.0, false, false))
    }
}
```

## License

This project is licensed under the [Apache License 2.0](../../LICENSE).