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
  •  
  •  
  •  
64 changes: 59 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ on:
push:
branches: [main, python-adcp-sdk-setup]
pull_request:
branches: [main]
branches: [main, conductor/reporting-webhook-activity-1168b]

# Default @adcp/sdk runner alias for storyboard jobs. Tracks the current
# stable @adcp/sdk release via the ``latest`` npm dist-tag.
Expand Down Expand Up @@ -91,7 +91,7 @@ jobs:

- name: Run adopter type-check suite
if: matrix.python-version == '3.12'
run: mypy --strict tests/type_checks/ examples/reporting_webhook_activity.py
run: mypy --strict tests/type_checks/ examples/reporting_webhook_activity.py examples/reporting_status_notifications.py

- name: Enforce adopter type-check fixture contract
if: matrix.python-version == '3.12'
Expand Down Expand Up @@ -169,6 +169,7 @@ jobs:
tests/conformance/decisioning/test_pg_reference_workflow_queue.py \
tests/conformance/reporting/ \
--ignore=tests/conformance/reporting/test_reporting_notification_process_matrix.py \
--ignore-glob='tests/conformance/reporting/test_reporting_status_*.py' \
Comment thread
bokelley marked this conversation as resolved.
-v
;;
process)
Expand All @@ -185,19 +186,67 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 5
permissions: {}
needs: pg-conformance
needs: [pg-conformance, pg-reporting-status]
if: ${{ always() }}
steps:
- name: Require every Postgres conformance lane
env:
PG_RESULT: ${{ needs.pg-conformance.result }}
STATUS_RESULT: ${{ needs.pg-reporting-status.result }}
run: |
if [ "$PG_RESULT" != "success" ]; then
if [ "$PG_RESULT" != "success" ] || [ "$STATUS_RESULT" != "success" ]; then
echo "Postgres conformance matrix result: $PG_RESULT"
echo "Reporting status conformance result: $STATUS_RESULT"
exit 1
fi
echo "All Postgres conformance lanes passed"

pg-reporting-status:
name: Reporting status conformance (Postgres 16)
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
services:
postgres:
image: postgres:16
env:
POSTGRES_HOST_AUTH_METHOD: trust
POSTGRES_DB: adcp_status_test
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 5s
--health-timeout 5s
--health-retries 10
steps:
- uses: actions/checkout@v6

- name: Fetch exact reviewed A and B compatibility artifacts
timeout-minutes: 1
run: |
git fetch --no-tags --depth=1 origin \
17ee407ae3978c8a2bb54437287afbf9dafb8130 \
0f34c666ac1961e9832fce43ef0ef6937b3c1dde

- name: Set up Python 3.12
uses: actions/setup-python@v6
with:
python-version: "3.12"
cache: pip
cache-dependency-path: pyproject.toml

- name: Install dependencies (with [pg] extra)
run: |
python -m pip install --upgrade pip
pip install -e ".[dev,pg]"

- name: Run status semantics, transaction, clock/process and rolling gates
env:
ADCP_PG_TEST_URL: postgresql://postgres@localhost:5432/adcp_status_test
run: pytest tests/conformance/reporting/test_reporting_status_*.py -v

conventional-commits:
name: Validate conventional commit format
runs-on: ubuntu-latest
Expand Down Expand Up @@ -283,6 +332,7 @@ jobs:
working-directory: /tmp
run: |
python - <<'PY'
import json
from importlib.resources import files

from adcp import get_adcp_spec_version
Expand All @@ -303,7 +353,11 @@ jobs:
if child.is_dir()
}
assert packaged_versions == {"2.5", "3.0", "3.1", current_bundle}
assert len(STANDARD_ERROR_CODES) == 120
error_schema = json.loads(
(schema_root / current_bundle / "enums/error-code.json").read_text(encoding="utf-8")
)
# NOT_SUPPORTED is the SDK's explicit extension to the pinned vocabulary.
assert set(STANDARD_ERROR_CODES) == {*error_schema["enum"], "NOT_SUPPORTED"}
assert adcp_error("AUTH_MISSING", "x")["errors"][0]["recovery"] == "correctable"
assert make_versioned_base("3.1", "ListCreativesRequest")
assert ReportingCapabilities.__name__ == "ReportingCapabilities"
Expand Down
4 changes: 2 additions & 2 deletions MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ 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-rc.3 *.json
recursive-include src/adcp/_schemas/3.2.0-rc.6 *.json
prune src/adcp/_schemas/3.1.0-*
# A clean VCS source tree has no ignored ``src/adcp/_schemas`` directory.
# Keep the tracked inputs needed by the build_py hook in source distributions
# so wheels built from an sdist have the same schema set as direct VCS wheels.
recursive-include schemas/cache/2.5 *.json
recursive-include schemas/cache/3.0 *.json
recursive-include schemas/cache/3.1 *.json
recursive-include schemas/cache/3.2.0-rc.3 *.json
recursive-include schemas/cache/3.2.0-rc.6 *.json
28 changes: 28 additions & 0 deletions MIGRATION_ADCP_3.1_TO_3.2.md
Original file line number Diff line number Diff line change
Expand Up @@ -292,3 +292,31 @@ At minimum, exercise these rows independently:
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.

## Reporting status notifications in rc.6

AdCP 3.2.0-rc.6 permits a consumer-mismatch waiver only after explicit bilateral agreement for
the exact caller/account issue, causing statement and diagnosed conflict. Obtain that consent
and retain its private audit before calling `set_issue_state(state="waived")`. `external_ref`
remains untrusted correlation text, not proof of agreement.

The SDK binds the waiver to that immutable statement and conflict. It removes the waived issue
from the public response and restores underlying seller health; unrelated impairments remain. A
later statement or different diagnosed conflict is evaluated independently and receives a new
occurrence. The original waiver and consumer statement are retained. Advertised status recovery
sends an empty/absent issue-ID list when the final impairment clears.

Apply the additive ledger and status schema migration before enabling status notifications.
PostgreSQL records waiver bindings in a separate table, preserving the existing 464-object
delivery/outbox contract. Status readiness also requires those binding objects and the updated
status snapshot function. Historical waivers without an exact binding remain in the audit
history and do not silently suppress a current disagreement; no consent evidence is invented or
backfilled.

Rolling controls use the delivery/outbox baseline `17ee407ae3978c8a2bb54437287afbf9dafb8130` and
webhook-activity baseline `0f34c666ac1961e9832fce43ef0ef6937b3c1dde`. Compatibility with earlier
binaries is untested and unclaimed. In particular, the earlier `21bf443e` snapshot uses
collation-dependent fingerprints. Whole-schema startup validation by the delivery/outbox
baseline after status activation is intentionally unsupported; the webhook-activity baseline's
subset readiness is the supported restart path. Drain old workers before status activation as
described in [reporting status notifications](docs/reporting-status-notifications.md).
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ This README serves both sides of an AdCP integration. Jump to what you're doing:

- **Connect as a buyer** → [Quick Start: Test Helpers](#quick-start-test-helpers) and [Quick Start: Distributed Operations](#quick-start-distributed-operations). Entry point: `from adcp import ADCPClient, AgentConfig`; start with the `client.simple.*` API.
- **Build a seller / agent** → [Building an AdCP Agent](#building-an-adcp-agent). Entry point: `from adcp.server import ADCPHandler, serve`; use the [production seller path](docs/production-seller.md) when adding tenants, durable tasks, and webhooks.
- **Run Reliable Reporting** → [Account currencies](docs/reporting-currency.md), [ledger migrations](docs/reporting-ledger-migration.md), and the [optional reconciliation storage contract](docs/reporting-reconciliation-storage.md).
- **Run Reliable Reporting** → [Account currencies](docs/reporting-currency.md), [ledger migrations](docs/reporting-ledger-migration.md), [reconciliation storage](docs/reporting-reconciliation-storage.md), and [durable status notifications](docs/reporting-status-notifications.md).
- **Understand the type system & imports** → [Type Safety](#type-safety) (import surface, partial modules, cold-start note).
- **Test against reference agents** → [Quick Start: Test Helpers](#quick-start-test-helpers) and [Test Helpers](#test-helpers). Entry point: `from adcp.testing import test_agent, creative_agent`.

Expand Down
159 changes: 58 additions & 101 deletions SCHEMA_DELTAS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,111 +2,68 @@

## Files added

- `core/applicable_package_id.py` — ApplicablePackageId
- `core/frequency_cap_constraints.py` — FrequencyCapConstraints
- `core/frequency_cap_duration_unit.py` — FrequencyCapDurationUnit
- `core/frequency_cap_impression_constraints.py` — AllowedValue, FrequencyCapImpressionConstraints
- `core/frequency_cap_interval_constraints.py` — AllowedInterval, FrequencyCapIntervalConstraints
- `core/frequency_cap_requirements.py` — FrequencyCapRequirements
- `core/geo_metro.py` — GeoMetro
- `core/media_buy_frequency_cap_capability.py` — MediaBuyFrequencyCapCapability
- `core/media_buy_frequency_cap_requirement.py` — MediaBuyFrequencyCapRequirement
- `core/media_buy_frequency_cap_support.py` — MediaBuyFrequencyCapSupport
- `core/media_buy_support.py` — ProductMediaBuySupport
- `core/media_buy_support_requirements.py` — ProductMediaBuySupportRequirements
- `core/negative_keyword.py` — NegativeKeyword
- `core/targeting_input.py` — KeywordTarget, TargetingOverlayInput
- `core/targeting_unknown_age_eligibility_constraint.py` — TargetingUnknownAgeEligibilityConstraint
- `core/targeting_verified_age_basis_constraint.py` — TargetingVerifiedAgeBasisConstraint
- `enums/frequency_cap_control_mode.py` — FrequencyCapControlMode
- `enums/frequency_cap_mutable_field.py` — FrequencyCapMutableField
- `enums/media_buy_frequency_cap_control_mode.py` — MediaBuyFrequencyCapControlMode
- `media_buy/product_purchase_input.py` — ProductPurchaseInput
- `compliance/get_creative_features_completion.py` — GetCreativeFeaturesComplianceCompletion
- `core/async_response_refs/creative/get_creative_features_async_response_submitted.py` — GetCreativeFeaturesSubmitted
- `core/verification_token_claims.py` — AgenticadvertisingOrgVerificationTokenClaims, Role, VerificationTokenGradingProfile, VerificationTokenMode
- `creative/get_creative_features_async_response_submitted.py` — GetCreativeFeaturesSubmitted
- `creative/get_creative_features_terminal_success.py` — GetCreativeFeaturesSuccess
- `enums/request_signing_error_code.py` — RequestSigningErrorCode
- `enums/seller_policy_decline_reason.py` — SellerPolicyDeclineReason
- `error_details/requote_required.py` — EnvelopeField, EnvelopeField1, EnvelopeField1Item, RequoteRequiredDetails

## Field changes

- `a2ui/si_catalog.py`
- **classes added**: Action23
- **classes removed**: Action22
- `account/sync_accounts_response.py`
- `Account`: `+account`
- `bundled/protocol/get_adcp_capabilities_response.py`
- **classes added**: AggregateFrequencyCapping, FrequencyCapConstraints, FrequencyCapIntervalConstraints, MaxImpressionsConstraints, MutableField, OperationsContact, ReachUnit, SupportedControlMode
- **classes removed**: SupportedPerUnit
- `MediaBuy`: `+aggregate_frequency_capping`
- `ReportingDelivery`: `+consumer_mismatch_escalation_seconds`, `+operations_contact`
- `MediaBuy`: `+anonymous_discovery`
- `Signals`: `+anonymous_discovery`
- `Specialism`: `+sales_exchange`, `+sales_retail_media`, `+sales_streaming_tv`
- `compliance/comply_test_controller_request.py`
- **classes added**: AdvanceTo
- `Operation`: `+restate_after_received`
- `Params`: `+advance_to`, `+received_reporting_revision_id`
- `core/canonical_media_buy_action.py`
- `Action`: `+update_media_buy_frequency_cap`
- `Action3`: `+update_media_buy_frequency_cap`
- `core/canonical_media_buy_action_fields.py`
- `CanonicalMediaBuyActionFields`: `+applicable_package_ids`
- `core/canonical_product.py`
- `CanonicalProduct`: `+media_buy_support`
- `core/media_buy.py`
- `MediaBuy`: `+frequency_cap`
- `core/media_buy_available_action.py`
- `MediaBuyAvailableAction`: `+applicable_package_ids`
- `core/product.py`
- `Product`: `+media_buy_support`
- `core/proposal.py`
- `Proposal`: `+frequency_cap`
- `core/reporting_consumer_status.py`
- **classes added**: MismatchCode
- `ConsumerStatus`: `+content_mismatch`
- `ReportingConsumerStatus`: `+mismatch_code`
- `core/reporting_delivery_capabilities.py`
- **classes added**: OperationsContact
- `ReportingDeliveryCapabilities`: `+consumer_mismatch_escalation_seconds`, `+operations_contact`
- `core/reporting_delivery_config.py`
- **classes added**: AuthoritativeParty
- `ReportingDeliveryConfiguration`: `+authoritative_party`
- `core/reporting_status_issue.py`
- **classes added**: IssueState
- `ReportingStatusIssue`: `+external_ref`, `+issue_state`, `+opened_at`
- `core/targeting.py`
- **classes added**: AudienceExclude, AudienceInclude, AxeExcludeSegment, AxeIncludeSegment, Browser, BrowserExclude, CollectionListExclude, DaypartTargets, Demographics, DevicePlatform, DevicePlatformExclude, DeviceType, DeviceTypeExclude, GeoCountries, GeoCountriesExclude, GeoMetrosExclude, GeoPlaces, GeoPlacesExclude, GeoPostalAreas, GeoPostalAreasExclude, GeoRegions, GeoRegionsExclude, PlacementSelection, PropertyListExclude, StoreCatchments
- **classes removed**: GeoMetro, NegativeKeyword
- `core/targeting_overlay_requirements.py`
- `TargetingOverlayRequirements`: `+frequency_cap_support`
- `core/targeting_overlay_support.py`
- `TargetingOverlaySupport`: `+frequency_cap_support`
- `enums/canonical_media_buy_action.py`
- `CanonicalMediaBuyActionName`: `+update_media_buy_frequency_cap`
- `Arm`: `+completed`
- `Operation`: `+advance_past_escalation`, `+advance_past_status_deadline`
- `Params`: `+evaluation_id`
- `compliance/comply_test_controller_response.py`
- `ComplyResponseArm`: `+completed`
- `Forced`: `+evaluation_id`
- `core/collection.py`
- `Collection`: `+publisher_domain`
- `core/registry_event.py`
- **classes added**: GradingProfile
- `Payload8`: `+grading_profile`
- `Payload9`: `+grading_profile`
- `core/x_entity_types.py`
- `XEntityTypes`: `+creative_evaluation`
- `creative/get_creative_features_request.py`
- `GetCreativeFeaturesRequest`: `+idempotency_key`, `+push_notification_config`
- `creative/get_creative_features_response.py`
- **classes added**: GetCreativeFeaturesResponse3
- **classes removed**: GetCreativeFeaturesResponse1
- `GetCreativeFeaturesResponse2`: `+evaluation_id`
- `enums/error_code.py`
- `ErrorCode`: `+COMMITTED_RESOURCE_PURGED`
- `enums/specialism.py`
- `AdcpSpecialism`: `+sales_exchange`, `+sales_retail_media`, `+sales_streaming_tv`
- `enums/task_type.py`
- `TaskType`: `+get_creative_features`
- `error_details/action_not_allowed.py`
- `ActionNotAllowedDetails`: `+decline_reason`
- `media_buy/accept_proposal_request.py`
- `AcceptProposalRequest`: `+name`
- `media_buy/accept_proposal_response.py`
- `AcceptProposalResponse1`: `+name`
- `media_buy/buy_products_request.py`
- `BuyProductsRequest`: `+frequency_cap`
- `media_buy/commercial_terms.py`
- `CommercialTerms`: `+frequency_cap`
- `media_buy/control_media_buy_request.py`
- `ControlMediaBuyRequest`: `+frequency_cap`
- `media_buy/create_media_buy_request.py`
- `CreateMediaBuyRequest`: `+frequency_cap`
- `media_buy/create_media_buy_response.py`
- `CreateMediaBuyResponse1`: `+frequency_cap`
- `media_buy/get_media_buys_response.py`
- `MediaBuy`: `+frequency_cap`
- `media_buy/get_products_request.py`
- `Field1`: `+media_buy_support`
- `Fields`: `+media_buy_support`
- `GetProductsRequest`: `+media_buy_frequency_cap`, `+required_media_buy_support`
- `media_buy/get_reporting_status_response.py`
- `ObligationCounts`: `+consumer_status_pending`
- `media_buy/product_discovery_criteria.py`
- `ProductDiscoveryCriteria`: `+media_buy_frequency_cap`, `+required_media_buy_support`
- `media_buy/product_fields.py`
- `ProductResponseField`: `+media_buy_support`, `+overlay_support`
- `media_buy/product_purchase.py`
- **classes added**: AgencyEstimateNumber, AudienceEvidencePins, Bidding, Budget, CatalogIds, Context, DailyBudgetCap, EndTime, Ext, FormatOptionRefs, MinSpendTarget, OptimizationGoals, Pacing, PerformanceStandards, Pricing, StartTime
- `media_buy/proposal_refinement.py`
- **classes added**: ProposalRefinement9
- `ProposalRefinement2`: `+remove_media_buy_frequency_cap`
- `ProposalRefinement3`: `+remove_media_buy_frequency_cap`
- `ProposalRefinement4`: `+remove_media_buy_frequency_cap`
- `ProposalRefinement5`: `+remove_media_buy_frequency_cap`
- `ProposalRefinement6`: `+remove_media_buy_frequency_cap`
- `ProposalRefinement7`: `+remove_media_buy_frequency_cap`
- `ProposalRefinement8`: `+remove_media_buy_frequency_cap`
- `media_buy/update_media_buy_request.py`
- `UpdateMediaBuyRequest`: `+frequency_cap`
- `media_buy/update_media_buy_response.py`
- `UpdateMediaBuyResponse1`: `+frequency_cap`
- `BuyProductsRequest`: `+name`
- `media_buy/buy_products_response.py`
- `BuyProductsResponse1`: `+name`
- `media_buy/media_buy_commitment_response.py`
- `MediaBuyCommitmentResponse1`: `+name`
- `protocol/get_adcp_capabilities_response.py`
- `MediaBuy`: `+aggregate_frequency_capping`
- `MediaBuy`: `+anonymous_discovery`
- `Signals`: `+anonymous_discovery`
- `protocol/sync_principal_response.py`
- **classes added**: Action33
- **classes removed**: Action32
12 changes: 7 additions & 5 deletions docs/reporting-notification-outbox.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
The optional reporting outbox commits a typed logical event in the ledger's
transaction, then expands subscribers and delivers HTTP in separate phases.
This implements the ledger and Managed readiness slice of [#1168](https://github.com/adcontextprotocol/adcp-client-python/issues/1168).
It retains a durable status-dirty handoff for the later complete status projector.
It retains the durable status-dirty input for the optional [C status projector](reporting-status-notifications.md).

| Committed change | Retained notification work |
| --- | --- |
Expand All @@ -12,11 +12,13 @@ It retains a durable status-dirty handoff for the later complete status projecto
| Managed destination/reconciliation change | Consumer-scoped status-dirty evidence |
| Verified materialization with its frozen Managed binding | `reporting.delivery_ready`, scoped to the reconciliation consumer |

There is no `reporting.status_changed` emitter. Clock sweeps and complete status
fingerprint deduplication belong to #1168C. The optional
This A outbox does not emit `reporting.status_changed`. The separate
[#1168C status lifecycle](reporting-status-notifications.md) provides that emitter,
clock sweeps and complete semantic fingerprinting in isolated queues. The optional
[#1168B activity layer](reporting-webhook-activity.md) adds durable HTTP reservations
and a list-accounts projection. Without that mounted layer the capability helper
omits `status_notification` and sets `supports_webhook_activity=false`.
and a list-accounts projection. Status and activity gates remain independent;
the A-only capability helper omits `status_notification` and sets
`supports_webhook_activity=false`.

## Optional wiring

Expand Down
Loading
Loading