Skip to content

docs: salesagent → adcp Python SDK migration guide#489

Closed
bokelley wants to merge 8 commits into
mainfrom
bokelley/docs-salesagent-migration
Closed

docs: salesagent → adcp Python SDK migration guide#489
bokelley wants to merge 8 commits into
mainfrom
bokelley/docs-salesagent-migration

Conversation

@bokelley
Copy link
Copy Markdown
Contributor

@bokelley bokelley commented May 3, 2026

Summary

Adds examples/multi_platform_seller/MIGRATION_FROM_ADAPTER_REGISTRY.md — the translation guide for adopters running salesagent-shaped multi-tenant adapter-registry code.

The headline message: business logic stays; cross-cutting concerns move into SDK primitives. Concrete before/after pairs with salesagent/src/adapters/... file:line citations on every before-shape and shipping SDK primitives on every after-shape.

Parent issue: #477 (the multi-platform proof). Implementation lands in parallel PR bokelley/feat-platform-router. This is the doc-only PR; the directory will be co-populated with the worked example by the parallel PR. Merge order doesn't matter — if there's a path conflict at merge, rebase resolves cleanly.

Sections

  1. Audience + scope (one-paragraph framing of who this is for and what migrating means)
  2. The high-level shift (the conceptual diagram — registry → router, per-adapter cross-cutting concerns → SDK primitives)
  3. Translation table — section per concept:
    • 3.1 ADAPTER_REGISTRYPlatformRouter
    • 3.2 AdServerAdapter ABC → DecisioningPlatform + SalesPlatform
    • 3.3 HITL gating → compose_method + ShortCircuit
    • 3.4 Sandbox toggles → Account.mode
    • 3.5 Mock fixtures → Account.mode='mock' (the ~1,800 LOC deletion)
    • 3.6 Compliance scaffolding → SDK comply_test_controller gate
    • 3.7 Lifecycle state machine → assert_media_buy_transition
    • 3.8 Webhook emission → F12 auto-emit
    • 3.9 Per-adapter HTTP client → UpstreamHttpClient
    • 3.10 Error projection → AdcpError + UpstreamHttpClient projection
  4. What NOT to migrate (dry_run, audit logger, tenant DB, admin UI)
  5. Migration order (Kevel-first, GAM-last, no flag day)
  6. What this doesn't solve (multi-protocol bridging, scale testing, admin UI, CAPI mismatch)

Reviewer

Brian O'Kelley (@bokelley) — salesagent author and the natural reviewer for whether the translation table reflects salesagent's actual shape.

Test plan

  • Reviewer confirms salesagent file:line citations are accurate
  • Reviewer confirms the after-shape primitives are accurate against main
  • Reviewer confirms migration order is realistic against salesagent's adapter set
  • (Optional) ad-hoc render check via Mintlify preview if the example gets one

🤖 Generated with Claude Code

bokelley and others added 6 commits May 3, 2026 14:20
…lation guide

Translation guide for adopters running salesagent-shaped multi-tenant
adapter-registry code. Covers the move from `ADAPTER_REGISTRY` /
`AdServerAdapter` ABC to `PlatformRouter` + `DecisioningPlatform` +
`SalesPlatform`, with concrete file:line references into salesagent for
each before-shape and shipping SDK primitives for each after-shape.

Refs #477. Implementation lands in parallel PR
`bokelley/feat-platform-router`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…t sink, migration order

- Add 'Foundations' section explaining how salesagent's monolithic Principal
  model maps onto two SDK lookups: BuyerAgentRegistry (agent identity) +
  AccountStore (account context). This is the JS-SDK-style separation Brian
  asked about.
- Clarify the router's accounts= field is a single global AccountStore that
  reads from per-tenant Principal rows, not a per-tenant store.
- Remove accounts = ... from the GAMPlatform example: in multi-platform mode
  behind a PlatformRouter, only the router declares accounts; child
  platforms work with ctx.account.
- Audit logger: replace 'framework doesn't manage it' with reference to the
  SDK's AuditSink Protocol (LoggingAuditSink, SlackAlertSink reference impls).
- Migration order: drop Kevel as the recommended starting point. Only GAM
  and Broadstreet have real client deployments; ~99% of clients are on GAM.
- Diagram + section 3.1 example: replace KevelPlatform with BroadstreetPlatform
  to reflect actual deployment shape.
…lisms, and salesagent's AdCP 3.0 gaps

The earlier draft framed the salesagent → SDK move as a 1:1 translation.
That misrepresents the territory: salesagent today implements a subset of
AdCP 3.0, and the migration is part-port, part-upgrade. Adds an honest
"what this adds, not just translates" framing plus three new translation
sections that were missing entirely:

- §3.3 Product discovery and the refine flow — the get_products →
  create_media_buy seam (formalizing the implementation_config plumbing
  salesagent already does inline) plus the multi-turn refine flow that
  salesagent's request schema accepts but no adapter consults.
- §3.4 Creative specialisms — splitting salesagent's single
  CreativeEngineAdapter.process_creatives surface across the two
  Platform Protocols (CreativeAdServerPlatform stateful library + tag
  gen, CreativeBuilderPlatform brief-to-creative), with
  CreativeBuilderPlatform flagged as greenfield.
- §3.5 Signals — the structural shift from a global core/tools/signals.py
  tool body to per-tenant SignalsPlatform instances behind the router.

Renumbers the existing 3.3–3.10 cross-cutting-concern sections to
3.6–3.13. Updates the migration order to add explicit phases for
porting creative, moving signals to per-tenant platforms, and adding
the refine handler, and pushes "stand up PlatformRouter" to the last
step (each platform validates standalone first). Extends "What this
doesn't solve" with the AdCP 3.0 surfaces that are gaps from the
migration rather than flaws in the SDK.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add §3.6 (reporting and delivery surfaces) covering the
get_media_buy_delivery clean translation, get_creative_delivery
greenfield, and capability-declared push reporting via webhook /
offline bucket.

Add §3.7 (governance specialisms) covering the three independently
claimable Platform Protocols (BrandRightsPlatform,
ContentStandardsPlatform, CampaignGovernancePlatform) and the
recommended per-tenant adoption order.

Update the translates / port-and-extend / greenfield buckets, the
migration order (insert push-reporting + governance steps,
renumbering 10-12 to 12-14), and "what this doesn't solve" to call
out per-creative reporting and active governance as
upstream-dependent / greenfield gaps.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…, adagents.json verification

Add §3.8 covering property lists, collection lists, and adagents.json
registry verification. Property lists translate from salesagent's global
core/tools/properties.py into a per-tenant PropertyListsPlatform behind
the router (same tool→platform shape change as signals); collection
lists are greenfield (salesagent has no collection-list code);
adagents.json fetch/verify infrastructure stays adopter-side, while
the SDK formalizes the wire-level references so buyers can
independently re-verify. Update bucket lists, migration order
(steps 12-13), and "what this doesn't solve" accordingly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…lley feedback

Four reframings on examples/multi_platform_seller/MIGRATION_FROM_ADAPTER_REGISTRY.md:

- Foundations (§Principal mapping): add caveat that Principal schema split
  into BuyerAgent + Account is healthier long-term, even though wrap-today
  works.
- §3.4 Creative: salesagent's current shape is fine for 3.0. AdCP creative
  is muddy at 3.0; SDK absorbs 3.0 → 3.1 translation. CreativeAdServerPlatform
  is upstream of sales agents in practice. Drop "port and extend" framing.
- §3.5 Signals: salesagent's signals is a slightly different implementation,
  not wrong-shape. Open architectural question is whether SDK should grow
  inventory_store / signal_store primitives for dynamic-product assembly +
  key-value targeting threading.
- §3.7 Governance: governance_agents is configuration declaring required
  governance enforcement, not decorative metadata. The governance-aware-seller
  lifecycle is unfinished on both sides — SDK seller-side check_governance
  wiring is "spec-recognized but unenforced," salesagent has the field but
  no enforcement code.

Updates §3.2 bucket entries, migration-order steps 7 and 11, and the
"What this doesn't solve" section to match. Net: -54 lines.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bokelley added a commit that referenced this pull request May 3, 2026
…composition (#502)

* docs(proposals): product architecture — layered model + two-platform composition

Foundational design doc capturing the layered architecture for products
across the SDK: wire / internal-config (recipe) / capability overlap /
supporting tables. Anchors the missing prerequisite that the salesagent
migration guide (PR #489) currently has no answer for.

Key conceptual moves:

- Four-layer model with explicit ownership per layer.
- Two-platform composition: ProposalManager + DecisioningPlatform,
  with the recipe (implementation_config) as the typed contract
  between them.
- Three concrete shapes: tight coupling (LinkedIn), sophisticated
  multi-decisioning (Prebid salesagent), naive (programmatic
  non-guaranteed).
- Path B recipe binding: discriminated-union with recipe_kind tag,
  explicit tenant binding, boot-time validation.
- ProposalCapabilities is sales-axis-scoped (not generic to all
  specialisms) — guaranteed vs non-guaranteed flavors.

Status: DRAFT (conceptual scaffold). Concrete examples sections marked
[citations pending] for follow-up agent pass once budget allows.

Python-first; ports back to JS once settled. Reverses the typical
direction (lifecycle-state proposal went JS-to-Python) because the
conceptual gap surfaced during the Python migration story.

Refs:
- PR #489 (migration guide reviewer feedback that prompted this)
- Issues #491-#497 (already-filed buyer-side request-shape helpers)
- #477 (multi-platform proof — interacts with tenant binding model)
- proposals/decisioning-platform-dispatch-design.md (current
  DecisioningPlatform design)

* docs(proposals): v1 ProposalManager is mock-backend forwarder, not new catalog impl

Brian's review correction: the mock seller backend (bin/adcp.js
mock-server) already implements the naive case via product fixtures.
SDK doesn't need a separate SimpleCatalogProposalManager.

v1 of ProposalManager is just the wiring that forwards to the mock
backend — symmetric with DecisioningPlatform's upstream_for(ctx)
mock-mode dispatch from Phase 2. Adopters declare a mock_upstream_url;
framework forwards get_products / refine; recipes flow back.

Updates:
- Shape 3 reframed: Naive → Mock-backed (v1 default)
- MockProposalManager sketch with parallel pattern to upstream_for
- Independent-modes table: ProposalManager and DecisioningPlatform
  can each be mock or live independently
- Future-issues list: SimpleCatalogProposalManager replaced with
  MockProposalManager (the v1 work is wiring, not a new impl)
- §What ships in v1 framing: framework wiring, not catalog content

* docs(proposals): address review comments — recipe lifecycle, hydration, finalize

Four corrections from Brian's review:

1. Recipe lifecycle (Layer 2): never on the wire. Lives in framework
   session cache during negotiation; persists alongside the committed
   proposal after finalize; the framework hydrates it for every
   subsequent operation in the buy's lifecycle. Adopters do NOT store
   recipes themselves — the SDK is the system of record.

2. ProposalManager flavors: don't lock the formal taxonomy. Two
   extremes today (simple catalog vs complex proposal); useful
   variants will emerge between. sales_specialism + capability flags
   let adopters declare their actual shape; naming variants is
   future-state work.

3. Proposal hydration + capability validation upstream of adapter:
   buyer may reference packages by proposal_id OR product_id;
   framework hydrates from session cache or persisted store and
   validates the buyer's request against capability_overlap BEFORE
   invoking adapter. Buyer asking for geo-metro targeting on a
   product that doesn't expose it gets a structured error without
   adapter code participating. Layer 3 capability-overlap seam
   gets concrete.

4. Acceptance IS in the spec — buying_mode='refine' with refine[]
   action='finalize' transitions a draft proposal to committed
   with locked pricing, expires_at hold window, optional HITL.
   I incorrectly said the spec had no proposal lifecycle.
   Rewrote the section to walk the actual wire flow:
   get_products → refine → finalize → create_media_buy. SDK
   responsibilities at the seam: session cache, finalize transition,
   expires_at enforcement, persistence through the buy lifecycle.
Per #502's "Migration impact" directive: §3.3 introduces the
proposal/decisioning seam adopters will split along long-term;
§3.5 resolves the inventory_store/signal_store open question to
"ProposalManager concern". §3.7 (governance) is unaffected by the
split. Cross-link added to See also.

Adopters land the migration on SalesPlatform today; ProposalManager
arrives as a follow-up Protocol that splits the class along the
recipe seam without re-porting either side.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bokelley added a commit that referenced this pull request May 4, 2026
Salesagent's multi-adapter abstraction is vestigial: GAM is the only
deployed backend (~99% of clients per migration guide §"Migration
order"); Kevel/Broadstreet/Triton/Xandr are scaffolding with no
client traffic; mock is a fixture. Treating salesagent as a GAM
agent that ships dead code simplifies the experiment in three
concrete ways:

1. Wrap target is unconditional GAM. The if-adapter-class switches
   in _impl (e.g., media_buy_create.py:2431-2464) collapse to
   unconditional logic; no compatibility surface to preserve.
2. Single recipe type — salesagent contributes only the GAM shape
   to #502's typed-recipe model. Phase 1 falsification narrows.
3. MockAdServer (~1,800 LOC) deletion joins post-experiment cleanup.

Updates:
- New "Reframing" section after Two phases
- Out of scope clarified: other adapters slated for deletion, not
  preserved
- Next steps adds adapter deprecation roadmap (~3,500-4,000 LOC
  deletion across 4 sequenced PRs) and side-car-to-runtime
  promotion path
- Note that #489 §3.1 needs a "single-adapter adopters skip
  PlatformRouter" addition (tracked separately)

Doesn't change experiment shape: two-platform composition seam,
recipe falsification target, HITL/webhook/auth shim work all stay.
Reframing simplifies the success path; doesn't shrink the questions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PlatformRouter is the right primitive for heterogeneous platforms
behind one process. Single-adapter adopters (salesagent today —
GAM-only deployments, with Kevel/Broadstreet/Triton/Xandr being
unused scaffolding) skip the router entirely: instantiate one
GAMPlatform, pass to serve(), let multi-tenancy ride on
Account.metadata['tenant_id'].

For that shape the migration is "delete the registry, instantiate
one GAMPlatform," not "translate registry into router." Adding a
"Who needs this" callout at the top of §3.1 so single-adapter
adopters don't waste time on the router pattern they don't need.

Also fixes a verbatim duplicate paragraph at the end of §3.1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bokelley added a commit that referenced this pull request May 4, 2026
Three more Step 0 investigations completed; doc updated.

(1) check_parameter_alignment.py analyzed
- Guard enumerates pairs of (mcp_wrapper, a2a_raw) from a hardcoded
  tools list (lines 36-78). Does NOT enumerate "all callers of
  _impl." Side-car's GAMDecisioningPlatform calling _impl directly
  is invisible to the guard.
- Confirmed: zero allowlist additions needed.

(2) _already_approved sentinel works as-is
- compose_method (compose.py:173-194) passes req through unchanged
  from before-hook to inner; no model_validate/model_copy/model_dump
  on the request side.
- Dispatcher only model_dumps on response side (dispatch.py:1234,
  1306-1307).
- Generated request models use extra='forbid' (validation-time only)
  without frozen=True or validate_assignment=True; setattr lands in
  __dict__ and persists through Python-level dispatch.
- No typed marker prototype needed for this experiment. Q4 design
  question stays open as a Protocol RFC.

(3) Webhook signing parity does NOT hold (real finding)
- Salesagent's webhook_authenticator.py emits X-Webhook-Signature +
  X-Webhook-Timestamp with payload f"{ts}.{json.dumps(payload,
  separators=(',',':'), sort_keys=True)}" canonicalization.
- SDK's from_adcp_legacy_hmac emits X-AdCP-Signature +
  X-AdCP-Timestamp + X-AdCP-Key-Id with different canonicalization.
- Different headers, different canonicalization, different scheme
  entirely.
- §3.14's "adopters delete their webhook plumbing wholesale" claim
  doesn't hold cleanly — production cutover requires buyer migration.
- Experiment validates SDK→SDK signing with adcp.WebhookReceiver as
  test buyer. Does not validate the migration claim against actual
  subscribed buyers.

Constraint added to Step 0:
- Local fork edits only. No upstream PRs to salesagent. The
  scheduler skips become hardcoded constants or env-var consults
  in our experiment fork; revert with `git checkout main`.

Updates flow through:
- Step 0.4: prereq is local fork patch, not alembic migration
- Step 0 Investigated section: three more ✅ items + one ⚠️
- Workstream 2.4: webhook test buyer is adcp.WebhookReceiver (not
  a real subscribed buyer; that's out of scope post-experiment)
- Risks: webhook signing parity bullet rewritten with the correct
  finding (incompatible, not "verify before run")
- Next steps adds 3a — correct §3.14 of #489 migration guide

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bokelley bokelley closed this May 4, 2026
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