Skip to content
Merged
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
11 changes: 11 additions & 0 deletions .changeset/signal-coverage-legacy-scalar.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"adcontextprotocol": minor
---

spec(signals): make deprecated `coverage_percentage` optional on signal responses.

`get_signals.signals[]` and wholesale feed signal payloads now keep `coverage_percentage` as an optional deprecated legacy scalar instead of a required deprecated field. `coverage_forecast` is the source of truth for detailed signal coverage planning; the scalar remains a backward-compatible fallback for clients that still consume it.

Adds validation coverage for `coverage_rate.low` and `coverage_rate.high` upper bounds, and pins the intended valid signal forecast shape where `presence: "present"` omits `signal_value`.

Closes #5089.
2 changes: 1 addition & 1 deletion docs/signals/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ The signal agent searches catalogs from every authorized data provider — autom
| Segment taxonomy | Signal catalog (`adagents.json`) |
| Data provider | Signal source (`data_provider_domain`) |
| Activate on my DSP | `activate_signal` with destination |
| Audience or inventory availability | Required `coverage_percentage`, plus `coverage_forecast` when the seller can disclose scope and value-level availability |
| Audience or inventory availability | `coverage_forecast` for authoritative signal-level coverage; optional deprecated `coverage_percentage` only as a legacy scalar fallback |
| Data cost | `pricing_options` in signal response |

</Accordion>
Expand Down
49 changes: 39 additions & 10 deletions docs/signals/tasks/get_signals.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ The response structure is identical across protocols, with only the transport wr
"description": "string",
"signal_type": "string",
"data_provider": "string",
"coverage_percentage": "number",
"coverage_percentage": "number (optional, deprecated)",
"coverage_forecast": {
"method": "estimate",
"forecast_range_unit": "availability",
Expand Down Expand Up @@ -178,8 +178,8 @@ The response structure is identical across protocols, with only the transport wr
- `owned` — first-party segment derived from data the signal source directly owns
- `custom` — source-native segment built on demand from models, composites, or buyer inputs (not attributable to a standing upstream provider)
- **data_provider**: Human-readable source name when applicable. For `scope: "data_provider"` signals this is the data provider; for `scope: "signal_source"` signals it may identify the signal source or proprietary origin.
- **coverage_percentage**: Legacy scalar percentage of audience coverage. When **coverage_forecast** is also present, this is the seller's total present-coverage estimate for the same signal. If `coverage_forecast` includes an absent bucket over the same denominator, `coverage_percentage` should align with `100 * (1 - absent coverage_rate.mid)`.
- **coverage_forecast**: Optional forecast-shaped availability guidance for the signal. `scope` declares the denominator, `bucket_semantics` declares whether returned value buckets are `exclusive` or `overlapping`, and `bucket_completeness` declares whether the returned buckets are a full denominator partition or a partial histogram. Each point can use a `kind: "signal"` dimension with canonical `signal_ref`, `presence: "present"` for values, or `presence: "absent"` plus `signal_value: null` for the not-present bucket. `metrics.coverage_rate` is a 0.0-1.0 fraction of the declared scope.
- **coverage_percentage**: Optional deprecated legacy scalar percentage of audience coverage. Use only as a fallback for clients that do not consume `coverage_forecast`. When **coverage_forecast** is present, `coverage_forecast` is authoritative for signal-level discovery and this scalar is fallback-only. If `coverage_forecast` includes an absent bucket over the same denominator, `coverage_percentage` should align with `100 * (1 - absent coverage_rate.mid)`.
- **coverage_forecast**: Optional forecast-shaped availability guidance for the signal. `scope` declares the denominator, `bucket_semantics` declares whether returned value buckets are `exclusive` or `overlapping`, and `bucket_completeness` declares whether the returned buckets are a full denominator partition or a partial histogram. Each point can use a `kind: "signal"` dimension with canonical `signal_ref`, `presence: "present"` for any present value, `presence: "present"` plus `signal_value` for a specific value bucket, or `presence: "absent"` plus `signal_value: null` for the not-present bucket. `metrics.coverage_rate` is a 0.0-1.0 fraction of the declared scope.
- **deployments**: Array of destination deployments
- **agent_url**: URL identifying the destination agent
- **account**: Account identifier if applicable
Expand Down Expand Up @@ -276,15 +276,43 @@ Because the authenticated caller matches the deployment target, the response inc
{
"signal_ref": {
"scope": "data_provider",
"data_provider_domain": "experian.com",
"data_provider_domain": "pinnacle-data.example",
"signal_id": "luxury_auto_intenders"
},
"signal_agent_segment_id": "luxury_auto_intenders",
"name": "Luxury Automotive Intenders",
"description": "High-income individuals researching luxury vehicles",
"signal_type": "marketplace",
"data_provider": "Experian",
"coverage_percentage": 12,
"data_provider": "Pinnacle Data",
"coverage_forecast": {
"method": "estimate",
"forecast_range_unit": "availability",
"scope": {
"kind": "inventory",
"label": "eligible destination inventory"
},
"bucket_semantics": "exclusive",
"bucket_completeness": "partial",
"points": [
{
"label": "present",
"dimensions": [
{
"kind": "signal",
"signal_ref": {
"scope": "data_provider",
"data_provider_domain": "pinnacle-data.example",
"signal_id": "luxury_auto_intenders"
},
"presence": "present"
}
],
"metrics": {
"coverage_rate": { "mid": 0.12 }
}
}
]
},
"deployments": [
{
"type": "agent",
Expand Down Expand Up @@ -680,10 +708,10 @@ Discover all available deployments across platforms:
- **description** (string): Detailed signal description
- **signal_type** (string): `marketplace` (resold third-party), `owned` (signal source first-party data), or `custom` (source-native segment built on demand)
- **data_provider** (string, optional): Human-readable source/provider name when applicable
- **coverage_percentage** (number): Legacy scalar estimated reach percentage
- **coverage_forecast** (object, optional): Forecast-shaped coverage breakdown with an explicit denominator and per-value availability points. Use `bucket_semantics: "exclusive"` when returned buckets do not overlap, or `"overlapping"` when multi-value signals can make returned rates sum above 1.0. Use `bucket_completeness: "complete"` only when the returned buckets cover the declared denominator; otherwise use `"partial"` and buyers must treat omitted share as undisclosed, other, or unsupported buckets.
- **coverage_percentage** (number, optional, deprecated): Legacy scalar estimated reach percentage. Use only as a fallback when `coverage_forecast` is absent or unsupported by the client.
- **coverage_forecast** (object, optional): Forecast-shaped coverage breakdown with an explicit denominator and availability points. Use `presence: "present"` with omitted `signal_value` for an aggregate "any present value" bucket; add `signal_value` only when the row is for a specific value. Use `bucket_semantics: "exclusive"` when returned buckets do not overlap, or `"overlapping"` when multi-value signals can make returned rates sum above 1.0. Use `bucket_completeness: "complete"` only when the returned buckets cover the declared denominator; otherwise use `"partial"` and buyers must treat omitted share as undisclosed, other, or unsupported buckets.

Use `coverage_forecast` for signal-first discovery: "how much inventory has this signal or its values?" Use product or proposal forecasts with `kind: "signal"` dimensions for product-specific planning: "how does this signal restrict this product's baseline availability?" Bucket-specific filtering is client-side for now; request filters such as `min_coverage_percentage` still use the legacy scalar.
Use `coverage_forecast` as the authoritative field for signal-first discovery: "how much inventory has this signal or its values?" Use product or proposal forecasts with `kind: "signal"` dimensions as the authoritative surface for product-specific planning: "how does this signal restrict this product's baseline availability?" Bucket-specific filtering is client-side for now; request filters such as `min_coverage_percentage` still use the legacy scalar.
- **deployments** (array): Platform-specific deployment information
- **platform** (string): Target platform name
- **account** (string, nullable): Specific account if account-specific
Expand Down Expand Up @@ -1131,7 +1159,8 @@ def generate_signals_message(signals, request):
price_commentary = ""
else:
price_commentary = ""
return f"I found a perfect match: '{signal.name}' from {signal.data_provider} with {signal.coverage_percentage}% coverage. {deployment_status} {price_commentary}"
coverage_commentary = f" with {signal.coverage_percentage}% coverage" if getattr(signal, "coverage_percentage", None) is not None else ""
return f"I found a perfect match: '{signal.name}' from {signal.data_provider}{coverage_commentary}. {deployment_status} {price_commentary}"
else:
return f"I found {len(signals)} signals matching your {extract_key_criteria(request.signal_spec)} criteria. {describe_best_option(best_signal)} {get_pricing_range(signals)}."

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ phases:
- signal_agent_segment_id for activation
- signal_id with source, data_provider_domain, and id
- name, description, and value_type (binary, categorical, or numeric)
- coverage_percentage (audience reach estimate)
- coverage_forecast when detailed availability is disclosed; deprecated coverage_percentage may be present as a legacy scalar fallback
- pricing_options with at least one pricing model
- signal_type: "marketplace"

Expand Down Expand Up @@ -155,10 +155,6 @@ phases:
- check: field_present
path: "signals[0].pricing_options"
description: "Each signal has pricing options"
- check: field_present
path: "signals[0].coverage_percentage"
description: "Each signal has a coverage estimate"

- check: field_present
path: "context"
description: "Response echoes back the context object"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ phases:
- signal_agent_segment_id for activation
- signal_id with source: "agent_native"
- name, description, and value_type
- coverage_percentage
- coverage_forecast when detailed availability is disclosed; deprecated coverage_percentage may be present as a legacy scalar fallback
- pricing_options
- signal_type: "owned"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,9 @@ required_tools:

narrative: |
Every AdCP agent must return responses that match the published JSON schemas.
For signals agents, this means get_signals responses must carry the required
fields on every signal: signal_id, signal_agent_segment_id, name, description,
signal_type, data_provider, coverage_percentage, deployments, and
pricing_options.
For signals agents, this means get_signals responses must carry the schema-required
fields on every signal: signal identity, signal_agent_segment_id, name, description,
signal_type, and deployments.

This storyboard validates schema compliance on get_signals responses —
confirming that each signal contains the fields buyers need to proceed to
Expand Down Expand Up @@ -95,16 +94,16 @@ phases:
comply_scenario: schema_compliance
stateful: false
expected: |
Return signals matching the spec. Each signal must have all required fields:
Return signals matching the spec. Each signal must have all schema-required fields:
- signal_id: reference to the data provider's catalog
- signal_agent_segment_id: opaque identifier used for activation
- name: human-readable signal name
- description: detailed signal description
- signal_type: marketplace, custom, or owned
- data_provider: human-readable name of the data provider
- coverage_percentage: audience coverage percentage (0–100)
- deployments: array of deployment targets
- pricing_options: array (minItems: 1) with pricing_option_id

Optional fields such as data_provider, pricing_options, coverage_forecast,
and deprecated coverage_percentage must still match the schema when present.

Responses missing any of these fields fail schema validation.

Expand Down
5 changes: 2 additions & 3 deletions static/schemas/source/core/wholesale-feed-event.json
Original file line number Diff line number Diff line change
Expand Up @@ -582,14 +582,14 @@
},
"coverage_percentage": {
"type": "number",
"description": "DEPRECATED for detailed planning. Scalar percentage of audience coverage retained for backwards compatibility. When coverage_forecast is also present, this is the seller's total present-coverage estimate for the same signal.",
"description": "DEPRECATED for detailed planning. Optional legacy scalar percentage of audience coverage retained only as a fallback for clients that do not consume coverage_forecast. When coverage_forecast is present, coverage_forecast is authoritative for signal-level discovery and coverage_percentage is fallback-only.",
"minimum": 0,
"maximum": 100,
"deprecated": true
},
"coverage_forecast": {
"$ref": "/schemas/core/signal-coverage-forecast.json",
"description": "Optional forecast-shaped signal availability guidance using the same wire shape as get_signals.signals[].coverage_forecast."
"description": "Optional forecast-shaped signal availability guidance using the same wire shape as get_signals.signals[].coverage_forecast. When present, this is authoritative for signal-level discovery coverage."
},
"deployments": {
"type": "array",
Expand All @@ -611,7 +611,6 @@
"name",
"description",
"signal_type",
"coverage_percentage",
"deployments"
],
"anyOf": [
Expand Down
5 changes: 2 additions & 3 deletions static/schemas/source/signals/get-signals-response.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,14 +86,14 @@
},
"coverage_percentage": {
"type": "number",
"description": "DEPRECATED for detailed planning. Scalar percentage of audience coverage retained for backwards compatibility. When coverage_forecast is also present, this is the seller's total present-coverage estimate for the same signal. If coverage_forecast includes an absent bucket over the same denominator, coverage_percentage SHOULD align with 100 * (1 - absent coverage_rate.mid).",
"description": "DEPRECATED for detailed planning. Optional legacy scalar percentage of audience coverage retained only as a fallback for clients that do not consume coverage_forecast. When coverage_forecast is present, coverage_forecast is authoritative for signal-level discovery and coverage_percentage is fallback-only. If coverage_forecast includes an absent bucket over the same denominator, coverage_percentage SHOULD align with 100 * (1 - absent coverage_rate.mid).",
"minimum": 0,
"maximum": 100,
"deprecated": true
},
"coverage_forecast": {
"$ref": "/schemas/core/signal-coverage-forecast.json",
"description": "Optional forecast-shaped signal availability guidance. Use this to disclose the denominator, bucket semantics, not-present bucket, and per-value coverage distribution for the signal."
"description": "Optional forecast-shaped signal availability guidance. When present, this is authoritative for signal-level discovery coverage. Use this to disclose the denominator, bucket semantics, not-present bucket, aggregate present bucket, and per-value coverage distribution for the signal."
},
"deployments": {
"type": "array",
Expand All @@ -116,7 +116,6 @@
"name",
"description",
"signal_type",
"coverage_percentage",
"deployments"
],
"anyOf": [
Expand Down
6 changes: 4 additions & 2 deletions tests/composed-schema-validation.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -729,6 +729,8 @@ async function runTests() {
{ type: 'platform', platform: 'example_dsp', is_live: true }
]
};
const signalListingCoreWithoutLegacyCoverage = { ...signalListingCore };
delete signalListingCoreWithoutLegacyCoverage.coverage_percentage;
const signalCoverageForecast = {
method: 'estimate',
forecast_range_unit: 'availability',
Expand Down Expand Up @@ -936,12 +938,12 @@ async function runTests() {
applies_to: { scope: 'public' },
signal: {
signal_id: legacySignalId,
...signalListingCore,
...signalListingCoreWithoutLegacyCoverage,
coverage_forecast: signalCoverageForecast
}
}
},
'Wholesale signal event accepts deprecated signal_id, relaxed data_provider/pricing_options, and coverage_forecast'
'Wholesale signal event accepts deprecated signal_id, optional legacy coverage_percentage, relaxed data_provider/pricing_options, and coverage_forecast'
);

log('Registry change feed schemas:', 'info');
Expand Down
Loading
Loading