Skip to content

Commit afaa0ca

Browse files
feat(packaging)!: split agentex-sdk into slim client + heavy ADK overlay
Publishes the existing wheel as two namespace-sharing packages so REST-only consumers install just the Stainless client without the ADK runtime. - agentex-client (slim, root pyproject): Stainless client + types + protocol; 6 deps; requires-python >=3.11; wheel excludes src/agentex/lib/**. - agentex-sdk (heavy, adk/): the ADK overlay (agentex/lib/*) via a hatchling build hook that force-includes ../src/agentex/lib and prunes test files (force-include ignores `exclude`, hatchling #1395); pins agentex-client floor-only; requires-python >=3.12. Heavy depends on slim, so existing `pip install agentex-sdk` consumers are unchanged. Both contribute disjoint files to the agentex.* namespace. uv workspace wiring (this repo is uv-based post rye→uv migration): - [tool.uv.workspace] members = ["adk"] + [tool.uv.sources] agentex-client = { workspace = true } so dev resolves the ADK's client dep to the local root; the published heavy wheel still pins the PyPI version. - CI + scripts/{bootstrap,test} sync `--all-packages` so the ADK member's deps install for lint/test; both wheels build via `uv build --all-packages --wheel` (--wheel load-bearing — the heavy's cross-dir force-include can't go via sdist). Release/publish wiring: - release-please two-component mode (`.` + `adk/`), include-component-in-tag. - bin/publish-pypi publishes slim before heavy via uv; `--check-url` makes the per-component-tag double-trigger idempotent. Dual tokens, PYPI_TOKEN fallback. - scripts/check-slim-deps CI guardrail fails if the slim dep set drifts from the 6-dep base (catches Stainless re-adding ADK deps). BREAKING CHANGE: release tag scheme changes from v* to <component>-v*. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent ce5af72 commit afaa0ca

18 files changed

Lines changed: 444 additions & 576 deletions

.github/workflows/agentex-tutorials-test.yml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -124,9 +124,11 @@ jobs:
124124
125125
- name: Build AgentEx SDK
126126
run: |
127-
echo "🔨 Building AgentEx SDK wheel..."
128-
uv build
129-
echo "✅ SDK built successfully"
127+
echo "🔨 Building both SDK wheels (slim client + heavy ADK overlay)..."
128+
# uv workspace builds both members into the root dist/. --wheel: the
129+
# heavy's cross-dir force-include can't build via the sdist default.
130+
uv build --all-packages --wheel
131+
echo "✅ Both SDK wheels built successfully"
130132
ls -la dist/
131133
132134
- name: Test Tutorial

.github/workflows/ci.yml

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,14 @@ jobs:
2929
version: '0.10.2'
3030

3131
- name: Install dependencies
32-
run: uv sync --all-extras
32+
run: uv sync --all-packages --all-extras
3333

3434
- name: Run lints
3535
run: ./scripts/lint
3636

37+
- name: Check slim dependency set
38+
run: ./scripts/check-slim-deps
39+
3740
build:
3841
if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata')
3942
timeout-minutes: 10
@@ -51,10 +54,12 @@ jobs:
5154
version: '0.10.2'
5255

5356
- name: Install dependencies
54-
run: uv sync --all-extras
57+
run: uv sync --all-packages --all-extras
5558

5659
- name: Run build
57-
run: uv build
60+
# Both workspace members. --wheel is load-bearing: the heavy's cross-dir
61+
# force-include can't build via the sdist-then-wheel default.
62+
run: uv build --all-packages --wheel
5863

5964
- name: Get GitHub OIDC Token
6065
if: |-

.github/workflows/publish-pypi.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,11 @@ jobs:
2525
run: |
2626
bash ./bin/publish-pypi
2727
env:
28+
# Heavy `agentex-sdk` package token (existing PyPI name).
29+
AGENTEX_PYPI_TOKEN: ${{ secrets.AGENTEX_PYPI_TOKEN }}
30+
# Slim `agentex-client` package token (new PyPI name; needs
31+
# to be added to repo secrets when the slim is registered).
32+
AGENTEX_CLIENT_PYPI_TOKEN: ${{ secrets.AGENTEX_CLIENT_PYPI_TOKEN }}
33+
# Back-compat fallback — used by bin/publish-pypi when the
34+
# dedicated tokens above are unset.
2835
PYPI_TOKEN: ${{ secrets.AGENTEX_PYPI_TOKEN || secrets.PYPI_TOKEN }}

.release-please-manifest.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
{
2-
".": "0.12.0"
2+
".": "0.12.0",
3+
"adk": "0.12.0"
34
}

adk/README.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# agentex-sdk
2+
3+
The Agent Development Kit (ADK) overlay for the Agentex API.
4+
5+
## What's in here
6+
7+
This package ships everything under `agentex.lib.*`:
8+
9+
- **ACP server** (`agentex.lib.sdk.fastacp`) — FastAPI-based agent control plane.
10+
- **Temporal workflows** (`agentex.lib.core.temporal`) — durable agent execution.
11+
- **CLI** (`agentex.lib.cli`) — `agentex init`, `agentex run`, deploy helpers.
12+
- **LLM provider integrations** (`agentex.lib.adk.providers`, `agentex.lib.core.temporal.plugins`) — OpenAI Agents, Claude Agent SDK, pydantic-ai, langgraph, litellm.
13+
- **Observability** (`agentex.lib.core.tracing`, `agentex.lib.core.observability`) — SGP, Datadog, OpenTelemetry tracing processors.
14+
15+
## Installation
16+
17+
```sh
18+
pip install agentex-sdk
19+
```
20+
21+
This automatically pulls in [`agentex-client`](../) (the slim Stainless-generated REST client) so `from agentex import Agentex, AsyncAgentex` works the same as before.
22+
23+
## When to use this vs `agentex-client`
24+
25+
- **`agentex-sdk`** — you're authoring agents. Pulls everything: ACP server, Temporal, MCP, LLM providers, observability, CLI. ~37 deps.
26+
- **`agentex-client`** — you only need to call the Agentex REST API. No agent authoring, no Temporal workflows, no FastACP server, no provider integrations. 6 deps.
27+
28+
The two packages contribute disjoint files to the `agentex.*` namespace — `agentex/lib/*` ships only from `agentex-sdk`.
29+
30+
## Repo layout
31+
32+
This package is hand-authored and lives at `adk/` inside [scaleapi/scale-agentex-python](https://github.com/scaleapi/scale-agentex-python). The Stainless generator preserves `adk/**` via `keep_files` so its codegen never touches anything here. The sibling `agentex-client` package lives at the repo root and IS Stainless-generated.

adk/hatch_build.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
"""Builds the agentex/lib force-include map per-file so test files can be pruned
2+
— force-include ignores `exclude` (hatchling #1395)."""
3+
4+
from __future__ import annotations
5+
6+
import os
7+
8+
from hatchling.builders.hooks.plugin.interface import BuildHookInterface
9+
10+
_SKIP_DIRS = {"__pycache__", "tests"}
11+
_SKIP_NAMES = {"conftest.py", "pytest.ini", "run_tests.py"}
12+
# Floor below the ~333 shippable files: a collapse means the walk broke — fail
13+
# loud rather than ship a near-empty wheel.
14+
_MIN_FILES = 320
15+
16+
17+
def _is_test_file(name: str) -> bool:
18+
return name in _SKIP_NAMES or (name.startswith("test_") and name.endswith(".py"))
19+
20+
21+
class CustomBuildHook(BuildHookInterface):
22+
PLUGIN_NAME = "custom"
23+
24+
def initialize(self, version: str, build_data: dict) -> None: # noqa: ARG002
25+
lib_root = os.path.normpath(os.path.join(self.root, "..", "src", "agentex", "lib"))
26+
force_include = build_data.setdefault("force_include", {})
27+
collected = 0
28+
for dirpath, dirnames, filenames in os.walk(lib_root):
29+
dirnames[:] = [d for d in dirnames if d not in _SKIP_DIRS]
30+
for name in filenames:
31+
if _is_test_file(name):
32+
continue
33+
src = os.path.join(dirpath, name)
34+
rel = os.path.relpath(src, lib_root)
35+
force_include[src] = os.path.join("agentex", "lib", rel)
36+
collected += 1
37+
if collected < _MIN_FILES:
38+
raise RuntimeError(
39+
f"agentex/lib force-include collected only {collected} files "
40+
f"(expected >= {_MIN_FILES}); aborting build."
41+
)

adk/pyproject.toml

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
[project]
2+
# Hand-authored ADK overlay for agentex. This package contributes only
3+
# `agentex/lib/*` to the agentex.* namespace; the REST client surface
4+
# (agentex/{__init__.py, _*.py, types/, resources/}) ships from the slim
5+
# sibling package `agentex-client` which is pinned as a runtime dep.
6+
#
7+
# This entire `adk/` directory must be preserved across Stainless codegen
8+
# via `keep_files: ["adk/**"]` in the Stainless dashboard config.
9+
name = "agentex-sdk"
10+
version = "0.12.0"
11+
description = "Agent Development Kit (ADK) overlay for the Agentex API — FastACP server, Temporal workflows, LLM provider integrations, observability"
12+
license = "Apache-2.0"
13+
authors = [
14+
{ name = "Agentex", email = "roxanne.farhad@scale.com" },
15+
]
16+
readme = "README.md"
17+
18+
dependencies = [
19+
# Co-released in lockstep; floor-only by design — a ceiling would
20+
# eventually exclude the co-versioned slim (release-please can't bump it).
21+
"agentex-client>=0.12.0",
22+
# CLI surface (agentex.lib.cli.*, agentex.lib.sdk.config.*)
23+
"typer>=0.16,<0.17",
24+
"questionary>=2.0.1,<3",
25+
"rich>=13.9.2,<14",
26+
"yaspin>=3.1.0",
27+
"pyyaml>=6.0.2,<7",
28+
"python-on-whales>=0.73.0,<0.74",
29+
"kubernetes>=25.0.0,<36.0.0",
30+
"jsonref>=1.1.0,<2",
31+
"jsonschema>=4.23.0,<5",
32+
"jinja2>=3.1.3,<4",
33+
"watchfiles>=0.24.0,<1.0",
34+
# ACP server (FastAPI app surface)
35+
"fastapi>=0.115.0",
36+
"starlette>=0.49.1",
37+
"uvicorn>=0.31.1",
38+
"aiohttp>=3.10.10,<4",
39+
# Temporal workflows
40+
"temporalio>=1.26.0,<2",
41+
"cloudpickle>=3.1.1",
42+
# Async streaming infra
43+
"redis>=5.2.0,<8",
44+
# LLM provider integrations
45+
"litellm>=1.83.7,<2",
46+
"openai-agents>=0.14.3,<0.15",
47+
"openai>=2.2,<3", # Required by openai-agents; litellm now supports openai 2.x (issue #13711 resolved: https://github.com/BerriAI/litellm/issues/13711)
48+
"claude-agent-sdk>=0.1.0",
49+
"pydantic-ai-slim>=1.0,<2",
50+
"langgraph-checkpoint>=2.0.0",
51+
"scale-gp>=0.1.0a59",
52+
"scale-gp-beta>=0.2.0",
53+
"mcp>=1.4.1",
54+
# Observability
55+
"ddtrace>=3.13.0",
56+
"opentelemetry-api>=1.20.0",
57+
"opentelemetry-sdk>=1.20.0",
58+
"json_log_formatter>=1.1.1",
59+
]
60+
61+
# agentex/lib/* uses `from typing import override` (3.12+) in 19 files.
62+
# The slim agentex-client keeps 3.11 support.
63+
requires-python = ">= 3.12,<4"
64+
classifiers = [
65+
"Typing :: Typed",
66+
"Intended Audience :: Developers",
67+
"Programming Language :: Python :: 3.12",
68+
"Programming Language :: Python :: 3.13",
69+
"Programming Language :: Python :: 3.14",
70+
"Operating System :: OS Independent",
71+
"Topic :: Software Development :: Libraries :: Python Modules",
72+
"License :: OSI Approved :: Apache Software License",
73+
]
74+
75+
[project.urls]
76+
Homepage = "https://github.com/scaleapi/scale-agentex-python"
77+
Repository = "https://github.com/scaleapi/scale-agentex-python"
78+
79+
[project.scripts]
80+
agentex = "agentex.lib.cli.commands.main:app"
81+
82+
[build-system]
83+
requires = ["hatchling"]
84+
build-backend = "hatchling.build"
85+
86+
# Ship only agentex/lib/*, pulled in from the parent repo's `src/agentex/lib`
87+
# tree. The rest of agentex.* (the Stainless-generated client) ships from the
88+
# sibling agentex-client package, which this package pins as a runtime dep.
89+
# Stainless explicitly preserves `src/agentex/lib/` across codegen (per
90+
# CONTRIBUTING.md), so it's safe to keep the source where it is.
91+
[tool.hatch.build.targets.wheel]
92+
bypass-selection = true
93+
94+
# Builds the ../src/agentex/lib force-include map per-file (see hatch_build.py)
95+
# so test files can be pruned — force-include ignores `exclude` (hatchling #1395).
96+
[tool.hatch.build.targets.wheel.hooks.custom]
97+
path = "hatch_build.py"
98+
99+
# Sdist deferred: hatchling can't represent the wheel's ../src/agentex/lib
100+
# force-include in an sdist include list. CI + bin/publish-pypi pass --wheel.
101+
[tool.hatch.build.targets.sdist]
102+
include = [
103+
"/pyproject.toml",
104+
"/README.md",
105+
]

bin/check-release-environment

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,21 @@
11
#!/usr/bin/env bash
22

3+
# This script is run by Release Doctor to validate the release environment.
4+
# After the dual-package split (slim agentex-client + heavy agentex-sdk),
5+
# both PyPI tokens must be present — one for each package name. If only
6+
# PYPI_TOKEN is set, fall back to using it for both (back-compat for legacy
7+
# single-token setups, which forces an account-scoped token).
8+
39
errors=()
410

5-
if [ -z "${PYPI_TOKEN}" ]; then
6-
errors+=("The PYPI_TOKEN secret has not been set. Please set it in either this repository's secrets or your organization secrets.")
11+
# Heavy `agentex-sdk` token (existing PyPI name).
12+
if [ -z "${AGENTEX_PYPI_TOKEN}" ] && [ -z "${PYPI_TOKEN}" ]; then
13+
errors+=("The AGENTEX_PYPI_TOKEN secret has not been set (and no fallback PYPI_TOKEN). Add it in repo secrets so the heavy 'agentex-sdk' package can be published.")
14+
fi
15+
16+
# Slim `agentex-client` token (new PyPI name).
17+
if [ -z "${AGENTEX_CLIENT_PYPI_TOKEN}" ] && [ -z "${PYPI_TOKEN}" ]; then
18+
errors+=("The AGENTEX_CLIENT_PYPI_TOKEN secret has not been set (and no fallback PYPI_TOKEN). Add it in repo secrets so the slim 'agentex-client' package can be published. Falling back to PYPI_TOKEN requires an account-scoped token.")
719
fi
820

921
lenErrors=${#errors[@]}

bin/publish-pypi

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
#!/usr/bin/env bash
22

3+
# Publish slim (root) before heavy (adk/): heavy pins slim, so a slim-first
4+
# failure aborts before shipping a heavy that needs an unreleased client.
5+
36
set -eux
7+
48
rm -rf dist
5-
mkdir -p dist
6-
uv build
7-
uv publish --token=$PYPI_TOKEN
9+
# --wheel: the heavy's cross-dir force-include can't build via sdist.
10+
uv build --all-packages --wheel
11+
12+
# --check-url makes the per-component-tag double-trigger idempotent.
13+
uv publish --check-url https://pypi.org/simple/ --token="${AGENTEX_CLIENT_PYPI_TOKEN:-$PYPI_TOKEN}" dist/agentex_client-*.whl
14+
uv publish --check-url https://pypi.org/simple/ --token="${AGENTEX_PYPI_TOKEN:-$PYPI_TOKEN}" dist/agentex_sdk-*.whl

examples/tutorials/run_agent_test.sh

Lines changed: 45 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -126,18 +126,26 @@ start_agent() {
126126

127127
if [ "$BUILD_CLI" = true ]; then
128128

129-
# Use wheel from dist directory at repo root
130-
local wheel_file=$(ls /home/runner/work/*/*/dist/agentex_sdk-*.whl 2>/dev/null | head -n1)
131-
if [[ -z "$wheel_file" ]]; then
132-
echo -e "${RED}❌ No built wheel found in dist/agentex_sdk-*.whl${NC}"
133-
echo -e "${YELLOW}💡 Please build the local SDK first by running: uv build${NC}"
134-
echo -e "${YELLOW}💡 From the repo root directory${NC}"
129+
# uv workspace builds both wheels into the root dist/ (slim + heavy ADK).
130+
# We need both: heavy pins agentex-client which isn't on PyPI yet,
131+
# so uv must resolve both from local wheels rather than the registry.
132+
local heavy_wheel=$(ls /home/runner/work/*/*/dist/agentex_sdk-*.whl 2>/dev/null | head -n1)
133+
local slim_wheel=$(ls /home/runner/work/*/*/dist/agentex_client-*.whl 2>/dev/null | head -n1)
134+
if [[ -z "$heavy_wheel" ]]; then
135+
echo -e "${RED}❌ No built heavy wheel found in dist/agentex_sdk-*.whl${NC}"
136+
echo -e "${YELLOW}💡 Build it first: uv build --all-packages --wheel${NC}"
137+
cd "$original_dir"
138+
return 1
139+
fi
140+
if [[ -z "$slim_wheel" ]]; then
141+
echo -e "${RED}❌ No built slim wheel found in dist/agentex_client-*.whl${NC}"
142+
echo -e "${YELLOW}💡 Build it first: uv build --wheel${NC}"
135143
cd "$original_dir"
136144
return 1
137145
fi
138146

139-
# Use the built wheel
140-
uv run --with "$wheel_file" agentex agents run --manifest "$manifest_path" > "$logfile" 2>&1 &
147+
# Pass both wheels so the local heavy resolves its slim dep locally
148+
uv run --with "$heavy_wheel" --with "$slim_wheel" agentex agents run --manifest "$manifest_path" > "$logfile" 2>&1 &
141149
else
142150
uv run agentex agents run --manifest manifest.yaml > "$logfile" 2>&1 &
143151
fi
@@ -269,14 +277,20 @@ run_test() {
269277
# robust across all tutorials regardless of how each declares test deps.
270278
local -a pytest_cmd=("uv" "run" "--with" "pytest" "--with" "pytest-asyncio" "pytest")
271279
if [ "$BUILD_CLI" = true ]; then
272-
local wheel_file
273-
wheel_file=$(ls /home/runner/work/*/*/dist/agentex_sdk-*.whl 2>/dev/null | head -n1)
274-
if [[ -z "$wheel_file" ]]; then
275-
wheel_file=$(ls "${SCRIPT_DIR}/../../dist/agentex_sdk-*.whl" 2>/dev/null | head -n1)
280+
local heavy_wheel slim_wheel
281+
heavy_wheel=$(ls /home/runner/work/*/*/dist/agentex_sdk-*.whl 2>/dev/null | head -n1)
282+
if [[ -z "$heavy_wheel" ]]; then
283+
heavy_wheel=$(ls "${SCRIPT_DIR}"/../../dist/agentex_sdk-*.whl 2>/dev/null | head -n1)
284+
fi
285+
slim_wheel=$(ls /home/runner/work/*/*/dist/agentex_client-*.whl 2>/dev/null | head -n1)
286+
if [[ -z "$slim_wheel" ]]; then
287+
slim_wheel=$(ls "${SCRIPT_DIR}"/../../dist/agentex_client-*.whl 2>/dev/null | head -n1)
276288
fi
277-
if [[ -n "$wheel_file" ]]; then
278-
pytest_cmd=("uv" "run" "--with" "$wheel_file" "--with" "pytest" "--with" "pytest-asyncio" "pytest")
289+
if [[ -z "$heavy_wheel" || -z "$slim_wheel" ]]; then
290+
echo -e "${RED}❌ BUILD_CLI=true but a wheel is missing (heavy='${heavy_wheel}' slim='${slim_wheel}'); refusing to test against the pre-installed SDK${NC}"
291+
return 1
279292
fi
293+
pytest_cmd=("uv" "run" "--with" "$heavy_wheel" "--with" "$slim_wheel" "--with" "pytest" "--with" "pytest-asyncio" "pytest")
280294
fi
281295

282296
local max_retries=5
@@ -350,7 +364,7 @@ execute_tutorial_test() {
350364
fi
351365
}
352366

353-
# Function to check if built wheel is available
367+
# Function to check if both built wheels are available
354368
check_built_wheel() {
355369

356370
# Navigate to the repo root (two levels up from examples/tutorials)
@@ -362,19 +376,26 @@ check_built_wheel() {
362376
return 1
363377
}
364378

365-
# Check if wheel exists in dist directory at repo root
366-
local wheel_file=$(ls /home/runner/work/*/*/dist/agentex_sdk-*.whl 2>/dev/null | head -n1)
367-
if [[ -z "$wheel_file" ]]; then
368-
echo -e "${RED}❌ No built wheel found in dist/agentex_sdk-*.whl${NC}"
369-
echo -e "${YELLOW}💡 Please build the local SDK first by running: uv build${NC}"
370-
echo -e "${YELLOW}💡 From the repo root directory${NC}"
379+
# Heavy ADK wheel + slim client wheel — we need both because heavy pins
380+
# agentex-client which isn't on PyPI yet.
381+
local heavy_wheel=$(ls /home/runner/work/*/*/dist/agentex_sdk-*.whl 2>/dev/null | head -n1)
382+
local slim_wheel=$(ls /home/runner/work/*/*/dist/agentex_client-*.whl 2>/dev/null | head -n1)
383+
if [[ -z "$heavy_wheel" ]]; then
384+
echo -e "${RED}❌ No built heavy wheel found in dist/agentex_sdk-*.whl${NC}"
385+
echo -e "${YELLOW}💡 Build it first: uv build --all-packages --wheel${NC}"
386+
cd "$original_dir"
387+
return 1
388+
fi
389+
if [[ -z "$slim_wheel" ]]; then
390+
echo -e "${RED}❌ No built slim wheel found in dist/agentex_client-*.whl${NC}"
391+
echo -e "${YELLOW}💡 Build it first: uv build --wheel${NC}"
371392
cd "$original_dir"
372393
return 1
373394
fi
374395

375-
# Test the wheel by running agentex --help
376-
if ! uv run --with "$wheel_file" agentex --help >/dev/null 2>&1; then
377-
echo -e "${RED}❌ Failed to run agentex with built wheel${NC}"
396+
# Test the heavy wheel by running agentex --help (uses both wheels for resolution)
397+
if ! uv run --with "$heavy_wheel" --with "$slim_wheel" agentex --help >/dev/null 2>&1; then
398+
echo -e "${RED}❌ Failed to run agentex with built wheels${NC}"
378399
cd "$original_dir"
379400
return 1
380401
fi

0 commit comments

Comments
 (0)