From 049ca155e5e3505858af494d1e25098a7325ec1f Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 30 Apr 2026 00:25:36 +0000 Subject: [PATCH 1/3] =?UTF-8?q?fix(examples):=20seller=5Fagent.py=20passes?= =?UTF-8?q?=20AdCP=203.0.1=20storyboard=20compliance=20(items=201=E2=80=93?= =?UTF-8?q?6=20of=20#304)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Six gaps identified by the media_buy_seller storyboard runner after the #296 transport fix exposed content-side failures in the reference example: 1. declare `adcp.idempotency` in capabilities so the runner does not downgrade to v2 mode (`idempotency={"supported": False}`) 2. include `total_budget` (schema-required number) in `get_media_buys` entries, computed as the sum of per-package budgets 3. return `status=pending_creatives` from `create_media_buy` when no `creative_assignments`/`creatives` are in the request packages, and transition to `active` in `update_media_buy` when creatives are attached 4. fix `list_creative_formats` render shape: wrap width/height in a `dimensions` object and add the required `role` field 5. honour the `format_ids` filter in `list_creative_formats`, matching on the full `(agent_url, id)` pair 6. return `PACKAGE_NOT_FOUND` in `update_media_buy` when a package ID in the update request does not exist in the stored media buy Item 7 (seed_product / controller_detected) remains blocked on #282. https://claude.ai/code/session_01HAP5upax2a7FrcrmgVwTX2 --- examples/seller_agent.py | 122 +++++++++++++++++++++++++-------------- 1 file changed, 79 insertions(+), 43 deletions(-) diff --git a/examples/seller_agent.py b/examples/seller_agent.py index 45601196..b9024575 100644 --- a/examples/seller_agent.py +++ b/examples/seller_agent.py @@ -107,6 +107,7 @@ async def get_adcp_capabilities( ) -> dict[str, Any]: return capabilities_response( ["media_buy"], + idempotency={"supported": False}, compliance_testing={ "scenarios": [ "force_account_status", @@ -223,14 +224,20 @@ async def create_media_buy(self, params: dict[str, Any], context: Any = None) -> } ) + has_creatives = any( + pkg.get("creative_assignments") or pkg.get("creatives") + for pkg in params["packages"] + ) + status = "active" if has_creatives else "pending_creatives" + mb_id = f"mb-{uuid.uuid4().hex[:8]}" media_buys[mb_id] = { - "status": "active", + "status": status, "currency": "USD", "packages": packages, "revision": 1, } - return media_buy_response(mb_id, packages, status="active") + return media_buy_response(mb_id, packages, status=status) async def get_media_buys(self, params: dict[str, Any], context: Any = None) -> dict[str, Any]: requested_ids = params.get("media_buy_ids") @@ -238,12 +245,14 @@ async def get_media_buys(self, params: dict[str, Any], context: Any = None) -> d for mb_id, mb in media_buys.items(): if requested_ids and mb_id not in requested_ids: continue + total_budget = sum((pkg.get("budget") or 0) for pkg in mb.get("packages", [])) results.append( { "media_buy_id": mb_id, "status": mb["status"], "currency": mb.get("currency", "USD"), "packages": mb.get("packages", []), + "total_budget": total_budget, } ) return media_buys_response(results) @@ -257,7 +266,25 @@ async def update_media_buy(self, params: dict[str, Any], context: Any = None) -> if params.get("revision") and params["revision"] != mb.get("revision", 1): return adcp_error("CONFLICT", "Revision mismatch - refetch and retry") + if params.get("packages"): + existing_pkg_ids = {p["package_id"] for p in mb.get("packages", [])} + for pkg_update in params["packages"]: + pkg_id = pkg_update.get("package_id") + if pkg_id and pkg_id not in existing_pkg_ids: + return adcp_error( + "PACKAGE_NOT_FOUND", + f"Package '{pkg_id}' not found in media buy {mb_id}", + field="package_id", + ) + status = mb["status"] + if status == "pending_creatives" and params.get("packages"): + if any( + pkg.get("creative_assignments") or pkg.get("creatives") + for pkg in params["packages"] + ): + mb["status"] = "active" + status = "active" if params.get("paused") is True and status == "active": mb["status"] = "paused" elif params.get("paused") is False and status == "paused": @@ -274,50 +301,59 @@ async def update_media_buy(self, params: dict[str, Any], context: Any = None) -> async def list_creative_formats( self, params: dict[str, Any], context: Any = None ) -> dict[str, Any]: - return creative_formats_response( - [ - { - "format_id": { - "agent_url": AGENT_URL, - "id": "display_300x250", - }, - "name": "Display 300x250", - "renders": [{"width": 300, "height": 250}], - "assets": [ - { - "item_type": "individual", - "asset_id": "image", - "asset_type": "image", - "required": True, - "accepted_media_types": [ - "image/png", - "image/jpeg", - ], - } - ], + all_formats: list[dict[str, Any]] = [ + { + "format_id": { + "agent_url": AGENT_URL, + "id": "display_300x250", }, - { - "format_id": { - "agent_url": AGENT_URL, - "id": "display_970x250", - }, - "name": "Display 970x250", - "renders": [{"width": 970, "height": 250}], - "assets": [ - { - "item_type": "individual", - "asset_id": "image", - "asset_type": "image", - "required": True, - "accepted_media_types": [ - "image/png", - "image/jpeg", - ], - } - ], + "name": "Display 300x250", + "renders": [{"role": "primary", "dimensions": {"width": 300, "height": 250}}], + "assets": [ + { + "item_type": "individual", + "asset_id": "image", + "asset_type": "image", + "required": True, + "accepted_media_types": [ + "image/png", + "image/jpeg", + ], + } + ], + }, + { + "format_id": { + "agent_url": AGENT_URL, + "id": "display_970x250", }, + "name": "Display 970x250", + "renders": [{"role": "primary", "dimensions": {"width": 970, "height": 250}}], + "assets": [ + { + "item_type": "individual", + "asset_id": "image", + "asset_type": "image", + "required": True, + "accepted_media_types": [ + "image/png", + "image/jpeg", + ], + } + ], + }, + ] + filter_ids = params.get("format_ids") + if filter_ids: + wanted = {(fid.get("agent_url"), fid["id"]) for fid in filter_ids if "id" in fid} + formats = [ + f + for f in all_formats + if (f["format_id"].get("agent_url"), f["format_id"]["id"]) in wanted ] - ) + else: + formats = all_formats + return creative_formats_response(formats) async def sync_creatives(self, params: dict[str, Any], context: Any = None) -> dict[str, Any]: results = [] From 5035c9b73db367b668baccf3e05b3983ca19c0a6 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 30 Apr 2026 00:29:09 +0000 Subject: [PATCH 2/3] fix(examples): align DemoStore.simulate_delivery reported_spend type with base class The base TestControllerStore declares reported_spend as dict[str, Any] | None (matching the ReportedSpend schema {amount, currency}). DemoStore had it as float | None, causing type mismatch and incorrect stored structure when the storyboard sends a structured object. https://claude.ai/code/session_01HAP5upax2a7FrcrmgVwTX2 --- examples/seller_agent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/seller_agent.py b/examples/seller_agent.py index b9024575..17861e26 100644 --- a/examples/seller_agent.py +++ b/examples/seller_agent.py @@ -450,7 +450,7 @@ async def simulate_delivery( impressions: int | None = None, clicks: int | None = None, conversions: int | None = None, - reported_spend: float | None = None, + reported_spend: dict[str, Any] | None = None, ) -> dict[str, Any]: if media_buy_id not in media_buys: raise TestControllerError("NOT_FOUND", f"Media buy {media_buy_id} not found") From 52d4dee7a5ae5fd9915c385fc898bc9fb77b9008 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 30 Apr 2026 00:31:54 +0000 Subject: [PATCH 3/3] fix(examples): explicitly pass valid_actions for pending_creatives status MEDIA_BUY_STATE_MACHINE on main lacks the pending_creatives key (it lands with PR #296). Without explicit valid_actions, media_buy_response() and update_media_buy_response() return valid_actions=[] for pending_creatives buys, blocking the storyboard from discovering that sync_creatives is available. Pass the expected actions list explicitly until #296 merges. https://claude.ai/code/session_01HAP5upax2a7FrcrmgVwTX2 --- examples/seller_agent.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/examples/seller_agent.py b/examples/seller_agent.py index 17861e26..0c0a8ca9 100644 --- a/examples/seller_agent.py +++ b/examples/seller_agent.py @@ -237,7 +237,12 @@ async def create_media_buy(self, params: dict[str, Any], context: Any = None) -> "packages": packages, "revision": 1, } - return media_buy_response(mb_id, packages, status=status) + pending_actions = ["sync_creatives", "cancel", "update_budget", "update_dates", + "update_packages", "add_packages"] + return media_buy_response( + mb_id, packages, status=status, + valid_actions=pending_actions if status == "pending_creatives" else None, + ) async def get_media_buys(self, params: dict[str, Any], context: Any = None) -> dict[str, Any]: requested_ids = params.get("media_buy_ids") @@ -296,7 +301,14 @@ async def update_media_buy(self, params: dict[str, Any], context: Any = None) -> return cancel_media_buy_response(mb_id, "buyer") mb["revision"] = mb.get("revision", 1) + 1 - return update_media_buy_response(mb_id, status=mb["status"], revision=mb["revision"]) + pending_actions = ["sync_creatives", "cancel", "update_budget", "update_dates", + "update_packages", "add_packages"] + return update_media_buy_response( + mb_id, + status=mb["status"], + revision=mb["revision"], + valid_actions=pending_actions if mb["status"] == "pending_creatives" else None, + ) async def list_creative_formats( self, params: dict[str, Any], context: Any = None