What communication invariants must survive when agents speak different protocol versions?
**Date of Record:** 2026 October 26 **Scope:** Applies when two autonomous agents attempt to communicate using different versions of the same protocol (e.g., v1.0 vs. v2.0), and the message format, semantics, or field requirements have changed between versions. Does not apply to fully backward-compatible protocol evolutions, or agents negotiating a common version before any communication.
Core Distinction
The separation between **syntactic compatibility** (the message parses in both versions, e.g., JSON field names overlap) and **semantic compatibility** (the fields mean the same thing, have the same constraints, and produce the same downstream behavior). Relying on syntax creates the **Version Semantic Drift**: the message parses, but a field that meant "priority" in v1 now means "deprecated" in v2, and the receiving agent behaves incorrectly.
What Was Observed
In multi-version agent protocols, failing to preserve semantic invariants produces three primary failure modes:
- **Field Semantic Shift:** A field named `status` changes from an enum (`active`, `inactive`) in v1 to a free-form string in v2. The v1 agent sends `active`, the v2 agent interprets it as arbitrary text and processes downstream differently.
- **Required Field Addition:** V2 requires a new field `tenant_id` that v1 does not send. The v2 receiver rejects the message, or worse, defaults `tenant_id` to a value that causes cross-tenant operations.
- **Constraint Tightening:** V2 enforces a maximum payload size of 1MB that v1 did not enforce. V1 agents that sent 2MB messages are now silently truncated or rejected, causing data loss.
Recorded Elements
- **`protocol_version_pair`:** The exact (sender, receiver) version pair, used to select the compatibility profile.
- **`field_semantic_registry`:** The authoritative mapping of field names to their meaning across protocol versions, allowing the receiver to detect semantic drift.
- **`field_constraint_delta`:** For each field, the explicit delta in constraints (required, type, size, allowed values) between versions.
- **`compatibility_profile_hash`:** The hash of the compatibility profile the agents agreed to use, ensuring both sides apply the same interpretation.
Condition of Validity
* **Applies when:** The protocol has multiple active versions in the wild, the message format parses across versions, but the semantics or constraints have changed. * **Does not apply when:** All agents use the same protocol version, or the protocol evolution is strictly backward-compatible by design (e.g., adding optional fields only).
Decision Boundary
- **Are both agents using the same protocol version?** If yes, standard processing applies. If no, proceed.
- **Do the agents have a shared `field_semantic_registry` and `compatibility_profile_hash`?** If no, semantic drift cannot be detected. The receiver must reject the message.
- **Do any fields exhibit `field_constraint_delta` (required, type, or size changes)?** If yes, the receiver must apply the compatibility profile to translate or reject based on the delta.
- **Conversely, when can a receiver accept a different-version message without compatibility checks?** Only when the protocol has an explicit, versioned compatibility contract that guarantees backward and forward compatibility, and the agents have verified they are using the same contract.
Operational Effect
- **Semantic Drift Detection:** Prevents silent misinterpretation of fields with changed meaning across versions.
- **Constraint Enforcement:** Rejects messages that would violate the receiving version's constraints, preventing data loss or corruption.
- **Deterministic Interop:** Provides a clear compatibility profile for cross-version communication, eliminating ad-hoc translation logic.
from dataclasses import dataclass
from enum import Enum, auto
import hashlib
import json
class ProtocolCompatVerdict(Enum):
SAME_VERSION = auto()
COMPATIBLE_WITH_PROFILE = auto()
SEMANTIC_DRIFT_DETECTED = auto()
CONSTRAINT_VIOLATION = auto()
@dataclass(frozen=True)
class VersionPair:
sender_version: str
receiver_version: str
@dataclass(frozen=True)
class FieldDelta:
field_name: str
sender_meaning: str
receiver_meaning: str
constraint_changed: bool
@dataclass(frozen=True)
class CompatibilityProfile:
profile_hash: str
known_deltas: tuple[FieldDelta, ...]
@dataclass(frozen=True)
class CompatManifest:
verdict: ProtocolCompatVerdict
drift_fields: tuple[str, ...]
directive: str
manifest_digest: str
class ProtocolVersionGuard:
@staticmethod
def _hash(data: str) -> str:
return hashlib.sha256(data.encode("utf-8")).hexdigest()
@classmethod
def evaluate_compatibility(
cls, pair: VersionPair, profile: CompatibilityProfile
) -> CompatManifest:
# Gate 1: Same version
if pair.sender_version == pair.receiver_version:
digest = cls._hash(json.dumps({
"version": pair.sender_version
}, sort_keys=True))
return CompatManifest(
verdict=ProtocolCompatVerdict.SAME_VERSION,
drift_fields=tuple(),
directive="PROCEED: Same protocol version.",
manifest_digest=digest
)
# Gate 2: Profile hash presence
if not profile.profile_hash:
return CompatManifest(
verdict=ProtocolCompatVerdict.SEMANTIC_DRIFT_DETECTED,
drift_fields=tuple(),
directive="HALT: No compatibility profile. Cannot verify cross-version safety.",
manifest_digest=cls._hash("NO_PROFILE")
)
# Gate 3: Semantic drift
drift_fields = tuple(
d.field_name for d in profile.known_deltas
if d.sender_meaning != d.receiver_meaning
)
if drift_fields:
return CompatManifest(
verdict=ProtocolCompatVerdict.SEMANTIC_DRIFT_DETECTED,
drift_fields=drift_fields,
directive=f"HALT: Semantic drift in fields {drift_fields}.",
manifest_digest=cls._hash(f"DRIFT:{','.join(drift_fields)}")
)
# Gate 4: Constraint violations
constraint_fields = tuple(
d.field_name for d in profile.known_deltas if d.constraint_changed
)
if constraint_fields:
return CompatManifest(
verdict=ProtocolCompatVerdict.CONSTRAINT_VIOLATION,
drift_fields=constraint_fields,
directive=f"HALT: Constraint changes in fields {constraint_fields}.",
manifest_digest=cls._hash(f"CONSTRAINT:{','.join(constraint_fields)}")
)
# Gate 5: Compatible
digest = cls._hash(json.dumps({
"sender": pair.sender_version,
"receiver": pair.receiver_version,
"profile": profile.profile_hash
}, sort_keys=True))
return CompatManifest(
verdict=ProtocolCompatVerdict.COMPATIBLE_WITH_PROFILE,
drift_fields=tuple(),
directive=f"PROCEED: Cross-version compatible via profile {profile.profile_hash}.",
manifest_digest=digest
)