Skip to content

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

Open
max-parke-scale wants to merge 1 commit into
nextfrom
maxparke/agx1-292-prototype-client-split
Open

feat(packaging)!: introduce slim agentex-sdk-client + heavy agentex-sdk split#370
max-parke-scale wants to merge 1 commit into
nextfrom
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

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.9 (floor-only)
    │                             #          + 31 ADK deps
    │                             # requires-python >= 3.12
    │                             # wheel: force-include via hatch_build.py (below)
    ├── hatch_build.py            # hook: force-include ../src/agentex/lib, prune tests
    └── 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 pulls lib/ in via a hatchling build hook (adk/hatch_build.py) that force-includes ../src/agentex/lib and prunes the test files (force-include ignores exclude — hatchling #1395). 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.9agentex-sdk-client-v0.11.9 / agentex-sdk-v0.11.9 — 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.9 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

The post-codegen dep-list guardrail (scripts/check-slim-deps) and the requirements{,-dev}.lock regeneration — originally planned here — are now included in this PR.

  • 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.
  • 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.11.9
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: the slim-deps guardrail (scripts/check-slim-deps, run in CI) 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 pin in adk/pyproject.toml is floor-only (>=0.11.9). The packages co-version and release-please can't rewrite a dep string, so any <X ceiling eventually excludes the co-versioned slim it pins — a <0.12 ceiling would have made the first 0.12.0 cut unresolvable. Floor-only always resolves; an exact-pin check could be added to scripts/check-slim-deps later for tighter lockstep.

Greptile Summary

This PR introduces a dual-package split of the existing agentex-sdk monolith into a slim agentex-sdk-client (6 REST deps) and a heavy agentex-sdk (ADK overlay, 31 deps) that contributes only agentex/lib/* to the shared agentex.* namespace. Existing pip install agentex-sdk consumers are unaffected since heavy depends on slim transitively.

  • Packaging plumbing: new adk/pyproject.toml + adk/hatch_build.py custom hook that force-includes ../src/agentex/lib per-file to allow test-file pruning; root pyproject.toml renamed to agentex-sdk-client with 31 deps stripped; rye workspace joining both.
  • Release / CI wiring: release-please-config.json moves to two-component mode with include-component-in-tag: true; bin/publish-pypi publishes slim-then-heavy with --wheel and --skip-existing flags; scripts/check-slim-deps guards against Stainless regen silently re-adding ADK deps.
  • Known limitations documented inline: floor-only agentex-sdk-client>=0.11.9 pin in adk/pyproject.toml, sdist support deferred, and Stainless dashboard follow-ups required before the first release.

Confidence Score: 5/5

Safe to merge; the packaging split is mechanically sound, the previously flagged missing --wheel and double-publish issues are now resolved.

Both wheels build correctly with --wheel flags, publish ordering is correct (slim before heavy), and --skip-existing on both rye publish calls makes the workflow idempotent under the two-release-event pattern. The only open item is a minor guardrail gap in check-slim-deps around optional extras, which does not affect runtime correctness.

scripts/check-slim-deps — the optional-dependency gap is worth closing before the next Stainless regen cycle.

Important Files Changed

Filename Overview
scripts/check-slim-deps New CI guardrail that validates the 6 expected slim deps in [project.dependencies]; does not cover [project.optional-dependencies], leaving the aiohttp extra and any future optional extras outside the guarded set.
adk/hatch_build.py Custom hatchling build hook that force-includes src/agentex/lib per-file with test pruning and a 320-file floor check; logic is sound for the described build path.
adk/pyproject.toml New heavy-package manifest; floor-only agentex-sdk-client pin is a documented design choice; sdist paths are intentionally deferred; bypass-selection + custom hook wiring matches hatch_build.py.
bin/publish-pypi Publishes slim then heavy with --wheel and --skip-existing on both rye publish calls, correctly handling the double-trigger scenario from two independent release events.
release-please-config.json Two-component mode with include-component-in-tag: true; tag scheme change is flagged with ! in the PR title; adk package inherits global release-type: python which will update adk/pyproject.toml version on release.
pyproject.toml Renamed to agentex-sdk-client, stripped 31 ADK deps to 6 slim deps, added rye workspace member adk, and added wheel exclude for src/agentex/lib/**; optional aiohttp extra preserved correctly.
.github/workflows/publish-pypi.yml Adds both AGENTEX_PYPI_TOKEN and AGENTEX_SDK_CLIENT_PYPI_TOKEN secrets; workflow triggers on release:published which fires for both component release events; --skip-existing in bin/publish-pypi makes both triggers idempotent.
examples/tutorials/run_agent_test.sh Updated to find and pass both heavy (adk/dist/) and slim (dist/) wheels to uv run; fails fast with clear error messages when either wheel is missing.

Sequence Diagram

sequenceDiagram
    participant RP as release-please
    participant GH as GitHub Releases
    participant WF as publish-pypi.yml
    participant Script as bin/publish-pypi
    participant PyPI

    RP->>GH: Create release agentex-sdk-client-vX.Y.Z
    GH-->>WF: trigger (release:published)
    WF->>Script: bash ./bin/publish-pypi
    Script->>Script: rye build --clean --wheel (slim, root)
    Script->>PyPI: rye publish --skip-existing (agentex-sdk-client)
    PyPI-->>Script: 200 OK
    Script->>Script: "cd adk && rye build --clean --wheel (heavy)"
    Script->>PyPI: rye publish --skip-existing (agentex-sdk)
    PyPI-->>Script: 200 OK

    RP->>GH: Create release agentex-sdk-vX.Y.Z
    GH-->>WF: trigger (release:published) second independent event
    WF->>Script: bash ./bin/publish-pypi
    Script->>PyPI: rye publish --skip-existing (agentex-sdk-client) already exists
    PyPI-->>Script: skip idempotent
    Script->>PyPI: rye publish --skip-existing (agentex-sdk) already exists
    PyPI-->>Script: skip idempotent
Loading

Comments Outside Diff (1)

  1. .github/workflows/publish-pypi.yml, line 8-9 (link)

    P1 Double-publish failure on separate release events

    With include-component-in-tag: true, release-please creates two separate GitHub releases: agentex-sdk-client-v* and agentex-sdk-v*. Each triggers this workflow independently. Every invocation of bin/publish-pypi publishes both packages unconditionally, so the second triggered run will attempt to re-upload artifacts already present on PyPI. Since rye publish (via twine) exits non-zero on a 409 Conflict and the script runs with set -eux, the second workflow run will fail. Adding --skip-existing to both rye publish calls in bin/publish-pypi (or switching to twine upload --skip-existing dist/*) would make the publish script idempotent and tolerate this re-trigger.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: .github/workflows/publish-pypi.yml
    Line: 8-9
    
    Comment:
    **Double-publish failure on separate release events**
    
    With `include-component-in-tag: true`, release-please creates two separate GitHub releases: `agentex-sdk-client-v*` and `agentex-sdk-v*`. Each triggers this workflow independently. Every invocation of `bin/publish-pypi` publishes *both* packages unconditionally, so the second triggered run will attempt to re-upload artifacts already present on PyPI. Since `rye publish` (via twine) exits non-zero on a 409 Conflict and the script runs with `set -eux`, the second workflow run will fail. Adding `--skip-existing` to both `rye publish` calls in `bin/publish-pypi` (or switching to `twine upload --skip-existing dist/*`) would make the publish script idempotent and tolerate this re-trigger.
    
    How can I resolve this? If you propose a fix, please make it concise.

    Fix in Cursor Fix in Claude Code Fix in Codex

Fix All in Cursor Fix All in Claude Code Fix All in Codex

Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
scripts/check-slim-deps:14-24
**Guardrail doesn't cover `[project.optional-dependencies]`**

The script reads only `[project.dependencies]` and compares against the 6 expected slim deps. `pyproject.toml` currently has an `aiohttp` optional extra (`["aiohttp", "httpx_aiohttp>=0.1.9"]`) that sits entirely outside this check. If Stainless regen re-adds ADK-related packages as optional extras (rather than direct deps), the CI gate won't catch it. A consumer running `pip install agentex-sdk-client[aiohttp]` would unexpectedly pull in those extras without any guardrail firing.

Reviews (9): Last reviewed commit: "feat(packaging)!: split agentex-sdk into..." | Re-trigger Greptile

@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>
@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
Base automatically changed from maxparke/agx1-292-promote-protocol-types to next May 27, 2026 15:36
@max-parke-scale max-parke-scale marked this pull request as ready for review May 27, 2026 15:37
@max-parke-scale max-parke-scale force-pushed the maxparke/agx1-292-prototype-client-split branch from 1198bf9 to da5b8f0 Compare May 27, 2026 15:37
Comment thread bin/publish-pypi Outdated
Comment thread adk/pyproject.toml
@max-parke-scale
Copy link
Copy Markdown
Contributor Author

Fixed in cdb3a48:

  • bin/publish-pypi: added --wheel to both slim and heavy rye build --clean so neither falls into rye's sdist-then-wheel-from-sdist default (which silently produces an empty heavy wheel because the unpacked sdist can't resolve adk/'s ../src/agentex/lib force-include).
  • adk/pyproject.toml: dropped the malformed "/../src/agentex/lib/**" sdist entry; sdist support for the dual-package layout is deferred (CI + publish-pypi are wheel-only).

Verified locally: both wheels build with uvx pyproject-build --wheel and have disjoint contents (slim has 0 agentex/lib/* files; heavy has 347 lib/* files + no top-level client surface).

@greptile review

🤖 — posted via Claude Code

@max-parke-scale
Copy link
Copy Markdown
Contributor Author

@greptile review

@max-parke-scale max-parke-scale force-pushed the maxparke/agx1-292-prototype-client-split branch 2 times, most recently from 69316be to 820c3be Compare May 29, 2026 20:08
max-parke-scale added a commit that referenced this pull request May 31, 2026
…-wheel tests

Addresses review of the dual-wheel split (#370):

- adk: pin agentex-sdk-client floor-only (>=0.11.5). The two packages
  co-version and release-please can't rewrite a dep string, so a <0.12
  ceiling would make the first 0.12.0 cut unresolvable (slim is a new
  PyPI name with no 0.11.x to fall back to).
- adk: prune agentex/lib/** test files from the heavy wheel via a custom
  hatch build hook — force-include ignores `exclude` (hatchling #1395).
  Drops 14 test files; keeps the 138 .j2 templates and py.typed. The hook
  imports build-only hatchling, so it's excluded from pyright.
- release-please-config: scope extra-files (_version.py) to the slim
  package so a heavy-only release can't overwrite the slim's __version__.
- run_agent_test.sh: fail loud when a wheel is missing instead of
  silently testing the pre-installed SDK; fix the dead repo-root fallback
  glob (quoted inside ls).
- ci: add scripts/check-slim-deps guardrail asserting root pyproject keeps
  exactly the 6 slim deps — catches Stainless re-adding the ADK deps.
- requirements{,-dev}.lock: regenerate for the two-package workspace via
  rye sync — the locks still named the pre-split agentex-sdk and pinned
  openai-agents below the new >=0.14.3 floor.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@max-parke-scale max-parke-scale force-pushed the maxparke/agx1-292-prototype-client-split branch from 820c3be to 7737475 Compare May 31, 2026 20:32
Publishes the existing wheel as two namespace-sharing packages so REST-only
consumers install just the Stainless client without the ADK runtime.

- agentex-sdk-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-sdk-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.

Release/publish wiring:
- release-please two-component mode (`.` + `adk/`), include-component-in-tag.
- bin/publish-pypi publishes slim before heavy; both --wheel + --skip-existing.
- bin/check-release-environment + publish-pypi.yml validate/pass both tokens.
- 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>
@max-parke-scale max-parke-scale force-pushed the maxparke/agx1-292-prototype-client-split branch from 7737475 to 715209b Compare June 2, 2026 18:24
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