Quando arriva una richiesta, Metnos non chiama piu' il pianificatore LLM cinque o sei volte di seguito. La affida a cinque moduli che si chiamano come cinque divinità greche: Mētis consiglia, Noûs esegue, Praxis ricorda, Pronoia salva quando si inciampa, Aporia ammette quando si è in un vicolo cieco. Insieme rispondono in dodici secondi dove prima ce ne volevano settantasei, senza chiedere mai aiuto al cloud.
Fino al 24 maggio 2026, Metnos lavorava così: a ogni turno il pianificatore (un Gemma 4 26B locale) veniva chiamato a ripetizione — uno step, l'osservazione, lo step successivo, di nuovo l'osservazione, e così via. Una pipeline di cinque passi voleva dire cinque o sei chiamate al modello. Ogni chiamata costava dieci-quindici secondi. Risultato: un turno medio impiegava un minuto e un quarto. E non sempre arrivava in fondo.
{0, 1, 0, 1, 1}. Tre volte su cinque il pianificatore "perdeva
il filo" a metà pipeline e cambiava direzione. Aumentare il budget di
ragionamento non aiutava: il problema era strutturale. Ogni chiamata al
modello e' un dado lanciato; lanciarne sei e sperare che escano tutti
sei e' irragionevole.
A questo si aggiungeva un terzo problema, più sottile: il search space. Il pianificatore vedeva ogni turno duecento tool e doveva ricostruire da zero la sequenza giusta, anche se era una sequenza vista mille volte ("trova mail, classifica spam, sposta in cestino"). Riscoprire l'ovvio è uno spreco — ma il sistema non aveva memoria.
Zeus sposo` Mētis, dea del consiglio astuto. Ma quando lei fu incinta, Zeus la ingoiò intera — per avere dentro di sé per sempre la sua saggezza, senza più dover chiedere consiglio all'esterno. Da allora Mētis vive nello stomaco di Zeus: gli suggerisce strategie dall'interno, ma non si vede più. — Esiodo, Teogonia, vv. 886-900
Il refactor del 25 maggio 2026 ha preso alla lettera questa immagine. Il PLANNER step-by-step viene ingoiato da un nuovo modulo chiamato Praxis (πράξις, "pratica appresa"). Il modello linguistico non scompare: vive ancora dentro Praxis nei panni di Mētis (Μῆτις, "consigliere strategico"). Ma cambia ruolo radicalmente:
È il pattern di Zeus che ingoia Mētis: la saggezza vive dentro Praxis, ma viene evocata solo se l'esperienza accumulata non basta. Quando il catalog matura, Mētis tace per giorni. Il sistema diventa il proprio consigliere — impara a non chiedere più.
Ma il mito non finisce qui. Ci sono due divinità ulteriori che entrano in gioco quando le cose vanno storte:
Pronoia, secondo alcune fonti madre di Prometeo, e' la provvidenza divina. Il suo nome significa letteralmente "intelletto prima": pro-noûs. Vede l'ostacolo mentre gli altri ci stanno già sbattendo contro. Interviene quando il piano fallisce, non quando e' ancora da fare. — Eschilo, Prometeo incatenato
Aporia (ἀπορία) significa "perplessità, vicolo cieco". Nei dialoghi socratici e' il momento in cui l'interlocutore si accorge di non sapere — e questa consapevolezza, lungi dall'essere una sconfitta, e' l'inizio della vera ricerca. — Platone, Menone, 80a-d
Pronoia interviene quando il piano di Mētis fallisce: capisce di che tipo è l'errore, propone un'alternativa, riprova. Aporia interviene quando neanche Pronoia salva: ammette onestamente che non si può proseguire, dice all'utente cosa serve per uscire dall'impasse, e tiene traccia della lacuna per il futuro.
File: runtime/praxis_propose.py (~229 LOC).
Cosa fa: riceve la query e propone un'intera pipeline multi-step in una sola chiamata al modello linguistico (Gemma 4 26B locale, tier wise, ragionamento attivo). Output JSON strutturato vincolato da grammatica GBNF: lista di step + segnaposti ${FILLER:nome} + template del messaggio finale.
Modo greco: astuta, situata, tecnica. Sa «il trucco» per uscire dalla situazione, non e' un risolutore generale. Per questo propone un piano, non valuta dieci alternative.
Quando parla: solo se Praxis non ha già la risposta. Una volta al turno, mai due. Tempo: circa 15 secondi.
File: runtime/praxis_executor.py (~430 LOC).
Cosa fa: riceve il framework da Mētis e lo realizza, step per step, in modo completamente deterministico. Niente modello linguistico nel loop: solo data routing. Espande i ${FILLER:nome} (un piccolo LLM fast tier al volo se serve), espande i ${stepN.field} da quello che hanno prodotto gli step precedenti, chiama l'executor, passa il risultato al Vaglio, accumula. Alla fine renderizza il messaggio finale.
Modo greco: chiaro, fedele, senza esitazioni. La mente che vede e agisce, non quella che dubita. Per questo non rivede mai il piano: lo realizza.
Quando lavora: sempre, su ogni piano (sia se viene da Praxis che da Mētis che da Pronoia).
File: runtime/praxis.py (~560 LOC) + sqlite ~/.local/share/metnos/praxis.sqlite.
Cosa fa: tiene memoria di tutti i framework che hanno funzionato, indicizzati per intent signature. Quando arriva un turno, calcola la firma dell'intent (verbo + oggetto + parole chiave) e cerca in O(1) sulla tabella skills. Se trova, si serve. Se non trova, chiama Mētis. Quando un piano funziona due volte, lo promuove automaticamente a skill attiva; quando fallisce tre volte di fila, lo mette in anti_skill per trenta giorni.
Modo greco: per Aristotele (Etica Nicomachea) la praxis e' l'azione che cresce con l'esperienza ripetuta. Non e' contemplazione, non e' produzione: e' il sapere-come-fare che diventa secondo natura. Per questo qui e' una memoria che apprende dal feedback.
Quando agisce: all'inizio di ogni turno (tenta il match) e alla fine (registra l'osservazione).
File: runtime/pronoia.py (~250 LOC).
Cosa fa: interviene solo se il piano di Mētis fallisce. Classifica l'errore in una di tre classi ortogonali (tool sbagliato, argomenti sbagliati, input mancante), seleziona un prompt specializzato per quella classe, e richiama Mētis con l'istruzione di escludere il tool fallito e proporre un'alternativa. Se Pronoia salva il turno, Praxis lo memorizza e la volta dopo si parte già dal piano corretto.
Modo greco: Pronoia (pro-noûs) e' l'intelletto-prima, la previdenza divina che vede l'ostacolo mentre gli altri ci stanno già inciampando. Madre di Prometeo secondo alcune fonti, e quindi nonna del fuoco rubato agli dei: una divinità che sblocca, che apre vie nuove.
Quando interviene: al massimo una volta per turno. Se Pronoia stessa fallisce, cede la parola ad Aporia.
File: runtime/aporia.py (~210 LOC) + sqlite ~/.local/share/metnos/aporiae.sqlite.
Cosa fa: ultima istanza onesta. Quando neanche Pronoia salva, Aporia non finge: classifica la lacuna in una di quattro categorie (serve azione utente, manca un executor, manca una skill, mancano i dati), genera un messaggio finale che dice all'utente esattamente cosa serve per uscire dall'impasse, e logga la lacuna persistente in sqlite. Se l'utente in seguito risolve (abilita una skill, condivide la posizione, costruisce l'indice), la query torna alla cascata normale e magari diventa una skill attiva.
Modo greco: per Platone l'aporia non è sconfitta ma punto di partenza. È il momento socratico in cui l'interlocutore riconosce di non sapere — e proprio quel riconoscimento è l'inizio della ricerca. Per questo Aporia non solo ammette il limite: lo trasforma in proposta evolutiva.
Quando agisce: raramente. Solo se la cascata e' arrivata in fondo senza soluzione. Tutto deterministico, zero LLM dentro Aporia.
Ecco il cammino completo, dal "ciao" dell'utente alla risposta finale. Si legge dall'alto verso il basso: ogni livello si tenta solo se quello sopra non ha già risolto.
UTENTE: "cerca le mail spam e mettile in cestino"
|
v
+-------------------+
| fast_path L0 | regex su pattern banalissimi ("che ora e'", "dove sono")
| (zero LLM) | --> match? rispondi in 50ms e termina
+-------------------+
| (no match)
v
+-------------------+
| intent_extractor | LLM fast tier ~370ms
| verb + object + | --> (verb="move", object="messages",
| keywords | keywords=["spam","cestino"])
+-------------------+
|
v
+--------------------------------------------------------------+
| PRAXIS ENGINE (le tre divinita' interne) |
| |
| +----------------+ |
| | Praxis | hash sha[:16] di intent_sig |
| | try_match | --> cache hit O(1) ? salta a Nous |
| | (sqlite) | |
| +----------------+ |
| | (miss) |
| v |
| +----------------+ |
| | Metis | LLM wise 1-shot, ~15s |
| | propose | --> framework JSON {steps, fillers, |
| | (Gemma 26B) | final_message} |
| +----------------+ |
| | |
| v |
| +----------------+ |
| | Nous | per ogni step: |
| | execute | - resolve from_step + FILLER |
| | (deterministico)| - invoke_executor |
| +----------------+ - vaglio.judge |
| | - accumulate |
| v |
| +----------------+ |
| | render final | template "Trovate {N} mail, spostate" |
| +----------------+ |
+--------------------------------------------------------------+
|
v (ok? si' --> Praxis registra + risponde a utente)
|
| (errore? si' --> entra Pronoia)
v
+-------------------+
| Pronoia | classifica errore: wrong_tool / wrong_args /
| classify + retry | missing_input
| | --> ri-propone framework con exclude failed_tool
| | --> esegue di nuovo via Nous
+-------------------+
|
v (ok? si' --> risposta)
|
| (ancora errore? --> entra Aporia)
v
+-------------------+
| Aporia | classifica root_cause: user_action_required /
| honest dead-end | missing_executor / missing_skill / missing_data
| | --> log in aporiae.sqlite
| | --> "Non posso risolvere: X. Per procedere: Y."
+-------------------+
|
v
UTENTE: messaggio finale (risposta o richiesta di azione)
Il cuore del motore è il database sqlite. Tre tabelle, tutte indicizzate per accesso O(1):
| Tabella | Cosa contiene | Quando viene scritta |
|---|---|---|
skills |
Pipeline che hanno funzionato, indicizzate per hash dell'intent. Per ognuna: framework JSON, contatori di utilizzo, success rate, versione. | Promosse automaticamente quando una pipeline ha avuto almeno 2-3 successi consecutivi sullo stesso intent. |
anti_skills |
Pipeline che hanno fallito ripetutamente. Per ognuna: motivo, scadenza TTL trenta giorni. | Quando una pipeline fallisce tre volte di fila sullo stesso intent. Dopo i trenta giorni, riprova. |
observations |
Log completo di ogni turno: intent, framework eseguito, verdetto, latenza. Append-only. | Sempre, a fine turno. Sorgente verità per audit e promozione/anti-skill. |
Il fenomeno interessante è che il sistema diventa più veloce nel tempo, da solo. La prima volta che si chiede «cerca le mail spam e mettile in cestino», Mētis ci mette i suoi quindici secondi buoni per generare il framework. La seconda volta, Praxis ha già in cache la stessa identica firma di intent: la pipeline parte in tre millisecondi. Da quel momento in poi, quella specifica richiesta non passa più per il modello linguistico.
Al di sotto di ogni risposta in chat, l'utente vede tre piccoli pulsanti: ✓ verde («risposta giusta»), ✗ rosso («risposta sbagliata»), ↺ arancio («riprova»). Quei tre simboli sono la lingua con cui Praxis impara:
Non è necessario premere niente: se l'utente non dà feedback e una pipeline arriva in fondo senza errori per due turni di seguito, Praxis la promuove comunque (auto-promote). Il feedback esplicito accelera e rifinisce, ma il sistema impara anche dal silenzio.
Mētis è l'unica delle cinque divinità che parla davvero con il modello linguistico. Il suo lavoro: data una query e un intent estratto, produrre in una sola chiamata il piano intero da eseguire. Niente reasoning step-by-step, niente tool-call iterativi: solo un grosso oggetto JSON che descrive tutta la pipeline.
{
"steps": [
{"tool": "find_messages",
"args": {"folder": "INBOX", "query": "is:unread"}},
{"tool": "classify_entries",
"args": {"from_step": 1, "category": "spam"}},
{"tool": "filter_entries",
"args": {"from_step": 2, "where": {"category": "spam"}}},
{"tool": "move_messages",
"args": {"from_step": 3, "dst_folder": "${FILLER:cestino_folder}"}}
],
"fillers": {
"cestino_folder": {
"prompt": "Come si chiama la cartella cestino per questo account?",
"default": "Trash",
"tier": "fast"
}
},
"final_message": "Spostate ${step4.ok_count} mail in cestino."
}
La chiamata al modello è vincolata da una grammatica GBNF deterministica: il modello non può produrre output malformati, perché il decoder rifiuta in tempo reale ogni token che violi lo schema. Tempo medio: 15 secondi sul Gemma 26B locale, con ragionamento attivo (budget 2048 token). Costo: zero. Velocità sei volte maggiore della modalità step-by-step legacy.
Se Pronoia chiama Mētis per un re-propose dopo un fallimento, le passa la lista dei tool che hanno fallito nel turno precedente. Il prompt contiene un'istruzione esplicita: «NON proporre questi tool, sono già falliti». Cosi' Mētis non gira in tondo, e ogni fallimento è un'opportunità di esplorare una strada diversa.
Noûs è il modulo più rigoroso: zero modello linguistico nel loop principale. Solo Python deterministico. Il suo job:
from_step: N (estrarre entries dallo step N-esimo).${FILLER:nome} chiamando un piccolo LLM fast tier (uno per filler, mezzo secondo l'uno) oppure il default se Praxis ha già in cache la risposta.${stepN.field} nel template del messaggio finale, estraendo il campo dal risultato dello step.Questo loop è deterministico per costruzione. Niente "perdita di rotta", niente ridiscussioni del piano. Se uno step fallisce, Noûs non improvvisa: si ferma e cede la parola a Pronoia.
Pronoia è la rete di sicurezza. Si attiva solo se Noûs ha sbattuto contro un errore mentre eseguiva il piano di Mētis. Il suo primo lavoro è capire che tipo di errore è:
| Classe | Quando | Strategia di recovery |
|---|---|---|
| wrong_tool | Il tool scelto non era adatto: ha crashato, e' semanticamente sbagliato, o ha "allucinato" un argomento. | Richiama Mētis dicendo «questo tool e' fuori dai giochi, propone un piano diverso». |
| wrong_args | Il tool era giusto ma gli argomenti erano malformati: una pipeline che gira a vuoto, un cap_steps raggiunto, un from_step che punta nel vuoto. | Richiama Mētis con l'istruzione «argomenti canonici, from_step esplicito». |
| missing_input | Un backend non risponde, un indice non è costruito, una directory non esiste, un filtro produce zero risultati. | Suggerisce uno step di setup (costruire l'indice, montare la share) oppure cede ad Aporia se serve l'utente. |
| out_of_scope | Errore strutturalmente non recuperabile: l'utente deve fare qualcosa fisicamente (per esempio condividere la posizione su Telegram) oppure manca una capability totalmente. | Pronoia non interviene: cede subito ad Aporia. |
La classificazione è deterministica: marker testuali, niente modello linguistico. Solo la ri-proposta del framework usa Mētis di nuovo (perché quello è intrinsecamente un atto creativo).
Budget: Pronoia interviene al massimo una volta per turno. Se neanche il piano alternativo riesce, cede ad Aporia. Niente loop infiniti, niente "uno step e mezzo" di nuovi tentativi.
Aporia è il pezzo più sottile del sistema. Non è un "gestore di errori": è un riconoscimento epistemico onesto. Quando arriva al suo turno, vuol dire che tutta la cascata ha tentato e fallito. Aporia non cerca di nascondere: classifica la lacuna, la nomina chiaramente all'utente, e suggerisce cosa servirebbe per uscire.
| Categoria | Esempio reale | Suggested action |
|---|---|---|
| user_action_required | «Dove sono?» senza posizione condivisa su Telegram. | «Per dirti dove sei, condividi la tua posizione qui in chat (pinza graffetta → Posizione).» |
| missing_executor | Vocabolo nuovo, intent non coperto dal catalog. | «Posso provare a costruire un nuovo executor per questo. Confermi?» (richiesta di synth) |
| missing_skill | «Cerca su Drive» ma la skill Google Drive non è abilitata. | «La skill Google Drive non e' abilitata. Vuoi che la configuriamo? Richiede un OAuth.» |
| missing_data | «Cerca foto del 2019» ma l'indice immagini non è mai stato costruito. | «Indice foto mancante. Vuoi che lo costruisca ora? Ci vorranno circa dieci minuti.» |
Tutto deterministico, zero LLM in Aporia. La lacuna viene loggata in
aporiae.sqlite: il giorno dopo, il numero di occorrenze
è visibile sul dashboard admin /admin/aporiae, e
si può decidere se è tempo di colmarla strutturalmente
(sintetizzare l'executor mancante, abilitare la skill, costruire l'indice).
Test set: 35 query reali, raccolte da turn log di produzione + smoke battery + campione E2E. Le query coprono mail, file, web, posizione, ora, dialoghi multi-turno, scheduling, gestione contatti.
| Modalità | Coverage | Latency media | Modello | Costo |
|---|---|---|---|---|
| PLANNER step-by-step legacy | ~33/35 stimato | 76.3 s | Anthropic Opus 4.7 | $$ |
| Praxis + Pronoia + Aporia | 33/35 (94%) | 12.5 s | Gemma 4 26B locale | 0 |
Sei volte più veloce. Coverage equivalente. Costo zero (il modello gira su Strix Halo 96GB unified). Codebase agente ridotto del 55% (circa 8000 LOC di PLANNER full rimosse).
La differenza visibile, dalla parte dell'utente, è in tre punti:
Anche al primo colpo, Praxis non chiama il pianificatore step-by-step ma fa una sola chiamata 1-shot a Mētis. Risultato: 12 secondi invece di 76. La sensazione è quella di un sistema che «ci pensa un attimo» invece di «si blocca per un minuto».
Dopo due-tre turni della stessa famiglia di richieste, la skill è attiva e i turn successivi partono in cache hit. La risposta arriva prima che si sia finito di leggere il messaggio mandato: meno di un secondo, spesso sotto i 500 ms. È la differenza fra «assistente» e «reazione immediata».
Quando qualcosa non si può fare, Aporia dice esattamente cosa serve. Niente messaggi vaghi del tipo «errore generico», niente loop di tentativi inutili: una frase chiara, un suggerimento operativo, e (se è il caso) un dialogo per risolvere la lacuna sul momento.
OpenClaw, Lobster, LangGraph e affini sono framework per scrivere «workflow di agenti LLM» in YAML o Python. Coprono in parte lo stesso problema: ridurre la varianza del LLM-in-loop dando struttura ai task. Ma hanno limiti che Praxis non ha:
| Aspetto | OpenClaw / LangGraph | Praxis Metnos |
|---|---|---|
| Origine dei workflow | scritti a mano in YAML, manutenzione manuale | seed opzionale, ma crescono automaticamente dal feedback ✓ ✗ ↻ |
| Apprendimento | no | sì: cache che si popola dai turni andati bene |
| Anti-errore | no | tabella anti_skills esclude i cammini falliti per 30gg |
| Recovery strutturato | retry generici | Pronoia con 3 classi ortogonali + re-propose mirato |
| Vicolo cieco onesto | retry infinito o crash | Aporia classifica + suggerisce azione utente |
| Audit | nessuno, o log testuali | tabella observations sqlite, UI /admin/changes |
| Vocab compliance | nomi liberi | Naming Authority (verbo + oggetto + qualifier) garantita |
| Decadimento | no | efficacy ager 30gg |
I framework LLM-in-loop tradizionali (OpenClaw, LangGraph) sono come artisti con tela bianca: ogni turno reinventano la composizione. Bella creatività, ma il rischio di allucinare un pennello al posto di una matita è reale, e ogni opera costa molto.
Praxis è come un artigiano con quaderno di schizzi: Mētis schizza il piano una volta (creatività concentrata e vincolata), e da quel momento in poi l'artigiano riapre il quaderno alla pagina giusta. La creatività del modello c'è ancora, ma è limitata al momento dell'invenzione: l'esecuzione è pura riproduzione fedele dello schizzo. Così si evitano le allucinazioni in esecuzione, si guadagna velocità, si guadagna costo zero quando il quaderno è già ricco.
framework_hash include riferimenti agli executor; cambio di catalog → nuovo framework_hash → cache miss naturale.consult_frontier. Quando attivarla automaticamente vs lasciarla come escalation utente? Strato 3 escalation UI (ADR 0159 ext) già copre il caso con 4-choice dialog.praxis_propose.py propone framework: non
risolutori generali, ma trovate tecniche per il caso concreto.