From f4e9dfcc27ad47937ee404d09a967435497566db Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Fri, 29 May 2026 04:45:35 -0400 Subject: [PATCH 1/2] Add creative canonical supported formats storyboard --- server/src/training-agent/task-handlers.ts | 171 +++++++++++--- server/tests/unit/training-agent.test.ts | 77 +++++++ .../canonical_supported_formats.yaml | 212 ++++++++++++++++++ 3 files changed, 426 insertions(+), 34 deletions(-) create mode 100644 static/compliance/source/protocols/creative/scenarios/canonical_supported_formats.yaml diff --git a/server/src/training-agent/task-handlers.ts b/server/src/training-agent/task-handlers.ts index 2bbece0b97..a4b080f7d1 100644 --- a/server/src/training-agent/task-handlers.ts +++ b/server/src/training-agent/task-handlers.ts @@ -223,6 +223,16 @@ const CANONICAL_FORMAT_SLOTS: Record = { { asset_group_id: 'landing_page_url', asset_type: 'url' }, ], }; +const BUILD_CREATIVE_FORMAT_ALIASES: Record = { + display_300x250_generative: 'display_300x250', + display_728x90_generative: 'display_728x90', + display_320x50_generative: 'display_320x50', + audio_30s: 'audio_spot', + vast_30s: 'video_preroll', +}; +const SUPPORTED_CANONICAL_BUILD_CAPABILITIES = [ + { capabilityId: 'training_image_generation', formatKind: 'image' }, +] as const; const MAX_VALIDATE_INPUT_TARGETS = 50; const VALID_CANONICAL_FORMAT_KINDS = new Set([...Object.keys(CANONICAL_FORMAT_SLOTS), 'custom']); const NONDETERMINISTIC_INCOMPATIBLE_SOURCES = new Set(['buyer_uploaded', 'publisher_host_recorded']); @@ -2019,16 +2029,25 @@ function wholesaleCapabilityProfile(ctx: TrainingContext): { } function supportedCanonicalFormatsCapability(): Array> { - return Object.entries(CANONICAL_FORMAT_SLOTS).map(([formatKind, slots]) => ({ + return SUPPORTED_CANONICAL_BUILD_CAPABILITIES.map(({ capabilityId, formatKind }) => ({ + capability_id: capabilityId, format: { format_kind: formatKind, params: { - slots: slots.map(slot => ({ ...slot })), + slots: CANONICAL_FORMAT_SLOTS[formatKind].map(slot => ({ ...slot })), }, }, })); } +function supportedCanonicalBuildCapability(formatId: string): { formatKind: string; slots: CanonicalSlot[] } | undefined { + const capability = SUPPORTED_CANONICAL_BUILD_CAPABILITIES.find(item => item.capabilityId === formatId); + if (!capability) return undefined; + const { formatKind } = capability; + const slots = CANONICAL_FORMAT_SLOTS[formatKind]; + return slots ? { formatKind, slots } : undefined; +} + function signalMatchesRef( signal: ReturnType[number], ref: Record, @@ -5923,6 +5942,12 @@ interface BuildCreativeArgs { message?: string; } +type ResolvedBuildTarget = { + requested: FormatID; + format?: { renders: Array> }; + formatKind?: NonNullable; +}; + function getDimensions(format: { renders: Array> } | undefined): { w: number; h: number } { const dims = format?.renders?.[0]?.dimensions as { width?: number; height?: number } | undefined; return { w: dims?.width || 300, h: dims?.height || 250 }; @@ -5939,6 +5964,26 @@ function buildCreativeCompleted(payload: T): T & { status: 'co return { status: 'completed', ...payload }; } +function buildCanonicalImageAssets(formatId: string, dimensions: { w: number; h: number }): AdcpCreativeManifest['assets'] { + return { + image_main: { + asset_type: 'image', + url: 'https://test-assets.adcontextprotocol.org/acme-outdoor/banner_300x250.jpg', + width: dimensions.w, + height: dimensions.h, + alt_text: `Generated image creative for ${formatId}`, + }, + headline: { + asset_type: 'text', + content: 'Trail-ready gear for every summit', + }, + landing_page_url: { + asset_type: 'url', + url: 'https://acmeoutdoor.example/trail-pro', + }, + }; +} + export async function handleBuildCreative(args: ToolArgs, ctx: TrainingContext): Promise; governance_context?: string }> { const req = args as unknown as BuildCreativeArgs; const session = await getSession(sessionKeyFromArgs(req as unknown as ToolArgs, ctx.mode, ctx.userId, ctx.moduleId)); @@ -5947,6 +5992,60 @@ export async function handleBuildCreative(args: ToolArgs, ctx: TrainingContext): const rawGovCtx = (req as unknown as Record).governance_context; const governanceContext = typeof rawGovCtx === 'string' && rawGovCtx.length <= 4096 ? rawGovCtx : undefined; const validFormatIds = new Map(formats.map(f => [f.format_id.id, f])); + const canonicalBuildsEnabled = includeThreeOneFields(ctx); + const acceptedTargetIds = new Set([ + ...validFormatIds.keys(), + ...Object.keys(BUILD_CREATIVE_FORMAT_ALIASES), + ...(canonicalBuildsEnabled ? SUPPORTED_CANONICAL_BUILD_CAPABILITIES.map(item => item.capabilityId) : []), + ]); + + const unsupportedFormatError = (formatId: FormatID, field: string) => ({ + code: 'FORMAT_NOT_SUPPORTED', + message: `Format "${formatId.id}" is not supported by this creative agent.`, + field, + recovery: 'correctable' as const, + details: { + format_id: formatId.id, + accepted_values: [...acceptedTargetIds].sort(), + }, + }); + + const resolveTarget = (formatId: FormatID, field: string): { target?: ResolvedBuildTarget; error?: ReturnType } => { + const aliasId = BUILD_CREATIVE_FORMAT_ALIASES[formatId.id] ?? formatId.id; + const format = validFormatIds.get(aliasId); + if (format) { + return { target: { requested: formatId, format } }; + } + + if (canonicalBuildsEnabled) { + const capability = supportedCanonicalBuildCapability(formatId.id); + if (capability) { + return { target: { requested: formatId, formatKind: capability.formatKind as NonNullable } }; + } + } + + return { error: unsupportedFormatError(formatId, field) }; + }; + + const buildManifest = (target: ResolvedBuildTarget, label: string): AdcpCreativeManifest => { + const { w, h } = getDimensions(target.format); + if (target.formatKind === 'image') { + return { + format_kind: target.formatKind, + assets: buildCanonicalImageAssets(target.requested.id, { w, h }), + } as AdcpCreativeManifest; + } + if (target.formatKind) { + return { + format_kind: target.formatKind, + assets: buildHtmlAssets(label), + } as AdcpCreativeManifest; + } + return { + format_id: { agent_url: agentUrl, id: target.requested.id }, + assets: buildHtmlAssets(label), + } as AdcpCreativeManifest; + }; // Determine target formats (cap at 50 to prevent response amplification) const MAX_TARGET_FORMATS = 50; @@ -5966,14 +6065,14 @@ export async function handleBuildCreative(args: ToolArgs, ctx: TrainingContext): } const formatId = targetIds[0] || creative.formatId; - const format = validFormatIds.get(formatId.id); - const { w, h } = getDimensions(format); + const resolved = resolveTarget(formatId, 'target_format_id'); + if (resolved.error) { + return buildCreativeCompleted({ errors: [resolved.error] }); + } + const { w, h } = getDimensions(resolved.target!.format); const base = { - creative_manifest: { - format_id: { agent_url: agentUrl, id: formatId.id }, - assets: buildHtmlAssets(`\n
Ad: ${escapeHtmlAttr(creative.name || req.creative_id!)}
`), - }, + creative_manifest: buildManifest(resolved.target!, `\n
Ad: ${escapeHtmlAttr(creative.name || req.creative_id!)}
`), }; // Return pricing when account is provided (paid creative agent mode) @@ -6006,14 +6105,16 @@ export async function handleBuildCreative(args: ToolArgs, ctx: TrainingContext): // Generate output for each target format if (targetIds.length > 1) { + const resolvedTargets = targetIds.map((fmtId, index) => resolveTarget(fmtId, `target_format_ids[${index}]`)); + const errors = resolvedTargets.flatMap(result => result.error ? [result.error] : []); + if (errors.length > 0) { + return buildCreativeCompleted({ errors, ...(governanceContext && { governance_context: governanceContext }) }); + } // Multi-format response - const creative_manifests = targetIds.map(fmtId => { - const format = validFormatIds.get(fmtId.id); - const { w, h } = getDimensions(format); - return { - format_id: { agent_url: agentUrl, id: fmtId.id }, - assets: buildHtmlAssets(`\n
Built: ${escapeHtmlAttr(fmtId.id)} (${w}x${h})
`), - }; + const creative_manifests = resolvedTargets.map(result => { + const target = result.target!; + const { w, h } = getDimensions(target.format); + return buildManifest(target, `\n
Built: ${escapeHtmlAttr(target.requested.id)} (${w}x${h})
`); }); return buildCreativeCompleted({ creative_manifests, ...(governanceContext && { governance_context: governanceContext }) }); @@ -6021,14 +6122,14 @@ export async function handleBuildCreative(args: ToolArgs, ctx: TrainingContext): // Single format response const fmtId = targetIds[0] || { agent_url: agentUrl, id: 'display_300x250' }; - const format = validFormatIds.get(fmtId.id); - const { w, h } = getDimensions(format); + const resolved = resolveTarget(fmtId, 'target_format_id'); + if (resolved.error) { + return buildCreativeCompleted({ errors: [resolved.error], ...(governanceContext && { governance_context: governanceContext }) }); + } + const { w, h } = getDimensions(resolved.target!.format); return buildCreativeCompleted({ - creative_manifest: { - format_id: { agent_url: agentUrl, id: fmtId.id }, - assets: buildHtmlAssets(`\n
Built: ${escapeHtmlAttr(fmtId.id)} (${w}x${h})
`), - }, + creative_manifest: buildManifest(resolved.target!, `\n
Built: ${escapeHtmlAttr(fmtId.id)} (${w}x${h})
`), ...(governanceContext && { governance_context: governanceContext }), }); } @@ -6036,26 +6137,28 @@ export async function handleBuildCreative(args: ToolArgs, ctx: TrainingContext): // Mode 3: Generative build (target_format_id + message, no manifest or library creative) if (targetIds.length > 0) { if (targetIds.length > 1) { - const creative_manifests = targetIds.map(fmtId => { - const format = validFormatIds.get(fmtId.id); - const { w, h } = getDimensions(format); - return { - format_id: { agent_url: agentUrl, id: fmtId.id }, - assets: buildHtmlAssets(`\n
Generated: ${escapeHtmlAttr(fmtId.id)} (${w}x${h})
`), - }; + const resolvedTargets = targetIds.map((fmtId, index) => resolveTarget(fmtId, `target_format_ids[${index}]`)); + const errors = resolvedTargets.flatMap(result => result.error ? [result.error] : []); + if (errors.length > 0) { + return buildCreativeCompleted({ errors, ...(governanceContext && { governance_context: governanceContext }) }); + } + const creative_manifests = resolvedTargets.map(result => { + const target = result.target!; + const { w, h } = getDimensions(target.format); + return buildManifest(target, `\n
Generated: ${escapeHtmlAttr(target.requested.id)} (${w}x${h})
`); }); return buildCreativeCompleted({ creative_manifests, ...(governanceContext && { governance_context: governanceContext }) }); } const fmtId = targetIds[0]; - const format = validFormatIds.get(fmtId.id); - const { w, h } = getDimensions(format); + const resolved = resolveTarget(fmtId, 'target_format_id'); + if (resolved.error) { + return buildCreativeCompleted({ errors: [resolved.error], ...(governanceContext && { governance_context: governanceContext }) }); + } + const { w, h } = getDimensions(resolved.target!.format); return buildCreativeCompleted({ - creative_manifest: { - format_id: { agent_url: agentUrl, id: fmtId.id }, - assets: buildHtmlAssets(`\n
Generated: ${escapeHtmlAttr(fmtId.id)} (${w}x${h})
`), - }, + creative_manifest: buildManifest(resolved.target!, `\n
Generated: ${escapeHtmlAttr(fmtId.id)} (${w}x${h})
`), ...(governanceContext && { governance_context: governanceContext }), }); } diff --git a/server/tests/unit/training-agent.test.ts b/server/tests/unit/training-agent.test.ts index 8712164d0f..f4b5111125 100644 --- a/server/tests/unit/training-agent.test.ts +++ b/server/tests/unit/training-agent.test.ts @@ -4323,6 +4323,83 @@ describe('build_creative pricing', () => { }); }); +describe('canonical creative build capabilities', () => { + beforeEach(() => { + invalidateCache(); + clearSessions(); + }); + + afterEach(() => { + clearSessions(); + }); + + it('advertises stable capability_id values on creative.supported_formats', async () => { + const server = createTrainingAgentServer(DEFAULT_CTX); + const { result } = await simulateCallTool(server, 'get_adcp_capabilities', {}); + + const supportedFormats = (result.creative as any).supported_formats as Array>; + const imageCapability = supportedFormats.find(format => format.capability_id === 'training_image_generation'); + expect(imageCapability?.format.format_kind).toBe('image'); + expect(imageCapability?.format.format_option_id).toBeUndefined(); + expect(supportedFormats.some(format => format.capability_id === 'build_html5')).toBe(false); + }); + + it('builds canonical manifests from supported capability_id targets', async () => { + const server = createTrainingAgentServer(DEFAULT_CTX); + const { result } = await simulateCallTool(server, 'build_creative', { + account: { brand: { domain: 'build-canonical.example' }, operator: 'build-canonical.example' }, + brand: { domain: 'build-canonical.example' }, + message: 'Create an image creative for the summer trail sale.', + target_format_id: { agent_url: TEST_AGENT_URL, id: 'training_image_generation' }, + }); + + const manifest = result.creative_manifest as Record; + expect(manifest.format_kind).toBe('image'); + expect(manifest.format_id).toBeUndefined(); + expect(manifest.assets.image_main.asset_type).toBe('image'); + }); + + it('rejects unsupported build targets with FORMAT_NOT_SUPPORTED', async () => { + const server = createTrainingAgentServer(DEFAULT_CTX); + const { result } = await simulateCallTool(server, 'build_creative', { + account: { brand: { domain: 'build-canonical.example' }, operator: 'build-canonical.example' }, + brand: { domain: 'build-canonical.example' }, + message: 'Create an unknown format.', + target_format_id: { agent_url: TEST_AGENT_URL, id: 'unknown_takeover_generation' }, + }); + + expect(result.code).toBe('FORMAT_NOT_SUPPORTED'); + expect(result.field).toBe('target_format_id'); + expect(result.recovery).toBe('correctable'); + }); + + it('rejects unimplemented canonical capabilities instead of emitting invalid manifests', async () => { + const server = createTrainingAgentServer(DEFAULT_CTX); + const { result } = await simulateCallTool(server, 'build_creative', { + account: { brand: { domain: 'build-canonical.example' }, operator: 'build-canonical.example' }, + brand: { domain: 'build-canonical.example' }, + message: 'Create an HTML5 creative.', + target_format_id: { agent_url: TEST_AGENT_URL, id: 'training_html5_generation' }, + }); + + expect(result.code).toBe('FORMAT_NOT_SUPPORTED'); + expect(result.field).toBe('target_format_id'); + }); + + it('does not accept 3.1 build capability selectors in 3.0 compat mode', async () => { + const server = createTrainingAgentServer({ ...DEFAULT_CTX, storyboardCompat: { version: '3.0' } }); + const { result } = await simulateCallTool(server, 'build_creative', { + account: { brand: { domain: 'build-canonical.example' }, operator: 'build-canonical.example' }, + brand: { domain: 'build-canonical.example' }, + message: 'Create an image creative.', + target_format_id: { agent_url: TEST_AGENT_URL, id: 'training_image_generation' }, + }); + + expect(result.code).toBe('FORMAT_NOT_SUPPORTED'); + expect(result.field).toBe('target_format_id'); + }); +}); + // ── report_usage handler ────────────────────────────────────────── describe('report_usage handler', () => { diff --git a/static/compliance/source/protocols/creative/scenarios/canonical_supported_formats.yaml b/static/compliance/source/protocols/creative/scenarios/canonical_supported_formats.yaml new file mode 100644 index 0000000000..2ec5c2d544 --- /dev/null +++ b/static/compliance/source/protocols/creative/scenarios/canonical_supported_formats.yaml @@ -0,0 +1,212 @@ +id: creative/canonical_supported_formats +version: "1.0.0" +title: "Creative canonical supported formats" +category: creative +summary: "Verifies the 3.1 creative-agent canonical path: supported_formats capability IDs, build_creative routing, and unsupported target rejection." +track: creative +introduced_in: "3.1" +required_tools: + - get_adcp_capabilities + - build_creative + +narrative: | + Creative agents in 3.1 declare what they can produce via + `creative.supported_formats` on `get_adcp_capabilities`. Each supported + format carries an agent-local stable `capability_id` that selects the build + path on that creative agent. This is separate from media-buy + `format_option_id`, which selects a product or publisher-catalog format + option. + + The buyer reads the canonical capability catalog, invokes one advertised + capability through `build_creative`, and verifies that an unsupported target + fails with `FORMAT_NOT_SUPPORTED` instead of silently producing an arbitrary + manifest. + +agent: + interaction_model: creative_agent + capabilities: + - supports_generation + - supports_transformation + examples: + - "Creative generation platforms" + - "Creative transformation services" + +caller: + role: buyer_agent + example: "Pinnacle Agency (buyer)" + +prerequisites: + description: | + The caller needs a brand identity and a creative brief. The test kit + provides fictional brand context for Acme Outdoor. + test_kit: "test-kits/acme-outdoor.yaml" + +phases: + - id: capability_discovery + title: "Discover canonical build capabilities" + narrative: | + The buyer calls `get_adcp_capabilities` and reads + `creative.supported_formats` as the creative agent's 3.1 canonical build + catalog. The first advertised image capability is used in the next phase. + + steps: + - id: get_capabilities + title: "Read supported canonical formats" + task: get_adcp_capabilities + schema_ref: "protocol/get-adcp-capabilities-request.json" + response_schema_ref: "protocol/get-adcp-capabilities-response.json" + doc_ref: "/protocol/get_adcp_capabilities" + comply_scenario: capability_discovery + stateful: false + expected: | + Return a creative capability block with implemented `supported_formats[]`. + Each entry has: + - `capability_id` at the supported-format entry level + - `format.format_kind` and `format.params.slots[]` + - no product-level `format_option_id` on the creative-agent + capability entry used for build routing + sample_request: + context: + correlation_id: "creative_canonical_supported_formats--get_capabilities" + context_outputs: + - key: "image_build_capability_id" + path: "creative.supported_formats[0].capability_id" + validations: + - check: response_schema + description: "Response matches get-adcp-capabilities-response.json schema" + - check: field_present + path: "creative.supported_formats" + description: "Creative canonical supported formats are advertised" + - check: field_value + path: "creative.supported_formats[0].capability_id" + value: "training_image_generation" + description: "The image build path has a stable local capability_id" + - check: field_value + path: "creative.supported_formats[0].format.format_kind" + value: "image" + description: "The first supported capability declares the image canonical" + - check: field_present + path: "creative.supported_formats[0].format.params.slots[0].asset_group_id" + description: "Canonical format declaration includes slot metadata" + - check: field_absent + path: "creative.supported_formats[0].format.format_option_id" + description: "Creative-agent build routing uses capability_id, not product format_option_id" + - check: field_absent + path: "creative.supported_formats[1].capability_id" + description: "Storyboard fixture only advertises implemented canonical build paths" + - check: field_value + path: "context.correlation_id" + value: "creative_canonical_supported_formats--get_capabilities" + description: "Context correlation_id returned unchanged" + + - id: build_from_capability + title: "Build using the advertised capability" + narrative: | + The buyer invokes `build_creative` using the advertised image build + capability. The produced manifest uses the 3.1 canonical manifest path + (`format_kind`) and does not fall back to a legacy named format. + + steps: + - id: build_image_capability + title: "Build with the advertised image capability" + task: build_creative + schema_ref: "media-buy/build-creative-request.json" + response_schema_ref: "media-buy/build-creative-response.json" + doc_ref: "/creative/task-reference/build_creative" + comply_scenario: creative_flow + stateful: false + expected: | + Return a completed build with a canonical image manifest: + - `creative_manifest.format_kind: image` + - generated assets keyed by canonical slot names + sample_request: + message: "Create a clean image creative for Acme Outdoor's summer trail sale. Use a direct headline and a clear landing page." + target_format_id: + agent_url: "https://your-agent.example.com" + id: "$context.image_build_capability_id" + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + brand: + domain: "acmeoutdoor.example" + quality: "draft" + idempotency_key: "$generate:uuid_v4#creative_canonical_supported_formats_build_image" + context: + correlation_id: "creative_canonical_supported_formats--build_image" + validations: + - check: response_schema + description: "Response matches build-creative-response.json schema" + - check: field_value + path: "creative_manifest.format_kind" + value: "image" + description: "Generated manifest uses the 3.1 canonical manifest path" + - check: field_absent + path: "creative_manifest.format_id" + description: "Canonical build does not invent a legacy named-format id" + - check: field_present + path: "creative_manifest.assets.image_main" + description: "Output manifest includes an image asset under the canonical slot" + - check: field_value + path: "context.correlation_id" + value: "creative_canonical_supported_formats--build_image" + description: "Context correlation_id returned unchanged" + + - id: reject_unsupported_capability + title: "Reject unsupported target capability" + narrative: | + A buyer may cache or mistype a build target. The creative agent must fail + closed with `FORMAT_NOT_SUPPORTED` instead of producing a manifest under a + target it does not recognize. Legacy named-format IDs may still be + accepted during the 3.x migration window; this phase probes an unknown + selector that is neither a legacy format nor an advertised capability. + + steps: + - id: reject_unknown_capability + title: "Reject an unknown build target" + task: build_creative + schema_ref: "media-buy/build-creative-request.json" + response_schema_ref: "media-buy/build-creative-response.json" + doc_ref: "/creative/task-reference/build_creative" + comply_scenario: creative_flow + stateful: false + expect_error: true + negative_path: payload_well_formed + expected: | + Return a build_creative error response with: + - errors[0].code: FORMAT_NOT_SUPPORTED + - errors[0].field: target_format_id + - errors[0].recovery: correctable + sample_request: + message: "Create an unsupported canonical creative." + target_format_id: + agent_url: "https://your-agent.example.com" + id: "unknown_takeover_generation" + account: + brand: + domain: "acmeoutdoor.example" + operator: "pinnacle-agency.example" + brand: + domain: "acmeoutdoor.example" + quality: "draft" + idempotency_key: "$generate:uuid_v4#creative_canonical_supported_formats_reject_unknown" + context: + correlation_id: "creative_canonical_supported_formats--reject_unknown" + validations: + - check: response_schema + description: "Response matches build-creative-response.json schema" + - check: error_code + value: "FORMAT_NOT_SUPPORTED" + description: "Unsupported build target is rejected with the canonical error code" + - check: field_value + path: "errors[0].field" + value: "target_format_id" + description: "Error points at the unsupported target selector" + - check: field_value + path: "errors[0].recovery" + value: "correctable" + description: "Buyer can correct the target by choosing an advertised capability_id" + - check: field_value + path: "context.correlation_id" + value: "creative_canonical_supported_formats--reject_unknown" + description: "Context correlation_id returned unchanged" From 887129588e72b945bc010605b41d6f091ac51a31 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Fri, 29 May 2026 04:54:25 -0400 Subject: [PATCH 2/2] Add changeset for creative storyboard coverage --- .../4591-canonical-supported-formats-storyboard.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .changeset/4591-canonical-supported-formats-storyboard.md diff --git a/.changeset/4591-canonical-supported-formats-storyboard.md b/.changeset/4591-canonical-supported-formats-storyboard.md new file mode 100644 index 0000000000..f3cafda0de --- /dev/null +++ b/.changeset/4591-canonical-supported-formats-storyboard.md @@ -0,0 +1,11 @@ +--- +"adcontextprotocol": minor +--- + +Add creative-agent canonical `supported_formats` storyboard coverage for 3.1. + +The training agent now advertises implemented canonical creative build +capabilities with agent-local `capability_id` values, accepts those IDs as +`build_creative` targets for implemented canonical outputs, rejects unsupported +targets with `FORMAT_NOT_SUPPORTED`, and keeps 3.0 compatibility mode from +accepting 3.1-only capability selectors.