runtime/mnestoma.py, esercitati da 20 test module + 6 cluster + integrazione end-to-end con agent_runtime. Schema del cap. 4 e soglie del cap. 6 verbatim, ager di base implementato come funzione invocabile. L'ager notturno (apply_executor_ager, introvertiva_propose, introvertiva_apply) e' gated dal segnale di interazione utente: un Metnos in vacanza non deprezza executor per inattività calendariale. L'introvertiva ha filtri qualità deterministici: vocab validator sui proposed_name, skip di flow args (entries, from_step, results) e di valori template {{stepN.field}}, soglia min_uses a 10. Volume proposte a 6 con qualità attesa alta.
events.mnest_id con ON DELETE CASCADE per supportare il purge dei proto-mnest senza orfani; (b) DB path default workspace/.mnestoma/mnest.sqlite, override via env MNESTOMA_DB_PATH per test isolati; (c) le query interne (top_k, by_tag, walk) vanno dirette sulla tabella mnests filtrando per stato; la vista v_mnestoma resta come da microdesign per consultazione esterna. Snapshot mensile e archiviazione decaying ancora non automatizzati (richiedono lo scheduler builtin).mnestoma — il grafo emergente di tutti i mnestQuesto documento definisce il mnestoma: la struttura dati che raccoglie l'insieme di tutti i mnest attivi e ne governa il ciclo di vita. Tecnicamente è un grafo orientato pesato persistito in SQLite; ontologicamente è la memoria relazionale di Metnos — ciò che ricorda di aver fatto insieme.
Il documento copre:
Non copre, e demanda altrove:
Il mnestoma è un grafo orientato pesato in cui:
Il mnestoma emerge. Nessuno lo disegna a priori: nasce vuoto al primo avvio e si forma turno dopo turno, mentre il gateway osserva quali executor passano output a quali altri. Le forme che prende riflettono come il sistema viene usato: un mnestoma di un utente «archivio fatture» ha cluster diversi da quello di un utente «rassegna stampa quotidiana», anche se il seed degli executor è lo stesso.
La parola «memoria» in un agente AI ha troppi significati. Vale la pena dire chiaramente cosa il mnestoma non è:
| Cosa il mnestoma NON è | Dov'è quel concetto, allora |
|---|---|
| La memoria conversazionale dell'LLM (turni recenti, contesto in token). | Nel working memory dell'execution trace. |
| La memoria episodica delle conversazioni passate (cosa Roberto disse ieri). | Nella episodic memory, indicizzata per sender. |
| La memoria semantica dei fatti su Roberto e il suo mondo («il cane si chiama Fido»). | Nella semantic memory, estratta via reflection e approvata dall'utente. |
| L'audit log delle azioni svolte. | In workspace/.audit/, append-only, JSONL. |
| Il workspace dei file dell'utente (note, fatture, foto). | In workspace/, gestito separatamente. |
| La definizione delle capacità (i singoli executor). | In workspace/executors/, file per file. |
Il mnestoma è la relazione osservata fra executor: chi ha lavorato con chi, quanto spesso, quanto recentemente. Niente di più.
Per capire cosa fa davvero un mnestoma, conviene smettere per un istante di pensarlo come una tabella SQLite e cominciare a pensarlo come un ambiente vivo: un brodo in cui piccole particelle fluttuano, si incontrano, si attraggono, si combinano. Da quella dinamica emergono strutture più complesse, secondo un processo di aggregazione progressiva che, di tanto in tanto, ricorda ciò che si osserva in altri sistemi naturali fatti di tanti elementi che interagiscono fra loro.
Quel che segue è una metafora, non un'identità. La uso con moderazione: serve a far vedere tre cose che il mnestoma fa davvero — aggregare ciò che si somiglia, riconoscere ciò che manca, far nascere nuove strutture quando il riconoscimento incontra il materiale giusto — senza dover rivendicare un parallelo letterale che non c'è.
All'inizio, il brodo è vuoto. Poi cominciano a comparire le particelle: piccole tracce di vita che il sistema raccoglie mentre l'utente lo usa. Sono di poche specie diverse, ognuna con la sua forma e il suo ruolo:
Ogni particella ha una storia: un mnest è il ricordo che «A è servito a B», un executor è un piccolo programma che sa fare una cosa precisa, un fast-path è una scorciatoia che il sistema ha appreso (la richiesta canonica «X si risolve così»), un proto-mnest è un mnest a metà: src reale, dst desiderato ma non ancora esistente. Si vede subito nel disegno che il proto-mnest ha una forma diversa, una specie di recettore aperto: c'è uno slot vuoto che aspetta di essere riempito.
Nel brodo, niente è davvero a caso. Le particelle si
attraggono fra loro in base alla somiglianza semantica,
misurata con il modello multilingua BGE-M3. Quando l'utente fa
ripetutamente la stessa cosa — tre richieste «scarica
questa pagina» che diventano sempre get_urls
seguito da describe_entries — i mnest osservati
nei tre turn riferiscono alla stessa coppia di executor. Quei mnest
formano un cluster cross-turn: un'evidenza che «questa
pipeline funziona».
A questo punto succede qualcosa di interessante: il cluster non resta una nuvola sfocata, cristallizza. Diventa un fastpath, un percorso preformato che descrive la sequenza in modo compatto e immediatamente eseguibile. Da quel momento il PLANNER non ha più bisogno di chiamare il modello linguistico per ripensare la pipeline da capo: legge il fastpath e lo usa direttamente. La figura sotto mostra le quattro fasi — osservazione, cluster, cristallizzazione, riuso — in sequenza.
Il fastpath che ne emerge non è un'astrazione: è proprio quella sequenza concreta, memorizzata, pronta all'uso. Il risparmio è tangibile — la chiamata al pianificatore LLM (circa dodici secondi) viene saltata interamente, e l'utente riceve la risposta in meno di un secondo. Il cluster che l'ha generato resta nel mnestoma come traccia di come si è arrivati al fastpath; il fastpath stesso vive nel suo registro separato (vedi fast_path.html).
A volte, mentre il sistema lavora, accade qualcosa di più
particolare: il pianificatore vorrebbe chiamare un executor che
non esiste. Magari l'utente ha chiesto qualcosa di nuovo,
e nella catena ideale il passo finale dovrebbe essere un
compose_report che non è mai stato scritto.
Quello che si registra, in quel momento, è un
proto-mnest: una traccia con il src
conosciuto e il dst appena un nome, non ancora un programma.
Possiamo vederlo come un recettore: una forma con una lacuna vuota in cui il dst dovrebbe entrare. La lacuna non è un dettaglio grafico, è la firma desiderata — dichiara cosa servirebbe per chiudere la forma. Il recettore aspetta: rimane stabile finché nessuno lo riempie, e diventa più visibile man mano che si presenta nello stesso modo, turno dopo turno.
Da qualche parte, nel brodo, lavorano le funzioni introspettive: i mestieri silenziosi di synt in modo introvertivo e del modulo introvertiva. Non hanno una funzione finale propria, servono a far accadere altre cose. La loro caratteristica è saper riconoscere i recettori: leggere la lacuna aperta e capire quale dst la chiuderebbe.
Quando una funzione introspettiva incontra un proto-mnest che si è visto più volte (la soglia è tre osservazioni ricorrenti, ), si avvicina, lo analizza, ne legge la signature desiderata, e poi propone: emette un candidato di sintesi. La proposta dice: «questo recettore esiste, ricorre, ed ecco un possibile dst che lo chiuderebbe».
C'è un altro fenomeno interessante che accade nel brodo: due fast-path che vivono adiacenti, e si presentano spesso nello stesso turno, finiscono per agganciarsi a formare un fast-path più lungo.
Pensiamo a un turno tipo: «scaricami questa pagina» attiva un fast-path L1 single-tool; subito dopo, «dimmi cosa dice» attiva un altro fast-path L1. Il sistema osserva la successione, registra la sequenza, e quando si ripete tre volte la promuove a fast-path L2 multi-step. Da quel momento la coppia «scarica + descrivi» è una sola sequenza riconosciuta.
La sequenza non si ferma a due. Tre fast-path adiacenti possono
formare un trimero, e così via. Il sistema conserva tutta la
catena, valuta quanto spesso si ripete e, oltre una soglia (50
utilizzi per default), un job notturno chiamato
multi_tool_promote presenta la pipeline a synt come
proto-mnest: «ecco una sequenza che vale la pena unificare in
un solo executor».
A questo punto conviene fermarsi e osservare che synt riceve proto-mnest da due strade diverse, indipendenti fra loro, che convergono nello stesso punto.
La via diretta: le funzioni introspettive osservano il mnestoma e individuano i recettori che ricorrono — quei proto-mnest che si presentano nello stesso modo turno dopo turno, soglia tre osservazioni ricorrenti. Quando ne trovano uno sufficientemente stabile, propongono direttamente un executor che chiuderebbe la lacuna. Non serve passare da un fast-path: l'analisi del singolo recettore basta. È il caso classico, e storicamente il primo che synt ha imparato a gestire.
La via indiretta: una pipeline di fast-path L2 che
ha accumulato abbastanza utilizzi (cinquanta, per default) viene
promossa dal job notturno multi_tool_promote a
proto-mnest. Questa volta il proto-mnest non nasce da una richiesta
fallita ma da una sequenza che ha funzionato tante volte: la
signature desiderata descrive un executor unificato che incorpora
tutta la catena di chiamate.
Le due strade conducono allo stesso ingresso. Synt riceve il proto-mnest, legge la signature, e la traduce in una struttura completa: esegue le sue cinque fasi (naming, signature, tests, description, code), produce un nuovo executor, lo firma con ed25519, lo iscrive al catalog. Il recettore non è più aperto: il dst esiste, il proto-mnest viene promosso a mnest attivo. La nuova capacità è unica, riusabile, e — che venga dall'analisi diretta di un recettore o dall'aggregazione di una pipeline — entra nel pool degli executor come tutti gli altri.
describe_urls, la pipeline aggregata
[get_urls, describe_entries] che l'aveva originata
viene immediatamente demoted: la sua forma si dissolve nel
brodo, e ogni nuova richiesta passa direttamente attraverso
l'executor monolitico.
Da fast-path candidato a non-creazione: simmetricamente, se
mentre il sistema sta per registrare un nuovo fast-path scopre che
l'executor che farebbe la stessa cosa esiste già nel catalog,
il fast-path non viene creato: non c'è nulla da memoizzare se
la capacità unificata è già disponibile.
Il runtime applica la regola al volo nel matcher (auto-demote) e nel
recording (skip silenzioso); il PLANNER la conosce dal suo prompt e
non emette mai la sequenza doppia se l'executor unificato è
nel pool. La forma ha la priorità sulla scorciatoia, in
ingresso e in uscita.
Quel che emerge, guardandolo da lontano, è un sistema in equilibrio dinamico: chiuso ma vivo, dove ogni componente partecipa a un ciclo. Le particelle nascono dall'osservazione del turno, si attraggono per somiglianza, formano cluster, alcuni cluster diventano fast-path, alcuni fast-path si aggregano in pipeline, le pipeline mature diventano proto-mnest che chiedono sintesi, la sintesi produce nuovi executor, i nuovi executor generano altri mnest e tutto ricomincia.
Allo stesso tempo c'è un ciclo dell'oblio: l'ager notturno (vedi capitolo 6) fa decadere le particelle inutilizzate, riduce il loro peso, le sposta in stato decaying, alla fine le archivia. Le cose che non si usano più spariscono. Il brodo si pulisce da solo.
Tutto questo accade dentro un singolo file SQLite di pochi MB. La metafora del brodo non è un trucco didattico: descrive abbastanza fedelmente cosa fa il codice. Capire questo livello rende ovvi i capitoli che seguono — lo schema dati, le operazioni, l'ager — che si leggono come la spiegazione tecnica di un processo che hai già visto in moto.
Il mnestoma vive in workspace/.mnestoma/mnest.sqlite, un
singolo file SQLite. La scelta di SQLite è deliberata: zero
servizi da avviare, zero dipendenze esterne, backup banale (copia del
file), affidabilità provata.
executors
Replica leggera del manifest, in lettura-veloce per le query del
gateway. Il manifest canonico resta sempre workspace/executors/<nome>/manifest.toml;
questa tabella è un indice.
CREATE TABLE executors (
name TEXT NOT NULL,
version TEXT NOT NULL,
state TEXT NOT NULL, -- seed | active | quarantine | archived
loaded_at TEXT NOT NULL, -- ISO 8601
manifest_hash TEXT NOT NULL, -- blake3
PRIMARY KEY (name, version)
);
CREATE INDEX idx_executors_state ON executors(state);
mnestsCREATE TABLE mnests (
id TEXT PRIMARY KEY, -- ULID
src_executor TEXT NOT NULL,
src_version TEXT NOT NULL,
dst_executor TEXT NOT NULL, -- nome canonico O nome desiderato (proto)
dst_version TEXT, -- NULL per proto
weight REAL NOT NULL CHECK (weight BETWEEN 0 AND 1),
uses INTEGER NOT NULL CHECK (uses >= 1),
ts_first TEXT NOT NULL,
ts_last TEXT NOT NULL,
decay_lambda REAL NOT NULL DEFAULT 0.018,
state TEXT NOT NULL, -- proto | active | decaying | superseded
tags TEXT, -- JSON array
desired_sig TEXT, -- JSON, solo per proto
CHECK (ts_last >= ts_first),
UNIQUE (src_executor, src_version, dst_executor, dst_version, state)
);
CREATE INDEX idx_mnests_dst ON mnests(dst_executor);
CREATE INDEX idx_mnests_src ON mnests(src_executor);
CREATE INDEX idx_mnests_weight ON mnests(weight DESC);
CREATE INDEX idx_mnests_state ON mnests(state);
eventsAppend-only, registra ogni rinforzo o decadimento applicato. Permette di ricostruire la traiettoria del peso di un mnest e di ricalcolare posteriormente in caso di cambio della formula.
CREATE TABLE events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
mnest_id TEXT NOT NULL,
ts TEXT NOT NULL,
kind TEXT NOT NULL, -- reinforce | decay | state_change
delta REAL, -- variazione del peso (NULL per state_change)
new_state TEXT, -- solo per state_change
reason TEXT, -- es. "ager nightly", "synt approved"
FOREIGN KEY (mnest_id) REFERENCES mnests(id)
);
CREATE INDEX idx_events_mnest ON events(mnest_id);
CREATE INDEX idx_events_ts ON events(ts);
canonical_query_log
Tabella di registrazione delle richieste utente in forma lemma, usata
dal fast-path introvertivo (L1
single-tool, ). Ogni volta che il pianificatore decide il
primo executor di un turno, una riga viene inserita o rinforzata qui:
canonical query (forma minuscola condensata), tool scelto, valori
osservati degli argomenti. Dopo tre passaggi consolidati, il
canonical_matcher usa BGE-M3 cosine per matchare nuove
richieste contro questo log e saltare il pianificatore.
CREATE TABLE canonical_query_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
canonical_query TEXT NOT NULL,
tool_name TEXT NOT NULL,
args_shape TEXT NOT NULL, -- template placeholder (back-compat)
args_observed TEXT, -- VALORI args reali (Fase 14 v5)
uses INTEGER NOT NULL DEFAULT 1,
ok_count INTEGER NOT NULL DEFAULT 0,
fail_count INTEGER NOT NULL DEFAULT 0,
ts_first TEXT NOT NULL,
ts_last TEXT NOT NULL,
state TEXT NOT NULL DEFAULT 'candidate',
UNIQUE(canonical_query, tool_name, args_shape)
);
La colonna args_observed (aggiunta via
migrazione idempotente) memorizza i valori concreti degli argomenti
osservati al primo passaggio del pianificatore. Quando una nuova
richiesta matcha la riga, l'estrattore di argomenti
(args_extractor) li
riusa direttamente, evitando di rifare l'estrazione regex o di
chiamare un LLM. La sequenza multi-step analoga vive in un sqlite
separato (multi_tool_paths.sqlite); vedi
fast_path.html per il dettaglio
completo.
v_mnestoma
Il grafo «vivo» per le query di lettura. Esclude
superseded e mostra active + proto:
CREATE VIEW v_mnestoma AS
SELECT id, src_executor, src_version, dst_executor, dst_version,
weight, uses, ts_last, state, tags, desired_sig
FROM mnests
WHERE state IN ('active', 'proto');
record_passingLa sola operazione di scrittura in fase di turno. Atomica per coppia. Pseudocodice:
def record_passing(src, dst, turn_ctx) -> mnest_id:
with conn.transaction:
if dst.exists:
row = SELECT * FROM mnests
WHERE src_executor = src.name AND src_version = src.version
AND dst_executor = dst.name AND dst_version = dst.version
AND state = 'active'
if row:
w = decay(row.weight, now - row.ts_last, row.decay_lambda) + REINFORCE_DELTA
UPDATE mnests SET weight = clamp01(w), uses = uses + 1, ts_last = now
INSERT INTO events (mnest_id, ts, kind, delta, reason)
VALUES (row.id, now, 'reinforce', REINFORCE_DELTA, turn_ctx.id)
return row.id
else:
id = ulid
INSERT INTO mnests (id, src_*, dst_*, weight, uses, ts_first, ts_last, state, tags)
VALUES (id,..., BOOTSTRAP_WEIGHT, 1, now, now, 'active', tags)
INSERT INTO events (mnest_id, ts, kind, delta, reason)
VALUES (id, now, 'reinforce', BOOTSTRAP_WEIGHT, turn_ctx.id)
return id
else:
# proto-mnest
id = ulid
desired = build_desired_signature(turn_ctx)
INSERT INTO mnests (..., dst_executor=dst.desired_name, dst_version=NULL,..., state='proto', desired_sig=desired)
return id
| Query | Uso |
|---|---|
top_k_outgoing(executor, k) | Dato un executor, restituisce i k mnest uscenti più pesanti. |
top_k_incoming(executor, k) | Dato un executor, restituisce i k mnest entranti più pesanti. |
walk(start, max_depth) | Visita BFS pesata dal nodo di partenza, fino a una profondità massima. Usato dal gateway per costruire il context d'esecuzione. |
recurring_protos(min_uses, since) | Lista dei proto-mnest che hanno superato una soglia di ricorrenza. Innesco del synt. |
by_tag(tag) | Tutti i mnest con un tag dato. Usato dall'osservabilità e dall'ager per cluster. |
decaying | Mnest in stato decaying. Usato dall'ager per la propost à di archiviazione. |
Le transizioni di stato sono fatte da componenti specifici (ager, synt,
gateway in caso di quarantena di un executor). Tutte sono accompagnate
da una riga events con kind = 'state_change'
e una reason obbligatoria; questo è il principio di
reversibilità (cap. 14 dell'Architettura): non si cambia stato
senza traccia del perché.
L'ager è il processo che applica il decadimento e segnala le transizioni di stato che il puro tempo richiede. Gira una volta al giorno, in finestra notturna (default: 04:00 ora locale), in una transazione lunga ma a basso impatto:
active, calcola weight_new = weight_old · exp(-λ · Δt) dove Δt è il tempo trascorso dall'ultimo evento. Aggiorna il peso e scrive una riga events di tipo decay.decay weight passa ad decaying; lo segnala nel canale di proposte al gateway.decaying che è sotto archive weight e con ts_last > 90 giorni entra nella lista delle proposte di archiviazione mostrate a Roberto.min proto weight è eliminato (anti-rumore).uses >= SYNTH_TRIGGER_USES e weight >= SYNTH_TRIGGER_WEIGHT; li segnala al synt come candidati per la sintesi.workspace/.mnestoma/snapshots/YYYY-MM.sqlite.L'ager è un esempio puro di componente che fa solo manutenzione: non modifica la struttura dei mnest, non crea executor, non parla con l'utente. Propone; chi decide è il synt + Roberto, oppure la policy di archiviazione che il gateway applica nel turno successivo.
Il primo dell'mese (UTC), l'ager produce uno snapshot del file vivo:
workspace/.mnestoma/snapshots/2026-04.sqlite # snapshot del mese
workspace/.mnestoma/snapshots/2026-03.sqlite.zst # vecchio, compresso
workspace/.mnestoma/snapshots/2026-02.sqlite.zst...
Lo snapshot serve a tre cose: rollback in caso di danneggiamento del
file vivo, analisi storica del grafo, traccia per chi vuole capire
come Metnos «si è formato» nel tempo. Snapshot
oltre 12 mesi vengono compressi in .zst ma mai
eliminati: la storia è un dato di prim'ordine.
Il file vivo si manterrebbe in piedi anche tutto pieno, ma per prestazioni e per pulizia conviene una potatura periodica:
superseded e archived vengono spostati alla tabella mnests_archive dopo 90 giorni (sempre nello stesso file SQLite);events viene partizionata per anno (tabelle events_2026, events_2027,...). Solo l'anno corrente è nello schema vivo; le precedenti vengono spostate in un secondo file SQLite events_history.sqlite;VACUUM di SQLite gira mensilmente subito dopo lo snapshot.
Quando un executor passa a archived, i suoi mnest passano
a superseded con reason = 'executor archived';
restano leggibili, sono esclusi dalla vista v_mnestoma,
e dopo 90 giorni vanno alla tabella di archivio.
Il termine ha due forme normative:
La doppia forma è intenzionale e va mantenuta. Tradurre mnestoma con mnestome in EN dà al lettore inglese il riflesso giusto (il suffisso biologico evoca un grafo emergente, come il microbiome). Tradurre mnestome con mnestoma in IT segue la regola fonetica italiana che evita suffissi sentiti come incongrui (mnestome in italiano sembrerebbe un imperativo verbale, «tu memorizzati!»).
Al primo avvio, il mnestoma è vuoto. La domanda «da dove si parte, allora?» ha tre risposte concrete:
state = 'seed' per distinguerli e peso BOOTSTRAP_WEIGHT (vedi mnest.html cap. 6).
L'utente può ispezionare il mnestoma da CLI (tramite il comando
Metnos mnestoma):
Metnos mnestoma list # tutti i mnest attivi
Metnos mnestoma list --tag fattura # filtrati per tag
Metnos mnestoma top 20 # i 20 più pesanti
Metnos mnestoma graph executor read_files # vicinato di un executor
Metnos mnestoma proto # i proto-mnest correnti
Metnos mnestoma history mnest_01HW... # storia eventi di un mnest
Metnos mnestoma snapshot --month 2026-03 # apre lo snapshot mensile
La dashboard web (vedi observability.html, da riscrivere) ha una vista grafica dello stesso contenuto, con filtri e drill-down. La regola di trasparenza: l'utente può sempre sapere cosa Metnos pensa di sapere.
Metnos — mnestoma microprogettazione
Doc canonico nuovo (concetto introdotto dalla ).