Skip to content

feat: make canonical creatives the primary SDK contract #972

Description

@bokelley

Context

The TypeScript SDK has made canonical creatives the primary contract in adcontextprotocol/adcp-client#2391. The reference implementation is merged and published as @adcp/sdk@13.0.0-rc.3; Python implementation is no longer gated on waiting for a TypeScript RC.

This is broader than the v1↔v2 projection layer completed in #741 and the comparison helpers completed in #885. Adopting the modern Python SDK must mean application code uses format_kind, format_options, and format_option_refs, while {agent_url, id} creative identity is confined to wire adapters and explicitly named migration APIs.

Target release

  • Target Python SDK 7.0.0-rc before GA.
  • Treat the public-surface changes as semver-major.
  • Keep negotiated AdCP protocol versions distinct from the Python package version in code, docs, and diagnostics.

Required public contract

  • Make canonical creative-bearing models the unqualified/default public types for products, packages, creatives, media-buy requests/responses, delivery responses, and server handler payloads.
  • Move raw {agent_url, id} shapes behind Legacy* model names and explicitly named legacy client/server methods. Follow the cross-SDK Legacy* convention; do not introduce AgentUrl* names.
  • Make list_creative_formats legacy-only. Canonical discovery is through product format_options and canonical declarations.
  • Remove format_ids from canonical Product and creative-filter models.
  • Make generic execute_task, multi-agent APIs, callbacks, response builders, server helpers, and asset helpers canonical by default. Any raw-format variant must be explicit and legacy-named.
  • Root Format must mean a canonical format declaration, not the generated named-format model.
  • FormatId must not be available from the normal adcp root import. Expose legacy identity only as LegacyFormatId or through an explicit legacy/raw module.
  • Primary discovery and write methods must be canonical across get_products, create_media_buy, update_media_buy, sync_creatives, list_creatives, media-buy reads, and creative-delivery reads.
  • Preserve raw wire access for migration/conformance tooling through conspicuous legacy methods, hard-deprecated for removal with AdCP 4.0.
  • Generated/raw protocol modules may remain available for wire tooling, but must not be primary application-facing imports.

Runtime Pydantic invariants

These are runtime requirements, not typing-only or documentation-only constraints:

  • For every primary model, model_json_schema() must recursively contain no property named format_id, format_ids, or format-scoped agent_url, including nested definitions, unions, aliases, request/response containers, callback payloads, and server payloads.
  • For every primary model, model_dump() and JSON serialization must be incapable of emitting those fields at any nesting depth, including when input was constructed from legacy/raw objects, aliases, extras, or nested generated models.
  • Tests must walk the complete primary schema and serialized value recursively. A top-level assertion or static type check is insufficient.

Wire negotiation and lifecycle boundaries

  • Apply one capability matrix consistently:
    • AdCP 3.0: legacy wire; upgrade reads and downgrade writes.
    • AdCP 3.1: the version alone is not canonical evidence; use media_buy.features.canonical_creatives, then request-local schema evidence where available, otherwise fail closed unless an unambiguous legacy projection exists.
    • AdCP 3.2+: canonical by contract; canonical_creatives: false is a capability error.
  • Use the same effective converter and canonical response boundary for polling, task continuation, input-required flows, async completion, callbacks, and webhooks.
  • For server/framework APIs, normalize legacy inbound requests before adopter handlers and project responses back to the caller's captured negotiated dialect.
  • Keep network resolution outside the pure projector. Feed it validated, cached catalog snapshots in precedence order (publisher → approved community mirror → agent-derived/configured).
  • Keep catalog lookup URL-sensitive, including explicit historical AAO aliases and AdCP URL canonicalization while preserving path/query scope.
  • Snap's canonical mirror declarations remain canonical-only unless an exact owner-sensitive v1_format_ref authorizes a legacy route; public availability or a matching ID alone does not create one.
  • Canonical Product requires at least one format_options entry. Retain partially mapped products with mapped options and sanitized FORMAT_PROJECTION_FAILED entries; omit wholly unmappable products from the canonical list while retaining the non-fatal error in portable errors[] and SDK diagnostics. Never emit format_options: [] for a mapped canonical product.

Cross-language golden parity

Pin validation to the exact inputs, outputs, diagnostics, and transition behavior shipped in TypeScript RC3—not merely equivalent high-level behavior. The fixture is currently repository-local rather than a genuinely shared fixture package, so Python must vendor a versioned copy of the RC3 golden corpus until a shared package exists.

RC3 reference corpus:

The stable migrated option-ID algorithm is normative:

identity = compact JSON([agent_url, id, width_or_null, height_or_null, duration_ms_or_null])
format_option_id = "migrated_" + HMAC-SHA256(key=b"", message=UTF8(identity)).hex()[0:32]

The complete legacy tuple and field order are part of the contract. Tests must assert exact IDs and stability under seller-array reordering.

Compatibility and release work

  • Add inbound legacy_format_converter support for seller-owned formats and a separate canonical-to-legacy resolver for persisted canonical custom formats.
  • Include a migration guide with canonical replacements and explicit legacy escape hatches.
  • Audit examples and all root/alias re-exports so old shapes are not silently retained by convenience imports.
  • Add transition tests for discovery → selection → serialization → create/update/sync → async/callback/webhook response.

Acceptance criteria

Primary model boundary

  • A normal Python client/server integration completes the creative lifecycle without importing or handling agent_url, format_id, or format_ids.
  • Recursive model_json_schema() tests prove those legacy fields are absent from every primary model.
  • Recursive model_dump()/JSON tests prove primary models cannot emit those legacy fields.
  • Root imports and all generic helpers expose the canonical surfaces described above; only explicit Legacy*/raw paths expose legacy identity.

Projection precedence and safety

  • Resolve exact owner + ID catalog identity first.
  • If exact lookup misses, allow a bare-ID fallback only when that ID has exactly one meaning in the bundled AAO catalog and the supplied owner is a valid public HTTPS owner.
  • Preserve the complete original legacy tuple for same-process legacy delivery; do not rewrite it to the AAO owner.
  • An explicit custom converter overrides compatibility inference, including intentional AAO-slug reuse.
  • Reject ambiguous/colliding IDs, unsafe or invalid owners, invalid parameters, catalog conflicts, and contradictory dimensions/duration.
  • Never reverse-guess after a process boundary. Persisted canonical selections require rediscovery/readback or an explicit durable canonical-to-legacy resolver backed by adopter storage.
  • Preserve exact owner-sensitive v1_format_ref routes, including dimensional discriminators, for legacy delivery.

Interoperability and fixtures

  • Legacy AdCP 3.0/3.1 peers interoperate through deterministic adapters or receive actionable projection errors.
  • Seller-specific custom formats have tested bidirectional conversion hooks.
  • Python vendors and passes the exact TypeScript RC3 golden inputs/outputs, including all 16 observed Vox, Triton, and OpenAds refs; historical aliases; unique bare-ID and collision cases; unsafe owners; contradictory parameters; original-tuple round trips; persistence boundaries; and all-unmappable discovery.
  • Migrated option IDs exactly match the normative empty-key HMAC algorithm above.
  • Public documentation clearly distinguishes Python SDK 7.0.0 from the negotiated AdCP protocol version.

References

Cross-SDK tracking

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions