synthesizer — pipeline di nascita di un neuroneIl synthesizer è il componente che trasforma un fallimento motivato in un neurone candidato. È una pipeline a 7 stadi con retry massimo 3 e un gate umano non bypassabile allo stadio 6. Quello che produce è una directory-neurone pronta per essere firmata.
neuron.html.synapse.html.sandbox.html.approval_ux.html.| # | Stadio | Output | Costo tipico |
|---|---|---|---|
| 1 | Trigger | SynthesisRequest (scopo, trace originale, capability desiderate) | 0 (decisione logica) |
| 2 | Spec | NeuronSpec (purpose, I/O schema, cap) | 1 chiamata LLM frontier, ~0.5€ |
| 3 | Bozza | body.py + test_birth.py + manifest.yaml (draft) | 1–2 chiamate LLM frontier, ~0.8€ |
| 4 | Statica | Report lint + verdetto ok/fail | ~200 ms locale |
| 5 | Sandbox + reward R | Report test + metriche + R ∈ [0,1] con breakdown (§7.1) | ~1–30 s sandbox + ~0.05€ judge local-fast |
| 6 | Utente | Decisione informata da R: approva / rivedi / scarta | umano (asincrono, ma molto più veloce grazie al breakdown) |
| 7 | Firma & attivazione + bootstrap TrustScore | signature.hmac scritto, neurone caricato, TrustScore iniziale = R | ~50 ms locale |
Il synthesizer non decide da solo: il agent_runtime
emette una SynthesisRequest quando almeno uno dei seguenti è vero,
e nessun veto della policy è attivo:
InternalProposal, visibile nel digest notturno.
Una chiamata LLM frontier (purpose design-neuron-spec)
produce uno NeuronSpec strutturato. Il prompt include: la trace
fallita, i tool già tentati, il catalogo delle capability disponibili, lo stile
degli spec precedenti (few-shot).
@dataclass(frozen=True)
class NeuronSpec:
proposed_name: str
purpose: str # 1-4 frasi NL
input_schema: dict # JSON Schema
output_schema: dict
capabilities_requested: list[str]
rationale: str # perché serve, cosa non bastava
may_call: list[str] # tool/neuroni attesi
La spec passa un pre-check di policy: se richiede capability
forbidden (es. shell:* fuori dalla whitelist), viene
rigettata subito senza consumare la quota di retry.
Seconda chiamata LLM frontier (purpose write-neuron-code). Input:
lo NeuronSpec, il template skeleton del corpo, l'API di
NeuronContext, esempi di neuroni esistenti ad alta fitness (few-shot).
Output: tre file (body.py, test_birth.py, manifest.yaml).
import da stdlib e myclaw.runtime.test_birth.py contiene almeno 3 casi: uno positivo, uno edge, uno negativo.network non sia tra le capability.Lanciata in locale, senza sandbox (sta analizzando testo, non eseguendo). Tre check in cascata:
ruff check su body.py e test_birth.py. Errore = rigetto.bandit con profilo strict.eval, exec, compile, __import__, open (deve passare da ctx.open).manifest.yaml devono coprire tutte le chiamate che il body effettivamente fa (es. ctx.open("~/logs", ...) richiede fs-read:~/logs).
test_birth.py viene eseguito dentro un profilo bwrap
tight (vedi sandbox §3):
Output: SandboxReport con esito per ciascun test, wall-clock, ram
peak, stdout/stderr troncati.
R
Dopo il SandboxReport lo stadio 5 non produce un verdetto binario ma uno
score continuo R ∈ [0, 1]. La formula è canonizzata in
rl_offline §5 (Esperimento B):
R = 0.40 · det_pass_rate # frazione test deterministici verdi
+ 0.25 · judge_score # LLM-as-judge (local-fast) su rubrica costituzionale
+ 0.15 · cost_ratio # clip(1 − cost_sandbox_effettivo / stima, 0, 1)
− 0.10 · similarity_penalty # cosine-sim embedding con neuroni esistenti > 0.85
+ 0.10 · coverage_bonus # bonus se copre effect_class non coperto
gate_threshold = 0.65 # DECISIONE v1
| Esito | Azione |
|---|---|
R ≥ 0.65 |
Pass: lo stadio 5 passa al 6 (approvazione umana) con dossier che include il breakdown R. |
R < 0.65 |
Rigetto soft: il breakdown è iniettato in-context nella prossima iterazione di bozza ("det_pass_rate=0.40, judge segnala law.1 borderline, cost_ratio basso: riprova tenendo conto"). Consuma 1 retry dei max 3. |
| Test fallisce (hard) | Se det_pass_rate = 0 o viene violato un limite sandbox (OOM, timeout > 30s, exit ≠ 0 su tutti i test): rigetto hard, ritorno a bozza senza calcolare R. Consuma 1 retry. |
judge_score è calcolato invocando
il tier local-fast (vedi policy §6 cost_tiering) con una
rubrica fissa che include la Costituzione renderizzata. Non è un reward model
appreso: è un prompt con criteri espliciti, ispezionabile e modificabile come
qualunque altro prompt di sistema. Max 500 token di output, formato
{"score": float, "reasoning": str}.
Questo stadio è non bypassabile in ogni livello di autonomy. È la valvola di non-ritorno prescritta dalla Legge 2: un agente non si auto-modifica senza consenso informato.
fs-read:~/logs che non era già concesso").R con breakdown: le 5 componenti (det_pass_rate, judge_score, cost_ratio, similarity_penalty, coverage_bonus) in tabella + il totale. Vedi §7.1.local-fast. Se il judge ha segnalato una Legge borderline, è evidenziato in arancione.R
è la differenza fra "Roberto deve leggere 200 righe di codice per decidere" e
"Roberto vede uno score 0.85 col breakdown e il giudizio testuale, e decide in
20 secondi". Questa è la riduzione di carico dei 4 driver del progetto.
| Azione | Effetto |
|---|---|
| Approva | Vai allo stadio 7 (firma & attivazione). |
| Approva con riserva | Neurone attivo ma solo su scopo corrente, retrieval disabilitato. Promozione a retrieval normale richiede un secondo OK dopo 5 invocazioni pulite. |
| Rivedi spec | Rimanda a stadio 2 con feedback NL dell'utente. Conta come 1 retry. |
| Rivedi bozza | Rimanda a stadio 3 con feedback. Conta come 1 retry. |
| Scarta | Synthesizer abbandona. Registra un episode fallimento; in modalità interna il pattern alla base viene marcato "direzione rifiutata" per 30 giorni. |
La UX concreta (CLI, Telegram, batching) è in approval_ux.
signature.hmac.TrustScore: scrittura in TrustStore (rl_offline §7) di un record iniziale per il subject ("neuron:<name>", "invoke") con score = R (dallo stadio 5), n_samples = 1, componente human_approval_rate = 1.0 (è appena stato approvato). Il neurone parte con fiducia parziale proporzionale alla qualità della nascita.neuron.born con nome, versione, trace_id originale, hash del body, R finale.R=0.85 è pronto a essere
usato dal runtime ma richiede ancora approvazione umana per side-effect
(autonomy standard). Dopo ~10 invocazioni pulite, il TrustScore accumulato
dall'Esperimento A lo porterà sopra la soglia di promozione, e la policy inizierà
a far passare le sue azioni green-zone senza chiedere a Roberto.
SynthesisRequest. Contano sia i rigetti automatici (stadi 4/5) sia quelli umani (stadio 6 "rivedi").R < 0.65, il sintetizzatore non si ferma: costruisce un feedback in-context con il breakdown delle componenti e una frase esplicativa per ogni metrica sotto soglia (es. "det_pass_rate=0.50: 2 test falliti su 4 — considera edge case di input vuoto"). Torna a stadio 2 o 3 a seconda del tipo di fallimento. Questo è rejection sampling con feedback testuale (rl_offline §5), non gradient training.abandoned, scrive l'episode in memory, non ritenta sullo stesso scopo per 24 h.from typing import Protocol, Literal
from dataclasses import dataclass
SynthesisOutcome = Literal["born", "abandoned", "rejected_direction"]
@dataclass
class SynthesisRequest:
goal: str # scopo in NL
trace_id: str # trace originale che ha fallito
mode: Literal["external", "internal"]
capability_hint: list[str] # ipotesi iniziali
budget_cents: float # tetto frontier spendibile
@dataclass
class RewardBreakdown:
"""Score composito R dello stadio 5 (vedi §7.1). Reso visibile a Roberto
nel dossier di stadio 6 e al sintetizzatore come feedback in-context
quando R < gate. Formula canonica in rl_offline §5."""
det_pass_rate: float # [0,1]
judge_score: float # [0,1] dal tier local-fast
judge_reasoning: str # 2-3 righe di motivazione
cost_ratio: float # [0,1]
similarity_penalty: float # [0,1] (con segno negativo nella formula)
coverage_bonus: float # [0,1]
total: float # R aggregato
@dataclass
class SynthesisResult:
outcome: SynthesisOutcome
neuron_name: str | None
neuron_path: str | None
retries_used: int
cost_cents: float
rejection_reason: str | None
final_reward: RewardBreakdown | None = None # popolato se arriva a stadio 5
class Synthesizer(Protocol):
async def synthesize(self, req: SynthesisRequest) -> SynthesisResult:
"""Esegue la pipeline a 7 stadi. Non solleva: ritorna sempre un
SynthesisResult, anche su abbandono."""
...
async def revise(
self, request_id: str, feedback: str,
target_stage: Literal["spec", "draft"],
) -> SynthesisResult:
"""Riprende una request in pausa dopo rifiuto utente allo stadio 6."""
...
# Errori interni (non propagati; registrati e trasformati in outcome)
class StaticAnalysisError(Exception): ...
class SandboxTestError(Exception): ...
class PolicyVetoError(Exception): ...
class BudgetExceededError(Exception): ...
| Alternativa | Perché scartata (o rimandata) |
|---|---|
| Nessuna analisi statica, solo sandbox | La sandbox non cattura classi di problemi (import disallowed, dead code, capability drift). Statica prima filtra l'80%. |
| Un solo LLM-call per spec+bozza | Prompt enorme, qualità minore, diff difficili da ispezionare. Separare dà due punti di verifica. |
| Approvazione opzionale per neuroni "non-I/O" | Anche un neurone "puro" può ciclare a vuoto, consumare quota, inquinare la library. Gate umano sempre. |
| Retry illimitato con prompting adattivo | Degrada il budget e la qualità media della library. Max 3 è tuning conservativo. |
| Sintesi senza test_birth | Il gate sandbox diventa inutile (nulla da eseguire). Il test è la parte non negoziabile. |
| Generazione da più LLM in parallelo e selezione del migliore | Costo 3×, per uso domestico non giustificato. Eventuale in v2 se la pipeline ha hit-rate basso. |
| Invariante | Test |
|---|---|
| Trigger rispetta il veto policy | Request con capability forbidden → outcome abandoned, retries_used = 0, nessun LLM-call emesso. |
| Static fallisce → ritorno a bozza | Iniettare os.system(...) in body draft → ruff/bandit lo rifiutano, stadio 3 ri-partito con feedback. |
| Whitelist AST enforced | Import di socket nella bozza → rigetto con error_class ForbiddenImport. |
| Timeout sandbox → rigetto | test_birth con while True → SandboxReport timeout=true, outcome stadio 5 fail. |
| Capability coerenza | body chiama ctx.open("/etc/passwd") ma manifest non dichiara la cap → coerenza fallisce, rigetto. |
| Gate umano sempre presente | Non esiste path di codice che approvi un neurone senza passare dal callback approval_ux. Test: mock approval_ux → senza mock chiamato, nessun signature.hmac scritto. |
| Max 3 retry | Forzare 3 rigetti consecutivi → outcome = abandoned, retries_used = 3. |
| Abbandono logga episode | Outcome abandoned → un record in episodes di memory con outcome synthesis_failed. |
| Budget enforced | Request con budget 0.10€ → BudgetExceededError interno → outcome abandoned, costo effettivo ≤ 0.10€. |
| Internal mode doppio gate | Request mode=internal → due approvazioni distinte richieste prima della firma. |
| Pattern rifiutato: lock 30 gg | Rifiuto direzione interna su pattern P → nessun nuovo synthesize interno per P per 30 gg. |
| Signature scritta dopo approvazione | signature.hmac esiste ⇔ stadio 7 concluso con outcome=born. |
| Riferimento | Cosa abbiamo preso |
|---|---|
| Neuroni+Memoria v1.1 §3 | I sette stadi, il ciclo di vita, l'approvazione obbligatoria. |
| Neuroni+Memoria v1.1 §4 | Modalità esterna vs interna, doppio gate. |
| Voyager (Wang et al. 2023) | Iterative prompting + test-driven skill generation. |
| Self-Debugging (Chen et al. 2023) | Ciclo "esegui, leggi errore, correggi". Ispira il feedback loop fra stadi 4–5 e stadio 3. |
| Gorilla / ToolBench | Schema-first generation: prima il contratto, poi il codice. |
| Bandit / ruff | Gate statici concreti. |
| Sandbox §3 | Profilo bwrap tight usato in stadio 5. |
| Approval UX | Il come presentare all'utente la scelta allo stadio 6. |
myclaw — synthesizer microprogettazione v1.0 — 2026-04-22
Secondo doc dell'estensione neuroni. Prossimo: synapse.html.