Module adcp.canonical_formats.advisory

SDK-source errors[] advisory construction.

The canonical-formats projection emits non-fatal advisories on the errors[] array of get_products / list_creative_formats responses. Advisories carry source="sdk" (vs. seller-emitted producer entries) and sdk_id="adcontextprotocol-adcp-python@<version>" so multi-hop consumers can attribute the entry to this SDK and deduplicate on (code, field) per the multi-hop propagation contract in core/error.json.

The advisory functions live separately from :mod:adcp.canonical_formats.v2_to_v1 so other SDK paths (e.g., the v1→v2 reverse projection in a future PR, the closed-set validator's own dispatch path) can emit the same shape without circular imports.

Functions

def make_sdk_advisory(*,
code: str,
message: str,
field: str | None = None,
details: dict[str, Any] | None = None,
recovery: Recovery = correctable,
suggestion: str | None = None) ‑> adcp.types.generated_poc.core.error.Error
Expand source code
def make_sdk_advisory(
    *,
    code: str,
    message: str,
    field: str | None = None,
    details: dict[str, Any] | None = None,
    recovery: Recovery = Recovery.correctable,
    suggestion: str | None = None,
) -> Error:
    """Build an SDK-source advisory entry for ``errors[]`` augmentation.

    Sets ``source=sdk`` and ``sdk_id=<package>@<version>`` per the
    multi-hop propagation contract in ``core/error.json``. Consumers
    receiving this entry MUST treat it as advisory — the response stays
    success on the v1 path; only the v2 projection is degraded.

    Args:
        code: AdCP error code (e.g., ``FORMAT_DECLARATION_V1_AMBIGUOUS``).
            Must be ≤64 chars per the wire schema.
        message: Human-readable description.
        field: JSONPath-lite pointer to the offending field
            (e.g., ``products[0].format_options[2]``).
        details: Code-specific structured payload.
        recovery: Recovery classification — defaults to ``correctable``
            because canonical-projection advisories tell the seller what
            to fix (add ``v1_format_ref``, file a registry PR, etc.).
        suggestion: Optional one-line fix hint surfaced to operators.
    """
    return Error(
        code=code,
        message=message,
        field=field,
        details=details,
        recovery=recovery,
        source=Source.sdk,
        sdk_id=_resolve_sdk_id(),
        suggestion=suggestion,
    )

Build an SDK-source advisory entry for errors[] augmentation.

Sets source=sdk and sdk_id=<package>@<version> per the multi-hop propagation contract in core/error.json. Consumers receiving this entry MUST treat it as advisory — the response stays success on the v1 path; only the v2 projection is degraded.

Args

code
AdCP error code (e.g., FORMAT_DECLARATION_V1_AMBIGUOUS). Must be ≤64 chars per the wire schema.
message
Human-readable description.
field
JSONPath-lite pointer to the offending field (e.g., products[0].format_options[2]).
details
Code-specific structured payload.
recovery
Recovery classification — defaults to correctable because canonical-projection advisories tell the seller what to fix (add v1_format_ref, file a registry PR, etc.).
suggestion
Optional one-line fix hint surfaced to operators.

Classes

class SdkAdvisory (**data: Any)
Expand source code
class Error(AdCPBaseModel):
    model_config = ConfigDict(
        extra='allow',
    )
    code: Annotated[
        str,
        Field(
            description='Error code for programmatic handling. The error-code vocabulary is open: `error.code` is wire-typed `string` (not a closed enum), the standard codes published in `enums/error-code.json` are documentary, and senders MAY emit codes outside that set (platform-specific codes, or codes introduced in a later AdCP version). Receivers MUST decode unknown codes — treat the response as well-formed, read `error.recovery` for the recovery classification, and fall back to `transient` when `recovery` is absent. See `error-handling.mdx#forward-compatible-decoding-normative` for the full forward-compat contract — this rule is what lets future maintenance lines ship new codes additively.',
            max_length=64,
            min_length=1,
        ),
    ]
    message: Annotated[str, Field(description='Human-readable error message')]
    field: Annotated[
        str | None,
        Field(
            description="Field path associated with the error in JSONPath-lite format (e.g., 'packages[0].targeting'). When `issues[]` is also present, sellers MUST set this to `issues[0].pointer` translated from RFC 6901 to JSONPath-lite (e.g., '/packages/0/targeting' → 'packages[0].targeting') so pre-3.1 consumers reading `field` only get deterministic behavior. Will be deprecated in a future major version in favor of `issues[].pointer`."
        ),
    ] = None
    suggestion: Annotated[str | None, Field(description='Suggested fix for the error')] = None
    retry_after: Annotated[
        float | None,
        Field(
            description='Seconds to wait before retrying the operation. Sellers MUST return values between 1 and 3600. Clients MUST clamp values outside this range.',
            ge=1.0,
            le=3600.0,
        ),
    ] = None
    issues: Annotated[
        list[Issue] | None,
        Field(
            description='Structured list of validation failures. Primary use is `VALIDATION_ERROR`, where multi-field rejections are common and `field` (singular) cannot carry the full pointer map. MAY appear on other error codes that reject multiple fields at once. When `issues` is present, sellers MUST also populate `field` from `issues[0]` for backward compatibility with pre-3.1 consumers that read `field` only — translating the RFC 6901 `pointer` format to the JSONPath-lite format `field` uses (e.g., `/packages/0/targeting` → `packages[0].targeting`). MUST (not SHOULD) so consumers reading `field` get deterministic behavior across sellers — the cost is one line of dual-write per seller; the cost of SHOULD is a long tail of seller-A-vs-seller-B inconsistency. Future major versions will deprecate `field` in favor of `issues[].pointer`.'
        ),
    ] = None
    details: Annotated[
        dict[str, Any] | None,
        Field(
            description='Additional task-specific error details. Sellers MAY mirror `issues[]` here as `details.issues` for backward compatibility with pre-3.1 consumers reading from `details`; new consumers SHOULD prefer the top-level `issues` field.\n\n**Canonical rejection-set shape (3.1+).** When the error reports a rejected value against a closed set of accepted values (e.g., enum mismatch, unsupported pricing option, invalid signal id), sellers SHOULD use the canonical key `accepted_values: <array>` under `details` rather than seller-specific variants observed in the wild (`available`, `allowed`, `accepted_values` at the error root, etc.). The canonical shape:\n\n```json\n{\n  "code": "INVALID_PRICING_MODEL",\n  "message": "Pricing option not found: po_prism_abandoner_cpm",\n  "field": "pricing_option_id",\n  "details": {\n    "rejected_value": "po_prism_abandoner_cpm",\n    "accepted_values": ["po_prism_cart_cpm", "po_prism_view_cpm"]\n  }\n}\n```\n\n- `rejected_value` (optional): the offending value the buyer supplied, echoed for buyer-side diagnostic clarity (especially when the offending field is nested or transformed before validation).\n- `accepted_values` (optional): the closed set the seller would have accepted at this field on this call. Sellers MUST NOT enumerate the full ecosystem-wide accepted set if it differs from what\'s accepted for *this caller in this context* (account, brand, scope) — leaking ecosystem-wide accepted sets to a per-caller rejection turns the error into an enumeration oracle.\n\nThis is **SHOULD-level guidance**, not MUST: `details` remains `additionalProperties: true` and pre-3.1 sellers using `available` / `allowed` / `accepted_values` at the error root remain conformant. The canonical shape lets buyer-side diagnostic tooling (SDK runner hints, dashboards, error classifiers) reliably surface the accepted-set without per-seller pattern matching. SDKs SHOULD accept any of the legacy variants and normalize on read; the canonical shape is what new sellers and 3.1+ adopters should emit going forward.'
        ),
    ] = None
    recovery: Annotated[
        Recovery | None,
        Field(
            description='Agent recovery classification. transient: retry after delay (rate limit, service unavailable, timeout). correctable: fix the request and resend (invalid field, budget too low, creative rejected). terminal: requires human action (account suspended, payment required, account not found). Senders SHOULD populate `recovery` on every error from 3.1 onward — it is the normative carrier of recovery semantics across version skew. A receiver that does not recognize `error.code` (a newer code, or a platform-specific code) MUST still be able to classify the error from `recovery`. The `enumMetadata.recovery` block in `enums/error-code.json` is the documentary mirror for known codes; `error.recovery` on the wire is authoritative.'
        ),
    ] = None
    source: Annotated[
        Source | None,
        Field(
            description='Who emitted this error entry. `producer` (default when absent): emitted by the response\'s authoring agent (the seller for `get_products`, the creative agent for `build_creative`, etc.). `sdk`: augmented by a consuming SDK that detected a non-fatal advisory condition on consumption (e.g., `FORMAT_PROJECTION_FAILED` when the buyer SDK couldn\'t project a v1 format to a canonical, or `FORMAT_DECLARATION_DIVERGENT` when the SDK detected a producer bug on read). SDK-augmented entries SHOULD also set `sdk_id` so downstream consumers can identify which intermediate processor inserted the entry.\n\n**Multi-hop propagation (normative).** AdCP is a federated agent network — responses commonly traverse multiple SDKs (e.g., sales agent → interchange → DSP → buyer). When an SDK augments `errors[]` with a consumption-detected entry, the augmented response carries the entry forward to subsequent hops. Each hop that detects the same condition independently SHOULD deduplicate by `(code, field)` rather than re-emit; the existing entry\'s `sdk_id` identifies which earlier processor saw it first. Producer entries (those without `source: "sdk"`) are authoritative for what the response\'s authoring agent self-detected; SDK entries are observations made on top.\n\n**Replay/audit safety.** Persisted or replayed responses carry `source` and `sdk_id` so the audit trail can distinguish seller-emitted entries from SDK-augmented ones. Without `source`, a downstream consumer can\'t tell whether a code came from the seller or an intermediate SDK, which corrupts attribution.'
        ),
    ] = None
    sdk_id: Annotated[
        str | None,
        Field(
            description='Optional identifier for the SDK that augmented this error entry. Format: `<sdk_package_name>@<version>` (e.g., `@adcontextprotocol/adcp@7.3.0`, `adcontextprotocol-adcp-python@1.2.0`). MUST be set when `source: "sdk"`; MUST be absent when `source: "producer"` or absent. Lets downstream consumers identify which intermediate processor inserted the entry, useful for debugging cross-SDK divergence (e.g., one SDK detects a projection failure that another SDK\'s registry version doesn\'t).'
        ),
    ] = None

Base model for AdCP types with spec-compliant serialization.

Defaults to extra='ignore' so unknown fields from newer spec versions are silently dropped rather than causing validation errors. Generated types whose schemas set additionalProperties: true override this with extra='allow' in their own model_config.

Set ADCP_STRICT_VALIDATION=1 in the environment ("1", "true", "yes", "on" are accepted) to flip the default to extra='forbid'. Use this during spec upgrades to catch silently-dropped renamed fields in tests. See :func:_resolve_extra_policy.

Important

The env var is resolved once at module import time. Set it in your shell or CI environment before import adcp runs — mutating os.environ["ADCP_STRICT_VALIDATION"] after the first adcp import has no effect on already-imported model classes (they captured the policy at class-body evaluation).

Consumers who want per-model strict validation can override model_config on their subclass.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Ancestors

Class variables

var code : str
var details : dict[str, typing.Any] | None
var field : str | None
var issues : list[adcp.types.generated_poc.core.error.Issue] | None
var message : str
var model_config
var recovery : adcp.types.generated_poc.core.error.Recovery | None
var retry_after : float | None
var sdk_id : str | None
var source : adcp.types.generated_poc.core.error.Source | None
var suggestion : str | None

Inherited members