Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/withdraw-forced-proposal-id-hold-unavailable.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"adcontextprotocol": minor
---

Withdraw `forced.proposal_id` and the `unable` value of `forced.arm` from the `comply_test_controller` response in 3.3 (adcp#8501, entry 1). The experimental `force_hold_unavailable` scenario (`media_buy.hold_policy`) now returns the proposal ID in `simulated.proposal_id` on the `SimulationSuccess` branch, which holds the values a call injected or applied. The `forced` object is closed, and a minor release does not add to a closed schema (DR-0036), so `ForcedDirectiveSuccess` goes back to its 3.2 shape: arms `submitted`, `completed`, `input-required` and `rejected`, and no `proposal_id`.

**Both values shipped in `3.3.0-beta.0` through `3.3.0-beta.4` and are removed after beta.4.** This is a beta-to-beta change, not a change to a stable release. Those betas are immutable, so a seller or SDK built from one of them that answers `force_hold_unavailable` with `forced: { arm: "unable", proposal_id }` should answer `simulated: { proposal_id }` instead. Nothing else about the scenario changes: its request, its errors, and what the next finalize returns stay the same.

The `media_buy_seller/hold_unavailable_batch` storyboard reads the proposal ID from `simulated.proposal_id`, and the training agent returns it there.
11 changes: 10 additions & 1 deletion docs/building/by-layer/L3/comply-test-controller.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -944,7 +944,16 @@ Experimental (`media_buy.hold_policy`). Makes the next [`refine_proposals`](/doc
}
```

A successful registration uses the same `ForcedDirectiveSuccess` response shape as the `force_*_arm` scenarios: `success: true`, `forced.arm: "unable"` (the per-entry finalize outcome), and `forced.proposal_id` echoing the draft. Nothing else changes yet, and the draft stays finalizable.
A successful registration uses the `SimulationSuccess` response shape: `success: true` and `simulated.proposal_id` echoing the draft. It carries no `forced` object, because the failure is a per-entry finalize outcome rather than a response arm. Nothing else changes yet, and the draft stays finalizable.

```json
{
"success": true,
"simulated": {
"proposal_id": "prop_draft_ctv"
}
}
```

The directive applies to the next finalize from the same sandbox account (account + principal pair) whose batch includes that proposal and that reaches hold placement. For that batch the seller MUST:

Expand Down
2 changes: 2 additions & 0 deletions docs/reference/3-3-beta.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,8 @@ Three features shipped in `3.3.0-beta.1` and are not part of 3.3. They leave the

The [migration guide](/docs/reference/migration/3-2-to-3-3#withdrawn-from-3-3-planned-for-4-0) lists every surface removed, and [Planned for 4.0](/docs/reference/migration/v4-compatibility#planned-for-4-0-withdrawn-from-3-3) keeps the designs.

One conformance-runner shape also changes after beta.4, for the same closed-schema reason. The experimental [`force_hold_unavailable`](/docs/building/by-layer/L3/comply-test-controller#force_hold_unavailable) controller scenario answered `forced: { arm: "unable", proposal_id }` in `3.3.0-beta.0` through `3.3.0-beta.4`, and now answers `simulated: { proposal_id }`.

## Required integration evidence

Retain the exact protocol bundle, SDK package versions, capability responses, redacted requests and responses, task IDs, and account and MediaBuy identifiers for each run.
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/experimental-status.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ Schemas marked with `x-status: experimental` are the authoritative source. The c
| `media_buy.measurement_provenance` | The `billing_measurement.counting_mechanisms[]` declaration on `core/measurement-terms.json`, the optional `vendor_metric_provenance[]` array (`core/vendor-metric-provenance.json`: `vendor`, `metric_id`, `qualifier?`, `mechanisms[]`, `deduplicated?`) beside `vendor_metric_values` on `core/delivery-metrics.json`, and the `enums/collection-mechanism.json` enum (#6878, #2041; see [Declaring how the billing count is built](/docs/media-buy/advanced-topics/billing-authority#declaring-how-the-billing-count-is-built-experimental) and [Collection-mechanism provenance on vendor values](/docs/media-buy/media-buys/optimization-reporting#collection-mechanism-provenance-on-vendor-values-experimental)) | New cross-party disclosure of what a count is built from, with no production adopter yet. The enum is closed and released values cannot be removed on a stable surface, so the vocabulary (for example whether `client_tracker` and `sdk` stay distinct, or whether a sensor value is needed for place-based media) is held experimental until two parties have used it. The `deduplicated` boolean may become a level enum, and the `impression_id` joinability and `seller_tracker_events[]` follow-ups may reshape the declaration. `measurement_source` on delivery metrics (the provider slug) is stable and unrelated. The closed `canonical-measurement-terms.json` and `vendor-metric-value.json` row do not carry these fields in 3.x (DR-0036): provenance is a sibling array joined on the row key, and `counting_mechanisms` reaches the canonical compact-lifecycle terms in 4.0 ([4.0 compatibility check](/docs/reference/migration/v4-compatibility#canonical_counting_mechanisms_required_in_4_0)). |
| `media_buy.lineage` | Forward-only buy-level lineage: the `predecessor_media_buy_id` and `lineage_reason` fields on [`create_media_buy`](/docs/media-buy/task-reference/create_media_buy), [`buy_products`](/docs/media-buy/task-reference/buy_products), and [`accept_proposal`](/docs/media-buy/task-reference/accept_proposal) requests and on [`get_media_buys`](/docs/media-buy/task-reference/get_media_buys) items (also `core/media-buy.json`), the `predecessor_media_buy_id` filter on `get_media_buys`, and the `enums/media-buy-lineage-reason.json` enum (#5849; see [Media Buy Lineage](/docs/media-buy/specification#media-buy-lineage)) | One production sales agent has run equivalent lineage operations for months, and IAB AAMP seller-agent 2.0 (`parent_deal_id`) is prior art for the same idea in deal-scoped form, not convergence evidence. The `lineage_reason` vocabulary is closed and may grow, and the `UNSUPPORTED_FEATURE` rejection and the same-account scope may change as more sellers implement it. |
| `media_buy.delegated_composition` | `criteria.product_ids_exclusive` on [`request_proposals`](/docs/media-buy/task-reference/request_proposals#binding-the-product-universe-with-product_ids_exclusive) and proposal refinement, the `media_buy.supports_exclusive_product_ids` capability, and `media_buy.proposal_refinement.hold_semantics` (`terms_only`) with its `committed`, `expires_at`, and `accept_proposal` semantics, described in the [delegated composition profile](/docs/media-buy/product-discovery/delegated-composition) | Zero validated adopters so far, and `terms_only` reinterprets what `committed` means for a capability-gated seller. The acceptance-only `accept_proposal` variant, the governance trigger when the MediaBuy is created on the requester, and a reference-based product universe are still open and may reshape these fields. A `terms_only` seller must not return `committed` proposals to callers below 3.3. |
| `media_buy.hold_policy` | The `hold_policy` object (schema: `core/hold-policy.json`: `available`, `min_duration`, `max_duration`, `extendable`, `max_total_duration`) as a planning hint on products (`core/product.json`, `core/canonical-product.json`) and as the binding policy on canonical proposals (`core/canonical-proposal.json`), the committed proposal's `hold_placed_at`, the per-entry resolution of non-holdable drafts to terms-only finalize, and its role overriding the agent-level `hold_semantics` default, described in [Hold policy](/docs/media-buy/task-reference/refine_proposals#hold-policy-experimental) ([#7661](https://github.com/adcontextprotocol/adcp/issues/7661)) | One named adopter so far: Availnet, a creator marketplace, where each creator's upcoming-video slot can be held while the creator confirms. `rankable` belongs to `media_buy.hold_ranking`, per-installment readback to `media_buy.installment_holds`, and `extendable` and `max_total_duration` bound `extend_hold` (`media_buy.hold_extension`). Reuses the `terms_only` semantics of `media_buy.delegated_composition` without requiring that feature id. Both fields are omitted for callers below 3.3. The `comply_test_controller` scenario `force_hold_unavailable` ([compliance test controller](/docs/building/by-layer/L3/comply-test-controller#force_hold_unavailable)) and its `forced.arm: "unable"` response echo ride on this feature id and carry no feature id of their own. |
| `media_buy.hold_policy` | The `hold_policy` object (schema: `core/hold-policy.json`: `available`, `min_duration`, `max_duration`, `extendable`, `max_total_duration`) as a planning hint on products (`core/product.json`, `core/canonical-product.json`) and as the binding policy on canonical proposals (`core/canonical-proposal.json`), the committed proposal's `hold_placed_at`, the per-entry resolution of non-holdable drafts to terms-only finalize, and its role overriding the agent-level `hold_semantics` default, described in [Hold policy](/docs/media-buy/task-reference/refine_proposals#hold-policy-experimental) ([#7661](https://github.com/adcontextprotocol/adcp/issues/7661)) | One named adopter so far: Availnet, a creator marketplace, where each creator's upcoming-video slot can be held while the creator confirms. `rankable` belongs to `media_buy.hold_ranking`, per-installment readback to `media_buy.installment_holds`, and `extendable` and `max_total_duration` bound `extend_hold` (`media_buy.hold_extension`). Reuses the `terms_only` semantics of `media_buy.delegated_composition` without requiring that feature id. Both fields are omitted for callers below 3.3. The `comply_test_controller` scenario `force_hold_unavailable` ([compliance test controller](/docs/building/by-layer/L3/comply-test-controller#force_hold_unavailable)) and its `simulated.proposal_id` response echo ride on this feature id and carry no feature id of their own. |
| `media_buy.hold_ranking` | `hold_policy.rankable` (`core/hold-policy.json`) and the committed proposal's `hold_rank` (`core/canonical-proposal.json`): advisory readback of the buyer's own position in a seller's queue of holds, described in [Hold ranking](/docs/media-buy/task-reference/refine_proposals#hold-ranking-experimental) ([#8086](https://github.com/adcontextprotocol/adcp/issues/8086)) | One named adopter so far: Availnet, a creator marketplace. Ranking is advisory only: no right-of-first-refusal deadline, no rank-change webhook, and no read of a committed proposal, so `hold_rank` is a snapshot that a buyer refreshes through an `extend_hold` successor. Whether buyers need a fresher readback, and whether sellers will report exact queue depth, needs field experience. `available: true` without `rankable: true` keeps its exclusive, first-position meaning. Requires `media_buy.hold_policy`; omitted for callers below 3.3, for whom the seller treats `rankable` as `false`. |
| `media_buy.collection_catalog` | [`list_collections`](/docs/media-buy/task-reference/list_collections) (schemas: `media-buy/list-collections-request.json`, `media-buy/list-collections-response.json`), its `collections_version` / `if_collections_version` tokens, and the `collection_card.updated`, `collection_card.removed` and `collection_catalog.bulk_change` webhook event types (branches of `core/wholesale-feed-event.json`, delivered in `core/wholesale-feed-webhook.json` and listed in `wholesale_feed_webhooks.event_types`) ([#8183](https://github.com/adcontextprotocol/adcp/issues/8183)) | Standalone catalog read and push channel for collection cards, with one adopter behind the card surface so far (Availnet, a creator marketplace) and no emitting agent yet. Requires `media_buy.collection_cards`, because the response carries card fields; a seller can serve cards in-band without it. Webhooks are optional within the feature and declared per type. The filter set, the reuse of the wholesale feed envelope with `collections_version` in `wholesale_feed_version`, and the bulk threshold for follower sweeps may change. Not served to callers below 3.3. Storyboards and a `seed_collection` controller scenario wait for training-agent support ([#8181](https://github.com/adcontextprotocol/adcp/issues/8181)). |
| `media_buy.installment_holds` | The committed proposal's `holds` array (`core/canonical-proposal.json`): `product_id`, `installment_ref`, and, under a rankable policy, `hold_rank` for each installment the hold covers, described in [Installment holds](/docs/media-buy/task-reference/refine_proposals#installment-holds-experimental) ([#8086](https://github.com/adcontextprotocol/adcp/issues/8086)) | One named adopter so far: Availnet, a creator marketplace, where the held unit is a creator's specific upcoming video. Readback only: entries describe one all-or-none hold and share the proposal's `expires_at`, so there are no partial holds. Whether entries need their own `expires_at`, which only makes sense if partial holds are ever allowed, needs field experience. Requires `media_buy.hold_policy`; omitted for callers below 3.3. |
Expand Down
1 change: 1 addition & 0 deletions docs/reference/migration/3-2-to-3-3.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,7 @@ What this means for 3.3 adopters:
- **Features that needed to add terms to `commercial_terms` are withdrawn** ([above](#withdrawn-from-3-3-planned-for-4-0)), and seller-held change terms are planned for 4.0 instead of shipping as `commercial_terms.change_terms[]`.
- **New 3.3 additions sit on open schemas or in sibling fields.** `target_audiences[]` is a sibling of the closed `audience_evidence_requirements`, `vendor_metric_provenance[]` is a sibling of the closed `vendor_metric_values[]` row, and `counting_mechanisms` reaches the closed canonical measurement terms in 4.0.
- **Send a 3.3 field on a reopened schema only to a 3.3 peer.** A strict validator on an earlier 3.2 patch still rejects it ([Send only to 3.3 peers](#send-only-to-3-3-peers)).
- **[`force_hold_unavailable`](/docs/building/by-layer/L3/comply-test-controller#force_hold_unavailable) answers in `simulated`.** In `3.3.0-beta.0` through `3.3.0-beta.4`, this experimental controller scenario answered `forced: { arm: "unable", proposal_id }`. It now answers `simulated: { proposal_id }`, and `forced` keeps its 3.2 arms.

### Capabilities are bounded by structure

Expand Down
7 changes: 0 additions & 7 deletions scripts/lint-additive-minor-baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,6 @@
"$comment": "Non-additive schema changes recorded by scripts/lint-additive-minor.cjs, relative to the release named in base. This list only shrinks: CI rejects an entry that the target branch does not have, unless its change already shipped in a stable release. Remove an entry when its change is reverted or released; record a WG decision in reason. An entry whose detail ends in \"(closed in <release>)\" records an addition to a schema that an earlier release of the major published closed (DR-0036). It shipped before the base and stays until the next major.",
"base": "3.2.8",
"entries": [
{
"file": "compliance/comply-test-controller-response.json",
"pointer": "/oneOf/3/properties/forced",
"kind": "property-added-to-closed",
"detail": "property proposal_id added",
"reason": "pending WG decision"
},
{
"file": "core/agent-notification-config.json",
"pointer": "",
Expand Down
5 changes: 4 additions & 1 deletion server/src/training-agent/holds/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -939,9 +939,12 @@ async function registerForcedHoldUnavailable(params: Json, ctx: TrainingContext)
saveHoldDocument(session, doc, await sandboxNow(ctx));
await flushDirtySessions();
}
// The registration answers on the SimulationSuccess branch: `simulated`
// carries the value this call applied. ForcedDirectiveSuccess (`forced.arm`)
// has no arm for a per-entry finalize outcome (adcp#8501).
return {
success: true,
forced: { arm: 'unable', proposal_id: proposalId },
simulated: { proposal_id: proposalId },
message: `The next finalize that includes ${proposalId} returns hold_unavailable`,
};
}
Expand Down
3 changes: 2 additions & 1 deletion server/tests/unit/training-agent-hold-vectors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,8 @@ describe('experimental 3.3 holds graded by the reference hold-policy grader', ()
scenario: 'force_hold_unavailable',
params: { proposal_id: forced.draft.proposal_id },
});
expect(directive.structured, JSON.stringify(directive.structured)).toMatchObject({ success: true, forced: { arm: 'unable', proposal_id: forced.draft.proposal_id } });
expect(directive.structured, JSON.stringify(directive.structured)).toMatchObject({ success: true, simulated: { proposal_id: forced.draft.proposal_id } });
expect(directive.structured).not.toHaveProperty('forced');
const drafts = [forced.draft, sibling.draft, termsOnly.draft];
const failed = await finalize(drafts, { label: 'forced batch' });
expect(failed.structured.results.map((entry: Json) => entry.reason_code)).toEqual(['hold_unavailable', 'batch_aborted', 'batch_aborted']);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -240,12 +240,8 @@ phases:
path: "success"
value: true
description: "The directive is registered"
- check: field_value
path: "forced.arm"
value: "unable"
description: "The directive registers the unable arm"
- check: field_equals_context
path: "forced.proposal_id"
path: "simulated.proposal_id"
context_key: "second_draft_id"
description: "The directive names the second draft"

Expand Down
Loading
Loading