Launcher and process model#
Every sample is self-contained: running it starts the XR-Media-Hub and all required processes automatically. No separate server launch step.
Each sample has two sub-projects:
Sub-project |
Role |
Dependencies |
|---|---|---|
|
Orchestrator — declares process list in code, launches all |
|
|
Agent worker — connects to hub via IPC, runs agent logic |
|
Configuration convention — the YAML configuration path for each process is declared
explicitly in the orchestrator’s PROCESSES list via the config= field of
Process. The launcher passes it as --config <path> to the subprocess.
All sample configuration files live in the yaml/ directory. Omit config= for
processes that use their own internal defaults.
The orchestrator declares the process sequence in code:
_BASE = Path(__file__).resolve().parent # sample root
PROCESSES = [
Process("hub", "../../server-runtime", "xr_media_hub",
config="yaml/xr_media_hub.yaml"),
Process("worker", "worker", "my_agent_worker",
config="yaml/my_agent_worker.yaml"),
# Optional shared components — add as needed:
# Process("cloudxr", "../../cloudxr-runtime", "cloudxr_runtime",
# config="yaml/cloudxr_runtime.yaml"),
# Process("mcp", "../../agent-mcp-servers/oxr-mcp", "oxr_mcp_server",
# config="yaml/oxr_mcp_server.yaml"),
]
def run() -> None:
run_stack(PROCESSES, _BASE)
Rules#
Processes start serially — each process must create its
--ready-filebefore the next one starts. Declare processes in dependency order (hub before workers, cloudxr before MCP servers that open OpenXR sessions, etc.).Every process accepts
--ready-file <path>and mustPath(path).touch()when it is fully initialized and ready to serve requests.xr_media_hubalways runs as its own process — never embedded in-process.The worker never imports anything from
server-runtimeorutils/xr-ai-launcher/.Process management lives in
utils/xr-ai-launcher/, not inside any process it manages.run_stackis fail-fast: if any process exits, the rest are terminated.
Serial and parallel items#
The stack is declared as a sequence of Process or Parallel items:
Process— started alone; the launcher waits for it to signal ready before moving on.Parallel([p1, p2, ...])— all processes in the group are started at once; the launcher waits for every member to signal ready before the next item in the sequence begins. If any member exits before signaling ready, the launcher shuts everything down, just as it would for a serial process.
PROCESSES = [
Process("hub", "../../server-runtime", "xr_media_hub"),
Parallel([
Process("stt", "../../ai-services/stt-server", "stt_server"),
Process("tts", "../../ai-services/tts/piper", "piper_tts_server"),
]),
Process("worker", "worker", "my_agent_worker"),
]
How run_stack works#
For each process the launcher:
Resolves the project directory and YAML configuration from the sample root (
base— all relative paths inProcess.projectandProcess.configare resolved against it).Spawns
uv run --project <dir> <command> --config <yaml> --ready-file <f>in a new process group, so the whole group (uvplus its children) can be torn down together rather than leaving orphans.Waits for the process to create
(the ready file), printing a progress line every five seconds so slow starts remain visible. Once all processes are ready, monitors them: any exit triggers a graceful shutdown of the rest (SIGTERM, escalating to SIGKILL after a timeout).
Each process is responsible for creating its own ready file at the moment it is fully initialized and able to serve requests — after model warm-up, after the IPC socket connects, after the HTTP server starts listening, etc.
Pass exit_after_ready=True to run_stack to return immediately once
everything is ready instead of monitoring — useful for launchers whose
processes are all launch_mode="persist" and should outlive the orchestrator
(e.g. model-servers).
The --ready-file protocol#
The launcher injects --ready-file <path> into every spawned command. The
process must Path(path).touch() the moment it is fully initialized and able
to serve requests. The launcher blocks on the file’s existence; if the process
exits before creating it, startup is aborted and the whole stack is torn down.
This makes readiness explicit and process-defined: a model server signals ready
after weights load, an HTTP server after it starts listening, a worker after
its IPC socket connects.
launch_mode: own, persist, reuse#
Process.launch_mode controls spawn and shutdown behaviour:
"own"(default) — the launcher spawns this process and kills it on shutdown."persist"— the launcher spawns this process but leaves it running on shutdown. Use for heavy model servers that should survive stack restarts (e.g. vLLM containers). Cleanup is the caller’s responsibility. The optionalportfield is used to stop such persistent services."reuse"— the launcher does not spawn this process; it is assumed to be already running (e.g. started bymodel-servers). The entry in the process list documents the dependency; the launcher skips it entirely and does not kill it on shutdown.
On a clean ready-exit, persist and reuse processes are left running. On an
abort during startup (Ctrl-C, or a process exiting before it signals ready)
the launcher tears down everything, including persist processes, so no
half-started service is left behind.
Adding a new managed process#
There is no per-process launcher module to write — the launcher spawns any uv sub-project generically. To add a new process to a stack:
Make the sub-project’s entry-point command accept
--ready-file <path>(touch it once ready) and, if it takes configuration,--config <path>.Add a
Process(orParallel) entry to the orchestrator’sPROCESSESlist, in dependency order, pointing at the sub-project directory and its entry-point command — exactly like thehubandworkerentries above.