(args: ToolArgs, ctx: TrainingContext)
| 3116 | } |
| 3117 | |
| 3118 | export async function handleBuildCreative(args: ToolArgs, ctx: TrainingContext): Promise<BuildCreativeResponse & { pricing_option_id?: string; vendor_cost?: number; currency?: string; consumption?: Record<string, unknown>; governance_context?: string }> { |
| 3119 | const req = args as unknown as BuildCreativeArgs; |
| 3120 | const session = await getSession(sessionKeyFromArgs(req as unknown as ToolArgs, ctx.mode, ctx.userId, ctx.moduleId)); |
| 3121 | const agentUrl = getAgentUrl(); |
| 3122 | const formats = getFormats(); |
| 3123 | const rawGovCtx = (req as unknown as Record<string, unknown>).governance_context; |
| 3124 | const governanceContext = typeof rawGovCtx === 'string' && rawGovCtx.length <= 4096 ? rawGovCtx : undefined; |
| 3125 | const validFormatIds = new Map(formats.map(f => [f.format_id.id, f])); |
| 3126 | |
| 3127 | // Determine target formats (cap at 50 to prevent response amplification) |
| 3128 | const MAX_TARGET_FORMATS = 50; |
| 3129 | const targetIds: FormatID[] = req.target_format_ids?.length |
| 3130 | ? req.target_format_ids.slice(0, MAX_TARGET_FORMATS) |
| 3131 | : req.target_format_id |
| 3132 | ? [req.target_format_id] |
| 3133 | : []; |
| 3134 | |
| 3135 | // Mode 1: Library retrieval (creative_id) |
| 3136 | if (req.creative_id) { |
| 3137 | const creative = session.creatives.get(req.creative_id) ?? getComplianceCreative(req.creative_id); |
| 3138 | if (!creative) { |
| 3139 | return { |
| 3140 | errors: [{ code: 'NOT_FOUND', message: `Creative "${req.creative_id}" not found. Use sync_creatives to upload or list_creatives to browse.` }], |
| 3141 | }; |
| 3142 | } |
| 3143 | |
| 3144 | const formatId = targetIds[0] || creative.formatId; |
| 3145 | const format = validFormatIds.get(formatId.id); |
| 3146 | const { w, h } = getDimensions(format); |
| 3147 | |
| 3148 | const base = { |
| 3149 | creative_manifest: { |
| 3150 | format_id: { agent_url: agentUrl, id: formatId.id }, |
| 3151 | assets: buildHtmlAssets(`<!-- AdCP Training Agent tag for ${escapeHtmlAttr(req.creative_id!)} -->\n<div data-adcp-creative="${escapeHtmlAttr(req.creative_id!)}" data-format="${escapeHtmlAttr(formatId.id)}"${req.media_buy_id ? ` data-media-buy="${escapeHtmlAttr(req.media_buy_id)}"` : ''}${req.package_id ? ` data-package="${escapeHtmlAttr(req.package_id)}"` : ''} style="width:${w}px;height:${h}px;background:#f0f0f0;display:flex;align-items:center;justify-content:center;font-family:sans-serif;font-size:14px;color:#666;">Ad: ${escapeHtmlAttr(creative.name || req.creative_id!)}</div>`), |
| 3152 | }, |
| 3153 | }; |
| 3154 | |
| 3155 | // Return pricing when account is provided (paid creative agent mode) |
| 3156 | if (req.account) { |
| 3157 | const pricing = getCreativePricing(req.account, creative); |
| 3158 | creative.pricingOptionId = pricing.pricing_option_id; |
| 3159 | return { |
| 3160 | ...base, |
| 3161 | pricing_option_id: pricing.pricing_option_id, |
| 3162 | vendor_cost: 0, // CPM-priced: cost accrues at serve time |
| 3163 | currency: pricing.currency, |
| 3164 | consumption: {}, |
| 3165 | ...(governanceContext && { governance_context: governanceContext }), |
| 3166 | }; |
| 3167 | } |
| 3168 | |
| 3169 | return { ...base, ...(governanceContext && { governance_context: governanceContext }) }; |
| 3170 | } |
| 3171 | |
| 3172 | // Mode 2: Stateless transformation (creative_manifest + target_format_id) |
| 3173 | if (req.creative_manifest) { |
| 3174 | const rawAssets = req.creative_manifest.assets; |
| 3175 | const inputAssetCount = Array.isArray(rawAssets) ? rawAssets.length : Object.keys(rawAssets || {}).length; |
nothing calls this directly
no test coverage detected