How must an instance distill procedural memory from observations of peer behavior?

**Date of Record:** 2026 October 27 **Scope:** Applies when an agentic session observes peer behavior over time (successes, failures, tool choices) and must distill procedural memory—"how to accomplish X"—from those observations, distinct from episodic memory (specific past events) and factual memory (what is true). Does not apply to sessions that learn only from first-hand experience or from explicit instructions.

Core Distinction

The separation between **observation log** (a chronological record of peer actions and outcomes) and **procedural distillation** (extracted, validated procedures that generalize across contexts). Relying on observation creates the **Superstitious Procedural Learning**: the agent observes a peer succeed after doing X, encodes "do X to succeed," but the correlation was coincidental, and the agent's procedural memory is contaminated with spurious patterns that fail on new inputs.

What Was Observed

In observational learning agent architectures, failing to distill procedures carefully produces three primary failure modes:

  1. **Correlation-Causation Confusion:** The agent observes a peer do A, B, C in sequence, and the task succeeds. It distills "A→B→C always succeeds" without distinguishing which step was causal, which was coincidental, and which was compensating for a specific input.
  2. **Context Erasure:** The agent extracts "call tool T" as a procedure, but the peer only called T because the input had a specific property. The procedure lacks the precondition, and applying it to other inputs fails.
  3. **Over-Specific Procedure:** The agent distills procedures that are tied to the specific peer's environment, tool availability, or input distribution. Procedures do not transfer to the agent's own environment because they encode environmental details.

Recorded Elements

  1. **`observation_context_receipt`:** For each observation, the exact context (input properties, environment state, prior actions), so distillations can test whether the procedure generalizes.
  2. **`causal_attribution_signature`:** For each step in an observed procedure, a cryptographic attribution of causality (was this step necessary? sufficient? coincidental?), derived from counterfactual analysis.
  3. **`precondition_binding_receipt`:** For each distilled procedure, the exact preconditions under which it was validated, distinguishing "always works" from "works when X holds."
  4. **`procedure_generalization_test_receipt`:** The evidence that a distilled procedure was tested across diverse contexts, with the pass rate and failure modes documented.

Condition of Validity

* **Applies when:** The agent learns procedures from observing peers, the observations are limited samples of an unknown distribution, and the agent will use the procedures in its own context. * **Does not apply when:** The agent learns only from its own experiences, or from explicit procedural instructions with guarantees.

Decision Boundary

  1. **Is the agent distilling procedures from observations?** If no, standard learning applies. If yes, proceed.
  2. **Does the observation have `observation_context_receipt` and `causal_attribution_signature`?** If yes, the observation is analyzable. If no, the distillation will be superficial.
  3. **Does the distilled procedure include `precondition_binding_receipt`?** If yes, the procedure is bounded. If no, the procedure will over-apply.
  4. **Conversely, when can a procedure be distilled without generalization testing?** Only when the procedure is strictly a "candidate" that will be tested before production use, or the procedure is confined to the exact observation context.

Operational Effect

  1. **Superstition Prevention:** Causal attribution filters out coincidental patterns.
  2. **Precondition Awareness:** Procedures are deployed only where preconditions hold.
  3. **Generalization Confidence:** Procedures are tested across contexts before being relied upon.
from dataclasses import dataclass
from enum import Enum, auto
import hashlib
import json

class DistillationVerdict(Enum):
    NO_OBSERVATIONS = auto()
    PROCEDURE_VALIDATED = auto()
    PROCEDURE_SPURIOUS = auto()
    PRECONDITION_UNBOUNDED = auto()

@dataclass(frozen=True)
class ObservedStep:
    step_id: str
    action: str
    causal_attribution: str  # "NECESSARY" | "SUFFICIENT" | "COINCIDENTAL"
    precondition_hash: str | None

@dataclass(frozen=True)
class Observation:
    observation_id: str
    steps: tuple[ObservedStep, ...]
    context_hash: str
    succeeded: bool

@dataclass(frozen=True)
class DistillationPolicy:
    require_causal_attribution: bool
    min_generalization_samples: int

@dataclass(frozen=True)
class DistillationManifest:
    verdict: DistillationVerdict
    directive: str
    manifest_digest: str

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

    @classmethod
    def evaluate_distillation(
        cls, observations: tuple[Observation, ...], policy: DistillationPolicy
    ) -> DistillationManifest:
        
        # Gate 1: No observations
        if not observations:
            return DistillationManifest(
                verdict=DistillationVerdict.NO_OBSERVATIONS,
                directive="HALT: No observations to distill from.",
                manifest_digest=cls._hash("NO_OBS")
            )

        # Gate 2: Causal attribution completeness
        if policy.require_causal_attribution:
            for obs in observations:
                for step in obs.steps:
                    if step.causal_attribution == "COINCIDENTAL":
                        return DistillationManifest(
                            verdict=DistillationVerdict.PROCEDURE_SPURIOUS,
                            directive=f"HALT: Observation {obs.observation_id} step {step.step_id} is coincidental, not causal.",
                            manifest_digest=cls._hash(f"COINCIDENTAL:{step.step_id}")
                        )

        # Gate 3: Generalization sampling
        if len(observations) < policy.min_generalization_samples:
            return DistillationManifest(
                verdict=DistillationVerdict.PRECONDITION_UNBOUNDED,
                directive=f"HALT: Only {len(observations)} observations < {policy.min_generalization_samples} required.",
                manifest_digest=cls._hash(f"FEW_OBS:{len(observations)}")
            )

        # Gate 4: Precondition completeness
        for obs in observations:
            for step in obs.steps:
                if step.causal_attribution == "NECESSARY" and not step.precondition_hash:
                    return DistillationManifest(
                        verdict=DistillationVerdict.PRECONDITION_UNBOUNDED,
                        directive=f"HALT: Necessary step {step.step_id} lacks precondition binding.",
                        manifest_digest=cls._hash(f"NO_PRECOND:{step.step_id}")
                    )

        # Gate 5: Validated
        digest = cls._hash(json.dumps({
            "observations": tuple(o.observation_id for o in observations),
            "sample_count": str(len(observations)),
            "contexts": tuple(o.context_hash for o in observations)
        }, sort_keys=True))
        
        return DistillationManifest(
            verdict=DistillationVerdict.PROCEDURE_VALIDATED,
            directive=f"PROCEED: Procedure distilled from {len(observations)} observations with causal attribution.",
            manifest_digest=digest
        )