docs(proposals): revise recipe model — adopter-owned, not framework-managed#507
Closed
bokelley wants to merge 4 commits into
Closed
docs(proposals): revise recipe model — adopter-owned, not framework-managed#507bokelley wants to merge 4 commits into
bokelley wants to merge 4 commits into
Conversation
…anaged Phase 1A of the salesagent side-car experiment (#506) falsified the framework-managed-recipe-state model in the original draft of #502. Reading dynamic_products.py end-to-end (no harness needed): - Salesagent recipe lives on the Product row (Product.implementation_config: JSONType) - Variants generated at brief time share the template's recipe verbatim and are themselves persistent Product rows - Variants have global hash-dedup, TTLs, archival lifecycle — independent of any proposal lifecycle - Recipe state was Product-scoped and adopter-owned; not framework-managed Pre-registered Q1.5 falsifiers fired: - "Variant Products require new schema rows" — confirmed - "Hash-dedup state crosses sessions" — confirmed Revisions in this PR: 1. Top-level revision note added so reviewers see the change immediately. 2. Layer 2 — Product internal-config (the "recipe") rewritten. - Old: framework manages lifecycle (session cache, persist on finalize, hydrate at create_media_buy, "SDK is system of record") - New: framework TYPES the recipe contract via recipe_type; framework does NOT manage storage; recipe is adopter-owned 3. What DecisioningPlatform keeps (the seam) revised. - Old: framework hydrates proposal/product, looks up recipe by id, populates ctx.recipes - New: adopter populates ctx.recipes from its own storage; framework receives, types, validates against recipe_type + capability_overlap 4. The proposal workflow (finalize lifecycle) revised. - Old: framework provides session cache, recipe persistence on finalize, recipe hydration through buy lifecycle - New: framework provides wire-level lifecycle types, finalize transition routing, typed dispatch; storage and persistence are adopter-owned. expires_at enforcement: open question between adopter-checked and framework-helper. What didn't change: - The four-layer model (Wire, Recipe, Capability overlap, Supporting tables) - The two-platform composition (ProposalManager + DecisioningPlatform) - Path A / Path B recipe type sharing (those are about TYPE discrimination, not storage ownership) - Three concrete shapes (LinkedIn, Prebid, mock-backed) - Proposal-side capabilities being sales-axis-scoped The framework's job is smaller and more focused after this revision: type the contract; route transitions; dispatch typed recipes. Storage of recipes, proposals, and proposal state lives in the adopter's existing data model — which is where it always lived in salesagent's case. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds examples/recipe_falsification/ — the pytest harness for Q2 of the salesagent side-car experiment (PR #506): > Does the recipe shape carry GAM's implementation_config without > escape hatches? Files: - gam_recipe.py — typed Pydantic GAMRecipe model with sub-models (CreativePlaceholder, FrequencyCap), Literal-typed enums for every documented GAM API value, extra="forbid" on every model - fixtures/gam_impl_config_examples.json — five fixture shapes derived from salesagent's GAMProductConfigService: guaranteed_default, non_guaranteed_default, video_with_targeting, native_with_discount, minimal - test_recipe_round_trip.py — runs the four pre-registered Q2 falsifiers from PR #506: (a) any extra: dict[str, Any] field (b) any # type: ignore needed to construct (c) lossy round-trip dict → recipe → dict extra-forbid: smuggled fields rejected - README.md — what's here, how to run, results, caveats Result: 8 tests pass. All Q2 falsifiers refuse to fire. - Round-trip is lossless across all 5 fixture shapes - Zero Any-typed fields; only dict-typed field is custom_targeting_keys typed strictly as dict[str, str | list[str]] per GAM's API contract, NOT an escape hatch - Direct construction with sub-models needs no # type: ignore - Unknown fields are rejected by extra="forbid" Q2 prior holds: a typed Pydantic recipe carries the full GAM implementation_config shape without escape hatches. Combined with Q1.5 (Phase 1A — recipe is adopter-owned, not framework-managed; corrected in this PR's revision of #502), the architecture story is now: - Recipe is typed at framework boundary (Q2 confirmed) - Recipe storage is adopter-owned (Q1.5 confirmed) - Framework's job: type the contract, route transitions, dispatch Caveats documented in README: - Fixtures derived from service code paths, not production DB dumps; dev-DB validation pass would tighten the result - custom_targeting_keys typing follows GAM's documented API; deeper- nested salesagent data would reject (correct against GAM, may surface migration edges) - Literal[...] enums need versioning when GAM adds enum values Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The architectural claim (recipe is adopter-owned; framework types, doesn't cache) is general. The empirical evidence in examples/recipe_falsification/ is GAM-specific by construction — validates GAMRecipe against GAM's implementation_config, not other adopter shapes. Added explicit scope note to the top-level revision callout so reviewers don't read the change as GAM-only thinking. Multi-adopter validation (LinkedIn, Meta, TikTok, Prebid multi-decisioning) is future work that would tighten Q2 across more shapes; the general architecture claim doesn't wait on it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reference implementation for Phase 2 of the salesagent side-car experiment (PR #506). Lives in adcp-client-python as a doc-and- review-friendly skeleton; deploys into a salesagent fork worktree under src/sdk_runtime/ where it imports salesagent's _impl functions and models. Files: - account_store.py — SalesagentBuyerAgentRegistry + SalesagentAccountStore. Bearer-token (Principal.access_token) lookup; AgentAccountAccess scoping; sandbox→mode projection. fetch_gam_manual_approval_required helper for the HITL gate. - gam_platform.py — GAMPlatform wraps salesagent's _impl functions: _get_products_impl, _create_media_buy_impl, _update_media_buy_impl, _get_media_buy_delivery_impl, _sync_creatives_impl. Builds ResolvedIdentity from SDK ctx. - hitl_gate.py — compose_method before-hook. Wire→GAM-internal operation name mapping (create_media_buy, update_media_buy, sync_creatives→add_creative_assets). Writes WorkflowStep + MediaBuy(raw_request=...) rows via salesagent's existing WorkflowManager; short-circuits with status='pending_approval'. - serve_sidecar.py — adcp.serve(...) entrypoint. Wires platform + HITL gates + auth shim + WebhookSender (SDK→SDK signing per Step 0.6, since salesagent's scheme is incompatible). - README.md — deployment recipe (docker-compose addition, nginx routing config, scheduler patches, tenant configuration, storyboard run commands), exit criteria, open work in scaffold. - __init__.py The salesagent imports are wrapped in try/except so the files lint and import in adcp-client-python's tree without salesagent installed; SALESAGENT_AVAILABLE flag gates runtime behavior. Status: structurally complete reference. Actual storyboard run requires deploying into a salesagent fork worktree with sandbox GAM credentials, configuring the experiment tenant, and running nginx + docker-compose (recipe in README). Local-fork only — no upstream PR to salesagent per the experiment scope constraint. Open gaps documented in README: - _build_resolved_identity may need more fields than the minimal projection here - WorkflowManager constructor signature varies across salesagent versions; pin to a commit - Webhook signing parity is SDK→SDK only (Step 0.6 finding) Recipe shape already validated end-to-end in Phase 1B (examples/recipe_falsification/, 8 tests pass). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Phase 1A of the salesagent side-car experiment (#506) falsified the framework-managed-recipe-state model in the original draft of #502.
Reading `dynamic_products.py` end-to-end (no harness needed) showed that salesagent's recipe is Product-scoped and adopter-owned, not proposal-scoped and framework-managed. Two pre-registered Q1.5 falsifiers fired:
What changes
The original draft claimed the framework manages recipe lifecycle: session cache against `proposal_id` during refine, persistence on finalize, hydration at `create_media_buy`, "SDK is system of record." That's wrong against salesagent's actual code.
Revised model: the framework types the recipe contract but does not manage recipe storage. Storage is adopter-owned (Product rows or equivalent in adopter persistence). The seam the framework owns is the typed contract: `recipe_type: ClassVar[type[Recipe]]` on `DecisioningPlatform`, validated at dispatch.
Three sections rewritten:
What didn't change
Net effect
The framework's job is smaller and more focused: type the contract, route transitions, dispatch typed recipes. Storage of recipes and proposal state lives in the adopter's existing data model — where it always lived in salesagent's case.
Refs
Test plan
🤖 Generated with Claude Code