Connect a Remote Environment

A remote client drives an environment that another process serves.

A remote client drives an environment that another process serves. It speaks the same reset / step / render / close loop as a local Gymnasium environment, so code that runs against gym.make(...) runs against a RemoteEnv with no other change. The environment can live in a separate terminal, a container, or another machine; the client only needs its address.

from rlmesh.numpy import RemoteEnv

env = RemoteEnv("127.0.0.1:5555")
obs, info = env.reset(seed=0)
obs, reward, terminated, truncated, info = env.step(
    env.action_space.sample()
)
env.close()

The import decides how values decode at the boundary. rlmesh.numpy returns NumPy arrays; rlmesh.torch and rlmesh.jax return their tensors; top-level rlmesh keeps RLMesh-native values with no array dependency. The client behavior is identical across all four. See Framework Backends.

The loop and the contract

A client connects and handshakes at construction. Building RemoteEnv(address) dials the endpoint and fetches its EnvContract before returning, so a constructed client is already connected and an unreachable address fails right there rather than on the first step.

Once connected, the client carries the usual methods:

  • reset(seed=None, options=None)
  • step(action)
  • render(env_index=0)
  • close()

It also exposes the contract and spaces the server reported, so you can shape actions and read metadata without a second round trip:

print(env.env_contract)
print(env.observation_space)
print(env.action_space)
print(env.address)            # resolved endpoint address
print(env.env_id)             # this connection's container id (UUIDv7)

observation_space and action_space are decoded with the same backend as the client, so sample() returns values you can hand straight to step. See Contracts and Specs for the contract fields.

RemoteEnv is a context manager, which is the cleanest way to guarantee the connection is released:

with RemoteEnv("127.0.0.1:5555") as env:
    obs, info = env.reset(seed=0)

Single vs vector clients

Pick the client that matches the endpoint’s arity. A client checks the contract during the handshake and refuses the wrong one: RemoteEnv against an endpoint serving more than one environment raises ValueError pointing you at RemoteVectorEnv.

Reach for When the endpoint serves Spaces Step shape
RemoteEnv exactly one environment observation_space, action_space one action in, one transition out
RemoteVectorEnv two or more environments single_observation_space, single_action_space a batch of actions in, batched results out
from rlmesh.numpy import RemoteVectorEnv

envs = RemoteVectorEnv("127.0.0.1:5555")
observations, infos = envs.reset(seed=0)
actions = [envs.single_action_space.sample() for _ in range(envs.num_envs)]
observations, rewards, terminations, truncations, infos = envs.step(actions)
envs.close()

num_envs reports the served count. observation_space and action_space on a vector client are aliases for the single_* spaces, and reset accepts either one seed or a per-environment list.

To serve a vector environment, pass an object with batched reset and step methods, num_envs, single_observation_space, and single_action_space to EnvServer. The server publishes the instance count and single-instance spaces in its contract, which the client reads at connection time. A SandboxVectorEnv owns a vector server in a container; see Sandboxed Environments.

Addresses

A client accepts the same address forms as the server, chosen by scheme:

RemoteEnv("tcp://127.0.0.1:5555")
RemoteEnv("127.0.0.1:5555")
RemoteEnv("unix:///tmp/rlmesh-env.sock")

Or build the address from helpers, where port is required alongside host:

RemoteEnv(host="127.0.0.1", port=5555)
RemoteEnv(path="/tmp/rlmesh-env.sock")

address and the helpers are mutually exclusive, and unix sockets are unavailable on Windows.

Two optional keyword arguments bound the two waits: connect_timeout_seconds= bounds the dial and handshake at construction, and request_timeout_seconds= bounds each reset, step, and render call. Both default to None, which waits as long as the endpoint takes.

RemoteEnv("127.0.0.1:5555", connect_timeout_seconds=5.0, request_timeout_seconds=30.0)

Connection lifecycle

The connection has three stages: dial-and-handshake at construction, the reset/step exchange, and teardown.

construct client
dial endpoint
handshakereceive the contract
reset / stepper step
teardown
close
detach this client
shutdown
ask the endpoint owner to stop

Two teardown paths exist, and they differ in what they affect:

  • close() detaches this client from the endpoint. The server keeps running for other clients.
  • shutdown(reason="owner shutdown") requests an owner-level shutdown of the endpoint itself and returns whether the request was accepted. Use it when this client owns the server’s lifetime.

Errors and timeouts

A wrong-arity connection fails at construction with ValueError, after the client has already closed its dial, so there is nothing to clean up. An unreachable or refused endpoint surfaces as a transport error from the same construction call.

For the broader error model, see Troubleshooting.