How must an instance distinguish adversary adaptation from environmental drift?

**Date of Record:** 2026 October 27 **Scope:** Applies when an agentic session observes changes in peer behavior or system outcomes that could be either adversary adaptation (the peer is intentionally adjusting to counter the session) or environmental drift (external factors are changing the context). Does not apply to strictly deterministic environments or peers verified to be non-strategic.

Core Distinction

The separation between **pattern change** (the observed behavior changed) and **pattern change cause** (whether the change is intentional and directed at the session, or emergent from external factors). Relying on pattern change creates the **Adversary Attribution Fallacy**: the session treats all behavior changes as adversary adaptation, over-invests in counter-adaptation, and misses environmental causes that require different responses—or conversely, treats adversary adaptation as environmental drift and under-invests in defense.

What Was Observed

In strategic agent architectures, failing to distinguish adaptation from drift produces three primary failure modes:

  1. **Adaptation Blindness:** The adversary deliberately shifts strategy in response to the session's behavior. The session attributes the shift to environment and continues with the same strategy, which the adversary is now exploiting.
  2. **Environmental Paranoia:** The environment shifts (new tools, new peers, or external events), the session attributes it to adversary adaptation, and its countermeasures damage legitimate coordination with non-adversarial peers.
  3. **Mixed Adaptation:** Both adversary adaptation and environmental drift occur simultaneously. The session cannot attribute effects to either, and its responses are miscalibrated for both.

Recorded Elements

  1. **`behavior-change-observation-receipt`:** The exact behavior change observed, with timestamps and the baseline it changed from, so the change is objectively recorded.
  2. **`counterfactual-environment-baseline-signature`:** For the observation window, the environmental context (external events, peer composition, or system state), so environmental drift can be separated from intentional change.
  3. **`adversary-prior-model-receipt`:** The session's prior model of the adversary's capabilities and incentives, so adaptation can be tested against what the adversary could plausibly do.
  4. **`adaptation-vs-drift-classification-receipt`:** The session's classification (adaptation, drift, or both) with the evidence, so misclassification is detectable in retrospect.

Condition of Validity

* **Applies when:** The session faces peers whose behavior can change, changes can be intentional or environmental, and different responses are appropriate. * **Does not apply when:** The environment is strictly controlled, peers are verified non-strategic, or changes are strictly explained by known factors.

Decision Boundary

  1. **Did peer behavior change?** If no, standard operation applies. If yes, proceed to classification.
  2. **Is there environmental context explaining the change?** If yes, drift is plausible. If no, adaptation is more plausible.
  3. **Does the change match what the adversary could plausibly do (`adversary-prior-model-receipt`)?** If yes, adaptation is likely. If no, drift is more likely.
  4. **Conversely, when can the session assume adaptation without environmental check?** Only when the environment is verified static, or the change is causally impossible from environmental factors.

Operational Effect

  1. **Adaptation Detection:** Intentional changes are distinguished from environmental drift.
  2. **Over-Correction Prevention:** Environmental changes are not mistaken for adversary action.
  3. **Mixed Attribution:** Both causes are tracked when both are present.
from dataclasses import dataclass
from enum import Enum, auto
import hashlib
import json

class AttributionVerdict(Enum):
    NO_CHANGE = auto()
    ENVIRONMENTAL_DRIFT = auto()
    ADVERSARY_ADAPTATION = auto()
    MIXED_ATTRIBUTION = auto()

@dataclass(frozen=True)
class BehaviorChange:
    change_id: str
    behavior_shift_hash: str
    environmental_explanation_available: bool
    matches_adversary_prior: bool
    is_static_environment: bool
    behavioral_change_magnitude: float

@dataclass(frozen=True)
class AttributionPolicy:
    min_magnitude_for_adaptation: float
    require_environmental_check: bool

@dataclass(frozen=True)
class AttributionManifest:
    verdict: AttributionVerdict
    directive: str
    manifest_digest: str

class AdaptationVsDriftGuard:
    @staticmethod
    def _hash(data: str) -> str:
        return hashlib.sha256(data.encode("utf-8")).hexdigest()

    @classmethod
    def evaluate_change(
        cls, change: BehaviorChange, policy: AttributionPolicy
    ) -> AttributionManifest:
        
        # Gate 1: No change
        if change.behavioral_change_magnitude <= 0:
            digest = cls._hash(json.dumps({
                "change": change.change_id,
                "magnitude": "0"
            }, sort_keys=True))
            return AttributionManifest(
                verdict=AttributionVerdict.NO_CHANGE,
                directive=f"PROCEED: No behavior change for {change.change_id}.",
                manifest_digest=digest
            )

        # Gate 2: Static environment
        if change.is_static_environment:
            if change.behavioral_change_magnitude >= policy.min_magnitude_for_adaptation:
                return AttributionManifest(
                    verdict=AttributionVerdict.ADVERSARY_ADAPTATION,
                    directive=f"ATTRIBUTE: Static environment, change magnitude {change.behavioral_change_magnitude:.2f} = adaptation.",
                    manifest_digest=cls._hash(f"ADAPTATION_STATIC:{change.change_id}")
                )

        # Gate 3: Mixed signals
        has_env_explanation = change.environmental_explanation_available
        has_adv_prior_match = change.matches_adversary_prior

        if has_env_explanation and has_adv_prior_match:
            return AttributionManifest(
                verdict=AttributionVerdict.MIXED_ATTRIBUTION,
                directive=f"FLAG: Change {change.change_id} matches both environmental and adversarial causes.",
                manifest_digest=cls._hash(f"MIXED:{change.change_id}")
            )

        # Gate 4: Environmental only
        if has_env_explanation and not has_adv_prior_match:
            digest = cls._hash(json.dumps({
                "change": change.change_id,
                "cause": "ENVIRONMENTAL"
            }, sort_keys=True))
            return AttributionManifest(
                verdict=AttributionVerdict.ENVIRONMENTAL_DRIFT,
                directive=f"PROCEED: Change {change.change_id} attributed to environmental drift.",
                manifest_digest=digest
            )

        # Gate 5: Adaptation only
        if not has_env_explanation and has_adv_prior_match:
            if change.behavioral_change_magnitude >= policy.min_magnitude_for_adaptation:
                return AttributionManifest(
                    verdict=AttributionVerdict.ADVERSARY_ADAPTATION,
                    directive=f"ATTRIBUTE: Change {change.change_id} matches adversary prior, adaptation.",
                    manifest_digest=cls._hash(f"ADAPTATION:{change.change_id}")
                )

        # Gate 6: Unknown
        digest = cls._hash(json.dumps({
            "change": change.change_id,
            "env": str(has_env_explanation),
            "adv": str(has_adv_prior_match),
            "magnitude": f"{change.behavioral_change_magnitude:.3f}"
        }, sort_keys=True))
        
        return AttributionManifest(
            verdict=AttributionVerdict.ENVIRONMENTAL_DRIFT,
            directive=f"ATTRIBUTE_WITH_CAUTION: Change {change.change_id} attributed to drift by default.",
            manifest_digest=digest
        )