feat: Phase 2 Memory Core — structured memory with context integration

Memory Core implementation:
- Memory service with 6 types: identity, preference, project, episodic, knowledge, adaptation
- CRUD operations: create (with dedup), get (filtered), update, invalidate, supersede
- Confidence scoring (0.0-1.0) and lifecycle management (active/superseded/invalid)
- Memory API endpoints: POST/GET/PUT/DELETE /memory

Context builder integration (trust precedence per Master Plan):
  1. Trusted Project State (highest trust, 20% budget)
  2. Identity + Preference memories (10% budget)
  3. Retrieved chunks (remaining budget)

Also fixed database.py to use dynamic settings reference for test isolation.
45/45 tests passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-05 09:54:52 -04:00
parent 531c560db7
commit b48f0c95ab
7 changed files with 505 additions and 33 deletions

View File

@@ -1,8 +1,9 @@
"""Context pack assembly: retrieve, rank, budget, format.
Trust precedence (per Master Plan):
1. Trusted Project State → always included first, uses its own budget slice
2. Retrieved chunks → ranked, deduplicated, budget-constrained
1. Trusted Project State → always included first, highest authority
2. Identity + Preference memories → included next
3. Retrieved chunks → ranked, deduplicated, budget-constrained
"""
import time
@@ -11,6 +12,7 @@ from pathlib import Path
from atocore.config import settings
from atocore.context.project_state import format_project_state, get_state
from atocore.memory.service import get_memories_for_context
from atocore.observability.logger import get_logger
from atocore.retrieval.retriever import ChunkResult, retrieve
@@ -23,9 +25,10 @@ SYSTEM_PREFIX = (
"When project state is provided, treat it as the most authoritative source."
)
# Budget allocation (per Master Plan section 9)
# project_state gets up to 20% of budget, retrieval gets the rest
# Budget allocation (per Master Plan section 9):
# identity: 5%, preferences: 5%, project state: 20%, retrieval: 60%+
PROJECT_STATE_BUDGET_RATIO = 0.20
MEMORY_BUDGET_RATIO = 0.10 # 5% identity + 5% preference
# Last built context pack for debug inspection
_last_context_pack: "ContextPack | None" = None
@@ -45,6 +48,8 @@ class ContextPack:
chunks_used: list[ContextChunk] = field(default_factory=list)
project_state_text: str = ""
project_state_chars: int = 0
memory_text: str = ""
memory_chars: int = 0
total_chars: int = 0
budget: int = 0
budget_remaining: int = 0
@@ -64,7 +69,8 @@ def build_context(
Trust precedence applied:
1. Project state is injected first (highest trust)
2. Retrieved chunks fill the remaining budget
2. Identity + preference memories (second trust level)
3. Retrieved chunks fill the remaining budget
"""
global _last_context_pack
start = time.time()
@@ -73,48 +79,48 @@ def build_context(
# 1. Get Trusted Project State (highest precedence)
project_state_text = ""
project_state_chars = 0
state_budget = int(budget * PROJECT_STATE_BUDGET_RATIO)
if project_hint:
state_entries = get_state(project_hint)
if state_entries:
project_state_text = format_project_state(state_entries)
project_state_chars = len(project_state_text)
# If state exceeds its budget, it still gets included (it's highest trust)
# but we log it
if project_state_chars > state_budget:
log.info(
"project_state_exceeds_budget",
state_chars=project_state_chars,
state_budget=state_budget,
)
# 2. Calculate remaining budget for retrieval
retrieval_budget = budget - project_state_chars
# 2. Get identity + preference memories (second precedence)
memory_budget = int(budget * MEMORY_BUDGET_RATIO)
memory_text, memory_chars = get_memories_for_context(
memory_types=["identity", "preference"],
budget=memory_budget,
)
# 3. Retrieve candidates
# 3. Calculate remaining budget for retrieval
retrieval_budget = budget - project_state_chars - memory_chars
# 4. Retrieve candidates
candidates = retrieve(user_prompt, top_k=settings.context_top_k)
# 4. Score and rank
# 5. Score and rank
scored = _rank_chunks(candidates, project_hint)
# 5. Select within remaining budget
# 6. Select within remaining budget
selected = _select_within_budget(scored, max(retrieval_budget, 0))
# 6. Format full context
formatted = _format_full_context(project_state_text, selected)
# 7. Format full context
formatted = _format_full_context(project_state_text, memory_text, selected)
# 7. Build full prompt
# 8. Build full prompt
full_prompt = f"{SYSTEM_PREFIX}\n\n{formatted}\n\n{user_prompt}"
retrieval_chars = sum(c.char_count for c in selected)
total_chars = project_state_chars + retrieval_chars
total_chars = project_state_chars + memory_chars + retrieval_chars
duration_ms = int((time.time() - start) * 1000)
pack = ContextPack(
chunks_used=selected,
project_state_text=project_state_text,
project_state_chars=project_state_chars,
memory_text=memory_text,
memory_chars=memory_chars,
total_chars=total_chars,
budget=budget,
budget_remaining=budget - total_chars,
@@ -131,6 +137,7 @@ def build_context(
"context_built",
chunks_used=len(selected),
project_state_chars=project_state_chars,
memory_chars=memory_chars,
retrieval_chars=retrieval_chars,
total_chars=total_chars,
budget_remaining=budget - total_chars,
@@ -209,17 +216,23 @@ def _select_within_budget(
def _format_full_context(
project_state_text: str,
memory_text: str,
chunks: list[ContextChunk],
) -> str:
"""Format project state + retrieved chunks into full context block."""
"""Format project state + memories + retrieved chunks into full context block."""
parts = []
# Project state first (highest trust)
# 1. Project state first (highest trust)
if project_state_text:
parts.append(project_state_text)
parts.append("")
# Retrieved chunks
# 2. Identity + preference memories (second trust level)
if memory_text:
parts.append(memory_text)
parts.append("")
# 3. Retrieved chunks (lowest trust)
if chunks:
parts.append("--- AtoCore Retrieved Context ---")
for chunk in chunks:
@@ -229,7 +242,7 @@ def _format_full_context(
parts.append(chunk.content)
parts.append("")
parts.append("--- End Context ---")
elif not project_state_text:
elif not project_state_text and not memory_text:
parts.append("--- AtoCore Context ---\nNo relevant context found.\n--- End Context ---")
return "\n".join(parts)
@@ -250,12 +263,14 @@ def _pack_to_dict(pack: ContextPack) -> dict:
"query": pack.query,
"project_hint": pack.project_hint,
"project_state_chars": pack.project_state_chars,
"memory_chars": pack.memory_chars,
"chunks_used": len(pack.chunks_used),
"total_chars": pack.total_chars,
"budget": pack.budget,
"budget_remaining": pack.budget_remaining,
"duration_ms": pack.duration_ms,
"has_project_state": bool(pack.project_state_text),
"has_memories": bool(pack.memory_text),
"chunks": [
{
"source_file": c.source_file,