From 94a33d61b4391eef0a16f35a70f672f24a63eac2 Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Sat, 23 May 2026 02:09:58 +0800 Subject: [PATCH 1/6] =?UTF-8?q?=E2=9C=A8=20(=5Finstall):=20Add=20module=20?= =?UTF-8?q?skeleton=20with=20BINARY=5FNAME,=20WHEEL=5FBUNDLED=5FBIN,=20INS?= =?UTF-8?q?TALL=5FHINT?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Establishes the install-time presence-check module. Module-level constants only; ensure_runtime() lands in the next commit. WHEEL_BUNDLED_BIN points to agent_assembly/bin/aasm — the same path runtime.py already searches, so both modules observe the same wheel artifact without inter-module coordination. INSTALL_HINT lists all install channels (pip [runtime] extra, Homebrew tap, curl installer) so users hitting the missing-binary case get copy-paste recovery commands. AAASM-1218 --- agent_assembly/_install.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 agent_assembly/_install.py diff --git a/agent_assembly/_install.py b/agent_assembly/_install.py new file mode 100644 index 0000000..fc45f21 --- /dev/null +++ b/agent_assembly/_install.py @@ -0,0 +1,33 @@ +"""Install-time runtime binary resolution for the agent-assembly Python SDK. + +This module is the lean, blocking presence check for the ``aasm`` sidecar +binary. It is intentionally separate from :mod:`agent_assembly.runtime`, +which manages the full lifecycle (port probe + subprocess spawn). The +intended use is at import time or at the start of long-running scripts: +fail fast with a clear install hint when the binary is unavailable, before +the user discovers it via a subtle subprocess failure deep in the SDK call. +""" + +from __future__ import annotations + +from pathlib import Path + +__all__ = [ + "BINARY_NAME", + "INSTALL_HINT", + "WHEEL_BUNDLED_BIN", +] + +BINARY_NAME = "aasm" + +# Path where the platform-wheel ([runtime] extra) bundles the sidecar binary. +# Mirrors the location runtime.py's find_aasm_binary() also searches, so +# both modules observe the same wheel artifact without coordination. +WHEEL_BUNDLED_BIN = Path(__file__).resolve().parent / "bin" / BINARY_NAME + +INSTALL_HINT = ( + "agent-assembly runtime binary `aasm` was not found.\n" + " Install the platform wheel: pip install agent-assembly[runtime]\n" + " Or install manually: brew install agent-assembly/tap/aasm\n" + " curl -fsSL https://get.agent-assembly.io | sh" +) From 8d99ad2d17706f5fb51b5946c67f02868d686c56 Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Sat, 23 May 2026 02:10:31 +0800 Subject: [PATCH 2/6] =?UTF-8?q?=E2=9C=A8=20(=5Finstall):=20Add=20ensure=5F?= =?UTF-8?q?runtime()=20with=20PATH/wheel/RuntimeError=20fallback?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three-branch resolution: 1. shutil.which(BINARY_NAME) — fastest, covers Homebrew tap, cargo install, and any binary on the user's PATH. 2. WHEEL_BUNDLED_BIN file+executable check — covers the platform-wheel install (`pip install agent-assembly[runtime]`). 3. Raise RuntimeError(INSTALL_HINT) — fail fast with copy-paste recovery commands. Distinct from runtime.py's find_aasm_binary() in that this is a synchronous presence check returning the Path (or raising), not a lifecycle helper that spawns the sidecar. Suitable for use at SDK import time or early in long-running scripts. AAASM-1218 --- agent_assembly/_install.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/agent_assembly/_install.py b/agent_assembly/_install.py index fc45f21..fad0c69 100644 --- a/agent_assembly/_install.py +++ b/agent_assembly/_install.py @@ -10,12 +10,15 @@ from __future__ import annotations +import os +import shutil from pathlib import Path __all__ = [ "BINARY_NAME", "INSTALL_HINT", "WHEEL_BUNDLED_BIN", + "ensure_runtime", ] BINARY_NAME = "aasm" @@ -31,3 +34,23 @@ " Or install manually: brew install agent-assembly/tap/aasm\n" " curl -fsSL https://get.agent-assembly.io | sh" ) + + +def ensure_runtime() -> Path: + """Return the resolved path to the ``aasm`` sidecar binary. + + Search order, fast to slow: + + 1. ``$PATH`` (Homebrew tap, ``cargo install``, ``curl`` installer default). + 2. ``agent_assembly/bin/aasm`` bundled by the ``[runtime]`` platform wheel. + + Raises: + RuntimeError: when no binary is found on either path. The message + carries :data:`INSTALL_HINT` with copy-paste install commands. + """ + path_hit = shutil.which(BINARY_NAME) + if path_hit: + return Path(path_hit) + if WHEEL_BUNDLED_BIN.is_file() and os.access(WHEEL_BUNDLED_BIN, os.X_OK): + return WHEEL_BUNDLED_BIN + raise RuntimeError(INSTALL_HINT) From 8b7b1c0e12b183fbecc2f99e834d7babbcf7650f Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Sat, 23 May 2026 02:12:26 +0800 Subject: [PATCH 3/6] =?UTF-8?q?=E2=9C=85=20(test):=20Add=20isolate=5Frunti?= =?UTF-8?q?me=20fixture=20for=20=5Finstall=20module=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pytest fixture that scrubs the host environment so ensure_runtime() tests are deterministic: * PATH is emptied — shutil.which() finds no system binary. * WHEEL_BUNDLED_BIN is patched to a tmp_path location — tests can create or skip the file to exercise each branch. Yields the fake-binary path so tests can populate it on demand. AAASM-1218 --- test/unit/test_install.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 test/unit/test_install.py diff --git a/test/unit/test_install.py b/test/unit/test_install.py new file mode 100644 index 0000000..820c06f --- /dev/null +++ b/test/unit/test_install.py @@ -0,0 +1,26 @@ +"""Unit tests for agent_assembly._install — install-time runtime resolution.""" + +from __future__ import annotations + +from pathlib import Path + +import pytest + +from agent_assembly import _install + + +@pytest.fixture +def isolate_runtime(monkeypatch, tmp_path: Path) -> Path: + """Isolate ensure_runtime() from the host environment. + + Yields a context where: + - PATH is empty (so ``shutil.which`` cannot find any system binary). + - ``WHEEL_BUNDLED_BIN`` points at ``tmp_path/bin/aasm`` (missing by + default; tests opt in by creating the file). + """ + fake_bin_dir = tmp_path / "bin" + fake_bin_dir.mkdir() + fake_binary = fake_bin_dir / _install.BINARY_NAME + monkeypatch.setattr(_install, "WHEEL_BUNDLED_BIN", fake_binary) + monkeypatch.setenv("PATH", "") + return fake_binary From 97dcef583609ec33f7cfcd588f41e4db61891a5d Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Sat, 23 May 2026 02:13:10 +0800 Subject: [PATCH 4/6] =?UTF-8?q?=E2=9C=85=20(test):=20Add=20ensure=5Fruntim?= =?UTF-8?q?e=20returns=20PATH-resolved=20binary?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Verifies the fast-path: a stub executable on PATH wins, regardless of whether the wheel-bundled path is present. Uses tmp_path to construct an isolated PATH so the test doesn't depend on the host's real `aasm` install state. AAASM-1218 --- test/unit/test_install.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/unit/test_install.py b/test/unit/test_install.py index 820c06f..3f91f03 100644 --- a/test/unit/test_install.py +++ b/test/unit/test_install.py @@ -24,3 +24,19 @@ def isolate_runtime(monkeypatch, tmp_path: Path) -> Path: monkeypatch.setattr(_install, "WHEEL_BUNDLED_BIN", fake_binary) monkeypatch.setenv("PATH", "") return fake_binary + + +def test_ensure_runtime_returns_path_match_first(monkeypatch, tmp_path: Path) -> None: + """When `aasm` is on PATH, ensure_runtime returns that resolved path.""" + import stat + + bin_dir = tmp_path / "system-bin" + bin_dir.mkdir() + on_path = bin_dir / _install.BINARY_NAME + on_path.write_text("#!/bin/sh\nexit 0\n") + on_path.chmod(on_path.stat().st_mode | stat.S_IXUSR) + monkeypatch.setenv("PATH", str(bin_dir)) + + resolved = _install.ensure_runtime() + + assert resolved == on_path From 14915ed8ec74a3757c9edf339c2ae5d3dfc7542b Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Sat, 23 May 2026 02:13:20 +0800 Subject: [PATCH 5/6] =?UTF-8?q?=E2=9C=85=20(test):=20Add=20ensure=5Fruntim?= =?UTF-8?q?e=20returns=20wheel-bundled=20binary=20when=20no=20PATH?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Verifies the second-branch fallback: with PATH scrubbed by isolate_runtime, ensure_runtime() resolves to WHEEL_BUNDLED_BIN when the file is present and executable. This exercises the `pip install agent-assembly[runtime]` installation path. AAASM-1218 --- test/unit/test_install.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/unit/test_install.py b/test/unit/test_install.py index 3f91f03..8357a26 100644 --- a/test/unit/test_install.py +++ b/test/unit/test_install.py @@ -40,3 +40,16 @@ def test_ensure_runtime_returns_path_match_first(monkeypatch, tmp_path: Path) -> resolved = _install.ensure_runtime() assert resolved == on_path + + +def test_ensure_runtime_falls_back_to_wheel_bundled(isolate_runtime: Path) -> None: + """When PATH has no aasm, ensure_runtime returns the wheel-bundled path.""" + import stat + + fake_binary = isolate_runtime + fake_binary.write_text("#!/bin/sh\nexit 0\n") + fake_binary.chmod(fake_binary.stat().st_mode | stat.S_IXUSR) + + resolved = _install.ensure_runtime() + + assert resolved == fake_binary From 3d6c20d63a420853d1daaa12596fcc3e0f4c6648 Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Sat, 23 May 2026 02:13:30 +0800 Subject: [PATCH 6/6] =?UTF-8?q?=E2=9C=85=20(test):=20Add=20ensure=5Fruntim?= =?UTF-8?q?e=20raises=20RuntimeError=20with=20install=20hint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Verifies the unhappy-path branch: with neither a PATH match nor a wheel-bundled binary, ensure_runtime() raises RuntimeError and the exception text contains the full INSTALL_HINT (pip [runtime], Homebrew tap, curl installer) — so a user hitting this in production gets actionable copy-paste recovery commands. AAASM-1218 --- test/unit/test_install.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test/unit/test_install.py b/test/unit/test_install.py index 8357a26..df8a3d5 100644 --- a/test/unit/test_install.py +++ b/test/unit/test_install.py @@ -53,3 +53,14 @@ def test_ensure_runtime_falls_back_to_wheel_bundled(isolate_runtime: Path) -> No resolved = _install.ensure_runtime() assert resolved == fake_binary + + +def test_ensure_runtime_raises_with_install_hint(isolate_runtime: Path) -> None: + """When no binary exists, raise RuntimeError carrying INSTALL_HINT.""" + # Sanity: isolate_runtime points at a path that doesn't exist yet. + assert not isolate_runtime.exists() + + with pytest.raises(RuntimeError) as exc_info: + _install.ensure_runtime() + + assert _install.INSTALL_HINT in str(exc_info.value)