feat(adcp)!: type media-buy seller follow-ups#142
Conversation
There was a problem hiding this comment.
Right shape on the typed fields. Breaking-change marker is the block — repo is at adcp-v1.0.0 and release-please will cut v1.1.0 with breaking Go API changes baked in, violating the SemVer contract adopters get by pinning ^v1.0.0.
The breaking surface is real:
adcp/types.go:480-487—PackageDelivery.Totalsfield removed. Adopters readingpkg.Totals.Impressionswill not compile.adcp/inputs.go:48,66—UpdateMediaBuyRequest.CanceledandPackageUpdate.Canceledflip fromboolto*bool. Callers passingCanceled: truewill not compile.adcp/inputs.go:32,71—CreativeAssignmentsflips from[]anyto[]CreativeAssignment. Callers using inlinemap[string]any{...}will not compile.adcp/responses.go:23-36—MediaBuyResponseno longer flattensext.*keys to the response root. Adopters using the old shape (or stuffingtask_id/messageintoExt) see a wire-shape change.
The single commit is feat(adcp): type media-buy seller follow-ups. It needs feat(adcp)!: (or a BREAKING CHANGE: footer) so release-please cuts v2.0.0. This is the MUST-FIX gate AGENTS.md documents — one-line fix at the commit subject.
Things I checked
- Schema/types coherence:
MediaBuyDataadded toKNOWN_TYPESinadcp/schemas/generate.pyandEXEMPTinadcp/schemas/lint.py:79;CreativeAssignmentregistered inEXPLICIT_SCHEMAatlint.py:124.adcp/types_gen.goregenerated cleanly (drops the oldMediaBuyData, updatesPackage.CreativeAssignmentsfrom[]anyto[]CreativeAssignment). Schema bundle is downloaded at build time (adcp/schemas/download.sh) — I trust the author'spython3 lint.py --strictrun. - The
decorateMediaBuypath atreference/seller-agent/cmd/seller-agent/main.go:632clearsExt, but the forced/submitted path at line 310 returns before decorate runs, so adopter-provided ext on submitted buys is preserved. weightedSpendinreference/seller-agent/cmd/seller-agent/main.go:608is the right fix — oldspend / float64(count)over-allocated equal shares across uneven budgets; new code weights bypkg.Budget / totalBudgetand the test atmain_test.go:328-359locks it in (100/300 split → 10/30 spend).- Context-echo in
adcp/seller.go:185-294is applied symmetrically on success and error paths across all six tools. Clean. MediaBuyDatais in bothEXEMPT(line 79) andEXPLICIT_SCHEMA(line 123) — theEXPLICIT_SCHEMAentry is now dead code becauseEXEMPTshort-circuits first atlint.py:381. Pre-existing condition that this PR worsens slightly.
Follow-ups (non-blocking — file as issues)
-
task_idemitted unconditionally on the submitted path —adcp/responses.go:26builds{"status": "submitted", "task_id": data.TaskID}even whendata.TaskID == "". AdCP's submitted envelope markstask_idas required-non-empty; the SDK should guard withif data.TaskID != ""or fail-fast inMediaBuyResponseso adopters can't ship an empty-string task_id. Reference seller always sets it (main.go:310), so no test catches this today. -
MediaBuyDatain EXEMPT loses drift coverage on the four new envelope fields.Currency,ValidActions,TaskID,Messageare not incore/media-buy.json— the PR's design intent is "base schema permits additional response properties." That's plausible (in-tree precedent:MediaBuyListItemcarries top-levelcurrency), butMediaBuysResponseatadcp/responses.go:39-42now returns raw[]MediaBuyDataitems. If the items-schema forget_media_buys.media_buys[]setsadditionalProperties: false, the four new top-level keys are wire drift the linter won't catch. Worth a sanity check. -
Dead
EXPLICIT_SCHEMAentry.adcp/schemas/lint.py:123mapsMediaBuyData→core/media-buy.json, butMediaBuyDatanow sits inEXEMPTat line 79; the short-circuit at line 381 means the EXPLICIT_SCHEMA entry never resolves. Drop one or the other.
Minor nits (non-blocking)
-
formatRefKeyseparator.reference/seller-agent/cmd/seller-agent/main.go:299joinsAgentURL + "\x00" + IDfor a map key.FormatRefis{AgentURL, ID}with both fields strings — comparable, somap[adcp.FormatRef]booldrops the separator entirely. Same correctness, less ceremony. -
decorateMediaBuyclearsExtsilently.reference/seller-agent/cmd/seller-agent/main.go:632setsbuy.Ext = nil. Today no path routes a forced/submitted buy throughdecorate, so adopter-provided ext is safe. A one-line comment ("clears Ext deliberately — typed fields are the contract") would prevent a future regression. -
MediaBuyData.Statusasymmetry.adcp/types.go:443keepsStatusnon-omitemptywhile peer schema-required fields (MediaBuyID:442,TotalBudget:448,Packages:449) gained,omitempty. Submitted path bypasses the struct so this doesn't bite today, but a zero-valueMediaBuyData{}marshals"status": ""to the wire viaMediaBuysResponse. Either keep the required fields non-omitemptyfor symmetry, or document the asymmetry.
Retitle the commit feat(adcp)!: type media-buy seller follow-ups. Happy to re-review once the breaking marker is in place.
e830235 to
97d2c6f
Compare
There was a problem hiding this comment.
Approving. Typed envelope beats stringly-typed Ext, and the KNOWN_TYPES / EXEMPT / EXPLICIT_SCHEMA story is consistent with AGENTS.md §58–60.
Things I checked
feat(adcp)!:on the commit. The wire-shape breaks (PackageDelivery.Totalsremoved,UpdateMediaBuyRequest.Canceledbool→*bool,MediaBuyDatano longer flattensext) get the breaking marker. Title drops the!but the commit doesn't, which is what release-please reads.adcp/types_gen.gois a clean regen: theMediaBuyDatablock is gone (now hand-written intypes.go), andPackage.CreativeAssignmentsswaps to[]CreativeAssignment. No hand-edits.MediaBuyDatais in bothKNOWN_TYPES(adcp/schemas/generate.py:34) andEXEMPT(adcp/schemas/lint.py:79).CreativeAssignmentis inKNOWN_TYPES(adcp/schemas/generate.py:70) andEXPLICIT_SCHEMA(adcp/schemas/lint.py:123), so strict-parity lint still checks the typed assignment shape againstcore/creative-assignment.json. The PR validation ranpython3 lint.py --strictclean.- No callers of
Canceled boolremain — four seller-agent tests, all flipped to&canceled. - Submitted-path guard at
adcp/responses.go:27(requireTaskID) is the right call. An async media-buy without a task ID is unrecoverable on the buyer side. item := *buyinget_media_buysis a value copy —decorateMediaBuy(&item)nillingExtdoes not touch the stored buy.weightedSpend(s, b, n*b, n) = s/n— equal-budget case preserves prior split semantics.attachContextrework: each shadowed innerresult, out, e := errorToResult(...)returns from within its ownifblock. No accidental return of the outer name.FormatRefis comparable, valid as a map key. The(agent_url, id)filter is the right shape — same-id-different-agent collisions don't.- 11 files, +271/-128. Test plan ran the full Go suite, all sub-packages, schema strict-lint, golangci-lint, and the storyboard reference seller (
59/59).
Follow-ups (non-blocking — file as issues)
MediaBuyData.TaskID/Messagesit on the media-buy struct alongside core resource fields.omitemptykeeps them off the wire on the active path today, but architecturally they're response-envelope fields, not media-buy fields. If upstream ever lands a media-buy resource that legitimately needs its owntask_id, this collides. Consider splittingMediaBuyData(resource) from a separateCreateMediaBuyEnvelope(envelope) at a future opportunity.ad-tech-protocol-expertflagged the shape, and the trade-off is the lint compromise —MediaBuyDatais now inEXEMPTrather thanEXPLICIT_SCHEMA, so strict-parity drift detection on the in-spec media-buy fields is off until the split lands.PackageDeliveryis now flat (Impressions,Clicks,Spend) whileMediaBuyDelivery.Totalsstays nested (DeliveryTotals). The asymmetry matches the PR's claim about the spec shape, butPackageDeliveryis inEXEMPT, so strict-lint won't catch drift here on the next schema sync. Worth eyeballing againstcore/package-delivery.jsonwhen 3.0.13 lands.forceMediaBuyStatusmutatesbuy.Statuswithout callingdecorateMediaBuy, so the stored buy'sValidActionsgoes stale until the nextget_media_buysrefresh re-decorates the copy. Test-controller path only, but inconsistent with every other status mutator in the file.
Minor nits (non-blocking)
- Redundant zero-total guard.
reference/seller-agent/cmd/seller-agent/main.go:533keepsif total == 0 { continue }after the refactor, even thoughweightedSpendnow handlestotalBudget == 0internally. Pick one location. - Filter equality uses full
FormatRef.reference/seller-agent/cmd/seller-agent/main.go:273usesmap[FormatRef]bool, which compares all five fields. The test only exercises the{agent_url, id}case where the others are zero. If a buyer ever sends a filter withWidth/Heightset, it will silently fail to match a stored creative whoseFormatRefcarries sizing metadata. Either narrow the key to{AgentURL, ID}or document the full-struct semantics. - Submitted-path error shape.
adcp/responses.go:27returns a plainfmt.Errorf. The rest of the SDK boundary usesadcp.NewError("INTERNAL_ERROR", ...)to keep the wire-shape consistent. Only one caller setsStatus: "submitted"andforce_create_media_buy_armvalidatesTaskIDupfront, so this is cosmetic.
LGTM. Follow-ups noted above.
97d2c6f to
3503e49
Compare
There was a problem hiding this comment.
LGTM. Follow-ups noted below. The Argus-followup story is real: typed valid_actions, typed task_id/message, weight *float64 to preserve 0, Canceled *bool, decorateMediaBuy nukes Ext, context echo plumbed through signals + collection — the right shape on every count. feat(adcp)!: is correctly load-bearing here.
Things I checked
feat(adcp)!:marker present on the only commit (3503e493).Canceled bool → *bool,CreativeAssignments []any → []CreativeAssignment,PackageDelivery.Totalsremoval, andMediaBuysResponsesandbox shape — every breaking flip is named in MIGRATING.md and aligned with the!.Bool(sandbox)always returns non-nil so the wire still emits\"sandbox\": false(adcp/responses.go:70).KNOWN_TYPES/EXEMPT/EXPLICIT_SCHEMAhygiene.MediaBuyData,MediaBuyHistoryEntry,SyncCreativeAssignmentadded to both KNOWN_TYPES (adcp/schemas/generate.py:34,47) and EXEMPT (adcp/schemas/lint.py:79-80).CreativeAssignmentcorrectly lands in KNOWN_TYPES and EXPLICIT_SCHEMA (lint.py:124) — that's the combination that keeps the generator from emitting it while leaving the linter to diff property drift againstcore/creative-assignment.json._assert_exempt_subset_knowninvariant holds. Thejson:\"-\"skip in the field parser (lint.py:183) matches the newExtrafield exactly.safe_commentrstrip is the source of the ~120 whitespace-only hunks inadcp/types_gen.go. Spot-checked the typed-name promotions (CreateMediaBuySubmitted,Package.CreativeAssignments []CreativeAssignment,SyncCreativesRequest.Assignments []SyncCreativeAssignment) — generator was rerun, not hand-edited.CreativeAssignmentround-trip is right shape.weight: 0survives via*float64, nil pointer omits, vendor fields land inExtraand emit back, typed fields win on key collision (adcp/inputs_test.go). Reference seller migrates to the typed form at every call site.MediaBuyResponsesubmitted branch rejects emptyTaskIDwithMISSING_FIELDand aSuggestion:pointing atCreateMediaBuySubmittedResponse. Only in-repo submitted-status producer isforce_create_media_buy_armand it now setsTaskID/Messageon the typed envelope (reference/seller-agent/cmd/seller-agent/main.go). Fail-closed beats fail-open.decorateMediaBuyzeroingExt— no in-repo scenario usesMediaBuyData.Extfor legit data; compliance scenarios (seed_product,seed_pricing_option,force_create_media_buy_arm) all go through typed fields. Safe.attachContextis wired on every signals + collection branch, error and success paths both.result, out, e := ...inside theif err != nilblocks declares fresh locals — no outer shadowing leak.weightedSpendhas the divide-by-zero guards in the right order (packageCount == 0short-circuit, thentotalBudget == 0 → equal split), parity with the old equal-split when total is zero, andTestDeliveryReporting_SimulateDeliveryWeightsSpendByBudgetpins the 100/300 → 10/30 split.- Validation block:
go test ./...,cd adcp && go test ./..., both reference agents,e2e/,bench/,lint.py --strict,golangci-lint, and storyboard59/59. Manual storyboard run is checked, not left unchecked.
Follow-ups (non-blocking — file as issues)
MediaBuyDatalint coverage regressed. MovingMediaBuyDataout ofEXPLICIT_SCHEMAintoEXEMPT(adcp/schemas/lint.py:79) turns off property-name drift detection againstcore/media-buy.json. The struct comment inadcp/types.go:434-437justifies it — the base schema allows additional response properties — but a future schema-side rename (sayconfirmed_at→confirmation_timestamp) will not trip the linter. Either reinstate an explicit-schema entry that allows extra Go fields, or keep EXEMPT but add a one-line allowlist comment so the next sync is verifiable. (ad-tech-protocol-expertflagged this.)MediaBuyDatamakes schema-required fieldsomitempty.media_buy_id,total_budget,packagesare allomitemptyon the multi-purpose struct (adcp/types.go:438,444,447). On the sync success path that's a regression vs the generatedCreateMediaBuySuccess. A buggy seller that forgetsMediaBuyIDwill emit a payload silently missing a required field instead of\"media_buy_id\": \"\". The submitted path is already split out viaCreateMediaBuySubmitted, so the success-success fields could stay required. Either dropomitemptyfrom the required-on-success fields, or splitMediaBuyDatainto success + envelope structs.update-media-buy-request.jsonstill not inTOOL_SCHEMAS. Pre-existing gap (not introduced here), butUpdateMediaBuyRequest/PackageUpdateare entirely hand-written with zero schema cross-check (adcp/schemas/generate.py:99-168). The*boolflip onCanceledis the right call given the generatedPackage.Canceledprecedent attypes_gen.go:1844, but the whole request type rides on the honor system. Worth wiring up.valid_actions: []vs omitted, andweight: 0vs omitted, are protocol semantics this PR locks into the SDK without upstream anchoring. Both are defensible interpretations —[]as a positive "no actions available" claim,0as "explicitly paused" — and the reference seller emits[]for terminal statuses consistently (reference/seller-agent/cmd/seller-agent/main.go:624). But neither is codified in the schema descriptions or inskills/adcp-media-buy/SKILL.md. Confirm with the spec authors so downstream buyers don't diverge on the interpretation, then put it on the wire.
Minor nits (non-blocking)
MediaBuyData.MarshalJSONshifts key ordering to alphabetical whenValidActions != nilbut keeps struct-declaration order when it's nil (adcp/types.go:465-480). Harmless for JSON consumers, breaks byte-level golden-file diffs. If you want one path one ordering, marshal directly intomap[string]anyand merge unconditionally.CreativeAssignment.MarshalJSONtreats every known key inisCreativeAssignmentFieldas a collision even when the typed field is the zero value (adcp/inputs.go:39-56). SettingWeight: nil, Extra: {\"weight\": 99}produces wire{}for the weight key, and a user who tuckedplacement_idsinto Extra by mistake will see it silently dropped. Either let Extra win when the typed field is the zero value, or document this on the field.CreateMediaBuySuccessResponseis a no-op alias forMediaBuyResponse(adcp/responses.go:48-51). Either delete or document why both names live in the public API — README/MIGRATING only reference one.adcp/seller.gorepeats theresult, out, err := X(...); return attachContext(result, input.Context), out, errpattern about a dozen times. ArespondWithContext(call, out, err, ctx)helper would shrink the file and make adding context echo to a new tool a one-liner. Refactor candidate.
Approving on the strength of the typed-envelope coverage, the storyboard 59/59, and the schema-lint hygiene — the follow-ups are real but live downstream of one design decision (one MediaBuyData for three response shapes) and the Minors are surgical.
3503e49 to
dc93b91
Compare
There was a problem hiding this comment.
LGTM. Follow-ups noted below. The shape is right: typed *bool for tri-state canceled, typed []CreativeAssignment with Extra round-trip for schema-allowed vendor fields, and three-branch dispatch on CreateMediaBuyResponse mapping to the schema oneOf. feat(adcp)!: carries the breaking marker correctly.
Things I checked
- Conventional-commit breaking marker present. Title
feat(adcp)!: type media-buy seller follow-upsand the single commitdc93b91both carry!. Wire-shape changes (Canceled bool→*bool,CreativeAssignments []any→[]CreativeAssignment,PackageDelivery.Totalsremoval,Config.CreateMediaBuyreturn type) are properly marked. - Schema-vs-types coherence.
adcp/types_gen.goregenerated in this PR — the comment-only trailing-whitespace churn across ~50 types confirmssafe_commentran with the new.rstrip().KNOWN_TYPESinadcp/schemas/generate.py:34-35,48,70lists the four new hand-written types (MediaBuyData,MediaBuyHistoryEntry,SyncCreativeAssignment,CreativeAssignment).FIELD_TYPE_OVERRIDESatgenerate.py:257correctly maps('SyncCreativesRequest', 'assignments')toSyncCreativeAssignment. - No protocol-managed skills touched. Only
skills/build-creative-agent,skills/build-generative-seller-agent,skills/build-retail-media-agent,skills/build-seller-agentmodified — all SDK-local.skills/adcp-*bundle is untouched, which is the right call (those are CODEOWNER-gated and stale buyer examples should fix upstream per #143). CreateMediaBuyResponseunion dispatch.adcp/responses.go:24-48handles pointer + value forms of all three schema oneOf branches (Success / Error / Submitted) and falls through toINVALID_REQUESTwith a clear suggestion.ad-tech-protocol-expertconfirmed 3.0.12 has no fourth branch — pending states surface viatasks/getagainst thetask_id, not as a separatecreate_media_buyshape.CreativeAssignmentround-trip.adcp/inputs.go:43-104.weight: 0preserved (typed*float64); typed fields beatExtracollisions; vendor fields round-trip throughExtra.adcp/inputs_test.gocovers both invariants.MediaBuyData.MarshalJSONempty-array semantics.adcp/types.go:457-472. nil →valid_actionsomitted;[]string{}→ emitted as[]. Matches the documented protocol distinction ([]means no actions; omission means seller did not say). Test atadcp/responses_test.go:61-82exercises the explicit-empty path.weightedSpendnumerics.reference/seller-agent/cmd/seller-agent/main.go:564-572.spend == 0 || packageCount == 0short-circuits;totalBudget == 0falls back to even split; no divide-by-zero. Test atmain_test.go:327-378asserts the 100/300 budget split lands at 10/30 spend.- Forced-arm lock interleaving.
reference/seller-agent/cmd/seller-agent/main.go:352-367. Read-and-consume ofb.forcedis one critical section;createMediaBuyResponseunlocks before callingcreateMediaBuyso no double-lock. Sound. Bool(false)wire shape preserved.MediaBuysResponsenow wraps inMediaBuysData{Sandbox: Bool(sandbox)}.*bool,omitemptyonly omits on nil, soBool(false)still emits"sandbox": false— same wire as the oldmap[string]any{"sandbox": false}path.- Test plan executed in full. PR body lists
go test ./..., both seller and context agents, e2e, bench,lint.py --strict, golangci-lint across three modules, and the storyboard runner at59/59. No unchecked items. - Expert verdicts.
code-reviewer: no blockers, one Medium follow-up (no*CreateMediaBuyErrortest) and one Low (createMediaBuySubmittedResponsemutates caller'sStatus).ad-tech-protocol-expert: sound-with-caveats — flags the lint.py regression below.python-expert: well-formed, confirms the same lint.py regression and one latent regex edge case.
Follow-ups (non-blocking — file as issues if not already)
MediaBuyDatalint coverage regression — file an issue.adcp/schemas/lint.py:79addsMediaBuyDatatoKNOWN_TYPESand the same change drops'MediaBuyData': 'core/media-buy.json'fromEXPLICIT_SCHEMAatlint.py:121. Net effect: the hand-writtenMediaBuyDatastruct inadcp/types.go:434-454is no longer compared against any schema file by lint.MediaBuyHistoryEntryandSyncCreativeAssignmentare in the same boat (they're inKNOWN_TYPESwith noEXPLICIT_SCHEMAmapping;KNOWN_TYPESlint behavior may exempt them entirely). The newCreativeAssignmententry inEXPLICIT_SCHEMAis the right pattern — apply it to the others so the hand-written get_media_buys item shape stays strictly checked. This is exactly the kind of drift the rest of the PR is trying to prevent.- Spec sanity on the hand-written
MediaBuyDatashape.ad-tech-protocol-expertflagged two items worth confirming against 3.0.12core/media-buy.json: (a) iscurrencyactually at the media-buy level or derived from packages, and (b) doeshistory[]live on the item versus the response envelope. If lint coverage is restored per #1 above, this resolves itself the next timepython3 lint.py --strictruns. *CreateMediaBuyErrorbranch is untested.adcp/responses.go:34-37,57-64setsresult.IsError = trueon the error variant. Add a test inadcp/responses_test.gothat passes&CreateMediaBuyError{Errors: []AdcpError{{Code: "X"}}}and assertsresult.IsError == trueplus the structured-content shape.CreativeAssignment.Extrarequires the schema to allow additional properties. If 3.0.12'score/creative-assignment.jsonisadditionalProperties: false, vendor fields will fail strict downstream validation even though they round-trip through this SDK. Worth grep'ing the schema once #1 is done.SyncCreativeAssignment— confirm noactiondiscriminator. Drafts of 3.0.12 carried an optionaladd/removediscriminator on sync assignments. If that landed, the 4-field struct silently drops it. Worth a one-line lookup againsttask/creative/sync-creatives-request.json.
Minor nits (non-blocking)
createMediaBuySubmittedResponsemutates the caller's struct.adcp/responses.go:69-72writesdata.Status = "submitted"when empty. Most response helpers don't mutate inputs. Either document this or shallow-copy. Not a correctness bug.safe_commentrstrip breadth.adcp/schemas/generate.py:277. Bare.rstrip()strips all Unicode whitespace (NBSP, narrow NBSP, ideographic space). Tightening to.rstrip(' \t')matches the actual bug being fixed (description with literal space at column 80). Defensible as-is.json:"-"skip regex edge case.adcp/schemas/lint.py:184-185.if fm.group(3) == '-'would also match the Go-documentedjson:"-,"escape (literal-field name), incorrectly skipping it. No struct currently uses this escape, so it's latent. Tighten toif fm.group(3) == '-' and fm.group(4) == ''if you want to be strict.
Safe to merge.
dc93b91 to
fd63567
Compare
There was a problem hiding this comment.
LGTM. Right shape, breaking marker correct, migration notes load-bearing. Follow-ups below.
The typed-sum on Config.CreateMediaBuy is the move the schema's been asking for — three branches on the wire, three concrete Go types, one marker interface. MediaBuyData rescoping to a get_media_buys row (vs. the previous create/list/task catchall) was overdue. The flattenExt smuggle is finally gone; Ext is now a real extension field, not a top-level-fields conduit.
Things I checked
feat(adcp)!:on the commit covers the wire-API removals: oldMediaBuyDatashape,MediaBuyResponse(*MediaBuyData)signature,Config.GetMediaBuysreturn type,flattenExt. release-please will treat this correctly.CreateMediaBuySuccess.Packagesstays[]Package(adcp/types_gen.go:2240); onlyMediaBuyData.Packagesis[]PackageStatus. The create-vs-list distinction is preserved, as the PR description claims.MediaBuysResponse(nil, true)emitsmedia_buys: [], notnull(adcp/responses_test.go:84). That was the stated fix.Canceled *boolround-trips nil-vs-true correctly throughupdate_media_buy;MIGRATING.md:9-12documents the "usePaused: Bool(false)for resume" contract.CreativeAssignment.MarshalJSONtyped-fields-win on collision (adcp/inputs_test.go:31). VendorExtraround-trips for scalar values.attachContextis uniform across the typed handlers inadcp/seller.go— every error path and every success path echoesinput.Context.cd adcp/schemas && python3 lint.py --strictper PR validation log;golangci-lintclean across all four modules; storyboard59/59.- Protocol-managed skills (
skills/adcp-*,skills/call-adcp-agent) untouched. Only SDK-localskills/build-*modified. CODEOWNERS gate intact.
Follow-ups (non-blocking — file as issues)
- Schema lint coverage strictly weaker on the rescoped types.
adcp/schemas/lint.py:79-81addsMediaBuyData,MediaBuyHistoryEntry,PackageStatus,PackageCreativeApproval,PackageSnapshot,SyncCreativeAssignmenttoEXEMPT, and dropsMediaBuyDatafromEXPLICIT_SCHEMA. The hand-written types are now drift-blind againstget_media_buys_response.json#/properties/media_buys/itemsand the package-row subschema. When 3.0.13 lands, upstream field add/remove on those shapes will passlint.py --strictsilently. Either pointEXPLICIT_SCHEMAat the subschema (needs a JSON-pointer extension inlint.py) or land a curated-subset diff so additions surface in CI.ad-tech-protocol-expertflagged this;code-revieweragreed. PackageStatus.MarshalJSONfield coverage is hand-rolled.adcp/types.go:497-551walks 14 basePackagefields plus 4 status-only ones. The generatedPackagehas 22+ properties —measurement_terms,performance_standards,format_ids_to_provide,optimization_goals,agency_estimate_numberare silently dropped onget_media_buysrows. Intentional per the curated-row narrative, but tied to the lint gap above: if any of those are in the upstream package-row schema, buyers parse a row that's missing fields the seller declared at create time. Worth acd adcp/schemas && ./download.sh 3.0.12 && diffaudit before the next protocol bump.listCreativesformat-filter key change.reference/seller-agent/cmd/seller-agent/main.go:259now keys on the fullFormatRefstruct. A buyer filtering{agent_url, id}against a creative synced with{agent_url, id, width:300}misses. Reference impl only, but a buyer porting their fixture across PRs will see a behavior change. The PR description frames this as "match the full format ref, not justid," which is the right intent; a sentence on dimension semantics inMIGRATING.mdwould close the loop.#143and#144already filed by the author. Stale buyer-facing examples in the managedskills/adcp-media-buybundle (upstream fix), andConfig.UpdateMediaBuy/Config.ListCreativesSDK helpers (additive). Not in scope here — noting the trail.
Minor nits (non-blocking)
- Redundant defaulting on submitted status.
adcp/responses.go:73-76defaultsdata.Status = "submitted"then errors if it isn't"submitted". After defaulting, the only path that reaches the error branch is a non-empty mismatched status (e.g., caller passing"pending"). Fine, just notable. - TOCTOU window in forced-arm consumption.
reference/seller-agent/cmd/seller-agent/main.go:354-369drops the lock between readingb.forcedand callingcreateMediaBuy(which re-locks). A concurrentforce_create_media_buy_armlands for the next call, not this one. Test fixture; one-line comment would spare a future reader the trace. CreativeAssignment.Extraround-trip is scalar-only in tests.adcp/inputs_test.godoesn't exercise nestedmap[string]anyor[]anyvalues. The any-typed round-trip is standardencoding/jsonand works, but a test would lock the contract in.
Approving on the strength of the typed-sum shape plus the conventional-commit breaking marker. Safe to merge.
Summary
Addresses the concrete Argus follow-ups from the Go reference seller harness merge, plus protocol/code/DX expert passes on the SDK surface:
CreateMediaBuyResponse = anycallback surface with a realCreateMediaBuyResultinterface implemented by the three generated schema variantsMediaBuyDatato theget_media_buys.media_buys[]item shape instead of using it as a create/task catchallConfig.GetMediaBuysthrough the generatedGetMediaBuysResponseenvelope so pagination/errors/context are reachable from normaladcp.RegisterhandlersGetMediaBuysResponse.MediaBuysas[]MediaBuyDataand addsPackageStatusfor get-media-buys package rows, including creative approvals, pending formats, and delivery snapshotsget_media_buysemitsmedia_buys: []instead ofnullfor empty/nil listsweight: 0is preserved and vendor-specific assignment fields round-trip viaCreativeAssignment.ExtraSyncCreativesRequest.Assignmentsas[]SyncCreativeAssignmentUpdateMediaBuyRequest.canceledand package updates to pointer booleans; docs now call out thatcanceledis nil-or-true while resume usespaused:falsetotals, with a comment for schema-required package spendlist_creativesformat filtering match the full format ref, not justidNotes
The protocol-managed
skills/adcp-media-buybundle still has stale buyer-facing examples for creative assignments/update wrappers/async status. I did not hand-edit that managed skill; it should be fixed upstream or refreshed through the bundle sync. Tracked in #143.First-class
Config.UpdateMediaBuyandConfig.ListCreativeshooks are still an additive SDK follow-up. Tracked in #144.The compliance scenario enum concern appears resolved in the current 3.0.12 schemas in this repo:
seed_product,seed_pricing_option, andforce_create_media_buy_armare already present in the request/response scenario enums. I left that as an issue-tracking/spec-sync item rather than changing runtime behavior here.Validation
go test ./...cd adcp && go test ./...cd cmd/router && go test ./...cd targeting/prommetrics && go test ./...cd reference/seller-agent && go test ./...cd reference/context-agent && go test ./...cd e2e && go test ./...cd bench && go test ./...cd adcp/schemas && python3 lint.py --strictgit diff --checkgolangci-lint run ./...cd adcp && golangci-lint run ./...cd reference/seller-agent && golangci-lint run ./...cd reference/context-agent && golangci-lint run ./...ADCP_RUNNER_BIN=adcp ADCP_PORT=3015 STORYBOARD_RESULT_PATH=.context/storyboard-final-dx.json SELLER_LOG_PATH=.context/storyboard-final-dx.log ./scripts/ci/run_storyboard_reference_seller.sh(59/59)