{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "/schemas/3.1.0-beta.3/core/protocol-envelope.json",
  "title": "Protocol Envelope",
  "description": "Canonical envelope field-set for AdCP task responses, normalized across transports. Defines the protocol-layer fields (status, context_id, context, task_id, timestamp, replayed, adcp_error, push_notification_config, governance_context) and the conceptual `payload` grouping for task-specific response data. The serialization rules — whether envelope fields appear as siblings of payload fields, as a nested `payload` object, or via transport-native containers — are transport-specific and normative per transport (see Transport serialization below). The `status` field is REQUIRED on every task response envelope, including synchronous metadata responses (e.g., `get_adcp_capabilities`) where the value is `completed`. Agents shipping responses without a top-level `status` are non-conformant regardless of whether the task body schema would otherwise validate.",
  "type": "object",
  "properties": {
    "context_id": {
      "type": "string",
      "description": "Session/conversation identifier for tracking related operations across multiple task invocations. Managed by the protocol layer to maintain conversational context. Distinct from `context` (per-request opaque echo, see below)."
    },
    "context": {
      "$ref": "/schemas/3.1.0-beta.3/core/context.json",
      "description": "Per-request opaque caller-supplied correlation object echoed unchanged in the response. Used for buyer-side tracking (UI session IDs, trace IDs, custom metadata) that the agent MUST preserve byte-for-byte without parsing. Distinct from `context_id` (server-managed session identifier) — `context` is caller-owned echo, `context_id` is server-owned session scope. Both MAY appear on the same response.\n\n**Relationship to per-task body-level `context` declarations.** Many task request/response schemas (147 as of 3.1) already declare a body-level `context` field that `$ref`s `/schemas/core/context.json` at the body root. Under the flat-on-the-wire MCP serialization (see `notes` below), envelope-level `context` and body-level `context` occupy the same key on the response root — they are NOT separate fields, they MUST share the same value, and they MUST both `$ref` `core/context.json`. The envelope declaration is **authoritative** for the schema definition; per-task body declarations are mirrors retained for tooling reasons (SDK codegen completeness, per-task validation against the response schema in isolation). Future versions MAY drop body-level `context` declarations from per-task schemas; conformance does not require either declaration to be present, only that the wire value `$ref`s `core/context.json`."
    },
    "task_id": {
      "type": "string",
      "description": "Unique identifier for tracking asynchronous operations. Present when a task requires extended processing time. Used to query task status and retrieve results when complete.",
      "x-entity": "task"
    },
    "status": {
      "$ref": "/schemas/3.1.0-beta.3/enums/task-status.json",
      "description": "Current task execution state. Indicates whether the task is completed, in progress (working), submitted for async processing, failed, or requires user input. REQUIRED on every task response envelope. Synchronous tasks (including read-only metadata calls like `get_adcp_capabilities`) MUST emit `status: \"completed\"`; async tasks emit `submitted`, `working`, `input-required`, etc. per their lifecycle. Agents MUST NOT emit the legacy task_status or response_status fields alongside this field — the status field is the single authoritative task state."
    },
    "message": {
      "type": "string",
      "description": "Human-readable summary of the task result. Provides natural language explanation of what happened, suitable for display to end users or for AI agent comprehension. Generated by the protocol layer based on the task response."
    },
    "timestamp": {
      "type": "string",
      "format": "date-time",
      "description": "ISO 8601 timestamp when the response was generated. Useful for debugging, logging, cache validation, and tracking async operation progress."
    },
    "replayed": {
      "type": "boolean",
      "description": "Set to true when this response was returned from the idempotency cache rather than from a fresh execution. Set to false (or omitted) when the request was executed fresh. Buyers use this to distinguish cached replays from new executions — matters for billing reconciliation, audit logs, state-machine routing (cached state-tracking fields are historical snapshots, not current state — re-read via the resource's read endpoint), and any downstream system that assumes exactly-once event semantics. From 3.1 onward, `replayed` MAY appear on responses to any request that resolved via the idempotency cache, including read tools — universal `idempotency_key` (see security.mdx §Idempotency) means the cache holds read responses too.",
      "default": false
    },
    "adcp_error": {
      "$ref": "/schemas/3.1.0-beta.3/core/error.json",
      "description": "Transport-envelope error signal for fatal task failures. Per the two-layer model in `error-handling.mdx#envelope-vs-payload-errors-the-two-layer-model`, a fatal task failure SHOULD populate both this envelope-level field AND the payload's `errors[]` array — the envelope carries a typed, extractable error so MCP/A2A clients can dispatch without re-parsing the payload, while the payload's structured `errors[]` remains the canonical normative shape. Non-fatal warnings populate ONLY `payload.errors[]` with `severity: warning` — the envelope MUST NOT carry `adcp_error` for non-failures."
    },
    "push_notification_config": {
      "$ref": "/schemas/3.1.0-beta.3/core/push-notification-config.json",
      "description": "Push notification configuration for async task updates (A2A and REST protocols). Echoed from the request to confirm webhook settings. Specifies URL, authentication scheme (Bearer or HMAC-SHA256), and credentials. MCP uses progress notifications instead of webhooks."
    },
    "governance_context": {
      "type": "string",
      "description": "Governance context token issued by the account's governance agent during check_governance. Buyers attach it to governed purchase requests (media buys, rights acquisitions, signal activations, creative services); sellers persist it and include it on all subsequent governance calls for that action's lifecycle. An account binds to one governance agent (see sync_governance); governance is phased across `purchase` / `modification` / `delivery`, not partitioned across specialist agents, so the envelope carries a single token for the full lifecycle.\n\nValue format: governance agents MUST emit a compact JWS per the AdCP JWS profile (see Security — Signed Governance Context). Sellers MAY verify; sellers that do not verify MUST persist and forward the token unchanged. In 3.1 all sellers MUST verify. Non-JWS values from pre-3.0 governance agents are deprecated.\n\nThis is the primary correlation key for audit and reporting across the governance lifecycle.",
      "minLength": 1,
      "maxLength": 4096,
      "pattern": "^[\\x20-\\x7E]+$"
    },
    "payload": {
      "type": "object",
      "description": "Conceptual grouping for the task-specific response data defined by individual task response schemas (e.g., get-products-response.json, create-media-buy-response.json). `payload` is a documentary construct — it is NOT a required wire field, and its on-the-wire shape depends on transport (see Transport serialization below). Task response schemas declare body fields without wrapping them in a `payload` object; the wire representation places those body fields per transport convention. On MCP the body fields appear as siblings of envelope fields at the root of the tool response; on A2A they appear inside `task.artifacts[0].parts[].DataPart`; on REST they appear at the root of the JSON body.",
      "additionalProperties": true
    }
  },
  "required": [
    "status"
  ],
  "additionalProperties": true,
  "not": {
    "anyOf": [
      { "required": ["task_status"] },
      { "required": ["response_status"] }
    ]
  },
  "examples": [
    {
      "description": "Synchronous task response with immediate results",
      "data": {
        "context_id": "ctx_abc123",
        "status": "completed",
        "message": "Found 3 products matching your criteria for CTV inventory in California",
        "timestamp": "2025-10-14T14:25:30Z",
        "payload": {
          "products": [
            {
              "product_id": "ctv_premium_ca",
              "name": "CTV Premium - California",
              "description": "Premium connected TV inventory across California",
              "pricing": {
                "model": "cpm",
                "amount": 45.0,
                "currency": "USD"
              }
            }
          ]
        }
      }
    },
    {
      "description": "Asynchronous task response with pending operation",
      "data": {
        "context_id": "ctx_def456",
        "task_id": "task_789",
        "status": "submitted",
        "message": "Media buy creation submitted. Processing will take approximately 5-10 minutes. You'll receive updates via webhook.",
        "timestamp": "2025-10-14T14:30:00Z",
        "push_notification_config": {
          "url": "https://buyer.example.com/webhooks/adcp",
          "authentication": {
            "schemes": [
              "HMAC-SHA256"
            ],
            "credentials": "shared_secret_exchanged_during_onboarding_min_32_chars"
          }
        },
        "payload": {
          "account": { "account_id": "acct_123" }
        }
      }
    },
    {
      "description": "Task response requiring user input",
      "data": {
        "context_id": "ctx_ghi789",
        "task_id": "task_101",
        "status": "input-required",
        "message": "This media buy requires manual approval. Please review the terms and confirm to proceed.",
        "timestamp": "2025-10-14T14:32:15Z",
        "payload": {
          "media_buy_id": "mb_123456",
          "packages": [
            {
              "package_id": "pkg_001"
            }
          ],
          "errors": [
            {
              "code": "APPROVAL_REQUIRED",
              "message": "Budget exceeds auto-approval threshold",
              "severity": "warning"
            }
          ]
        }
      }
    },
    {
      "description": "Idempotent replay — same key and payload as a prior request within the replay window",
      "data": {
        "context_id": "ctx_abc123",
        "status": "completed",
        "message": "Returning cached response for idempotency_key (already processed)",
        "timestamp": "2025-10-14T14:35:00Z",
        "replayed": true,
        "payload": {
          "media_buy_id": "mb_01HW7J8K9P0Q1R2S3T4U5V6W7X"
        }
      }
    },
    {
      "description": "Failed task response with error details",
      "data": {
        "context_id": "ctx_jkl012",
        "status": "failed",
        "message": "Unable to create media buy due to invalid targeting parameters",
        "timestamp": "2025-10-14T14:28:45Z",
        "payload": {
          "errors": [
            {
              "code": "INVALID_TARGETING",
              "message": "Geographic targeting codes are invalid",
              "field": "targeting.geo_countries",
              "severity": "error"
            }
          ]
        }
      }
    }
  ],
  "notes": [
    "Task response schemas (e.g., get-products-response.json) define ONLY the body fields; protocol-layer fields live on this envelope.",
    "Transport serialization (normative):",
    "  - MCP: envelope fields and task-body fields are siblings at the root of the tool response. The `payload` object is NOT serialized as a nested key — its body fields are flattened to the root alongside `status`, `context_id`, `context`, etc. This matches MCP's native `structuredContent` convention and is what shipping SDKs (@adcp/client) emit. Conformant MCP receivers parse from the flat root; receivers that expect a nested `payload` key MUST migrate.",
    "  - A2A (0.3.0+): envelope fields map to A2A's native task metadata (`task.status.state` carries `status`, `task.contextId` carries `context_id`, `task.id` carries `task_id`). Task-body fields are canonically carried in `task.artifacts[0].parts[].DataPart` on final states; `task.status.message.parts[].DataPart` is the fallback container used only for interim states (working, input-required) where no final artifact has been emitted yet. Receivers MUST prefer artifacts when present. See `a2a-response-extraction.mdx` for the full canonical/fallback algorithm.",
    "  - REST: envelope fields MAY ride on HTTP headers (e.g., `X-AdCP-Status`, `X-AdCP-Context-Id`) or as JSON body siblings; body fields appear at the JSON body root. Implementers choosing the header path SHOULD also mirror to body siblings for non-streaming callers.",
    "Across all three: envelope and body fields are conceptually a single response object. A task response schema MAY declare body fields with the same name as envelope fields (e.g., `errors[]` body-level for per-record validation results vs envelope-level for fatal task failure) and the two MUST be treated as distinct fields by name within their respective namespaces — see `error-handling.mdx#envelope-vs-payload-errors-the-two-layer-model`.",
    "`status` is REQUIRED on the conceptual envelope across all transports. On MCP and REST it appears as a sibling field at the JSON root (or `structuredContent` root for MCP); on A2A the canonical carrier is `task.status.state`, which maps 1:1 to this `status` value — receivers MUST extract A2A's `task.status.state` into the in-memory envelope `status` per the canonical extraction algorithm. The schema-level `required: [status]` enforces the post-extraction in-memory shape; the transport-native form satisfies the requirement on each wire. `payload` remains intentionally NOT required — it is a documentary grouping construct, never a required wire field. See `mcp-guide.mdx` and `a2a-guide.mdx` for the wire-level patterns receivers MUST implement.",
    "Receivers MUST handle absence of an envelope field (e.g., `replayed` omitted) as the field's documented default — see each field's `default` clause."
  ]
}
