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
3 changes: 2 additions & 1 deletion .github/scripts/check_python_api_compatibility.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@
)
_GRIFFE_VERSION = version("griffe")
_PACKAGE_STATUS_ROW = re.compile(
r"^\| `(?P<name>[^`]+)` \| `(?P<path>python/packages/[^`]+)` \| `(?P<state>[^`]+)` \|$"
r"^\| `(?P<name>[^`]+)` \| `(?P<path>python/packages/[^`]+)` \| `(?P<state>[^`]+)` \|"
r"(?: `[^`]+` \|)?$"
)


Expand Down
10 changes: 10 additions & 0 deletions .github/tests/test_python_api_compatibility.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,16 @@ def test_discovers_only_released_non_lab_modules(self) -> None:

self.assertEqual([spec.module for spec in specs], ["stable_api"])

def test_package_status_row_allows_optional_meta_package_column(self) -> None:
rows = [
"| `agent-framework-core` | `python/packages/core` | `released` |",
"| `agent-framework-core` | `python/packages/core` | `released` | `yes` |",
]

for row in rows:
with self.subTest(row=row):
self.assertIsNotNone(api_checker._PACKAGE_STATUS_ROW.match(row))

def test_filters_base_unreleased_apis_but_not_head_only_decorators(self) -> None:
self.write_status([("agent-framework-core", "core", "released")])
self.write_module(
Expand Down
4 changes: 4 additions & 0 deletions python/.github/skills/python-feature-lifecycle/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ These are related, but they are **not the same thing**.
- **Feature-stage decorators are only for exceptions** when a feature is behind the package's default stage.
- Do **not** decorate every class or function just because the package is experimental or release candidate.

This skill describes staged API behavior within a package. For creating or promoting a package, including sample
placement, meta-package membership, versioning, and lazy-loading namespace decisions, follow the
`python-package-management` skill.

### Important default

If a package is still in **beta / experimental preview**, all public APIs in that package are experimental by default.
Expand Down
110 changes: 89 additions & 21 deletions python/.github/skills/python-package-management/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,15 @@ python/

- `agent-framework-core` contains core abstractions and OpenAI/Azure OpenAI built-in
- Provider packages extend core with specific integrations
- Root `agent-framework` depends on `agent-framework-core[all]`
- Root `agent-framework` depends on `agent-framework-core[all]`. Despite the existing extra name, this is a
curated standard install set, not every package at a particular lifecycle stage.
Comment thread
eavanvalkenburg marked this conversation as resolved.
- Entries in `agent-framework-core[all]` carry lower and upper bounds. Workspace integrations use the version
released with the current Core cohort as their lower bound so pip does not backtrack across old cyclic
`core[all] -> integration -> core` combinations.
- `packages/lab` is intentionally excluded from the root uv workspace. It has its own
`pyproject.toml`, `uv.lock`, environment, CI matrix, and dependency updates so experimental
dependencies cannot constrain released provider packages. Lab is also an explicit exception
to the lifecycle rule that beta packages belong in `agent-framework-core[all]`. Lab resolves
released Core and provider distributions rather than local root-workspace packages.
dependencies cannot constrain released provider packages. Lab resolves released Core and provider
distributions rather than local root-workspace packages.

## Dependency Management

Expand Down Expand Up @@ -114,6 +114,35 @@ uv run poe test
- Run Lab dependency and validation commands from `packages/lab`; root workspace tasks deliberately do not
discover or update the Lab project.

## Meta-package Membership

Package lifecycle and membership in the root `agent-framework` meta-package are independent decisions. A package
starts outside `agent-framework-core[all]`, and promotion to `beta`, `rc`, or `released` does not add it
automatically.

Adding a package to or removing a package from the standard install set requires an agreed GitHub issue and an
explicit core-team decision. For additions, consider:

- whether the package is useful across common Agent Framework workloads rather than for one specialized storage
backend, hosting adapter, channel, or deployment target;
- whether its dependency footprint is proportionate and compatible with the meta-package's supported Python and
platform matrix, especially for native, heavyweight, or conflict-prone dependencies;
- whether it belongs in the documented standard getting-started experience; and
- whether including one package would create an expectation that an open-ended family of alternative backends or
adapters must also be included.

Default to a separate install when the case is unclear. Existing membership is not precedent for every package in
the same lifecycle stage or category.

For removals, the issue must also assess compatibility for existing `pip install agent-framework` and
`agent-framework-core[all]` users, document any migration or replacement path, determine whether the change is
breaking, and identify the required Core and root meta-package version changes. Do not treat removal as ordinary
dependency cleanup.

Track the decision in the `Meta package` column of `python/PACKAGE_STATUS.md`. When membership changes, update
`packages/core/pyproject.toml` `[project.optional-dependencies].all`, the Core and root package versions as required,
and the status table in the same change.

## Lazy Loading Pattern

### Root core API
Expand Down Expand Up @@ -151,6 +180,19 @@ def __getattr__(name: str) -> Any:
) from exc
```

### Namespace compatibility

The core namespace module and the objects it re-exports have different compatibility rules:

- While an owning package is `alpha`, `beta`, or `rc`, imports for objects from that package may be added, removed,
or moved between approved lazy-loading namespaces by explicit core-team decision. Those object paths follow the
owning package's prerelease lifecycle.
- Do not remove a core namespace module unless it is empty after the change and is used exclusively by packages
that have not reached `released`.
- If a namespace exposes objects from any released package, keep the namespace. Changing import paths for released
objects requires a deprecated forwarding path where feasible; otherwise the agreed issue must include migration
guidance, explicit breaking-change acknowledgement, and the required Core and root meta-package version changes.

## Adding a New Connector Package

**Important:** Do not create a new package unless approved by the core team.
Expand All @@ -166,9 +208,9 @@ Every new package starts as `alpha`.
5. Include samples inside the package (e.g., `packages/my-connector/samples/`)
6. Do **NOT** add to `[all]` extra in `packages/core/pyproject.toml`
7. Do **NOT** create lazy loading in core yet
8. Add the package to `python/PACKAGE_STATUS.md` and keep that file updated when packages are added,
removed, renamed, or promoted. If the package exposes individually staged APIs, keep the feature list
there current too.
8. Add the package to `python/PACKAGE_STATUS.md` with lifecycle state `alpha` and meta-package membership `no`,
and keep that file updated when packages are added, removed, renamed, promoted, or have their membership
changed. If the package exposes individually staged APIs, keep the feature list there current too.
9. Keep the `Typing :: Typed` classifier and the `py.typed` marker in step. The classifier alone is a
promise PEP 561 keeps through the `py.typed` file inside the package directory, and that file is
the signal checkers act on: a package that declares `Typing :: Typed` without shipping it is not
Expand Down Expand Up @@ -204,6 +246,19 @@ Recommended dependency workflow during connector implementation:

### Promotion path

Create a GitHub issue for every package promotion and get core-team agreement before implementing the promotion.
The issue must record:

- the current and target lifecycle stages;
- whether the package should be included in the root meta-package, with rationale based on the criteria above; and
- the lazy-loading namespace decision for the transition: `alpha` -> `beta` selects whether and where to expose the
package, while `beta` -> `rc` and `rc` -> `released` preserve the existing namespace by default and may change
the package's prerelease object imports only by explicit core-team decision and under the namespace compatibility
rules above.

Record an explicit `no` or `none` when the package should remain outside the meta-package or should not have a
lazy-loading namespace. Do not infer either decision from the target lifecycle stage.

Promotion work is not isolated to the package being promoted. If a promotion changes dependency
metadata for downstream packages, also update the dependent packages' own versions so they publish
new metadata alongside the promoted dependency bounds.
Expand All @@ -212,14 +267,16 @@ promotions as well as standalone version update work.

#### Alpha -> Beta

Move a package to `beta` when it is stable enough to be part of the main install surface.
Move a package to `beta` when it is stable enough for broader prerelease use.

1. Update the package version to the beta pattern: `1.0.0b<date>`
2. Update the classifier to `Development Status :: 4 - Beta`
3. Add the package to `[all]` in `packages/core/pyproject.toml`
4. Move samples to the root `samples/` tree and remove package-local samples
5. Create or update the relevant lazy-loading namespace in core when the package belongs under one
6. Update `python/PACKAGE_STATUS.md`
3. Move samples to the root `samples/` tree and remove package-local samples
4. Apply the lazy-loading decision recorded in the promotion issue, creating or updating the approved namespace
when applicable
5. Apply the meta-package decision recorded in the promotion issue. The default is to remain excluded; add the
package to `[all]` only when inclusion is explicitly approved.
6. Update the lifecycle state and meta-package membership in `python/PACKAGE_STATUS.md`

After `alpha`, there should be no samples left inside a package folder.

Expand All @@ -230,20 +287,30 @@ Move a package to `rc` when its API is close to the final released shape.
1. Update the package version to the release-candidate pattern: `1.0.0rc<number>`
2. Keep the classifier at `Development Status :: 4 - Beta` because PyPI does not have a separate
release-candidate classifier
3. Keep the package in `core[all]`
4. Keep samples only in the root `samples/` tree
5. Update `python/PACKAGE_STATUS.md` to show the package as `rc`
3. Apply the meta-package decision recorded in the promotion issue, preserving current membership when unchanged
or using the membership-change process above when an addition or removal is explicitly approved; the stage
transition alone does not add it to `core[all]`
4. Preserve the existing lazy-loading namespace by default. Changes to the package's prerelease object imports are
discouraged and require explicit core-team approval recorded in the promotion issue; apply the namespace
compatibility rules above
5. Keep samples only in the root `samples/` tree
6. Update `python/PACKAGE_STATUS.md` to show the package as `rc` and keep its membership value current

#### RC -> Released

Move a package to `released` when it no longer carries a prerelease qualifier.

1. Update the package version to the stable pattern: `1.0.0`
2. Update the classifier to `Development Status :: 5 - Production/Stable`
3. Keep the package in `core[all]`
4. Keep samples only in the root `samples/` tree
5. Update `python/PACKAGE_STATUS.md` to show the package as `released`
6. Update all `README.md` files that install that package with
3. Apply the meta-package decision recorded in the promotion issue, preserving current membership when unchanged
or using the membership-change process above when an addition or removal is explicitly approved; release does
not imply inclusion in `core[all]`
4. Preserve the lazy-loading namespace used at RC by default. Any final changes to the package's prerelease object
imports require explicit core-team approval recorded in the promotion issue; apply the namespace compatibility
rules above
5. Keep samples only in the root `samples/` tree
6. Update `python/PACKAGE_STATUS.md` to show the package as `released` and keep its membership value current
7. Update all `README.md` files that install that package with
`pip install agent-framework-... --pre` so they use `pip install agent-framework-...` without
the `--pre` suffix

Expand Down Expand Up @@ -291,8 +358,8 @@ Move a package to `released` when it no longer carries a prerelease qualifier.

```bash
pip install agent-framework-core # Core only
pip install agent-framework-core[all] # Core + all connectors
pip install agent-framework # Same as core[all]
pip install agent-framework-core[all] # Core + curated standard integrations
pip install agent-framework # Same standard install set as core[all]
pip install agent-framework-foundry # Specific connector (pulls in core)
```

Expand All @@ -305,6 +372,7 @@ When changing a package, check if its `AGENTS.md` needs updates:

Keep `python/PACKAGE_STATUS.md` updated when:
- A package is added, removed, renamed, or promoted between lifecycle stages
- A package is added to or removed from the root meta-package
- A package starts or stops exposing individually staged experimental or release-candidate APIs

When a package adds, removes, or renames environment variables, update the related documentation in the same
Expand Down
4 changes: 2 additions & 2 deletions python/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ Instructions for AI coding agents working in the Python codebase.
- `python-development` — coding standards, type annotations, docstrings, logging, performance
- `python-testing` — test structure, fixtures, async mode, running tests
- `python-code-quality` — linting, formatting, type checking, prek hooks, CI workflow
- `python-feature-lifecycle` — package vs feature lifecycle stages, decorators, enums, and promotion guidance
- `python-package-management` — monorepo structure, lazy loading, versioning, new packages
- `python-feature-lifecycle` — package-stage meaning plus staged API decorators, enums, and feature promotion
- `python-package-management` — package creation and promotion, meta-package membership, lazy loading, and versioning
- `pull-requests` — writing PR descriptions (template) and handling/resolving PR review comments
- `agent-framework-py-release` — Python release PR workflow, CHANGELOG-driven package bumps, lifecycle version rules, and dependency-floor validation

Expand Down
50 changes: 7 additions & 43 deletions python/CODING_STANDARD.md
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,7 @@ python/
├── samples/ # Sample code and examples
├── packages/
│ ├── core/ # agent-framework-core - Core abstractions and implementations
│ │ ├── pyproject.toml # Defines [all] extra that includes all connector packages
│ │ ├── pyproject.toml # Defines the curated standard-integration [all] extra
│ │ ├── tests/ # Tests for core package
│ │ └── agent_framework/
│ │ ├── __init__.py # Lazy runtime public API exports
Expand Down Expand Up @@ -423,47 +423,11 @@ def __getattr__(name: str) -> Any:
) from exc
```

### Adding a New Connector Package

**Important:** Do not create a new package unless there is an issue that has been reviewed and approved by the core team.

#### Initial Release (Preview Phase)

For the first release of a new connector package:

1. Create a new directory under `packages/` (e.g., `packages/my-connector/`)
2. Add the package to `tool.uv.sources` in the root `pyproject.toml`
3. Include samples inside the package itself (e.g., `packages/my-connector/samples/`)
4. **Do NOT** add the package to the `[all]` extra in `packages/core/pyproject.toml`
5. **Do NOT** create lazy loading in core yet

#### Promotion to Stable

After the package has been released and gained a measure of confidence:

1. Move samples from the package to the root `samples/` folder
2. Add the package to the `[all]` extra in `packages/core/pyproject.toml`
3. Create a provider folder in `agent_framework/` with lazy loading `__init__.py`

### Versioning and Core Dependency

All non-core packages declare a lower bound on `agent-framework-core` (e.g., `"agent-framework-core>=1.0.0b260130"`). Follow these rules when bumping versions:

- **Core version changes**: When `agent-framework-core` is updated with breaking or significant changes and its version is bumped, update the `agent-framework-core>=...` lower bound in every other package's `pyproject.toml` to match the new core version.
- **Non-core version changes**: Non-core packages (connectors, extensions) can have their own versions incremented independently while keeping the existing core lower bound pinned. Only raise the core lower bound if the non-core package actually depends on new core APIs.

### External Dependency Version Bounds

The guiding principle for external dependencies is to make the range of allowed versions as broad as possible, even if that means we have to do some conditional imports, and other tricks to allow small changes in versions.
So we use bounded ranges for external package dependencies in `pyproject.toml`:


- For stable dependencies (`>=1.0.0`), use a lower bound at a known-good version and an explicit upper bound that reflects the maximum major version we currently support (for example: `openai>=1.99.0,<3`).
- For prerelease (`dev`/`a`/`b`/`rc`) dependencies, use a known-good lower bound with a hard upper boundary in the same prerelease line (for example: `azure-ai-projects>=2.0.0b3,<2.0.0b4`).
- For `<1.0.0` dependencies, use a known-good bounded range with an explicit upper cap. Prefer the broadest validated range the package can actually support: that may be a patch line, a minor line, or multiple minor lines (for example: `a2a-sdk>=0.3.5,<0.4.0`, `fastapi>=0.115.0,<0.136.0`, `uvicorn>=0.30.0,<0.39.0`).
- For prerelease (`dev`/`a`/`b`/`rc`) dependencies, use a known-good bounded range with a hard upper cap and keep the range only as broad as the package's validation coverage justifies.
- Prefer keeping support for multiple major versions when practical. This may mean that the upper bound spans multiple major versions when the dependency maintains backward compatibility; if APIs differ between supported majors, version-conditional imports/branches are acceptable to preserve compatibility.
- When adding or changing an external dependency, first run `uv run poe validate-dependency-bounds-test` to validate workspace-wide lower/upper compatibility, then run `uv run poe validate-dependency-bounds-project --mode both --package <workspace-package-name> --dependency "<dependency-name>"` to expand package-scoped bounds.
The decision to expose a package through lazy loading, and the namespace to use, is a package-governance decision
rather than a coding convention. Follow
[`python-package-management`](.github/skills/python-package-management/SKILL.md) for package creation, lifecycle
promotion, sample placement, meta-package membership, lazy-loading approval, versioning, and dependency bounds.
This section only defines how to implement an approved lazy-loading surface.

### Installation Options

Expand All @@ -473,7 +437,7 @@ Connectors are distributed as separate packages and are not imported by default
# Install core only
pip install agent-framework-core

# Install core with all connectors
# Install core with the curated standard integrations
pip install agent-framework-core[all]
# or (equivalently):
pip install agent-framework
Expand Down
Loading
Loading