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
38 changes: 38 additions & 0 deletions .github/workflows/framework-resolution-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: Framework resolution check

# AAASM-4518 pre-publish guard: agent-assembly's runtime dependency floors must
# stay co-installable with the supported agent frameworks (AutoGen / CrewAI /
# Semantic Kernel). A floor raised above what a framework permits is a metadata-only
# break that only surfaces AFTER the immutable wheel is published to PyPI β€” this job
# turns that into a PR-time failure. See scripts/ci/check_framework_resolution.py.
on:
pull_request:
paths:
- "pyproject.toml"
- "uv.lock"
- "scripts/ci/check_framework_resolution.py"
- ".github/workflows/framework-resolution-check.yml"
push:
branches: [master]
paths:
- "pyproject.toml"
- "uv.lock"
- "scripts/ci/check_framework_resolution.py"
- ".github/workflows/framework-resolution-check.yml"

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
framework-floors-resolve:
name: Dependency floors co-resolve with supported frameworks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v7
- name: Resolve agent-assembly floors against the framework matrix
run: python scripts/ci/check_framework_resolution.py --python-version 3.12
50 changes: 31 additions & 19 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,28 +24,40 @@ classifiers = [
"Programming Language :: Python :: 3.14",
]
dependencies = [
"pydantic>=2.13.4,<3.0.0",
# AAASM-4518: floor held at the true minimum 2.0 β€” the SDK only uses
# BaseModel/Field/model_dump, all stable since pydantic 2.0. AAASM-4434 raised
# this to >=2.13.4 by pinning the floor to the then-resolved version, which made
# agent-assembly un-co-installable with CrewAI (caps pydantic<2.13) and Semantic
# Kernel (caps <2.12) β€” a framework-compat regression vs rc.3. Keep the floor at
# what the code needs, not what the lockfile happened to resolve to.
"pydantic>=2.0.0,<3.0.0",
"httpx>=0.27.0,<1.0.0",
"typing-extensions>=4.16.0",
# AAASM-1654 (PR-E): OpControlSubscriber consumes PolicyService.OpControlStream
# via gRPC. Held to grpcio's modern stable line.
"grpcio>=1.82.1,<2",
# AAASM-4434 (dependency completeness re-audit): resolves to 6.33.6, not raw
# PyPI latest (7.35.1), even though >=5,<8 permitted 7.x. Confirmed this is a
# REAL transitive ceiling, not a stale "permissive floor" resolution: the
# `dev`/test group's pydantic-ai>=2.9.0 pulls in
# `pydantic-ai-slim[...,logfire,...]`, and logfire (<=4.37.0) pins
# `opentelemetry-exporter-otlp-proto-http<1.43.0`; every otlp-proto-http
# release in that range depends on an `opentelemetry-proto` that caps
# `protobuf<7.0`. Verified by temporarily setting `protobuf>=7.35.1,<8` and
# re-running `uv lock`, which fails with a resolver conflict rooted at
# pydantic-ai's logfire extra; a bare `uv lock --upgrade` (no floor change)
# also leaves protobuf pinned at 6.33.6. Re-audit once logfire raises its
# opentelemetry-exporter-otlp-proto-http ceiling above 1.43.0.
# AAASM-4434 (manifest transparency follow-up): floor tightened from the
# wide-open "5" to the exact resolved 6.33.6 so the manifest states what is
# actually locked, not just the outer bound of what's permitted.
"protobuf>=6.33.6,<8",
# via gRPC. AAASM-4518: floor restored to rc.3's >=1.66. AAASM-4434 raised it to
# >=1.82.1 to mirror the then-resolved version β€” an over-tighten, not a real
# minimum required by the SDK.
"grpcio>=1.66,<2",
# AAASM-4518: floor restored to rc.3's >=5 so agent-assembly can co-install with
# AutoGen (autogen-core>=0.7.5 caps protobuf<5.30). AAASM-4434 raised this to
# >=6.33.6 by pinning the floor to the then-resolved version, which made rc.4
# unresolvable with AutoGen β€” a framework-compat regression vs rc.3.
#
# CAVEAT (pre-existing, NOT introduced by this change): the checked-in gRPC stubs
# under agent_assembly/proto/ are protobuf gencode 6.31.1 and require a protobuf
# 6.x RUNTIME (>=6.31.1) to import β€” protobuf 5.x raises a VersionError. Those
# stubs are imported only by the optional OpControl gRPC transport
# (agent_assembly/op_control.py), never at package-import time, so mainstream
# framework quick-starts (which don't use that transport) run fine under protobuf
# 5.x β€” exactly as they did on rc.3, which also shipped 6.31.1 gencode under a
# >=5 floor since 2026-05-21. A user who needs the OpControl gRPC path must have
# protobuf>=6.31.1. Regenerating the stubs at 5.x gencode to make protobuf 5.x
# fully runtime-safe is a separate concern, not this compat fix.
#
# dev/test note: a bare `uv lock` still resolves protobuf to 6.33.6 because the
# dev-only pydantic-ai logfire extra caps protobuf<7.0 (via opentelemetry-proto);
# that transitive dev-time ceiling is independent of this runtime floor.
"protobuf>=5,<8",
]

[project.optional-dependencies]
Expand Down
137 changes: 137 additions & 0 deletions scripts/ci/check_framework_resolution.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
#!/usr/bin/env python3
"""Pre-publish guard: agent-assembly's dependency floors must co-resolve with the
supported agent frameworks.

WHY this exists (AAASM-4518): a well-meaning "pin the floor to the resolved
version" bump (AAASM-4434) raised ``pydantic``/``protobuf`` floors above what
CrewAI, Semantic Kernel, and AutoGen permit. Because the floors are metadata, the
break only surfaced *after* the wheel was published to PyPI (rc.4 is immutable) β€”
downstream users could no longer ``pip install agent-assembly`` alongside their
framework. This guard turns that post-publish surprise into a PR-time failure.

It resolves the SDK's *declared runtime floors* (read live from
``[project.dependencies]``) together with each framework at its current release
floor, using ``uv pip compile`` as an offline-metadata SAT solve. It never builds
or imports agent-assembly, so it is fast and runs on any platform. A floor that
excludes a framework's supported range makes the union unsatisfiable and fails the
guard, naming the conflicting constraint.

Usage::

python scripts/ci/check_framework_resolution.py # check current tree
python scripts/ci/check_framework_resolution.py --python-version 3.12
python scripts/ci/check_framework_resolution.py --pyproject path/to/pyproject.toml

Exit code 0 = every framework co-resolves; 1 = at least one conflict (details
printed); 2 = harness error (uv missing, pyproject unreadable).
"""

from __future__ import annotations

import argparse
import subprocess
import sys
import tempfile
import tomllib
from pathlib import Path

# The frameworks agent-assembly ships quick-start examples for, each pinned at the
# CURRENT release floor. The floor matters: an unconstrained `framework` lets the
# resolver pick an ancient version that trivially resolves, hiding the very conflict
# we guard against. Bump these floors when a framework's supported release moves.
# (Mirrors the frameworks called out in AAASM-4518 / examples#268.)
FRAMEWORK_MATRIX: list[str] = [
"autogen-core>=0.7.5", # caps protobuf<5.30
"crewai>=1.15.2", # caps pydantic<2.13
"semantic-kernel>=1.30", # caps pydantic<2.12
]

DEFAULT_PYTHON_VERSION = "3.12" # the SDK's minimum supported interpreter


def read_runtime_floors(pyproject: Path) -> list[str]:
"""Return the ``[project.dependencies]`` specifiers verbatim from pyproject.toml."""
with pyproject.open("rb") as fh:
data = tomllib.load(fh)
deps = data.get("project", {}).get("dependencies")
if not deps:
raise SystemExit(f"error: no [project.dependencies] found in {pyproject}")
# Drop self-referential extras (e.g. `agent-assembly[runtime]`) β€” they are not
# third-party floors and would force a build of the local project.
return [d for d in deps if not d.replace(" ", "").startswith("agent-assembly")]


def resolve(reqs: list[str], python_version: str) -> tuple[bool, str]:
"""Try to resolve ``reqs`` together. Returns (ok, combined resolver output)."""
with tempfile.TemporaryDirectory() as tmp:
req_in = Path(tmp) / "req.in"
req_out = Path(tmp) / "req.txt"
req_in.write_text("\n".join(reqs) + "\n")
proc = subprocess.run(
[
"uv",
"pip",
"compile",
str(req_in),
"--python-version",
python_version,
"--output-file",
str(req_out),
"--quiet",
"--no-header",
],
capture_output=True,
text=True,
)
return proc.returncode == 0, (proc.stderr + proc.stdout).strip()


def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--pyproject",
default=str(Path(__file__).resolve().parents[2] / "pyproject.toml"),
help="path to pyproject.toml (default: repo root)",
)
parser.add_argument(
"--python-version",
default=DEFAULT_PYTHON_VERSION,
help=f"target interpreter for resolution (default: {DEFAULT_PYTHON_VERSION})",
)
args = parser.parse_args()

try:
floors = read_runtime_floors(Path(args.pyproject))
except (OSError, tomllib.TOMLDecodeError) as exc:
print(f"error: cannot read {args.pyproject}: {exc}", file=sys.stderr)
return 2

print("agent-assembly runtime floors under test:")
for spec in floors:
print(f" {spec}")
print(f"target python: {args.python_version}\n")

failures: list[tuple[str, str]] = []
for framework in FRAMEWORK_MATRIX:
ok, output = resolve(floors + [framework], args.python_version)
status = "OK " if ok else "FAIL"
print(f"[{status}] agent-assembly floors + {framework}")
if not ok:
failures.append((framework, output))

if failures:
print("\nFramework resolution conflicts (a floor excludes a supported framework):")
for framework, output in failures:
print(f"\n--- {framework} ---\n{output}")
print(
"\nRelax the offending floor to the widest range the SDK actually "
"supports before publishing. See AAASM-4518."
)
return 1

print("\nAll frameworks co-resolve with the current dependency floors.")
return 0


if __name__ == "__main__":
raise SystemExit(main())
6 changes: 3 additions & 3 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading