memory — i tre livelli operativiIl doc di fondamenti Neuroni+Memoria ha introdotto i tre livelli (immediata, media, lunga) come metafora. Qui li traduciamo in strutture dati concrete: RAM vs SQLite vs markdown+embeddings. Applichiamo il vocabolario CoALA (working / episodic / semantic) per connetterci alla letteratura.
neuron.html): stessa famiglia ma doc dedicato.| Termine narrativo (fondamenti) | Termine CoALA standard | Storage concreto |
|---|---|---|
| Immediata | Working memory | RAM, dentro l'ExecutionTrace del turno corrente. |
| Media | Episodic memory | SQLite con FTS5 (workspace/memory/episodic.db). |
| Lunga — fatti | Semantic memory | MEMORY.md + vector index su workspace/memory/embeddings.db. |
| Lunga — Costituzione + identità | Core memory (Letta pattern) | Sempre in prompt, da SOUL/IDENTITY/USER/AGENTS/MEMORY-core. |
| (non coperto qui) | Procedural memory | Skill library = neuroni (workspace/neurons/). |
Vive dentro la ExecutionTrace del turno corrente (agent_runtime §5).
Contiene: tutto il loop ReAct — Thought, ActionStep, risultati intermedi,
planning inline.
| Proprietà | Valore |
|---|---|
| Storage | In-memory (dataclass) per la durata del turno + JSONL append-only su chiusura |
| Durata | Minuti (un turno completo) |
| Persistenza post-turno | Scritta in .audit/YYYY-MM.jsonl, leggibile ma non più iniettata in prompt |
| Dimensione tipica | 1-10 KB per turno |
| Iniezione prompt | Blocco ⑥ (cronologia turno) in agent_runtime §3 |
Non c'è un WorkingMemory store separato: è letteralmente l'ExecutionTrace. Questa unificazione è il punto forte architetturale (vedi agent_runtime §5: "una sola fonte di verità").
Contiene le sessioni passate recenti: l'agente "ricorda" che ieri Roberto ha chiesto di scaricare il rapporto di marzo. È la memoria conversazionale.
-- workspace/memory/episodic.db
CREATE TABLE episodes (
episode_id TEXT PRIMARY KEY,
trace_id TEXT NOT NULL,
session_id TEXT NOT NULL,
sender TEXT NOT NULL,
started_at DATETIME NOT NULL,
user_message TEXT NOT NULL,
final_response TEXT,
outcome TEXT NOT NULL,
summary TEXT, -- riassunto sintetico (100-300 token)
importance REAL DEFAULT 0.5 -- per reflection
);
CREATE VIRTUAL TABLE episodes_fts USING fts5(
user_message, final_response, summary,
content='episodes', content_rowid='rowid'
);
CREATE INDEX idx_episodes_sender_time ON episodes(sender, started_at DESC);
local-fast, purpose compress) in un summary di 100-300 token.importance = 0.3·novelty + 0.3·user_engagement + 0.4·outcome_qualityimportance < 0.3 e più vecchi di 60 giorni vengono archiviati (spostati in .audit/, non eliminati, ma fuori dall'index FTS).sender, ordinato per importance·recency.| Periodo | Episodes attesi | Size DB |
|---|---|---|
| 1 giorno (uso tipico) | 20-60 | ~200-500 KB |
| 1 mese | 600-1800 | ~6-15 MB |
| 1 anno (con archiviazione) | 3000-5000 attivi + archivio | ~30-50 MB + archivi |
La memoria episodic è indicizzata per sender, non per
session_id. Motivazione: una conversazione può spezzarsi in più
sessioni (idle > 30 min, cambio di autonomy), ma l'identità dell'utente è la
stessa. session_id vive nella riga come metadato utile al debug,
non come chiave di retrieval. La regola canonica del binding è fissata in
gateway §4, sotto "Binding con la memoria".
Due sotto-parti con trattamento diverso:
SOUL.md (Costituzione) + IDENTITY.md + USER.md + AGENTS.md + MEMORY.md core slice (≤ 2 KB).*_propose + approvazione.
La parte non-core di MEMORY.md + fatti promossi da episodic via reflection.
workspace/MEMORY.md # fonte di verità human-readable
workspace/memory/embeddings.db # SQLite con vettori
-- struttura
CREATE TABLE semantic_facts (
fact_id TEXT PRIMARY KEY,
text TEXT NOT NULL,
embedding BLOB NOT NULL, -- 768 dim float32
source TEXT, -- "manual" | "reflection:trace_id"
created_at DATETIME NOT NULL,
importance REAL DEFAULT 0.5,
access_count INTEGER DEFAULT 0,
last_access DATETIME
);
Gli embedding sono calcolati con supra_embed (vedi tool).
Il rebuild dell'index si fa a ogni modifica di MEMORY.md (hot-reload) o
quando reflection promuove nuovi fatti.
Un fatto della semantic può essere proposto per la core slice se:
Reflection è il nome consolidato (da Generative Agents 2023) del meccanismo "distilla gli episodes ricorrenti in fatti stabili". È un job periodico non interattivo.
count(episodes) - count(episodes_riflesse_già) > 20.myclaw memory reflect.summarize-rich): "questi 5 episodes parlano della stessa cosa. Qual è il fatto sottostante?"FactProposal.MEMORY.md (sezione "Aggiunti da reflection YYYY-MM-DD") + indicizzati.reflected=true (non rientrano in reflection futura).Per ogni turno, al momento di costruire il prompt (agent_runtime §3), il runtime interroga la memory per:
score(fact, query) =
w1 · cosine_similarity(emb(fact), emb(query)) # relevance
+ w2 · exp(-decay · days_since_last_access) # recency
+ w3 · importance(fact) # salience
+ w4 · log(1 + access_count) # usage reinforcement
# Default weights (tunable)
w1 = 0.50 # relevance domina
w2 = 0.20
w3 = 0.20
w4 = 0.10
w1=0.5, w2=0.2, w3=0.2, w4=0.1 come
default. Gli episodi passati con access alto (rilevati spesso) sono favoriti —
è il rinforzo hebbiano applicato al retrieval. Se in pratica il retrieval
produce troppa rumore, riduciamo w4.
| Livello | Blocco prompt | Volume tipico | Caching |
|---|---|---|---|
| Core | ② Identity nucleus | ~3-5 KB | cached ever-present |
| Semantic retrieved | ④ Memoria lunga retrieved | ~1-2 KB | per-session (cambia per query) |
| Episodic | ⑤ Memoria media (session digest) | ~1-2 KB | per-session |
| Working | ⑥ Cronologia turno | variabile 0-10 KB | mai cached |
from typing import Protocol, Literal
from dataclasses import dataclass
@dataclass
class Episode:
episode_id: str
trace_id: UUID
session_id: str
sender: str
started_at: datetime
user_message: str
final_response: str | None
outcome: str
summary: str | None
importance: float
@dataclass
class SemanticFact:
fact_id: str
text: str
source: Literal["manual", "reflection"]
created_at: datetime
importance: float
access_count: int
@dataclass
class MemoryBundle:
"""Tutto ciò che serve al runtime per costruire i blocchi ②④⑤ del prompt.
Invariante: core_text non è mai stringa vuota. Vedi ensure_core()."""
core_text: str # sempre in prompt, mai vuoto
semantic_retrieved: list[SemanticFact]
episodic_recent: list[Episode] # ultime N per sender
episodic_relevant: list[Episode] # top-K per query match
class WorkspaceMissingError(Exception):
"""Sollevato da Memory.ensure_core() se SOUL.md / IDENTITY.md mancano o
il workspace è corrotto. Il runtime intercetta e rifiuta la sessione con
un messaggio attuabile (vedi agent_runtime §4)."""
class Memory(Protocol):
async def get_bundle_for_turn(
self,
sender: str,
user_message: str,
autonomy: str,
) -> MemoryBundle:
"""Richiede che ensure_core() abbia avuto successo all'avvio. Se
chiamato con workspace inconsistente, solleva WorkspaceMissingError
invece di ritornare un bundle degradato."""
...
async def ensure_core(self) -> str:
"""
Carica e valida la core memory (SOUL + IDENTITY + USER + AGENTS +
MEMORY.md core slice). Chiamato una volta al boot del gateway.
Policy di fallback:
1. SOUL.md mancante → WorkspaceMissingError ("workspace non pareggiato").
2. SOUL.md presente ma altri file opzionali mancanti → segnaposto
minimo ("(nessuna identità utente configurata)") + log warning.
3. Nessun silenzioso: il boot fallisce invece di partire senza core.
Ritorna il testo finale usato come core_text.
"""
...
async def index_episode(self, trace: ExecutionTrace) -> Episode:
"""Al termine di una trace, indicizza come episode."""
...
async def reflect(
self,
since: datetime | None = None,
) -> list["FactProposal"]:
"""Esegue reflection: ritorna proposte da approvare."""
...
async def apply_approved_facts(
self,
approved_ids: list[str],
) -> None:
"""Aggiunge fatti approvati a MEMORY.md + embeddings.db."""
...
async def access(self, fact_id: str) -> None:
"""Incrementa access_count + last_access di un fatto (hebbian reinforce)."""
...
| Alternativa | Perché scartata |
|---|---|
| Neo4j / graph database | Overhead infrastrutturale. SQLite + FTS + vettori bastano per uso domestico per anni. |
| HippoRAG personalized PageRank | In valutazione (Letteratura §5 #11), rimandato fino a quando memoria semantica supera ~1000 fatti. |
| Memoria totalmente automatica (no approval) | Memory poisoning è rischio #1 degli agenti con memoria (Greshake 2023). Approval è parte del design. |
| Un solo "memory store" senza 3 livelli | Complessità di retrieval + costi di token. La separazione per durata/funzione è la chiave di CoALA. |
| Mem0 come layer esterno | In valutazione (Letteratura #6), bene per future se serve conflict resolution sofisticata. Partiamo semplici. |
| Fine-tuning invece di RAG | Costoso, fragile, richiede infrastruttura. RAG è il pattern giusto per home agent. |
| Invariante | Test |
|---|---|
| Trace chiusa → episode indicizzato | Dopo ExecutionTrace con outcome=success, SELECT COUNT(*) FROM episodes WHERE trace_id=? = 1. |
| Summary scatta sopra 20 step | Trace con 25 step → episode ha summary non null, lunghezza 100-300 token. |
| Importance normalizzato | Per ogni episode: 0 ≤ importance ≤ 1. |
| Retrieval filtra per sender | get_bundle_for_turn(sender="X", ...) non include episodes di sender "Y". |
| Core sempre presente | MemoryBundle.core_text mai vuoto (SOUL.md obbligatorio da workspace §4). |
| Semantic budget rispettato | len(bundle.semantic_retrieved) * avg_tokens < 2 KB garantito. |
| Reflection produce proposte, non applica | reflect() ritorna list[FactProposal]; MEMORY.md invariato finché apply_approved_facts() non chiamato. |
| Access incrementa counter | access(fact_id) → access_count += 1, last_access updated. |
| Retention archivia vecchi | Dopo 60gg + importance < 0.3 + job retention → episode non in FTS ma visibile in .audit/archived/. |
| Embedding rebuild su MEMORY edit | Edit manuale di MEMORY.md → index embeddings ricostruito entro 1s (hot-reload). |
| No promozione senza approvazione | FactProposal generata → MEMORY.md invariato fino a apply_approved_facts([id]). |
| Riferimento | Cosa abbiamo preso |
|---|---|
| CoALA (Sumers et al. 2023) | Vocabolario working/episodic/semantic/procedural. |
| MemGPT / Letta (Packer et al. 2023) | Distinzione core (in prompt) vs semantic (retrievable). |
| Generative Agents (Park et al. 2023) | Reflection, formula recency × importance × relevance. |
| MemoryBank (Zhong et al. 2023) | Curva Ebbinghaus per decay (reimpiegata nella formula §7). |
| ACT-R activation | Rinforzo via access_count (hebbian; §7 w4). |
| Greshake et al. 2023 | Memory poisoning: approval-first design (§6). |
| SQLite FTS5 | Full-text search index per episodic. |
| Workspace §5 | Distinzione core-slice vs resto di MEMORY.md. |
| Agent_runtime §3 | Blocchi ② ④ ⑤ ⑥ del prompt. |
myclaw — memory microprogettazione v1.0 — 2026-04-22
Terzo doc di fase 2. Prossimo: observability.html.