Build your application#
Read this when building an XR AI application. The directions below assume the
application is under apps/<your-app>/. This directory is for applications
that are not part of the repository’s sample catalog. The XR AI SDK
distributions are not published to a package registry; they resolve through
relative [tool.uv.sources] entries in each application’s pyproject.toml.
Start from a source checkout#
Use the latest stable release by default, falling back to the latest
prerelease when no stable release exists. Use main when the application
needs unreleased changes. The documentation and checkout must use the same
ref: use the /latest/ documentation with the selected release or /main/
with main.
Create the checkout:
git clone https://github.com/NVIDIA/xr-ai.git
cd xr-ai
To use the latest stable release, or the latest prerelease when no stable release exists, run:
release_tag="$(git tag --list 'v*' | python3 .github/scripts/select_latest_docs_release.py)"
if [ -n "$release_tag" ]; then
git checkout "$release_tag"
else
echo "No release tag found; continuing on main." >&2
fi
After checking out a release, confirm that
docs/source/guides/building-your-app.md and
skills/build-your-app/SKILL.md exist in the checkout. If either is absent,
ask before switching the documentation, skill, and checkout to main.
Read AGENTS.md, then review the
system requirements and
credential sources before resolving the
application. The copied reference application uses local model services by
default. When configuring hosted LLM or VLM endpoints, refer to the “Hosting
models on NVIDIA NIM” section of AI services.
Choose the layers your application needs#
Package |
Use it for |
|---|---|
xr-ai-launcher |
Stdlib-only process management for starting DeviceIOHub, workers, and application services in dependency order. |
xr-ai-hub-client |
IPC with DeviceIOHub, participant events, data and audio messages, and live frame access. |
xr-ai-logging |
Shared logging setup used by the copied orchestrator and worker. |
xr-ai-models |
Typed LLM, VLM, STT, TTS, and embedding clients selected from deployment profiles. |
xr-ai-agent-runtime |
Typed agent lifecycle, registration, publication, and participant-scoped subscriptions. |
xr-ai-tools |
Typed |
xr-ai-voice |
The |
xr-ai-voicegate |
Wake-phrase and follow-up-turn gating used by the copied voice application. |
xr-ai-web-events |
A bounded browser view for compact application events explicitly selected by the application. |
The distribution names, import names, and source directories are not always
identical. In particular, xr-ai-hub-client comes from
agent-sdk/xr-ai-hub, and xr-ai-agent-runtime comes from
agent-sdk/xr-ai-runtime. Refer to Agent SDK for package
ownership boundaries and composition guidance.
Copy the reference application#
agent-samples/simple-vlm-example/ is the smallest complete voice-and-vision
application. Copy its tracked files from the repository root and rename its
Python package and worker configuration:
mkdir -p apps/my-app
git archive HEAD agent-samples/simple-vlm-example |
tar -x --strip-components=2 -C apps/my-app
mv apps/my-app/worker/simple_vlm_example_worker apps/my-app/worker/my_app_worker
mv apps/my-app/yaml/simple_vlm_example_worker.yaml apps/my-app/yaml/my_app_worker.yaml
Replace the sample names consistently:
Existing name |
Application name |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
Substitute the application’s real kebab-case name for my-app, then derive its
snake-case package and CamelCase class names mechanically. Apply the
replacements in the listed order across the entire copied tree, including
comments and docstrings. Update any remaining agent-samples/
working-directory references to apps/, and change prose that still describes
the application as a repository sample. The following check should produce no
output:
grep -rniE 'simple[-_ ]?vlm' apps/my-app
The apps/ and agent-samples/ directories are at the same depth, so the
copied projects’ relative sources continue to work. Keep each
[tool.uv.sources] entry paired with its distribution in
[project].dependencies. Every unpublished XR AI dependency must retain a
repository source mapping.
Use this map to separate reusable scaffolding from the VLM example:
Files |
Reuse |
Replace or adapt |
|---|---|---|
|
Keep the |
Rename the project, command, logging namespace, worker command, and worker YAML path. Add only application-owned service processes. |
|
Keep the typed DeviceIOHub configuration shape. |
Set the room, ports, web client, and network behavior. |
|
Keep the named-package layout, console entry point, argument parsing, and delegation to |
Rename the distribution, package, and entry point. Remove dependencies only after their imports and features are gone. |
|
Keep typed loading and paths resolved relative to the YAML file. |
Replace the prompt, frame, model, voice, and application-specific settings with the fields the application consumes. |
|
Keep the |
Replace the VLM warmup, model roles, and paired |
|
Keep the participant-scoped task ownership, detached-task context and scope, and cancellation patterns. Refer to xr-ai-runtime. |
Replace the vision question-and-answer workflow, topics, tool calls, and prompt. |
|
Keep the adapter and endpoint separation. |
Declare only the logical model roles, client adapters, and shared endpoints the application needs. Do not add model-server deployment or readiness settings. |
|
Keep it when speech input needs wake phrases and follow-up turns. |
Tune the behavior or remove it with the voice runtime. |
|
Keep a short configure and run path. |
Describe the application, its owned and reused processes, settings, and exact start commands. |
Rewrite the copied README as application-owned documentation. Replace its title
and the “Simple VLM example” and “This sample” descriptions, and remove links to
the Simple VLM sample reference. In particular, make sure the name replacements
did not leave a nonexistent /reference/my-app.html link. Change its working
directory to apps/my-app/ and use the model-server command shown below.
The copied DeviceIOHub YAML contains development-only credential placeholders.
They can remain in place: LIVEKIT_API_KEY and LIVEKIT_API_SECRET from the
environment overwrite them at startup. Never put actual keys or tokens in
source files. Refer to Credentials for the supported
credential sources.
Frame selection and vision inference use separate public paths: import
live-frame selection from xr_ai_tools.current_frame and image inference from
xr_ai_tools.vision.
Resolve and run the application#
From apps/my-app/, resolve both projects before starting the stack:
uv --config-file ../../uv.toml sync
uv --config-file ../../uv.toml sync --project worker
The shared model launcher requires HF_TOKEN by default. Configure it through
one of the sources in Credentials before starting the
copied VLM application. If the weights are already cached or unauthenticated
rate limits are acceptable, append --allow-anonymous to the model_servers
command below.
The copied application reuses STT, VLM, and TTS services. Start those shared services first and wait for the launcher to report that all processes are ready:
uv --config-file ../../uv.toml run \
--project ../../model-server-samples/model-servers model_servers
Then start the application from the same directory:
uv --config-file ../../uv.toml run my_app
Open the authenticated web-client URL printed by DeviceIOHub, grant the media permissions the application needs, and connect.
The application launcher owns only DeviceIOHub, the worker, and any other
application processes. The separate model-server launcher owns shared model
startup and readiness. The copied worker does not poll the STT, VLM, or TTS
health endpoints; it explicitly exercises VLM inference as an application
warmup before announcing readiness. Keep or replace that probe according to the
capabilities the application must warm or verify. When a model role changes,
update yaml/models.json and the worker’s service construction. Do not add
shared model services to the application’s PROCESSES list.
Verify without hardware#
From the repository root, load the worker configuration before starting services. This check imports the renamed package, reads the application YAML, and resolves packaged files such as the system prompt:
uv --config-file uv.toml run --project apps/my-app/worker \
python - <<'PY'
from pathlib import Path
from my_app_worker.config import load_config
load_config(Path("apps/my-app/yaml/my_app_worker.yaml"))
PY
Exercise model wire behavior with tests/_stub_openai.py, which provides an
httpx.MockTransport implementation of OpenAI-compatible endpoints. The wire
tests exercise xr_ai_models clients without importing the application worker.
Use tests/test_simple_vlm_example_wire.py as the small STT, VLM, and TTS
wire-format pattern; use tests/test_xr_render_demo_wire.py for LLM tool-call
flows.
The hub, make_connector, and make_processor fixtures in
tests/conftest.py run DeviceIOHub IPC over local ZMQ sockets. They demonstrate
participant routing and worker-facing endpoint coverage without LiveKit, a
camera, a microphone, Docker, or a GPU. Pytest does not load that file for
tests under apps/my-app/ because it is not an ancestor of the application
test directory. Keep application-owned fixtures in
apps/my-app/tests/conftest.py; copy and adapt only the repository fixtures the
application needs.
Add the application’s pytest settings to apps/my-app/pyproject.toml:
[tool.pytest.ini_options]
asyncio_mode = "auto"
pythonpath = ["worker"]
markers = [
"gpu: requires local GPU, Docker, or NVENC",
"integration: starts a real service process",
]
Point model clients at a StubOpenAI-style transport, then run the application
test directory explicitly with the repository test environment:
uv --config-file uv.toml run --project tests \
pytest -v apps/my-app/tests -m "not gpu"
Refer to Testing for the complete test commands and marker rules.
Understand the application boundary#
Repository file checks exclude the top-level apps/ directory. Application
projects do not enter DEPENDENCIES.md or the dependency manifest, and the
repository’s Ruff and SPDX checks do not inspect application-owned files. The
sample documentation, configuration catalogs, and test discovery remain scoped
to agent-samples/ and tests/. Git ignores apps/* by default so private
application work is not staged accidentally. Remove that ignore rule when the
application owner wants to track the application in the fork.
The rest of the checkout remains repository-owned. Commit-wide hooks such as
DCO sign-off still run when installed, and changes outside apps/ continue to
follow AGENTS.md and the repository’s contribution checks.