Skip to content

The generated request hierarchy changed shape twice in two majors; no type means "an AdCP request" #1313

Closed

Description

@KonstantinMirin

Summary

The generated class hierarchy for request models has changed shape twice in two major
versions, once because codegen changed and once because the schemas changed convention. The
result is that no public type means "an AdCP request" any more: 78 of 92 canonical request
models descend from AdcpVersionEnvelope and 14 do not, and the 14 are the new 3.2 compact
tools.

A consumer cannot write a mixin, a generic boundary function, or an isinstance check over
requests. The wire contract is unaffected either way, which is what makes this easy to ship
and expensive to consume.

Two separate causes, both measured

1. Codegen stopped rendering the root allOf as inheritance in 7.0.2, and resumed in 8.0.0.

6.6.0 7.0.2 8.0.0
Requests descending from AdcpVersionEnvelope 75 of 80 0 of 77 78 of 92

media-buy/get-products-request.json is byte-identical in the 6.6.0 and 7.0.2 wheels,
sha256 f057852819442a6f, and in both it composes
{"allOf": [{"$ref": ".../core/version-envelope.json"}]} with no adcp_version property of
its own. So 7.0.2 was a rendering change against an unchanged contract.

2. The 3.2 compact tools do not compose the envelope at all. They declare adcp_version
as their own property, so codegen is rendering them faithfully and the inconsistency is in the
schemas:

media-buy/get-products-request.json     allOf: [.../core/version-envelope.json]  own adcp_version: no
creative/sync-creatives-request.json    allOf: [.../core/version-envelope.json]  own adcp_version: no
media-buy/list-products-request.json    allOf: [no envelope ref]                 own adcp_version: YES
media-buy/buy-products-request.json     allOf: [no envelope ref]                 own adcp_version: YES

The same split covers accept-proposal-request.json, control-media-buy-request.json and
decline-proposals-request.json. Those six are exactly the tools that replace get_products,
so the newest surface is the inconsistent one and the gap widens with each tool added.

Why it costs a consumer

AdcpVersionEnvelope is not public in adcp.types in any of the three wheels, so the only way
to say "this is an AdCP request" has always been to import it from
generated_poc.core.version_envelope and test descent. That works for 78 of 92 today, and for
0 of 77 under 7.0.2.

The Prebid Sales Agent refuses, at import, to register a tool whose request DTO fails
issubclass(model, AdcpVersionEnvelope). The refusal exists because one tool once grew two
hand-written parallel request models, neither a subclass of the other, differing by three
required fields. Under 7.0.2 that refusal fired for every tool, which is why 7.0.2 was not
adoptable. Under 8.0.0 it passes for the 3.1 surface and will refuse all six 3.2 compact tools.

The obvious substitute does not hold. Asserting the two version fields instead of descent is
forgeable: a hand-written parallel model declaring adcp_version and adcp_major_version
passes a field test and fails issubclass, and that model is precisely the case the refusal
exists to catch. There is no candidate base to switch to either —
CanonicalBoundaryModel covers 11 of 77 requests at 7.0.2 and also appears on 12 of 131
responses, so it cannot discriminate a request from a response.

Ask

Two things, and the second is the one that lasts.

1. Settle the convention. Either every request composes core/version-envelope.json at its
root, or none does and adcp_version is declared inline everywhere. Both are defensible; the
split is not. If composition is the intent, the six 3.2 compact tools are the ones to change;
if inline is the intent, say so and the rest follow.

2. Guarantee the emitted hierarchy, with a test rather than an intention. A schema whose
root composes by $ref should render as inheritance, and that should be asserted over the
whole generated tree rather than left to whatever the generator does this release. The scalar
rewrite in #1277 has exactly this shape: a ratchet that walks every generated module and fails
on any leftover, so a regression cannot ship quietly and a new case fails loudly instead of
being silently mis-rendered.

The hierarchy is part of the public surface for anyone who extends these models, which the
documented inheritance pattern invites. A change to it is breaking even when the wire bytes are
identical, and three of the last four majors changed it without a major-version signal.

Versions

adcp 6.6.0, 7.0.2 and 8.0.0 from PyPI. Descent counts produced by importing adcp.types and
walking each exported class's MRO against
adcp.types.generated_poc.core.version_envelope.AdcpVersionEnvelope. Schema claims from the
_schemas sets vendored inside each wheel; the sha256 comparison from the two wheels' copies
of the same file.

Activity

  1. added
    claude-triagingTriage routine is actively working on this issue (1-3 min)
    on Oct 2, 2026
  2. KonstantinMirin commented on Oct 2, 2026

    @KonstantinMirin
    CollaboratorAuthor

    This is #1136 again, one surface over — and #1136's fix is still holding

    Worth recording, because it is the argument for a rule rather than another fix.

    #1136 reported that oneOf response branches lost the root allOf ProtocolEnvelope, 19 of
    24 SuccessResponse aliases. It was fixed. Measured on adcp 8.0.0, it has held:

    *SuccessResponse aliases: 23      descending from ProtocolEnvelope: 23
    CreateMediaBuyResponse1   envelope=True   status=True
    CreateMediaBuyResponse2   envelope=True   status=True
    CreateMediaBuyResponse3   envelope=True   status=True
    

    So the response side is correct and stayed correct across two majors. The request side then
    lost the same construct twice: 7.0.2's codegen flattened it against byte-identical schemas
    (75/80 -> 0/77 -> 78/92), and the six 3.2 compact tools stopped composing it in the schemas
    themselves (adcontextprotocol/adcp#7892).

    Same construct — a root allOf $ref that should render as a base class. Same failure —
    flattened into per-class fields. Different surface each time, and each time it was found by a
    consumer rather than by the build.

    The pattern across all of them, including the four scalar-RootModel reports that produced
    #1277: a report names the instances it can see, the instances get fixed, and the construct
    reappears somewhere the report did not look. #1136 fixed responses. Nothing then prevented
    requests. #1277's remedy was deliberately different — a whole-tree assertion with no list,
    which is why a new case fails loudly instead of shipping quietly.

    That is what this issue is asking for on the composition axis, and the fact that #1136's fix
    has held perfectly for the surface it covered is the evidence that coverage, not correctness,
    is the problem.

  3. bokelley commented on Oct 2, 2026

    @bokelley
    Contributor

    Triage

    Classification: Bug
    Bucket(s): client (types) — client and cross-repo labels not found in this repo (gap noted)
    Status: ready-for-human

    What the experts said:

    • ad-tech-protocol-expert: The 6 compact 3.2 schemas are a spec defect — they use property-level $ref fragments instead of allOf composition, directly contradicting core/version-envelope.json's own doc ("composed via allOf into every AdCP request"). Wire bytes are identical; zero version-negotiation impact. Fix: add allOf: [{"$ref": "core/version-envelope.json"}] and remove the redundant property entries in adcontextprotocol/adcp. Open Q: align additionalProperties: false between groups before filing the spec PR.
    • code-reviewer: Ratchet test is fully feasible — tests/test_protocol_envelope_inheritance.py is the exact template. Important: 8 of the 14 non-inheriting schemas intentionally inline (trusted-match group carries an explicit privacy-boundary comment); the test must match only on schemas containing allOf: [{"$ref": "version-envelope.json"}]. Public export of AdcpVersionEnvelope should be a separate issue.

    My take: Ask 1 is a spec defect requiring a cross-repo PR in adcontextprotocol/adcp — @bokelley needs to decide and the codegen rerun is mechanical once the 6 schemas are fixed. Ask 2 (ratchet test) ships independently now with no dependency on Ask 1.

    Ready to implement (Ask 2 — ratchet test, non-breaking):

    • New tests/test_version_envelope_inheritance.py, mirroring test_protocol_envelope_inheritance.py
    • Walk schemas/cache/<bundle>/; for each schema whose root allOf contains {"$ref": "version-envelope.json"}, derive the generated_poc module path (hyphen↔underscore), import it, assert issubclass(cls, AdcpVersionEnvelope) (import from generated_poc directly — test code is exempt from the src/adcp/ layering rule)
    • Lower-bound ratchet: assert matched-schema count >= 78
    • Defer AdcpVersionEnvelope public export to a separate issue scoped to adopter-facing API needs

    Needs @bokelley for Ask 1: spec convention decision + spec PR in adcontextprotocol/adcp.


    Triaged by Claude Code. Session: https://claude.ai/code/session_01WCbQx6fe9MT9stUmwRFiVR


    Generated by Claude Code

  4. added
    bugSomething isn't working
    and removed
    claude-triagingTriage routine is actively working on this issue (1-3 min)
    on Oct 2, 2026
  5. added a commit that references this issue on Oct 2, 2026
    4e19467
  6. added a commit that references this issue on Oct 4, 2026
    f4bdda7
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions