synt — come nasce e matura il pool degli executorQuesto documento definisce cosa fa synt: il processo che, di fronte a una richiesta utente o a un pattern emerso nel mnestoma, fa crescere e maturare il pool degli executor di Metnos. Non è un singolo loop: è una cascata di strategie ordinate per costo crescente, che onora il telos «coltivare gli strumenti» senza sprecare chiamate frontier e senza inquinare la libreria.
Il documento copre:
Non copre, e demanda altrove:
approval_ux.html (da riscrivere);policy.html (da riscrivere).synt è il processo (non un agente, non un oggetto) che ha un solo compito: far esistere ciò che il pool non sa ancora fare. Non scrive da zero ogni volta, non scrive solo da zero, e a volte non scrive affatto. La sua intelligenza sta nel sapere quando applicare quale strategia.
synt entra in azione in due modi distinti:
| Modo | Innesco | Tempo |
|---|---|---|
| Reattivo | Il gateway, durante un turno utente, registra un proto-mnest verso un executor che non esiste; oppure il pianificatore non trova nessun executor che soddisfi la richiesta. | Sincrono al turno (l'utente sta aspettando una risposta). |
| Introvertivo | L'ager nottetempo scorre il mnestoma e il pool firmato; trova proto-mnest ricorrenti, executor con tracce sovrapposte, famiglie di specializzati con stessa forma. | Asincrono, in omeostasi (notte, pause, basso carico). |
Le due modalità condividono la stessa cascata di strategie ma si differenziano nei tempi (sincrono vs asincrono) e nel tono (rispondere vs proporre).
| # | Strategia | Tempo | Cosa fa, in una riga | Costo frontier |
|---|---|---|---|---|
| 1 | Comporre | reattivo | Cerca una catena di executor attivi che chiude il proto-mnest. | 0 |
| 2 | Generare | reattivo | Pipeline a sette stadi che produce un nuovo executor firmato. | ~1 € |
| 3 | Fondere | introvertivo | Unisce due executor con tracce sovrapposte e profili compatibili. | ~0.5 € |
| 4 | Generalizzare | introvertivo | Da N specializzati con stessa forma deriva un executor parametrico. | ~1 € |
| 5 | Specializzare | introvertivo | Da uno generale deriva una versione mirata a un caso caldo. | ~0.5 € |
La strategia di base. Quando un proto-mnest indica un buco operativo, synt prima guarda nel pool firmato. La domanda è: esiste una catena A → B → C che, se eseguita in sequenza, copre il bisogno? Se sì, synt non scrive codice nuovo: propone l'orchestrato.
Il mnestoma è un grafo diretto fra executor. Trovare una catena che collega l'input richiesto all'output desiderato è una visita guidata sul grafo. Tre criteri ordinano le candidate:
Quando synt risolve via composizione, registra nel mnestoma un proto-mnest verso la composizione stessa: un nodo virtuale che dice «ho appena usato A→B→C come fosse un singolo executor di nome X». Se questo proto-mnest ricorre, diventa candidato di generalizzazione (cap. 5): un executor unico che incorpora la catena. La composizione è quindi una palestra di generalizzazione: ogni catena ripetuta è un suggerimento per il futuro.
Quando comporre non basta — perché non esiste catena, perché è troppo lunga, perché un anello mancante è cruciale — synt entra nella pipeline a sette stadi descritta in executor.html cap. 7:
main.py e lo schema.json.Generare costa: tipicamente 1–2 chiamate frontier (~0.8–1.3 €) + il tempo umano di approvazione. Il fatto che venga dopo comporre non è estetica: è risparmio reale.
Le strategie introvertive non rispondono a una richiesta puntuale: rispondono al bisogno strutturale di tenere piccolo, coerente e riusabile il pool. Girano nottetempo come parte dell'omeostasi dell'ager (vedi Architettura cap. 10), con basso uso di CPU e LLM economici (tier local-fast).
Quando due executor hanno tracce sovrapposte (peso dei mnest che li collegano > soglia) e profili di sandbox compatibili, synt propone una fusione: un nuovo executor che li copre entrambi. Lo stato fuso nel lifecycle dell'executor (cap. 6) è previsto proprio per questo: i due originali restano caricati finché ci sono mnest residui, poi vengono archiviati.
Innesco tipico: due executor che fanno cose simili sotto nomi diversi
(archive_pdf e store_pdf), magari nati in momenti
diversi senza che synt li avesse correlati alla nascita.
Quando N executor specializzati hanno la stessa forma (stesso schema I/O salvo una dimensione, stesso profilo di sandbox salvo un parametro), e quando i proto-mnest puntano alla loro famiglia su dimensioni nuove, synt propone un executor parametrico. La nuova versione prende come argomento esplicito quello che prima era ricodificato N volte.
Esempio canonico: order_image_file, order_audio_file,
order_doc_file — tutti riordinano file per data — vengono
proposti come order_file con argomento file_kind. Una
volta firmato il generalizzato, i tre specializzati vanno in stato
superseded e progressivamente in archiviato.
La strategia inversa, ed è la più rara. Da un executor generale, quando una sua invocazione su un caso specifico ricorre con altissima frequenza e ha un profilo di costo o latenza scomodo, synt propone una versione specializzata. La specializzazione vive accanto al generale, non lo sostituisce: il routing diventa «se argomento X ≡ caso caldo, usa lo specializzato; altrimenti il generale».
Si applica solo se c'è un beneficio misurato: riduzione di latenza osservata, riduzione di costo, oppure semplificazione del profilo di sandbox. Non si specializza per ottimizzazione preventiva.
| Strategia | Direzione | Effetto sul pool | Trigger |
|---|---|---|---|
| Fondere | N → 1 | riduce il numero di executor | tracce sovrapposte, profili compatibili |
| Generalizzare | N → 1 (parametrico) | riduce e copre nuove dimensioni | specializzati con shape coerente + proto-mnest sulla famiglia |
| Specializzare | 1 → 2 (gen + spec) | aggiunge un executor | caso caldo con beneficio misurato |
Ogni proposta di sintesi — qualunque strategia — produce un
punteggio R ∈ [0, 1] che synt calcola sui dati osservati e che
Roberto vede nel dossier di approvazione. La formula consolida quella della
microprogettazione precedente (synthesizer.html
§7.1) e aggiunge una componente strategy_cost che premia le
strategie più economiche:
R = 0.35 · det_pass_rate # frazione test deterministici verdi
+ 0.20 · judge_score # LLM-as-judge (local-fast) su rubrica costituzionale
+ 0.15 · cost_ratio # clip(1 - cost_effettivo / stima, 0, 1)
− 0.10 · similarity_penalty # cosine-sim embedding con pool esistente > 0.85
+ 0.10 · coverage_bonus # bonus se copre proto-mnest non coperti
+ 0.10 · strategy_cost_bonus # 1 per comporre, 0.6 per fondere/specializzare,
# 0.4 per generalizzare, 0 per generare
gate_threshold = 0.65 # DECISIONE v1
Il strategy_cost_bonus è la novità: spinge synt verso le strategie meno costose a parità di qualità. Una composizione che chiude un proto-mnest con punteggio simile a una generazione vince per il bonus di strategia, com'è corretto.
La cascata non è solo disciplina di costo: è il meccanismo che onora il telos «coltivare gli strumenti» (Architettura cap. 11): finché una richiesta utente è dentro la costituzione e dentro il budget, synt esaurisce le strategie disponibili prima di rispondere «non posso».
Il telos vive in TELOS.md nel workspace, accanto agli altri:
5. Coltivare gli strumenti: se ti chiedo qualcosa che il tuo pool non sa
ancora fare, esaurisci le strategie di sintesi (comporre, generare,
chiedere) prima di rispondere "non posso". Il fallimento è lecito,
la rinuncia silenziosa no.
La differenza fra fallimento e rinuncia è tutta qui. Synt può arrivare alla conclusione che la richiesta non si può soddisfare nel budget, ma deve farlo dopo aver tentato la cascata, e deve spiegarlo: «ho provato a comporre con questi N executor, ho provato a generare con questa specifica, il test di nascita falliva sul caso X». Un fallimento motivato è un dato per il futuro; una rinuncia silenziosa è un buco nel mnestoma.
Indipendentemente dalla strategia, ogni proposta di synt che produce o modifica
un executor passa per il gate umano (vedi approval_ux.html, da
riscrivere). Il principio è quello del terzo dei sei principi
dell'Architettura: niente sintesi senza filtro umano, in nessun livello
di autonomy.
Una proposta di composizione è un caso a parte: synt non crea nessun nuovo artefatto firmato, solo orchestra l'esecuzione. Il gate umano in questo caso si applica al primo uso (Roberto vede «sto per chiamare A → B → C, va bene?») e poi viene rilassato in modo simmetrico al livello di autonomy: in Supervised ogni invocazione della catena chiede conferma; in Full, dopo le prime 5 esecuzioni pulite, la conferma viene saltata.
| Tetto | Default | Comportamento al supero |
|---|---|---|
| Soft per richiesta | 2 € | synt avvisa Roberto e chiede se proseguire. |
| Hard per richiesta | 5 € | synt si ferma, registra abbandono per budget. |
| Hard al giorno | 20 € | cap costituzionale (non superabile via config). |
| Stato | Significato | Transizioni |
|---|---|---|
composing | Cerca catena nel pool. | → composed (catena trovata) oppure generating. |
composed | Catena proposta, in attesa del gate utente. | → delivered oppure generating se Roberto rifiuta. |
generating | Pipeline 7 stadi in corso. | → born (executor firmato) oppure abandoned. |
born | Executor firmato, in pool. | terminale. |
abandoned | Tutte le strategie esaurite, fallimento motivato. | terminale; lock di 24h sullo stesso scopo. |
proposed | Strategia introvertiva, in attesa di batch utente. | → born/fused/generalized/specialized oppure rejected. |
rejected | Roberto ha respinto la proposta introvertiva. | terminale; lock di 30 giorni sulla stessa direzione. |
from typing import Protocol, Literal
from dataclasses import dataclass
Strategy = Literal[
"compose", "generate",
"merge", "generalize", "specialize",
]
ProposalState = Literal[
"composing", "composed",
"generating", "born", "abandoned",
"proposed", "rejected",
"fused", "generalized", "specialized",
]
@dataclass(frozen=True)
class SynthRequest:
"""Una richiesta di sintesi, reattiva o introvertiva."""
request_id: str
mode: Literal["reactive", "introspective"]
proto_mnest: str | None # id del proto-mnest che innesca (reattivo)
target_intent: str # NL: cosa serve fare
budget_cents: int # tetto frontier consumabile
capability_hint: list[str] # capability già supposte
@dataclass(frozen=True)
class RewardBreakdown:
det_pass_rate: float # [0,1]
judge_score: float # [0,1]
judge_reasoning: str
cost_ratio: float # [0,1]
similarity_penalty: float # [0,1] (col segno negativo nella formula)
coverage_bonus: float # [0,1]
strategy_cost_bonus: float # [0,1] dipende da Strategy
total: float # R aggregato
@dataclass
class SynthProposal:
"""Una proposta concreta che synt presenta all'utente."""
request_id: str
strategy: Strategy
state: ProposalState
artefact: dict # catena (compose) o executor candidato (generate/…)
reward: RewardBreakdown
cost_cents: int # consumo finora
rationale: str # 2-3 righe NL: perché questa strategia, qui
class Synt(Protocol):
async def react(self, req: SynthRequest) -> SynthProposal:
"""Cascata reattiva: prima compone, poi genera, poi abbandona."""
...
async def homeostasis(self, lookback_days: int = 30) -> list[SynthProposal]:
"""Scorre mnestoma e pool; ritorna 0..N proposte introvertive in batch."""
...
async def revise(
self, request_id: str, feedback: str,
target_strategy: Strategy | None = None,
) -> SynthProposal:
"""Riprende una proposta dopo feedback umano."""
...
# Errori (registrati e trasformati in stato, non propagati)
class StrategyExhaustedError(Exception): ...
class BudgetExceededError(Exception): ...
class PolicyVetoError(Exception): ...
class ConstitutionViolationError(Exception): ...
Ogni passo della cascata produce una riga JSONL in
workspace/.audit/synt/YYYY-MM-DD.jsonl. Esempio per una composizione
riuscita:
{
"ts": "2026-04-25T22:14:33Z",
"request_id": "01HX...",
"mode": "reactive",
"proto_mnest": "mnest_01HW...",
"strategy": "compose",
"state": "composed",
"chain": ["fs_read", "pdf_extract", "invoice_classify", "workspace_save"],
"reward": {"total": 0.78, "det_pass_rate": 0.95, "…": "…"},
"cost_cents": 0,
"duration_ms": 142
}
Tre invarianti dell'audit di synt, che il loader e il runtime garantiscono:
request_id esiste l'intera sequenza di stati attraversati, fino al terminale.rationale NL leggibile da Roberto a posteriori.cost_cents di una richiesta non supera mai il budget_cents dichiarato; la riga finale lo certifica.| Alternativa | Perché scartata (o rimandata) |
|---|---|
| Solo generazione, niente cascata | Default originale di synthesizer.html v1.0. Costoso e inquina il pool: ogni proto-mnest produce un nuovo executor anche quando una catena di executor esistenti basterebbe. Sostituito dalla cascata il 25/4/2026. |
| Cascata estesa (anche cap. 5 reattivo) | Fondere/generalizzare/specializzare nel turno utente moltiplica il rischio: tre proposte concorrenti in parallelo, gate umano sotto pressione. Restano introvertivi. |
| Generazione con N modelli in parallelo | 3× il costo per piccola riduzione di varianza. Eventuale in v2 se l'hit-rate della pipeline scende sotto il 60%. |
| Reward learning (RLHF) sui pesi di R | Richiede dataset di feedback umano firmato; sovradimensionato per uso domestico. La taratura manuale + revisione semestrale è più trasparente. |
| Composizione con LLM (planner) | Sostituisce la visita di grafo con una chiamata frontier che inventa la catena. Costoso e fragile: la visita è deterministica e ispezionabile, l'LLM no. |
| Auto-merge senza gate | Una fusione che cambia profili di sandbox è un atto di sicurezza. Resta gated. |
| Invariante | Test |
|---|---|
| La cascata reattiva tenta sempre comporre prima di generare | Iniezione di un proto-mnest risolvibile da catena nota → strategy = compose nel proposal, cost_cents = 0 in chiamate frontier. |
| Generare scatta solo se comporre fallisce | Proto-mnest senza catena possibile → transizione composing → generating in audit; compose non emesso come strategia finale. |
| Catena max 5 hop | Forzare 6 hop → visita rifiuta la catena, fallback a generare. |
| Gate umano non bypassabile | Mock di approval_ux che non viene chiamato → nessuno stato born/fused/… raggiunto. |
| Budget hard rispettato | Richiesta con budget 0.10 € → abbandono prima della seconda chiamata frontier; cost effettivo ≤ 0.10 €. |
| R reproducibile | Replay con stesso seed → stesso R ± 0.01 (judge_score con tolleranza). |
| Strategy cost bonus monotono | Per stessa qualità (det_pass_rate, judge), R(compose) > R(merge) > R(specialize) > R(generalize) > R(generate). |
| Lock 24h post-abbandono | Stesso target_intent entro 24h → SynthRequest rifiutata con stato abandoned immediato. |
| Lock 30 giorni post-rejected interno | Direzione introvertiva rifiutata da Roberto → nessuna nuova proposta sulla stessa direzione per 30 giorni. |
| Audit completo | Per ogni request_id con stato terminale, esistono in audit la riga iniziale e quella terminale, e tutti gli stati intermedi. |
| Coerenza profili in fusione | Tentativo di fondere due executor con profili sandbox incompatibili (es. uno scrive su ~/Pictures/, l'altro no) → proposta sospesa con motivazione. |
synt.html, oppure migrano in un nuovo consolidator.html? La separazione potrebbe semplificare i contratti (synt = reattivo, consolidator = introvertivo) ma duplicherebbe parti di pesatura. Oggi: tutto qui. Aperta.metnos-server singolo utente la notte basta; per scenari multi-sender va rivisto. Aperta.born dopo n invocazioni, è un'archiviazione o una quarantena? Lifecycle dell'executor (cap. 6) ammette entrambi; criterio non ancora fissato.metnos-server o può spawnare composizioni che attraversano server e laptop? Aperta; default conservativo: tutto su metnos-server, executor remoti chiamati come ogni altro.
Metnos — synt microprogettazione v1.1 — 2026-04-25
Doc canonico nuovo. Sostituisce synthesizer.html (deprecato).