Skip to content

Commit 588b3a1

Browse files
bokelleyclaude
andcommitted
docs(migration): cross-link product architecture (#502) into §3.3 + §3.5
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>
1 parent 70afb7e commit 588b3a1

1 file changed

Lines changed: 104 additions & 41 deletions

File tree

examples/multi_platform_seller/MIGRATION_FROM_ADAPTER_REGISTRY.md

Lines changed: 104 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -140,10 +140,12 @@ yet and become greenfield work during the port.
140140
implementation — a global tool with `signals_agent_registry`
141141
cross-tenant lookup, including dynamic-product assembly from
142142
signal-agent inputs. The SDK has `SignalsPlatform` per-tenant
143-
behind the router; the bigger open question is whether the SDK
144-
should grow first-class `inventory_store` / `signal_store`
145-
primitives so `get_products` can assemble dynamically the way
146-
salesagent does today. See §3.5.
143+
behind the router for the marketplace surface; the
144+
dynamic-product-assembly piece is proposal-side per
145+
[#502](https://github.com/adcontextprotocol/adcp-client-python/pull/502)
146+
— any `inventory_store` / `signal_store` primitives the SDK ships
147+
land on `ProposalManager`, not on `SignalsPlatform` or
148+
`DecisioningPlatform`. See §3.5.
147149
* **Properties.** `src/core/tools/properties.py` is a global tool
148150
too. AdCP 3.0 lifts list publishing onto `PropertyListsPlatform`
149151
per-tenant; same tool→platform shape change as signals.
@@ -344,6 +346,31 @@ ceremony, and the dry-run plumbing.
344346

345347
### 3.3 Product discovery and the refine flow
346348

349+
§3.3 names a seam the rest of the guide treats lightly. `get_products`
350+
is *proposal-side* — it assembles candidate inventory from a buyer
351+
brief. `create_media_buy` is *decisioning-side* — it executes the
352+
buy against an upstream. Salesagent fuses these inside one
353+
`AdServerAdapter` class today. The SDK is moving toward a two-platform
354+
composition — `ProposalManager` (proposal assembly) + `DecisioningPlatform`
355+
(upstream execution) — with the typed `implementation_config` "recipe"
356+
as the contract between them. The
357+
[product architecture doc (#502)](https://github.com/adcontextprotocol/adcp-client-python/pull/502)
358+
walks through the layered model and the binding shapes.
359+
360+
For this migration, the split doesn't change what you write today.
361+
`SalesPlatform` carries both surfaces — `get_products` and
362+
`create_media_buy` ride on the same class — and that's the
363+
recommended port target. But knowing the seam exists shapes how you
364+
port: keep proposal-assembly logic (catalog projection, refinement,
365+
signal-driven assembly) separable from decisioning-side translation
366+
(upstream API calls, error projection, lifecycle assertions) inside
367+
the platform body. When `ProposalManager` lands as a first-class
368+
Protocol, you split the class along the seam without re-porting
369+
either side. The `Product.implementation_config: JSONType` column
370+
salesagent already carries (`models.py:256`) is the recipe — it
371+
already flows from proposal-side assembly to decisioning-side
372+
execution; the SDK just gives the seam a name.
373+
347374
Salesagent already has the right *idea* for product config — the
348375
`Product.implementation_config: JSONType` column carries
349376
adapter-specific config (line item template id, ad unit ids, GAM
@@ -512,6 +539,21 @@ This is genuinely new work in salesagent. There's no existing code
512539
path to translate; the migration is to add a refine handler beside
513540
the existing brief handler.
514541

542+
The full proposal lifecycle — `refine` with
543+
`action='finalize'` transitioning a draft proposal to committed with
544+
a locked `expires_at` inventory hold — is what
545+
[#502](https://github.com/adcontextprotocol/adcp-client-python/pull/502)
546+
calls out as `ProposalManager` territory. The framework will own the
547+
session cache for in-flight recipes, the `finalize` transition, and
548+
`expires_at` enforcement at `create_media_buy` time. For this port:
549+
land the refine handler in `SalesPlatform.get_products` as described
550+
above; the proposal-store plumbing arrives separately when
551+
`ProposalManager` lands. Adopters who want to start emitting
552+
`proposal_id` today can do so against the existing
553+
`find_proposal_by_id` hook; full lifecycle handling (draft → committed,
554+
HITL approval routing, persistence through buy lifetime) is framework
555+
work, not adopter work.
556+
515557
### 3.4 Creative: keep what you have, the SDK absorbs spec churn
516558

517559
Salesagent's current creative shape — `CreativeEngineAdapter` with
@@ -559,7 +601,7 @@ neither is required for the salesagent migration. Keep the
559601
existing creative engine as it is; let the SDK carry the spec
560602
revision when the wire shape firms up.
561603

562-
### 3.5 Signals: a slightly different shape, plus an open architectural question
604+
### 3.5 Signals: a slightly different shape, plus dynamic-product assembly on the proposal side
563605

564606
Salesagent's signals surface
565607
(`src/core/tools/signals.py` + `src/core/signals_agent_registry.py`)
@@ -569,27 +611,33 @@ already call an internal publisher signals agent and assemble
569611
dynamic products from signal-agent inputs — that's real cross-tenant
570612
logic the SDK doesn't model directly today.
571613

572-
The framing isn't tool-shaped → platform-shaped. The framing is:
573-
the existing logic ports across, and there's an open architectural
574-
question about how far the SDK should grow into the territory
575-
salesagent currently owns.
576-
577-
**The open question.** Salesagent today has dynamic products from
578-
the signals agent — `get_products` can assemble pieces using
579-
signal-agent inputs and the resulting products can carry
580-
key-value targeting that threads through to `create_media_buy`.
581-
The SDK doesn't have first-class primitives for that assembly. A
582-
plausible direction is for the SDK to grow `inventory_store` and
583-
`signal_store` concepts so `get_products` can compose products
584-
dynamically without each adopter rebuilding the assembly logic.
585-
That hasn't happened yet.
586-
587-
**The threading concern.** When `get_products` returns assembled
588-
products with key-value targeting, that targeting needs to flow
589-
through to `create_media_buy`. This is real work. Today it lives
590-
adopter-side; long-term it might move into the SDK alongside the
591-
inventory/signal-store concepts. Either way, one side has to own
592-
it — naming the seam matters more than where it lands first.
614+
The framing isn't tool-shaped → platform-shaped. The framing is two
615+
distinct concerns: (1) the signals-marketplace surface ports to
616+
`SignalsPlatform`; (2) the dynamic-product-assembly logic that
617+
consults signals at `get_products` time is *proposal-side*, and
618+
[#502](https://github.com/adcontextprotocol/adcp-client-python/pull/502)
619+
names the home for it.
620+
621+
**Dynamic-product assembly is a `ProposalManager` concern.** Salesagent
622+
today has dynamic products from the signals agent — `get_products` can
623+
assemble pieces using signal-agent inputs and the resulting products
624+
can carry key-value targeting that threads through to
625+
`create_media_buy`. Per #502's layered model, this is proposal-side
626+
logic: the assembly reads supporting tables (inventory, signals, rate
627+
cards) and produces typed recipes for the decisioning side to execute.
628+
If the SDK grows `inventory_store` / `signal_store` primitives, they
629+
land on `ProposalManager`, not `DecisioningPlatform`. For the
630+
migration, that resolves where this code lives long-term: on the
631+
proposal side of the platform, even though `SalesPlatform` carries
632+
both surfaces today.
633+
634+
**The threading concern is the recipe.** When `get_products` returns
635+
assembled products with key-value targeting, that targeting needs to
636+
flow through to `create_media_buy`. The `implementation_config` /
637+
recipe (§3.3) IS the threading mechanism — typed JSON that flows from
638+
proposal-side assembly through the framework to decisioning-side
639+
execution. Salesagent already does this with its
640+
`Product.implementation_config` column; the SDK formalizes the seam.
593641

594642
**For the migration today:** port the existing
595643
`core/tools/signals.py` body into a `SignalsPlatform` impl per
@@ -633,13 +681,17 @@ router entirely; buyers calling `get_signals` against those tenants
633681
get `UNSUPPORTED_FEATURE` from the framework via
634682
`validate_platform()`.
635683

636-
**Expect this surface to evolve.** If the SDK grows `inventory_store`
637-
+ `signal_store` primitives, the dynamic-product-assembly logic
638-
that lives in salesagent's `get_products` today moves into shared
639-
SDK infrastructure, and the key-value-targeting threading becomes
640-
SDK-owned. Adopters who port to `SignalsPlatform` now will inherit
641-
that evolution; the platform method bodies don't need to change
642-
when the assembly primitives land.
684+
**Expect this surface to evolve.** Per #502, if the SDK grows
685+
`inventory_store` / `signal_store` primitives they live on
686+
`ProposalManager`, and the dynamic-product-assembly logic that lives
687+
in salesagent's `get_products` today migrates onto the proposal side
688+
of the platform. The key-value-targeting threading is already
689+
SDK-owned in concept — it's the recipe. Adopters who port to
690+
`SignalsPlatform` now will inherit the evolution; the
691+
`SignalsPlatform` method bodies don't change when the assembly
692+
primitives land, because they're orthogonal — `SignalsPlatform` is
693+
the marketplace-facing surface, the assembly primitives are
694+
proposal-side.
643695

644696
### 3.6 Reporting and delivery surfaces
645697

@@ -1363,14 +1415,19 @@ gaps from the migration, not flaws in the SDK:
13631415
§3.3 covers the wire shape and the `find_proposal_by_id` threading
13641416
hook. The first pragmatic pass is per-product narrowing; the full
13651417
proposal lifecycle (`'draft'``'committed'`, expiry,
1366-
inventory-reservation semantics) is a larger build.
1367-
* **Signals has an open architectural question.** §3.5 covers the
1368-
port from `core/tools/signals.py` to a per-tenant
1369-
`SignalsPlatform`. The bigger question — whether the SDK should
1370-
grow `inventory_store` / `signal_store` primitives so
1371-
`get_products` can assemble dynamic products with key-value
1372-
targeting threading — is unresolved. Adopters port what they
1373-
have today; expect that surface to evolve.
1418+
inventory-reservation semantics) is `ProposalManager` framework
1419+
work per [#502](https://github.com/adcontextprotocol/adcp-client-python/pull/502),
1420+
not adopter work — adopters land the refine handler today and
1421+
inherit the lifecycle when `ProposalManager` lands.
1422+
* **Signals dynamic-product assembly is proposal-side.** §3.5 covers
1423+
the port from `core/tools/signals.py` to a per-tenant
1424+
`SignalsPlatform`. The dynamic-product-assembly logic that consults
1425+
signals at `get_products` time lives on the proposal side of the
1426+
platform; per #502 it's a `ProposalManager` concern, and any
1427+
`inventory_store` / `signal_store` primitives the SDK eventually
1428+
ships land there. Adopters port what they have today; the
1429+
marketplace-facing `SignalsPlatform` surface and the assembly
1430+
primitives are orthogonal and don't block each other.
13741431
* **Per-creative delivery analytics are upstream-dependent.**
13751432
`get_creative_delivery` (§3.6) requires reporting at creative
13761433
granularity. GAM exposes this; most other ad servers don't. If
@@ -1407,3 +1464,9 @@ gaps from the migration, not flaws in the SDK:
14071464
* [Issue #477](https://github.com/adcontextprotocol/adcp-client-python/issues/477)
14081465
— the multi-platform proof, the `PlatformRouter` recipe, and the
14091466
acceptance criteria the parallel implementation PR satisfies.
1467+
* [`docs/proposals/product-architecture.md`](../../docs/proposals/product-architecture.md)
1468+
([PR #502](https://github.com/adcontextprotocol/adcp-client-python/pull/502))
1469+
— the layered product model and two-platform composition
1470+
(`ProposalManager` + `DecisioningPlatform`). §3.3 and §3.5 of this
1471+
guide reference it for the proposal/decisioning seam adopters will
1472+
split along long-term.

0 commit comments

Comments
 (0)