Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
a942fd2
feat(types): add the AdcpRequest/AdcpResponse lattice and normalize t…
KonstantinMirin Oct 4, 2026
4a5f772
test(types): record the forged-model refusal and the two predicates' …
KonstantinMirin Oct 4, 2026
82bfda6
refactor(types): give the task-message resolution one home, runnable …
KonstantinMirin Oct 4, 2026
2183e58
fix(types): refuse to normalize a schema whose union arm is a bare $ref
KonstantinMirin Oct 4, 2026
5db51c4
refactor(types)!: make every canonical model a subclass, not a runtim…
KonstantinMirin Oct 4, 2026
e77e348
feat(types): add AdcpVersionEnvelope to the curated protocol surface
KonstantinMirin Oct 4, 2026
fcbf422
feat(codegen): derive a union arm's name from its schema node
KonstantinMirin Oct 4, 2026
5d705ca
test(codegen): pin the two response arms the naming rule cannot name …
KonstantinMirin Oct 4, 2026
e3b07c8
fix(types): an inherited validator that reads a removed field reads None
KonstantinMirin Oct 4, 2026
f8706bc
fix(codegen): read a composing root's alias the way Annotated is defined
KonstantinMirin Oct 4, 2026
37367fd
fix(types): stop a tolerant delivery model passing as its strict wire…
KonstantinMirin Oct 4, 2026
24c4559
test(types): grade validator inheritance instead of clone carrying
KonstantinMirin Oct 4, 2026
2549291
fix(types): take the domain mirror in the SDK's own decisioning helper
KonstantinMirin Oct 4, 2026
731ffa7
docs: document the type surface and the generation pipeline
KonstantinMirin Oct 4, 2026
83d4c23
refactor(codegen)!: generate the domain tree as the public tree
KonstantinMirin Oct 5, 2026
724751c
docs: the generated tree is the public tree, not an internal layer
KonstantinMirin Oct 5, 2026
866d59c
test(types): grade the canonical field removal, and hide it from the …
KonstantinMirin Oct 5, 2026
48c2368
feat(types)!: serve the pre-9.0 generated_poc path from the module it…
KonstantinMirin Oct 5, 2026
3bb00f4
feat(types)!: make format_kind an open str the SDK never refuses
KonstantinMirin Oct 6, 2026
607adf4
fix(types): serve a split generated_poc module from both of its halves
KonstantinMirin Oct 6, 2026
633559b
fix(types): make every Legacy name the generated class, one per respo…
KonstantinMirin Oct 6, 2026
152f15b
docs: name the one generated_poc stem that split, where an adopter re…
KonstantinMirin Oct 6, 2026
0107506
fix(types): tell a split deprecated name's reader where its classes went
KonstantinMirin Oct 6, 2026
2687a8e
chore(codegen): move the unwired union-arm naming rule to #1409
bokelley Oct 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1162,7 +1162,7 @@ jobs:
# The deep submodule path (some older import sites reach this far)
# must also surface the migration pointer, not a bare ModuleNotFoundError.
try:
from adcp.types.generated_poc.enums.format_category import FormatCategory # noqa: F401
from adcp.types.domains.enums.format_category import FormatCategory # noqa: F401
except ImportError as exc:
assert "MIGRATION_v3_to_v4" in str(exc), exc
else:
Expand Down Expand Up @@ -1208,7 +1208,7 @@ jobs:

# Blocking. Grades the COMMITTED generated tree against a regeneration
# from the COMMITTED schemas, so it runs before the sync steps below
# replace `schemas/cache/`. A hand edit to `src/adcp/types/generated_poc/**`,
# replace `schemas/cache/`. A hand edit to `src/adcp/types/domains/**`,
# `_generated.py` or `_ergonomic.py` fails here; without this step the
# "Generate models" step overwrites the edit before anything inspects it.
# `generate_types.py --check` stages the regeneration in a temp tree and
Expand Down
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@ repos:
args: [--line-length=100]
# Version-scoped stubs are deterministically emitted and checked by
# make validate-generated; reformatting them makes the checked output stale.
exclude: ^(src/adcp/types/generated_poc/|src/adcp/types/v(30|31|32)\.pyi$|src/adcp/types/versioned_bases/_?v(30|31|32)\.(py|pyi)$)
exclude: ^(src/adcp/types/domains/|src/adcp/types/v(30|31|32)\.pyi$|src/adcp/types/versioned_bases/_?v(30|31|32)\.(py|pyi)$)

# Ruff linting (ignoring line-length for now - black handles it)
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.9.2
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix, --ignore=E501]
exclude: ^(src/adcp/types/generated_poc/|src/adcp/types/versioned_bases/_?v(30|31|32)\.(py|pyi)$)
exclude: ^(src/adcp/types/domains/|src/adcp/types/versioned_bases/_?v(30|31|32)\.(py|pyi)$)

# Mypy type checking (local hook to use project dependencies)
- repo: local
Expand Down
4 changes: 3 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,9 @@ from adcp.types import GetProductsRequest, CreateMediaBuyRequest, Product, Packa
# Or import from a curated partial module for a narrower surface:
# adcp.types.media_buy / creative / signals / protocol / buyer / seller
from adcp.types.media_buy import CreateMediaBuyRequest
# Never import from adcp.types.generated_poc.* or adcp.types._generated (internal)
# adcp.types.domains.<domain>[.<schema>] is public — it is where codegen defines
# every class. Reach for it when a name several schemas declare is not bound flat.
# Never import from adcp.types._generated (internal: one winner per bare name)

# Response variant types (discriminated unions)
from adcp.types.aliases import CreateMediaBuySuccessResponse, CreateMediaBuyErrorResponse
Expand Down
29 changes: 18 additions & 11 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,17 @@
`Idempotency3`, `Type11`) by traversal order. Adding or reordering even one
sibling schema shifts the entire numbering window — a clean re-run can produce
a 600+ file diff with zero semantic change. Treat any `git diff` on
`generated_poc/` that consists of only `ClassNameN → ClassNameM` renames as
churn, not a schema delta, and discard it. `aliases.py` re-exports these
`src/adcp/types/domains/` that consists of only `ClassNameN → ClassNameM`
renames as churn, not a schema delta, and discard it. `aliases.py` re-exports these
numbered classes under semantic names; accepting the renumber breaks the
alias layer for nothing.

**Import Architecture for Generated Types**
The type system has a strict layering to prevent brittleness:

```
generated_poc/*.py (internal, auto-generated from schemas)
domains/**/*.py (auto-generated from schemas; where every class is DEFINED,
and a public address: adcp.types.domains.<domain>.<schema>)
↓
_generated.py (internal consolidation)
↓
Expand All @@ -39,19 +40,25 @@ runtime `__getattr__`/`__dir__` live under `if not TYPE_CHECKING:` so type
checkers see the surface only via the explicit `TYPE_CHECKING` re-export block
— a typo'd import is flagged, not silently typed as `object`.

Only these modules may import from `generated_poc/` or `_generated.py`
(enforced by `tests/test_import_layering.py`):
- `_generated.py`: Consolidates exports from `generated_poc/` into a flat namespace
`domains/` is where the generator writes, and that is the whole point: there
is no private tree behind the public one and no re-export tree in front of it,
so nothing is copied and nothing can drift. A domain path is a supported
import — reach for it when the flat `adcp.types` namespace cannot bind the
name you need, because several schemas declare it. The generated class NAMES
are still generator-derived, so a numbered name can renumber on a schema bump;
`aliases.py` is what gives those a stable spelling.

Only these modules may import from `_generated.py` — the one remaining
internal namespace, where a bare name resolves to a single winner (enforced by
`tests/test_import_layering.py`):
- `_generated.py`: Consolidates `domains/` exports into a flat namespace
- `_eager.py`: Eager realization of the public surface — binds every exported name and runs the import-time patchers (`_ergonomic`, `_forward_compat`)
- `aliases.py`: Creates semantic aliases for numbered discriminated union types
- `capabilities.py`: Re-exports `get_adcp_capabilities_response` sub-models with disambiguated names
- `_ergonomic.py`: Applies BeforeValidator coercion for type ergonomics
- `_forward_compat.py`: Patches `Format.assets` / `RepeatableAssetGroup.assets` with open union types at import time
- `legacy.py`: Explicit facade for generated named-format wire models during the v7 migration
- `canonical_creative.py`: Canonical-first boundary models that replace generated creative lifecycle surfaces
- `__init__.py`: Public API surface (thin lazy facade)

All other source code should import from `adcp.types` (the public API).
All other source code should import from `adcp.types` (the public API), or from
a domain path when the name it needs is not bound there.

**Type Checking Best Practices**
- Use `TYPE_CHECKING` for optional dependencies to avoid runtime import errors
Expand Down
14 changes: 10 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,10 +150,16 @@ Pick the surface you are adding to:
be exported from `adcp.types` — partial modules only re-curate that surface; they
never import the generated layer.

Never import from `adcp.types.generated_poc.*` or `adcp.types._generated` outside
the allowlisted layering modules (`_generated.py`, `aliases.py`, `_ergonomic.py`,
`_forward_compat.py`, `capabilities.py`, `canonical_decl.py`, `_eager.py`, and
`types/__init__.py`). The generated class names are unstable across schema regen.
Never import from `adcp.types._generated` outside the allowlisted layering
modules (`_generated.py`, `aliases.py`, `capabilities.py`, `_eager.py`, and
`types/__init__.py`): it resolves a bare name that several schemas declare to a
single sort-order winner, so a schema addition can repoint it silently.

`adcp.types.domains.<domain>[.<schema>]` is public — codegen defines every class
there and `tests/fixtures/public_api_snapshot.json` pins what each domain root
resolves to. Prefer the flat `adcp.types` surface; take a domain path for a name
the flat surface cannot bind. The generated class NAMES are still unstable across
schema regen, which is what `aliases.py` exists to absorb.

After an intentional change to `adcp.__all__` or `adcp.types.__all__`, regenerate
the public-API snapshot:
Expand Down
20 changes: 12 additions & 8 deletions LADON.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,19 +34,23 @@ moment upstream adds a variant.

### Mandatory: type-system import layering

Only `_generated.py`, `_eager.py`, `aliases.py`, `capabilities.py`,
`_ergonomic.py`, `_forward_compat.py`, and `types/__init__.py` may import from
`src/adcp/types/generated_poc/**` or `src/adcp/types/_generated.py`;
`tests/test_import_layering.py` enforces the allowlist. Any other module
importing generated names directly is a `high` finding — it ships unstable
codegen identifiers to adopters. `adcp/types/__init__.py` is a PEP 562 lazy
Only `_generated.py`, `_eager.py`, `aliases.py`, `capabilities.py`, and
`types/__init__.py` may import from `src/adcp/types/_generated.py`;
`tests/test_import_layering.py` enforces the allowlist. `_generated.py` binds a
bare name several schemas declare to one sort-order winner, so importing it
outside those layers is a `high` finding.

`src/adcp/types/domains/**` is the public definition site, not an internal
layer: importing a domain path is allowed anywhere. Prefer `adcp.types`, and
flag an import of a NUMBERED generated class (`Assets162`, `Type11`) wherever it
appears outside `aliases.py` — that identifier is what renumbers. `adcp/types/__init__.py` is a PEP 562 lazy
facade with its runtime `__getattr__`/`__dir__` under `if not TYPE_CHECKING:`;
reintroducing eager imports there, or moving the `__getattr__` out from under the
guard, regresses both import cost and type-checker coverage.

### Mandatory: generated code is not source

A hand-edit inside `src/adcp/types/generated_poc/**` or
A hand-edit inside `src/adcp/types/domains/**` or
`src/adcp/types/_generated.py` is a `high` finding. Legitimate changes there are
regeneration output and pair with an upstream schema change under `schemas/` or an
`src/adcp/ADCP_VERSION` bump; check that `aliases.py` and `_ergonomic.py` still
Expand Down Expand Up @@ -106,7 +110,7 @@ public export landing without a matching entry in `AGENTS.md`'s handler table is

## Trivial Paths

- src/adcp/types/generated_poc/**
- src/adcp/types/domains/**
- src/adcp/types/_generated.py
- CHANGELOG.md

Expand Down
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ Pre-configured agents (all include `.simple` accessor):

See [examples/simple_api_demo.py](examples/simple_api_demo.py) for a complete comparison.

> **Tip**: Import types from the main `adcp` package (e.g., `from adcp import GetProductsRequest`), from `adcp.types`, or from a curated partial module (`adcp.types.media_buy`, `.creative`, `.signals`, `.protocol`, `.buyer`, `.seller`) — never from the internal `adcp.types.generated_poc.*` layer. `import adcp` is lightweight; the generated type graph is built only when you import a type.
> **Tip**: Import types from the main `adcp` package (e.g., `from adcp import GetProductsRequest`), from `adcp.types`, or from a curated partial module (`adcp.types.media_buy`, `.creative`, `.signals`, `.protocol`, `.buyer`, `.seller`); `adcp.types.domains.*` is public too, and is where a name several schemas declare is unambiguous. `import adcp` is lightweight; the generated type graph is built only when you import a type.

## Quick Start: Distributed Operations

Expand Down Expand Up @@ -368,6 +368,8 @@ the SDK's supported versions in the error details.

- **[API Reference](https://adcontextprotocol.github.io/adcp-client-python/)** - Complete API documentation with type signatures and examples
- **[Protocol Spec](https://github.com/adcontextprotocol/adcp)** - Ad Context Protocol specification
- **[The type surface](docs/type-surface.md)** - Where a generated class lives, which spelling to import it by, and what `AdcpRequest` and `AdcpResponse` guarantee
- **[How the type tree is generated](docs/design/type-generation.md)** - The generation pipeline, name derivation, the guards, and the generator pin
- **[Handler authoring](docs/handler-authoring.md)** - Building an AdCP-compliant agent on `adcp.server`
- **[Production seller path](docs/production-seller.md)** - Choose the server abstraction and wire durable multi-tenant tasks and webhook delivery
- **[Validation contract](docs/validation-contract.md)** - Canonical wire validation versus structural Pydantic models
Expand Down Expand Up @@ -515,7 +517,7 @@ from adcp.types.buyer import GetProductsRequest, CpmPricingOption
from adcp.types.seller import Offering, PropertyList, ContentStandards
```

If a type you need isn't in `adcp.types`, open an issue — we'll add an alias. The `adcp.types.generated_poc.*` modules are internal; class names and module paths shift on every schema regeneration and are not a supported API.
If a type you need isn't in `adcp.types`, open an issue — we'll add an alias. The `adcp.types.domains.*` modules are public and are where codegen defines every class, so they always have it; the caveat is the class NAMES, which codegen derives and renumbers (`Assets162`) — an alias is the stable spelling.

The six partial modules (`media_buy`, `creative`, `signals`, `protocol`, `buyer`, `seller`) are for curation and discoverability — they group types by domain and give you a smaller import surface. They are **not** a per-domain performance tier: the first AdCP type you touch through any of them builds the same single Pydantic graph.

Expand Down Expand Up @@ -559,7 +561,7 @@ See `examples/type_aliases_demo.py` for more examples.
**Import guidelines:**
- ✅ **DO**: Import from main package: `from adcp import GetProductsRequest`
- ✅ **DO**: Use semantic aliases: `from adcp import CreateMediaBuySuccessResponse`
- ⚠️ **AVOID**: Import from `adcp.types.generated_poc.*` — paths and class names (including numbered `Assets*` variants) change on every schema regeneration.
- ⚠️ **AVOID**: Import a numbered generated class (`Assets5`, `Type11`) by any path — codegen renumbers them on every schema regeneration. `adcp.types.domains.<domain>.<schema>` itself is a supported import.

The main package exports provide a stable API while internal generated types may change.

Expand Down
27 changes: 26 additions & 1 deletion SCHEMA_DELTAS.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,28 @@
# Generated-types delta

_No field-shape changes detected._
## Field changes

- `account/list_account_changes_request.py`
- `ListAccountChangesRequest`: `-adcp_major_version`, `-adcp_version`
- `brand/search_brands_request.py`
- `SearchBrandsRequest`: `-adcp_major_version`
- `media_buy/accept_proposal_request.py`
- `AcceptProposalRequest`: `-adcp_major_version`, `-adcp_version`
- `media_buy/buy_products_request.py`
- `BuyProductsRequest`: `-adcp_major_version`, `-adcp_version`
- `media_buy/control_media_buy_request.py`
- `ControlMediaBuyRequest`: `-adcp_major_version`, `-adcp_version`
- `media_buy/decline_proposals_request.py`
- `DeclineProposalsRequest`: `-adcp_major_version`, `-adcp_version`
- `media_buy/list_products_request.py`
- `ListProductsRequest`: `-adcp_major_version`, `-adcp_version`
- `media_buy/refine_proposals_request.py`
- `RefineProposalsRequest`: `-adcp_major_version`, `-adcp_version`
- `media_buy/request_proposals_request.py`
- `RequestProposalsRequest`: `-adcp_major_version`, `-adcp_version`
- `media_buy/sync_reporting_status_request.py`
- `SyncReportingStatusRequest`: `-adcp_major_version`, `-adcp_version`
- `trusted_match/context_match_request.py`
- `ContextMatchRequest`: `-adcp_major_version`, `-adcp_version`
- `trusted_match/identity_match_request.py`
- `IdentityMatchRequest`: `-adcp_major_version`, `-adcp_version`
Loading
Loading