(args: ToolArgs, ctx: TrainingContext)
| 2151 | } |
| 2152 | |
| 2153 | export async function handleListCreatives(args: ToolArgs, ctx: TrainingContext) { |
| 2154 | const req = args as unknown as ListCreativesRequest & ToolArgs & { creative_ids?: string[]; include_pricing?: boolean; include_snapshot?: boolean }; |
| 2155 | const session = await getSession(sessionKeyFromArgs(req, ctx.mode, ctx.userId, ctx.moduleId)); |
| 2156 | const filterIds = req.creative_ids || req.filters?.creative_ids; |
| 2157 | |
| 2158 | let creatives = Array.from(session.creatives.values()); |
| 2159 | if (filterIds?.length) { |
| 2160 | creatives = creatives.filter(c => filterIds.includes(c.creativeId)); |
| 2161 | } else if (creatives.length === 0) { |
| 2162 | // Empty session falls back to compliance fixtures so storyboards that |
| 2163 | // reference stable IDs (e.g., campaign_hero_video in creative_ad_server) |
| 2164 | // resolve without the SDK's controller_seeding auto-fire. Sessions that |
| 2165 | // have synced their own creatives return only those — no mixing. |
| 2166 | creatives = getComplianceCreatives(); |
| 2167 | } |
| 2168 | |
| 2169 | const totalMatching = creatives.length; |
| 2170 | // Schema declares max_results min=1, max=100, default=50. Honor the cap; |
| 2171 | // do not silently lift sub-1 values — those should surface as schema |
| 2172 | // violations through the SDK's request validator, not be quietly corrected. |
| 2173 | const requestedMax = req.pagination?.max_results; |
| 2174 | const maxResults = Math.min(typeof requestedMax === 'number' ? requestedMax : 50, 100); |
| 2175 | const offset = decodeCreativeCursor(req.pagination?.cursor); |
| 2176 | if (offset === null) { |
| 2177 | return { |
| 2178 | errors: [{ code: 'INVALID_REQUEST', message: 'pagination.cursor is malformed' }] as TaskError[], |
| 2179 | }; |
| 2180 | } |
| 2181 | const pageEnd = Math.min(offset + maxResults, totalMatching); |
| 2182 | const pageCreatives = creatives.slice(offset, pageEnd); |
| 2183 | const hasMore = pageEnd < totalMatching; |
| 2184 | |
| 2185 | // Ad-server-capable sellers (creative.has_creative_library) quote per- |
| 2186 | // creative pricing whenever an account is present, independent of the |
| 2187 | // buyer setting include_pricing. Explicit `include_pricing: false` still |
| 2188 | // suppresses — matches the spec wording while letting callers that omit |
| 2189 | // the flag (e.g., SDK request builders that drop it) still receive pricing. |
| 2190 | // Spec today says "When false or omitted, pricing is not computed"; the |
| 2191 | // emission-on-omit behaviour here is deliberate per the has_creative_library |
| 2192 | // gate in #2847 and tracks the spec-side clarification referenced there. |
| 2193 | const emitPricing = Boolean(req.account) && req.include_pricing !== false; |
| 2194 | const agentUrl = getAgentUrl(); |
| 2195 | |
| 2196 | return { |
| 2197 | query_summary: { |
| 2198 | total_matching: totalMatching, |
| 2199 | returned: pageCreatives.length, |
| 2200 | }, |
| 2201 | pagination: { |
| 2202 | has_more: hasMore, |
| 2203 | total_count: totalMatching, |
| 2204 | // Cursor MUST be present iff has_more is true — see |
| 2205 | // static/schemas/source/core/pagination-response.json. Carrying a stale |
| 2206 | // cursor on a terminal page invites callers to follow it past the end |
| 2207 | // (caught by universal/pagination-integrity.yaml). |
| 2208 | ...(hasMore && { cursor: encodeCreativeCursor(pageEnd) }), |
| 2209 | }, |
| 2210 | creatives: pageCreatives.map(c => { |
nothing calls this directly
no test coverage detected