Skip to content

feat(packaging)!: introduce slim agentex-sdk-client + heavy agentex-sdk split#370

Draft
max-parke-scale wants to merge 2 commits into
maxparke/agx1-292-promote-protocol-typesfrom
maxparke/agx1-292-prototype-client-split
Draft

feat(packaging)!: introduce slim agentex-sdk-client + heavy agentex-sdk split#370
max-parke-scale wants to merge 2 commits into
maxparke/agx1-292-promote-protocol-typesfrom
maxparke/agx1-292-prototype-client-split

Conversation

@max-parke-scale
Copy link
Copy Markdown
Contributor

@max-parke-scale max-parke-scale commented May 26, 2026

Stacked on #371. Do not merge until #371 lands.

Summary

Publishes the existing agentex-sdk wheel as two namespace-sharing packages so REST-only consumers can install just the Stainless REST surface without dragging in the full ADK runtime.

Install command Deps Ships
pip install agentex-sdk-client 6 agentex/{__init__.py, _*.py, _utils/, types/, resources/, protocol/, py.typed}
pip install agentex-sdk 6 (transitive) + 31 ADK only agentex/lib/*; depends on agentex-sdk-client

The two packages contribute disjoint files to the agentex.* namespace. Existing pip install agentex-sdk consumers see no change: heavy depends on slim, so the slim deps install transitively.

Motivating consumer: packages/egp-api-backend hand-rolls a ~600-line JSON-RPC gateway today because it can't import the typed wire shapes without pulling in temporalio, fastapi, claude-agent-sdk, and 28 other deps. With this split + #371 (which moves protocol types to agentex.protocol.*), that gateway can pin agentex-sdk-client and use from agentex.protocol.acp import RPCMethod, CreateTaskParams, ....

Tracking: AGX1-292.

Note on the PR stack

This change was originally drafted as a single large PR (history visible in the force-push). On review it was clearly two distinct concerns, so it's been split:

Reviewing them separately should be much more tractable.

Repo layout after merge

scale-agentex-python/
├── pyproject.toml                # Stainless-managed: agentex-sdk-client
│                                 # 6 deps, requires-python >= 3.11
│                                 # wheel `exclude` keeps lib/ out of the slim wheel
├── src/agentex/                  # shared source tree
│   ├── __init__.py, _*.py, _utils/, types/, resources/   # Stainless-generated
│   ├── protocol/                                          # (from #371)
│   └── lib/                                               # hand-authored ADK overlay
└── adk/                          # hand-authored — preserved via keep_files
    ├── pyproject.toml            # agentex-sdk
    │                             # depends on agentex-sdk-client>=0.11.4,<0.12
    │                             #          + 31 ADK deps
    │                             # requires-python >= 3.12
    │                             # wheel: force-include ../src/agentex/lib → agentex/lib
    └── README.md

src/agentex/lib/ stays where it is — Stainless already preserves it per CONTRIBUTING.md. The slim wheel's [tool.hatch.build.targets.wheel].exclude keeps lib/ out of the slim. The heavy wheel uses hatchling force-include "../src/agentex/lib" = "agentex/lib" to pull lib/ into the heavy. Same source file, two disjoint wheels.

Python-version pins

  • agentex-sdk-client: requires-python = ">= 3.11,<4". Zero 3.12-only imports in the Stainless surface.
  • agentex-sdk: requires-python = ">= 3.12,<4". agentex/lib/* uses from typing import override (3.12+ stdlib) in 19 files. The combined package's prior >= 3.11 pin was de-facto broken on 3.11; this PR aligns the pin with what actually works.

Release / publish wiring

  • bin/publish-pypi: publishes slim before heavy. Heavy depends on slim, so flipping the order means a slim-side failure (token, transient PyPI 5xx, name collision) aborts before we ship a heavy that pins an unreleased slim.
  • bin/check-release-environment: validates both AGENTEX_SDK_CLIENT_PYPI_TOKEN and AGENTEX_PYPI_TOKEN, with legacy PYPI_TOKEN as fallback.
  • .github/workflows/publish-pypi.yml: passes both token secrets to the script.
  • release-please-config.json: two-package mode (. and adk/) with include-component-in-tag = true. Tag scheme changes from v0.11.4agentex-sdk-client-v0.11.4 / agentex-sdk-v0.11.4 — flagged with ! in the title and commit. Any downstream tooling filtering by raw v* tags will need updating.
  • .release-please-manifest.json: seeds adk/ at 0.11.4 so the first release produces matched versions.
  • CI build job: builds both wheels via rye build --wheel (the --wheel flag is important — sdist-then-wheel-from-sdist can't resolve adk's cross-directory force-include from inside an unpacked sdist tarball).

Required maintainer follow-ups before this can ship

  • Stainless dashboard:
    • Add adk/** to keep_files so the ADK overlay persists across codegen.
    • Reduce the dashboard's emitted dep list for root pyproject.toml to the 6 slim-base deps. (See "Risks" below — if this isn't done, every Stainless regen will silently re-add the 31 ADK deps to slim's dependencies = [...].)
  • PyPI: claim the agentex-sdk-client package name; add AGENTEX_SDK_CLIENT_PYPI_TOKEN to repo secrets. agentex-sdk publishing continues using AGENTEX_PYPI_TOKEN from adk/.
  • Verify: after Stainless dashboard config, trigger a Stainless regen and confirm adk/ survives and root pyproject's slim shape isn't clobbered.

Planned follow-up PRs

  • D (in this repo): post-codegen CI guardrail asserting root pyproject.toml's dependencies = [...] is exactly the 6-dep slim set. Catches dashboard drift if Stainless re-adds ADK deps.
  • E (in scaleapi/scaleapi): migrate packages/egp-api-backend from hand-rolled JSON-RPC to typed agentex.protocol.acp shapes; pin agentex-sdk-client. ~600 lines of dict-literal construction become typed model usage.
  • Lockfile regeneration: requirements.lock / requirements-dev.lock need refresh after rye sync against the workspace; not yet committed because the local env doesn't have rye installed. Easy to fix in this PR if reviewers want, or as a fast-follow.
  • README split: the root README.md describes capabilities the slim doesn't ship; deferring to its own PR so this one stays focused on packaging.
  • agentex.__version__ policy: release-please-config.json's extra-files updates only root _version.py, so the runtime __version__ reflects the slim only. Either lockstep-version both (recommended, since they're co-released) or add a separate agentex.lib.__version__.

Verification (local)

# Slim wheel
python -m build --wheel
unzip -p dist/agentex_sdk_client-*.whl '*.dist-info/METADATA' | grep ^Name
# Name: agentex-sdk-client
unzip -p dist/agentex_sdk_client-*.whl '*.dist-info/METADATA' | grep -cE "^Requires-Dist:" | grep -v "extra =="
# 6
unzip -l dist/agentex_sdk_client-*.whl | grep -c "agentex/lib"
# 0  (lib excluded)
unzip -l dist/agentex_sdk_client-*.whl | grep -c "agentex/protocol"
# 3  (from #371)

# Heavy wheel
(cd adk && python -m build --wheel)
unzip -p adk/dist/agentex_sdk-*.whl '*.dist-info/METADATA' | grep "agentex-sdk-client"
# Requires-Dist: agentex-sdk-client<0.12,>=0.11.4
unzip -l adk/dist/agentex_sdk-*.whl | awk '{print $4}' | grep "^agentex/" | grep -v "^agentex/lib"
# (empty — only lib/* in heavy)

# Dual install on Python 3.13
python3.13 -m venv /tmp/c && /tmp/c/bin/pip install \
  dist/agentex_sdk_client-*.whl adk/dist/agentex_sdk-*.whl
/tmp/c/bin/python -c "
from agentex import Agentex                                          # slim
from agentex.protocol.acp import RPCMethod, CreateTaskParams         # slim (from #371)
from agentex.lib.utils.logging import make_logger                    # heavy
from agentex.lib.types.acp import RPCMethod as RM2                   # heavy (shim from #371)
assert RPCMethod is RM2  # shim re-exports canonical
print('OK')
"
# OK

Risks

  • Stainless dashboard dep-list is more load-bearing than the wheel exclude. The slim's [tool.hatch.build.targets.wheel].exclude = ["src/agentex/lib/**"] is a hand-edit to Stainless's emitted file. Manual edits to dependencies = [...] survive Stainless 3-way merge historically (~7 confirmed examples from git log). We're betting the wheel-target exclude survives the same way. If it gets clobbered, the slim wheel would start re-including lib/ and conflict with the heavy on install. Detection: PR D's guardrail check catches it. Mitigation if it happens: re-add manually or move the exclude to the Stainless dashboard if configurable.
  • Tag scheme change. Downstream tooling filtering by v* tags needs to update — flagged with ! in title/commit per Conventional Commits.
  • adk/ sdist support deferred. Wheels publish fine; sdist for adk/ doesn't because rye build's default sdist-then-wheel-from-sdist flow can't resolve the cross-directory force-include. PyPI tolerates wheel-only releases; if reviewers want sdist support, options are (a) configure hatchling to copy ../src/agentex/lib into the sdist, or (b) explicitly disable sdist for adk/.
  • agentex-sdk-client version pin in adk/pyproject.toml is >=0.11.4,<0.12. Release-please doesn't automatically bump this when slim moves; bumping the range is a manual step each minor/major. Comment in the file flags it. A release-please plugin or a small CI substitution rule could automate this in a follow-up.

@max-parke-scale max-parke-scale force-pushed the maxparke/agx1-292-prototype-client-split branch from 0d7db31 to 4ad75f7 Compare May 26, 2026 23:07
@max-parke-scale max-parke-scale changed the title feat(packaging): introduce slim agentex-sdk-client sibling package feat(packaging): split into slim Stainless-managed client + hand-authored ADK overlay May 26, 2026
max-parke-scale added a commit that referenced this pull request May 26, 2026
…end_path

Follows up the slim/heavy split with the test relocations + dev-install
plumbing that should have been part of the original commit:

- Move tests/lib/* and tests at tests/ root that exercise lib code
  (test_function_tool.py, test_model_utils.py, test_header_forwarding.py)
  into adk/tests/. Tests now live with the code they test. The Path-based
  source references in test_claude_agents_*.py (`_SRC = parents[2] / "src"`)
  resolve correctly to adk/src/ via the new location.

- Fix test_function_tool.py's broken `src.agentex.lib.*` import — switch
  to the installed-package path `agentex.lib.*` so it works against the
  editable install.

- Add `from pkgutil import extend_path; __path__ = extend_path(...)` to
  src/agentex/__init__.py. This is the load-bearing fix for dev workflow:
  without it, two editable installs (slim at root, heavy at adk/) each
  contributing files to `agentex/` get only the first source dir in
  `agentex.__path__`, so `import agentex.lib.*` fails. With it, Python
  discovers both source trees and the namespace merges. Wheel installs
  (production) already worked because both wheels' files land in the same
  site-packages/agentex/ directory.

- scripts/bootstrap: after `rye sync`, also `pip install -e ./adk` so
  agentex-sdk's deps land in the dev venv. agentex-sdk-client is already
  installed via the root sync, so adk's dep on it resolves to the local
  editable install (no PyPI lookup needed).

- pyproject.toml [tool.pytest.ini_options].testpaths includes "adk/tests".
- pyproject.toml [tool.ruff.lint.per-file-ignores] extends test-friendly
  ignores to adk/tests/.
- Drop the rye workspace config — pkgutil.extend_path + explicit pip
  install -e ./adk in bootstrap gives the same dev experience without
  rye-workspace-version-mismatch quirks.
- .github/workflows/ci.yml: lint + test jobs call ./scripts/bootstrap
  instead of `rye sync` directly; build job builds both packages.

Self-review took: I shipped the file move without running the test suite
locally — that's why CI broke on PR #370. Mea culpa. The functional design
is correct; the rollout was sloppy. Verified locally:
- `ruff check .` → All checks passed
- `pytest --collect-only adk/tests/` → 100+ tests collect cleanly
- `pytest adk/tests/test_function_tool.py` → 10 passed
- Dev install (`pip install -e .` + `pip install -e ./adk`):
  `from agentex import Agentex` and `from agentex.lib.* import …` both work

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…dk split

Stacks on #371 (which moved protocol types to agentex.protocol.*).
Together they enable REST-only consumers to install just the slim
package and use typed protocol types without pulling the full ADK stack.

Two-package layout:

- agentex-sdk-client (slim, root pyproject.toml)
    * 6 deps: httpx, pydantic, typing-extensions, anyio, distro, sniffio
    * Ships agentex/{__init__.py, _*.py, _utils/, types/, resources/,
      protocol/, py.typed}
    * requires-python >= 3.11
    * Stainless-managed
    * Slim wheel excludes src/agentex/lib/**

- agentex-sdk (heavy, adk/pyproject.toml)
    * Depends on agentex-sdk-client>=0.11.4,<0.12 (lockstep, see comment)
    * Plus 31 ADK deps: temporalio, fastapi, redis, MCP, LLM providers,
      observability, CLI surface
    * Ships only agentex/lib/* via hatchling force-include from
      ../src/agentex/lib (lib/ stays at its historical location)
    * requires-python >= 3.12 (lib uses `from typing import override`,
      a 3.12+ stdlib API)
    * Hand-authored overlay; entire adk/ directory must be preserved
      across Stainless codegen via `keep_files: ["adk/**"]`

Existing consumers (`pip install agentex-sdk`) see no change: heavy
depends on slim, so the slim deps install transitively. Both packages
contribute disjoint files to the agentex.* namespace.

Release / publish wiring:

- bin/publish-pypi publishes slim BEFORE heavy. Heavy depends on slim,
  so if the slim publish errors we abort before shipping a broken
  heavy that pins an unreleased slim. (Staff-engineer review fix.)
- bin/check-release-environment validates BOTH PyPI tokens, with the
  legacy PYPI_TOKEN as a back-compat fallback.
- .github/workflows/publish-pypi.yml passes AGENTEX_SDK_CLIENT_PYPI_TOKEN
  and AGENTEX_PYPI_TOKEN to the script.
- release-please-config.json: two-package mode with components
  (agentex-sdk-client at root, agentex-sdk at adk/), and
  include-component-in-tag=true so tags become
  e.g. `agentex-sdk-client-v0.11.4` and `agentex-sdk-v0.11.4`.
- .release-please-manifest.json: seeds adk/ at 0.11.4 so the first
  release produces matched versions.

CI build job runs `rye build --wheel` for both packages; --wheel only
(not sdist) because adk's wheel uses cross-directory force-include
which can't resolve from inside an unpacked sdist tarball.

Tag scheme breakage flagged with `!` in commit/PR title — downstream
tooling that filters by raw `v*` tags will need updates.

Required maintainer follow-ups before this can ship:
- Stainless dashboard: add `adk/**` to keep_files; reduce dashboard
  dep-list to the 6 slim-base deps so codegen doesn't re-add the 31
  ADK deps to root pyproject's `dependencies = [...]`.
- PyPI: claim `agentex-sdk-client` package name; add
  AGENTEX_SDK_CLIENT_PYPI_TOKEN to repo secrets.
- (Defer to a follow-up PR) post-codegen CI guardrail that asserts root
  pyproject's `dependencies = [...]` matches the 6-dep slim set.

Verified locally:
- Both wheels build cleanly via `rye build --wheel`
- Slim ships agentex/protocol/* and excludes agentex/lib/*
- Heavy ships only agentex/lib/* (incl. the back-compat shims from #371)
- Dual install on Python 3.13: from agentex import Agentex AND from
  agentex.protocol.acp import RPCMethod AND from agentex.lib.utils.logging
  import make_logger AND from agentex.lib.types.acp import RPCMethod
  (shim) — all resolve; shim and canonical identity-check as the same
  class objects.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@max-parke-scale max-parke-scale force-pushed the maxparke/agx1-292-prototype-client-split branch from 2a08eeb to 3a95482 Compare May 27, 2026 03:58
@max-parke-scale max-parke-scale changed the base branch from next to maxparke/agx1-292-promote-protocol-types May 27, 2026 03:58
@max-parke-scale max-parke-scale changed the title feat(packaging): split into slim Stainless-managed client + hand-authored ADK overlay feat(packaging)!: introduce slim agentex-sdk-client + heavy agentex-sdk split May 27, 2026
…wheels to uv

The tutorial test workflow boots an agentex agent via `uv run --with <wheel>
agentex agents run ...`. After the package split:

- The slim `agentex_sdk_client-*.whl` ships from `dist/` (root)
- The heavy `agentex_sdk-*.whl` (which provides the `agentex` CLI) now
  ships from `adk/dist/`

Old workflow ran `uv build` at root (produces slim only) and the script
hunted for `dist/agentex_sdk-*.whl` (heavy) — which doesn't exist there
anymore. CI saw "❌ No built wheel found in dist/agentex_sdk-*.whl".

Also: heavy pins `agentex-sdk-client>=0.11.4,<0.12` as a runtime dep, and
the slim isn't on PyPI yet, so uv must resolve the slim from the LOCAL
wheel too. Single `--with` isn't enough; need both.

Fixes:
- `.github/workflows/agentex-tutorials-test.yml`: build both packages
  in the "Build AgentEx SDK" step.
- `examples/tutorials/run_agent_test.sh`: three sites (agent run, pytest
  invocation, check_built_wheel) now find both wheels at their new paths
  (slim at `dist/agentex_sdk_client-*.whl`, heavy at
  `adk/dist/agentex_sdk-*.whl`) and pass both to `uv run --with`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant