Skip to content

Commit dece72e

Browse files
authored
feat(protocol): support AdCP 3.2 beta lifecycle (#1034)
* feat(protocol): support AdCP 3.2 beta lifecycle * perf(server): compile schemas for advertised tools only * fix(protocol): preserve adapter subclass compatibility
1 parent 75152b0 commit dece72e

1,893 files changed

Lines changed: 1916780 additions & 4926 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AGENTS.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@ Override these in your `ADCPHandler` subclass. Unimplemented methods return `not
1212
|---|---|---|---|
1313
| `get_adcp_capabilities` | protocol | GetAdcpCapabilitiesRequest | Declare supported domains/features |
1414
| `get_products` | media_buy | GetProductsRequest | Return ad products matching a brief |
15+
| `list_products` | media_buy | ListProductsRequest | List products with the compact lifecycle |
16+
| `request_proposals` | media_buy | RequestProposalsRequest | Request seller proposals |
17+
| `refine_proposals` | media_buy | RefineProposalsRequest | Refine seller proposals |
18+
| `decline_proposals` | media_buy | DeclineProposalsRequest | Decline seller proposals |
19+
| `buy_products` | media_buy | BuyProductsRequest | Commit a direct product purchase |
20+
| `accept_proposal` | media_buy | AcceptProposalRequest | Accept a proposal and create its media buy |
21+
| `control_media_buy` | media_buy | ControlMediaBuyRequest | Control an existing media buy |
1522
| `list_creative_formats` | media_buy | ListCreativeFormatsRequest | List available creative formats |
1623
| `create_media_buy` | media_buy | CreateMediaBuyRequest | Create a new media buy |
1724
| `update_media_buy` | media_buy | UpdateMediaBuyRequest | Update an existing media buy |

MANIFEST.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ recursive-include src/adcp py.typed
1010
recursive-include src/adcp/_schemas/2.5 *.json
1111
recursive-include src/adcp/_schemas/3.0 *.json
1212
recursive-include src/adcp/_schemas/3.1 *.json
13+
recursive-include src/adcp/_schemas/3.2.0-beta.0 *.json
1314
prune src/adcp/_schemas/3.1.0-*

MIGRATION_ADCP_3.1_TO_3.2.md

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
# Migrating an integration from AdCP 3.1 to 3.2 beta
2+
3+
Python SDK 8 beta supports the AdCP `3.2.0-beta.0` schemas and the compact
4+
product/media-buy lifecycle that becomes the foundation of AdCP 4.0. The SDK
5+
continues to support AdCP 3.0 and 3.1, and the deprecated
6+
`get_products`/`create_media_buy`/`update_media_buy` lifecycle remains available
7+
throughout AdCP 3.x.
8+
9+
Protocol version and lifecycle shape are separate compatibility axes. A 3.2
10+
seller may expose only the old lifecycle, a direct-buy subset of the compact
11+
lifecycle, or the complete proposal lifecycle. Do not infer supported tools
12+
from the version alone; read `media_buy.lifecycle_tools` or MCP `tools/list`.
13+
14+
## Pin the beta precisely
15+
16+
Use the release-precision prerelease identifier while 3.2 is in beta:
17+
18+
```python
19+
client = ADCPClient(agent, adcp_version="3.2-beta.0")
20+
server = adcp_server("seller", adcp_version="3.2-beta.0")
21+
```
22+
23+
`"3.2"` intentionally does not alias to a prerelease. Exact prerelease pins
24+
prevent a deployment from silently changing contracts when 3.2 stable ships.
25+
26+
## Choose the lifecycle subset
27+
28+
| Workflow | Compact tools |
29+
|---|---|
30+
| Product feed/read | `list_products` |
31+
| Direct buy | `list_products`, `buy_products`, `control_media_buy` |
32+
| Proposal buy | `request_proposals`, `refine_proposals`, `decline_proposals`, `accept_proposal` |
33+
34+
Sellers can combine these subsets. Declare exactly what is implemented:
35+
36+
```python
37+
from adcp.decisioning import DecisioningCapabilities, DecisioningPlatform
38+
from adcp.decisioning.capabilities import LifecycleTool, MediaBuy
39+
40+
class Seller(DecisioningPlatform):
41+
capabilities = DecisioningCapabilities(
42+
specialisms=["sales-non-guaranteed"],
43+
media_buy=MediaBuy(
44+
lifecycle_tools=[
45+
LifecycleTool.list_products,
46+
LifecycleTool.buy_products,
47+
LifecycleTool.control_media_buy,
48+
]
49+
),
50+
)
51+
52+
def list_products(self, req, ctx): ...
53+
def buy_products(self, req, ctx): ...
54+
def control_media_buy(self, req, ctx): ...
55+
56+
# Keep the 3.x compatibility facades while older buyers migrate.
57+
def get_products(self, req, ctx): ...
58+
def create_media_buy(self, req, ctx): ...
59+
def update_media_buy(self, media_buy_id, patch, ctx): ...
60+
```
61+
62+
Decisioning server startup fails if `lifecycle_tools` claims a method the
63+
platform does not implement. Tools omitted from the declaration are not
64+
advertised.
65+
66+
Class-based `ADCPHandler` servers and decorator servers use the same names:
67+
68+
```python
69+
class Handler(ADCPHandler):
70+
async def list_products(self, params, context=None): ...
71+
async def request_proposals(self, params, context=None): ...
72+
async def refine_proposals(self, params, context=None): ...
73+
async def decline_proposals(self, params, context=None): ...
74+
async def buy_products(self, params, context=None): ...
75+
async def accept_proposal(self, params, context=None): ...
76+
async def control_media_buy(self, params, context=None): ...
77+
```
78+
79+
## Update buyer calls
80+
81+
Request and response models are public from `adcp`, `adcp.types`,
82+
`adcp.types.buyer`, and `adcp.types.media_buy`:
83+
84+
```python
85+
from adcp import ListProductsRequest, BuyProductsRequest
86+
87+
products = await client.list_products(ListProductsRequest(...))
88+
purchase = await client.buy_products(BuyProductsRequest(...))
89+
```
90+
91+
The same methods are available on `ADCPMultiAgentClient`. Do not import from
92+
`adcp.types._generated` or `adcp.types.generated_poc`.
93+
94+
Each stateful compact task has its own idempotency identity. Retry with the
95+
same tool name and idempotency key; never retry `buy_products` as
96+
`create_media_buy`, or `accept_proposal` as another operation. Compact buy and
97+
control requests do not accept inline creatives—use the dedicated creative
98+
lifecycle.
99+
100+
## Select the request-signing profile
101+
102+
AdCP 3.2 tightens RFC 9421 handling: `Signature` Structured Fields binary
103+
values use standard padded Base64, and every signed body-bearing request covers
104+
`content-digest`. The SDK signer defaults to the 3.2 wire format. Select a
105+
legacy profile only when negotiating with a 3.0/3.1 peer:
106+
107+
```python
108+
from adcp.signing import SigningConfig, VerifyOptions
109+
110+
legacy_buyer = SigningConfig(
111+
private_key=key,
112+
key_id="buyer-key",
113+
signing_profile_version="3.1",
114+
)
115+
116+
strict_3_2_verifier = VerifyOptions(
117+
...,
118+
signing_profile_version="3.2",
119+
)
120+
```
121+
122+
Choose the verifier profile from trusted endpoint configuration and negotiated
123+
capabilities, never from an unsigned request-body field. The default verifier
124+
profile remains 3.1-compatible so existing deployments do not begin rejecting
125+
legacy signatures until they explicitly complete negotiation.
126+
127+
## Test the two-dimensional matrix
128+
129+
At minimum, exercise these rows independently:
130+
131+
| Wire version | Lifecycle variant | Expected surface |
132+
|---|---|---|
133+
| 3.0 | legacy | `get_products`, `create_media_buy`, `update_media_buy` |
134+
| 3.1 | legacy | same legacy facade with canonical creative negotiation |
135+
| 3.2 beta | legacy | compatibility facade still works |
136+
| 3.2 beta | direct | list → buy → control |
137+
| 3.2 beta | proposal | request → refine/decline → accept → control |
138+
139+
Keep legacy and compact tests against the same business implementation where
140+
possible. This catches accidental divergence between compatibility facades and
141+
the new task-specific contracts.

MIGRATION_v7_to_v8.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Migrating from Python SDK 7 to 8
22

3+
SDK 8 beta also updates the generated protocol surface from AdCP 3.1.15 to
4+
AdCP 3.2.0-beta.0 and adds the compact product/media-buy lifecycle. The old
5+
3.x lifecycle remains supported. See
6+
[Migrating an integration from AdCP 3.1 to 3.2 beta](MIGRATION_ADCP_3.1_TO_3.2.md)
7+
for lifecycle selection, capability declarations, and the compatibility test
8+
matrix.
9+
310
SDK 8 makes the legacy `ADCPClient.handle_webhook()` convenience path fail
411
closed. Calls without a configured `webhook_secret` no longer accept unsigned
512
MCP callbacks.

README.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -276,16 +276,16 @@ async with ADCPMultiAgentClient(
276276

277277
## AdCP version support
278278

279-
The SDK 8 beta line is built against **AdCP 3.1.15 stable**, makes canonical
280-
creatives the primary Python contract, and negotiates AdCP 3.0, 3.1, and 3.2
281-
wire dialects. The SDK package version and protocol version are intentionally
282-
independent; AdCP 3.2 beta support will land separately:
279+
The SDK 8 beta line is built against **AdCP 3.2.0-beta.0**, makes canonical
280+
creatives the primary Python contract, and negotiates AdCP 3.0, 3.1, and the
281+
exact 3.2 beta wire dialect. The SDK package version and protocol version are
282+
intentionally independent:
283283

284284
```python
285285
import adcp
286286

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

291291
If you talk to an agent on a newer spec than this SDK validates, the response
@@ -300,6 +300,7 @@ forward traffic degrades gracefully rather than failing.
300300
- **[Handler authoring](docs/handler-authoring.md)** - Building an AdCP-compliant agent on `adcp.server`
301301
- **[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
302302
- **[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
303+
- **[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
303304
- **[Testing your AdCP server](docs/testing-your-adcp-server.md)** - In-process harness for unit tests plus storyboard-runner compliance grading
304305
- **[Multi-tenant contract](docs/multi-tenant-contract.md)** - Scope invariants every multi-tenant agent must satisfy
305306
- **[Examples](examples/)** - Code examples and usage patterns

SCHEMA_DELTAS.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
11
# Generated-types delta
22

3-
_No field-shape changes detected._
3+
## Field changes
4+
5+
- `media_buy/create_media_buy_response.py`
6+
- `CreateMediaBuyResponse1`: `+status`
7+
- `media_buy/update_media_buy_response.py`
8+
- `UpdateMediaBuyResponse1`: `+status`

pyproject.toml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ adcp = [
184184
"_schemas/2.5/**/*.json",
185185
"_schemas/3.0/**/*.json",
186186
"_schemas/3.1/**/*.json",
187+
"_schemas/3.2.0-beta.0/**/*.json",
187188
# Vendored canonical-formats reference fixtures (14 v2 products +
188189
# 50-entry v1 catalog) so :mod:`adcp.canonical_formats.fixtures`
189190
# can serve them to adopter test suites without forcing each
@@ -249,7 +250,11 @@ disable_error_code = ["import-not-found", "no-untyped-def", "var-annotated", "op
249250

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

254259
[[tool.mypy.overrides]]
255260
module = "tests.type_checks.*"
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"title": "A2UI Bound Value",
4+
"description": "A value that can be a literal or bound to a path in the data model",
5+
"oneOf": [
6+
{
7+
"type": "object",
8+
"description": "Literal string value",
9+
"properties": {
10+
"literalString": {
11+
"type": "string",
12+
"description": "Static string value"
13+
}
14+
},
15+
"required": [
16+
"literalString"
17+
],
18+
"additionalProperties": false
19+
},
20+
{
21+
"type": "object",
22+
"description": "Literal number value",
23+
"properties": {
24+
"literalNumber": {
25+
"type": "number",
26+
"description": "Static number value"
27+
}
28+
},
29+
"required": [
30+
"literalNumber"
31+
],
32+
"additionalProperties": false
33+
},
34+
{
35+
"type": "object",
36+
"description": "Literal boolean value",
37+
"properties": {
38+
"literalBoolean": {
39+
"type": "boolean",
40+
"description": "Static boolean value"
41+
}
42+
},
43+
"required": [
44+
"literalBoolean"
45+
],
46+
"additionalProperties": false
47+
},
48+
{
49+
"type": "object",
50+
"description": "Path to data model value",
51+
"properties": {
52+
"path": {
53+
"type": "string",
54+
"description": "JSON pointer path to value in data model (e.g., '/products/0/title')"
55+
}
56+
},
57+
"required": [
58+
"path"
59+
],
60+
"additionalProperties": false
61+
},
62+
{
63+
"type": "object",
64+
"description": "Literal with path binding (sets default and binds)",
65+
"properties": {
66+
"literalString": {
67+
"type": "string"
68+
},
69+
"path": {
70+
"type": "string"
71+
}
72+
},
73+
"required": [
74+
"literalString",
75+
"path"
76+
],
77+
"additionalProperties": false
78+
}
79+
]
80+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"title": "A2UI Component",
4+
"description": "A component in an A2UI surface",
5+
"type": "object",
6+
"properties": {
7+
"id": {
8+
"type": "string",
9+
"description": "Unique identifier for this component within the surface"
10+
},
11+
"parentId": {
12+
"type": "string",
13+
"description": "ID of the parent component (null for root)"
14+
},
15+
"component": {
16+
"type": "object",
17+
"description": "Component definition (keyed by component type)",
18+
"minProperties": 1,
19+
"maxProperties": 1,
20+
"additionalProperties": {
21+
"type": "object",
22+
"description": "Component properties"
23+
}
24+
}
25+
},
26+
"required": [
27+
"id",
28+
"component"
29+
],
30+
"additionalProperties": true
31+
}

0 commit comments

Comments
 (0)