# CAG Layer Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Add KV-Cache Preloading to Cognithor's memory system via deterministic prefix generation for automatic LLM prefix caching, plus explicit KV-state save/load for native llama-cpp-python.

**Architecture:** New `src/cognithor/memory/cag/` package with content normalizer, cache store, selector, metrics, 2 builders (prefix + native), and manager. Integrates via 2 hooks: Gateway prepares stable prefix, Planner uses it instead of raw core_memory_text.

**Tech Stack:** Python 3.12+, hashlib (sha256), pathlib, asyncio, optional llama-cpp-python

---

## File Map

### New files
```
src/cognithor/memory/cag/__init__.py
src/cognithor/memory/cag/models.py
src/cognithor/memory/cag/content_normalizer.py
src/cognithor/memory/cag/cache_store.py
src/cognithor/memory/cag/selectors.py
src/cognithor/memory/cag/metrics.py
src/cognithor/memory/cag/builders/__init__.py
src/cognithor/memory/cag/builders/base.py
src/cognithor/memory/cag/builders/prefix.py
src/cognithor/memory/cag/builders/native.py
src/cognithor/memory/cag/manager.py
tests/memory/cag/__init__.py
tests/memory/cag/test_content_normalizer.py
tests/memory/cag/test_cache_store.py
tests/memory/cag/test_selectors.py
tests/memory/cag/test_metrics.py
tests/memory/cag/test_prefix_builder.py
tests/memory/cag/test_native_builder.py
tests/memory/cag/test_manager.py
tests/memory/cag/test_integration.py
tests/memory/cag/test_edge_cases.py
```

### Modified files
```
src/cognithor/config.py          — add CAGConfig + field on MemoryConfig
src/cognithor/models.py          — add cag_prefix field to WorkingMemory
src/cognithor/gateway/gateway.py — Hook 1: prepare CAG prefix before Planner
src/cognithor/core/planner.py    — Hook 2: use stable prefix instead of raw core_memory_text
src/cognithor/memory/manager.py  — 3 new public methods
```

---

### Task 1: Models + Package Scaffold

**Files:**
- Create: `src/cognithor/memory/cag/__init__.py`
- Create: `src/cognithor/memory/cag/models.py`
- Create: `src/cognithor/memory/cag/builders/__init__.py`
- Create: `tests/memory/cag/__init__.py`

**Scope:** Package init, all dataclasses (CacheEntry, CAGMetrics, CAGStatus, CAGRefreshReport), builder factory stub.

- [ ] **Step 1:** Create `__init__.py` files for `cag/` and `builders/`
- [ ] **Step 2:** Implement `models.py` with 4 dataclasses from the spec: CacheEntry (frozen), CAGMetrics (mutable), CAGStatus (frozen), CAGRefreshReport (frozen)
- [ ] **Step 3:** Implement `builders/__init__.py` with `get_builder(backend: str) -> CacheBuilder` factory — returns PrefixCacheBuilder for "auto"/"prefix", NativeLlamaCppBuilder for "llamacpp_native", raises ValueError for unknown
- [ ] **Step 4:** Commit: `feat(memory/cag): add data models and package scaffold`

---

### Task 2: Content Normalizer

**Files:**
- Create: `src/cognithor/memory/cag/content_normalizer.py`
- Create: `tests/memory/cag/test_content_normalizer.py`

**Scope:** Deterministic text normalization + sha256 hashing. The core of prefix stability.

- [ ] **Step 1:** Write `test_content_normalizer.py`:
  - `test_strip_bom`: text with `\ufeff` → stripped
  - `test_normalize_line_endings`: `\r\n` and `\r` → `\n`
  - `test_collapse_blank_lines`: multiple blank lines → single
  - `test_strip_trailing_whitespace`: trailing spaces per line removed
  - `test_deterministic`: same input always produces same output
  - `test_different_input_different_hash`: different text → different hash
  - `test_has_changed_true`: stored hash vs modified text → True
  - `test_has_changed_false`: stored hash vs same text → False
  - `test_empty_text`: empty string normalizes to empty, hash is consistent

- [ ] **Step 2:** Implement `ContentNormalizer` with 3 static methods: `normalize(text) -> str`, `compute_hash(normalized_text) -> str`, `has_changed(stored_hash, current_text) -> bool`

- [ ] **Step 3:** Run tests, verify pass
- [ ] **Step 4:** Commit: `feat(memory/cag): add content normalizer with deterministic hashing`

---

### Task 3: Cache Store

**Files:**
- Create: `src/cognithor/memory/cag/cache_store.py`
- Create: `tests/memory/cag/test_cache_store.py`

**Scope:** Disk persistence for CacheEntry as JSON + .txt sidecar. Atomic writes via tmp+rename.

- [ ] **Step 1:** Write `test_cache_store.py`:
  - `test_save_and_load`: save entry, load by cache_id, all fields match
  - `test_load_nonexistent`: returns None
  - `test_delete`: save then delete, load returns None
  - `test_list_entries`: save 3 entries, list returns all 3
  - `test_total_size_bytes`: reflects actual file sizes
  - `test_atomic_write`: file exists only after complete write (check no partial files)
  - `test_txt_sidecar_created`: `.txt` file with normalized text exists alongside `.json`

- [ ] **Step 2:** Implement `CacheStore` with `__init__(cache_dir: Path)`, `save(entry: CacheEntry)`, `load(cache_id: str) -> CacheEntry | None`, `delete(cache_id: str)`, `list_entries() -> list[CacheEntry]`, `total_size_bytes() -> int`. JSON files at `cache_dir/<cache_id>.json`, text at `cache_dir/<cache_id>.txt`. Atomic writes: write to `.tmp`, then `os.replace()`.

- [ ] **Step 3:** Run tests, verify pass
- [ ] **Step 4:** Commit: `feat(memory/cag): add cache store with atomic writes`

---

### Task 4: Selectors

**Files:**
- Create: `src/cognithor/memory/cag/selectors.py`
- Create: `tests/memory/cag/test_selectors.py`

**Scope:** Determine which memory content qualifies for caching based on tier rules.

- [ ] **Step 1:** Write `test_selectors.py`:
  - `test_core_memory_always_selected`: core memory text → selected with cache_id "core_memory"
  - `test_empty_core_memory_skipped`: empty core memory → not selected
  - `test_short_content_skipped`: <50 tokens → not selected
  - `test_returns_cache_candidates`: each candidate has cache_id, content, source_tier, priority

- [ ] **Step 2:** Implement `CAGSelector` with `select(core_memory_text: str) -> list[dict]`. For now: only Core Memory is selected (Semantic/Procedural conditional selection deferred to follow-up). Returns list of dicts with `cache_id`, `content`, `source_tier`, `priority`.

- [ ] **Step 3:** Run tests, verify pass
- [ ] **Step 4:** Commit: `feat(memory/cag): add cache selector`

---

### Task 5: Metrics

**Files:**
- Create: `src/cognithor/memory/cag/metrics.py`
- Create: `tests/memory/cag/test_metrics.py`

**Scope:** Track prefix hit/miss, build count, latency. All local, no external dependencies.

- [ ] **Step 1:** Write `test_metrics.py`:
  - `test_record_hit`: hit count increments
  - `test_record_miss`: miss count increments
  - `test_hit_rate`: hits / (hits + misses)
  - `test_record_build`: build count + build_ms tracked
  - `test_prefix_hash_tracking`: last_prefix_hash updated on hit/miss
  - `test_reset`: all counters back to zero

- [ ] **Step 2:** Implement `CAGMetricsCollector` with `record_hit(prefix_hash: str)`, `record_miss(prefix_hash: str)`, `record_build(duration_ms: float)`, `get_metrics() -> CAGMetrics`, `reset()`.

- [ ] **Step 3:** Run tests, verify pass
- [ ] **Step 4:** Commit: `feat(memory/cag): add metrics collector`

---

### Task 6: Prefix Builder

**Files:**
- Create: `src/cognithor/memory/cag/builders/base.py`
- Create: `src/cognithor/memory/cag/builders/prefix.py`
- Create: `tests/memory/cag/test_prefix_builder.py`

**Scope:** CacheBuilder ABC + PrefixCacheBuilder that generates stable system-message prefix from cached entries. Works with any HTTP-based LLM backend.

- [ ] **Step 1:** Implement `builders/base.py` with `CacheBuilder` ABC: `async prepare_prefix(entries, model_id) -> str`, `async is_available() -> bool`, `supports_native_state() -> bool`

- [ ] **Step 2:** Write `test_prefix_builder.py`:
  - `test_prepare_prefix_format`: entries produce `[CAG:<cache_id>]\n<text>` blocks
  - `test_sorted_by_cache_id`: entries sorted alphabetically for determinism
  - `test_multiple_entries`: 2 entries produce 2 blocks separated by blank line
  - `test_empty_entries`: returns empty string
  - `test_is_available`: always True
  - `test_supports_native_state`: False
  - `test_deterministic`: same entries → same prefix every time

- [ ] **Step 3:** Implement `PrefixCacheBuilder(CacheBuilder)` — `prepare_prefix()` sorts entries by cache_id, formats each as `[CAG:{cache_id}]\n{normalized_text}`, joins with `\n\n`. `is_available()` returns True. `supports_native_state()` returns False.

- [ ] **Step 4:** Run tests, verify pass
- [ ] **Step 5:** Commit: `feat(memory/cag): add prefix cache builder`

---

### Task 7: Native LlamaCpp Builder

**Files:**
- Create: `src/cognithor/memory/cag/builders/native.py`
- Create: `tests/memory/cag/test_native_builder.py`

**Scope:** Explicit KV-state save/load for llama-cpp-python. Fully mocked in tests.

- [ ] **Step 1:** Write `test_native_builder.py`:
  - `test_is_available_with_import`: mock llama_cpp importable → True
  - `test_is_available_without_import`: mock ImportError → False
  - `test_supports_native_state`: True
  - `test_build_state_calls_save`: mock Llama, verify save_state called with path
  - `test_load_state_calls_load`: mock Llama, verify load_state called
  - `test_build_state_no_llama_cpp`: raises ImportError gracefully

- [ ] **Step 2:** Implement `NativeLlamaCppBuilder(CacheBuilder)`:
  - `is_available()`: try import llama_cpp, return True/False
  - `supports_native_state()`: True
  - `prepare_prefix()`: same as PrefixCacheBuilder (text-based)
  - `async build_state(content, model_path, target_path) -> Path`: create Llama, eval content, save_state
  - `async load_state(state_path, model_path) -> Any`: create Llama, load_state, return context

- [ ] **Step 3:** Run tests, verify pass
- [ ] **Step 4:** Commit: `feat(memory/cag): add native llama.cpp builder`

---

### Task 8: CAG Manager

**Files:**
- Create: `src/cognithor/memory/cag/manager.py`
- Create: `tests/memory/cag/test_manager.py`

**Scope:** Orchestrates the full lifecycle: select → normalize → hash → store → build prefix. Auto-rebuild on content change (debounced).

- [ ] **Step 1:** Write `test_manager.py`:
  - `test_build_all_creates_entries`: provide core_memory_text, build_all creates cache entry
  - `test_build_all_skips_unchanged`: same content → skipped in report
  - `test_build_all_rebuilds_changed`: different content → refreshed in report
  - `test_get_stable_prefix_returns_text`: after build, prefix is non-None
  - `test_get_stable_prefix_records_hit`: second call with same hash → hit metric
  - `test_get_stable_prefix_records_miss`: after content change → miss metric
  - `test_invalidate_forces_rebuild`: invalidate + build → refreshed
  - `test_get_status`: returns enabled, backend, entries, metrics
  - `test_is_active_when_enabled`: True when enabled and builder available
  - `test_is_active_when_disabled`: False when config.enabled = False

- [ ] **Step 2:** Implement `CAGManager`:
  ```python
  class CAGManager:
      def __init__(self, config, cache_store, builder, selector, metrics_collector):
          self._config = config
          self._store = cache_store
          self._builder = builder
          self._selector = selector
          self._metrics = metrics_collector
          self._lock = asyncio.Lock()
          self._last_prefix: str | None = None
          self._last_prefix_hash: str = ""
      
      @property
      def is_active(self) -> bool
      async def build_all(self, core_memory_text: str, model_id: str) -> CAGRefreshReport
      async def get_stable_prefix(self, core_memory_text: str, model_id: str) -> str | None
      async def invalidate(self, cache_id: str) -> None
      async def get_status(self) -> CAGStatus
      async def get_metrics(self) -> CAGMetrics
  ```

- [ ] **Step 3:** Run tests, verify pass
- [ ] **Step 4:** Commit: `feat(memory/cag): add CAG manager`

---

### Task 9: Config + WorkingMemory + MemoryManager

**Files:**
- Modify: `src/cognithor/config.py`
- Modify: `src/cognithor/models.py`
- Modify: `src/cognithor/memory/manager.py`

**Scope:** CAGConfig, WorkingMemory.cag_prefix field, 3 new MemoryManager methods.

- [ ] **Step 1:** Add `CAGConfig` to `config.py` after `HierarchicalConfig`:
  ```python
  class CAGConfig(BaseModel):
      enabled: bool = Field(default=False, description="Enable CAG (opt-in)")
      backend: str = Field(default="auto", description="'auto' | 'prefix' | 'llamacpp_native'")
      cache_dir: str = Field(default="~/.cognithor/cag_cache")
      auto_rebuild_on_change: bool = Field(default=True)
      rebuild_debounce_seconds: int = Field(default=30, ge=5, le=300)
  ```
  Add to `MemoryConfig`: `cag: CAGConfig = Field(default_factory=CAGConfig)`

- [ ] **Step 2:** Add to `WorkingMemory` in `models.py`: `cag_prefix: str | None = None`

- [ ] **Step 3:** Add to `MemoryManager.__init__()` (after hierarchical init):
  ```python
  self._cag_manager = None
  if self._mc.cag.enabled:
      try:
          from cognithor.memory.cag.cache_store import CacheStore
          from cognithor.memory.cag.selectors import CAGSelector
          from cognithor.memory.cag.metrics import CAGMetricsCollector
          from cognithor.memory.cag.manager import CAGManager
          from cognithor.memory.cag.builders import get_builder
          
          _cag_dir = Path(self._mc.cag.cache_dir).expanduser()
          _cag_store = CacheStore(_cag_dir)
          _cag_builder = get_builder(self._mc.cag.backend)
          _cag_selector = CAGSelector()
          _cag_metrics = CAGMetricsCollector()
          self._cag_manager = CAGManager(self._mc.cag, _cag_store, _cag_builder, _cag_selector, _cag_metrics)
      except Exception:
          logger.debug("cag_init_failed", exc_info=True)
  ```

- [ ] **Step 4:** Add 3 public methods on MemoryManager:
  ```python
  async def enable_cag(self, backend=None): ...
  async def refresh_cag_cache(self, cache_id=None) -> CAGRefreshReport: ...
  async def get_cag_status(self) -> CAGStatus: ...
  ```

- [ ] **Step 5:** Commit: `feat(memory/cag): add config, WorkingMemory field, MemoryManager methods`

---

### Task 10: Gateway + Planner Hooks

**Files:**
- Modify: `src/cognithor/gateway/gateway.py`
- Modify: `src/cognithor/core/planner.py`
- Create: `tests/memory/cag/test_integration.py`

**Scope:** Hook 1 (Gateway prepares prefix), Hook 2 (Planner uses it). End-to-end integration test.

- [ ] **Step 1:** In `gateway.py`, in `handle_message()` after `wm.core_memory_text` is set (around line 2032), add:
  ```python
  # CAG: prepare stable prefix for LLM prefix caching
  if hasattr(self, '_memory_manager') and self._memory_manager and getattr(self._memory_manager, '_cag_manager', None):
      try:
          _cag_mgr = self._memory_manager._cag_manager
          if _cag_mgr.is_active:
              _model_id = self._config.models.planner.name
              _cag_prefix = await _cag_mgr.get_stable_prefix(wm.core_memory_text, _model_id)
              if _cag_prefix:
                  wm.cag_prefix = _cag_prefix
      except Exception:
          log.debug("cag_prefix_preparation_failed", exc_info=True)
  ```

- [ ] **Step 2:** In `planner.py`, in `_build_messages()` (around line 1003 where `core_memory_text` is injected), change:
  ```python
  # BEFORE:
  if working_memory.core_memory_text:
      messages.append({"role": "system", "content": f"Dein Hintergrund:\n{working_memory.core_memory_text[:500]}"})
  
  # AFTER:
  if working_memory.cag_prefix:
      messages.append({"role": "system", "content": working_memory.cag_prefix})
  elif working_memory.core_memory_text:
      messages.append({"role": "system", "content": f"Dein Hintergrund:\n{working_memory.core_memory_text[:500]}"})
  ```

  Apply this same pattern in ALL places where `core_memory_text` is injected (grep for all occurrences in planner.py, likely 2-3 spots).

- [ ] **Step 3:** Write `test_integration.py`:
  - `test_end_to_end_prefix_generation`: create CAGManager with mock builder, build_all with core memory text, get_stable_prefix returns normalized text
  - `test_prefix_replaces_core_memory`: mock WorkingMemory with cag_prefix set, verify planner uses cag_prefix not core_memory_text
  - `test_fallback_without_cag`: WorkingMemory without cag_prefix, planner uses core_memory_text as before
  - `test_config_disabled_no_cag`: CAGConfig.enabled=False → no prefix generated
  - `test_metrics_tracked`: after multiple calls, metrics show hits/misses

- [ ] **Step 4:** Run tests, verify pass
- [ ] **Step 5:** Run full memory tests: `pytest tests/memory/ tests/test_memory/ -q` — no regressions
- [ ] **Step 6:** Commit: `feat(memory/cag): integrate CAG hooks into Gateway and Planner`

---

### Task 11: Edge Cases

**Files:**
- Create: `tests/memory/cag/test_edge_cases.py`

**Scope:** All 9 edge cases from the spec.

- [ ] **Step 1:** Write 9 test functions:
  - `test_builder_not_available`: mock is_available=False → standard path, no crash
  - `test_model_switch`: change model_id between calls → prefix unchanged (content same)
  - `test_disk_full`: mock OSError on save → caught by manager, fallback to standard
  - `test_corrupt_cache_file`: write invalid JSON → load returns None, rebuild succeeds
  - `test_concurrent_rebuild`: asyncio.gather two build_all calls → no race (AsyncLock)
  - `test_empty_content`: <50 tokens → skip, log info
  - `test_core_memory_changed`: different text → CAGRefreshReport shows refreshed
  - `test_schema_version_mismatch`: entry with wrong schema_version → rebuild
  - `test_native_state_wrong_model`: model_id mismatch in metadata → refuse load

- [ ] **Step 2:** Run tests, verify pass
- [ ] **Step 3:** Commit: `test(memory/cag): add all 9 edge case tests`

---

### Task 12: Final Verification

**Files:** All from Tasks 1-11

- [ ] **Step 1:** `ruff format src/cognithor/memory/cag/ tests/memory/cag/`
- [ ] **Step 2:** `ruff check src/ tests/ --select=F821,F811 --no-fix` — all pass
- [ ] **Step 3:** `pytest tests/memory/cag/ -v` — ALL PASS
- [ ] **Step 4:** `pytest tests/ -x -q --tb=line` — no regressions (first 2000 tests)
- [ ] **Step 5:** `python -c "from cognithor.memory.cag.manager import CAGManager; print('OK')"`
- [ ] **Step 6:** `python -c "from cognithor.config import JarvisConfig; c = JarvisConfig(); print('cag enabled:', c.memory.cag.enabled)"`
- [ ] **Step 7:** Commit: `feat(memory/cag): complete CAG Layer — KV-Cache Preloading`
