@@ -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+
347374Salesagent already has the right * idea* for product config — the
348375` Product.implementation_config: JSONType ` column carries
349376adapter-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
512539path to translate; the migration is to add a refine handler beside
513540the 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
517559Salesagent's current creative shape — ` CreativeEngineAdapter ` with
@@ -559,7 +601,7 @@ neither is required for the salesagent migration. Keep the
559601existing creative engine as it is; let the SDK carry the spec
560602revision 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
564606Salesagent'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
569611dynamic products from signal-agent inputs — that's real cross-tenant
570612logic 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
633681get ` 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