Skip to content

feat!: align media-buy responses with AdCP 3.1#842

Merged
bokelley merged 2 commits into
mainfrom
bokelley/issue-825
May 24, 2026
Merged

feat!: align media-buy responses with AdCP 3.1#842
bokelley merged 2 commits into
mainfrom
bokelley/issue-825

Conversation

@bokelley
Copy link
Copy Markdown
Contributor

@bokelley bokelley commented May 24, 2026

Summary

Fixes the reference seller and Python SDK response surfaces for AdCP 3.1 storyboard compatibility around issue #825.

  • adds cache_scope="public" for cacheable reference-seller product responses
  • strips unsupported governance categories and emits the current account-governance URL shape
  • projects media-buy health and stateful impairment records for rejected creatives, including stable open impairment IDs/timestamps and blocked-package scoping
  • separates MCP task-envelope status from media-buy lifecycle media_buy_status for create/update responses
  • keeps dict response helpers payload-level while normalizing legacy lifecycle status at the MCP boundary, including enum-valued dict payloads
  • aligns typed create/update media-buy success/submitted response arms, public exports, and tools/list output schemas with the wire shape, while preserving legacy typed construction like status="active"
  • advertises proposal support from the sales-proposal-mode example and runs its proposal_finalize storyboard on @adcp/sdk@8.1.0-beta.7

Validation

  • PYTHONPATH=src python3 -m ruff check src/ -> passed
  • PYTHONPATH=src python3 -m mypy src/adcp/ -> passed
  • PYTHONPATH=src python3 -m mypy --strict tests/type_checks/ -> passed
  • PYTHONPATH=src python3 -m pytest tests/ -q -> 5109 passed, 30 skipped, 10 deselected, 1 xfailed
  • PYTHONPATH=src python3 -m pytest tests/test_code_generation.py tests/test_public_api.py -q -> 25 passed
  • PYTHONPATH=src python3 -m pytest tests/test_proposal_lifecycle.py tests/test_proposal_auto_commit.py tests/test_proposal_lifecycle_e2e.py -q -> 56 passed
  • @adcp/sdk@8.1.0-beta.7 reference seller storyboard -> passing, 88 passed, 0 failed, 2 skipped
  • @adcp/sdk@8.1.0-beta.7 sales-proposal-mode proposal_finalize storyboard -> passing, 5 passed, 0 failed, 0 skipped
  • pre-commit on amend: black, ruff, mypy, bandit, whitespace/EOF/YAML/JSON/large-file/conflict/key checks all passed
  • expert review pass run through protocol/code reviewers; blockers were addressed before push

Breaking Change

CreateMediaBuySuccessResponse.status and UpdateMediaBuySuccessResponse.status now represent the task-envelope status and read as "completed". Read media_buy_status for the media-buy lifecycle state.

BREAKING CHANGE: read media_buy_status for lifecycle; status is now the task envelope.

Comment thread src/adcp/types/generated_poc/media_buy/update_media_buy_response.py Fixed
@bokelley bokelley force-pushed the bokelley/issue-825 branch from cb1f48a to e82197a Compare May 24, 2026 01:15
@bokelley bokelley changed the title Fix reference seller 3.1 storyboard compatibility fix: reference seller 3.1 storyboard compatibility May 24, 2026
@bokelley bokelley force-pushed the bokelley/issue-825 branch 3 times, most recently from d067104 to 50d130a Compare May 24, 2026 01:36
@bokelley bokelley force-pushed the bokelley/issue-825 branch from 50d130a to 63e0999 Compare May 24, 2026 01:39
@bokelley bokelley marked this pull request as ready for review May 24, 2026 01:44
Copy link
Copy Markdown
Contributor

@aao-ipr-bot aao-ipr-bot Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Request changes. Wire shape is right; the semver signal is wrong. Read-shape breaks on the adcp.* public surface but the squash will land under fix: — dependents on ^6 pick it up automatically.

Block (must fix before merge)

Conventional-commit prefix. src/adcp/types/generated_poc/media_buy/create_media_buy_response.py:46 and update_media_buy_response.py:48 flip status from MediaBuyStatus | None = None to Literal[\"completed\"] (required). _normalize_legacy_status is load-bearing for constructionCreateMediaBuySuccessResponse(status=\"active\") still works — but the read side breaks: if resp.status == MediaBuyStatus.active is silent False, resp.status.value raises AttributeError. The PR-internal diff at examples/v3_reference_seller/tests/test_smoke_broadening.py:730,789,831 is the proof — every existing call site moves from result.status.value to result.media_buy_status.value. Land as feat!: or add a BREAKING CHANGE: footer with a one-line migration note ("read media_buy_status for lifecycle; status is now the task envelope") so release-please gates this behind a major.

Things I checked

  • Wire shape vs AdCP 3.1: ad-tech-protocol-expert verdict sound. Three-arm Response1|Response2|Response3 matches media-buy/create-media-buy-response.json and update-media-buy-response.json oneOf. cache_scope=\"public\" on the anonymous wholesale feed is mandated by get-products-response.json:315-321. Stripped categories on governance_agents echo is mandated by sync-governance-response.json:38-58 (additionalProperties: false). Impairment shape matches core/impairment.json required-field set.
  • Generated-type audit: create_media_buy_response.py and update_media_buy_response.py are output of scripts/post_generate_fixes.py:1919-2020. Not hand-edited.
  • Type-layering: aliases.py/_generated.py are the only new importers — within the allowlist.
  • _normalize_response_envelope (src/adcp/server/mcp_tools.py:53-87) gates on sync media-buy success (media_buy_id present; task_id/errors absent). Submitted-envelope path bypassed correctly.
  • New is_update_media_buy_submitted (src/adcp/types/guards.py:165-173) and the early-return tweaks at :179/:188 correctly short-circuit success/error guards on the submitted arm.
  • open_impairments (examples/seller_agent.py:83) survives status flaps with stable IDs; the lifecycle test (tests/test_seller_agent_storyboard.py:529-562) covers reopen → new ID.

Follow-ups (non-blocking — file as issues once the block clears)

  • _MEDIA_BUY_STATUS_VALUES diverges between MCP boundary and generated models. src/adcp/server/mcp_tools.py:36-46 includes \"draft\", \"completed\", \"cancelled\" (double-l). src/adcp/types/generated_poc/media_buy/create_media_buy_response.py:26-33 does not. A handler returning {\"media_buy_id\": \"X\", \"status\": \"draft\"} gets MCP-normalized to media_buy_status=\"draft\" — and MediaBuyStatus (src/adcp/types/generated_poc/enums/media_buy_status.py) doesn't have draft. The boundary manufactures a wire payload that fails buyer-side enum parse. Extract a single shared constant; drop \"draft\" and the double-l \"cancelled\".
  • cancel_media_buy_response still writes envelope status=\"canceled\"; A2A doesn't normalize. src/adcp/server/helpers.py wasn't moved to the new contract, and src/adcp/server/a2a_server.py:452-471 doesn't call _normalize_response_envelope. MCP catches it via update_media_buy normalization; A2A ships envelope status=\"canceled\" un-normalized. Move _normalize_response_envelope to a transport-agnostic spot and call it from both sides.
  • _normalize_legacy_status is asymmetric on mismatch. create_media_buy_response.py:48-65: {status=\"pending_start\", media_buy_status=\"active\"} errors loudly; {status=\"completed\", media_buy_status=\"active\"} passes silently. Pick one — raise on any genuine mismatch, or always prefer the new shape.
  • Docs drift on the new shape. skills/adcp-media-buy/SKILL.md:151-153 documents status as "Current lifecycle state — pending_creatives, pending_start, or active" — after this PR that's media_buy_status. skills/call-adcp-agent/SKILL.md:191 shows the sync arm without the new dual-status field. AGENTS.md Type Guards section doesn't list is_create_media_buy_submitted / is_update_media_buy_submitted. llms.txt:41-42 doesn't surface the *SubmittedResponse aliases. Buyer-side LLMs reading the skills will mis-parse.
  • media_buy_response(..., status=...) kwarg is now misnamed. src/adcp/server/responses.py:342. The kwarg maps to lifecycle, not envelope status. Rename to media_buy_status= with a deprecation alias for one release.
  • Three copies of the _value/_enum_value helper + lifecycle set. mcp_tools.py, both generated files, with the codegen template at scripts/post_generate_fixes.py:1925-2020 inlining two of them. Shared module needed before this drifts further.
  • open_impairments module-global isn't account-partitioned. examples/seller_agent.py:83. Reference seller is single-tenant-demo per the _DEFAULT_ACCOUNT_ID comment at :85 but open_impairments doesn't carry the same caveat. Add the comment or key by (account_id, media_buy_id, creative_id).

Minor nits (non-blocking)

  1. _value helper name is too generic. src/adcp/types/generated_poc/media_buy/create_media_buy_response.py:36-37. Rename to _unwrap_enum so future codegen helpers don't collide.
  2. _normalize_submitted_status is redundant under use_enum_values=True. create_media_buy_response.py:80-86, update_media_buy_response.py:82-88. Pydantic already accepts the string \"submitted\" and the enum interchangeably. Drop the validator.
  3. Validation list omits test_smoke_broadening.py. That's the file demonstrating the read-side migration — the contract change this PR ships. Worth listing explicitly.

Unblock by amending the squash subject to feat!: or appending a BREAKING CHANGE: footer.

@bokelley bokelley changed the title fix: reference seller 3.1 storyboard compatibility feat!: align media-buy responses with AdCP 3.1 May 24, 2026
Copy link
Copy Markdown
Contributor

@aao-ipr-bot aao-ipr-bot Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Right factoring: payload helpers carry lifecycle, MCP boundary owns the task envelope. Adopters keep status="active" ergonomics through model_validator(mode='before'), on-the-wire status reads "completed" per 3.1.

Things I checked

  • feat!: title + BREAKING CHANGE: footer + migration note in body — semver signal correct for release-please.
  • CreateMediaBuyResponse1.status: Literal["completed"] with _normalize_legacy_status validator preserves status="active" construction by remapping to media_buy_status. Conflict case (raw_status != media_buy_status) falls through to a Pydantic ValidationError — not silent.
  • Boundary at src/adcp/server/mcp_tools.py:73-85 and model-level validator agree on the migration direction. status="completed" + media_buy_status=None correctly does not infer media_buy_status="completed""completed" is the task envelope marker, not a lifecycle value.
  • _looks_like_sync_media_buy_success excludes payloads carrying task_id so the submitted envelope isn't rewritten as sync at the boundary.
  • is_update_media_buy_submitted short-circuits both success and error guards — parity with the existing create-side is_create_media_buy_submitted pattern.
  • examples/seller_agent.py::_health_fields_for_media_buy impairment shape (impairment_id, resource_type, resource_id, package_ids, transition, reason_code, reason, observed_at, remediation) matches schemas/cache/3.1.0-beta.3/core/impairment.json; lifecycle test (reopen gets fresh impairment_id) passes.
  • supports_proposals=true is the canonical 3.1 discovery flag per bundled/protocol/get-adcp-capabilities-response.json.
  • governance_agents strip of categories matches sync-governance-response.json (additionalProperties: false, required: [url]).
  • cache_scope on proposal_finalize projection — per get-products-response.json the field is REQUIRED on the projection; account if params.account else public matches the schema directive.
  • Generated files match the scripts/post_generate_fixes.py:1520+ post-processor injection — not hand-edited.
  • Public API snapshot updated for both UpdateMediaBuyResponse3 and UpdateMediaBuySubmittedResponse; is_update_media_buy_submitted listed in guards __all__.

Follow-ups (non-blocking — file as issues)

  • Export asymmetry: UpdateMediaBuyResponse3 is publicly exported by numbered name; CreateMediaBuyResponse3 is not (only the semantic CreateMediaBuySubmittedResponse alias is). Per CLAUDE.md numbered names are unstable — pick one direction. Either drop UpdateMediaBuyResponse3 from __all__ and route adopters through UpdateMediaBuySubmittedResponse, or add CreateMediaBuyResponse3 for symmetry.
  • _MEDIA_BUY_STATUS_VALUES divergence between the boundary set (mcp_tools.py:36-46: includes draft, cancelled, completed) and the model-level set (generated_poc/.../create_media_buy_response.py:20-27: six core lifecycle values). Handler returning status="draft" gets normalized at boundary, then trips Pydantic on the enum. Narrow the boundary set or document the legacy aliases the boundary intentionally translates (cancelledcanceled).
  • result.setdefault("status", "completed") at mcp_tools.py:87 runs unconditionally; a handler that returns {"task_id": ...} without status (partial migration) gets stamped "completed" and looks malformed. Gate on "task_id" not in result for media-buy methods or log a one-time deprecation when this fires.
  • Conflict case in _normalize_legacy_status (raw_status != media_buy_status, both set) currently falls through to a ValidationError on the Literal["completed"] mismatch. A ValueError from the validator with both observed values would land faster for adopters who hit it.

Minor nits (non-blocking)

  1. Shared _value / _MEDIA_BUY_STATUS_VALUES helpers. Identical bodies appear in generated_poc/media_buy/create_media_buy_response.py:20-32, update_media_buy_response.py:20-32, and a third copy in mcp_tools.py:36-50. Extract to generated_poc/core/ so they don't drift on the next regeneration.
  2. "__anonymous__" sentinel. examples/seller_agent.py:131 uses a string sentinel for the no-media_buy_id case; collides with any real media-buy ID that happens to match. Use an object-identity sentinel or namespace it.
  3. Pre-1.0 release-please semver. feat!: on a beta line with bump-minor-pre-major: true produces a minor bump, not a major. The ! is still the right changelog signal — just confirm the version delta matches release-manager intent.

Safe to merge.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant