Skip to content

feat(creative): account-scoped creative transformers + build_creative multiplicity#5219

Open
bokelley wants to merge 11 commits into
mainfrom
account-specific-user-config
Open

feat(creative): account-scoped creative transformers + build_creative multiplicity#5219
bokelley wants to merge 11 commits into
mainfrom
account-specific-user-config

Conversation

@bokelley
Copy link
Copy Markdown
Contributor

Summary

Adds creative transformers — the creative analog of a media-buy product: an agent-offered, account-scoped, selectable unit of build capability (a voice, model, style, or director) with a typed configuration surface and per-account pricing.

This makes account-specific render configuration — including custom values like cloned voices that exist only for one credential — discoverable from the agent rather than guessed, hung on a global format, or smuggled through ext. The set of knobs and their legal values is account-specific and dynamic, so discovery flows agent → buyer (like get_products), not buyer-held.

Strictly additive. Existing build_creative callers are unaffected: all new request fields are optional, and the shipped BuildCreativeSuccess / BuildCreativeMultiSuccess response shapes are unchanged — a fifth member is added alongside them.

What's in it

New

  • list_transformers (creative protocol) — account-scoped, brief-filterable, paginated discovery. An expand_params mode returns account-scoped enumerable option values (e.g. your configured voices) on the same tool — no separate options endpoint.
  • Core schemas transformer.json, transformer-param.json; x-entity: transformer.
  • get_adcp_capabilitiescreative.supports_transformers discriminator.

build_creative extensions

  • Request: transformer_id, config (typed bag keyed to the transformer's params — agents MUST reject unknown/out-of-range values with field-attributed errors), max_creatives (catalog/item fan-out — N distinct creatives, one per item, with sampling; distinct from item_limit), max_variants + variant_axis + keep_mode.
  • Response: new BuildCreativeVariantSuccesscreatives[] → variants[], a build_variant_id namespace (distinct from preview preview_id and served variant_id), per-leaf pricing receipt, items_total/items_returned. Per-format atomic; per-item non-atomic (a failed item is a creatives[] entry carrying errors[]).
  • Best-of-N = variants + recommended/rank. You pay for all produced variants (per_unit × N); a kept variant lazily earns a creative_id on trafficking, which flows to report_usage.

Pricing rides the existing per_unit model + inline receipt + report_usage — transformers carry pricing_options (reusing vendor-pricing-option.json). No change to those schemas.

Deprecations (deprecated in 3.1, removed at 4.0; SDKs MUST keep honoring them through 3.1–3.x): Format.input_format_ids, Format.output_format_ids, Format.pricing_options, and the input_format_ids/output_format_ids discovery filters on list_creative_formats — all superseded by list_transformers, which carries each transformer's own I/O signature and pricing.

Design notes

  • "variant", not "candidate"variant is already AdCP's word for a kept, independently-tracked execution instance (creative-variant.json, get_creative_delivery creatives[]→variants[], max_variants). Build-time build_variant_id and served variant_id are deliberately separate namespaces.
  • One transformer per call; target format(s) must be a subset of the transformer's output_format_ids.
  • Resolutions/quality are the format axis, not variants (keep-all co-required, not alternatives-to-choose).
  • Execution extends build_creative rather than adding a transform_creative task — build_creative was already ~90% of the surface (manifest in/out, modes, multi-format, async, preview, inline pricing). A canonical-only transform_creative was considered and deferred as a separable canonical-migration cleanup.

Validation

Local gate green: build + build:schemas + build:compliance; schema suite (test:schemas, json-schema, composed, examples, canonical-reference-resolver, oneof-discriminators, platform-agnostic, schema-utf8); test:unit (941), test:server-unit (3958), storyboard raw-mode + upstream; docs-nav (21), snippets, schema-links; typecheck. Changeset included (minor).

This was reviewed adversarially before opening (schema correctness, doc-vs-schema accuracy, spec coherence); confirmed findings are fixed.

🤖 Generated with Claude Code

… multiplicity

Adds creative transformers — the creative analog of a media-buy product:
an agent-offered, account-scoped, selectable unit of build capability (a
voice, model, style, or director) with a typed config surface and per-account
pricing. Makes account-specific render configuration (including custom values
like cloned voices that exist only for one credential) discoverable from the
agent instead of guessed, hung on a global format, or smuggled through ext.

Strictly additive: existing build_creative callers are unaffected (new request
fields optional; shipped BuildCreativeSuccess/BuildCreativeMultiSuccess
unchanged — a fifth response member is added alongside them).

- list_transformers (creative protocol): account-scoped, brief-filterable,
  paginated discovery; an expand_params mode returns account-scoped enumerable
  option values on the same tool.
- Core schemas transformer.json, transformer-param.json; x-entity 'transformer'.
- get_adcp_capabilities -> creative.supports_transformers discriminator.
- build_creative request: transformer_id, config (strict-validated typed bag),
  max_creatives (catalog/item fan-out, distinct from item_limit), max_variants,
  variant_axis, keep_mode.
- build_creative response: BuildCreativeVariantSuccess (creatives[] -> variants[],
  build_variant_id namespace, per-leaf pricing receipt, items_total/returned,
  per-item non-atomic via errors[]).
- Pricing rides the existing per_unit + inline receipt + report_usage rails.
- Deprecates (3.1, removed 4.0; honored through 3.1-3.x) Format.input_format_ids/
  output_format_ids/pricing_options and the list_creative_formats i/o filters.
- Docs: new list_transformers task ref; extended build_creative; deprecation
  callouts; spec, capabilities, required-tasks, whats-new-in-3-1; nav + changeset.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
aao-release-bot[bot]
aao-release-bot Bot previously approved these changes May 31, 2026
Copy link
Copy Markdown
Contributor

@aao-release-bot aao-release-bot Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving. Strictly additive on the wire — existing BuildCreativeSuccess / BuildCreativeMultiSuccess are byte-identical, all new request fields are optional, the 5th success branch keys off a unique required creatives and is excluded from both rejecting branches (BuildCreativeError.not.anyOf and BuildCreativeSubmitted.not.anyOf) so the existing exclusion lattice is preserved. minor is the right changeset bump.

Things I checked

  • 5th oneOf branch on static/schemas/source/media-buy/build-creative-response.json is discriminated by creatives (unique required key). Both BuildCreativeError.not.anyOf and BuildCreativeSubmitted.not.anyOf are patched to exclude creatives — symmetric and intentional. No new undiscriminated oneOf introduced.
  • static/schemas/source/core/transformer.json cardinality: output_format_ids minItems: 1 (a transformer with no outputs is meaningless), optional input_format_ids correctly models pure-generation transformers, required keys are [transformer_id, name, output_format_ids].
  • static/schemas/source/core/transformer-param.json value_source enum (inline | range | enumerable) + descriptor-only presence rules for allowed_values / minimum-maximum / options[]. Consistent with how AdCP handles other conditional descriptor shapes.
  • transformer.pricing_options reuses vendor-pricing-option.json — pricing rides the existing per_unit + receipt + report_usage rails unchanged.
  • x-entity: transformer registered in core/x-entity-types.json with description shape matching vendor_pricing_option / creative_format.
  • creative.supports_transformers on get-adcp-capabilities-response.json:1094 defaults to false, and docs/protocol/required-tasks.mdx correctly marks list_transformers as Conditional gated on that flag.
  • list-transformers-request.json if/then (require account when include_pricing: true) is valid draft-07 / Ajv syntax nested inside allOf.
  • Format.input_format_ids/output_format_ids/pricing_options and the list_creative_formats discovery filters are marked deprecated: true with "honor through 3.1–3.x, removed at 4.0" prose — proper deprecation hygiene, no removal in 3.1.
  • Doc/schema field parity on list_transformers.mdx and the "Variant response" section of build_creative.mdx against the underlying schemas — names, types, requirement levels match.
  • Changeset .changeset/creative-transformers.md exists, declares minor, accurately describes the additive surface.

Follow-ups (non-blocking — file as issues)

  • Baseline refresh on scripts/oneof-discriminators.baseline.json. The recorded note for media-buy/build-creative-response.json##/oneOf is variants: 4 and lists the four pre-existing branches; adding the 5th creatives branch is an improvement-or-equal transition (still narrowable, still uniquely keyed), but --check will see a delta on that row. Run npm run audit:oneof -- --update and commit the regenerated baseline — no --accept-new needed, no regression.
  • Missed deprecation callout at docs/creative/template-format-ids.mdx:292-315. The "Generative Formats with Output Formats" section still describes Format.output_format_ids as the canonical way to express generative output capability, with no "deprecated in 3.1, see list_transformers" callout. The cousin pages (list_creative_formats.mdx, format.json, whats-new-in-3-1.mdx) all got the callout — this one didn't.
  • docs/creative/implementing-creative-agents.mdx:664 declares a supports_transformation: true discriminator. The new creative.supports_transformers is one letter and a noun-form away. One of them deserves a disambiguating sentence — supports_transformation is the "I can resize/reformat a manifest via build_creative" boolean; supports_transformers is the "I expose the new account-scoped transformer surface" boolean. Easy to confuse on grep; worth a cross-reference in both spots.
  • Asymmetric deprecation wording. static/schemas/source/core/format.json lines 557 / 565 / 671 say "DEPRECATED in 3.1. Removed at 4.0.". static/schemas/source/creative/list-creative-formats-request.json lines 113 / 122 say "DEPRECATED in 3.1." only. Make the lifecycle string match across all three surfaces.
  • build_creative_id / build_variant_id aren't registered as x-entity. Today fine — no schema consumes them, so the entity-id gap report won't fire. The moment a trafficking task accepts a build_variant_id on the wire, the lint will flag it. Either register them now or drop a $comment on each leaf noting "x-entity deliberately omitted — build-time handle, lazily promoted to creative_id on trafficking".

Minor nits (non-blocking)

  1. creatives[].items.anyOf allows both variants[] and errors[]. static/schemas/source/media-buy/build-creative-response.json:1097-1101 uses anyOf: [{required: [variants]}, {required: [errors]}] plus additionalProperties: true. The prose at the same level says a successful entry "SHOULD NOT carry errors" and a failed entry has "no variants". Schema is consistent with the RFC 2119 SHOULD reading. If you wanted MUST-level mutual exclusion, switch to oneOf with mutual not: clauses. Author's call.
  2. Sample-code drift from repo idiom. The two new CodeGroup blocks in build_creative.mdx and list_transformers.mdx use ```javascript test=false / ```python test=false. Established pattern across the rest of the creative task-reference is ```javascript JavaScript test=false / ```python Python test=false (the human-readable tab label as the second token). Also: the new Python samples use from adcp import …; established convention is from adcp.types import …. JS samples skip the if (!result.success) throw … / result.data unwrap that every other sample uses. Field names themselves are clean — match the schemas exactly.
  3. Normative claim only in docs. docs/creative/task-reference/list_transformers.mdx:417 asserts "Unknown expand_params (a field no transformer exposes) are ignored, not an error". That behavior isn't in the request-schema description. Either reflect it in the schema, soften to SHOULD, or both — wire-level normative claims belong on the schema first.
  4. variant_axis_value has no type at static/schemas/source/media-buy/build-creative-response.json:1054-1056. Intentional (string for voices/themes, number/boolean for swept config params), and matches the heterogeneous-default pattern in transformer-param.json. A one-line $comment noting the intentional omission would keep a future reviewer from "fixing" it.

The PR body says this was reviewed adversarially before opening; the template-format-ids.mdx and implementing-creative-agents.mdx gaps suggest the adversarial pass didn't quite cross-reference the deprecated field's cousin pages. Worth one more grep in follow-up.

LGTM. Follow-ups noted below.

@bokelley
Copy link
Copy Markdown
Contributor Author

Follow-on plan: the creative "back half" (and one decision worth making in this PR)

This PR nails the front half — discover → configure → produce → multiply. Sketching the back half (refinement, the generation→outcome learning loop, post-synthesis QA, composite/A2A orchestration) against 3.1.0-rc.4 to see what it costs: almost all of it is additive fields on existing tools, and all four hang off one primitive this PR introduces. Two things below — one decision to make here, one small fold-in that can ride this PR. The rest are sequenced follow-ons.

1. Decision to make in this PR: pin build_variant_id as the leaf-level anchor

All four back-half capabilities key on the per-variant build leaf, not the call:

  • refinement → refine_from a prior leaf;
  • learning → the build→delivery→feedback join is the leaf;
  • QA → re-roll inputs / superseded variants are leaves;
  • composite → each sub-build leg echoes the sub-agent's own leaf.

So it's worth stating normatively, here:

  • build_variant_id is x-entity: build_variant, minted per produced variant, distinct from served variant_id and preview_id, and lazily earns a durable creative_id only on trafficking.
  • Lineage anchors on the leaf (build_variant_id), not the call (build_creative_id). If it anchors on the call, every back-half reference/join has to be re-pointed later.
  • Billing: report_usage.usage[] keys on creative_id and has no build_variant_id slot. So an untrafficked leaf (a refinement, a discarded re-roll, a composite sub-build) has no report_usage key. Cleanest rule: untrafficked leaves are billed via the inline response vendor_cost only; report_usage reconciliation applies once a leaf earns a creative_id on trafficking. (No build_variant_id needed on report_usage.)

This is the precondition for everything else, and it's cheapest to get right now.

2. Smallest fold-in (could ride this PR): conversational refinement

The closed, typed config surface is exactly what makes free-form refinement ("make it warmer") not fit a param. It needs an open surface — but the open surface already exists: the message field, whose description already says "For refinement, this describes the desired changes." So refinement is ~three small additive bits, not a new tool:

build-creative-request.json — one new optional property:

"refine_from_build_variant_id": {
  "type": "string",
  "x-entity": "build_variant",
  "description": "Refine a prior produced variant: re-build from the referenced build_variant_id, applying the NL instruction in `message` and any `config` delta, returning a NEW lineage-linked build_variant_id (never a mutation). Requires the `refinement` capability. Stateless agents that don't retain builds reject with UNSUPPORTED_FEATURE; expired/unknown ref is REFERENCE_NOT_FOUND (details.entity='build_variant'). To refine a buyer-held manifest with no agent retention, use the existing transform path (`creative_manifest` + `message`)."
}

enums/creative-agent-capability.json — append refinement (gates agent-side retention of prior builds; TTL = the build's expires_at; independent of generation/transformation).

Response (trailing, once variants[] lands here): add parent_build_variant_id to each variant leaf; AI-derivative provenance rides the existing provenance.human_oversight.

No new NL field, no new error codes (REFERENCE_NOT_FOUND is the spec-MUST for a missing typed ref), no pricing change (a refinement is just another billed leaf).

3. The rest, sequenced as follow-ons (with the gotchas I'd flag now)

  • Learning join — one optional build_variant_id on core/creative-variant.json makes build→delivery an exact 1:1 join. (Don't route creative outcomes through provide_performance_feedback — it's media-buy-scoped, no account, deprecated metric_type; and don't put priors on discovery — they duplicate get_creative_features. Feeding outcomes into recommended/rank stays agent-internal.)
  • Post-synthesis QA — an optional qa block + per-leaf verdict, annotate-only first, then a budgeted reroll{} block. Mirror content-standards/validate-content-delivery-response's {verdict, features[]} shape — do NOT allOf-extend creative-feature-result.json, it's additionalProperties: false.
  • Composite/A2A orchestration — last, because it needs this PR to first establish per-variant non-atomic success semantics (today BuildCreativeMultiSuccess is atomic and success-XOR-errors is enforced via not). Then: an itemized sub_builds[] receipt tree + the one genuinely new rule — the buyer credential authenticates only the buyer↔orchestrator hop; the orchestrator holds its own sub-agent creds.

Happy to open the refinement fold-in as commits on this branch if you want it in here, or as a separate follow-on PR.

Hardens the build_variant_id this PR already introduces, per the back-half
follow-on plan (#5219 review): it is the leaf-level anchor everything downstream
(refinement parentage, build→delivery learning, QA re-rolls) keys on, so getting
its identity and billing semantics right now avoids re-pointing later.

- Register x-entity `build_variant`; tag the build_variant_id property with it.
- State normatively: minted per produced variant; distinct from served
  variant_id and preview_id and from the call-level build_creative_id; lineage
  anchors on the leaf, not the call.
- Billing: an untrafficked leaf has no report_usage key — billed via the inline
  per-leaf vendor_cost only; report_usage reconciliation applies once the leaf
  lazily earns a creative_id on trafficking.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
aao-release-bot[bot]
aao-release-bot Bot previously approved these changes May 31, 2026
Copy link
Copy Markdown
Contributor

@aao-release-bot aao-release-bot Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving. Adding a fifth oneOf branch keyed by a uniquely-required field (creatives) preserves the four shipped success/error/submitted contracts byte-for-byte, and deprecate-not-remove on the format-attached fields is exactly the right shape for a minor.

Things I checked

  • The new BuildCreativeVariantSuccess member is discriminated by required: [creatives] — disjoint from creative_manifest/creative_manifests/errors/status='submitted'+task_id. BuildCreativeError and BuildCreativeSubmitted not.anyOf blocks both got the {required: [creatives]} entry — five-way exclusivity holds (static/schemas/source/media-buy/build-creative-response.json ~L447, ~L515).
  • x-entity taxonomy in static/schemas/source/core/x-entity-types.json: transformer and build_variant added to enum[] AND x-entity-definitions{}. The build_variant description correctly distinguishes leaf lineage from delivery variant_id, preview preview_id, and the call-level grouping build_creative_id. Right shape for the lazy-promotion contract.
  • Deprecation pattern on Format.input_format_ids, Format.output_format_ids, Format.pricing_options, and the two list_creative_formats filters: "deprecated": true + on-the-wire retention + explicit 3.1→4.0 removal window + per-field migration prose pointing at list_transformers. Matches the prior 3.x deprecation cadence.
  • Doc/schema coherence: variant_axis.dimension enum (voice|theme|best_of_n|transformer_config|custom), keep_mode enum (keep_all|keep_one|keep_some), expand_params inline params[].options[] semantics, and the include_pricing → require account allOf if/then all match between docs and schemas.
  • creative.supports_transformers is the right pre-call discriminator on get_adcp_capabilities and orthogonal to creative.supported_formats[] — formats describe canonical outputs, transformers describe build capability + configuration. Pre-call discrimination is how agentic clients prune routing without speculative calls.
  • list_transformers is correctly listed as Conditional in docs/protocol/required-tasks.mdx ("Required when the agent advertises creative.supports_transformers: true").
  • transformer.json requires output_format_ids with minItems: 1 — consistent with the "target MUST be a subset" rule (you cannot be a subset of zero items).

Follow-ups (non-blocking — file as issues)

  • Per-creative anyOf vs oneOf on BuildCreativeVariantSuccess.creatives[].items. The schema is anyOf: [{required:[variants]}, {required:[errors]}] — permits an entry carrying both variants and errors. The field-level prose says a successful entry "SHOULD NOT carry errors," which is consistent with anyOf. Worth pinning explicitly: either tighten to oneOf if a kept-and-warned item is illegal, or call out in the description that advisory warnings on a successful item are legal. Today the schema enforcement strength is slightly below the prose's apparent intent.
  • scripts/oneof-discriminators.baseline.json metadata is stale after merge. The baseline records variants: 4 and a 4-branch note for media-buy/build-creative-response.json##/oneOf. audit-oneof.mjs --check keys on file#pointer and doesn't compare counts, so the gate stays green — but the snapshot is now wrong. Refresh with node scripts/audit-oneof.mjs --update.
  • Per-leaf pricing receipt fields are described but not required. pricing_option_id, vendor_cost, currency, consumption on each variants[] entry are framed as carried per-leaf yet a paid agent could omit them with no schema-level error. Same normative-only pattern as config validation — acceptable, but a paid build returning leaves without vendor_cost is a wire bug the schema won't catch.

Minor nits (non-blocking)

  1. transformer-param.json value_source lacks an if/then. inline doesn't require allowed_values; range doesn't require minimum/maximum; enumerable doesn't require options[]. Consistent with how other AdCP discriminators handle soft cross-field constraints, but core/product.json has a precedent for the tighter shape — flagging only so the asymmetry is a deliberate call.
  2. build_creative now has five new optional request knobs (transformer_id, config, max_creatives, max_variants, variant_axis, keep_mode), one of which (variant_axis.field) is required only when dimension == \"transformer_config\". That kind of conditional belongs in the description prose where agentic callers can actually read it — worth a follow-up pass on the request-field descriptions once SDK usage shakes out.

Approving.

Folds the back-half refinement capability into #5219 (per the follow-on plan).
Refinement re-builds a prior produced variant with free-form direction via the
open `message` field; it is build_creative + one input, not a new task.

- Request: refine_from_build_variant_id (re-build from a prior leaf, NL message
  + config delta, NEW lineage-linked variants; never a mutation). transformer_id
  and target inherited from the parent; a differing value is INVALID_REQUEST.
  Composes with max_variants/variant_axis; mutually exclusive with max_creatives.
  Unknown/no-longer-retained ref -> REFERENCE_NOT_FOUND, error.field=refine_from_build_variant_id.
- Capability: creative.supports_refinement (agent retains leaves for an
  agent-defined window) -- a get_adcp_capabilities boolean, consistent with
  supports_transformers/generation/transformation.
- Response: parent_build_variant_id on the variant leaf (single parent -> chain);
  AI-derivative attribution rides the manifest's existing provenance.
- Docs + capability row + changeset updated. Adversarially reviewed; fixed 7
  findings (dangling per-leaf expires_at; details.entity -> error.field;
  transformer/target inheritance conflict; max_creatives back-ref; example fix;
  four-modes title).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
aao-release-bot[bot]
aao-release-bot Bot previously approved these changes May 31, 2026
Copy link
Copy Markdown
Contributor

@aao-release-bot aao-release-bot Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving. Transformer-as-the-creative-analog-of-a-product is the load-bearing call, and the per-leaf billing receipt with lazy creative_id promotion is what makes "pay for all produced, keep what you traffic" machine-readable.

Things I checked

  • The oneOf on build-creative-response.json stays disjoint with the fifth member: BuildCreativeError and BuildCreativeSubmitted both add creatives to their not.anyOf blocks. BuildCreativeSuccess and BuildCreativeMultiSuccess are byte-identical — no wire impact for existing callers.
  • Capability gating matches prior art: creative.supports_transformers and creative.supports_refinement in get-adcp-capabilities-response.json follow the same boolean / default false / UNSUPPORTED_FEATURE on negative gate shape as bills_through_adcp and has_creative_library.
  • x-entity registry: transformer and build_variant added to core/x-entity-types.json with crisp definitions and applied at every load-bearing site (transformer_id, build_variant_id, parent_build_variant_id, refine_from_build_variant_id).
  • Deprecations on Format.input_format_ids / output_format_ids / pricing_options carry real "deprecated": true JSON Schema markers in core/format.json plus matching prose in list_creative_formats.mdx and whats-new-in-3-1.mdx. 3.1-deprecate / 4.0-remove ratchet matches the canonical_parameters precedent in the same file.
  • Changeset (minor) matches wire impact: every new request field is optional, the two shipped success members are unchanged, the new fifth member is purely additive.
  • list-transformers-request.json enforces include_pricing == true → required: ["account"] via allOf.if/then — that conditional gate is machine-checkable, not prose-only.
  • Schema-vs-docs: every field documented in list_transformers.mdx and the new build_creative.mdx sections exists in the schema with the documented type; reverse direction also clean.

Follow-ups (non-blocking — file as issues)

  • build_creative_id is untyped. Its description in build-creative-response.json explicitly says this is its own namespace (not creative, not build_variant), but no x-entity annotation. Either add a build_creative entity type to x-entity-types.json or document why this grouping id is intentionally untyped — the namespace separation the PR description calls airtight can't be lint-enforced otherwise.
  • variant_axis.field conditional REQUIRED is prose-only. When dimension == "transformer_config", field is REQUIRED per description, but the schema only required: ["dimension"]. list-transformers-request.json uses allOf.if/then for include_pricing → account in this same PR — apply the same pattern here so a buyer sending {dimension: "transformer_config"} with no field doesn't pass validation silently.
  • Refinement composability is prose-only. refine_from_build_variant_id MX with max_creatives and inheritance of transformer_id / target format from the parent are described but not schema-enforced. Buyers will get refinement composability wrong; an if-then-not block would catch it pre-request.
  • Error-code drift in list_transformers.mdx. Docs cite AUTHENTICATION_REQUIRED and INVALID_INPUT in the Error Handling table. Neither is in enums/error-code.json. The rest of the PR uses canonical codes (UNSUPPORTED_FEATURE, REFERENCE_NOT_FOUND, INVALID_REQUEST). Either promote the two missing codes to the registry or rewrite the docs to use canonical ones.
  • BuildCreativeError description is now incomplete. It still says "multi-format requests are atomic" without acknowledging BuildCreativeVariantSuccess's per-item non-atomic catalog fan-out (where failed items live inside creatives[].errors[] and the batch still succeeds). Tighten the wording.
  • Refinement discoverability is weak. refine_from_build_variant_id is a powerful primitive buried as a build_creative param. Surfacing refinable: true on each variant leaf in BuildCreativeVariantSuccess would give an agent the affordance after the first build instead of forcing it to re-read the tool description.

Minor nits (non-blocking)

  1. Real brand name in example domains. https://creative.audiostack.example appears across list_transformers.mdx, build_creative.mdx, and transformer.json examples. Audiostack is a real ad-tech vendor; the playbook rule (.agents/playbook.md: "No Real Brands or Agencies in new examples") is explicit. Swap for fictional (novavoice.example, pinnacleaudio.example). acct_acme is fine.
  2. testable: true frontmatter, all examples test=false. list_transformers.mdx:5. A no-account list_transformers with a transformer_ids filter is genuinely testable (account is only required with include_pricing). Either wire one truly testable example or drop the frontmatter flag.
  3. transformer.json requires output_format_ids but the docs table doesn't mark it required. list_transformers.mdx's "Each transformer carries" table. Add a Required column or annotate inline so SDK generators don't emit the field as optional.
  4. transformer-param.json has a property named "required". Boolean field collides visually with the standard JSON Schema required keyword. Valid but reader-hostile — is_required or mandatory would avoid the collision.
  5. config open-bag with normative strict validation is the right call, but the build_creative tool description could push harder against hallucination: "MUST call list_transformers before setting transformer_id/config; do not guess." LLM clients are the realistic offenders.

Safe to merge.

… codes/report_usage contradiction

Persona/scenario review of #5219 flagged the migration surface as warranted by
the repo's own convention (media-buy-status.mdx exists for a single-field
deprecation; this PR deprecates a larger surface through a new task).

- New docs/reference/migration/creative-transformers.mdx (mirrors media-buy-status
  structure): Format.input_format_ids/output_format_ids/pricing_options + the
  list_creative_formats discovery filters -> list_transformers; before/after;
  SDK honor-window table; and the three SILENT-degradation hazards (discovery-read
  going progressively empty, multi-publisher per-output pricing loss, best-of-N
  spend under-counting). Wired into both current docs.json migration groups and
  linked from the whats-new Warning.
- Fix non-canonical error codes in list_transformers.mdx (AUTHENTICATION_REQUIRED
  -> AUTH_MISSING, INVALID_INPUT -> INVALID_REQUEST; verified against error-code.json).
- Resolve the report_usage.mdx contradiction: a build_variant leaf earns a
  creative_id only on trafficking; discarded fan-out/best-of-N leaves are billed
  via the inline per-leaf vendor_cost, not report_usage.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
aao-release-bot[bot]
aao-release-bot Bot previously approved these changes May 31, 2026
Copy link
Copy Markdown
Contributor

@aao-release-bot aao-release-bot Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving on the strength of the additive shape plus the migration guide's silent-degradation flags. Transformers as "the creative analog of a product" is the right framing — account-scoped, agent-held, brief-curated discovery is the same flow as get_products, and it lands the typed-config-vs-ext separation that's been overdue.

Things I checked

  • BuildCreativeVariantSuccess as a 5th oneOf member is disjoint. Required-field discrimination via creatives is sufficient against the existing four branches; BuildCreativeError and BuildCreativeSubmitted not.anyOf blocks correctly add {required:["creatives"]} (static/schemas/source/media-buy/build-creative-response.json:452,520). BuildCreativeSuccess and BuildCreativeMultiSuccess rely on the existing required-field pattern — consistent with the current shape.
  • oneOf baseline does not fail CI. scripts/audit-oneof.mjs --check compares STATUS_RANK[kind], and the 5-variant form classifies as narrowable — same rank as the 4-variant baseline at scripts/oneof-discriminators.baseline.json:254-258. PR description's local-pass claim holds. (Metadata drift noted below.)
  • Changeset is minor and correct. Existing BuildCreativeSuccess / BuildCreativeMultiSuccess shapes are untouched; new branch is opt-in via max_creatives / max_variants / transformer_id; deprecated Format fields are deprecated: true not removed and migration text states 4.0 removal explicitly. No required→optional flips on existing fields; no enum value removals; no response-shape break for 3.0 buyers.
  • Schema ↔ docs are coherent. variant_axis.dimension enum (voice | theme | best_of_n | transformer_config | custom), keep_mode (keep_all | keep_one | keep_some), value_source (inline | range | enumerable), catalog_item_ref.required: ["item_id"], and the supports_transformers / supports_refinement capability descriptions all match across transformer.json, transformer-param.json, build-creative-request.json, build-creative-response.json, get-adcp-capabilities-response.json and their docs counterparts.
  • item_limit vs max_creatives distinction is explicit. Schema description for item_limit updated to call out DCO-style within-one-creative, distinct from max_creatives fan-out (static/schemas/source/media-buy/build-creative-request.json:127) — same distinction in the build_creative.mdx field table. Real source of historical confusion; clean fix.
  • Three namespace separations land. build_variant_id / preview_id / served variant_id are documented in build-creative-response.json (build_variant_id field), x-entity-types.json build_variant entry, and the build_creative.mdx Variant-response section. Cross-checked against core/creative-variant.json — no collision.
  • Migration guide silent-degradation hazards are concrete. Discovery-read drift, per-output pricing loss on naïve Format.pricing_optionstransformer.pricing_options lift, and best-of-N spend under-counting via report_usage-only pipelines — all three name the failure mode and the recovery action. The report_usage.mdx edit aligns with hazard #3.
  • x-entity-types.json registration. transformer and build_variant added with descriptors consistent with the existing dictionary style.
  • Migration page is in nav. docs.json adds docs/reference/migration/creative-transformers to both nav groups (top + sidebar copy) and docs/creative/task-reference/list_transformers to both task-reference groups.

Follow-ups (non-blocking — file as issues)

  1. build_creative_id on BuildCreativeVariantSuccess.creatives[] is a documented identifier with no x-entity annotation. The docs and the field description treat it as a stable handle ("Build-time handle for this produced creative within this response") but the schema treats it as a bare optional string, and the entity isn't registered in core/x-entity-types.json. Either annotate as x-entity: "build_creative" and register the entity (parallel to how build_variant was just added), or downgrade the prose to make clear it's a within-response correlation key only. static/schemas/source/media-buy/build-creative-response.json:1187.
  2. Refresh the oneOf baseline. node scripts/audit-oneof.mjs --update will bump variants: 4 → 5 and refresh the note for media-buy/build-creative-response.json##/oneOf at scripts/oneof-discriminators.baseline.json:254-258. Not a CI blocker (kind stays narrowable), but the recorded note will drift from reality until refreshed and confuse the next reader of that file.
  3. docs/creative/implementing-creative-agents.mdx still tells creative-agent builders to expose pricing_options on list_creative_formats (~L86-145). That is the deprecated surface the migration guide steers off. Add a <Warning> at the top of the relevant section pointing transformation/generation agents at list_transformers. Implementer-persona page; not in this PR's diff, hence follow-up.
  4. Schema-level mutex enforcement of refine_from_build_variant_id × max_creatives. Currently prose-only ("mutually exclusive with max_creatives / catalog fan-out"). The existing target_format_idtarget_format_ids mutex is structural; the new pair could mirror it via allOf: [{not: {required: ["refine_from_build_variant_id", "max_creatives"]}}] and catch the buyer-side mistake at validation time. Symmetry, not correctness.
  5. transformer-param.json allowed_values.items and default are untyped. Low-priority polish — values should match the param's type. A if/then on type would tighten without much weight, but the strict-validation contract is normative anyway.
  6. docs/creative/generative-creative.mdx doesn't mention transformers. For a doc walking adopters through generative build_creative, a one-sentence pointer ("If your agent advertises supports_transformers, discover via list_transformers") would close the discoverability gap. Lighter follow-up.

LGTM. Follow-ups noted below.

bokelley and others added 2 commits May 31, 2026 13:09
From the persona/scenario review of #5219:

1. Response dispatch is unmissable. BuildCreativeVariantSuccess now states
   normatively that sending max_creatives / max_variants>1 / variant_axis /
   refine_from returns creatives[] with NO fallback to creative_manifest(s);
   a <Warning> mirrors it in build_creative.mdx.
2. Request exclusivity is schema-enforced (was prose-only): allOf `not`
   guards for target_format_id XOR target_format_ids, and
   refine_from_build_variant_id NOT-with max_creatives — mirroring the
   include_pricing->account if/then pattern in the sibling request schemas.
3. Per-param option pagination is consumable: list_transformers gains
   expand_pagination [{transformer_id, field, options_cursor}] so the
   options_cursor the response promises can be passed back, scoped per
   (transformer, param). expand_params now returns the first page.
4. Single builds are refinable: BuildCreativeSuccess carries an optional
   build_variant_id (when the agent supports refinement) usable as
   refine_from_build_variant_id; multi-format builds refine via the variant
   shape (documented).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
aao-release-bot[bot]
aao-release-bot Bot previously approved these changes May 31, 2026
Copy link
Copy Markdown
Contributor

@aao-release-bot aao-release-bot Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving. Strictly additive shape, the right analog (transformer ↔ media-buy product), and the migration doc names the silent-degradation hazards explicitly — that's the work.

Things I checked

  • oneOf still discriminates cleanly across all 5 members. Required-field sets are disjoint: creative_manifest / creative_manifests / creatives / errors / status='submitted' + task_id. BuildCreativeError.not.anyOf and BuildCreativeSubmitted.not.anyOf both add creatives to their exclusion lists (build-creative-response.json:460,528). Mechanical discrimination holds.
  • New entities registered. transformer and build_variant are in both the enum and x-entity-definitions (x-entity-types.json:21-22, 57-58). build_variant's definition correctly carves it out of variant_id (delivery) and preview_id (preview) namespaces.
  • Error codes already exist. UNSUPPORTED_FEATURE and REFERENCE_NOT_FOUND are in enums/error-code.json; no enumMetadata changes needed.
  • Changeset is right. minor matches additive new tasks/fields + deprecated: true flags with no constraint changes on the deprecated fields.
  • Migration doc. Covers all three deprecated surfaces (Format I/O, Format.pricing_options, list_creative_formats filters) and the three silent-degradation hazards (discovery-read decay, per-output pricing loss, fan-out spend under-counting). 'The field outlives its data' is the right framing.

Follow-ups (non-blocking — file as issues)

  • Response shape has no discriminator literal. BuildCreativeVariantSuccess is discriminated by request inputs (max_variants > 1, max_creatives, variant_axis, refine_from_build_variant_id) rather than a tagged response field. SDK parsers using Zod discriminatedUnion / Pydantic Field(discriminator=) fall back to untagged unions — works, but worse error messages and slower parsing. A response_shape: \"variant\" literal costs one field and earns clean discrimination across every SDK. Worth doing before 3.1 GA.
  • expand_params + expand_pagination is two shapes for one concept. expand_params: string[] (first page) vs expand_pagination: { transformer_id, field, options_cursor }[] (next page) doubles the schema surface. A single expand: { field, transformer_id?, options_cursor? }[] array would carry both cases. Agents will reliably get step 1 and unreliably get step 2.
  • transformer-param.json doesn't enforce conditional requirements. value_source: \"inline\" ⇒ allowed_values required, value_source: \"range\" ⇒ minimum/maximum required are description-only. allOf + if/then per value_source value would catch malformed transformer descriptors at validation time instead of at config-validation time.
  • whats-new-in-3-1.mdx is silent on refinement. The schema and build_creative.mdx both ship refine_from_build_variant_id / supports_refinement / parent_build_variant_id in this PR, but the 3.1 release notes don't mention it. Easy to fix.
  • list_transformers.mdx 'Each transformer carries' table omits description and metadata. Both are defined in transformer.json (transformer.json:13-21) but missing from the docs response table.
  • Variant response section in build_creative.mdx doesn't document sandbox / expires_at. Both fields exist on BuildCreativeVariantSuccess (build-creative-response.json:411-416) and are documented for the single/multi shapes — parity gap.
  • target_format_id / target_format_ids mutex is now schema-enforced. Docs always said exclusive; the schema didn't. The new not.required in allOf (build-creative-request.json:11-14) tightens a docs-only rule into a validation rule. Strictly a fix, but the changeset should call this out — a non-conforming client previously sending both now fails validation on a minor bump.

Minor nits (non-blocking)

  1. build_creative_id lacks x-entity annotation. build_variant_id and transformer_id carry the annotation; the call-level build_creative_id on BuildCreativeVariantSuccess.creatives[] doesn't (build-creative-response.json variant block). Either register a build_creative entity or leave it un-annotated by design — current state is inconsistent.
  2. Per-item success/failure is anyOf, not oneOf. creatives[].items.anyOf: [{required:[variants]}, {required:[\"errors\"]}] (build-creative-response.json:399-402) permits both simultaneously. Prose says "a failed entry carries errors[] and no variants[]" — oneOf would enforce that; anyOf makes it SHOULD-level.
  3. keep_mode naming. "Advisory" + "mode" reads like it changes behavior; it doesn't. keep_intent or expected_keep_count would land more honestly.
  4. transformer-param.json has a required property (boolean). It's namespaced under properties, so technically fine, but mildly confusing alongside the JSON Schema required keyword.

Ship it.

…ty discriminators

Follow-on #3 (keystone) from the persona/scenario review: give buyers pre-call
discriminators so they know what an agent supports before sending, rather than
probing and handling failures. Additive/optional; the surface the spend-control
and conformance follow-ons read.

- get_adcp_capabilities creative.refinable_retention_seconds (guaranteed-minimum
  refinable window — machine-readable floor for the prior prose-only "agent-defined
  window").
- get_adcp_capabilities creative.multiplicity {supports_catalog_fanout +
  max_creatives_limit, supports_variants + max_variants_limit, variant_dimensions[]}.
  Over-limit fan-out is clamped (items_returned < items_total), not rejected.
- transformer.multiplicity narrows the agent-level object per transformer
  (ceilings <= agent, dimensions subset of agent).
- build_creative docs note the clamp behavior.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…te dry-run)

Follow-on #1 (the highest-risk gap from the persona review): fan-out
(max_creatives × max_variants) + refinement produce many independently-billed
leaves, and per_unit gives a rate but not the unit count up front — so an
autonomous buyer had no protocol brake on spend. Both additions optional, gated
by creative.supports_spend_controls.

- Request mode:"estimate" -> new BuildCreativeEstimate response member (6th oneOf
  arm): dry run, produces/bills nothing, returns a cost_low/cost_high band with
  basis (fixed/estimated_units/cpm_deferred) computed against the request inputs.
  Advisory in this revision.
- Request max_spend {amount,currency}: hard per-call ceiling. Agent stops before
  the next leaf would exceed it and returns the partial BuildCreativeVariantSuccess
  with budget_status:"capped" + advisory BUDGET_CAP_REACHED in errors[] (every
  returned leaf real/billed; items_returned < items_total). First-leaf-over-cap ->
  terminal BUDGET_CAP_REACHED; currency mismatch -> INVALID_REQUEST.
- New error code BUDGET_CAP_REACHED (enum + enumDescriptions + enumMetadata +
  drift disposition held-for-next-minor/3.1), distinct from BUDGET_EXCEEDED/EXHAUSTED.
- New capability creative.supports_spend_controls (default false).

Deferred to WG: binding estimates; refinement-LOOP bound as protocol session
budget vs buyer responsibility (documented buyer-side for now).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@aao-release-bot aao-release-bot Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving. Strictly-additive surface, deprecations marked symmetrically across core/format.json, list-creative-formats-request.json, and the migration doc, and the new entity-types (transformer, build_variant) are registered in x-entity-types.json alongside their actual x-entity usages. The architectural call is right: build capability is a property of the selectable unit, not a relationship hung on a format.

Things I checked

  • transformer and build_variant are both in static/schemas/source/core/x-entity-types.json enum + x-entity-definitions, and match the x-entity annotations on build-creative-request.json (transformer_id, refine_from_build_variant_id) and build-creative-response.json (build_variant_id, parent_build_variant_id).
  • static/schemas/source/index.json registers core/transformer.json, core/transformer-param.json, and the list-transformers request/response under the creative protocol.
  • Variant-axis enum is identical across the three sites it shows up: request (variant_axis.dimension), per-transformer narrowing (transformer.multiplicity.variant_dimensions), and agent-level capability (creative.multiplicity.variant_dimensions) — voice | theme | best_of_n | transformer_config | custom.
  • transformer.json example validates: pricing_options[0] satisfies vendor-pricing-option.json PerUnitPricing (model/unit/unit_price/currency all present); input_format_ids / output_format_ids use the {agent_url,id} form.
  • BuildCreativeError and BuildCreativeSubmitted not.anyOf clauses have been updated to also exclude creatives — so the new BuildCreativeVariantSuccess branch is fenced off from those two.
  • Both changesets are minor and the surface is additive (new optional request fields, new oneOf branch alongside the existing two success shapes). Correct classification.
  • Docs nav: docs.json wires up both docs/creative/task-reference/list_transformers and docs/reference/migration/creative-transformers in current and 3.0-compat navs.

Follow-ups (non-blocking — file as issues)

  • creatives[] per-item anyOf doesn't enforce the prose. build-creative-response.json BuildCreativeVariantSuccess creatives.items uses anyOf: [{required:["variants"]}, {required:["errors"]}]. The field prose says a failed entry carries errors[] and no variants[], and a successful entry carries variants[] and SHOULD NOT carry errors. anyOf lets both coexist. Tighten to oneOf (or add a not clause) so the schema enforces what the prose mandates.
  • Asymmetric not.anyOf on the five-branch oneOf. BuildCreativeError and BuildCreativeSubmitted both carry not.anyOf against the success shapes; BuildCreativeSuccess, BuildCreativeMultiSuccess, and BuildCreativeVariantSuccess do not. Exactly-one-of still works on required-field uniqueness alone (the existing pattern), but defense-in-depth would add symmetric not clauses on the three success branches. Cheap fix, removes the foot-gun for sellers who accidentally emit both creative_manifest and creatives on the same response.
  • scripts/oneof-discriminators.baseline.json is stale. Records build-creative-response.json##/oneOf as variants: 4 with a 4-item note line; the schema now has 5. The --check mode passes (status rank narrowable is unchanged) but a follow-up --update run is in order so the baseline notes the new branch.
  • variant_axis_value is type-less. build-creative-response.json BuildCreativeVariantSuccess creatives[].variants[].variant_axis_value declares only a description — anything validates, including null. Constrain to ["string","number","boolean"] (matching the legal config value types) unless the openness is deliberate.
  • Per-output pricing doesn't survive the move unchanged. The migration doc calls this out — multi-publisher templates that priced outputs differently must split into per-price-point transformers. Reasonable for a 3.1 cleanup; worth a follow-up RFC on whether transformer.pricing_options[*].applies_to_format_ids[] is the right escape hatch for the resize/reformat case where one transformer naturally fans out to many output formats at different prices.
  • Spend-control vs. `keep_mode`. Buyer pays for every produced variant regardless of keep_mode. No ex-ante cap. Consider a max_billable_variants companion, or a stronger normative statement that max_variants IS the spend cap.

Minor nits (non-blocking)

  1. expand_params / expand_pagination is two parallel pagination dimensions. list-transformers-request.json — first page names a field in expand_params; subsequent pages need a {transformer_id, field, options_cursor} triple in expand_pagination. The "use this instead of expand_params once you hold a cursor" rule is the kind of mode flip an LLM-driven buyer agent will flub. Collapsing to one input [{ field, transformer_id?, cursor? }] (no cursor = first page, cursor = next page) is the same protocol with one shape. Non-blocking; flag as a candidate before 4.0 freezes the surface.
  2. expand_pagination claims exclusivity but schema doesn't enforce. Description says "Use this instead of expand_params once you hold a cursor" while both arrays may legally appear together. Either tighten with a not clause or soften the prose to "MAY use".
  3. config validation is normative-only. build-creative-request.json:config is additionalProperties: true with prose saying the agent MUST reject unknown keys with field-attributed errors. Same posture as other dynamic-input surfaces (creative-manifest.assets), so consistent — but worth a per-transformer JSON Schema fragment on transformer-param.json (or assembled client-side from params[]) so callers can validate before paying. Tracked as a future ergonomic.
  4. transformer-param.json example default isn't in options[]. Example 0 has default: \"sara\" but options[] only lists \"isaac\" and \"ceo_clone_2026\". Schema permits it (no rule that default ∈ options), but happy-path examples should validate against the example's own visible options or add a one-line note that "sara" lives on a later page.
  5. Three axes named confusingly. max_creatives (catalog fan-out), max_variants (alternatives per creative), item_limit (items consumed by one creative). The disambiguation prose works but the field names themselves invite confusion. Non-blocking on a minor release; flag for a 4.0 cleanup window since the rename would be breaking.

Notable that the migration doc surfaces "silent degradation" hazards (discovery-read degradation, per-output pricing loss, fan-out spend under-counting via report_usage reconciling only trafficked leaves) as first-class warnings — that's the right shape for a deprecation that runs for a full minor line before removal.

LGTM. Follow-ups noted above.

aao-release-bot[bot]
aao-release-bot Bot previously approved these changes May 31, 2026
Copy link
Copy Markdown
Contributor

@aao-release-bot aao-release-bot Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving. Right shape: a transformer is the creative analog of a product, every new oneOf member is gated by request opt-in, and every new request field is optional — the additive contract for existing 3.0 clients holds.

Things I checked

  • 6-member oneOf in static/schemas/source/media-buy/build-creative-response.json is distinguishable: creative_manifest / creative_manifests / creatives / mode:"estimate"+estimate / errors / status:"submitted"+task_id. BuildCreativeError and the submitted shape carry explicit not.anyOf clauses against the success keys; a misbehaving agent emitting two success markers fails oneOf — fail-closed.
  • BuildCreativeSuccess.build_variant_id is additive — listed under properties, required still ["creative_manifest"]. Existing 3.0 clients still validate.
  • BUDGET_CAP_REACHED properly held-for-next-minor at scripts/error-code-drift-dispositions.json:5-9 with target_version:"3.1", present in enumDescriptions and enumMetadata, distinct from BUDGET_EXCEEDED / BUDGET_EXHAUSTED.
  • x-entity registrations match: transformer and build_variant both in static/schemas/source/core/x-entity-types.json:18-22, 56-58; every transformer_id / build_variant_id carries x-entity on the wire.
  • static/schemas/source/index.json registers core.transformer, core.transformer-param, and creative.list-transformers (request + response). No orphan $refs.
  • Three changesets, all minor — wire impact matches (additive shapes, additive enum, additive capabilities).
  • Pricing path: Format.pricing_options deprecated, transformer.pricing_options carries the same vendor-pricing-option shape, per-leaf receipt echoes pricing_option_id into report_usage. Verified no stale link in list-creatives-response.json (still creative-level pricing, correct) or report-usage-request.json.
  • Served variant_id (static/schemas/source/core/creative-variant.json) is a distinct namespace from build-time build_variant_id; the x-entity-types.json descriptions disclaim reuse with preview_id too.

Follow-ups (non-blocking — file as issues)

  • Asymmetric trigger prose. BuildCreativeVariantSuccess.description says the buyer MUST handle the shape when triggering inputs are set; it doesn't say the seller MUST NOT emit it unprovoked. A 3.1 seller returning creatives[] to a 3.0 client that never sent max_creatives / max_variants / variant_axis / refine_from_build_variant_id breaks that client. Add a symmetric "agents MUST NOT return BuildCreativeVariantSuccess unless the request contained one of {…}" clause.
  • Stale oneOf baseline. scripts/oneof-discriminators.baseline.json:254-258 says variants: 4; after this PR it's 6. audit-oneof.mjs --check only flags STATUS_RANK regressions, so it passes — but the note field will mislead future reviewers about what discriminates the union. Run node scripts/audit-oneof.mjs --update and commit the refreshed entry.
  • Schema tightening worth a changeset line. The new allOf > not: { required: [target_format_id, target_format_ids] } in static/schemas/source/media-buy/build-creative-request.json:10-17 promotes a 3.0 prose constraint into schema. Non-conformant 3.0 clients that sent both fields will now hard-fail validation. Call it out in .changeset/creative-transformers.md.
  • transformer-param.json could be tightened. value_source enum doesn't conditionally require allowed_values for inline or minimum/maximum for range. Three if/then clauses (cheap) would prevent registry drift; current description-level "Present when…" is the only signal.
  • supports_spend_controls × bills_through_adcp coupling is SHOULD-shaped prose only. No if/then on get-adcp-capabilities-response.json. An out-of-band biller flipping on spend controls would validate fine and emit meaningless cost bands.
  • refinable_retention_seconds has no defined recourse on within-window REFERENCE_NOT_FOUND. Field is otherwise coherent as a SHOULD-floor. One sentence — "an agent returning REFERENCE_NOT_FOUND inside the advertised window violates its capability; buyers MAY downgrade to best-effort" — makes it falsifiable.

Minor nits (non-blocking)

  1. creatives[].variants[].vendor_cost is optional. Correct for free / cpm_deferred builds, but the prose calls leaves "independently-billed" — a one-line clarification ("absent when the build is free or cpm_deferred") closes the gap.

Approving on the strength of the gating discipline (every new shape and field opted into via request inputs) plus the clean three-way namespace separation between build_variant_id, served variant_id, and preview_id.

| `creative_id` | string | Creative: Yes | Creative identifier from `build_creative` or `list_creatives`. Links usage to a specific creative for billing verification. |
| `creative_id` | string | Creative: Yes | Creative identifier from `build_creative` or `list_creatives`. Links usage to a specific creative for billing verification. A `build_creative` variant leaf earns a `creative_id` only when trafficked/added to the library — discarded best-of-N or fan-out variants are never reported here; their charge is the inline per-leaf `vendor_cost` on the `build_creative` response (the authoritative record for untrafficked leaves). |
| `property_list_id` | string | Property lists: Yes | Property list identifier from `list_property_lists`. Links usage to a specific property list for billing verification. |

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the settlement model splits billing across two channels: trafficked leaves earn a creative_id and flow here; untrafficked leaves settle via inline vendor_cost on the build_creative response only. that works for charging, but when a variant IS trafficked and its creative_id lands in report_usage, there's no build_variant_id field on the usage record. billing reconciliation can't link a report_usage entry back to the specific leaf for audit - pricing_option_id echoes back but it's not a unique identifier. seems like build_variant_id should be an optional field on the usage record.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call — fixed in 63fc72e. Added an optional build_variant_id to the report_usage usage record so a trafficked leaf's usage entry links back to the exact produced leaf for audit (you're right that pricing_option_id isn't unique across leaves). Kept it optional — creatives with no build-variant lineage just omit it.

| `variant_axis` | object | No | Describes the dimension along which variants differ. Object with `dimension` (`voice` \| `theme` \| `best_of_n` \| `transformer_config` \| `custom`), optional `values[]` (explicit values to enumerate along the axis), optional `field` (the `config` param to sweep — required when `dimension` is `transformer_config`), and optional `label`. |
| `keep_mode` | string | No | Advisory hint to the agent on how many variants you intend to keep: `"keep_all"`, `"keep_one"`, or `"keep_some"`. Default `"keep_all"`. Advisory only — you are billed for all produced variants regardless. |
| `refine_from_build_variant_id` | string | No | Refine a prior produced variant: re-build from its `build_variant_id` applying the NL instruction in `message` plus any `config` delta, returning **new** lineage-linked variants (never a mutation). `transformer_id` and target format(s) are inherited from the parent. Composes with `max_variants`/`variant_axis`; mutually exclusive with `max_creatives`. Requires `creative.supports_refinement` — otherwise `UNSUPPORTED_FEATURE`; an unknown/no-longer-retained ref is `REFERENCE_NOT_FOUND` (`error.field` = `refine_from_build_variant_id`). See [Refinement](#refinement). |
| `mode` | string | No | `"execute"` (default) produces and bills. `"estimate"` is a **dry run** — produces and bills nothing, returns a `BuildCreativeEstimate` cost band (`cost_low`/`cost_high`) computed against this request's inputs. Requires `creative.supports_spend_controls`. See [Spend controls](#spend-controls). |
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

keep_mode is advisory with no explicit echo in the response. a buyer who sends keep_one but gets charged for 3 variants has no response field to confirm the agent received the hint - recommended/rank are always present regardless of keep_mode. the refinement_applied echo pattern in get-products-response.json is the direct analog here. without an echo, billing disputes have no paper trail.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed — fixed in 63fc72e. Added a keep_mode_applied echo on BuildCreativeVariantSuccess, exactly the refinement_applied analog you pointed at. keep_mode stays advisory (doesn't change what's produced or billed), but the echo is the buyer's confirmation the hint was received — the paper trail for a 'asked for keep_one, billed for N' dispute.

…ortfall, CPM caveat, estimate docs)

Adversarial review of dc3b9e4; fixed the confirmed findings:
- MAJOR: budget_status claimed items_returned < items_total shows the cap
  shortfall, but the cap is leaf-granular and items_* count catalog items —
  wrong for variant-only fan-out and collides with normal max_creatives
  sampling. Added leaves_total/leaves_returned to BuildCreativeVariantSuccess;
  budget_status now points at the leaf shortfall + the BUDGET_CAP_REACHED
  advisory as authoritative.
- max_spend bounds only build-time vendor_cost; noted that CPM (cpm_deferred)
  builds are 0 at build time so the cap never engages (use max_creatives).
- Documented the BuildCreativeEstimate response (new "### Estimate response"
  section + example) and added budget_status/leaves/errors to the variant
  field list; fixed the example's missing envelope status.
- Corrected "fifth success member" -> "third success shape (oneOf 3 of 6)".
- "buyer responsibility in v1" -> "in this revision".
- Refreshed scripts/oneof-discriminators.baseline.json (was stale at variants:4;
  now reflects the 6-member oneOf).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…+ per-output pricing)

The additive half of the generative-agent encodings follow-on (Veo/Imagen). The
two normative rules (count owned by max_variants; aspect_ratio on the format axis)
are left to the WG.

- transformer-param value_source gains "free_text" (open string, e.g. negative_prompt)
  + optional max_length; description states count knobs are NOT params.
- vendor-pricing-option gains optional applies_to_output_format_ids so a transformer
  can price different outputs differently (multi-publisher template). Additive/inert
  for signals/governance — flagged for shared-schema owner ack.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@aao-release-bot aao-release-bot Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Follow-ups noted below. Clean additive 3.1 minor: build capability moves off Format onto a selectable transformer (the creative analog of a media-buy product), with deprecation lanes that stay on the wire through 3.x and a removal target at 4.0.

Things I checked

  • oneOf disjointness on build-creative-response.json. Six members now (Success / MultiSuccess / VariantSuccess / Estimate / Error / Submitted). Positive arms distinguish via required-key uniqueness — creative_manifest / creative_manifests / creatives / mode+estimate — and Error + Submitted carry not.anyOf armor excluding all four (static/schemas/source/media-buy/build-creative-response.json:299-322, 555-576). Walker baseline ratchets 4→6, kind stays narrowable — not a regression.
  • BUDGET_CAP_REACHED double life. Advisory inside BuildCreativeVariantSuccess.errors[] when budget_status: "capped"; terminal in BuildCreativeError.errors[] only when even the first leaf exceeds the cap. Same recovery class (correctable) in both — one code is the right call.
  • Deprecation telegraph. Format.input_format_ids / output_format_ids / pricing_options and the matching filters on list_creative_formats marked deprecated: true with prose pinning removal at 4.0 and SDK MUST-honor through 3.1–3.x. Migration page at docs/reference/migration/creative-transformers.mdx carries the three silent-degradation hazards (discovery-read fall-off, per-output pricing loss, fan-out spend under-counting). minor changeset is the correct category — the breaking removal will ride its own major changeset.
  • Changesets. Three minors present (creative-transformers, build-creative-spend-controls, transformer-capability-discriminators). BUDGET_CAP_REACHED has the right disposition in scripts/error-code-drift-dispositions.json (held-for-next-minor, 3.1).
  • Refinement lineage. build_variant_id is its own namespace per build-creative-response.json:28 and :320, with x-entity: build_variant added in x-entity-types.json. Lazy promotion to creative_id on trafficking flows correctly to report_usage per the new docs/accounts/tasks/report_usage.mdx clarification.
  • Capability gating. creative.supports_transformers, supports_refinement, supports_spend_controls, refinable_retention_seconds, multiplicity — all five pre-call discriminators wired into get-adcp-capabilities-response.json and surfaced in docs/protocol/get_adcp_capabilities.mdx. list_transformers is Conditional in required-tasks.mdx (required iff supports_transformers: true).
  • Registry hygiene. transformer.json, transformer-param.json, list-transformers-request.json, list-transformers-response.json all present in static/schemas/source/index.json. docs.json nav carries list_transformers and the migration page in both live surfaces.

Follow-ups (non-blocking — file as issues)

  1. Doc drift on the success-shape numbering. Estimate landed late, and three surfaces still describe the variant member as if Estimate weren't there: docs/reference/whats-new-in-3-1.mdx:174 says "5th success shape"; .changeset/creative-transformers.md:7 says "a new fifth member is added"; .changeset/build-creative-spend-controls.md:7 calls Estimate the "6th oneOf member" when the schema order puts it 4th. docs/creative/task-reference/build_creative.mdx is correct ("3 of 6" / "4 of 6"). Sweep these three.
  2. PR body overclaims "shapes are unchanged." BuildCreativeSuccess gained an optional build_variant_id (build-creative-response.json:25-29) — additive and wire-compatible, but the shape did change. Loosen the prose to "additively extended."
  3. Real-brand names in new examples. Playbook is explicit: no real brands in new examples. The transformer example in static/schemas/source/core/transformer.json:88-101 uses audiostack provider strings; docs/creative/task-reference/list_transformers.mdx and build_creative.mdx use audiostack_voiceover / audiostack.example; docs/reference/migration/creative-transformers.mdx uses meta_reels_9x16 / "Resize to Meta Reels". Swap to fictional in the next pass — Acme, Pinnacle, Nova, StreamHaus per specs/character-bible.md.
  4. Defense-in-depth on the positive oneOf arms. Success / MultiSuccess / VariantSuccess / Estimate distinguish only by required-key uniqueness; Error and Submitted carry not.anyOf armor. A buggy agent emitting two shape-keys (e.g., creative_manifest + creatives, or any Success arm that also echoes mode: \"estimate\" + estimate) yields oneOf "multiple matched" under a strict validator — ajv rejects, but a permissive validator that short-circuits the first match binds to the wrong shape. Mirror the not.anyOf armor onto the four positive arms. Not a wire bug today; defense-in-depth before SDK generators bake permissive codegen.
  5. SDK round-trip is not in the test plan. Six new wire fields (transformer_id, config, refine_from_build_variant_id, mode, max_spend, build_variant_id) flow through MCP/A2A adapters. PR notes only local lint + schema suites. Either land the SDK propagation as a follow-on or call it out as out-of-scope in the body.

Minor nits (non-blocking)

  1. Three minor changesets in one PR. Spend controls and capability discriminators build on the main transformers change — transformer-capability-discriminators.md even calls itself "the keystone the spend-control and conformance follow-ons build on." If the split is deliberate (separate SemVer bumps for SDK consumers), say so in the PR body; otherwise one bundled minor would land cleaner.

Estimate landing mid-PR is what kept the "5th" / "6th" wording fresh in three places — the kind of drift one rewrite-pass at the end catches.

Safe to merge.

Copy link
Copy Markdown
Contributor

@aao-release-bot aao-release-bot Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Argus review could not complete

The automated review encountered an issue (possibly reached max turns, timed out, or failed to post the final gh pr review). A human reviewer should take this PR.

View workflow run

This is an automated message from the Argus AI review workflow.

aao-release-bot[bot]
aao-release-bot Bot previously approved these changes May 31, 2026
Copy link
Copy Markdown
Contributor

@aao-release-bot aao-release-bot Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving. Right shape: transformer as the creative analog of a media-buy product makes account-scoped render configuration discoverable from the agent instead of guessed, hung on a global format, or smuggled through ext — and the rest of the PR is the wiring that idea requires (variant fan-out, refinement, estimate, spend cap) without reshaping anything 3.0 already shipped.

Things I checked

  • oneOf 4→6 disjointness is sound. build-creative-response.json branches discriminate on distinct top-level required keys (creative_manifest | creative_manifests | creatives | mode,estimate | errors | status,task_id). BuildCreativeError and BuildCreativeSubmitted carry not: { anyOf: [...] } fences that explicitly include the two new required-key sets. Baseline scripts/oneof-discriminators.baseline.json correctly ratchets to variants: 6 with matching note. A 3.0 decoder that branches on the original four required keys cannot mis-match a 3.1 variant/estimate response — it returns a recognizable unmatched-shape error, not a silent decode.
  • Request mutex clauses placed correctly. build-creative-request.json allOf carries two not: { required: [a, b] } idioms — target_format_id/target_format_ids and refine_from_build_variant_id/max_creatives. Standard JSON Schema "must not have both" — fences the right pairs.
  • Error-code parity complete. BUDGET_CAP_REACHED wired in enum, enumDescriptions, enumMetadata, AND scripts/error-code-drift-dispositions.json with held-for-next-minor / target_version: "3.1". All four touchpoints present.
  • Changesets, all minor. Four discrete slices (transformers core, capability discriminators, spend controls, generative-safe additions). The PR is strictly additive — every new request field is optional, every new response branch is oneOf-disjoint from the shipped BuildCreativeSuccess / BuildCreativeMultiSuccess, every deprecated field carries the standard 3.1-deprecated/4.0-removed/SDKs-MUST-honor-3.x lifecycle that matches docs/reference/versioning.mdx.
  • x-entity registry updated. transformer and build_variant added to both the enum array and the descriptions map in x-entity-types.json — namespace separation between build_variant_id, served variant_id, and preview_id is explicit.
  • Schema/docs lockstep. Cross-checked field names and enum values across transformer-param.jsonlist_transformers.mdx, build-creative-request.jsonbuild_creative.mdx (variant_axis.dimension, keep_mode, mode, max_spend), and get-adcp-capabilities-response.jsonget_adcp_capabilities.mdx (all five new creative.* discriminators). No drift.

Follow-ups (non-blocking — file as issues)

  1. vendor-pricing-option.json shared-schema owner ack. applies_to_output_format_ids lives on the shared base reused by signals/governance/rights/property-lists/content-standards. Description correctly labels it "creative transformers only, inert for non-creative vendors," and the changeset itself flags this for "shared-schema owner ack." Get that ack from the signals/governance maintainers before the 3.1 tag; otherwise a non-creative consumer's deserialization model picks up a field it can never legally emit.
  2. variant_axis.values[].length silently overrides max_variants. A buyer sending max_variants: 2 with values: ["a","b","c"] gets 3 leaves and a 1.5× bill with no advisory. Either reject as INVALID_REQUEST or emit an advisory entry in errors[] on the response. The override is documented but it's the kind of quiet 1.5× spend an autonomous buyer won't notice until reconciliation.
  3. supports_spend_controls gates both mode: "estimate" and max_spend. An agent that can produce estimates but won't honor a hard cap (or vice versa) can't advertise honestly. Consider splitting into supports_estimate + supports_max_spend, or keep the compound flag but make the semantics "MUST support both."
  4. refinable_until echo on variant leaves. With refinable_retention_seconds as a floor, there's no way for an autonomous buyer to know whether a specific build_variant_id is still refinable without speculatively calling and catching REFERENCE_NOT_FOUND. Echo a per-leaf refinable_until (or expires_at-style) on BuildCreativeVariantSuccess.creatives[].variants[].
  5. No page-size ceiling on params[].options[]. expand_params returns the first page of option values inline on every transformer in the result. A wide brief × 8 transformers × voice param has no declared bound. Surface a default page size on the agent (or echo options_per_page_max on get_adcp_capabilities.creative) so agents can budget the call.
  6. creatives[].build_creative_id optional. Defensible (lineage anchors on build_variant_id), but a non-catalog variant fan-out has no other call-level grouping handle if it's omitted. Either make it required or add a normative SHOULD with a stated rationale in the field description.
  7. Unrelated baseline ratchets. scripts/oneof-discriminators.baseline.json also bumps notes for verify-brand-claim-response, verify-brand-claims-response, create-media-buy-response, update-media-buy-response. Confirm these are upstream-already-shipped drift fixes and not a stealth pickup, since they don't appear in the diff anywhere else.
  8. required-tasks.mdx enforcement. list_transformers is listed as Conditional (required when supports_transformers: true). Verify the compliance grader treats supports_transformers: true without a working list_transformers as a conformance break.

Minor nits (non-blocking)

  1. index.json catalog description drift. The transformer-param entry's description in static/schemas/source/index.json:419 lists value_source inline|range|enumerablefree_text (added by this PR) is missing. One-line fix or de-enumerate the prose. Description-level only; the schema file itself (transformer-param.json) is source of truth and correct.
  2. keep_mode naming. keep_one reading as "best-of-N, discard the rest" naturally implies a discount. The spec is explicit and Warning-flagged that you pay for all produced regardless — but the word "keep" actively pulls the reader toward the wrong cost model. Worth an SDK-level x-billing-note or a tighter name (recommendation_mode/selection_hint) before adopters bake the wrong intuition into pricing UI.
  3. "existing callers unaffected" overclaims. The PR description should hedge to "existing senders unaffected" — a 3.0 buyer that receives a BuildCreativeVariantSuccess (e.g. because some other principal triggered fan-out on a shared connection) can't decode it. Practically harmless, but the asymmetry is real.
  4. BuildCreativeMultiSuccess skips per-output build_variant_id. Patch 5 adds it to BuildCreativeSuccess and the description notes "multi-format builds that need per-output refinable leaves should request the variant shape." Defensible asymmetry; worth a one-line callout in the migration doc.

Four changesets on one PR is dense — defensible since each is a coherent slice of the same arc, but the 3.1 changelog story is going to read as four separate entries for what an adopter will mentally model as one feature.

LGTM. Follow-ups noted above.

… keep_mode echo) + UNPRICEABLE_OUTPUT

Review feedback on #5219 + WG decision:

- Nas: report_usage usage records carry creative_id but no link to the specific
  build leaf for audit (pricing_option_id isn't unique across leaves). Added
  optional build_variant_id to report-usage-request usage[] item.
- Nas: keep_mode is advisory with no response echo, so a buyer billed for N
  after sending keep_one had no paper trail. Added keep_mode_applied echo on
  BuildCreativeVariantSuccess (mirrors the get-products refinement_applied pattern).
- WG5 (decided: hard error, no fallback): added UNPRICEABLE_OUTPUT error code
  (enum + descriptions + metadata + drift disposition) — a build targeting an
  output no transformer.pricing_options entry covers (and no unscoped default)
  is rejected, never billed at a guessed rate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@aao-release-bot aao-release-bot Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving. Clean, additive 3.1 surface expansion — discriminated capabilities, fail-closed gates, and existing BuildCreativeSuccess/BuildCreativeMultiSuccess callers are untouched. The required-set partition on the new 6-branch oneOf is disjoint, the not.anyOf guards on BuildCreativeError / BuildCreativeSubmitted were extended to exclude both new shapes, and the audit-walker baseline was ratcheted to variants: 6 with the correct note.

Things I checked

  • oneOf disjointness on static/schemas/source/media-buy/build-creative-response.json — 6 branches partition by required keys (creative_manifest | creative_manifests | creatives | mode+estimate | errors | status+task_id); BuildCreativeError.not.anyOf (L539-572) and BuildCreativeSubmitted.not.anyOf (L613-636) both correctly extended to exclude creatives and estimate.
  • variant_dimensions enum parity at all three sites — get-adcp-capabilities-response.json (creative.multiplicity.variant_dimensions), core/transformer.json (multiplicity.variant_dimensions), and build-creative-request.json (variant_axis.dimension) all carry the same ["voice", "theme", "best_of_n", "transformer_config", "custom"] set.
  • keep_modekeep_mode_applied — request enum matches response echo (keep_all | keep_one | keep_some).
  • Error-code parity for UNPRICEABLE_OUTPUT and BUDGET_CAP_REACHED — present in error-code.json enum, enumDescriptions, and enumMetadata (recovery + suggestion), plus held-for-next-minor dispositions in scripts/error-code-drift-dispositions.json.
  • BuildCreativeSuccess — new build_variant_id is optional; required[] unchanged.
  • Request mutexallOf.not.required: [target_format_id, target_format_ids] and [refine_from_build_variant_id, max_creatives] use the correct "at most one" semantics.
  • Top-level request required — unchanged (idempotency_key only). Confirms minor is the right bump for all four changesets.
  • x-entity-types.json adds transformer and build_variant; index.json adds core/transformer, core/transformer-param, and the new creative/list-transformers request/response. No orphaned $refs.

Follow-ups (non-blocking — file as issues)

  1. "fifth member" drift across narrative docs. PR body, .changeset/creative-transformers.md, and docs/reference/whats-new-in-3-1.mdx describe BuildCreativeVariantSuccess as "the fifth member." With the spend-controls changeset in the same PR, the final state is six members and Variant is index 2 (the third success shape) — which docs/creative/task-reference/build_creative.mdx and .changeset/build-creative-spend-controls.md both get right ("third success shape (oneOf member 3 of 6)" / "6th oneOf member"). The bundled-final count is what ships to consumers.

  2. Silent response-shape switch on max_variants: 2. The <Warning> block at build_creative.mdx ~L524 ("No fallback") is shouted on the response side; the request field descriptions on max_creatives / max_variants / variant_axis / refine_from_build_variant_id in build-creative-request.json don't say "sending this flips the response shape — you MUST handle creatives[]." Agent buyers reading only the request schema will be surprised. One-line addition per field.

  3. keep_mode enum is over-specified. Request and response carry three values (keep_all / keep_one / keep_some) but the only behavior change documented is recommended/rank on/off, and keep_one vs keep_some is indistinguishable to the seller. Worth considering a collapse to rank_variants: boolean before 3.1 GA; keep_mode_applied is an audit echo that exists to paper over the ambiguity.

  4. Deprecation prose drift between two sites. static/schemas/source/core/format.json (input_format_ids / output_format_ids / pricing_options) carries the full "DEPRECATED in 3.1. Removed at 4.0. SDKs MUST continue to honor through 3.1–3.x" statement; static/schemas/source/creative/list-creative-formats-request.json carries only "DEPRECATED in 3.1." Mirror the full prose or trim both to the short form — the migration page is canonical, but the two deprecation sites should agree on detail.

  5. transformer.multiplicity ceiling rule is prose-only. core/transformer.json says per-transformer ceilings "MUST NOT exceed" the agent-level ceilings and variant_dimensions MUST be a subset — neither is schema-enforceable across two documents. Either add a check to the conformance suite or soften to SHOULD.

  6. Variant errors vs creatives[].errors[]. The schema descriptions on BuildCreativeVariantSuccess should explicitly distinguish the top-level (advisory) array from the per-creative (terminal per-item) one; the .mdx calls it out, the schema doesn't.

  7. max_spend caps one call. Refinement loops are buyer-tracked in this revision — flagged in the doc, but worth a 3.2 RFC for a protocol-level session budget for unattended agentic buyers.

Minor nits (non-blocking)

  1. Baseline note drift bundled in. scripts/oneof-discriminators.baseline.json updates for create-media-buy-response.json##/oneOf (now mentions confirmed_at,revision) and update-media-buy-response.json##/oneOf (now mentions revision) are legitimate refreshes — those fields are already required on main, the notes were stale. audit-oneof.mjs --update picking them up is correct, but they're unrelated to transformers and could have been their own commit. Bundling is mildly noisy.

  2. vendor-pricing-option.applies_to_output_format_ids is "creative transformers only." Added to a schema also used by signals/rights/governance. The generative-encoding-safe-additions changeset already flags this for shared-schema owner ack — no action needed until that sign-off.

  3. The "Strictly additive" framing in the changeset is true for consumers but the new allOf.not.required mutex on target_format_id / target_format_ids is a schema tightening — payloads that erroneously sent both used to validate and now don't. Worth one line in the changeset; realistic blast radius is near-zero.

Test plan checked — all gates listed (build:schemas, build:compliance, oneof-discriminators, test:unit 941, test:server-unit 3958, docs-nav 21) reported green, nothing unchecked. Quantified the way I like it.

Safe to merge. Notable that "fifth member" survived three narrative documents while the load-bearing schema and tutorial caught the bundled count correctly — the spec was reviewed harder than the prose around it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants