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
1 change: 1 addition & 0 deletions MIGRATION_v9_to_v10.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ the SDK-relevant inventory is [docs/adcp-3.3-sdk-inventory.md](docs/adcp-3.3-sdk
| 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 still drops it for 3.2 callers); 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`. |
| 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. |
| `server_version` pins keep wire precision | `ADCPClient(server_version=...)` and `get_server_version()` keep a 3.x pin at release precision: `"3.1.0-beta.1"` reads back as `"3.1-beta.1"` (SDK 9 reported the bundle key, `"3.1.0-beta.1"`). Stable pins still collapse (`"3.0.7"` → `"3.0"`). |
| A server pin governs its handlers | Handlers run on one *application version*: the server's `adcp_version` pin, else the newest release in its `supported_versions`, else the packaged `3.3-beta.3`. Every dispatch path (MCP, A2A, `create_tool_caller`, the Decisioning dispatcher) converts a buyer's request to that release before validation, unknown-field policy and typed parsing, and converts the handler's response back to the buyer's release: up to `3.3-beta.2`, unchanged for a same-release buyer, or down to 3.1 / 3.0. `resolve_requested_adcp_version(params)` inside a handler returns the application version; `ToolContext.application_adcp_version` reports it directly, and `ToolContext.resolved_adcp_version` still reports the buyer's negotiated version. A 3.2-pinned seller's dict handlers therefore see exactly 3.2 payloads, its requests and responses are validated against the 3.2 schemas (with strict validation, a response its own 3.2 contract refuses fails before conversion), and a conversion the target schema refuses fails as `ADCPVersionConversionError` / `VERSION_UNSUPPORTED` instead of reaching the peer invalid. Typed handler parameters (`params: GetProductsRequest`) and typed responses still use the generated 3.3 classes: they are additive supersets of 3.2, so a 3.2 payload parses into them unchanged and dumps back as the same 3.2 payload, but a field 3.3 added reads as unset (its model default) on a 3.2 server. The unpinned default is unchanged: handlers receive `3.3-beta.2`. |
Expand Down
4 changes: 2 additions & 2 deletions docs/adcp-3.3-sdk-inventory.md
Original file line number Diff line number Diff line change
Expand Up @@ -281,8 +281,8 @@ in it.
| Wire pin | `3.3.0-beta.3` / `3.3-beta.3`. `3.3-beta.2` is no longer advertised or accepted as a pin (as with `3.3-beta.0`, `3.3-beta.1` and the 3.2 candidates); its cache and compliance trees stay in the source tree and leave the wheel. | done |
| Protocol envelope composition (adcontextprotocol/adcp#8547) | Every task response and async status payload composes `core/protocol-envelope.json`, which composes `core/version-envelope.json`; submitted, working and input-required bodies compose `core/task-submitted.json` / `task-working.json` / `task-input-required.json`; `task_id` and the task-level `message` have one definition each (`core/task-id.json`, `core/task-message.json`). | generated (the #1741 codegen layout). The response-arm emitter folds the composed task-state schema into each arm, so submitted arms keep `status: submitted` and a required `task_id`; `get_task_status`'s required `task_id` / `status` take the envelope's declarations instead of `Any`; `task_id` / `message` references generate as constrained `str` |
| Live creative updates | Experimental `creative.live_updates`, `staged_update` on `list_creatives` / `sync_creatives`, the `creative.staged_update_changed` notification, `CREATIVE_LIVE_UPDATE_UNSUPPORTED`. | generated; no handler wiring (no new task) |
| Account requests | Money terms on `matched` / `created`, `effective_at` on `updated` money changes, `not_carried_over` on a separate account, consistent `currency` / `timezone` changeability. | generated; the `accounts.ensure` outcome mapping follows in the account slice |
| Reporting subscriptions (adcontextprotocol/adcp#8613, #8617, #8618, #8619, #8622) | `status: "inactive"` with `deactivated_at` on coverage; billing `stub` obligations and `cancellation_effective_at`; `flight_history[]`, `authorization.regranted_at`, `packages[].reporting_delivery_offering_ids`, offering `available_from` / `withdrawn_at`; sticky admission and per-package resolution, `destination_overrides[]`, the paged readback (`*_count`, `reporting_subscription_page`), in-place membership edits, several pacing / analytics feeds per purpose, `SCOPE_INSUFFICIENT` for partial grants, `account_id` in `primary_keys`; the day-start rule and the other period-arithmetic rulings. | generated. `_reporting_subscription_preview` and the beta.1 / beta.2 projections are removed; `serves_subscriptions` is the one version predicate. Engine conformance to the beta.3 rules and the re-pinned coverage vectors follow in the next slice |
| Account requests | Money terms on `matched` / `created`, `effective_at` on `updated` money changes, `not_carried_over` on a separate account, consistent `currency` / `timezone` changeability. | generated; `AccountReady` carries `resolution`, the money terms (`billing`, `billing_entity`, `payment_terms`), `effective_at` and `not_carried_over` |
| Reporting subscriptions (adcontextprotocol/adcp#8613, #8617, #8618, #8619, #8622) | `status: "inactive"` with `deactivated_at` on coverage; billing `stub` obligations and `cancellation_effective_at`; `flight_history[]`, `authorization.regranted_at`, `packages[].reporting_delivery_offering_ids`, offering `available_from` / `withdrawn_at`; sticky admission and per-package resolution, `destination_overrides[]`, the paged readback (`*_count`, `reporting_subscription_page`), in-place membership edits, several pacing / analytics feeds per purpose, `SCOPE_INSUFFICIENT` for partial grants, `account_id` in `primary_keys`; the day-start rule and the other period-arithmetic rulings. | generated. `_reporting_subscription_preview` and the beta.1 / beta.2 projections are removed; `serves_subscriptions` is the one version predicate. The coverage rule and the seller engine follow every listed rule, and the parity test runs the spec's 120 beta.3 vectors |
| Extensible `reporting-dimension` | The schema marks the vocabulary `x-extensible`; the generator closes it. | `adcp.types._forward_compat` reopens the five fields that carry it to the vocabulary's `x-pattern` |
| `comply_test_controller` `reporting_subscription_probe` | `revoke_grant`, `regrant`, `reduce_grant` (`allowed_tasks`), `set_account_timezone` (`timezone`), `advance_time` (`target_time`). | `TestControllerStore.reporting_subscription_probe` hook with parameter validation; not implemented by default |
| Generated renames | `PriorVersion` → `ReportingSubscriptionPriorVersion`, `AccountsWithException` → `ReportingSubscriptionAccountExceptions`, `AccountsUnauthorizedItem` → `ReportingSubscriptionUnauthorizedAccount`; numbered names renumber (`Status20` → `Status54`, ...). `adcp.types._generated.AllowedTask` now resolves to the controller's `allowed_tasks` item (both are task-name tokens); `adcp.types.domains.core.AllowedTask` is unchanged. | generated |
2 changes: 1 addition & 1 deletion docs/reporting-subscriptions-buyer.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ result = await client.reporting.subscribe(
result.action, result.subscription_version
```

- **Versioning.** The version is bumped only when the selector, scope, destination, `reconciliation_mode`, `exclude_account_ids` or `operator_domains` change.
- **Versioning.** The version is bumped only when the selector, the scope kind, the destination or its `destination_overrides`, `reconciliation_mode` or `operator_domains` change. Adding or removing `account_ids`, and changing `exclude_account_ids`, apply in place (3.3.0-beta.3): the seller records each edit per account as an `excluded_by_buyer` interval, so periods that started before a removal stay owed and an added account is owed only from when it joined.
- **Pre-dispatch checks.** These fail with `dispatched=False`:
- the scope must be explicit;
- the destination must exist;
Expand Down
10 changes: 10 additions & 0 deletions docs/reporting-subscriptions-seller.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,16 @@ The SDK reads these surfaces under each principal's identity. A principal sees e

A decisioning `Account` carries `timezone=` and `operator=`. An account without a timezone is refused rather than read as UTC.

## AdCP 3.3.0-beta.3 rules the engine applies

- **Admission under `max_accounts`.** Candidates are ordered by `authorization.authorized_at`, then `account_id` in byte order, and an admitted account keeps its slot. A waiting candidate carries a `capacity` exception.
- **Several feeds per purpose.** A caller may hold several pacing or analytics subscriptions for one account. Where two resolve the same offering for a package, the lowest `subscription_id` owes it and the other records an `overridden` exception naming the `package_ids`. Billing stays one per account at a scope level.
- **Membership in place.** Membership edits (`account_ids`, `exclude_account_ids`) keep the version. A removed account stays listed, `excluded`, with an `excluded_by_buyer` entry from the cutover. An added account gets a closed entry covering the time before it joined.
- **`destination_overrides`.** An account named by an override, by `account_ids` or `operator_domains`, resolves against the override's destination method. Its owed sets carry the override as the version's `destination`, so `bind_managed_generation` binds and checks the override's `path_template`.
- **Bounded readback.** `prior_versions` lists the 16 newest inline, and the account lists 1,000, each with a total count. `get_principal` `reporting_subscription_page` pages the rest from the engine's state.
- **Partial grants.** A grant that keeps the account covered refuses a task it does not list with `SCOPE_INSUFFICIENT`.
- **Billing stubs and `regranted_at`.** A billing cutover owes a stub obligation (`stub: true`), and `list_accounts` serves `authorization.regranted_at`.

## What the SDK derives

- **The principal runtime.** `install()` serves `get_principal` and `sync_principal` and advertises the principal section. The service's `PrincipalService` is `reporting.principals`.
Expand Down
29 changes: 29 additions & 0 deletions src/adcp/account_outcomes.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,17 @@ class AccountReady:

``status`` is the account's status, which may still need buyer action
before spend (``payment_required``, for example).

On the ``request_account`` path (AdCP 3.3.0-beta.3, adcp#8620):

* ``resolution`` is ``matched``, ``created`` or ``updated``. A matched or
created account carries its own money terms (:attr:`billing`,
:attr:`billing_entity`, :attr:`payment_terms`), which can differ from
the terms requested: read them before spending.
* ``effective_at`` is when changed money terms take effect on an updated
account; delivery before it is invoiced under the previous terms.
* ``not_carried_over`` names the account-level configurations a separate
new account (a change resolved as ``created``) does not have.
"""

account_id: str | None
Expand All @@ -98,6 +109,24 @@ class AccountReady:
request_id: str | None = None
account: Mapping[str, Any] | None = None
kind: Literal["ready"] = "ready"
resolution: str | None = None
effective_at: str | None = None
not_carried_over: tuple[str, ...] = ()

@property
def billing(self) -> Any:
"""The account's ``billing`` (who is invoiced), as the seller resolved it."""
return None if self.account is None else self.account.get("billing")

@property
def billing_entity(self) -> Any:
"""The account's ``billing_entity``, without write-only fields."""
return None if self.account is None else self.account.get("billing_entity")

@property
def payment_terms(self) -> Any:
"""The account's ``payment_terms``, as the seller resolved them."""
return None if self.account is None else self.account.get("payment_terms")


@dataclass(frozen=True)
Expand Down
11 changes: 11 additions & 0 deletions src/adcp/accounts.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,12 +225,23 @@ def _request_outcome(
request_id = str(wire["request_id"])
if state == "resolved":
account = wire.get("account") if isinstance(wire.get("account"), Mapping) else None
resolution = wire.get("resolution")
effective_at = wire.get("effective_at")
return AccountReady(
account_id=str(wire["account_id"]),
status=str((account or {}).get("status") or "active"),
via="request_account",
request_id=request_id,
account=account,
# adcp#8620: money terms ride on the resolved account; an updated
# money change says when it takes effect.
resolution=None
if resolution is None
else str(getattr(resolution, "value", resolution)),
effective_at=None if effective_at is None else str(effective_at),
not_carried_over=tuple(
str(getattr(item, "value", item)) for item in wire.get("not_carried_over") or ()
),
)
if state == "in_review":
return AccountPending(
Expand Down
2 changes: 2 additions & 0 deletions src/adcp/reporting/advanced.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
apply_reporting_subscriptions,
refresh_destination_states,
render_path_template,
reporting_subscription_destination_for_account,
)
from adcp.reporting.subscriptions.resolution import (
STANDARD_REPORTING_DIMENSIONS,
Expand Down Expand Up @@ -160,6 +161,7 @@
"apply_reporting_subscriptions",
"refresh_destination_states",
"render_path_template",
"reporting_subscription_destination_for_account",
# Service planner hooks
"ManagedGenerationBinder",
"PlanningErrorHandler",
Expand Down
2 changes: 2 additions & 0 deletions src/adcp/reporting/subscriptions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@
apply_reporting_subscriptions,
refresh_destination_states,
render_path_template,
reporting_subscription_destination_for_account,
)
from adcp.reporting.subscriptions.resolution import (
STANDARD_REPORTING_DIMENSIONS,
Expand Down Expand Up @@ -160,6 +161,7 @@
"apply_reporting_subscriptions",
"refresh_destination_states",
"render_path_template",
"reporting_subscription_destination_for_account",
"REPORTING_SUBSCRIPTIONS_FEATURE",
"REPORTING_SUBSCRIPTIONS_SECTION",
"STANDARD_REPORTING_DIMENSIONS",
Expand Down
Loading
Loading