Skip to content

hylo-serve

Hylo serving: policy server (Hylo + openpi protocols), workcell runtime next to a robot (async chunk execution, hold, watchdog, operator events)

Repository hylo-serve, packages/hylo-serve. Licence: Apache-2.0. Depends on: hylo-core, websockets>=13, fastapi>=0.110, uvicorn>=0.29, pillow>=10.

Registered entry points
Group Name Object
commands serve hylo.serve.cli:main
Module Summary
hylo.serve
hylo.serve.cli hylo serve policy
hylo.serve.policy Policy server (S1): any Hylo agent as a network service, for robots and for other harnesses
hylo.serve.runtime Real-time episodes next to a robot: the core kernel in realtime mode (core.kernel.Timing)
hylo.serve.workcell Workcell service (S2): runs next to the robot (or simulator) and executes episodes with a remote policy

hylo.serve

hylo.serve.cli

hylo serve policy | workcell | env | agent: start a serving role (policy.py, workcell.py) or one component.

Registered as the serve subcommand of hylo (core.cli). env / agent serve a single plugin over WebSocket (core.remote.ws), e.g. a simulator in its own venv.

main

main(argv: Sequence[str]) -> int

serve a policy, a workcell, or one env / agent component

hylo.serve.policy

Policy server (S1): any Hylo agent as a network service, for robots and for other harnesses.

One WebSocket server, two protocols, chosen by path: - /hylo the Hylo agent protocol (hylo.agentserver/v0.1, JSON): spec / reset / act / on_step / on_episode_end. reset carries the client's EnvSpec and the session is refused there, before any motion, if the agent is not compatible with it (core.compat). Client: core.remote.ws.WSAgent. - /openpi openpi's protocol (msgpack-numpy): the server sends its metadata, then answers every observation dict with {"actions": (H, A), "server_timing": {...}}, so openpi clients (DROID, RoboArena, openpi's own examples) can run our agents. openpi has no reset message: the agent is reset on connect, when the prompt changes, or when the client sends {"reset": true}. - /healthz liveness, /meta the served agent's spec as JSON.

One session at a time (a robot talks to one policy; a second client is refused with code 1013, like LeRobot's admission). The agent is built once and reused across sessions. With campaign, the server follows that campaign's active version through its active.json (core.deployment): between sessions, never during one, it rebuilds the agent when a promotion or rollback changed it.

hylo serve policy --agent '{"type": "lerobot.steered", ...}'      # or --config EXP.yaml / --campaign DIR

OPENPI_PRESETS module-attribute

OPENPI_PRESETS: dict[str, dict[str, str]] = {'libero': {'observation/image': 'camera:agentview', 'observation/wrist_image': 'camera:wrist', 'observation/state': 'state:eef_pos=3,eef_axis_angle=3,gripper_qpos=2', 'prompt': 'instruction'}}

PolicyServer

PolicyServer(*, agent: dict[str, Any] | None = None, config: str | None = None, campaign: str | None = None, host: str = '127.0.0.1', port: int = 8000, token: str | None = None, openpi_inputs: dict[str, str] | str = 'libero', openpi_max_steps: int = 10000, log: Callable[[str], None] = lambda m: print(m, file=sys.stderr, flush=True))

served instance-attribute

served = _Agent(agent, config, campaign)

openpi_inputs instance-attribute

openpi_inputs = OPENPI_PRESETS[openpi_inputs] if isinstance(openpi_inputs, str) else dict(openpi_inputs)

openpi_max_steps instance-attribute

openpi_max_steps = openpi_max_steps

sessions instance-attribute

sessions = 0

metadata

metadata() -> dict[str, Any]

start

start() -> 'PolicyServer'

serve_forever

serve_forever() -> None

shutdown

shutdown() -> None

observation_from_openpi

observation_from_openpi(obs: dict[str, Any], mapping: dict[str, str]) -> Observation

hylo.serve.runtime

Real-time episodes next to a robot: the core kernel in realtime mode (core.kernel.Timing).

The loop itself (aligned chunk merging, hold action, watchdog, operator channel, virtual / wall clock) lives in core.kernel, so a benchmark can measure exactly what a workcell runs (timing: {mode: realtime, clock: virtual} in an experiment config). This module keeps the workcell's configuration (runtime: in a workcell config) and the result shape the workcell reports.

ABSOLUTE_MODES module-attribute

ABSOLUTE_MODES = ('joint_pos', 'eef_abs_pose')

OperatorChannel

OperatorChannel()

Thread-safe operator input: stop, estop (latched), clear_fault, reset_done, verdict.

fault instance-attribute

fault: str | None = None

stop instance-attribute

stop = threading.Event()

reset_done instance-attribute

reset_done = threading.Event()

verdicts instance-attribute

verdicts: 'queue.Queue[dict[str, Any]]' = queue.Queue()

log instance-attribute

log: list[dict[str, Any]] = []

post

post(event: Mapping[str, Any]) -> None

drain

drain() -> list[dict[str, Any]]

RealtimeConfig

Bases: Timing

Timing in realtime mode, plus what the workcell records.

mode class-attribute instance-attribute

mode: Literal['sync', 'realtime'] = 'realtime'

image_every class-attribute instance-attribute

image_every: int = Field(default=1, ge=0)

record_privileged class-attribute instance-attribute

record_privileged: bool = True

timing

timing() -> Timing

RealtimeResult dataclass

RealtimeResult(outcome: Any, holds: int, chunks: int, safety_interventions: int, verdict: dict[str, Any] | None = None, events: int = 0, extra: dict[str, Any] = dict())

outcome instance-attribute

outcome: Any

holds instance-attribute

holds: int

chunks instance-attribute

chunks: int

safety_interventions instance-attribute

safety_interventions: int

verdict class-attribute instance-attribute

verdict: dict[str, Any] | None = None

events class-attribute instance-attribute

events: int = 0

extra class-attribute instance-attribute

extra: dict[str, Any] = field(default_factory=dict)

RealtimeEpisodeRunner

RealtimeEpisodeRunner(env: Any, agent: Any, *, config: RealtimeConfig | None = None, safety: Mapping[str, Any] | None = None, trace: TraceStore | None = None, bus: EventBus | None = None, operator: OperatorChannel | None = None)

cfg instance-attribute

cfg = config or RealtimeConfig()

operator instance-attribute

operator = operator or OperatorChannel()

runner instance-attribute

runner = EpisodeRunner(env, agent, trace=trace, bus=bus, safety=safety, timing=self.cfg.timing(), operator=self.operator, image_every=self.cfg.image_every, record_privileged=self.cfg.record_privileged)

run

run(task: TaskInstance, *, episode_id: str | None = None) -> RealtimeResult

hold_action

hold_action(env: Any, spec: EnvSpec, last: ndarray | None) -> np.ndarray

What to execute when no action is queued: the env's own hold_action(last), the last target for absolute control, or zero motion (gripper kept) for delta / velocity control.

hylo.serve.workcell

Workcell service (S2): runs next to the robot (or simulator) and executes episodes with a remote policy.

It owns the environment, the SafetyShield, the real-time runner and the trace, so safety never depends on the network: if the policy server stalls the robot holds and the watchdog aborts; an e-stop is latched here until an operator clears it. The policy is reached over the network (policy.protocol: "hylo" = the Hylo agent protocol, "openpi" = openpi's). Episodes are recorded as an ordinary run (split deployment by default, which no learner reads unless asked) so replay, judges and the catalog work on them.

hylo serve workcell --config workcell.yaml --port 8100

name: sim-libero
env: {type: libero, remote: {...}}                     # any env plugin: a simulator or a robot adapter
benchmark: {type: libero, params: {suite: libero_10}}  # optional: named tasks and init states
policy: {url: "ws://gpu:8000/hylo", protocol: hylo}
runtime: {clock: wall, refill_steps: 10, max_hold_steps: 40, require_reset_confirm: true, verdict_timeout_s: 60}
safety: {max_delta: 0.5}

HTTP API: hylo.workcell/v0.1 (core.remote.workcell): GET /healthz, GET /status, POST /episodes, POST /operator, WS /events (live stream).

WorkcellConfig

Bases: Contract

name instance-attribute

name: str

env instance-attribute

env: EnvRef

benchmark class-attribute instance-attribute

benchmark: ComponentRef | None = None

policy class-attribute instance-attribute

policy: PolicyEndpoint | None = None

runtime class-attribute instance-attribute

runtime: RealtimeConfig = Field(default_factory=RealtimeConfig)

safety class-attribute instance-attribute

safety: dict[str, JsonAny] = Field(default_factory=dict)

image_every class-attribute instance-attribute

image_every: int = 10

split class-attribute instance-attribute

split: str = 'deployment'

max_steps class-attribute instance-attribute

max_steps: int = 600

output_dir class-attribute instance-attribute

output_dir: str = 'runs/workcell'

load classmethod

load(path: str | Path) -> 'WorkcellConfig'

Workcell

Workcell(cfg: WorkcellConfig)

cfg instance-attribute

cfg = cfg

operator instance-attribute

operator = OperatorChannel()

env instance-attribute

env: Any = None

status instance-attribute

status: dict[str, Any] = {'state': 'idle', 'episode': None, 'last': None}

run_dir instance-attribute

run_dir = Path(cfg.output_dir) / f'workcell-{cfg.name}'

store instance-attribute

store = TraceStore(self.run_dir)

subscribe

subscribe(loop: AbstractEventLoop) -> asyncio.Queue

unsubscribe

unsubscribe(q: Queue) -> None

start

start(req: dict[str, Any]) -> dict[str, Any]

close

close() -> None

create_app

create_app(cfg: WorkcellConfig, *, token: str | None = None) -> FastAPI

serve_workcell

serve_workcell(config: str, *, host: str = '127.0.0.1', port: int = 8100, token: str | None = None) -> int