Skip to content

Commit b1fe9d5

Browse files
authored
feat(storyboards): require full proposal_finalize chain after @adcp/sdk@6.10.0 (#565)
The storyboard runner v6.10.0 closed the sync_accounts cascade gap (adcp#4053, adcp-client#1146/#1545) — the sole-stateful-step exemption now lets refine_proposal / finalize_proposal / accept_proposal run end-to-end against agents that don't implement sync_accounts. CI assertion was previously gated to setup + brief_with_proposals (the only phases the runner could execute on the older SDK). Now asserts all 5 phases pass, with per-scenario likely-cause hints in the failure message so a contributor breaking finalize wiring gets pointed at the right module + design-doc anchor. Adapter fix: examples/sales_proposal_mode_seller/src/platform.py incorrectly required either proposal_id OR ctx.recipes — but the storyboard runner accepts a committed proposal by sending packages[] directly (LLM-derived from the committed proposal's allocations). The check now allows three valid shapes: proposal_id alone (framework hydrates ctx.recipes), packages[] alone (regular create_media_buy flow), or both empty (rejected as INVALID_REQUEST). Documented in the method docstring. Storyboard locally: 5/5 phases PASS (was 2/5). Test suite unaffected.
1 parent 2e351b0 commit b1fe9d5

2 files changed

Lines changed: 55 additions & 24 deletions

File tree

.github/workflows/ci.yml

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -817,12 +817,12 @@ jobs:
817817
818818
- name: Run storyboard — proposal_finalize
819819
timeout-minutes: 5
820-
# Targeted run of the v1.5-relevant scenario. Setup +
821-
# brief_with_proposals must pass — those exercise the framework's
822-
# finalize-interception seam and draft-persistence wiring. Later
823-
# phases (refine / finalize / accept) chain through stateful
824-
# state that the storyboard runner seeds via sync_accounts;
825-
# broader stateful-chain support is orthogonal v1.5 work.
820+
# Full proposal_finalize storyboard. With @adcp/sdk@6.10.0 the
821+
# runner's stateful-chain controller exercises every phase
822+
# (setup, brief, refine, finalize, accept) end-to-end against
823+
# the framework's intercept seam. Earlier SDK versions skipped
824+
# refine/finalize/accept due to a sync_accounts cascade gap
825+
# that 6.10.0 closed (adcp#4053, adcp-client#1146/#1545).
826826
run: |
827827
adcp storyboard run \
828828
http://127.0.0.1:3003/mcp media_buy_seller/proposal_finalize \
@@ -840,14 +840,17 @@ jobs:
840840
sys.exit(1)
841841
with p.open() as f:
842842
d = json.load(f)
843-
# Assert the v1.5 dispatch-path scenarios that exercise the
844-
# framework's intercept seam pass. The seam runs in:
845-
# - proposal_finalize/setup (sync_accounts is skip-tolerant)
846-
# - proposal_finalize/brief_with_proposals (manager.get_products
847-
# round-trip + framework draft persistence)
843+
# Every phase of the proposal_finalize storyboard must pass:
844+
# setup → brief_with_proposals → refine_proposal →
845+
# finalize_proposal → accept_proposal. Each exercises a
846+
# different framework seam — see the per-scenario hints
847+
# below for what's wired where.
848848
required_passing = {
849849
'media_buy_seller/proposal_finalize/setup',
850850
'media_buy_seller/proposal_finalize/brief_with_proposals',
851+
'media_buy_seller/proposal_finalize/refine_proposal',
852+
'media_buy_seller/proposal_finalize/finalize_proposal',
853+
'media_buy_seller/proposal_finalize/accept_proposal',
851854
}
852855
passed = set()
853856
for track in d.get('tracks', []) or []:
@@ -861,9 +864,8 @@ jobs:
861864
# rather than just naming a scenario.
862865
hints = {
863866
'media_buy_seller/proposal_finalize/setup': (
864-
'sync_accounts dispatch failed. The seam is in '
865-
'src/adcp/decisioning/handler.py and '
866-
'src/adcp/decisioning/proposal_dispatch.py. See '
867+
'sync_accounts dispatch failed (or the runner stopped '
868+
'applying the sole-stateful-step exemption). See '
867869
'docs/proposals/proposal-manager-v15-design.md § D5.'
868870
),
869871
'media_buy_seller/proposal_finalize/brief_with_proposals': (
@@ -872,6 +874,25 @@ jobs:
872874
'in src/adcp/decisioning/proposal_dispatch.py. See '
873875
'docs/proposals/proposal-manager-v15-design.md § D1.'
874876
),
877+
'media_buy_seller/proposal_finalize/refine_proposal': (
878+
'Refine iteration broke. Check refine_products on '
879+
'examples/sales_proposal_mode_seller/src/proposal_manager.py '
880+
'and the maybe_persist_draft_after_get_products '
881+
'overwrite path. See § D1 (refine iteration).'
882+
),
883+
'media_buy_seller/proposal_finalize/finalize_proposal': (
884+
'Finalize interception broke. Check '
885+
'maybe_intercept_finalize in proposal_dispatch.py + '
886+
'manager.finalize_proposal in the example. The seam '
887+
'must commit via store.commit before projecting. See § D2.'
888+
),
889+
'media_buy_seller/proposal_finalize/accept_proposal': (
890+
'create_media_buy(proposal_id=...) or '
891+
'create_media_buy(packages=[...]) broke. Check '
892+
'maybe_hydrate_recipes_for_create_media_buy + '
893+
'mark_proposal_consumed in proposal_dispatch.py and '
894+
'the platform.create_media_buy adapter. See § D3 + D7.'
895+
),
875896
}
876897
print('FAIL: required scenarios did not pass:')
877898
for s in sorted(missing):
@@ -882,7 +903,7 @@ jobs:
882903
print('--- raw storyboard result ---')
883904
print(json.dumps(d, indent=2))
884905
sys.exit(1)
885-
print('PASS: v1.5 dispatch-path scenarios pass')
906+
print('PASS: full proposal_finalize storyboard chain (5/5 phases)')
886907
"
887908
888909
- if: always()

examples/sales_proposal_mode_seller/src/platform.py

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -90,26 +90,36 @@ def create_media_buy(
9090
req: Any,
9191
ctx: RequestContext[Any],
9292
) -> dict[str, Any]:
93-
"""Accept a proposal-id-driven media buy.
93+
"""Accept a media buy.
9494
95-
Per the wire spec when ``proposal_id`` is present, ``packages``
96-
may be empty — the platform derives packages from the proposal.
97-
Read recipes from ``ctx.recipes`` to get the typed
98-
:class:`ProposalModeRecipe` the framework hydrated.
95+
Three valid shapes:
96+
97+
* ``proposal_id`` set, ``packages`` empty — the framework
98+
hydrated ``ctx.recipes`` from the committed proposal; the
99+
adapter reads recipes to wire upstream line items.
100+
* ``packages[]`` populated, ``proposal_id`` empty — buyer
101+
constructed packages explicitly. Storyboard runner v6.10.0
102+
accepts a committed proposal this way (LLM-derived packages
103+
from the proposal's allocations). The adapter processes
104+
packages directly; no recipes hydration.
105+
* Both empty — invalid request, neither path is wireable.
99106
"""
100107
proposal_id = getattr(req, "proposal_id", None)
101-
if proposal_id is None and not ctx.recipes:
108+
packages = getattr(req, "packages", None) or []
109+
if proposal_id is None and not packages and not ctx.recipes:
102110
raise AdcpError(
103111
"INVALID_REQUEST",
104112
message=(
105-
"create_media_buy requires either packages[] or " "proposal_id; got neither."
113+
"create_media_buy requires either packages[] or proposal_id; got neither."
106114
),
107115
recovery="correctable",
108116
field="proposal_id",
109117
)
110118

111-
# Recipes are keyed by product_id. The framework populated them
112-
# from the committed proposal.
119+
# Recipes are keyed by product_id when the buyer accepted a
120+
# committed proposal via proposal_id; empty when the buyer
121+
# constructed packages explicitly. Both shapes flow through
122+
# this adapter.
113123
recipes_seen: dict[str, str] = {}
114124
for product_id, recipe in ctx.recipes.items():
115125
if not isinstance(recipe, ProposalModeRecipe):

0 commit comments

Comments
 (0)