Skip to content

Commit 594dbd9

Browse files
authored
Merge pull request #56 from AI-agent-assembly/v0.0.1/AAASM-1218/feat/install_ensure_runtime
[AAASM-1218] ✨ (_install): Add ensure_runtime() install-time runtime fallback
2 parents a3c1e8f + 3d6c20d commit 594dbd9

2 files changed

Lines changed: 122 additions & 0 deletions

File tree

agent_assembly/_install.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
"""Install-time runtime binary resolution for the agent-assembly Python SDK.
2+
3+
This module is the lean, blocking presence check for the ``aasm`` sidecar
4+
binary. It is intentionally separate from :mod:`agent_assembly.runtime`,
5+
which manages the full lifecycle (port probe + subprocess spawn). The
6+
intended use is at import time or at the start of long-running scripts:
7+
fail fast with a clear install hint when the binary is unavailable, before
8+
the user discovers it via a subtle subprocess failure deep in the SDK call.
9+
"""
10+
11+
from __future__ import annotations
12+
13+
import os
14+
import shutil
15+
from pathlib import Path
16+
17+
__all__ = [
18+
"BINARY_NAME",
19+
"INSTALL_HINT",
20+
"WHEEL_BUNDLED_BIN",
21+
"ensure_runtime",
22+
]
23+
24+
BINARY_NAME = "aasm"
25+
26+
# Path where the platform-wheel ([runtime] extra) bundles the sidecar binary.
27+
# Mirrors the location runtime.py's find_aasm_binary() also searches, so
28+
# both modules observe the same wheel artifact without coordination.
29+
WHEEL_BUNDLED_BIN = Path(__file__).resolve().parent / "bin" / BINARY_NAME
30+
31+
INSTALL_HINT = (
32+
"agent-assembly runtime binary `aasm` was not found.\n"
33+
" Install the platform wheel: pip install agent-assembly[runtime]\n"
34+
" Or install manually: brew install agent-assembly/tap/aasm\n"
35+
" curl -fsSL https://get.agent-assembly.io | sh"
36+
)
37+
38+
39+
def ensure_runtime() -> Path:
40+
"""Return the resolved path to the ``aasm`` sidecar binary.
41+
42+
Search order, fast to slow:
43+
44+
1. ``$PATH`` (Homebrew tap, ``cargo install``, ``curl`` installer default).
45+
2. ``agent_assembly/bin/aasm`` bundled by the ``[runtime]`` platform wheel.
46+
47+
Raises:
48+
RuntimeError: when no binary is found on either path. The message
49+
carries :data:`INSTALL_HINT` with copy-paste install commands.
50+
"""
51+
path_hit = shutil.which(BINARY_NAME)
52+
if path_hit:
53+
return Path(path_hit)
54+
if WHEEL_BUNDLED_BIN.is_file() and os.access(WHEEL_BUNDLED_BIN, os.X_OK):
55+
return WHEEL_BUNDLED_BIN
56+
raise RuntimeError(INSTALL_HINT)

test/unit/test_install.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
"""Unit tests for agent_assembly._install — install-time runtime resolution."""
2+
3+
from __future__ import annotations
4+
5+
from pathlib import Path
6+
7+
import pytest
8+
9+
from agent_assembly import _install
10+
11+
12+
@pytest.fixture
13+
def isolate_runtime(monkeypatch, tmp_path: Path) -> Path:
14+
"""Isolate ensure_runtime() from the host environment.
15+
16+
Yields a context where:
17+
- PATH is empty (so ``shutil.which`` cannot find any system binary).
18+
- ``WHEEL_BUNDLED_BIN`` points at ``tmp_path/bin/aasm`` (missing by
19+
default; tests opt in by creating the file).
20+
"""
21+
fake_bin_dir = tmp_path / "bin"
22+
fake_bin_dir.mkdir()
23+
fake_binary = fake_bin_dir / _install.BINARY_NAME
24+
monkeypatch.setattr(_install, "WHEEL_BUNDLED_BIN", fake_binary)
25+
monkeypatch.setenv("PATH", "")
26+
return fake_binary
27+
28+
29+
def test_ensure_runtime_returns_path_match_first(monkeypatch, tmp_path: Path) -> None:
30+
"""When `aasm` is on PATH, ensure_runtime returns that resolved path."""
31+
import stat
32+
33+
bin_dir = tmp_path / "system-bin"
34+
bin_dir.mkdir()
35+
on_path = bin_dir / _install.BINARY_NAME
36+
on_path.write_text("#!/bin/sh\nexit 0\n")
37+
on_path.chmod(on_path.stat().st_mode | stat.S_IXUSR)
38+
monkeypatch.setenv("PATH", str(bin_dir))
39+
40+
resolved = _install.ensure_runtime()
41+
42+
assert resolved == on_path
43+
44+
45+
def test_ensure_runtime_falls_back_to_wheel_bundled(isolate_runtime: Path) -> None:
46+
"""When PATH has no aasm, ensure_runtime returns the wheel-bundled path."""
47+
import stat
48+
49+
fake_binary = isolate_runtime
50+
fake_binary.write_text("#!/bin/sh\nexit 0\n")
51+
fake_binary.chmod(fake_binary.stat().st_mode | stat.S_IXUSR)
52+
53+
resolved = _install.ensure_runtime()
54+
55+
assert resolved == fake_binary
56+
57+
58+
def test_ensure_runtime_raises_with_install_hint(isolate_runtime: Path) -> None:
59+
"""When no binary exists, raise RuntimeError carrying INSTALL_HINT."""
60+
# Sanity: isolate_runtime points at a path that doesn't exist yet.
61+
assert not isolate_runtime.exists()
62+
63+
with pytest.raises(RuntimeError) as exc_info:
64+
_install.ensure_runtime()
65+
66+
assert _install.INSTALL_HINT in str(exc_info.value)

0 commit comments

Comments
 (0)