Skip to content

remote

remote streams observations over a WebSocket to a PolicyServer on a GPU host and returns its action chunks, so a CPU robot host runs any policy at control rate.

By the end of this page you can serve any policy from a GPU host with PolicyServer and drive a robot from a CPU host with RemotePolicy, through the same run_policy call you use for a local provider.

pip install 'strands-robots[inference]'    # websockets only; composes with lerobot

What it is

Two dashed hosts. Left, the robot host, a laptop or a Jetson: Robot("so101") or Robot("so101", mode="real"), which owns the control loop and send_action; the operator gate, the one green element, stays on this host; the control loop consumes the chunk at the control frequency; RemotePolicy, create_policy("ws://gpu:8765"), provider remote, a proxy with the Policy contract. Right, the GPU host: PolicyServer wraps any Policy with the [inference] extra and calls its get_actions; the Policy is loaded once. Four wires between them, in order: ready with the policy's metadata from the server; set_state_keys, set_control_frequency and reset from the client; get_actions with the encoded observation, the instruction and the delay each control step; and actions back, a JSON list of action dicts, drawn dashed. Footnote: what crosses the wire is an observation and a chunk of actions; the robot, the gate and the audit never do.Two dashed hosts. Left, the robot host, a laptop or a Jetson: Robot("so101") or Robot("so101", mode="real"), which owns the control loop and send_action; the operator gate, the one green element, stays on this host; the control loop consumes the chunk at the control frequency; RemotePolicy, create_policy("ws://gpu:8765"), provider remote, a proxy with the Policy contract. Right, the GPU host: PolicyServer wraps any Policy with the [inference] extra and calls its get_actions; the Policy is loaded once. Four wires between them, in order: ready with the policy's metadata from the server; set_state_keys, set_control_frequency and reset from the client; get_actions with the encoded observation, the instruction and the delay each control step; and actions back, a JSON list of action dicts, drawn dashed. Footnote: what crosses the wire is an observation and a chunk of actions; the robot, the gate and the audit never do.

RemotePolicy is a Policy whose get_actions forwards each observation to a PolicyServer over a WebSocket (WS-JSON) and returns the action chunk the server computed. The checkpoint stays on the machine with the GPU; the robot host installs websockets. create_policy resolves any ws:// or wss:// string to this provider, and the client mirrors the served policy's requires_images, execution_horizon, actions_per_step and supports_rtc, so the runtime sizes chunks and skips camera rendering as it would in process. The connection opens on first use.

from strands_robots.inference import PolicyServer
from strands_robots.simulation import create_simulation

server = PolicyServer(policy_provider="mock", port=0).start()   # port=0 asks the OS for a free port
sim = create_simulation("mujoco")
sim.create_world()
sim.add_robot("so101")
result = sim.run_policy(
    robot_name="so101",
    policy_provider=f"ws://127.0.0.1:{server.port}",   # ws:// resolves to remote
    n_steps=20,
    control_frequency=50.0,
)
print(result["status"], result["content"][0]["text"])
server.stop()
sim.cleanup()

Constructor keywords

keyword type default
endpoint str \| None None
host str '127.0.0.1'
port int 8765
connect_timeout float 10.0
request_timeout float 60.0
**ignored_kwargs unknown keywords are ignored

endpoint supersedes host and port; without it the client dials ws://host:port. host is checked for delimiters and port must be an int in [1, 65535] before the URI exists. connect_timeout and request_timeout are positive seconds; 0, a negative or True is a ValueError at construction.

The server

from strands_robots.inference import PolicyServer

PolicyServer(policy_provider="robotfuel/act_so101_t16b", host="0.0.0.0").serve()   # built by provider name
PolicyServer(policy=my_policy, port=8765).serve()                            # or an object you loaded

PolicyServer takes exactly one of policy or policy_provider (policy_config goes to create_policy). It binds 127.0.0.1; set host="0.0.0.0" to accept other machines. serve() blocks; start() returns after binding and stop() closes accepted connections. port=0 asks the OS for a free port and writes it back to server.port.

Transport auth and TLS are out of scope at this commit: run the link inside a tailscale or wireguard tunnel beyond one LAN. The server serves one client at a time; the wrapped policy holds per-episode state (RTC chunk seams, diffusion RNG), so a lock serialises inference and a second client waits.

Real-Time Chunking end to end

The runner counts rtc_observed_delay_steps on the robot host, the client forwards it on every request, and the server applies it before the wrapped policy blends chunk seams. A policy that supports RTC behaves the same behind the WebSocket as in process. The result's avg_inference_ms is network plus inference; size control_frequency against it.

Hardware

from strands_robots import Robot, create_policy

arm = Robot("so101", mode="real", port="/dev/ttyACM0")
policy = create_policy("ws://gpu-box:8765")
arm.run_policy(policy, instruction="pick up the cube", duration=30.0)

On hardware run_policy takes a policy object built from the same string, and the Agents gate sits in front of it when an agent makes the call. When the served policy requires_images, attach the cameras it was trained on; lerobot_local lists the camera keys.

Edit page