Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/calm-canvases-coordinate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@adcp/sdk': patch
---

Adopt AdCP 3.2.0-beta.6 schemas, including coordinated placements, seller-rendered stateful display, creative component assets, and A2A 1.0 request-signing method names.
2 changes: 1 addition & 1 deletion ADCP_VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.2.0-beta.5
3.2.0-beta.6
3 changes: 2 additions & 1 deletion docs/TYPE-SUMMARY.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# AdCP Type Summary

> Generated at: 2026-08-23
> Generated at: 2026-08-24
> @adcp/sdk v14.0.0-beta.7

Curated reference of the types that matter for using the AdCP client. For full generated types see `src/lib/types/tools.generated.ts` and `src/lib/types/core.generated.ts`.
Expand Down Expand Up @@ -900,6 +900,7 @@ _Request:_
start_date: string
end_date: string
include_package_daily_breakdown: boolean
requested_metrics: object[]
time_granularity: Reporting Frequency
include_window_breakdown: boolean
attribution_window: object
Expand Down
11 changes: 9 additions & 2 deletions docs/llms.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Ad Context Protocol (AdCP)

> Generated at: 2026-08-23
> Generated at: 2026-08-24
> Library: @adcp/sdk v14.0.0-beta.7
> AdCP major version: 3
> Canonical URL: https://adcontextprotocol.github.io/adcp-client/llms.txt
Expand Down Expand Up @@ -520,7 +520,7 @@ Request parameters for retrieving media buy status, creative approvals, and deli
Request parameters for retrieving comprehensive delivery metrics.

**Request:**
- Optional: `account: Account Ref`, `media_buy_ids: string[]`, `status_filter: Media Buy Status | object[]`, `start_date: string`, `end_date: string`, `include_package_daily_breakdown: boolean`, `time_granularity: Reporting Frequency`, `include_window_breakdown: boolean`, +3 more
- Optional: `account: Account Ref`, `media_buy_ids: string[]`, `status_filter: Media Buy Status | object[]`, `start_date: string`, `end_date: string`, `include_package_daily_breakdown: boolean`, `requested_metrics: object[]`, `time_granularity: Reporting Frequency`, +4 more

**Response (success branch):**
- Required: `reporting_period: object`, `currency: string`, `media_buy_deliveries: object[]`
Expand Down Expand Up @@ -1162,6 +1162,12 @@ Flow: `build_creative → sync_accounts → sync_governance → list_transformer
**Canonical format validate_input** — Validates 3.1 canonical-format dry-run semantics: structural pass/fail across canonical slots and unvalidatable_nondeterministic for seeded products.
Flow: `validate_input`

**CTV experience profile validate_input** — Validates AdCP 3.2 ctv_ad_experience matrix pairings, duration/interactivity constraint profiles, the menu focus/video pairing, and non-blocking activation-copy warnings via validate_input against seeded CTV products.
Flow: `validate_input`

**Premium display canonical validation** — Validates AdCP 3.2 seller_rendered_stateful_display supply-mode contracts (components, rendered_canvases, layered_source), single-state reveal shape, policy floors, and coordinated_placements sequence/serving_policy, alongside transition/canvas coverage and shared-slot resolution.
Flow: `validate_input`

### Campaign Governance

**Governance denial and human escalation** — Buyer's governance agent denies a media buy that exceeds spending authority, escalates to a human who approves with conditions.
Expand Down Expand Up @@ -1688,6 +1694,7 @@ Agents use the `recovery` classification to decide what to do: `transient` → r
| `FORMAT_NOT_SUPPORTED` | correctable | A requested creative operation route is not supported by this creative agent. On the canonical 3.2 path, returned when build_creative.target_capability_id(s), preview_creative.target_capability_id, or validate_input targets[] kind capability does not match an advertised creative.supported_formats[].capability_id carrying the requested operation. Also returned when preview renderer inference has zero or multiple compatible matches. Sellers SHOULD attribute the error to the selector field and MAY include supported capability IDs in error.details.supported_capability_ids when safe. Deprecated target_format_id(s) and preview format_id retain legacy named-format error attribution during the 3.x compatibility window. |
| `FORMAT_OPTION_UNRESOLVED` | correctable | Non-fatal advisory raised when a placement in `adagents.json` (or any consumer of `placement-definition.json`) carries `format_options[].format_option_id` referencing a `format_option_id` that does NOT exist in the file's top-level `formats[]`. The reference is broken — the publisher's catalog claims the placement accepts a format option that isn't declared. **Resolution scope is same-file only.** Cross-file `format_option_id` lookup is not supported by design (closes off format_option_id squatting across publisher boundaries — a malicious file cannot reference another publisher's format_option_id and claim its narrowing). Buyer SDKs MUST fail closed for the placement (drop the format from the placement's accepted format set) and MUST surface this code rather than silently dropping or guessing what the publisher meant. Surface placement: same single-mandate as the other FORMAT_* codes — SDKs that detect on consumption MUST augment the response's `errors[]` with `source: "sdk"`, `sdk_id`, `code: "FORMAT_OPTION_UNRESOLVED"`, `field` pointing at the offending placement (e.g., `placements[2].format_options[1].format_option_id`), and `error.details` SHOULD carry `{ placement_id, format_option_id, declared_format_options: [<list of format_option_ids actually in formats[]>] }` so the publisher can fix. |
| `FORMAT_PROJECTION_FAILED` | correctable | Non-fatal advisory raised when a legacy named format on a product cannot be projected to a canonical-formats `ProductFormatDeclaration` via the resolution order in `v1-canonical-mapping.json` (explicit `canonical` field → format_id_glob → structural match → fail-closed). The product is still valid on the legacy named-format path; only the 3.1+ `format_options` projection failed. Primarily a **consumer-SDK concern** — the seller didn't fail; the consumer-side SDK couldn't project on their behalf. `error.field` MUST point at the offending product (e.g., `products[3].format_ids[0]`); `error.details` SHOULD carry `{ format_id, product_id, resolution_failure: "no_explicit_canonical" | "no_registry_match" | "no_structural_match" }` so buyer SDKs can route remediation (suggest the seller add an explicit `canonical` field, or file a registry PR). **Surface placement (normative).** SDKs that detect this on consumption MUST augment the response's `errors[]` array with an entry carrying `source: "sdk"`, `sdk_id: "<package>@<version>"`, `code: "FORMAT_PROJECTION_FAILED"`, and the field+details described above. This is the single mandated surface — logger-only is insufficient and a separate lint-output channel is NOT acceptable (AdCP is a multi-hop agent network; warnings need to propagate across hops or each hop has to re-detect locally). Sellers MAY emit this code on their own response when they self-detect a non-projectable format on emit; producer-emitted entries omit `source` (or set `source: "producer"`). The response stays 200/success regardless of who emits; this is non-fatal. **Multi-hop deduplication.** Each hop that detects the same condition SHOULD deduplicate by `(code, field)` rather than re-emit. The existing entry's `sdk_id` identifies which earlier processor saw it first; downstream SDKs SHOULD NOT add a second entry for the same `(code, field)` pair unless they have materially different `error.details` (e.g., a different `resolution_failure` reason from a different registry version). See canonical-formats.mdx 'Dual emission and v2↔v1 projection' for the full rules. |
| `FORMAT_SHAPE_PROMOTED` | correctable | Non-fatal deprecation advisory raised when a 3.2-aware SDK encounters `format_kind: custom` with a `format_shape` that has been promoted to a first-class canonical. SDKs MUST preserve the declaration during its transition window and SHOULD augment the containing response's `errors[]` with `source: sdk`, `sdk_id`, this code, and `details: { format_shape, promoted_to, promotion_release, transition_end }`. Producers that self-detect their own legacy declaration MAY emit the same advisory with `source: producer`. Recovery is seller-side: dual-emit during the published transition window, migrate consumers, then replace the custom declaration with the promoted canonical. |
| `GOVERNANCE_DENIED` | correctable | A registered governance agent denied the transaction. Sellers MUST place the denial in the operation's structured rejection arm when one exists (e.g., `acquire_rights` → `AcquireRightsRejected`, or an `approval_webhook` delivery → `CreativeRejected`); otherwise in `errors[]` + `adcp_error`. Buyers MUST dispatch on the response's discriminated `status` first and fall back to `errors[].code` / `adcp_error.code` only when no rejection arm exists for that operation. The buyer may restructure the buy (e.g., reduce budget, split into smaller transactions), escalate to human spending authority, or contact the governance agent for details. Wire placement (full guidance). Governance denial is a structured business outcome, not a system error — the governance call SUCCEEDED and the agent returned a denial verdict. Two cases: 1. Operation or webhook payload defines a structured rejection arm. The arm IS the canonical denial shape. The seller populates `reason` (human-readable, propagating governance findings) and `suggestions` (optional) and does NOT additionally emit `GOVERNANCE_DENIED` in `errors[]` or `adcp_error`. The rejection arms enforce this at the schema layer: e.g., `AcquireRightsRejected` and `CreativeRejected` both declare `not: { required: [errors] }`, so dual-emission is already a schema violation. The code does not appear on the wire when the rejection arm is used. Transport-level success markers MUST NOT be flipped (HTTP 200, MCP `isError: false`, A2A `succeeded`) — the operation completed successfully and produced a structured response. 2. Operation response has no rejection arm (e.g., `create_media_buy` returns Success / Error / Submitted arms only). The seller populates `errors[].code: GOVERNANCE_DENIED` in the payload AND `adcp_error.code: GOVERNANCE_DENIED` on the envelope per the two-layer model in `error-handling.mdx#envelope-vs-payload-errors-the-two-layer-model`. Transport-level failure markers DO flip in this case (HTTP 4xx, MCP `isError: true`, A2A `failed`) — the task could not produce a success artifact. The rule generalizes to any current or future operation or webhook payload whose response defines a discriminated rejection arm. In either placement, sellers SHOULD propagate governance findings verbatim — buyers' recovery decisions depend on what specifically was rejected. `GOVERNANCE_DENIED` is reserved for verdicts received from a reachable governance agent; if the governance call itself failed (timeout, network, config error), use `GOVERNANCE_UNAVAILABLE` instead. |
| `GOVERNANCE_UNAVAILABLE` | transient | A registered governance agent is unreachable. Sellers MUST place this code in `errors[]` + `adcp_error` (never a structured rejection arm) and flip transport-level failure markers (HTTP 5xx, MCP `isError: true`, A2A `failed`). Distinct from `GOVERNANCE_DENIED` (agent reachable and explicitly denied — see that code's wire-placement guidance). Wire placement (full guidance). Governance unavailability is a system error — the governance call FAILED (timeout, network, config error) and the seller could not get a verdict at all. Always populate both layers per the two-layer model in `error-handling.mdx#envelope-vs-payload-errors-the-two-layer-model`. Do NOT use a structured rejection arm for unavailability even when the task offers one — the buyer's recovery semantics differ (retry-with-backoff for unavailability vs. restructure-or-escalate for denial), and conflating them masks the system-error signal. |
| `IDEMPOTENCY_CONFLICT` | correctable | An earlier request with the same idempotency_key was processed with a different canonical payload within the seller's replay window. Distinct from CONFLICT (concurrent write) — this indicates the client reused a key across semantically different requests. |
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -672,7 +672,7 @@
},
"minimatch": "^10.2.1"
},
"adcp_version": "3.2.0-beta.5",
"adcp_version": "3.2.0-beta.6",
"sideEffects": [
"**/default-invariants.*"
]
Expand Down
39 changes: 27 additions & 12 deletions scripts/generate-zod-from-ts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,19 @@ const TS7056_SCHEMAS: Array<{ name: string; tsType?: string; objectShape?: boole
{ name: 'WholesaleFeedWebhookSchema' },
{ name: 'ComplyTestControllerRequestSchema', objectShape: true },
{ name: 'ListCreativesResponseSchema' },
// 3.2.0-beta.6 adds two deeply nested canonical presentation formats.
// Their inlined appearances push these public validation schemas over the
// declaration serializer limit; none are used for shape-based composition.
{ name: 'ProductFormatDeclarationSchema' },
{ name: 'PlacementSchema' },
{ name: 'FormatSchema' },
{ name: 'TransformerSchema' },
{ name: 'AvailablePackageSchema' },
{ name: 'ListCreativeFormatsResponseSchema' },
{ name: 'PackageStatusSchema' },
{ name: 'ListTransformersResponseCreativeAgentSchema' },
{ name: 'GetAdCPCapabilitiesResponseSchema' },
{ name: 'ListTransformersResponseSchema' },
];

function postProcessTS7056Annotations(content: string): string {
Expand Down Expand Up @@ -1170,18 +1183,8 @@ function postProcessCreativeRuntimeConstraints(content: string): string {
preserveCreativeConstraints('CreativeAssetSchema');
preserveCreativeConstraints('CreativeManifestSchema');

const creativeManifest = schemaBlock('CreativeManifestSchema');
const assetValueSchema = `const CreativeAssetValueSchema: z.ZodType = z.unknown().superRefine((value, ctx) => {
const variants = Array.isArray(value) ? value : [value];
if (variants.length === 0 || variants.some(variant => !AssetVariantSchema.safeParse(variant).success)) {
ctx.addIssue({
code: "custom",
message: "creative slot must contain an asset or non-empty array of assets"
});
}
});

const CreativeAssetsSchema: z.ZodType<Record<string, unknown>> = z.record(z.string(), z.unknown()).superRefine((assets, ctx) => {
const creativeAssets = schemaBlock('CreativeAssetsSchema');
const creativeAssetsSchema = `export const CreativeAssetsSchema: z.ZodType<Record<string, unknown>> = z.record(z.string(), z.unknown()).superRefine((assets, ctx) => {
for (const [slotKey, assetValue] of Object.entries(assets)) {
if (/^[a-z0-9_]+$/.test(slotKey) && !CreativeAssetValueSchema.safeParse(assetValue).success) {
ctx.addIssue({
Expand All @@ -1191,6 +1194,18 @@ const CreativeAssetsSchema: z.ZodType<Record<string, unknown>> = z.record(z.stri
});
}
}
});`;
content = content.slice(0, creativeAssets.start) + creativeAssetsSchema + content.slice(creativeAssets.end);

const creativeManifest = schemaBlock('CreativeManifestSchema');
const assetValueSchema = `const CreativeAssetValueSchema: z.ZodType = z.unknown().superRefine((value, ctx) => {
const variants = Array.isArray(value) ? value : [value];
if (variants.length === 0 || variants.some(variant => !AssetVariantSchema.safeParse(variant).success)) {
ctx.addIssue({
code: "custom",
message: "creative slot must contain an asset or non-empty array of assets"
});
}
});

`;
Expand Down
2 changes: 1 addition & 1 deletion skills/adcp-creative/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ The Creative Protocol provides standardized discovery, build, and preview tasks:

## Canonical formats (AdCP 3.2)

Products and manifests use 12 canonical `format_kind` values: `image`, `html5`, `display_tag`, `image_carousel`, `video_hosted`, `video_vast`, `audio_hosted`, `audio_daast`, `sponsored_placement`, `native_in_feed`, `responsive_creative`, and `agent_placement`. Use `custom` only for a shape outside those canonicals, with required `format_shape` and `format_schema`.
Products and manifests use 14 canonical `format_kind` values: `image`, `html5`, `display_tag`, `image_carousel`, `video_hosted`, `video_vast`, `audio_hosted`, `audio_daast`, `sponsored_placement`, `native_in_feed`, `responsive_creative`, `agent_placement`, `seller_rendered_stateful_display`, and `coordinated_placements`. Use `custom` only for a shape outside those canonicals, with required `format_shape` and `format_schema`.

A `ProductFormatDeclaration` carries:

Expand Down
2 changes: 1 addition & 1 deletion skills/adcp-media-buy/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ The Media Buy Protocol provides these common standardized tasks:

Products carry `format_options[]`: a list of `ProductFormatDeclaration` entries describing the creative shapes the product accepts. Each declaration carries:

- `format_kind` — one of the 12 canonicals: `image`, `html5`, `display_tag`, `image_carousel`, `video_hosted`, `video_vast`, `audio_hosted`, `audio_daast`, `sponsored_placement`, `native_in_feed`, `responsive_creative`, or `agent_placement`; use `custom` only with `format_shape` and `format_schema`
- `format_kind` — one of the 14 canonicals: `image`, `html5`, `display_tag`, `image_carousel`, `video_hosted`, `video_vast`, `audio_hosted`, `audio_daast`, `sponsored_placement`, `native_in_feed`, `responsive_creative`, `agent_placement`, `seller_rendered_stateful_display`, or `coordinated_placements`; use `custom` only with `format_shape` and `format_schema`
- `params` — per-canonical parameters narrowing the format (dimensions, durations, codecs, char limits, CTA enums)
- Optional `format_option_id` — disambiguates product options and identifies publisher-catalog declarations when paired with `publisher_domain`
- Optional `v1_format_ref: [{agent_url, id}]` — array linking this v2 declaration to one or more v1 named formats (for dual emission during the v1↔v2 migration). Multi-size declarations should carry one ref per size
Expand Down
25 changes: 25 additions & 0 deletions src/lib/conformance/schemaArbitrary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -527,11 +527,36 @@ function enforceSimpleConditionals(value: Record<string, unknown>, schema: JsonS
for (const entry of (schema.allOf as JsonSchema[] | undefined) ?? []) {
current = enforceRequiredTriggerConst(current, entry);
current = avoidUnsatisfiedConstConditional(current, entry, schema);
current = enforceElseForbidden(current, entry);
}
current = enforceConstThenForbidden(current, schema);
return current;
}

/**
* Handle the common `if const / else not-required` shape by removing fields
* that are only legal when the discriminator matches. Beta.6 creative
* manifests use this to reserve `component_assets` for coordinated placements.
*/
function enforceElseForbidden(value: Record<string, unknown>, conditional: JsonSchema): Record<string, unknown> {
const ifSchema = conditional.if as JsonSchema | undefined;
const ifProps = ifSchema?.properties as Record<string, JsonSchema> | undefined;
const triggerKeys = Array.isArray(ifSchema?.required) ? (ifSchema.required as string[]) : [];
const elseNot = (conditional.else as JsonSchema | undefined)?.not as JsonSchema | undefined;
const forbidden = Array.isArray(elseNot?.required) ? (elseNot.required as string[]) : [];
if (!ifProps || triggerKeys.length === 0 || forbidden.length === 0) return value;

const matches = triggerKeys.every(key => {
const prop = ifProps[key];
return key in value && prop && typeof prop === 'object' && 'const' in prop && value[key] === prop.const;
});
if (matches) return value;

const next = { ...value };
for (const key of forbidden) delete next[key];
return next;
}

/**
* Optional selector fields commonly activate a required sibling (or a
* required nested field) through if/then. When the random base record did
Expand Down
Loading
Loading