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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ downloads/
eggs/
.eggs/
lib/
# The pinned TypeScript RC3 transition corpus intentionally mirrors an
# upstream test/lib directory inside test fixtures.
!tests/fixtures/canonical/typescript-13.0.0-rc.3/test/lib/
!tests/fixtures/canonical/typescript-13.0.0-rc.3/test/lib/**
lib64/
parts/
sdist/
Expand Down
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ Only these modules may import from `generated_poc/` or `_generated.py`
- `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).
Expand Down
46 changes: 46 additions & 0 deletions MIGRATION_v6_to_v7.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Migrating from Python SDK 6 to 7

Python SDK 7 makes canonical creatives the default application contract. The
package release (`7.0.0-rc`) is distinct from the negotiated AdCP protocol
version (`3.0` or `3.1`). AdCP 3.2 is not advertised until the SDK ships its
validator bundle.

Use `Format`, `Product.format_options`, `format_kind`, and
`format_option_refs` in normal application code. `Format` now means a
canonical declaration. Products, packages, creatives, filters, delivery
reads, callbacks, generic task execution, multi-agent clients, server
handlers, response builders, and asset helpers enforce this boundary.

Legacy named-format identity is explicit:

```python
from adcp.types.legacy import LegacyGetProductsRequest

raw = await client.get_products_legacy(LegacyGetProductsRequest(...))
```

`FormatId` and `list_creative_formats` are no longer normal root surfaces.
Use `LegacyFormatId`, `adcp.types.legacy`, and methods ending in `_legacy`
only for migration or conformance tooling. These methods emit
`DeprecationWarning` and are scheduled for removal with AdCP 4.0.

For seller-owned named formats, configure `legacy_format_converter`. For
canonical selections persisted across a JSON/process boundary, configure a
separate `canonical_format_legacy_resolver`; the SDK never reverse-guesses a
legacy tuple. Catalog snapshots can build both:

```python
from adcp.canonical_formats import projection_adapters_from_catalog_snapshots

adapters = projection_adapters_from_catalog_snapshots(snapshots)
client = ADCPClient(
agent,
legacy_format_converter=adapters.legacy_format_converter,
canonical_format_legacy_resolver=adapters.canonical_format_legacy_resolver,
)
```

AdCP 3.0 is upgraded on reads and downgraded on writes. AdCP 3.1 requires the
`media_buy.features.canonical_creatives` capability or unambiguous
request-local evidence. AdCP 3.2 will be canonical by contract once supported;
advertising `canonical_creatives: false` there will be an error.
24 changes: 15 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -276,13 +276,15 @@ async with ADCPMultiAgentClient(

## AdCP version support

The 6.x line is built against **AdCP 3.1.8 stable** and natively validates
both AdCP 3.0 and 3.1 wire shapes. Check the versions at runtime:
The 7.x line is built against **AdCP 3.1.8 stable**, makes canonical creatives
the primary Python contract, and negotiates AdCP 3.0, 3.1, and 3.2 wire
dialects. The SDK package version and protocol version are intentionally
independent:

```python
import adcp

adcp.get_adcp_sdk_version() # SDK package version, e.g. "6.4.1"
adcp.get_adcp_sdk_version() # SDK package version, e.g. "7.0.0rc1"
adcp.get_adcp_spec_version() # AdCP spec this build targets, e.g. "3.1.8"
```

Expand All @@ -296,6 +298,7 @@ forward traffic degrades gracefully rather than failing.
- **[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
- **[Handler authoring](docs/handler-authoring.md)** - Building an AdCP-compliant agent on `adcp.server`
- **[Migrating from SDK 6 to 7](MIGRATION_v6_to_v7.md)** - Canonical creative replacements and legacy escape hatches
- **[Testing your AdCP server](docs/testing-your-adcp-server.md)** - In-process harness for unit tests plus storyboard-runner compliance grading
- **[Multi-tenant contract](docs/multi-tenant-contract.md)** - Scope invariants every multi-tenant agent must satisfy
- **[Examples](examples/)** - Code examples and usage patterns
Expand Down Expand Up @@ -955,7 +958,8 @@ All AdCP tools with full type safety:

**Media Buy Lifecycle:**
- `get_products()` - Discover advertising products
- `list_creative_formats()` - Get supported creative formats
- Product `format_options` from `get_products()` - Discover accepted canonical formats
- `list_creative_formats_legacy()` - Explicit deprecated raw-format escape hatch
- `create_media_buy()` - Create new media buy
- `update_media_buy()` - Update existing media buy
- `sync_creatives()` - Upload/sync creative assets
Expand Down Expand Up @@ -1050,14 +1054,16 @@ Build and deliver production-ready creatives:
```python
from adcp import ADCPClient, AgentConfig
from adcp import PreviewCreativeRequest, BuildCreativeRequest
from adcp import CreativeManifest
from adcp import CreativeManifest, LegacyListCreativeFormatsRequest

# 1. Connect to creative agent
config = AgentConfig(id="creative_agent", agent_uri="https://...", protocol="mcp")
async with ADCPClient(config) as client:

# 2. List available formats
formats_result = await client.list_creative_formats()
# 2. Legacy-only named-format discovery for this pre-canonical build flow
formats_result = await client.list_creative_formats_legacy(
LegacyListCreativeFormatsRequest()
)

if formats_result.success:
# format_id is a FormatReferenceStructuredObject; reuse it directly
Expand Down Expand Up @@ -1135,7 +1141,7 @@ async with ADCPMultiAgentClient(

# 2. Get creative formats from creative agent
creative_agent = client.agent("creative")
formats = await creative_agent.simple.list_creative_formats()
formats = await creative_agent.simple.list_creative_formats_legacy()
format_id = formats.formats[0].format_id

# 3. Build creative asset
Expand Down Expand Up @@ -1534,7 +1540,7 @@ uvx adcp myagent create_media_buy '{
}'

# List creative formats with JSON output
uvx adcp --json myagent list_creative_formats | jq '.data'
uvx adcp --json myagent list_creative_formats_legacy | jq '.data'

# Debug connection issues
uvx adcp --debug myagent list_tools
Expand Down
2 changes: 1 addition & 1 deletion examples/fetch_preview_urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ async def main():

try:
# List formats with preview URL generation
result = await creative_agent.list_creative_formats(
result = await creative_agent.list_creative_formats_legacy(
ListCreativeFormatsRequest(), fetch_previews=True
)

Expand Down
2 changes: 1 addition & 1 deletion examples/hello_seller.py
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,7 @@ def get_media_buys(
"""
return {"media_buys": []}

def list_creative_formats(
def list_creative_formats_legacy(
self,
req: Any,
ctx: RequestContext[Any],
Expand Down
16 changes: 6 additions & 10 deletions examples/hello_seller_creative.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,14 @@
SingletonAccounts,
serve,
)
from adcp.types import AudioContent, CreativeManifest, FormatReferenceStructuredObject
from adcp.types import AudioContent, CreativeManifest


class HelloCreativeSeller(DecisioningPlatform):
"""The canonical minimal ``creative-generative`` adopter.

Implements only ``build_creative`` — the one required method on
:class:`CreativeBuilderPlatform`. Optional ``preview_creative``
Implements only ``build_creative_legacy`` — the one required method on
:class:`CreativeBuilderPlatform`. Optional ``preview_creative_legacy``
is omitted; the framework's ``_require_platform_method`` gate
returns ``UNSUPPORTED_FEATURE`` to buyers who call it on this
seller.
Expand All @@ -58,7 +58,7 @@ class HelloCreativeSeller(DecisioningPlatform):
)
accounts = SingletonAccounts(account_id="hello-creative")

def build_creative(
def build_creative_legacy(
self,
req: Any,
ctx: RequestContext[Any],
Expand All @@ -68,18 +68,14 @@ def build_creative(
Returns a bare :class:`CreativeManifest` — the framework's
projection layer wraps it into the wire envelope. The brief
message is on ``req.message``; the requested format is on
``req.target_format_id`` (or ``req.target_format_ids`` for
multi-format builds).
``req.creative_manifest.format_kind`` for canonical builds.
"""
# Real adopters call their generation API here; this stub
# synthesizes a placeholder URL for the example.
creative_id = f"cr-{uuid.uuid4().hex[:12]}"
return CreativeManifest(
creative_id=creative_id,
format_id=FormatReferenceStructuredObject(
agent_url="https://creative.adcontextprotocol.org/",
id="audio_30s",
),
format_kind="audio_hosted",
assets={
# Note: ``AudioContent`` (not ``AudioAsset``) — 4.0
# renamed payload-describing types to ``*Content`` so
Expand Down
4 changes: 3 additions & 1 deletion examples/multi_platform_seller/src/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,9 @@ def build_router() -> PlatformRouter:
idempotency=IdempotencyUnsupported(supported=False),
),
account=CapabilitiesAccount(supported_billing=["operator"]),
media_buy=MediaBuy(supported_pricing_models=["cpm"]),
media_buy=MediaBuy(
supported_pricing_models=["cpm"],
),
supported_protocols=[SupportedProtocol.media_buy],
)

Expand Down
4 changes: 3 additions & 1 deletion examples/multi_platform_seller/src/mock_guaranteed.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,9 @@ class MockGuaranteedPlatform(DecisioningPlatform, SalesPlatform):
idempotency=IdempotencyUnsupported(supported=False),
),
account=CapabilitiesAccount(supported_billing=["operator"]),
media_buy=MediaBuy(supported_pricing_models=["cpm"]),
media_buy=MediaBuy(
supported_pricing_models=["cpm"],
),
supported_protocols=[SupportedProtocol.media_buy],
)

Expand Down
4 changes: 3 additions & 1 deletion examples/multi_platform_seller/src/mock_non_guaranteed.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,9 @@ class MockNonGuaranteedPlatform(DecisioningPlatform, SalesPlatform):
idempotency=IdempotencyUnsupported(supported=False),
),
account=CapabilitiesAccount(supported_billing=["operator"]),
media_buy=MediaBuy(supported_pricing_models=["cpm"]),
media_buy=MediaBuy(
supported_pricing_models=["cpm"],
),
supported_protocols=[SupportedProtocol.media_buy],
)

Expand Down
Loading
Loading