feat(creative): account-scoped creative transformers + build_creative multiplicity#5219
feat(creative): account-scoped creative transformers + build_creative multiplicity#5219bokelley wants to merge 11 commits into
Conversation
… 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>
There was a problem hiding this comment.
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
oneOfbranch onstatic/schemas/source/media-buy/build-creative-response.jsonis discriminated bycreatives(unique required key). BothBuildCreativeError.not.anyOfandBuildCreativeSubmitted.not.anyOfare patched to excludecreatives— symmetric and intentional. No new undiscriminatedoneOfintroduced. static/schemas/source/core/transformer.jsoncardinality:output_format_idsminItems: 1(a transformer with no outputs is meaningless), optionalinput_format_idscorrectly models pure-generation transformers, required keys are[transformer_id, name, output_format_ids].static/schemas/source/core/transformer-param.jsonvalue_sourceenum (inline | range | enumerable) + descriptor-only presence rules forallowed_values/minimum-maximum/options[]. Consistent with how AdCP handles other conditional descriptor shapes.transformer.pricing_optionsreusesvendor-pricing-option.json— pricing rides the existingper_unit+ receipt +report_usagerails unchanged.x-entity: transformerregistered incore/x-entity-types.jsonwith description shape matchingvendor_pricing_option/creative_format.creative.supports_transformersonget-adcp-capabilities-response.json:1094defaults tofalse, anddocs/protocol/required-tasks.mdxcorrectly markslist_transformersas Conditional gated on that flag.list-transformers-request.jsonif/then(requireaccountwheninclude_pricing: true) is valid draft-07 / Ajv syntax nested insideallOf.Format.input_format_ids/output_format_ids/pricing_optionsand thelist_creative_formatsdiscovery filters are markeddeprecated: truewith "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.mdxand the "Variant response" section ofbuild_creative.mdxagainst the underlying schemas — names, types, requirement levels match. - Changeset
.changeset/creative-transformers.mdexists, declaresminor, accurately describes the additive surface.
Follow-ups (non-blocking — file as issues)
- Baseline refresh on
scripts/oneof-discriminators.baseline.json. The recorded note formedia-buy/build-creative-response.json##/oneOfisvariants: 4and lists the four pre-existing branches; adding the 5thcreativesbranch is an improvement-or-equal transition (still narrowable, still uniquely keyed), but--checkwill see a delta on that row. Runnpm run audit:oneof -- --updateand commit the regenerated baseline — no--accept-newneeded, no regression. - Missed deprecation callout at
docs/creative/template-format-ids.mdx:292-315. The "Generative Formats with Output Formats" section still describesFormat.output_format_idsas the canonical way to express generative output capability, with no "deprecated in 3.1, seelist_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:664declares asupports_transformation: truediscriminator. The newcreative.supports_transformersis one letter and a noun-form away. One of them deserves a disambiguating sentence —supports_transformationis the "I can resize/reformat a manifest viabuild_creative" boolean;supports_transformersis 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.jsonlines 557 / 565 / 671 say "DEPRECATED in 3.1. Removed at 4.0.".static/schemas/source/creative/list-creative-formats-request.jsonlines 113 / 122 say "DEPRECATED in 3.1." only. Make the lifecycle string match across all three surfaces. build_creative_id/build_variant_idaren't registered asx-entity. Today fine — no schema consumes them, so the entity-id gap report won't fire. The moment a trafficking task accepts abuild_variant_idon the wire, the lint will flag it. Either register them now or drop a$commenton each leaf noting "x-entity deliberately omitted — build-time handle, lazily promoted to creative_id on trafficking".
Minor nits (non-blocking)
creatives[].items.anyOfallows bothvariants[]anderrors[].static/schemas/source/media-buy/build-creative-response.json:1097-1101usesanyOf: [{required: [variants]}, {required: [errors]}]plusadditionalProperties: 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 tooneOfwith mutualnot:clauses. Author's call.- Sample-code drift from repo idiom. The two new
CodeGroupblocks inbuild_creative.mdxandlist_transformers.mdxuse```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 usefrom adcp import …; established convention isfrom adcp.types import …. JS samples skip theif (!result.success) throw …/result.dataunwrap that every other sample uses. Field names themselves are clean — match the schemas exactly. - Normative claim only in docs.
docs/creative/task-reference/list_transformers.mdx:417asserts "Unknownexpand_params(afieldno 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. variant_axis_valuehas notypeatstatic/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 intransformer-param.json. A one-line$commentnoting 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.
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 1. Decision to make in this PR: pin
|
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>
There was a problem hiding this comment.
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
BuildCreativeVariantSuccessmember is discriminated byrequired: [creatives]— disjoint fromcreative_manifest/creative_manifests/errors/status='submitted'+task_id.BuildCreativeErrorandBuildCreativeSubmittednot.anyOfblocks both got the{required: [creatives]}entry — five-way exclusivity holds (static/schemas/source/media-buy/build-creative-response.json~L447, ~L515). x-entitytaxonomy instatic/schemas/source/core/x-entity-types.json:transformerandbuild_variantadded toenum[]ANDx-entity-definitions{}. Thebuild_variantdescription correctly distinguishes leaf lineage from deliveryvariant_id, previewpreview_id, and the call-level groupingbuild_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 twolist_creative_formatsfilters:"deprecated": true+ on-the-wire retention + explicit 3.1→4.0 removal window + per-field migration prose pointing atlist_transformers. Matches the prior 3.x deprecation cadence. - Doc/schema coherence:
variant_axis.dimensionenum (voice|theme|best_of_n|transformer_config|custom),keep_modeenum (keep_all|keep_one|keep_some),expand_paramsinlineparams[].options[]semantics, and theinclude_pricing → require accountallOfif/thenall match between docs and schemas. creative.supports_transformersis the right pre-call discriminator onget_adcp_capabilitiesand orthogonal tocreative.supported_formats[]— formats describe canonical outputs, transformers describe build capability + configuration. Pre-call discrimination is how agentic clients prune routing without speculative calls.list_transformersis correctly listed as Conditional indocs/protocol/required-tasks.mdx("Required when the agent advertisescreative.supports_transformers: true").transformer.jsonrequiresoutput_format_idswithminItems: 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
anyOfvsoneOfonBuildCreativeVariantSuccess.creatives[].items. The schema isanyOf: [{required:[variants]}, {required:[errors]}]— permits an entry carrying bothvariantsanderrors. The field-level prose says a successful entry "SHOULD NOT carry errors," which is consistent withanyOf. Worth pinning explicitly: either tighten tooneOfif 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.jsonmetadata is stale after merge. The baseline recordsvariants: 4and a 4-branch note formedia-buy/build-creative-response.json##/oneOf.audit-oneof.mjs --checkkeys onfile#pointerand doesn't compare counts, so the gate stays green — but the snapshot is now wrong. Refresh withnode scripts/audit-oneof.mjs --update.- Per-leaf pricing receipt fields are described but not
required.pricing_option_id,vendor_cost,currency,consumptionon eachvariants[]entry are framed as carried per-leaf yet a paid agent could omit them with no schema-level error. Same normative-only pattern asconfigvalidation — acceptable, but a paid build returning leaves withoutvendor_costis a wire bug the schema won't catch.
Minor nits (non-blocking)
transformer-param.jsonvalue_sourcelacks anif/then.inlinedoesn't requireallowed_values;rangedoesn't requireminimum/maximum;enumerabledoesn't requireoptions[]. Consistent with how other AdCP discriminators handle soft cross-field constraints, butcore/product.jsonhas a precedent for the tighter shape — flagging only so the asymmetry is a deliberate call.build_creativenow 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 whendimension == \"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>
There was a problem hiding this comment.
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
oneOfonbuild-creative-response.jsonstays disjoint with the fifth member:BuildCreativeErrorandBuildCreativeSubmittedboth addcreativesto theirnot.anyOfblocks.BuildCreativeSuccessandBuildCreativeMultiSuccessare byte-identical — no wire impact for existing callers. - Capability gating matches prior art:
creative.supports_transformersandcreative.supports_refinementinget-adcp-capabilities-response.jsonfollow the sameboolean / default false / UNSUPPORTED_FEATURE on negative gateshape asbills_through_adcpandhas_creative_library. x-entityregistry:transformerandbuild_variantadded tocore/x-entity-types.jsonwith 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_optionscarry real"deprecated": trueJSON Schema markers incore/format.jsonplus matching prose inlist_creative_formats.mdxandwhats-new-in-3-1.mdx. 3.1-deprecate / 4.0-remove ratchet matches thecanonical_parametersprecedent 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.jsonenforcesinclude_pricing == true → required: ["account"]viaallOf.if/then— that conditional gate is machine-checkable, not prose-only.- Schema-vs-docs: every field documented in
list_transformers.mdxand the newbuild_creative.mdxsections exists in the schema with the documented type; reverse direction also clean.
Follow-ups (non-blocking — file as issues)
build_creative_idis untyped. Its description inbuild-creative-response.jsonexplicitly says this is its own namespace (notcreative, notbuild_variant), but nox-entityannotation. Either add abuild_creativeentity type tox-entity-types.jsonor document why this grouping id is intentionally untyped — the namespace separation the PR description calls airtight can't be lint-enforced otherwise.variant_axis.fieldconditional REQUIRED is prose-only. Whendimension == "transformer_config",fieldis REQUIRED per description, but the schema onlyrequired: ["dimension"].list-transformers-request.jsonusesallOf.if/thenforinclude_pricing → accountin this same PR — apply the same pattern here so a buyer sending{dimension: "transformer_config"}with nofielddoesn't pass validation silently.- Refinement composability is prose-only.
refine_from_build_variant_idMX withmax_creativesand inheritance oftransformer_id/ target format from the parent are described but not schema-enforced. Buyers will get refinement composability wrong; anif-then-notblock would catch it pre-request. - Error-code drift in
list_transformers.mdx. Docs citeAUTHENTICATION_REQUIREDandINVALID_INPUTin the Error Handling table. Neither is inenums/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. BuildCreativeErrordescription is now incomplete. It still says "multi-format requests are atomic" without acknowledgingBuildCreativeVariantSuccess's per-item non-atomic catalog fan-out (where failed items live insidecreatives[].errors[]and the batch still succeeds). Tighten the wording.- Refinement discoverability is weak.
refine_from_build_variant_idis a powerful primitive buried as abuild_creativeparam. Surfacingrefinable: trueon each variant leaf inBuildCreativeVariantSuccesswould give an agent the affordance after the first build instead of forcing it to re-read the tool description.
Minor nits (non-blocking)
- Real brand name in example domains.
https://creative.audiostack.exampleappears acrosslist_transformers.mdx,build_creative.mdx, andtransformer.jsonexamples. 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_acmeis fine. testable: truefrontmatter, all examplestest=false.list_transformers.mdx:5. A no-accountlist_transformerswith atransformer_idsfilter is genuinely testable (accountis only required withinclude_pricing). Either wire one truly testable example or drop the frontmatter flag.transformer.jsonrequiresoutput_format_idsbut 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.transformer-param.jsonhas a property named"required". Boolean field collides visually with the standard JSON Schemarequiredkeyword. Valid but reader-hostile —is_requiredormandatorywould avoid the collision.configopen-bag with normative strict validation is the right call, but thebuild_creativetool description could push harder against hallucination: "MUST calllist_transformersbefore settingtransformer_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>
There was a problem hiding this comment.
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
BuildCreativeVariantSuccessas a 5th oneOf member is disjoint. Required-field discrimination viacreativesis sufficient against the existing four branches;BuildCreativeErrorandBuildCreativeSubmittednot.anyOfblocks correctly add{required:["creatives"]}(static/schemas/source/media-buy/build-creative-response.json:452,520).BuildCreativeSuccessandBuildCreativeMultiSuccessrely on the existing required-field pattern — consistent with the current shape.- oneOf baseline does not fail CI.
scripts/audit-oneof.mjs --checkcomparesSTATUS_RANK[kind], and the 5-variant form classifies asnarrowable— same rank as the 4-variant baseline atscripts/oneof-discriminators.baseline.json:254-258. PR description's local-pass claim holds. (Metadata drift noted below.) - Changeset is
minorand correct. ExistingBuildCreativeSuccess/BuildCreativeMultiSuccessshapes are untouched; new branch is opt-in viamax_creatives/max_variants/transformer_id; deprecatedFormatfields aredeprecated: truenot 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.dimensionenum (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 thesupports_transformers/supports_refinementcapability descriptions all match acrosstransformer.json,transformer-param.json,build-creative-request.json,build-creative-response.json,get-adcp-capabilities-response.jsonand their docs counterparts. item_limitvsmax_creativesdistinction is explicit. Schema description foritem_limitupdated to call out DCO-style within-one-creative, distinct frommax_creativesfan-out (static/schemas/source/media-buy/build-creative-request.json:127) — same distinction in thebuild_creative.mdxfield table. Real source of historical confusion; clean fix.- Three namespace separations land.
build_variant_id/preview_id/ servedvariant_idare documented inbuild-creative-response.json(build_variant_id field),x-entity-types.jsonbuild_variantentry, and thebuild_creative.mdxVariant-response section. Cross-checked againstcore/creative-variant.json— no collision. - Migration guide silent-degradation hazards are concrete. Discovery-read drift, per-output pricing loss on naïve
Format.pricing_options→transformer.pricing_optionslift, and best-of-N spend under-counting viareport_usage-only pipelines — all three name the failure mode and the recovery action. Thereport_usage.mdxedit aligns with hazard #3. x-entity-types.jsonregistration.transformerandbuild_variantadded with descriptors consistent with the existing dictionary style.- Migration page is in nav.
docs.jsonaddsdocs/reference/migration/creative-transformersto both nav groups (top + sidebar copy) anddocs/creative/task-reference/list_transformersto both task-reference groups.
Follow-ups (non-blocking — file as issues)
build_creative_idonBuildCreativeVariantSuccess.creatives[]is a documented identifier with nox-entityannotation. 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 incore/x-entity-types.json. Either annotate asx-entity: "build_creative"and register the entity (parallel to howbuild_variantwas 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.- Refresh the oneOf baseline.
node scripts/audit-oneof.mjs --updatewill bumpvariants: 4 → 5and refresh thenoteformedia-buy/build-creative-response.json##/oneOfatscripts/oneof-discriminators.baseline.json:254-258. Not a CI blocker (kind staysnarrowable), but the recorded note will drift from reality until refreshed and confuse the next reader of that file. docs/creative/implementing-creative-agents.mdxstill tells creative-agent builders to exposepricing_optionsonlist_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 atlist_transformers. Implementer-persona page; not in this PR's diff, hence follow-up.- Schema-level mutex enforcement of
refine_from_build_variant_id×max_creatives. Currently prose-only ("mutually exclusive withmax_creatives/ catalog fan-out"). The existingtarget_format_id↔target_format_idsmutex is structural; the new pair could mirror it viaallOf: [{not: {required: ["refine_from_build_variant_id", "max_creatives"]}}]and catch the buyer-side mistake at validation time. Symmetry, not correctness. transformer-param.jsonallowed_values.itemsanddefaultare untyped. Low-priority polish — values should match the param'stype. Aif/thenontypewould tighten without much weight, but the strict-validation contract is normative anyway.docs/creative/generative-creative.mdxdoesn't mention transformers. For a doc walking adopters through generativebuild_creative, a one-sentence pointer ("If your agent advertisessupports_transformers, discover vialist_transformers") would close the discoverability gap. Lighter follow-up.
LGTM. Follow-ups noted below.
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>
There was a problem hiding this comment.
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.anyOfandBuildCreativeSubmitted.not.anyOfboth addcreativesto their exclusion lists (build-creative-response.json:460,528). Mechanical discrimination holds. - New entities registered.
transformerandbuild_variantare in both the enum andx-entity-definitions(x-entity-types.json:21-22, 57-58).build_variant's definition correctly carves it out ofvariant_id(delivery) andpreview_id(preview) namespaces. - Error codes already exist.
UNSUPPORTED_FEATUREandREFERENCE_NOT_FOUNDare inenums/error-code.json; no enumMetadata changes needed. - Changeset is right.
minormatches additive new tasks/fields +deprecated: trueflags with no constraint changes on the deprecated fields. - Migration doc. Covers all three deprecated surfaces (Format I/O,
Format.pricing_options,list_creative_formatsfilters) 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.
BuildCreativeVariantSuccessis 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 ZoddiscriminatedUnion/ PydanticField(discriminator=)fall back to untagged unions — works, but worse error messages and slower parsing. Aresponse_shape: \"variant\"literal costs one field and earns clean discrimination across every SDK. Worth doing before 3.1 GA. expand_params+expand_paginationis two shapes for one concept.expand_params: string[](first page) vsexpand_pagination: { transformer_id, field, options_cursor }[](next page) doubles the schema surface. A singleexpand: { field, transformer_id?, options_cursor? }[]array would carry both cases. Agents will reliably get step 1 and unreliably get step 2.transformer-param.jsondoesn't enforce conditional requirements.value_source: \"inline\" ⇒ allowed_values required,value_source: \"range\" ⇒ minimum/maximum requiredare description-only.allOf+if/thenpervalue_sourcevalue would catch malformed transformer descriptors at validation time instead of at config-validation time.whats-new-in-3-1.mdxis silent on refinement. The schema andbuild_creative.mdxboth shiprefine_from_build_variant_id/supports_refinement/parent_build_variant_idin this PR, but the 3.1 release notes don't mention it. Easy to fix.list_transformers.mdx'Each transformer carries' table omitsdescriptionandmetadata. Both are defined intransformer.json(transformer.json:13-21) but missing from the docs response table.- Variant response section in
build_creative.mdxdoesn't documentsandbox/expires_at. Both fields exist onBuildCreativeVariantSuccess(build-creative-response.json:411-416) and are documented for the single/multi shapes — parity gap. target_format_id/target_format_idsmutex is now schema-enforced. Docs always said exclusive; the schema didn't. The newnot.requiredinallOf(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)
build_creative_idlacksx-entityannotation.build_variant_idandtransformer_idcarry the annotation; the call-levelbuild_creative_idonBuildCreativeVariantSuccess.creatives[]doesn't (build-creative-response.jsonvariant block). Either register abuild_creativeentity or leave it un-annotated by design — current state is inconsistent.- Per-item success/failure is
anyOf, notoneOf.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[]" —oneOfwould enforce that;anyOfmakes it SHOULD-level. keep_modenaming. "Advisory" + "mode" reads like it changes behavior; it doesn't.keep_intentorexpected_keep_countwould land more honestly.transformer-param.jsonhas arequiredproperty (boolean). It's namespaced underproperties, so technically fine, but mildly confusing alongside the JSON Schemarequiredkeyword.
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>
There was a problem hiding this comment.
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
transformerandbuild_variantare both instatic/schemas/source/core/x-entity-types.jsonenum +x-entity-definitions, and match thex-entityannotations onbuild-creative-request.json(transformer_id,refine_from_build_variant_id) andbuild-creative-response.json(build_variant_id,parent_build_variant_id).static/schemas/source/index.jsonregisterscore/transformer.json,core/transformer-param.json, and thelist-transformersrequest/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.jsonexample validates:pricing_options[0]satisfiesvendor-pricing-option.jsonPerUnitPricing (model/unit/unit_price/currency all present);input_format_ids/output_format_idsuse the{agent_url,id}form.- BuildCreativeError and BuildCreativeSubmitted
not.anyOfclauses have been updated to also excludecreatives— so the new BuildCreativeVariantSuccess branch is fenced off from those two. - Both changesets are
minorand the surface is additive (new optional request fields, new oneOf branch alongside the existing two success shapes). Correct classification. - Docs nav:
docs.jsonwires up bothdocs/creative/task-reference/list_transformersanddocs/reference/migration/creative-transformersin current and 3.0-compat navs.
Follow-ups (non-blocking — file as issues)
creatives[]per-itemanyOfdoesn't enforce the prose.build-creative-response.jsonBuildCreativeVariantSuccesscreatives.itemsusesanyOf: [{required:["variants"]}, {required:["errors"]}]. The field prose says a failed entry carrieserrors[]and novariants[], and a successful entry carriesvariants[]and SHOULD NOT carryerrors.anyOflets both coexist. Tighten tooneOf(or add anotclause) so the schema enforces what the prose mandates.- Asymmetric
not.anyOfon the five-branch oneOf. BuildCreativeError and BuildCreativeSubmitted both carrynot.anyOfagainst 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 symmetricnotclauses on the three success branches. Cheap fix, removes the foot-gun for sellers who accidentally emit bothcreative_manifestandcreativeson the same response. scripts/oneof-discriminators.baseline.jsonis stale. Recordsbuild-creative-response.json##/oneOfasvariants: 4with a 4-item note line; the schema now has 5. The--checkmode passes (status ranknarrowableis unchanged) but a follow-up--updaterun is in order so the baseline notes the new branch.variant_axis_valueis type-less.build-creative-response.jsonBuildCreativeVariantSuccesscreatives[].variants[].variant_axis_valuedeclares only a description — anything validates, includingnull. Constrain to["string","number","boolean"](matching the legalconfigvalue 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 amax_billable_variantscompanion, or a stronger normative statement thatmax_variantsIS the spend cap.
Minor nits (non-blocking)
expand_params/expand_paginationis two parallel pagination dimensions.list-transformers-request.json— first page names a field inexpand_params; subsequent pages need a{transformer_id, field, options_cursor}triple inexpand_pagination. The "use this instead ofexpand_paramsonce 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.expand_paginationclaims exclusivity but schema doesn't enforce. Description says "Use this instead ofexpand_paramsonce you hold a cursor" while both arrays may legally appear together. Either tighten with anotclause or soften the prose to "MAY use".configvalidation is normative-only.build-creative-request.json:configisadditionalProperties: truewith 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 ontransformer-param.json(or assembled client-side fromparams[]) so callers can validate before paying. Tracked as a future ergonomic.transformer-param.jsonexample default isn't inoptions[]. Example 0 hasdefault: \"sara\"butoptions[]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.- 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 aminorrelease; 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.
There was a problem hiding this comment.
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
oneOfinstatic/schemas/source/media-buy/build-creative-response.jsonis distinguishable:creative_manifest/creative_manifests/creatives/mode:"estimate"+estimate/errors/status:"submitted"+task_id.BuildCreativeErrorand the submitted shape carry explicitnot.anyOfclauses against the success keys; a misbehaving agent emitting two success markers failsoneOf— fail-closed. BuildCreativeSuccess.build_variant_idis additive — listed underproperties,requiredstill["creative_manifest"]. Existing 3.0 clients still validate.BUDGET_CAP_REACHEDproperly held-for-next-minor atscripts/error-code-drift-dispositions.json:5-9withtarget_version:"3.1", present inenumDescriptionsandenumMetadata, distinct fromBUDGET_EXCEEDED/BUDGET_EXHAUSTED.x-entityregistrations match:transformerandbuild_variantboth instatic/schemas/source/core/x-entity-types.json:18-22, 56-58; everytransformer_id/build_variant_idcarriesx-entityon the wire.static/schemas/source/index.jsonregisterscore.transformer,core.transformer-param, andcreative.list-transformers(request + response). No orphan$refs.- Three changesets, all
minor— wire impact matches (additive shapes, additive enum, additive capabilities). - Pricing path:
Format.pricing_optionsdeprecated,transformer.pricing_optionscarries the samevendor-pricing-optionshape, per-leaf receipt echoespricing_option_idintoreport_usage. Verified no stale link inlist-creatives-response.json(still creative-level pricing, correct) orreport-usage-request.json. - Served
variant_id(static/schemas/source/core/creative-variant.json) is a distinct namespace from build-timebuild_variant_id; thex-entity-types.jsondescriptions disclaim reuse withpreview_idtoo.
Follow-ups (non-blocking — file as issues)
- Asymmetric trigger prose.
BuildCreativeVariantSuccess.descriptionsays 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 returningcreatives[]to a 3.0 client that never sentmax_creatives/max_variants/variant_axis/refine_from_build_variant_idbreaks that client. Add a symmetric "agents MUST NOT returnBuildCreativeVariantSuccessunless the request contained one of {…}" clause. - Stale oneOf baseline.
scripts/oneof-discriminators.baseline.json:254-258saysvariants: 4; after this PR it's 6.audit-oneof.mjs --checkonly flagsSTATUS_RANKregressions, so it passes — but thenotefield will mislead future reviewers about what discriminates the union. Runnode scripts/audit-oneof.mjs --updateand commit the refreshed entry. - Schema tightening worth a changeset line. The new
allOf > not: { required: [target_format_id, target_format_ids] }instatic/schemas/source/media-buy/build-creative-request.json:10-17promotes 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.jsoncould be tightened.value_sourceenum doesn't conditionally requireallowed_valuesforinlineorminimum/maximumforrange. Threeif/thenclauses (cheap) would prevent registry drift; current description-level "Present when…" is the only signal.supports_spend_controls×bills_through_adcpcoupling is SHOULD-shaped prose only. Noif/thenonget-adcp-capabilities-response.json. An out-of-band biller flipping on spend controls would validate fine and emit meaningless cost bands.refinable_retention_secondshas no defined recourse on within-windowREFERENCE_NOT_FOUND. Field is otherwise coherent as a SHOULD-floor. One sentence — "an agent returningREFERENCE_NOT_FOUNDinside the advertised window violates its capability; buyers MAY downgrade to best-effort" — makes it falsifiable.
Minor nits (non-blocking)
creatives[].variants[].vendor_costis optional. Correct for free /cpm_deferredbuilds, but the prose calls leaves "independently-billed" — a one-line clarification ("absent when the build is free orcpm_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. | | ||
|
|
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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). | |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
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 carrynot.anyOfarmor excluding all four (static/schemas/source/media-buy/build-creative-response.json:299-322, 555-576). Walker baseline ratchets 4→6, kind staysnarrowable— not a regression. - BUDGET_CAP_REACHED double life. Advisory inside
BuildCreativeVariantSuccess.errors[]whenbudget_status: "capped"; terminal inBuildCreativeError.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_optionsand the matching filters onlist_creative_formatsmarkeddeprecated: truewith prose pinning removal at 4.0 and SDK MUST-honor through 3.1–3.x. Migration page atdocs/reference/migration/creative-transformers.mdxcarries the three silent-degradation hazards (discovery-read fall-off, per-output pricing loss, fan-out spend under-counting).minorchangeset is the correct category — the breaking removal will ride its ownmajorchangeset. - Changesets. Three
minors present (creative-transformers,build-creative-spend-controls,transformer-capability-discriminators).BUDGET_CAP_REACHEDhas the right disposition inscripts/error-code-drift-dispositions.json(held-for-next-minor,3.1). - Refinement lineage.
build_variant_idis its own namespace perbuild-creative-response.json:28and:320, withx-entity: build_variantadded inx-entity-types.json. Lazy promotion tocreative_idon trafficking flows correctly toreport_usageper the newdocs/accounts/tasks/report_usage.mdxclarification. - Capability gating.
creative.supports_transformers,supports_refinement,supports_spend_controls,refinable_retention_seconds,multiplicity— all five pre-call discriminators wired intoget-adcp-capabilities-response.jsonand surfaced indocs/protocol/get_adcp_capabilities.mdx.list_transformersisConditionalinrequired-tasks.mdx(required iffsupports_transformers: true). - Registry hygiene.
transformer.json,transformer-param.json,list-transformers-request.json,list-transformers-response.jsonall present instatic/schemas/source/index.json.docs.jsonnav carrieslist_transformersand the migration page in both live surfaces.
Follow-ups (non-blocking — file as issues)
- 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:174says "5th success shape";.changeset/creative-transformers.md:7says "a new fifth member is added";.changeset/build-creative-spend-controls.md:7calls Estimate the "6th oneOf member" when the schema order puts it 4th.docs/creative/task-reference/build_creative.mdxis correct ("3 of 6" / "4 of 6"). Sweep these three. - PR body overclaims "shapes are unchanged."
BuildCreativeSuccessgained an optionalbuild_variant_id(build-creative-response.json:25-29) — additive and wire-compatible, but the shape did change. Loosen the prose to "additively extended." - 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-101usesaudiostackprovider strings;docs/creative/task-reference/list_transformers.mdxandbuild_creative.mdxuseaudiostack_voiceover/audiostack.example;docs/reference/migration/creative-transformers.mdxusesmeta_reels_9x16/ "Resize to Meta Reels". Swap to fictional in the next pass — Acme, Pinnacle, Nova, StreamHaus perspecs/character-bible.md. - Defense-in-depth on the positive oneOf arms. Success / MultiSuccess / VariantSuccess / Estimate distinguish only by required-key uniqueness; Error and Submitted carry
not.anyOfarmor. A buggy agent emitting two shape-keys (e.g.,creative_manifest+creatives, or any Success arm that also echoesmode: \"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 thenot.anyOfarmor onto the four positive arms. Not a wire bug today; defense-in-depth before SDK generators bake permissive codegen. - 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)
- Three minor changesets in one PR. Spend controls and capability discriminators build on the main transformers change —
transformer-capability-discriminators.mdeven 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.
There was a problem hiding this comment.
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.
This is an automated message from the Argus AI review workflow.
There was a problem hiding this comment.
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
oneOf4→6 disjointness is sound.build-creative-response.jsonbranches discriminate on distinct top-level required keys (creative_manifest|creative_manifests|creatives|mode,estimate|errors|status,task_id).BuildCreativeErrorandBuildCreativeSubmittedcarrynot: { anyOf: [...] }fences that explicitly include the two new required-key sets. Baselinescripts/oneof-discriminators.baseline.jsoncorrectly ratchets tovariants: 6with 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.jsonallOfcarries twonot: { required: [a, b] }idioms —target_format_id/target_format_idsandrefine_from_build_variant_id/max_creatives. Standard JSON Schema "must not have both" — fences the right pairs. - Error-code parity complete.
BUDGET_CAP_REACHEDwired inenum,enumDescriptions,enumMetadata, ANDscripts/error-code-drift-dispositions.jsonwithheld-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 isoneOf-disjoint from the shippedBuildCreativeSuccess/BuildCreativeMultiSuccess, every deprecated field carries the standard 3.1-deprecated/4.0-removed/SDKs-MUST-honor-3.x lifecycle that matchesdocs/reference/versioning.mdx. x-entityregistry updated.transformerandbuild_variantadded to both the enum array and the descriptions map inx-entity-types.json— namespace separation betweenbuild_variant_id, servedvariant_id, andpreview_idis explicit.- Schema/docs lockstep. Cross-checked field names and enum values across
transformer-param.json↔list_transformers.mdx,build-creative-request.json↔build_creative.mdx(variant_axis.dimension, keep_mode, mode, max_spend), andget-adcp-capabilities-response.json↔get_adcp_capabilities.mdx(all five newcreative.*discriminators). No drift.
Follow-ups (non-blocking — file as issues)
vendor-pricing-option.jsonshared-schema owner ack.applies_to_output_format_idslives 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.variant_axis.values[].lengthsilently overridesmax_variants. A buyer sendingmax_variants: 2withvalues: ["a","b","c"]gets 3 leaves and a 1.5× bill with no advisory. Either reject asINVALID_REQUESTor emit an advisory entry inerrors[]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.supports_spend_controlsgates bothmode: "estimate"andmax_spend. An agent that can produce estimates but won't honor a hard cap (or vice versa) can't advertise honestly. Consider splitting intosupports_estimate+supports_max_spend, or keep the compound flag but make the semantics "MUST support both."refinable_untilecho on variant leaves. Withrefinable_retention_secondsas a floor, there's no way for an autonomous buyer to know whether a specificbuild_variant_idis still refinable without speculatively calling and catchingREFERENCE_NOT_FOUND. Echo a per-leafrefinable_until(orexpires_at-style) onBuildCreativeVariantSuccess.creatives[].variants[].- No page-size ceiling on
params[].options[].expand_paramsreturns the first page of option values inline on every transformer in the result. A widebrief× 8 transformers ×voiceparam has no declared bound. Surface a default page size on the agent (or echooptions_per_page_maxonget_adcp_capabilities.creative) so agents can budget the call. creatives[].build_creative_idoptional. Defensible (lineage anchors onbuild_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.- Unrelated baseline ratchets.
scripts/oneof-discriminators.baseline.jsonalso bumps notes forverify-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. required-tasks.mdxenforcement.list_transformersis listed as Conditional (required whensupports_transformers: true). Verify the compliance grader treatssupports_transformers: truewithout a workinglist_transformersas a conformance break.
Minor nits (non-blocking)
index.jsoncatalog description drift. Thetransformer-paramentry's description instatic/schemas/source/index.json:419listsvalue_source inline|range|enumerable—free_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.keep_modenaming.keep_onereading 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-levelx-billing-noteor a tighter name (recommendation_mode/selection_hint) before adopters bake the wrong intuition into pricing UI.- "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. BuildCreativeMultiSuccessskips per-outputbuild_variant_id. Patch 5 adds it toBuildCreativeSuccessand 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>
There was a problem hiding this comment.
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
oneOfdisjointness onstatic/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) andBuildCreativeSubmitted.not.anyOf(L613-636) both correctly extended to excludecreativesandestimate.variant_dimensionsenum parity at all three sites —get-adcp-capabilities-response.json(creative.multiplicity.variant_dimensions),core/transformer.json(multiplicity.variant_dimensions), andbuild-creative-request.json(variant_axis.dimension) all carry the same["voice", "theme", "best_of_n", "transformer_config", "custom"]set.keep_mode↔keep_mode_applied— request enum matches response echo (keep_all|keep_one|keep_some).- Error-code parity for
UNPRICEABLE_OUTPUTandBUDGET_CAP_REACHED— present inerror-code.jsonenum,enumDescriptions, andenumMetadata(recovery + suggestion), plusheld-for-next-minordispositions inscripts/error-code-drift-dispositions.json. BuildCreativeSuccess— newbuild_variant_idis optional; required[] unchanged.- Request mutex —
allOf.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_keyonly). Confirmsminoris the right bump for all four changesets. x-entity-types.jsonaddstransformerandbuild_variant;index.jsonaddscore/transformer,core/transformer-param, and the newcreative/list-transformersrequest/response. No orphaned$refs.
Follow-ups (non-blocking — file as issues)
-
"fifth member" drift across narrative docs. PR body,
.changeset/creative-transformers.md, anddocs/reference/whats-new-in-3-1.mdxdescribeBuildCreativeVariantSuccessas "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) — whichdocs/creative/task-reference/build_creative.mdxand.changeset/build-creative-spend-controls.mdboth get right ("third success shape (oneOfmember 3 of 6)" / "6thoneOfmember"). The bundled-final count is what ships to consumers. -
Silent response-shape switch on
max_variants: 2. The<Warning>block atbuild_creative.mdx~L524 ("No fallback") is shouted on the response side; the request field descriptions onmax_creatives/max_variants/variant_axis/refine_from_build_variant_idinbuild-creative-request.jsondon't say "sending this flips the response shape — you MUST handlecreatives[]." Agent buyers reading only the request schema will be surprised. One-line addition per field. -
keep_modeenum is over-specified. Request and response carry three values (keep_all/keep_one/keep_some) but the only behavior change documented isrecommended/rankon/off, andkeep_onevskeep_someis indistinguishable to the seller. Worth considering a collapse torank_variants: booleanbefore 3.1 GA;keep_mode_appliedis an audit echo that exists to paper over the ambiguity. -
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.jsoncarries 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. -
transformer.multiplicityceiling rule is prose-only.core/transformer.jsonsays per-transformer ceilings "MUST NOT exceed" the agent-level ceilings andvariant_dimensionsMUST be a subset — neither is schema-enforceable across two documents. Either add a check to the conformance suite or soften to SHOULD. -
Variant
errorsvscreatives[].errors[]. The schema descriptions onBuildCreativeVariantSuccessshould explicitly distinguish the top-level (advisory) array from the per-creative (terminal per-item) one; the.mdxcalls it out, the schema doesn't. -
max_spendcaps 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)
-
Baseline note drift bundled in.
scripts/oneof-discriminators.baseline.jsonupdates forcreate-media-buy-response.json##/oneOf(now mentionsconfirmed_at,revision) andupdate-media-buy-response.json##/oneOf(now mentionsrevision) are legitimate refreshes — those fields are already required onmain, the notes were stale.audit-oneof.mjs --updatepicking them up is correct, but they're unrelated to transformers and could have been their own commit. Bundling is mildly noisy. -
vendor-pricing-option.applies_to_output_format_idsis "creative transformers only." Added to a schema also used by signals/rights/governance. Thegenerative-encoding-safe-additionschangeset already flags this for shared-schema owner ack — no action needed until that sign-off. -
The "Strictly additive" framing in the changeset is true for consumers but the new
allOf.not.requiredmutex ontarget_format_id/target_format_idsis 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.
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 (likeget_products), not buyer-held.Strictly additive. Existing
build_creativecallers are unaffected: all new request fields are optional, and the shippedBuildCreativeSuccess/BuildCreativeMultiSuccessresponse 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. Anexpand_paramsmode returns account-scoped enumerable option values (e.g. your configured voices) on the same tool — no separate options endpoint.transformer.json,transformer-param.json;x-entity: transformer.get_adcp_capabilities→creative.supports_transformersdiscriminator.build_creativeextensionstransformer_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 fromitem_limit),max_variants+variant_axis+keep_mode.BuildCreativeVariantSuccess—creatives[] → variants[], abuild_variant_idnamespace (distinct from previewpreview_idand servedvariant_id), per-leaf pricing receipt,items_total/items_returned. Per-format atomic; per-item non-atomic (a failed item is acreatives[]entry carryingerrors[]).recommended/rank. You pay for all produced variants (per_unit× N); a kept variant lazily earns acreative_idon trafficking, which flows toreport_usage.Pricing rides the existing
per_unitmodel + inline receipt +report_usage— transformers carrypricing_options(reusingvendor-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 theinput_format_ids/output_format_idsdiscovery filters onlist_creative_formats— all superseded bylist_transformers, which carries each transformer's own I/O signature and pricing.Design notes
variantis already AdCP's word for a kept, independently-tracked execution instance (creative-variant.json,get_creative_deliverycreatives[]→variants[],max_variants). Build-timebuild_variant_idand servedvariant_idare deliberately separate namespaces.output_format_ids.build_creativerather than adding atransform_creativetask —build_creativewas already ~90% of the surface (manifest in/out, modes, multi-format, async, preview, inline pricing). A canonical-onlytransform_creativewas 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