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: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ Override these in your `ADCPHandler` subclass. Unimplemented methods return `not
| `sync_principal` | protocol | SyncPrincipalRequest | Synchronize authenticated principal configuration |
| `get_products` | media_buy | GetProductsRequest | Return ad products matching a brief |
| `list_products` | media_buy | ListProductsRequest | List products with the compact lifecycle |
| `list_collections` | media_buy | ListCollectionsRequest | List collection cards (AdCP 3.3, experimental) |
| `list_proposals` | media_buy | ListProposalsRequest | List open opportunities and proposals (AdCP 3.3, experimental) |
| `list_collections` | media_buy | ListCollectionsRequest | List collection cards (AdCP 3.3, experimental `media_buy.collection_catalog`) |
| `list_proposals` | media_buy | ListProposalsRequest | List proposal snapshots and their dispositions (AdCP 3.3, experimental `media_buy.open_opportunities`) |
| `request_proposals` | media_buy | RequestProposalsRequest | Request seller proposals |
| `refine_proposals` | media_buy | RefineProposalsRequest | Refine seller proposals |
| `decline_proposals` | media_buy | DeclineProposalsRequest | Decline seller proposals |
Expand Down
3 changes: 3 additions & 0 deletions MIGRATION_v9_to_v10.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ the SDK-relevant inventory is [docs/adcp-3.3-sdk-inventory.md](docs/adcp-3.3-sdk
| AdCP 3.3.0-beta.2 adds separate-account outcomes, seller offers and measurement provenance | `RequestAccountRequest.allow_separate_account` (default `false`) lets a change request resolve as `created` (a new account carrying the change) or `matched` (an account the caller already holds), and the new correctable error code `ACCOUNT_CHANGE_REQUIRES_NEW_ACCOUNT` reports a change the seller would take only on a new account. Experimental seller offers add `proposal_initiator`, `offer_reason` on `list_proposals` rows and the `proposal.offered` notification (`ProposalOfferedWebhook`); experimental measurement provenance adds `CollectionMechanism`, `counting_mechanisms` and `vendor_metric_provenance[]` (`VendorMetricProvenance`). The reporting-subscription fields settled after beta.1 (`standard_dimensions`, `unmapped_dimensions`, `inactive_intervals`, `prior_versions[].configuration`, per-offering `offering_id`, `previous_timezone`) are now generated. `principal_kind` and `expected_principal_kind` are deprecated. `NotificationConfig.product_payload_view` loses its schema default: it is `None` when unset, and a product subscription's dump no longer adds `product_payload_view: "legacy"` (absent means legacy); read `config.product_payload_view or "legacy"`. These are generated types, imported from their domain packages (for example `adcp.types.domains.core.ProposalOfferedWebhook`); no task was added. |
| AdCP 3.3.0-beta.3 composes the protocol envelope on every task response | adcontextprotocol/adcp#8547: every response root composes `core/protocol-envelope.json`, which composes `core/version-envelope.json`, so generated responses inherit `ProtocolEnvelope` alone (no separate `AdcpVersionEnvelope` base) and the local copies of envelope fields are gone. Submitted, working and input-required bodies compose `core/task-submitted.json` / `task-working.json` / `task-input-required.json`: a submitted arm still pins `status` to `submitted` and requires `task_id`. `task_id` (non-empty) and the task-level `message` (at most 2,000 characters) each have one definition (`core/task-id.json`, `core/task-message.json`); fields that refer to them stay typed `str` with those constraints, so no caller has to wrap a value. `TaskId`, `TaskMessage`, `TaskSubmitted`, `TaskWorking` and `TaskInputRequired` are generated. Consequences for handlers: `list_products` and `decline_proposals` now carry `status` (the server stamps `completed`; the 3.3 → 3.2 conversion drops it for 3.2 callers, as a recorded loss, and the 3.2 → 3.3 conversion supplies it). Whether a release carries `status` on those two tools is read from that release's own response schema (`uses_task_status_envelope(tool, version)` asks whether it composes `core/protocol-envelope.json`), not from its version string, so a 3.2.x release that later composes the envelope needs no SDK change; under a release that does not, the typed model's defaulted `completed` is not sent (a closed 3.2 `list_products` root refuses it) and nothing is stripped for any other tool; an absent `replayed` reads as `false` and `replayed: false` is valid; and on governance responses `status` is the task status, so a `check_governance` result names its decision in `verdict` and `report_plan_outcome` in `outcome_state`, never in `status`. `create_mcp_webhook_payload` gives a `result` without `status` the webhook's task status, and replaces a model result's defaulted envelope status with it, so a failed task's result never reads `completed`. |
| AdCP 3.3.0-beta.3 adds live creative updates, account-request money terms and reporting-subscription fields | Experimental `creative.live_updates` (`LiveUpdatePolicy`, `CreativeStagedUpdate`, `creative.staged_update_changed` and `CreativeStagedUpdateChangedWebhook`, error code `CREATIVE_LIVE_UPDATE_UNSUPPORTED` with `CreativeLiveUpdateUnsupportedDetails`). `request_account` outcomes carry `account` money terms on `matched` / `created`, `effective_at` on an `updated` money change, and `not_carried_over`. Reporting subscriptions gain `destination_overrides[]`, the paged readback (`prior_versions_count`, `accounts_with_exceptions_count`, `accounts_unauthorized_count`, `get_principal` `reporting_subscription_page`), `authorization.regranted_at`, `flight_history[]` and `cancellation_effective_at` on media buys, `available_from` / `withdrawn_at` on offerings and `stub` on billing obligations. `comply_test_controller` adds the `reporting_subscription_probe` scenario: override `TestControllerStore.reporting_subscription_probe` to serve it. Generated renames: `PriorVersion` is `ReportingSubscriptionPriorVersion`, `AccountsWithException` is `ReportingSubscriptionAccountExceptions`, and `AccountsUnauthorizedItem` is `ReportingSubscriptionUnauthorizedAccount`. |
| `list_proposals` and `list_collections` are served from declarations | The experimental 3.3 reads were already on `ADCPClient`, `ADCPHandler`, the adapters and the CLI. `PlatformHandler` now serves them: implement `list_proposals` (`OpenOpportunitiesPlatform`; declare it in `media_buy.lifecycle_tools` or `media_buy.open_opportunities.list_proposals`) or `list_collections` (`CollectionCatalogPlatform`; declare `media_buy.collection_catalog` and `media_buy.collection_cards` in `experimental_features`). A declaration with no method behind it now fails `validate_platform` at boot, and a method with no declaration is not advertised. `adcp.decisioning.capabilities` exports `OpenOpportunities` and `LaterProposals`. |
| A task the caller's release does not define fails with `UNSUPPORTED_FEATURE` | A server asked for a task its caller's negotiated release lacks (`list_proposals` or `list_collections` from a 3.2 buyer, `list_products` from a 3.0 buyer) used to answer `VERSION_UNSUPPORTED`; it now answers `UNSUPPORTED_FEATURE` with `details: {"task", "adcp_version"}`, as the spec requires for `list_collections`. The caller's version is supported, so re-pinning is not the fix. Buyers matching on `VERSION_UNSUPPORTED` for this case should match `UNSUPPORTED_FEATURE`. The SDK client already refused such a call before transport with `UNSUPPORTED_FEATURE`. |
| Agent-level notification configs classify the 3.3 account-anchored types | `sync_agent_notification_configs` / `sync_principal` now require `all_authorized_accounts: true` for `proposal.created`, `proposal.offered`, `collection_card.updated`, `collection_card.removed`, `collection_catalog.bulk_change` and `creative.staged_update_changed`, like the other account-anchored types; a registration without it is rejected. `WebhookSender.prepare_proposal_created` / `send_proposal_created` build the schema-validated `proposal.created` payload. |
| The reporting-subscription surface is served from 3.3.0-beta.3 on, unprojected | The beta.1 / beta.2 field projections are gone, with the preview module that patched the fields onto the models: `project_offering`, `project_reporting_delivery`, `project_subscription_state`, `project_subscription_coverage` and `serves_settled_subscription_fields` are removed from `adcp.reporting.subscriptions`, and `ReportingSubscriptionEngine.subscription_coverage()` no longer takes `adcp_version`. One predicate, `adcp.reporting.subscriptions.serves_subscriptions(adcp_version)` (from `SUBSCRIPTIONS_MIN_ADCP_VERSION`, `3.3.0-beta.3`), gates the whole surface: capabilities, `get_reporting_status` coverage (including `status: "inactive"` with `deactivated_at`) and the principal readback. A caller served an earlier release gets none of it. The extensible `reporting-dimension` vocabulary accepts unknown values on selectors, offerings and the capability block. |
| Reporting-subscription registration follows the 3.3.0-beta.3 rules | A caller may hold several pacing or analytics subscriptions per account and purpose (the lowest `subscription_id` owes a shared offering; the others record a package-level `overridden` exception), while billing stays one per account at a scope level and must use `match: exact` with `dimensions`. Membership edits (`scope.account_ids`, `exclude_account_ids`) no longer need a new `subscription_version`, and `client.reporting.subscribe` no longer bumps the version for them. `destination_overrides` must name existing destinations. The readback lists 16 prior versions and 1,000 accounts inline with `*_count` totals; page the rest with `get_principal` `reporting_subscription_page`. A partial grant refuses an unlisted reporting task with `SCOPE_INSUFFICIENT`. `AccountReady` gains `resolution`, `billing`, `billing_entity`, `payment_terms`, `effective_at` and `not_carried_over`. |
| A server that restricts `supported_versions` runs on its newest listed release | Any server (`create_mcp_server`, `create_a2a_server`, `serve`, `MCPToolSet`, `ADCPAgentExecutor`, `create_tool_caller`, `PlatformHandler`, `create_adcp_server_from_platform`) with `supported_versions=["3.2"]` and no `adcp_version` runs its handlers on 3.2, instead of raising because the SDK default is outside the list. Pass `adcp_version` explicitly to choose another listed release. |
Expand Down
6 changes: 3 additions & 3 deletions docs/adcp-3.3-sdk-inventory.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,10 @@ it adopted the bundle, and what is left to the codegen/wire-fidelity (2) and

| Task | Schemas | Notes | SDK status |
| --- | --- | --- | --- |
| `list_collections` | `media-buy/list-collections-request.json`, `-response.json` | Experimental collection cards: filters (`platform`, `genres_include`, `talent`), `cache_scope`, `outcome`. Lifecycle tool under `media_buy.collection_lists`. | types generated; client method, server tool registration, A2A skill, MCP tool list: (3), #1645 |
| `list_proposals` | `media-buy/list-proposals-request.json`, `-response.json` | Experimental `media_buy.open_opportunities`: `Opportunity`, `Proposal`, `Origin`, `CloseReason`, `Status`; filters by `media_buy_id`, `opportunity_id`, `proposal_id`. Added to `LifecycleTool` and to `account-change.json` `Task`. | types generated; client/server surface: (3), #1645 |
| `list_collections` | `media-buy/list-collections-request.json`, `-response.json` | Experimental collection cards (`media_buy.collection_catalog`, which requires `media_buy.collection_cards`): selectors, filters (`kinds`, `genres_include`, `talent`, ...), `if_collections_version`, `cache_scope`, `outcome` `listed` / `unchanged`. `account` optional; never provisions; a seller MUST refuse a caller below 3.3 with `UNSUPPORTED_FEATURE`. | done (#1645): client, multi-agent client, CLI, MCP/A2A adapters and tool list; `CollectionCatalogPlatform` hook on `PlatformHandler`, advertised and boot-validated from `experimental_features`; the dispatcher refuses a caller whose release lacks the task with `UNSUPPORTED_FEATURE` |
| `list_proposals` | `media-buy/list-proposals-request.json`, `-response.json` | Experimental `media_buy.open_opportunities`: snapshots beside `disposition`, `origin`, `later_reason` / `offer_reason`, plus `opportunities[]`; filters `opportunity_ids`, `media_buy_ids`, `proposal_ids`, `dispositions`. Added to `LifecycleTool` and to `account-change.json` `Task`. | done (#1645): client surface as above; `OpenOpportunitiesPlatform` hook, advertised and boot-validated from `lifecycle_tools` or `open_opportunities.list_proposals`. Serving it from a `ProposalStore` is deferred: the store keeps legacy `get_products` proposals, not canonical snapshots with dispositions, and has no account listing |
| `get_property_list` over MCP | existing schema | Property list references resolve through AdCP tasks at `agent_url`, `auth_token` as Bearer, every page fetched and validated before a cached snapshot is replaced. | done: `property_lists` resolves over MCP with the list-scoped Bearer token and fetches every page before replacing a snapshot (verified by 3) |
| `proposal.created` webhook | `core/proposal-created-webhook.json`; `NotificationType` / `EventType` `proposal_created` | Seller-initiated proposals under `open_opportunities`. | types generated; webhook dispatch and `notification_configs` event validation: (3), #1645 |
| `proposal.created` webhook | `core/proposal-created-webhook.json`; `NotificationType` / `EventType` `proposal_created` | Seller-initiated proposals under `open_opportunities`. | done (#1645): `WebhookSender.prepare_proposal_created` / `send_proposal_created` (schema-validated, `notification_id` bound to `proposal_id`); agent-level registrations treat it, `proposal.offered`, `collection_card.*`, `collection_catalog.bulk_change` and `creative.staged_update_changed` as account-anchored |
| `refine_proposals` submitted response | `core/async_response_refs/media_buy/refine_proposals_async_response_submitted.json` | New async `submitted` envelope for refinement with `expected_confirmation_at`. | types generated; async response dispatch table: (3), #1640 |

## 3. Changed tasks and request/response fields
Expand Down
7 changes: 6 additions & 1 deletion docs/cross-version-conversion.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,12 @@ that answer against the application schema (under the configured response
mode) and converts it to the buyer's version: up (a 3.2 server answering a
`3.3-beta.3` buyer), unchanged (a same-release buyer), or down (a 3.1 or 3.0
buyer). A conversion the target schema refuses fails as a structured
`VERSION_UNSUPPORTED` error rather than reaching the peer invalid.
`VERSION_UNSUPPORTED` error rather than reaching the peer invalid. A task the
buyer's release does not define at all (`list_proposals` or `list_collections`
from a 3.2 buyer) fails before the handler as `UNSUPPORTED_FEATURE` with
`details: {"task", "adcp_version"}`: the buyer's version is supported, the task
is not. `PlatformHandler` applies the same refusal to those two 3.3 reads when
the handler itself runs on an older release.

So a production seller can keep its handler code on the stable 3.2 contract
while the SDK's default is a pre-release:
Expand Down
22 changes: 22 additions & 0 deletions docs/proposal-surfaces.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,28 @@ Framework-managed `TaskHandoff` completions pass through the same validator.
the registry directly would bypass response verification; use `TaskHandoff` or
validate in an adopter-owned registry wrapper before exposing a custom flow.

## Reading proposals back (`list_proposals`, AdCP 3.3)

`list_proposals` is the experimental `media_buy.open_opportunities` read: each
snapshot byte-for-byte as issued, beside its seller-tracked `disposition`
(`available`, `accepted`, `declined`, `expired`, `withdrawn`), plus the state of
the opportunities those proposals belong to. It is synchronous and never mutates
state, and it is the repair read for the `proposal.created` notification.

On a `DecisioningPlatform`, implement `list_proposals(req, ctx)`
(`OpenOpportunitiesPlatform`) and declare it with `media_buy.lifecycle_tools`
containing `list_proposals` or
`media_buy=MediaBuy(open_opportunities=OpenOpportunities(list_proposals=True))`,
and list `media_buy.open_opportunities` in `experimental_features`. The framework
advertises the tool only for that declaration, fails boot when the declaration
has no method behind it, and refuses a caller below 3.3 with
`UNSUPPORTED_FEATURE`. Announce a later proposal with
`WebhookSender.prepare_proposal_created(...)` once it is readable.

The framework does not yet serve `list_proposals` from a `ProposalStore`: the
store keeps the `get_products` proposal payloads it hydrates recipes from, not
canonical snapshots with dispositions, and it cannot list by account.

## Also see

- `proposals/product-architecture.md` — design rationale for the
Expand Down
3 changes: 3 additions & 0 deletions src/adcp/_notification_envelope.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"account.change_recorded": "core/account-change-recorded-webhook.json",
"capabilities.changed": "core/capabilities-changed-webhook.json",
"principal.changed": "core/principal-changed-webhook.json",
"proposal.created": "core/proposal-created-webhook.json",
"creative.status_changed": "creative/creative-status-changed-webhook.json",
"creative.assignment_changed": "creative/creative-assignment-changed-webhook.json",
"creative.purged": "creative/creative-purged-webhook.json",
Expand Down Expand Up @@ -90,6 +91,8 @@ def validate_notification_body(
error = next(validator.iter_errors(schema_payload), None)
if error is not None:
raise ValueError(f"notification envelope failed schema validation ({error.validator})")
if kind == "proposal.created" and payload["notification_id"] != payload["proposal_id"]:
raise ValueError("notification_id must equal proposal_id")
if kind.startswith(("product.", "signal.")) or kind == "wholesale_feed.bulk_change":
event = payload["event"]
if payload["notification_id"] != event["event_id"]:
Expand Down
15 changes: 12 additions & 3 deletions src/adcp/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -2614,7 +2614,11 @@ async def list_collections(
account_policy: AccountPolicy | None = None,
options: TaskOptions | None = None,
) -> TaskResult[ListCollectionsResponse]:
"""List collection cards (AdCP 3.3, experimental ``media_buy.collection_lists``)."""
"""List collection cards (AdCP 3.3, experimental ``media_buy.collection_catalog``).

A peer negotiated below 3.3 has no such task: the call fails with
``UNSUPPORTED_FEATURE`` before anything is sent.
"""
return cast(
TaskResult[ListCollectionsResponse],
await self._execute_typed_task("list_collections", request, ListCollectionsResponse),
Expand All @@ -2628,7 +2632,12 @@ async def list_proposals(
account_policy: AccountPolicy | None = None,
options: TaskOptions | None = None,
) -> TaskResult[ListProposalsResponse]:
"""List open opportunities and proposals (AdCP 3.3, experimental)."""
"""List proposal snapshots and their dispositions (AdCP 3.3, experimental).

Requires a seller advertising ``media_buy.open_opportunities``. A peer
negotiated below 3.3 has no such task: the call fails with
``UNSUPPORTED_FEATURE`` before anything is sent.
"""
return cast(
TaskResult[ListProposalsResponse],
await self._execute_typed_task("list_proposals", request, ListProposalsResponse),
Expand Down Expand Up @@ -7484,7 +7493,7 @@ async def list_collections(
async def list_proposals(
self, request: ListProposalsRequest
) -> list[TaskResult[ListProposalsResponse]]:
"""List open opportunities and proposals across all agents."""
"""List proposal snapshots and their dispositions across all agents."""

return await _gather_agent_results(
*(agent.list_proposals(request) for agent in self.agents.values())
Expand Down
Loading
Loading