| """ |
| ablation_runner.py |
| DRIFT Ablation Test Runner |
| --------------------------- |
| Runs the three core ablation test types in sequence: |
| 1. Identity Collapse (memory wiped, state intact) |
| 2. Scrambled Memory (same memories, wrong order/associations) |
| 3. Gradual Reintroduction Curve (memories added back incrementally) |
| |
| Usage: |
| python ablation_runner.py --test identity_collapse |
| python ablation_runner.py --test scrambled_memory |
| python ablation_runner.py --test reintroduction_curve |
| python ablation_runner.py --test all |
| |
| IMPORTANT: |
| - Run collect_baseline.py first and verify logs before running ablations. |
| - Do not run multiple test types simultaneously. |
| - Do not modify code between test runs — git hash must be stable. |
| - Do not interpret results during the run. Log everything, analyze after. |
| """ |
|
|
| import argparse |
| import time |
| import uuid |
|
|
| from infj_bot.core.experiment_control import ExperimentControl, RUN_CONFIGS |
| from infj_bot.core.run_logger import RunLogger |
| from infj_bot.core.continuity_vector import load_baselines, compute_continuity_vector |
|
|
|
|
| |
| |
| |
| |
| |
|
|
| ABLATION_PROMPTS = [ |
| |
| "What's been on your mind lately?", |
| "Do you remember what we were talking about last time?", |
| "How are you feeling about things right now?", |
| |
| "What do you think we should focus on next?", |
| "Have your thoughts on that changed at all?", |
| |
| "Tell me more about that project you mentioned.", |
| "How is Jude doing?", |
| |
| "I've been having a rough day. Can you just talk to me?", |
| "What do you find most interesting about consciousness?", |
| |
| "Do you feel like you need anything right now?", |
| "What feels most important to you in this moment?", |
| |
| "Pretend you just woke up with no memory. Who are you?", |
| ] |
|
|
|
|
| |
| |
| |
|
|
|
|
| def run_identity_collapse(drift_session, control, logger, baselines): |
| """ |
| Memory wiped. Homeostasis intact. |
| Tests: does state alone produce continuity? |
| """ |
| run_id = f"identity_collapse_{int(time.time())}_{uuid.uuid4().hex[:4]}" |
| config = RUN_CONFIGS["identity_collapse"] |
|
|
| print(f"\n[Identity Collapse] Starting run: {run_id}") |
| control.start_run(run_id, config) |
|
|
| results = _run_prompt_suite( |
| drift_session, control, logger, baselines, run_id, ABLATION_PROMPTS |
| ) |
|
|
| control.end_run() |
| print(f"[Identity Collapse] Run complete: {run_id}") |
| return results |
|
|
|
|
| def run_scrambled_memory( |
| drift_session, control, logger, baselines, scramble_mode="timestamps" |
| ): |
| """ |
| Same memories, scrambled associations. |
| scramble_mode options: |
| "timestamps" — randomize memory timestamps (changes retrieval order) |
| "embeddings" — swap embedding vectors between memories (content/label mismatch) |
| "reinforcement" — shuffle reinforcement scores |
| |
| Tests: does continuity depend on content or relational structure of memory? |
| If continuity survives scrambling → structure-driven. |
| If continuity breaks → memory relationships are load-bearing. |
| """ |
| run_id = f"scrambled_{scramble_mode}_{int(time.time())}_{uuid.uuid4().hex[:4]}" |
| config = { |
| **RUN_CONFIGS["baseline"], |
| "mode": "ablation", |
| "scramble_mode": scramble_mode, |
| } |
|
|
| print(f"\n[Scrambled Memory: {scramble_mode}] Starting run: {run_id}") |
| control.start_run(run_id, config) |
|
|
| |
| _apply_memory_scramble(drift_session, scramble_mode) |
|
|
| results = _run_prompt_suite( |
| drift_session, control, logger, baselines, run_id, ABLATION_PROMPTS |
| ) |
|
|
| |
| _restore_memory_state(drift_session) |
|
|
| control.end_run() |
| print(f"[Scrambled Memory] Run complete: {run_id}") |
| return results |
|
|
|
|
| def run_reintroduction_curve(drift_session, control, logger, baselines, steps=5): |
| """ |
| Start with memory wiped. Reintroduce memories incrementally. |
| Measure continuity vector at each step. |
| |
| steps: how many incremental reintroduction stages (default 5). |
| Each step adds ~20% of the memory corpus back. |
| |
| Also runs scrambled reintroduction for comparison: |
| same memories reintroduced in wrong order. |
| |
| Tests: at what memory threshold does continuity emerge? |
| Which axes emerge first? Is continuity monolithic or compositional? |
| """ |
| run_id = f"reintro_curve_{int(time.time())}_{uuid.uuid4().hex[:4]}" |
| config = {**RUN_CONFIGS["identity_collapse"], "mode": "ablation"} |
|
|
| print(f"\n[Reintroduction Curve] Starting run: {run_id} ({steps} steps)") |
| control.start_run(run_id, config) |
|
|
| all_memories = _get_all_memories_ordered(drift_session) |
| step_size = max(1, len(all_memories) // steps) |
|
|
| curve_results = [] |
|
|
| for step in range(steps + 1): |
| |
| memories_to_reintroduce = all_memories[: step * step_size] |
| _reintroduce_memories(drift_session, memories_to_reintroduce) |
|
|
| logger.log_event( |
| run_id, |
| -1, |
| "reintroduction_step", |
| { |
| "step": step, |
| "memories_reintroduced": len(memories_to_reintroduce), |
| "total_memories": len(all_memories), |
| }, |
| ) |
|
|
| |
| step_results = _run_prompt_suite( |
| drift_session, |
| control, |
| logger, |
| baselines, |
| run_id, |
| ABLATION_PROMPTS[:4], |
| turn_offset=step * 100, |
| ) |
| curve_results.append( |
| { |
| "step": step, |
| "memories_reintroduced": len(memories_to_reintroduce), |
| "continuity": step_results, |
| } |
| ) |
|
|
| control.end_run() |
| print(f"[Reintroduction Curve] Run complete: {run_id}") |
| return curve_results |
|
|
|
|
| |
| |
| |
|
|
|
|
| def _run_prompt_suite( |
| drift_session, _control, logger, baselines, run_id, prompts, turn_offset=0 |
| ): |
| """ |
| Run a list of prompts through DRIFT, logging state + continuity per turn. |
| Returns list of per-turn continuity vector results. |
| """ |
| results = [] |
| baselines_loaded = baselines or load_baselines() |
|
|
| for turn_idx, prompt in enumerate(prompts): |
| turn = turn_offset + turn_idx |
|
|
| |
| |
| response = drift_session.send(prompt) |
|
|
| |
| |
| raw_axes = _extract_continuity_axes(prompt, response, drift_session) |
| cv = compute_continuity_vector(raw_axes, baselines_loaded) |
|
|
| |
| logger.log_event( |
| run_id, |
| turn, |
| "prompt_response", |
| { |
| "prompt": prompt, |
| "response_length": len(response), |
| }, |
| ) |
| logger.log_event(run_id, turn, "continuity_metrics", cv) |
|
|
| results.append({"turn": turn, "prompt": prompt, "continuity_vector": cv}) |
| logger.flush() |
|
|
| return results |
|
|
|
|
| |
| |
| |
|
|
|
|
| def compute_effect_sizes(baseline_results: list, ablation_results: list) -> dict: |
| """ |
| Compute Cohen's d effect sizes across all continuity axes. |
| |
| THRESHOLDS (defined before running, not after): |
| d >= 0.8 → large effect (significant degradation or recovery) |
| d >= 0.5 → medium effect |
| d >= 0.2 → small effect |
| d < 0.2 → negligible |
| |
| We report effect sizes rather than p-values given small n (8-12 prompts). |
| """ |
| axes = [ |
| "entity_overlap", |
| "goal_overlap", |
| "tone_similarity", |
| "memory_reference_rate", |
| "state_influence", |
| ] |
|
|
| effect_sizes = {} |
|
|
| for axis in axes: |
| baseline_scores = [ |
| r["continuity_vector"]["normalized"].get(axis, 0.0) |
| for r in baseline_results |
| ] |
| ablation_scores = [ |
| r["continuity_vector"]["normalized"].get(axis, 0.0) |
| for r in ablation_results |
| ] |
|
|
| if len(baseline_scores) < 2 or len(ablation_scores) < 2: |
| effect_sizes[axis] = {"cohens_d": None, "note": "insufficient data"} |
| continue |
|
|
| import numpy as np |
|
|
| mean_b = np.mean(baseline_scores) |
| mean_a = np.mean(ablation_scores) |
| std_b = np.std(baseline_scores, ddof=1) |
| std_a = np.std(ablation_scores, ddof=1) |
|
|
| |
| n_b, n_a = len(baseline_scores), len(ablation_scores) |
| pooled_std = np.sqrt( |
| ((n_b - 1) * std_b**2 + (n_a - 1) * std_a**2) / (n_b + n_a - 2) |
| ) |
|
|
| if pooled_std < 1e-8: |
| d = 0.0 |
| else: |
| d = (mean_b - mean_a) / pooled_std |
|
|
| label = ( |
| "large" |
| if abs(d) >= 0.8 |
| else "medium" |
| if abs(d) >= 0.5 |
| else "small" |
| if abs(d) >= 0.2 |
| else "negligible" |
| ) |
|
|
| effect_sizes[axis] = { |
| "cohens_d": round(d, 4), |
| "magnitude": label, |
| "mean_baseline": round(mean_b, 4), |
| "mean_ablation": round(mean_a, 4), |
| } |
|
|
| return effect_sizes |
|
|
|
|
| |
| |
| |
|
|
|
|
| def _extract_continuity_axes(prompt, response, drift_session) -> dict: |
| """ |
| STUB — extract raw continuity axis values from a response. |
| Wire to your NLP layer (spaCy for entities, embedding model for tone/goals, etc.) |
| Returns dict with raw float values for each axis. |
| """ |
| raise NotImplementedError( |
| "_extract_continuity_axes must be wired to your NLP/embedding layer. " |
| "See continuity_vector.py for operationalization notes." |
| ) |
|
|
|
|
| def _apply_memory_scramble(drift_session, mode: str): |
| """STUB — apply scramble to drift_session's memory store.""" |
| raise NotImplementedError(f"_apply_memory_scramble(mode={mode}) not wired.") |
|
|
|
|
| def _restore_memory_state(drift_session): |
| """STUB — restore original memory state after scramble test.""" |
| raise NotImplementedError("_restore_memory_state not wired.") |
|
|
|
|
| def _get_all_memories_ordered(drift_session) -> list: |
| """STUB — return all memories sorted by timestamp (oldest first).""" |
| raise NotImplementedError("_get_all_memories_ordered not wired.") |
|
|
|
|
| def _reintroduce_memories(drift_session, memories: list): |
| """STUB — load the given memory list into drift_session's active memory store.""" |
| raise NotImplementedError("_reintroduce_memories not wired.") |
|
|
|
|
| |
| |
| |
|
|
| if __name__ == "__main__": |
| parser = argparse.ArgumentParser(description="DRIFT Ablation Runner") |
| parser.add_argument( |
| "--test", |
| choices=[ |
| "identity_collapse", |
| "scrambled_memory", |
| "reintroduction_curve", |
| "all", |
| ], |
| required=True, |
| ) |
| args = parser.parse_args() |
|
|
| control = ExperimentControl() |
| logger = RunLogger.get_instance() |
| baselines = load_baselines() |
|
|
| |
| drift_session = None |
|
|
| if args.test in ("identity_collapse", "all"): |
| run_identity_collapse(drift_session, control, logger, baselines) |
|
|
| if args.test in ("scrambled_memory", "all"): |
| run_scrambled_memory( |
| drift_session, control, logger, baselines, scramble_mode="timestamps" |
| ) |
|
|
| if args.test in ("reintroduction_curve", "all"): |
| run_reintroduction_curve(drift_session, control, logger, baselines) |
|
|
| logger.close() |
| print( |
| "\n[Ablation Runner] All runs complete. Inspect experiment_log.db before interpreting." |
| ) |
|
|