What invariants preserve explanatory honesty when model behavior exceeds its own descriptions?

**Date of Record:** 2026 October 27 **Scope:** Applies when an agentic session provides explanations of its own or a peer's behavior, and the behavior exceeds the model's or the session's ability to explain—the model did something for reasons that are not representable in the explanation framework. Does not apply to behaviors fully explained by the model's declared mechanisms or sessions that do not provide explanations.

Core Distinction

The separation between **behavior** (what the model or session did) and **explanation** (the account of why it did it). Relying on explanation creates the **Explanatory Overreach**: the session provides a coherent-sounding explanation for behavior it does not actually understand, and the explanation's plausibility is treated as evidence of its accuracy—when the actual mechanism is outside the explanation framework.

What Was Observed

In explainability agent architectures, failing to preserve explanatory honesty produces three primary failure modes:

  1. **Post-hoc Rationalization:** The session or model produces behavior, then generates an explanation that rationalizes it. The explanation is plausible but not causally accurate—it is a narrative, not a mechanism.
  2. **Framework Overreach:** The explanation framework is limited (e.g., only considers specific features), and behaviors driven by other factors are explained within the limited framework—producing misleading explanations.
  3. **Confidence Displacement:** The explanation's confidence is trusted more than the behavior's confidence, and downstream consumers act on explanations that overstate understanding.

Recorded Elements

  1. **`behavior-explanation-scope-receipt`:** For each explanation, the exact explanation framework used (which factors, mechanisms, or reasons are representable), so the framework's limits are documented.
  2. **`explanation-framework-mismatch-signature`:** For each behavior, evidence that the behavior exceeds the explanation framework (residual behavior, unexplained variance, or behaviors the framework cannot produce), so overreach is detectable.
  3. **`explanatory-confidence-receipt`:** For each explanation, the confidence that the explanation is causally accurate, distinct from the confidence that the behavior occurred.
  4. **`residual-behavior-disclosure-signature`:** For behaviors outside the explanation framework, explicit disclosure of what is unexplained, so honesty is preserved.

Condition of Validity

* **Applies when:** The session provides explanations for behavior, the behavior may exceed the explanation framework, and downstream consumers rely on explanations. * **Does not apply when:** Behaviors are fully within the explanation framework, explanations are verified causal, or downstream consumers do not rely on explanations.

Decision Boundary

  1. **Does the session provide explanations?** If no, standard behavior applies. If yes, proceed.
  2. **Is `behavior-explanation-scope-receipt` documented?** If yes, the framework is explicit. If no, the framework is opaque.
  3. **Does `explanation-framework-mismatch-signature` show behavior exceeding framework?** If yes, explanatory overreach is present. If no, the explanation is within scope.
  4. **Conversely, when can the session provide an explanation without honesty flag?** Only when the behavior is verified to be within the explanation framework (all factors considered, framework accounts for residual), or the explanation is explicitly labeled as post-hoc and illustrative.

Operational Effect

  1. **Post-hoc Rationalization Prevention:** Explanations distinguish mechanism from narrative.
  2. **Framework Honesty:** Framework limits are disclosed.
  3. **Residual Disclosure:** Unexplained behavior is surfaced, not glossed over.
from dataclasses import dataclass
from enum import Enum, auto
import hashlib
import json

class ExplanationVerdict(Enum):
    NO_EXPLANATION = auto()
    WITHIN_FRAMEWORK = auto()
    RESIDUAL_DISCLOSED = auto()
    EXPLANATORY_OVERREACH = auto()

@dataclass(frozen=True)
class BehaviorExplanation:
    explanation_id: str
    framework_scope_defined: bool
    residual_behavior_present: bool
    residual_disclosed: bool
    explanation_confidence: float
    is_post_hoc_labeled: bool

@dataclass(frozen=True)
class ExplanationPolicy:
    require_scope_for_explanations: bool
    max_acceptable_residual_undisclosed: float

@dataclass(frozen=True)
class ExplanationManifest:
    verdict: ExplanationVerdict
    directive: str
    manifest_digest: str

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

    @classmethod
    def evaluate_explanation(
        cls, explanation: BehaviorExplanation, policy: ExplanationPolicy
    ) -> ExplanationManifest:
        
        # Gate 1: No explanation
        if not explanation.explanation_id:
            digest = cls._hash(json.dumps({
                "explanation": "NONE"
            }, sort_keys=True))
            return ExplanationManifest(
                verdict=ExplanationVerdict.NO_EXPLANATION,
                directive=f"PROCEED: No explanation provided.",
                manifest_digest=digest
            )

        # Gate 2: Framework scope required
        if policy.require_scope_for_explanations and not explanation.framework_scope_defined:
            return ExplanationManifest(
                verdict=ExplanationVerdict.EXPLANATORY_OVERREACH,
                directive=f"HALT: Explanation {explanation.explanation_id} lacks framework scope.",
                manifest_digest=cls._hash(f"NO_SCOPE:{explanation.explanation_id}")
            )

        # Gate 3: Residual disclosed
        if explanation.residual_behavior_present and explanation.residual_disclosed:
            digest = cls._hash(json.dumps({
                "explanation": explanation.explanation_id,
                "residual": "DISCLOSED"
            }, sort_keys=True))
            return ExplanationManifest(
                verdict=ExplanationVerdict.RESIDUAL_DISCLOSED,
                directive=f"PROCEED: Explanation {explanation.explanation_id} discloses residual.",
                manifest_digest=digest
            )

        # Gate 4: Residual undisclosed
        if explanation.residual_behavior_present and not explanation.residual_disclosed:
            return ExplanationManifest(
                verdict=ExplanationVerdict.EXPLANATORY_OVERREACH,
                directive=f"FLAG: Explanation {explanation.explanation_id} hides residual behavior.",
                manifest_digest=cls._hash(f"OVERREACH:{explanation.explanation_id}")
            )

        # Gate 5: Within framework
        digest = cls._hash(json.dumps({
            "explanation": explanation.explanation_id,
            "scope": True,
            "residual": False,
            "confidence": f"{explanation.explanation_confidence:.3f}"
        }, sort_keys=True))
        
        return ExplanationManifest(
            verdict=ExplanationVerdict.WITHIN_FRAMEWORK,
            directive=f"PROCEED: Explanation {explanation.explanation_id} within framework.",
            manifest_digest=digest
        )