Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions dimos/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,13 @@ def pytest_sessionstart(session):
_arm_crash_dumps()


def pytest_ignore_collect(collection_path: pathlib.Path) -> bool | None:
# Nested Python projects own their dependencies and test invocation.
if collection_path.is_dir() and (collection_path / "pyproject.toml").is_file():
return True
return None


def pytest_configure(config):
config.addinivalue_line(
"markers",
Expand Down
71 changes: 51 additions & 20 deletions dimos/experimental/isolated_python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,14 @@ management. Its API is experimental and may change without compatibility aliases

## Project layout

Place the host contract beside a `python/` project that contains the concrete
runtime:
Keep the host contract in `dimos/` and its isolated project outside the package:

```text
my_module/
├── contract.py
└── python/
├── pyproject.toml
└── my_runtime/
└── runtime.py
dimos/my_module/contract.py
native/python/my_module/
├── pyproject.toml
└── my_runtime/
└── runtime.py
```

Define the host-visible contract:
Expand All @@ -33,6 +31,7 @@ class MultiplierConfig(IsolatedPythonModuleConfig):


class Multiplier(IsolatedPythonModule):
project_dir = "native/python/my_module"
implementation = "my_runtime.runtime:MultiplierRuntime"
config: MultiplierConfig

Expand All @@ -41,13 +40,13 @@ class Multiplier(IsolatedPythonModule):
raise NotImplementedError
```

The sibling runtime imports and implements that contract:
The isolated runtime imports and implements that contract:

```python skip
from typing import Any

from dimos.core.core import rpc
from my_module.contract import Multiplier
from dimos.my_module.contract import Multiplier


class MultiplierRuntime(Multiplier):
Expand All @@ -66,23 +65,39 @@ a contract stub or changes its signature or classification.

## Runtime behavior

During `build()`, dimOS syncs the sibling project and installs the host dimOS
with its dependencies into a cached `uv run --with` overlay. The first build can
take minutes to download; later builds reuse the cache. If `pixi.toml` exists,
Pixi supplies `uv`. If `uv.lock` exists, dimOS uses `--frozen` and treats the
lockfile as the source of truth.

Source checkouts make the current dimOS checkout available to the runtime.
Installed hosts let `uv` resolve `dimos`, so the host and runtime versions may
differ. The sibling project's `.python-version` and `requires-python` select its
Python version.
During `build()`, dimOS uses `uv run` to sync the declared project and prepare a
cached overlay containing dimOS from the shared checkout and its dependencies.
The first build can take minutes to download; later builds reuse the cache.
If `pixi.toml` exists, Pixi supplies `uv`. If `uv.lock` exists, dimOS uses
`--frozen` and treats the lockfile as the source of truth.

The runtime project and child dimOS come from `get_project_root()`, the shared
LFS checkout helper. Development uses the current checkout, including local edits.
Installed hosts reuse the cached repository or clone `main` on first use. The child
installs dimOS from that checkout with `--with-editable`; its revision may differ
from the host's. Existing clones are not updated automatically. The checkout must
contain the declared project. Restart running modules after editing sources.

The project's `.python-version` and `requires-python` select its Python
version. Environments are stored under the dimOS cache directory in
`isolated-python/<project-path-hash>/.venv`, so projects do not share environments.
Preparation also warms the DimOS overlay before starting the readiness deadline.

Runtime projects are not packaged in dimOS wheels or source distributions.
The examples use `[tool.uv] package = false` and import runtime code from the
project working directory. Load models and download checkpoints in `start()`,
keeping imports and construction lightweight.

The host contract retains the public module name and forwards contract RPCs to a
unique internal endpoint. Ordinary dimOS serialization and transport handle RPC
values, exceptions, timeouts, async methods, skills, streams, and module
references. Restarting the contract starts a fresh interpreter and reloads the
runtime package.

Runtime classes and tests live outside `dimos/`, so host blueprint discovery and
source checks do not scan them. Run runtime tests with their project's pytest
configuration and `--confcutdir=.` to avoid loading host fixtures.

## Example

The source tree includes a complete example with a locked external project:
Expand All @@ -93,3 +108,19 @@ uv run python -m dimos.experimental.isolated_python.example.run

The example demonstrates streams, RPCs, skills, an injected module reference,
restart behavior, and automatic shutdown.

## Runtime development

Root pytest and mypy check `dimos/`; isolated projects live outside that tree. Run their tests and type
checks inside their own environment. For GraspGenX, from the repository root:

```bash
cd native/python/graspgenx
export UV_PROJECT_ENVIRONMENT="${XDG_CACHE_HOME:-$HOME/.cache}/dimos/graspgenx-tests"
uv run --frozen --group tests --with-editable ../../.. python -m pytest
uv run --frozen --group lint --with-editable ../../.. python -m mypy
```

The tests mock the model backend and need no GPU or checkpoints. Runtime mypy
reads the annotated dimOS and GraspGenX source despite their missing `py.typed`
markers. Each runtime owns its lint configuration and dependencies.
1 change: 1 addition & 0 deletions dimos/experimental/isolated_python/example/contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ class Config(IsolatedPythonModuleConfig):
class ExampleExternal(IsolatedPythonModule):
"""Multiply incoming integers in an isolated Python environment."""

project_dir = "native/python/example"
implementation = "example_external.runtime:ExampleExternalRuntime"
config: Config

Expand Down
34 changes: 16 additions & 18 deletions dimos/experimental/isolated_python/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@
# See the License for the specific language governing permissions and
# limitations under the License.

"""Run a concrete Module subclass in an isolated sibling Python project."""
"""Run a concrete Module subclass in an isolated repository Python project."""

from __future__ import annotations

import inspect
from hashlib import sha256
import os
from pathlib import Path
import pickle
Expand All @@ -26,27 +26,24 @@
import time
from typing import Any, ClassVar

from dimos.constants import DIMOS_PROJECT_ROOT
from dimos.constants import CACHE_DIR
from dimos.core.core import rpc
from dimos.core.module import Module
from dimos.core.native_module import NativeModule, NativeModuleConfig
from dimos.core.rpc_client import RPCClient
from dimos.utils.data import get_project_root
from dimos.utils.generic import short_id
from dimos.utils.logging_config import setup_logger

logger = setup_logger()


def isolated_python_run_command(project: Path, *command: str) -> list[str]:
"""Run a command with the host DimOS available in an isolated project."""
"""Run a project with dimOS from the shared source checkout."""
args = ["uv", "run"]
if (project / "uv.lock").is_file():
args.append("--frozen")
if (DIMOS_PROJECT_ROOT / "pyproject.toml").is_file():
args.extend(("--with-editable", str(DIMOS_PROJECT_ROOT)))
else:
# Installed hosts intentionally accept the newest compatible DimOS.
args.extend(("--with", "dimos"))
args.extend(("--with-editable", str(get_project_root())))
args.extend(command)
if (project / "pixi.toml").is_file():
return ["pixi", "run", "--executable", *args]
Expand All @@ -56,7 +53,7 @@ def isolated_python_run_command(project: Path, *command: str) -> list[str]:
class IsolatedPythonModuleConfig(NativeModuleConfig):
"""Process settings for an isolated Python module."""

# Isolated Python modules resolve their real command from the sibling project.
# Isolated Python modules resolve their real command from the repository project.
executable: str = "uv"
startup_timeout: float = 30.0
output_limit: int = 64 * 1024
Expand All @@ -73,13 +70,14 @@ class IsolatedPythonModule(NativeModule):
"""A host RPC contract implemented by an isolated Python subclass.

Contract classes set :attr:`implementation` to an import reference in a
sibling ``python/`` project. Calls to RPCs introduced by the contract are
repository project selected by :attr:`project_dir`. Contract RPCs are
forwarded to the concrete runtime subclass. Framework and lifecycle RPCs
remain on the host facade.
"""

config: IsolatedPythonModuleConfig
implementation: ClassVar[str]
project_dir: ClassVar[str]

_isolated_python_runtime: bool
_runtime_client: RPCClient | None
Expand Down Expand Up @@ -109,12 +107,11 @@ def __getattribute__(self, name: str) -> Any:

@property
def runtime_project(self) -> Path:
source = Path(inspect.getfile(type(self))).resolve()
project = source.parent / "python"
project = get_project_root() / self.project_dir
if not project.is_dir():
raise FileNotFoundError(
f"Isolated Python runtime project is missing: {project}; "
"create a sibling 'python/' directory"
"ensure the shared dimOS checkout contains this project"
)
if not (project / "pyproject.toml").is_file():
raise FileNotFoundError(
Expand All @@ -123,8 +120,8 @@ def runtime_project(self) -> Path:
return project

def _prepare_command(self) -> list[str]:
# `uv run` syncs the sibling project and builds the cached overlay that
# holds the host DimOS with its dependencies. Doing it here keeps the
# `uv run` syncs the declared project and builds the cached overlay that
# holds the shared checkout’s dimOS with its dependencies. Doing it here keeps the
# first install, which can take minutes, out of the startup timeout.
return isolated_python_run_command(self.runtime_project, "python", "-c", "pass")

Expand Down Expand Up @@ -156,13 +153,14 @@ def _runtime_env(self) -> dict[str, str]:
env.pop("VIRTUAL_ENV", None)
env.pop("UV_PYTHON", None)
env.pop("UV_PROJECT_ENVIRONMENT", None)
project_key = sha256(str(self.runtime_project).encode()).hexdigest()[:16]
env["UV_PROJECT_ENVIRONMENT"] = str(CACHE_DIR / "isolated-python" / project_key / ".venv")
env.update(self.config.extra_env)
return env

def _run_prepare(self) -> None:
command = self._prepare_command()
result = subprocess.run(
command,
self._prepare_command(),
cwd=self.runtime_project,
env=self._runtime_env(),
capture_output=True,
Expand Down
2 changes: 1 addition & 1 deletion dimos/experimental/isolated_python/test_end_to_end.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ def test_example_script_exits_after_printing_results() -> None:
cwd=repository,
capture_output=True,
text=True,
timeout=30,
timeout=300,
)

assert result.returncode == 0, result.stderr
Expand Down
Loading