Skip to content

hylo-playground

Hylo playground: a local web UI to explore runs, replay episodes, run live episodes, compare agents and inspect improvement campaigns

Repository hylo-studio, packages/hylo-playground. Licence: Apache-2.0. Depends on: hylo-core, hylo-eval, hylo-catalog, fastapi>=0.110, uvicorn>=0.29, pillow>=10.

Registered entry points
Group Name Object
commands playground hylo.playground.app:main
Module Summary
hylo.playground.app Hylo playground: a local web UI over the existing system (FastAPI backend + React frontend)
hylo.playground.control Control plane (S4): the playground backend as the operator's single entry point to workcells and policy servers
hylo.playground.jobs Background jobs: the playground launches the same CLI commands a user would type (hylo run / evolve / learn /
hylo.playground.live Live sandbox: run one episode of any experiment config and stream it to the browser as it happens
hylo.playground.render Image encoding for the browser

hylo.playground.app

Hylo playground: a local web UI over the existing system (FastAPI backend + React frontend).

hylo-playground --root runs --port 8765            # then open http://127.0.0.1:8765

Read-only pages go through the catalog API (hylo.catalog), never through other packages' internals. Actions go through public paths only: live episodes through the kernel and SafetyShield, components through the plugin registry, jobs and approval / rollback through the hylo command. Protected settings (safety, benchmark, trace, scoring) cannot be overridden from the UI. Binds to 127.0.0.1 by default; with --token every API call needs it.

It is also the control plane for workcells and policy servers (control.py): --workcell name=url, --policy name=url, and per-role tokens (--role-token viewer=... operator=... approver=...).

STATIC module-attribute

STATIC = Path(__file__).parent / 'static'

DEFAULT_JUDGE module-attribute

DEFAULT_JUDGE = 'artifact:l10-trace-v2@54a1efadf00d34d8'

APPROVER_PATHS module-attribute

APPROVER_PATHS = ('/api/campaign/approve', '/api/campaign/rollback')

create_app

create_app(root: str | Path = 'runs', *, repo: str | Path = '.', token: str | None = None, judge: str | None = None, workcells: list[Endpoint] | None = None, policies: list[Endpoint] | None = None, role_tokens: dict[str, str] | None = None) -> FastAPI

main

main(argv: list[str] | None = None) -> int

run the playground: run explorer, live sandbox, campaigns and the workcell control plane

hylo.playground.control

Control plane (S4): the playground backend as the operator's single entry point to workcells and policy servers.

It never sends actions. It lists workcells and policy servers, starts episodes on a workcell (choosing the policy endpoint), relays operator events (reset confirmation, verdicts, stop, e-stop, clearing a fault) and proxies a workcell's live event stream to the browser.

Roles, by token (--role-token viewer=... --role-token operator=... --role-token approver=...): - viewer: read everything, and press the e-stop (stopping is never gated behind a higher role); - operator: start episodes, reset / verdict / stop / clear a fault, live sandbox, jobs; - approver: everything, plus promote / roll back campaign versions. Without role tokens (and without --token) the local user has every role.

ROLES module-attribute

ROLES = ('viewer', 'operator', 'approver')

Endpoint dataclass

Endpoint(name: str, url: str, token: str | None = None)

name instance-attribute

name: str

url instance-attribute

url: str

token class-attribute instance-attribute

token: str | None = None

parse classmethod

parse(text: str) -> 'Endpoint'

name=url or name=url#token

Roles

Roles(tokens: dict[str, str] | None = None, legacy: str | None = None)

by_token instance-attribute

by_token = {t: r for r, t in (tokens or {}).items()}

open instance-attribute

open = not self.by_token

role

role(token: str | None) -> str | None

allows staticmethod

allows(role: str | None, needed: str) -> bool

ControlPlane

ControlPlane(workcells: list[Endpoint], policies: list[Endpoint], timeout_s: float = 3.0)

workcells instance-attribute

workcells = {w.name: w for w in workcells}

policies instance-attribute

policies = {p.name: p for p in policies}

timeout_s instance-attribute

timeout_s = timeout_s

workcell

workcell(name: str) -> Endpoint

list_workcells

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

list_policies

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

start

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

operator

operator(name: str, event: dict[str, Any]) -> Any

events_url

events_url(name: str) -> str

hylo.playground.jobs

Background jobs: the playground launches the same CLI commands a user would type (hylo run / evolve / learn / calibrate) as subprocesses, with their output in runs/playground/jobs/.log. Nothing bypasses the CLI's checks.

KINDS module-attribute

KINDS = {'run': lambda a: ['run', a['config'], *(['--split', a['split']] if a.get('split') else []), *[x for kv in a.get('sets', []) for x in ('--set', kv)]], 'evolve': lambda a: ['evolve', 'run', a['config'], *[x for kv in a.get('sets', []) for x in ('--set', kv)]], 'learn': lambda a: ['learn', a['job'], *(['--python', a['python']] if a.get('python') else [])], 'calibrate': lambda a: ['calibrate', '--judge', json.dumps(a['judge']), '--runs', *a['runs'], '--splits', *a.get('splits', ['search_val']), *(['--task-prefix', a['task_prefix']] if a.get('task_prefix') else []), *(['--limit', str(a['limit'])] if a.get('limit') else []), *(['--out', a['out']] if a.get('out') else [])]}

JobManager

JobManager(root: str | Path, cwd: str | Path)

dir instance-attribute

dir = Path(root) / 'playground' / 'jobs'

cwd instance-attribute

cwd = Path(cwd)

start

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

list

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

log

log(jid: str, tail: int = 200) -> str

stop

stop(jid: str) -> None

run_cli

run_cli(args: list[str], *, cwd: str | Path, timeout: float = 300) -> str

Run one hylo command synchronously and return its last output line; raise with its output on failure.

hylo.playground.live

Live sandbox: run one episode of any experiment config and stream it to the browser as it happens.

The episode runs through the unchanged kernel (EpisodeRunner, SafetyShield). A bus subscriber turns events into compact messages (camera frames as JPEG every step); the trace keeps every trace.image_every-th frame like any run, in runs/playground/live/, under split "playground", which no learner ever reads. Environment and agent are kept warm between episodes of the same configuration (loading a VLA takes tens of seconds).

User controls: task and seed from the benchmark, config overrides (protected scopes refused), an instruction override (the agent sees the user's text; the environment's task and success check are unchanged), stop.

PROTECTED module-attribute

PROTECTED = ('safety', 'benchmark', 'trace', 'scorers', 'evolution', 'schema_version')

SPLITS module-attribute

SPLITS = ('validation', 'search', 'search_val', 'gate', 'heldout', 'train', 'test')

Stopped

Bases: RuntimeError

LiveManager

LiveManager(root: str | Path)

out_root instance-attribute

out_root = Path(root) / 'playground' / 'live'

stop instance-attribute

stop = threading.Event()

busy instance-attribute

busy = False

close

close() -> None

run

run(request: dict[str, Any], emit: Callable[[dict[str, Any]], None]) -> dict[str, Any]

load_config

load_config(path: str | Path, overrides: dict[str, Any] | None = None) -> ExperimentConfig

task_options

task_options(cfg: ExperimentConfig) -> dict[str, Any]

hylo.playground.render

Image encoding for the browser.

jpeg

jpeg(img: Any, quality: int = 85) -> bytes