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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
7 changes: 7 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ Override these in your `ADCPHandler` subclass. Unimplemented methods return `not
|---|---|---|---|
| `get_adcp_capabilities` | protocol | GetAdcpCapabilitiesRequest | Declare supported domains/features |
| `get_products` | media_buy | GetProductsRequest | Return ad products matching a brief |
| `list_products` | media_buy | ListProductsRequest | List products with the compact lifecycle |
| `request_proposals` | media_buy | RequestProposalsRequest | Request seller proposals |
| `refine_proposals` | media_buy | RefineProposalsRequest | Refine seller proposals |
| `decline_proposals` | media_buy | DeclineProposalsRequest | Decline seller proposals |
| `buy_products` | media_buy | BuyProductsRequest | Commit a direct product purchase |
| `accept_proposal` | media_buy | AcceptProposalRequest | Accept a proposal and create its media buy |
| `control_media_buy` | media_buy | ControlMediaBuyRequest | Control an existing media buy |
| `list_creative_formats` | media_buy | ListCreativeFormatsRequest | List available creative formats |
| `create_media_buy` | media_buy | CreateMediaBuyRequest | Create a new media buy |
| `update_media_buy` | media_buy | UpdateMediaBuyRequest | Update an existing media buy |
Expand Down
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ recursive-include src/adcp py.typed
recursive-include src/adcp/_schemas/2.5 *.json
recursive-include src/adcp/_schemas/3.0 *.json
recursive-include src/adcp/_schemas/3.1 *.json
recursive-include src/adcp/_schemas/3.2.0-beta.0 *.json
prune src/adcp/_schemas/3.1.0-*
141 changes: 141 additions & 0 deletions MIGRATION_ADCP_3.1_TO_3.2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
# Migrating an integration from AdCP 3.1 to 3.2 beta

Python SDK 8 beta supports the AdCP `3.2.0-beta.0` schemas and the compact
product/media-buy lifecycle that becomes the foundation of AdCP 4.0. The SDK
continues to support AdCP 3.0 and 3.1, and the deprecated
`get_products`/`create_media_buy`/`update_media_buy` lifecycle remains available
throughout AdCP 3.x.

Protocol version and lifecycle shape are separate compatibility axes. A 3.2
seller may expose only the old lifecycle, a direct-buy subset of the compact
lifecycle, or the complete proposal lifecycle. Do not infer supported tools
from the version alone; read `media_buy.lifecycle_tools` or MCP `tools/list`.

## Pin the beta precisely

Use the release-precision prerelease identifier while 3.2 is in beta:

```python
client = ADCPClient(agent, adcp_version="3.2-beta.0")
server = adcp_server("seller", adcp_version="3.2-beta.0")
```

`"3.2"` intentionally does not alias to a prerelease. Exact prerelease pins
prevent a deployment from silently changing contracts when 3.2 stable ships.

## Choose the lifecycle subset

| Workflow | Compact tools |
|---|---|
| Product feed/read | `list_products` |
| Direct buy | `list_products`, `buy_products`, `control_media_buy` |
| Proposal buy | `request_proposals`, `refine_proposals`, `decline_proposals`, `accept_proposal` |

Sellers can combine these subsets. Declare exactly what is implemented:

```python
from adcp.decisioning import DecisioningCapabilities, DecisioningPlatform
from adcp.decisioning.capabilities import LifecycleTool, MediaBuy

class Seller(DecisioningPlatform):
capabilities = DecisioningCapabilities(
specialisms=["sales-non-guaranteed"],
media_buy=MediaBuy(
lifecycle_tools=[
LifecycleTool.list_products,
LifecycleTool.buy_products,
LifecycleTool.control_media_buy,
]
),
)

def list_products(self, req, ctx): ...
def buy_products(self, req, ctx): ...
def control_media_buy(self, req, ctx): ...

# Keep the 3.x compatibility facades while older buyers migrate.
def get_products(self, req, ctx): ...
def create_media_buy(self, req, ctx): ...
def update_media_buy(self, media_buy_id, patch, ctx): ...
```

Decisioning server startup fails if `lifecycle_tools` claims a method the
platform does not implement. Tools omitted from the declaration are not
advertised.

Class-based `ADCPHandler` servers and decorator servers use the same names:

```python
class Handler(ADCPHandler):
async def list_products(self, params, context=None): ...
async def request_proposals(self, params, context=None): ...
async def refine_proposals(self, params, context=None): ...
async def decline_proposals(self, params, context=None): ...
async def buy_products(self, params, context=None): ...
async def accept_proposal(self, params, context=None): ...
async def control_media_buy(self, params, context=None): ...
```

## Update buyer calls

Request and response models are public from `adcp`, `adcp.types`,
`adcp.types.buyer`, and `adcp.types.media_buy`:

```python
from adcp import ListProductsRequest, BuyProductsRequest

products = await client.list_products(ListProductsRequest(...))
purchase = await client.buy_products(BuyProductsRequest(...))
```

The same methods are available on `ADCPMultiAgentClient`. Do not import from
`adcp.types._generated` or `adcp.types.generated_poc`.

Each stateful compact task has its own idempotency identity. Retry with the
same tool name and idempotency key; never retry `buy_products` as
`create_media_buy`, or `accept_proposal` as another operation. Compact buy and
control requests do not accept inline creatives—use the dedicated creative
lifecycle.

## Select the request-signing profile

AdCP 3.2 tightens RFC 9421 handling: `Signature` Structured Fields binary
values use standard padded Base64, and every signed body-bearing request covers
`content-digest`. The SDK signer defaults to the 3.2 wire format. Select a
legacy profile only when negotiating with a 3.0/3.1 peer:

```python
from adcp.signing import SigningConfig, VerifyOptions

legacy_buyer = SigningConfig(
private_key=key,
key_id="buyer-key",
signing_profile_version="3.1",
)

strict_3_2_verifier = VerifyOptions(
...,
signing_profile_version="3.2",
)
```

Choose the verifier profile from trusted endpoint configuration and negotiated
capabilities, never from an unsigned request-body field. The default verifier
profile remains 3.1-compatible so existing deployments do not begin rejecting
legacy signatures until they explicitly complete negotiation.

## Test the two-dimensional matrix

At minimum, exercise these rows independently:

| Wire version | Lifecycle variant | Expected surface |
|---|---|---|
| 3.0 | legacy | `get_products`, `create_media_buy`, `update_media_buy` |
| 3.1 | legacy | same legacy facade with canonical creative negotiation |
| 3.2 beta | legacy | compatibility facade still works |
| 3.2 beta | direct | list → buy → control |
| 3.2 beta | proposal | request → refine/decline → accept → control |

Keep legacy and compact tests against the same business implementation where
possible. This catches accidental divergence between compatibility facades and
the new task-specific contracts.
7 changes: 7 additions & 0 deletions MIGRATION_v7_to_v8.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Migrating from Python SDK 7 to 8

SDK 8 beta also updates the generated protocol surface from AdCP 3.1.15 to
AdCP 3.2.0-beta.0 and adds the compact product/media-buy lifecycle. The old
3.x lifecycle remains supported. See
[Migrating an integration from AdCP 3.1 to 3.2 beta](MIGRATION_ADCP_3.1_TO_3.2.md)
for lifecycle selection, capability declarations, and the compatibility test
matrix.

SDK 8 makes the legacy `ADCPClient.handle_webhook()` convenience path fail
closed. Calls without a configured `webhook_secret` no longer accept unsigned
MCP callbacks.
Expand Down
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -276,16 +276,16 @@ async with ADCPMultiAgentClient(

## AdCP version support

The SDK 8 beta line is built against **AdCP 3.1.15 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; AdCP 3.2 beta support will land separately:
The SDK 8 beta line is built against **AdCP 3.2.0-beta.0**, makes canonical
creatives the primary Python contract, and negotiates AdCP 3.0, 3.1, and the
exact 3.2 beta wire dialect. The SDK package version and protocol version are
intentionally independent:

```python
import adcp

adcp.get_adcp_sdk_version() # SDK package version, e.g. "8.0.0b1"
adcp.get_adcp_spec_version() # AdCP spec this build targets, e.g. "3.1.15"
adcp.get_adcp_spec_version() # AdCP spec this build targets, e.g. "3.2.0-beta.0"
```

If you talk to an agent on a newer spec than this SDK validates, the response
Expand All @@ -300,6 +300,7 @@ forward traffic degrades gracefully rather than failing.
- **[Handler authoring](docs/handler-authoring.md)** - Building an AdCP-compliant agent on `adcp.server`
- **[Migrating from SDK 6 to 7](https://github.com/adcontextprotocol/adcp-client-python/blob/main/MIGRATION_v6_to_v7.md)** - Breaking API, security, concurrency, and webhook changes
- **[Migrating from SDK 7 to 8](https://github.com/adcontextprotocol/adcp-client-python/blob/main/MIGRATION_v7_to_v8.md)** - Secure webhook defaults and telemetry changes
- **[Migrating from AdCP 3.1 to 3.2 beta](MIGRATION_ADCP_3.1_TO_3.2.md)** - Compact lifecycle adoption and old/new compatibility matrix
- **[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
7 changes: 6 additions & 1 deletion SCHEMA_DELTAS.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
# Generated-types delta

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

- `media_buy/create_media_buy_response.py`
- `CreateMediaBuyResponse1`: `+status`
- `media_buy/update_media_buy_response.py`
- `UpdateMediaBuyResponse1`: `+status`
7 changes: 6 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ adcp = [
"_schemas/2.5/**/*.json",
"_schemas/3.0/**/*.json",
"_schemas/3.1/**/*.json",
"_schemas/3.2.0-beta.0/**/*.json",
# Vendored canonical-formats reference fixtures (14 v2 products +
# 50-entry v1 catalog) so :mod:`adcp.canonical_formats.fixtures`
# can serve them to adopter test suites without forcing each
Expand Down Expand Up @@ -249,7 +250,11 @@ disable_error_code = ["import-not-found", "no-untyped-def", "var-annotated", "op

[[tool.mypy.overrides]]
module = "adcp.types.generated_poc.*"
disable_error_code = ["valid-type"]
# JSON Schema allOf/const intersections intentionally narrow inherited Pydantic
# fields in generated models. Mypy treats those schema-valid refinements as
# mutable Python attribute overrides; keep assignment checking enabled for the
# handwritten public surface while excluding this codegen implementation layer.
disable_error_code = ["valid-type", "assignment", "unused-ignore"]

[[tool.mypy.overrides]]
module = "tests.type_checks.*"
Expand Down
80 changes: 80 additions & 0 deletions schemas/cache/3.2.0-beta.0/a2ui/bound-value.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "A2UI Bound Value",
"description": "A value that can be a literal or bound to a path in the data model",
"oneOf": [
{
"type": "object",
"description": "Literal string value",
"properties": {
"literalString": {
"type": "string",
"description": "Static string value"
}
},
"required": [
"literalString"
],
"additionalProperties": false
},
{
"type": "object",
"description": "Literal number value",
"properties": {
"literalNumber": {
"type": "number",
"description": "Static number value"
}
},
"required": [
"literalNumber"
],
"additionalProperties": false
},
{
"type": "object",
"description": "Literal boolean value",
"properties": {
"literalBoolean": {
"type": "boolean",
"description": "Static boolean value"
}
},
"required": [
"literalBoolean"
],
"additionalProperties": false
},
{
"type": "object",
"description": "Path to data model value",
"properties": {
"path": {
"type": "string",
"description": "JSON pointer path to value in data model (e.g., '/products/0/title')"
}
},
"required": [
"path"
],
"additionalProperties": false
},
{
"type": "object",
"description": "Literal with path binding (sets default and binds)",
"properties": {
"literalString": {
"type": "string"
},
"path": {
"type": "string"
}
},
"required": [
"literalString",
"path"
],
"additionalProperties": false
}
]
}
31 changes: 31 additions & 0 deletions schemas/cache/3.2.0-beta.0/a2ui/component.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "A2UI Component",
"description": "A component in an A2UI surface",
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Unique identifier for this component within the surface"
},
"parentId": {
"type": "string",
"description": "ID of the parent component (null for root)"
},
"component": {
"type": "object",
"description": "Component definition (keyed by component type)",
"minProperties": 1,
"maxProperties": 1,
"additionalProperties": {
"type": "object",
"description": "Component properties"
}
}
},
"required": [
"id",
"component"
],
"additionalProperties": true
}
Loading
Loading