diff --git a/.changeset/assets-discovery.md b/.changeset/assets-discovery.md new file mode 100644 index 00000000..43d94fd6 --- /dev/null +++ b/.changeset/assets-discovery.md @@ -0,0 +1,11 @@ +--- +"adcontextprotocol": minor +--- + +Add unified `assets` field to format schema for better asset discovery + +- Add new `assets` array to format schema with `required` boolean per asset +- Deprecate `assets_required` (still supported for backward compatibility) +- Enables full asset discovery for buyers and AI agents to see all supported assets +- Optional assets like impression trackers can now be discovered and used + diff --git a/.github/workflows/broken-links.yml b/.github/workflows/broken-links.yml index b8b2bce7..8b3045af 100644 --- a/.github/workflows/broken-links.yml +++ b/.github/workflows/broken-links.yml @@ -2,9 +2,9 @@ name: Check for Broken Links on: push: - branches: [ main, develop ] + branches: [ main, develop, '2.6.x' ] pull_request: - branches: [ main, develop ] + branches: [ main, develop, '2.6.x' ] jobs: broken-links: diff --git a/.github/workflows/changeset-check.yml b/.github/workflows/changeset-check.yml index 346ed620..267de5f1 100644 --- a/.github/workflows/changeset-check.yml +++ b/.github/workflows/changeset-check.yml @@ -2,7 +2,7 @@ name: Changeset Check on: pull_request: - branches: [main] + branches: [main, '2.6.x'] jobs: check: diff --git a/.github/workflows/check-testable-snippets.yml b/.github/workflows/check-testable-snippets.yml index 7dea6c92..f28614b7 100644 --- a/.github/workflows/check-testable-snippets.yml +++ b/.github/workflows/check-testable-snippets.yml @@ -2,6 +2,7 @@ name: Check Testable Snippets on: pull_request: + branches: [main, '2.6.x'] paths: - 'docs/**/*.md' - 'docs/**/*.mdx' @@ -26,7 +27,7 @@ jobs: if [ -s changed_files.txt ]; then echo "📋 Checking documentation changes for testable snippets..." - node scripts/check-testable-snippets.js + node scripts/check-testable-snippets.cjs else echo "✓ No documentation files changed" fi diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c5665ce6..176da7a2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,6 +4,7 @@ on: push: branches: - main + - '2.6.x' concurrency: ${{ github.workflow }}-${{ github.ref }} diff --git a/.github/workflows/schema-validation.yml b/.github/workflows/schema-validation.yml index 7d772503..a599e1ad 100644 --- a/.github/workflows/schema-validation.yml +++ b/.github/workflows/schema-validation.yml @@ -2,9 +2,9 @@ name: JSON Schema Validation on: push: - branches: [ main, develop ] + branches: [ main, develop, '2.6.x' ] pull_request: - branches: [ main, develop ] + branches: [ main, develop, '2.6.x' ] jobs: schema-validation: diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ac14daf..82e40e15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,44 @@ # Changelog +## 2.6.0 + +### Minor Changes + +- Add unified `assets` field to format schema for better asset discovery + + **Schema Changes:** + + - **format.json**: Add new `assets` array field that includes both required and optional assets + - **format.json**: Deprecate `assets_required` (still supported for backward compatibility) + + **Rationale:** + + Previously, buyers and AI agents could only see required assets via `assets_required`. There was no way to discover optional assets that enhance creatives (companion banners, third-party tracking pixels, etc.). + + Since each asset already has a `required` boolean field, we introduced a unified `assets` array where: + - `required: true` - Asset MUST be provided for a valid creative + - `required: false` - Asset is optional, enhances the creative when provided + + This enables: + - **Full asset discovery**: Buyers and AI agents can see ALL assets a format supports + - **Richer creatives**: Optional assets like impression trackers can now be discovered and used + - **Cleaner schema**: Single array instead of two separate arrays + + **Example:** + + ```json + { + "format_id": { "agent_url": "https://creative.adcontextprotocol.org", "id": "video_30s" }, + "assets": [ + { "item_type": "individual", "asset_id": "video_file", "asset_type": "video", "required": true }, + { "item_type": "individual", "asset_id": "end_card", "asset_type": "image", "required": false }, + { "item_type": "individual", "asset_id": "impression_tracker", "asset_type": "url", "required": false } + ] + } + ``` + + **Migration:** Non-breaking change. `assets_required` is deprecated but still supported. New implementations should use `assets`. + ## 2.5.1 ### Patch Changes diff --git a/docs/creative/asset-types.mdx b/docs/creative/asset-types.mdx index c3d58aa6..927fbedf 100644 --- a/docs/creative/asset-types.mdx +++ b/docs/creative/asset-types.mdx @@ -338,7 +338,9 @@ The keys in the assets object correspond to the `asset_id` values defined in the ## Usage in Creative Formats -Creative formats specify their required assets using `assets_required` (an array): +Creative formats specify their assets using the `assets` array. Each asset has a `required` boolean: +- **`required: true`** - Asset MUST be provided for a valid creative +- **`required: false`** - Asset is optional, enhances the creative (e.g., companion banners, third-party tracking pixels) ```json { @@ -346,7 +348,7 @@ Creative formats specify their required assets using `assets_required` (an array "agent_url": "https://creative.adcontextprotocol.org", "id": "video_15s_hosted" }, - "assets_required": [ + "assets": [ { "item_type": "individual", "asset_id": "video_file", @@ -359,11 +361,41 @@ Creative formats specify their required assets using `assets_required` (an array "acceptable_resolutions": ["1920x1080", "1280x720"], "max_file_size_mb": 30 } + }, + { + "item_type": "individual", + "asset_id": "impression_tracker", + "asset_type": "url", + "required": false, + "requirements": { + "format": ["url"], + "description": "Third-party impression tracking pixel URL" + } + } + ], + + // DEPRECATED: Use "assets" above instead. Kept for backward compatibility. + "assets_required": [ + { + "item_type": "individual", + "asset_id": "video_file", + "asset_type": "video", + "requirements": { + "duration_seconds": 15, + "acceptable_formats": ["mp4"], + "acceptable_codecs": ["h264"], + "acceptable_resolutions": ["1920x1080", "1280x720"], + "max_file_size_mb": 30 + } } ] } ``` + +**Backward Compatibility**: The `assets_required` field is deprecated but still supported. Existing implementations can continue using `assets_required` for required assets only. New implementations should use `assets` with the `required` boolean for full asset discovery. + + ## Repeatable Asset Groups For formats with asset sequences (like carousels, slideshows, stories), see the [Carousel & Multi-Asset Formats guide](/docs/creative/channels/carousels) for complete documentation on repeatable asset group patterns. diff --git a/docs/creative/formats.mdx b/docs/creative/formats.mdx index 54ae1f1c..ba6b100d 100644 --- a/docs/creative/formats.mdx +++ b/docs/creative/formats.mdx @@ -222,7 +222,7 @@ Formats are JSON objects with the following key fields: }, "name": "30-Second Hosted Video", "type": "video", - "assets_required": [ + "assets": [ { "item_type": "individual", "asset_id": "video_file", @@ -234,6 +234,53 @@ Formats are JSON objects with the following key fields: "format": ["MP4"], "resolution": ["1920x1080", "1280x720"] } + }, + { + "item_type": "individual", + "asset_id": "end_card_image", + "asset_type": "image", + "asset_role": "end_card", + "required": false, + "requirements": { + "dimensions": "1920x1080", + "format": ["PNG", "JPG"] + } + }, + { + "item_type": "individual", + "asset_id": "companion_banner", + "asset_type": "image", + "asset_role": "companion", + "required": false, + "requirements": { + "dimensions": "300x250", + "format": ["PNG", "JPG", "GIF"] + } + }, + { + "item_type": "individual", + "asset_id": "impression_tracker", + "asset_type": "url", + "asset_role": "third_party_tracking", + "required": false, + "requirements": { + "description": "Third-party impression tracking pixel URL" + } + } + ], + + // DEPRECATED: Use "assets" above instead. Kept for backward compatibility. + "assets_required": [ + { + "item_type": "individual", + "asset_id": "video_file", + "asset_type": "video", + "asset_role": "hero_video", + "requirements": { + "duration": "30s", + "format": ["MP4"], + "resolution": ["1920x1080", "1280x720"] + } } ] } @@ -243,10 +290,24 @@ Formats are JSON objects with the following key fields: - **format_id**: Unique identifier (may be namespaced with `domain:id`) - **agent_url**: The creative agent authoritative for this format - **type**: Category (video, display, audio, native, dooh, rich_media) -- **assets_required**: Array of asset specifications +- **assets**: Array of all asset specifications with `required` boolean indicating mandatory vs optional +- **assets_required**: *(Deprecated)* Array of required asset specifications - use `assets` instead - **asset_role**: Identifies asset purpose (hero_image, logo, cta_button, etc.) - **renders**: Array of rendered outputs with dimensions - see below +### Asset Discovery + +The `assets` array enables comprehensive asset discovery. Each asset has a `required` boolean: + +- **`required: true`** - Asset MUST be provided for a valid creative +- **`required: false`** - Asset is optional, enhances the creative when provided (e.g., companion banners, third-party tracking pixels) + +This unified approach helps creative tools and AI agents understand the full capabilities of a format, enabling richer creative experiences when optional assets are available while clearly indicating minimum requirements. + + +**Backward Compatibility**: The `assets_required` field is deprecated but still supported. Existing implementations can continue using `assets_required` for required assets only. New implementations should use `assets` with the `required` boolean on each asset for full asset discovery. + + ### Rendered Outputs and Dimensions Formats specify their rendered outputs via the `renders` array. Most formats produce a single render, but some (companion ads, adaptive formats, multi-placement) produce multiple renders: diff --git a/docs/creative/implementing-creative-agents.mdx b/docs/creative/implementing-creative-agents.mdx index 98671f1e..abb86bb5 100644 --- a/docs/creative/implementing-creative-agents.mdx +++ b/docs/creative/implementing-creative-agents.mdx @@ -100,7 +100,7 @@ Creative agents must implement these two tasks: Return all formats your agent defines. This is how buyers discover what creative formats you support. **Key responsibilities:** -- Return complete format definitions with all `assets_required` +- Return complete format definitions with all `assets` (both required and optional) - Include your `agent_url` in each format - Use proper namespacing for `format_id` values @@ -169,7 +169,7 @@ When validating manifests: When updating format definitions: -- **Additive changes** (new optional assets) are safe +- **Additive changes** (new optional assets with `required: false` in `assets`) are safe - **Breaking changes** (removing assets, changing requirements) require new format_id - Consider versioning: `youragency.com:format_name_v2` - Maintain backward compatibility when possible diff --git a/docs/creative/task-reference/list_creative_formats.mdx b/docs/creative/task-reference/list_creative_formats.mdx index 648c6f67..af0348f7 100644 --- a/docs/creative/task-reference/list_creative_formats.mdx +++ b/docs/creative/task-reference/list_creative_formats.mdx @@ -56,7 +56,7 @@ Formats may produce multiple rendered pieces (e.g., video + companion banner). D | Field | Description | |-------|-------------| -| `formats` | Array of full format definitions (format_id, name, type, requirements, assets_required, renders) | +| `formats` | Array of full format definitions (format_id, name, type, requirements, assets, assets_required, renders) | | `creative_agents` | Optional array of other creative agents providing additional formats | See [Format schema](https://adcontextprotocol.org/schemas/v2/core/format.json) for complete format object structure. @@ -421,7 +421,8 @@ Each format includes: | `name` | Human-readable format name | | `type` | Format type (audio, video, display, dooh) | | `requirements` | Technical requirements (duration, file types, bitrate, etc.) | -| `assets_required` | Array of required assets with specifications | +| `assets` | Array of all assets with `required` boolean indicating mandatory vs optional | +| `assets_required` | *(Deprecated)* Array of required assets - use `assets` instead | | `renders` | Array of rendered output pieces (dimensions, role) | ### Asset Roles diff --git a/package-lock.json b/package-lock.json index c249c1f9..9218d630 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "adcontextprotocol", - "version": "2.5.1", + "version": "2.6.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "adcontextprotocol", - "version": "2.5.1", + "version": "2.6.0", "dependencies": { "@adcp/client": "^3.4.0", "@anthropic-ai/sdk": "^0.71.2", @@ -12010,7 +12010,7 @@ "license": "MIT" }, "node_modules/fast-memoize": { - "version": "2.5.2", + "version": "2.6.0", "resolved": "https://registry.npmjs.org/fast-memoize/-/fast-memoize-2.5.2.tgz", "integrity": "sha512-Ue0LwpDYErFbmNnZSF0UH6eImUwDmogUO1jyE+JbN2gsQz/jICm1Ve7t9QT0rNSsfJt+Hs4/S3GnsDVjL4HVrw==", "dev": true, @@ -21123,6 +21123,16 @@ "dev": true, "license": "MIT" }, + "node_modules/standardwebhooks": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz", + "integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==", + "license": "MIT", + "dependencies": { + "@stablelib/base64": "^1.0.0", + "fast-sha256": "^1.3.0" + } + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -21557,13 +21567,12 @@ } }, "node_modules/svix": { - "version": "1.82.0", - "resolved": "https://registry.npmjs.org/svix/-/svix-1.82.0.tgz", - "integrity": "sha512-K2M7yFSzuwJVPxi2/I5R9STofIQ8gO4PZ+ptZ5RB+zhTNHO12UtYk+uuuA2wIQ4wCj3GYY1WhvKYDeYqptIwKg==", + "version": "1.84.1", + "resolved": "https://registry.npmjs.org/svix/-/svix-1.84.1.tgz", + "integrity": "sha512-K8DPPSZaW/XqXiz1kEyzSHYgmGLnhB43nQCMeKjWGCUpLIpAMMM8kx3rVVOSm6Bo6EHyK1RQLPT4R06skM/MlQ==", "license": "MIT", "dependencies": { - "@stablelib/base64": "^1.0.0", - "fast-sha256": "^1.3.0", + "standardwebhooks": "1.0.0", "uuid": "^10.0.0" } }, diff --git a/package.json b/package.json index 58c04491..3a3d4aa1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "adcontextprotocol", - "version": "2.5.1", + "version": "2.6.0", "private": true, "type": "module", "scripts": { diff --git a/server/src/http.ts b/server/src/http.ts index 0f46048f..5cb4fe83 100644 --- a/server/src/http.ts +++ b/server/src/http.ts @@ -5956,18 +5956,23 @@ Disallow: /api/admin/ return res.json({ success: true, - formats: formats.map(format => ({ - format_id: format.format_id, - name: format.name, - type: format.type, - description: format.description, - preview_image: format.preview_image, - example_url: format.example_url, - renders: format.renders, - assets_required: format.assets_required, - output_format_ids: format.output_format_ids, - agent_url: format.agent_url, - })), + formats: formats.map(format => { + // Cast to allow 'assets' field (added in schema v2.5.2, @adcp/client may not have it yet) + const formatWithAssets = format as typeof format & { assets?: unknown }; + return { + format_id: format.format_id, + name: format.name, + type: format.type, + description: format.description, + preview_image: format.preview_image, + example_url: format.example_url, + renders: format.renders, + assets_required: format.assets_required, // deprecated but kept for backward compatibility + assets: formatWithAssets.assets, // new unified field + output_format_ids: format.output_format_ids, + agent_url: format.agent_url, + }; + }), }); } catch (error) { logger.error({ err: error, url }, 'Agent formats fetch error'); diff --git a/static/schemas/source/core/format.json b/static/schemas/source/core/format.json index d778d5de..a018252d 100644 --- a/static/schemas/source/core/format.json +++ b/static/schemas/source/core/format.json @@ -121,7 +121,8 @@ }, "assets_required": { "type": "array", - "description": "Array of required assets or asset groups for this format. Each asset is identified by its asset_id, which must be used as the key in creative manifests. Can contain individual assets or repeatable asset sequences (e.g., carousel products, slideshow frames).", + "deprecated": true, + "description": "DEPRECATED: Use 'assets' instead. Array of required assets or asset groups for this format. Each asset is identified by its asset_id, which must be used as the key in creative manifests. Can contain individual assets or repeatable asset sequences (e.g., carousel products, slideshow frames). This field is maintained for backward compatibility; new implementations should use 'assets' with the 'required' boolean on each asset.", "items": { "oneOf": [ { @@ -217,6 +218,109 @@ ] } }, + "assets": { + "type": "array", + "description": "Array of all assets supported for this format. Each asset is identified by its asset_id, which must be used as the key in creative manifests. Use the 'required' boolean on each asset to indicate whether it's mandatory. This field replaces the deprecated 'assets_required' and enables full asset discovery for buyers and AI agents.", + "items": { + "oneOf": [ + { + "description": "Individual asset specification", + "type": "object", + "properties": { + "item_type": { + "type": "string", + "const": "individual", + "description": "Discriminator indicating this is an individual asset" + }, + "asset_id": { + "type": "string", + "description": "Unique identifier for this asset. Creative manifests MUST use this exact value as the key in the assets object." + }, + "asset_type": { + "$ref": "/schemas/enums/asset-content-type.json", + "description": "Type of asset" + }, + "asset_role": { + "type": "string", + "description": "Optional descriptive label for this asset's purpose (e.g., 'hero_image', 'logo', 'third_party_tracking'). Not used for referencing assets in manifests—use asset_id instead. This field is for human-readable documentation and UI display only." + }, + "required": { + "type": "boolean", + "description": "Whether this asset is required (true) or optional (false). Required assets must be provided for a valid creative. Optional assets enhance the creative but are not mandatory." + }, + "requirements": { + "type": "object", + "description": "Technical requirements for this asset (dimensions, file size, duration, etc.). For template formats, use parameters_from_format_id: true to indicate asset parameters must match the format_id parameters (width/height/unit and/or duration_ms).", + "additionalProperties": true + } + }, + "required": ["item_type", "asset_id", "asset_type", "required"] + }, + { + "description": "Repeatable asset group (for carousels, slideshows, playlists, etc.)", + "type": "object", + "properties": { + "item_type": { + "type": "string", + "const": "repeatable_group", + "description": "Discriminator indicating this is a repeatable asset group" + }, + "asset_group_id": { + "type": "string", + "description": "Identifier for this asset group (e.g., 'product', 'slide', 'card')" + }, + "required": { + "type": "boolean", + "description": "Whether this asset group is required. If true, at least min_count repetitions must be provided." + }, + "min_count": { + "type": "integer", + "description": "Minimum number of repetitions required (if group is required) or allowed (if optional)", + "minimum": 0 + }, + "max_count": { + "type": "integer", + "description": "Maximum number of repetitions allowed", + "minimum": 1 + }, + "assets": { + "type": "array", + "description": "Assets within each repetition of this group", + "items": { + "type": "object", + "properties": { + "asset_id": { + "type": "string", + "description": "Identifier for this asset within the group" + }, + "asset_type": { + "$ref": "/schemas/enums/asset-content-type.json", + "description": "Type of asset" + }, + "asset_role": { + "type": "string", + "description": "Optional descriptive label for this asset's purpose. Not used for referencing assets in manifests—use asset_id instead. This field is for human-readable documentation and UI display only." + }, + "required": { + "type": "boolean", + "description": "Whether this asset is required within each repetition of the group", + "default": false + }, + "requirements": { + "type": "object", + "description": "Technical requirements for this asset. For template formats, use parameters_from_format_id: true to indicate asset parameters must match the format_id parameters (width/height/unit and/or duration_ms).", + "additionalProperties": true + } + }, + "required": ["asset_id", "asset_type", "required"] + } + } + }, + "required": ["item_type", "asset_group_id", "required", "min_count", "max_count", "assets"] + } + ] + } + }, "delivery": { "type": "object", "description": "Delivery method specifications (e.g., hosted, VAST, third-party tags)", diff --git a/static/schemas/source/index.json b/static/schemas/source/index.json index 2056c90c..6ea9e42a 100644 --- a/static/schemas/source/index.json +++ b/static/schemas/source/index.json @@ -4,7 +4,7 @@ "title": "AdCP Schema Registry v1", "version": "1.0.0", "description": "Registry of all AdCP JSON schemas for validation and discovery", - "adcp_version": "2.5.1", + "adcp_version": "2.6.0", "standard_formats_version": "2.0.0", "versioning": { "note": "AdCP uses path-based versioning. The schema URL path (/schemas/) indicates the version. Individual request/response schemas do NOT include adcp_version fields. Compatibility follows semantic versioning rules."