From 17266892aeb0144a140c3d50b4ea3f111d3a18da Mon Sep 17 00:00:00 2001 From: Max Parke Date: Wed, 17 Jun 2026 12:07:21 -0400 Subject: [PATCH] fix(packaging): guard agentex-client surface, bump floor, smoke-test wheel install MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Harden the 0.13.0 split (agentex-sdk + agentex-client share the agentex.* namespace) against the partial-install break: - Bump the agentex-client floor to >=0.13.0 (first release where it ships separately) so an old client can't satisfy the dep. Kept floor-only: a ceiling would exclude the co-versioned slim (release-please can't bump it). - Sync uv.lock to the 0.13.0 workspace versions (it lagged pyproject at 0.12.0) so frozen installs resolve a client that satisfies the new floor. - Add an import-time guard (agentex.lib) that canaries the agentex-client REST surface and raises a clear, actionable error if a symbol/resource the ADK needs is absent — instead of a cryptic `cannot import name 'Event' from 'agentex.types'`. It does not gate on version (newer clients are additive and must not be rejected); it can't preempt a fully-missing client surface (import agentex fails in the client's own __init__ first) — the wheel smoke covers that. - Add scripts/check-wheel-install (wired into the build CI job): builds both wheels, installs them together into a fresh venv, and imports agentex.lib.adk plus the agentex.types/resources client surface. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/ci.yml | 4 ++++ adk/pyproject.toml | 2 +- scripts/check-wheel-install | 25 +++++++++++++++++++++++++ src/agentex/lib/__init__.py | 4 ++++ src/agentex/lib/_version_guard.py | 28 ++++++++++++++++++++++++++++ tests/lib/test_version_guard.py | 26 ++++++++++++++++++++++++++ uv.lock | 4 ++-- 7 files changed, 90 insertions(+), 3 deletions(-) create mode 100755 scripts/check-wheel-install create mode 100644 src/agentex/lib/_version_guard.py create mode 100644 tests/lib/test_version_guard.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0c0fa982f..8c96ca356 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -61,6 +61,10 @@ jobs: # force-include can't build via the sdist-then-wheel default. run: uv build --all-packages --wheel + - name: Smoke-test wheel install + # Both wheels must install together into one working agentex.* namespace. + run: ./scripts/check-wheel-install + - name: Get GitHub OIDC Token if: |- github.repository == 'stainless-sdks/agentex-sdk-python' && diff --git a/adk/pyproject.toml b/adk/pyproject.toml index 33132a736..a33f33647 100644 --- a/adk/pyproject.toml +++ b/adk/pyproject.toml @@ -15,7 +15,7 @@ readme = "README.md" dependencies = [ # Co-released in lockstep; floor-only by design — a ceiling would # eventually exclude the co-versioned slim (release-please can't bump it). - "agentex-client>=0.12.0", + "agentex-client>=0.13.0", # CLI surface (agentex.lib.cli.*, agentex.lib.sdk.config.*) "typer>=0.16,<0.17", "questionary>=2.0.1,<3", diff --git a/scripts/check-wheel-install b/scripts/check-wheel-install new file mode 100755 index 000000000..b80aed2d2 --- /dev/null +++ b/scripts/check-wheel-install @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +# Smoke: agentex-client + agentex-sdk must install together into one working +# agentex.* namespace. Builds + installs in a clean temp dir to avoid stale dist/. + +set -euo pipefail + +cd "$(dirname "$0")/.." + +work="$(mktemp -d)" +echo "==> building both wheels into $work/dist" +uv build --all-packages --wheel --out-dir "$work/dist" + +venv="$work/venv" +uv venv "$venv" >/dev/null +echo "==> installing both wheels into a fresh venv" +uv pip install --python "$venv" "$work"/dist/agentex_client-*.whl "$work"/dist/agentex_sdk-*.whl + +echo "==> importing the merged namespace from the installed wheels" +"$venv/bin/python" - <<'PY' +import agentex.lib.adk # ADK overlay — ships in agentex-sdk +from agentex.types import Event # client surface — ships in agentex-client +from agentex.resources import states # client surface that "didn't land" in the incident + +print("agentex namespace OK:", Event.__name__, states.__name__) +PY diff --git a/src/agentex/lib/__init__.py b/src/agentex/lib/__init__.py index e69de29bb..9d960dfee 100644 --- a/src/agentex/lib/__init__.py +++ b/src/agentex/lib/__init__.py @@ -0,0 +1,4 @@ +from agentex.lib._version_guard import verify_client_compatibility + +# Fail fast + clearly on a skewed/incomplete agentex-client install. +verify_client_compatibility() diff --git a/src/agentex/lib/_version_guard.py b/src/agentex/lib/_version_guard.py new file mode 100644 index 000000000..a05572efc --- /dev/null +++ b/src/agentex/lib/_version_guard.py @@ -0,0 +1,28 @@ +"""Fail fast with a clear error on an incomplete agentex-client install instead +of a cryptic `cannot import name ... from agentex.types`.""" + +from __future__ import annotations + +from importlib.metadata import PackageNotFoundError, version + + +def _installed(package: str) -> str: + try: + return version(package) + except PackageNotFoundError: + return "unknown" + + +def verify_client_compatibility() -> None: + # Canary on the client REST surface, not the version: newer clients are fine + # (additive); we only fail if a symbol/resource the ADK needs is absent. + try: + from agentex.types import Event as _Event # noqa: F401 + from agentex.resources import states as _states # noqa: F401 + except (ImportError, AttributeError) as exc: + raise ImportError( + f"agentex-sdk could not import the agentex-client REST surface it " + f"depends on (agentex-sdk={_installed('agentex-sdk')}, " + f"agentex-client={_installed('agentex-client')}). Reinstall both at a " + f"compatible version, e.g. `pip install --force-reinstall agentex-sdk`." + ) from exc diff --git a/tests/lib/test_version_guard.py b/tests/lib/test_version_guard.py new file mode 100644 index 000000000..45bd2be7a --- /dev/null +++ b/tests/lib/test_version_guard.py @@ -0,0 +1,26 @@ +"""Tests for the agentex-client compatibility guard (0.13.0 split regression).""" + +from __future__ import annotations + +import pytest + +import agentex.lib._version_guard as guard + + +def test_passes_when_surface_present() -> None: + guard.verify_client_compatibility() # full client surface installed + + +def test_newer_client_not_rejected(monkeypatch: pytest.MonkeyPatch) -> None: + # Version is not a gate: a newer client (additive) with the full surface passes. + monkeypatch.setattr(guard, "version", lambda pkg: "0.14.0" if pkg == "agentex-client" else "0.13.0") + guard.verify_client_compatibility() + + +def test_raises_when_client_surface_incomplete(monkeypatch: pytest.MonkeyPatch) -> None: + import agentex.types + + # A partial install missing a needed symbol fails with an actionable error. + monkeypatch.delattr(agentex.types, "Event", raising=False) + with pytest.raises(ImportError, match="could not import the agentex-client REST surface"): + guard.verify_client_compatibility() diff --git a/uv.lock b/uv.lock index be3d8bbd8..8a41ba29c 100644 --- a/uv.lock +++ b/uv.lock @@ -15,7 +15,7 @@ members = [ [[package]] name = "agentex-client" -version = "0.12.0" +version = "0.13.0" source = { editable = "." } dependencies = [ { name = "anyio" }, @@ -91,7 +91,7 @@ dev = [ [[package]] name = "agentex-sdk" -version = "0.12.0" +version = "0.13.0" source = { editable = "adk" } dependencies = [ { name = "agentex-client" },