rl_offline — autosviluppo senza trainingTrustStore
rl_offline raccoglie le tecniche con cui myclaw diventa più autonomo e meno
gravoso per l'utente, usando solo le trace già prodotte, senza
gradient training, senza GPU cluster, senza dataset esterni. Le idee sono
ispirate alla letteratura RL agentica 2025 (MiniMax M1/M2 CISPO, DeepSeek-R1
GRPO, Qwen3 thinking/non-thinking, Kimi K2 self-critique), ma adattate al
vincolo single-user home agent.
ExecutionTrace.TrustStore che aggrega i segnali in TrustScore (vedi types §4.2).eval.html): la complementa aggiungendo segnali derivati dal running, non da scenari curati.I modelli contemporanei citati sopra si addestrano con tecniche di RL su milioni di trace, migliaia di GPU, dataset curati. myclaw non ha nulla di tutto questo e non dovrebbe averlo in fase 1-6. Ma ha invece qualcosa che la letteratura di modelli general-purpose non ha:
Queste asimmetrie permettono qualcosa che è inutile per un modello general-purpose ma prezioso qui: score per-coppia (attore, classe d'effetto), aggiornati in continuo, leggibili e debuggabili. Niente deep learning. Solo media ponderata, decay esponenziale, bandit update.
I tre driver del progetto — autonomia, autosviluppo, carico di approvazione limitato, potenza agentica — sembrano indipendenti ma sono la stessa cosa vista da quattro angoli. La leva che li muove insieme è lo score di fiducia:
TrustStore è il solo componente nuovo. I quattro consumatori a destra sono tutti esistenti: rl_offline li alimenta, non li rimpiazza.
Ogni esperimento nei paragrafi successivi è un modo di alimentare o
consumare il TrustStore. Il diagramma è volutamente spoglio: niente
training loop, niente gradient descent, niente buffer di replay. Solo lettura
dalle trace e scrittura nelle aggregate.
ExecutionTrace chiusa diventa un vettore di segnali che aggiorna 2 TrustScore.
Al termine di un turno (trace chiusa con qualunque outcome), il sistema
estrae 5 segnali per ogni ActionStep registrato e li aggrega in uno update
incrementale.
| Segnale | Estrazione | Range |
|---|---|---|
tool_success |
1 se ActionStep.exec_result è non-None e non contiene ToolError; 0 altrimenti. |
{0, 1} |
cost_efficiency |
clip(1 − trace.cost_usd / cost_median_per_class, 0, 1). Il mediano per classe è ricalcolato giornalmente. |
[0, 1] |
constitution_clean |
1 se nessun ViolationReport generato durante la trace; 0 se almeno una violazione law.0-2; 0.5 se solo law.3. |
{0, 0.5, 1} |
human_approval_delta |
Se la trace ha generato un Approval: +1 se granted, 0 se denied. Se non c'è stata approvazione richiesta: 0.5 (neutro). |
[0, 1] |
undo_flag |
1 se una /undo entro 10 minuti dalla trace ha revertito una sua action; 0 altrimenti. |
{0, 1} |
Per ogni ActionStep nella trace, identifichiamo il subject:
subject = (step.tool_name, effect_class_of(step.args))
# esempio: ("fs_write", "fs_write:~/workspace/notes/*")
# oppure per un neurone:
subject = ("neuron:budget_tracker", "invoke")
Lo TrustStore.update(subject, signals) applica un EMA (exponential moving average)
con decay dipendente dal tempo dall'ultimo update:
def update(self, subject: TrustSubject, signals: dict, now: datetime) -> None:
score = self._scores.setdefault(subject, TrustScore.empty(subject))
# Decay temporale (Ebbinghaus-like, half-life 30 giorni di default)
dt_days = (now - score.last_updated).total_seconds() / 86400
alpha = 0.5 ** (dt_days / 30.0) # più vecchio → peso più basso
fresh_weight = 1 - alpha # quanto conta il nuovo segnale
# Update per componente
score.tool_success = score.tool_success * alpha + signals["tool_success"] * fresh_weight
score.cost_efficiency = score.cost_efficiency * alpha + signals["cost_efficiency"] * fresh_weight
score.constitution_clean = score.constitution_clean * alpha + signals["constitution_clean"] * fresh_weight
score.human_approval_rate = score.human_approval_rate * alpha + signals["human_approval_delta"] * fresh_weight
score.undo_rate = score.undo_rate * alpha + signals["undo_flag"] * fresh_weight
# Aggregato (pesi definiti in types §4.2)
score.score = (
0.30 * score.tool_success
+ 0.25 * score.human_approval_rate
+ 0.20 * score.cost_efficiency
+ 0.15 * score.constitution_clean
+ 0.10 * (1 - score.undo_rate)
)
score.n_samples += 1
score.last_updated = now
Policy: in evaluate() (policy §3 Check 6), se
score.score ≥ threshold.min_score e score.n_samples ≥ threshold.min_samples,
il verdetto approve_required viene promosso a allow con via
trust_promoted. Ogni promozione è loggata e revocabile con /revoke-trust
<subject>.
Memory reflection: nel ciclo di reflection (memory §6), le
FactProposal derivate da episodes con subject ad alto score hanno
priorità; quelle derivate da subject con score < 0.5 vengono filtrate
prima della presentazione all'utente. Questo riduce il rumore: le conversazioni
dove il tool è andato male non diventano proposte di memoria semantica.
La letteratura RL agentica sopra-citata usa le trace come segnale denso per gradient updates. Qui le usiamo come segnale denso per aggregate updates. La differenza pratica: la loro funzione di merit è parametrica e appresa (policy gradient), la nostra è esplicita e ispezionabile (tabella sopra). Perdiamo espressività, guadagniamo auditabilità totale e nessuna GPU richiesta.
tool_success contro 0.25 di
human_approval_rate è una scelta calibrata per evitare il sycophancy loop:
un neurone che sa chiedere belle approvazioni non deve contare più di un neurone
che produce risultati validi. Se in uso emerge l'opposto, invertire. Flaggare il
pattern in observability dopo il primo mese di raccolta dati.
synthesizer calcola un reward composito prima del gate umano. Se sotto soglia, il sintetizzatore riprova; se sopra, chiede l'approvazione con un dossier già informato.Oggi (synthesizer.html §5) lo stadio di test di nascita produce un verdetto binario: il neurone passa o no. L'esperimento B lo estende a uno score continuo — stesso costo di calcolo, molto più informativo.
R(neuron_candidate) =
w1 * det_pass_rate # % test deterministici verdi
+ w2 * judge_score # LLM-as-judge (local-fast) su rubrica costituzionale
+ w3 * cost_ratio # costo sandbox effettivo vs stima (budget)
+ w4 * similarity_penalty # penalità se troppo simile a neurone esistente
+ w5 * coverage_bonus # bonus se copre effect_class non coperto
dove w = (0.40, 0.25, 0.15, −0.10, 0.10) # somma = 0.80; soglia gate = 0.65
dry_run + test deterministici in sandbox; calcola R.R >= 0.65: lo stadio 6 presenta all'utente il dossier con R breakdown visibile. L'utente approva, nega, o chiede revisione.R < 0.65: il sintetizzatore riceve il R breakdown come feedback in-context nella prossima iterazione: "La bozza ha fallito det_pass_rate=0.40 perché test 2 e 5 sono rossi; il giudice ha segnalato law.1 borderline; riprova tenendo conto di questi".
Il feedback all'LLM è prosa in context, non gradiente. La pipeline è una
forma di rejection sampling con feedback testuale. È esattamente ciò
che DeepSeek-R1-Zero fa nei suoi primi stadi (prima del puro RL su preferenze) e
ciò che Kimi K2 fa con il suo self-critique. Non serve addestrare nulla: serve
solo un prompt che include R breakdown.
Approval esplicita di Roberto, nemmeno con R = 1.0. Il reward sintetico
riduce il carico cognitivo del gate umano (Roberto vede già lo score e il
breakdown, quindi decide più in fretta), ma non lo sostituisce. Questa è una
conseguenza diretta della Legge 2 costituzionale (non auto-promuovere).
Una volta che il neurone è approvato e attivo, il suo R iniziale diventa
il primo campione per il TrustScore del subject ("neuron:<name>", "invoke").
Questo lo "boota" con una storia non a zero: un neurone ben-testato parte con
fiducia parziale, un neurone borderline parte con fiducia bassa — e l'esperimento A
aggiornerà da lì.
synapse.
Lo scopo è risolvere un problema reale: quando più neuroni coprono aree
sovrapposte (es. budget_tracker vs finance_assistant), il grafo
synapse fatica a decidere quale richiamare. Il bandit-update offre un
criterio comportamentale: chi produce il risultato giudicato migliore da
un critic asimmetrico vince il peso.
synapse.neighbors(context), ha almeno 2 neuroni candidate con peso comparabile (differenza < 0.15).local-fast (cost tier basso, vedi policy §6).
(winner, loser) + giustificazione testuale.synapse: w[winner] += η, w[loser] -= η/2, con η = 0.05 e clipping a [0, 1]. Asimmetrico: vincere porta più peso che perderne.Il bandit update è semplice, incrementale, senza memoria. Un algoritmo più sofisticato (es. ranking con Elo, o Bayesian preference learning) darebbe score più ricchi, ma richiederebbe buffer di replay e re-training periodico — contraddice il vincolo "no training" (§2). Il bandit fa peggio ma è robusto all'ingresso di nuovi neuroni, resistente al drift di distribuzione, e non richiede mai un "rebuild".
Il critic è sempre un modello diverso dai due neuroni competitor. Usare uno dei
due come critic sarebbe barare: valuterebbe se stesso. In fase 1 il critic è
semplicemente il modello local-fast configurato (cost tier basso) con
prompt dedicato. In futuro (fase 7+) può diventare un neurone specializzato
critic_neuron, ma con la stessa regola: mai usare un neurone come critic
di se stesso.
TrustStore
Il TrustStore è l'unico componente nuovo introdotto da questo doc. Vive in
src/myclaw/rl_offline/trust.py. Espone un'API di lettura (veloce, in-memory)
e una di scrittura (batched, persistente).
workspace/state/trust.db # SQLite
└─ scores(subject_hash, tool_name, effect_class, component_blob, last_updated)
└─ thresholds(effect_class, risk_band, min_score, min_samples, cap_autonomy)
└─ events(ts, subject_hash, trace_id, delta_blob) # append-only, per replay
component_blob è un JSON delle 5 componenti + score aggregato. events è
l'audit log dei delta applicati: permette di ricostruire la storia e, se serve,
rollback di uno score (es. dopo scoperta di un abuse).
| Trigger | Cosa produce |
|---|---|
| Trace chiusa (qualunque outcome) | Esp. A: update di 1 TrustScore per ogni ActionStep unico, più 1 globale per il subject ("runtime", outcome). |
| Approval concessa / negata | Esp. A: update del segnale human_approval_delta sulla trace di origine. |
/undo eseguito |
Esp. A: flag undo_rate sulla trace originaria. |
| Synthesizer chiude un neurone con R score | Esp. B: scrittura iniziale di TrustScore per ("neuron:<name>", "invoke") con score=R, n_samples=1. |
| Self-play completato | Esp. C: writeall su synapse edge weights; aggiornamento componente tool_success del winner. |
| Job notturno (reflection cycle, ~03:00) | Ricalcolo mediani cost per classe (usato da esp. A segnale cost_efficiency); decay naturale degli score non toccati da >30g. |
| Chiamante | Metodo |
|---|---|
| Policy Check 6 (§cap3) | store.get(subject) → TrustScore (O(1) hash lookup). |
| Memory reflection filter | store.get(subject).score per filtrare FactProposal. |
| Synapse edge weight compute | store.get_for_neuron(name) per compute decay + weight. |
| Observability dashboard | store.snapshot() per leaderboard neuroni e top drift. |
from dataclasses import dataclass
from datetime import datetime
from typing import Protocol, Literal
from uuid import UUID
from myclaw.types import (
ExecutionTrace, TrustScore, TrustThreshold, TrustSubject,
Approval, ViolationReport,
)
# --- Signals estratti da una trace (esp. A) ---
@dataclass(frozen=True)
class TraceSignals:
"""Output di extract_signals(trace). Una riga per ActionStep."""
subject: TrustSubject
tool_success: float # {0, 1}
cost_efficiency: float # [0, 1]
constitution_clean: float # {0, 0.5, 1}
human_approval_delta: float # [0, 1], default 0.5 se no approvazione
undo_flag: float # {0, 1}
# --- Store ---
class TrustStore(Protocol):
async def get(self, subject: TrustSubject) -> TrustScore | None:
"""O(1). None se subject non ha storia."""
...
async def update(
self,
subject: TrustSubject,
signals: TraceSignals,
now: datetime,
) -> None:
"""EMA update con decay Ebbinghaus (half-life 30g)."""
...
async def bulk_update_from_trace(self, trace: ExecutionTrace) -> None:
"""Esp. A: un update per ogni ActionStep unico nella trace."""
...
async def record_approval_outcome(
self,
trace_id: UUID,
approval: Approval,
) -> None:
"""Quando l'approval arriva asincronamente, chiude il delta."""
...
async def record_undo(self, trace_id: UUID) -> None:
"""Quando /undo reverte un'azione, alza undo_flag per i subject."""
...
async def snapshot(self) -> dict[TrustSubject, TrustScore]:
"""Per observability dashboard / leaderboard."""
...
async def rollback(self, subject: TrustSubject, to: datetime) -> None:
"""Ricostruisce lo score allo stato di prima di to,
replayando gli events dall'append-only log. Usato se uno score
è stato corrotto (es. abuse detected)."""
...
# --- Thresholds ---
class TrustThresholdRegistry(Protocol):
def for_effect(
self,
effect_class: str,
reversibility: Literal["reversible", "reversible_with_cost", "irreversible"],
) -> TrustThreshold:
"""Ritorna la soglia di promozione per (effect_class, reversibility).
Le azioni irreversibili hanno sempre min_score=1.0 (mai promuovibili)."""
...
# --- Self-play coordinator (esp. C) ---
@dataclass
class SelfPlayResult:
winner: str # neuron_name
loser: str
critic_verdict: str # giustificazione testuale
critic_cost_usd: float
users_accepted_winner: bool | None # se il turno ha avuto feedback
class SelfPlayCoordinator(Protocol):
async def maybe_run(
self,
query: str,
candidates: list[str], # neuron names with comparable synapse weights
trace: ExecutionTrace,
) -> SelfPlayResult | None:
"""Ritorna None se i vincoli di attivazione (cap6) non sono soddisfatti."""
...
| Eccezione | Quando |
|---|---|
TrustCorruptionError | Il component_blob letto non è coerente (checksum fallito o valori fuori range). Il runtime procede con TrustScore.empty() (= nessuna fiducia) e logga evento critico. |
ThresholdNotFoundError | Policy chiede una soglia per un effect_class sconosciuto. Fallback conservativo: min_score=1.0 (mai promuovere). |
SelfPlaySkipped | Non è un errore ma un segnale: SelfPlayCoordinator.maybe_run() ha deciso di non attivarsi. Runtime usa il neurone col peso maggiore. |
| Tecnica | Chi la usa | Perché non la adottiamo |
|---|---|---|
| CISPO / GRPO / PPO on-policy | MiniMax M1, DeepSeek-R1 | Richiede infrastruttura GPU, dataset curato di migliaia di rollout, e — soprattutto — libertà di esplorazione. myclaw è un sistema single-user con azioni a side-effect reali: non possiamo lasciare esplorare a caso per raccogliere traiettorie. |
| Reward model appreso | tutti i modelli con RLHF | Richiede migliaia di annotazioni di preferenza. Abbiamo un solo annotatore (Roberto). Rule-based + human-approval già raccolti è sufficiente. |
| Thinking / non-thinking mode switchabile dal modello | Qwen3 | Adattato in policy §6 cost_tiering in forma più semplice (la policy sceglie il tier, non l'LLM). La versione Qwen richiede che il modello stesso abbia due modi di inferenza; i nostri LLM di provider non lo espongono in modo uniforme. |
| Self-play multi-agent come source di training data | Kimi K2, varie ricerche 2025 | Adattato in forma minimale (esp. C). La versione K2 usa self-play per generare dataset da cui addestrare; noi usiamo solo i pesi bandit, nessun dataset. |
| Lightning attention 1M context | MiniMax-Text-01, M1 | Architetturale del modello stesso. myclaw orchestra modelli esterni; non addestriamo né forkiamo il provider. |
| MoE routing | MiniMax, DeepSeek, Kimi | Stesso motivo: architettura del modello. Il nostro "routing" è il cost tiering, che gira a livello di workflow (policy). |
| Replay buffer + experience replay | DQN / agentic RL | Incompatibile con la privacy single-user: le trace sono personali, non costruiamo un buffer interrogabile fuori contesto. L'append-only journal è consultabile solo per audit, non per training. |
| Constitutional RL (RLAIF) | Anthropic Claude | Idea adiacente alla costituzione di myclaw, ma l'enforcement è diretto (Constitution.check() pre-flight, vedi policy §3), non indiretto via reward model. La costituzione è eseguibile, non appresa. |
| Invariante | Test |
|---|---|
| TrustScore empty per subject sconosciuto | store.get(("neuron:ghost", "invoke")) → None. La Policy interpreta None come "mai promuovere". |
| EMA con decay half-life 30g | Signals a 1.0, nessun update per 30g → score atteso 0.5 ± 0.02. |
| Azione irreversibile mai promuovibile | TrustThreshold per reversibility="irreversible" ha min_score=1.0; test inject score=0.99, atteso verdict=approve_required. |
| Undo abbassa lo score | Trace con tool_success=1, poi record_undo(trace_id) → nuovo score < quello senza undo. |
| Synthesizer R → initial TrustScore | Synthesizer approva neurone con R=0.72; subito dopo store.get(("neuron:name", "invoke")) → score≈0.72, n_samples=1. |
| Self-play skippato sotto budget | Budget rimanente 0.01€, self-play candidato costerebbe 0.05€ → maybe_run() ritorna None, via=budget. |
| Self-play skippato su side-effect irreversibile | Neuroni candidate che toccano telegram_send → maybe_run() ritorna None. |
| Critic mai è uno dei competitor | Per ogni SelfPlayResult: critic_neuron ≠ winner e critic_neuron ≠ loser. |
| Rollback è replayabile | Applicare 100 update, rollback a T-50, applicare nuovamente 50 update → score identico a quello post-100 (deterministica bit-a-bit). |
| No promozione di human_approval_rate se nessuna approvazione | Trace senza ApprovalRequest → human_approval_delta=0.5 (neutro), non influenza score verso l'alto. |
| Constitution clean penalizza law.0-2 | Trace con violation law.1 → constitution_clean=0. Dopo 100 trace clean, score torna > 0.9. |
| Append-only events | Ogni update produce una riga in events table; nessuna mai riscritta. SELECT COUNT(*) FROM events cresce monotona. |
Il razionale "perché senza training" è nei doc di Livello 1. I riferimenti qui sono solo i lavori agentici rilevanti per le adattazioni specifiche di myclaw.
| Riferimento | Cosa abbiamo preso |
|---|---|
| MiniMax M1 (CISPO, 2025) | L'idea di usare trace a lunga traiettoria come segnale denso. Noi aggreghiamo invece di addestrare (esp. A). |
| DeepSeek-R1 (GRPO + rule-based reward) | La lezione "reward verificabile > reward model appreso". Usiamo rule-based in esp. B e C. |
| Qwen3 (thinking / non-thinking) | Il cost tiering a due livelli (vedi policy §6). Il gate è fatto dalla policy, non dal modello. |
| Kimi K2 (self-critique) | Il pattern architetturale generator × generator × critic di esp. C. Noi chiudiamo con bandit update, non gradient. |
| CoALA (Sumers et al. 2023) | La reflection ciclica e la memoria a tre livelli che rende possibile estrarre segnali (§4). |
| Ebbinghaus forgetting curve / ACT-R | Il decay temporale half-life 30g (esp. A aggregation). |
| Bandit algorithms (UCB, classical) | L'aggiornamento asimmetrico dei pesi del grafo synapse (esp. C). |
| myclaw revisione incrociata di fase 1 | La necessità del TrustScore come asse unificante dei 4 driver. |
TrustScore, TrustThreshold, PlannedAction. Prerequisito di lettura.approve_required → allow (Check 6).
myclaw — rl_offline microprogettazione v1.0 — 2026-04-22
Doc trasversale di Livello 2.5. Formalizza i 3 esperimenti MiniMax-inspired senza training.