Lição 4 · Curso de Fusão · O loop fechado · como uma run terminada deixa a próxima mais inteligente
Alembic × Hermes · O Curso de Fusão · Lição 4 · O resultado-pedra-angular

O loop fechado de auto-aprimoramento

A Lição 3 batizou o loop de aprendizado como a pedra angular da fusão. Aqui é como ele de fato roda: três subsistemas — memory, learning e curator — que juntos deixam uma run terminada tornar a próxima run mais inteligente, sem nunca gravar automaticamente uma lição não-validada na memória durável.

Leia primeiro (fonte primária)
docs/adr/0018-internalize-validator-gated-self-improvement-loop.md — a decisão "propõe → dispõe"

Esta lição destila o ADR-0018 (o loop com gate do Validator) e o ancora no código real de packages/hermes/src/{memory,learning,curator}. Todo número e cada regra abaixo foram conferidos contra o arquivo-fonte — nada é inventado.

Leia a versão simples, ou abra a camada técnica em qualquer seção.
O que você vai conseguir fazer
  • Explicar por que o snapshot da memória é congelado no início da sessão — e o que isso tem a ver com o cache do prompt.
  • Distinguir propõe (o reviewer) de dispõe (o Validator) e dizer onde mora a decisão.
  • Prever o destino de uma proposta em função do seu score e do gate padrão de 0.7.
  • Nomear as quatro regras do curator e por que ele nunca deleta.
  • Defender, com base no ADR-0018 + ADR-0006, por que auto-aplicar foi rejeitado de propósito.
Suposições tolas (o que presumimos de você)
  • Você sabe o que é uma run no Alembic (um trabalho atravessando o harness) — visto nas lições anteriores.
  • Você ouviu falar em Result<T,Error> e "fail-closed". Se não, basta saber: erro nunca é silencioso.
  • O resto — snapshot, gate, dedup, máquina de estados — é construído aqui, do zero.
01

Três partes, um loop


Imagine um aprendiz que termina o expediente, escreve no caderno só as lições que de fato deram certo, e amanhã começa o dia já sabendo mais. Esse é o loop fechado. Ele tem três engrenagens que se encaixam:

memory/ é o caderno. learning/ é o ritual de fim de turno que decide o que merece entrar no caderno. curator/ é a faxina periódica das habilidades que o agente foi acumulando — sem nunca rasgar uma página.

A intuição: o segredo não é "escrever mais"; é escrever só o que passou por um filtro de qualidade. Uma run terminada melhora a próxima exatamente porque o que ela aprendeu teve de cruzar um piso antes de virar memória durável.

O contorno dos três pacotes

Os três subsistemas vivem em packages/hermes/src/: memory/ (o store com snapshot congelado), learning/ (o kernel reviewAndLearn + o gate), e curator/ (a máquina de estados determinística). Nenhum deles faz IO direto: tudo passa por ports injetados (FsPort, Clock, ModelAdapter) — é o que torna cada um testável e composável com o harness, em vez de um daemon solto.

1 · memory/ snapshot congelado no início 2 · learning/ propõe → gate → aplica 3 · curator/ active → stale → archived resumo do turno telemetria escritas aprovadas sedimentam → o próximo snapshot é mais rico curator mantém o lado das skills limpo (nunca deleta)
O fluxo do turno (laranja) anda da esquerda para a direita; a realimentação entre sessões (oliva, tracejada) retorna ao snapshot. O loop só "fecha" porque a volta passa por um filtro.
02

Em uma imagem


Antes de descer aos detalhes, fixe o mapa inteiro. Os três estágios e a seta de volta são a lição toda — o resto é zoom.

Infográfico do loop fechado de auto-aprimoramento: três cartões em sequência — memory (snapshot congelado), learning (propõe, gate, aplica, com chips applied/rejected/failed) e curator (active, stale, archived) — ligados por setas de fluxo do turno, e uma grande seta tracejada verde-oliva de realimentação retornando do learning ao memory, mostrando que escritas aprovadas sedimentam para o próximo snapshot.

O loop fechado em um quadro: o turno empurra para a direita; a aprovação sedimenta de volta à esquerda. Nada passa sem cruzar o piso de qualidade.

Faça sua aposta antes de continuar

Uma escrita na memória acontece no meio da sessão e dá certo (vai pro disco). O system prompt daquela sessão muda do meio para o fim?

Não. A escrita é durável imediatamente, mas o snapshot que entrou no prompt foi congelado no início da sessão — ele só é relido na próxima sessão. Por isso "a próxima run é mais inteligente" é literal, e o cache do prefixo do prompt fica quente o tempo todo. Detalhamos isso já na próxima seção.
03

1 · Memory — o snapshot congelado


Duas memórias em arquivo persistem entre sessões: MEMORY.md (as anotações do próprio agente) e USER.md (o que ele sabe sobre você). As duas entram no prompt como um snapshot congelado no início da sessão. A disciplina que importa:

  • Escritas no meio da sessão vão para o disco na hora (duráveis), mas não mudam o snapshot — então o cache do prefixo do prompt fica quente a sessão inteira.
  • O snapshot só é atualizado no próximo início de sessão (uma releitura nova). É isso que torna "a próxima run é mais inteligente" literal.
  • Herdado do Hermes exatamente: uma operação memory com action ∈ {add, replace, remove}; replace/remove acham o alvo por uma substring curta e única (sem IDs); entradas são separadas por § em sua própria linha; limites são em caracteres, não tokens (independente de modelo).
Por que congelar? Reescrever o caderno toda vez que você anota uma linha obrigaria o leitor (o modelo) a reler tudo do zero — caro e lento. Congelar o snapshot deixa o "início do prompt" idêntico durante a sessão; só amanhã a versão atualizada é carregada.

As constantes, direto do arquivo

O delimitador e os limites de caracteres são clonados do Hermes e ficam fixos no código (não dependem do tokenizador de nenhum modelo):

packages/hermes/src/memory/memory-store.ts:50-57
// Entry delimiter — the section sign on its own line.
export const ENTRY_DELIMITER = '\n§\n';
/** Default character limit for the MEMORY.md store (Hermes default). */
export const DEFAULT_MEMORY_CHAR_LIMIT = 2200;
/** Default character limit for the USER.md store (Hermes default). */
export const DEFAULT_USER_CHAR_LIMIT = 1375;

Este subsistema é um CLONE fiel de tools/memory_tool.py. Os desvios são deliberados: o IO é injetado via FsPort e toda operação falível devolve Result<T,Error> em vez de um dict Python.

início N snapshot congelado escrita → disco escrita → disco prompt NÃO muda (cache quente) fim N início N+1 relê snapshot mais rico o que foi gravado em N só aparece no prompt em N+1
Duráveis na hora, visíveis no prompt só na próxima sessão. Essa defasagem é uma escolha, não um descuido.
substring única (sem IDs)
"…usa pnpm, não npm…" pnpm acha 1 entrada → replace/remove
limite em caracteres, não em tokens
MEMORY.md · 2200 USER.md · 1375 mesmo teto em qualquer tokenizador
retrieval · memory
Por que o snapshot é congelado no início da sessão?
clique para virar
resposta
Para manter o cache do prefixo do prompt quente: escritas do meio vão ao disco mas não invalidam o prompt. A versão nova só é lida no próximo início.
04

2 · Learning — propõe, e o Validator dispõe


A escolha de design mais importante — ADR-0018

O Hermes grava sozinho na memória depois de um turno. O Alembic não. O reviewer só propõe; o Validator que já existe no Alembic dispõe. Escritas são controladas por gate, nunca auto-aplicadas.

Por que mudar? Duas razões do ADR, ambas de princípio:

  • Não existe um AIAgent Python no Alembic para bifurcar como thread-daemon — um passo síncrono depois da unidade, sobre ports injetados, é a unidade certa e compõe com o harness.
  • Mais importante: auto-gravar contornaria o Validator Gate e deixaria lições não-validadas endurecerem na memória durável — exatamente o modo de falha que o ADR-0006 existe para impedir ("nada sedimenta sem cruzar um piso de qualidade").
Quem propõe ≠ quem decide. É a separação entre o estagiário que sugere uma anotação e o editor que aprova. No Alembic, o "editor" é o Validator — e ele pode ser plugado depois sem mudar uma linha do kernel.
reviewer propõe (sugere) Validator / gate dispõe (decide) ReviewProposal { score } a decisão mora em verdict.approved — nunca no proposer
A linha que separa propor de decidir é a linha que o ADR-0018 traça. Por isso o Validator pode trocar sem o reviewer saber.
Infográfico do fluxo propõe-dispõe do ADR-0018: ReviewProposer devolve uma ReviewProposal, ela passa por validação Zod na fronteira, então o ReviewGate (default scoreThresholdGate 0.7) bifurca o resultado em três trilhas — applied (verde, vai para o MemoryStore com dedup), rejected (âmbar) e failed (cinza, a loja recusou) — destacando que uma rejeição é um ok() normal, não um erro.

Um único caminho que se bifurca: propôs → validou → passou pelo gate → caiu em uma das três cestas. Só um erro do proposer ou do gate aborta o passo inteiro.

Então o loop são três ports injetados e um kernel:

PortPapel
ReviewProposerDevolve ReviewProposals a partir do resumo do turno — cada uma um { target, op, rationale, score }. Em produção envolve uma chamada ao ModelAdapter; nos testes, um fake.
ReviewGateDispõe de cada proposta (aprova/rejeita). O default é scoreThresholdGate(0.7); o Validator real da coda entra depois fornecendo o seu gate — sem mudar o kernel.
MemoryStoreO store onde as escritas aprovadas se aplicam — reusando o dedup dele, então rever um fato o reforça em vez de duplicar.

reviewAndLearn — o kernel

packages/hermes/src/learning/review.ts:54-69
export const reviewAndLearn = async (summary, deps) => {
  if (summary.trim().length === 0) return ok(emptyOutcome());   // "Nada a salvar."
  const proposed = await deps.proposer(summary);
  if (!proposed.ok) return proposed;                          // erro do proposer → fail-closed
  if (proposed.value.length === 0) return ok(emptyOutcome());
  const acc = { applied: [], rejected: [], failed: [] };
  for (const raw of proposed.value) {
    const stepErr = await processOne(raw, deps, acc);   // valida → gate → aplica
    if (stepErr) return stepErr;                            // erro do gate → fail-closed
  }
  return ok({ applied: acc.applied, rejected: acc.rejected, failed: acc.failed });
};

Três cestas de resultado — applied / rejected / failed — então nada some em silêncio. A saída do proposer é validada com Zod na fronteira (em produção é saída de modelo não-confiável). Um erro do proposer ou do gate falha o passo inteiro fechado; já uma recusa da loja a uma escrita aprovada é registrada em failed, nunca lançada.

applied

passou no gate e a loja aceitou. Sedimenta no próximo snapshot.

rejected

o gate disse não (ex.: score baixo). Resultado normal, não erro.

failed

passou no gate, mas a loja recusou. Registrado, jamais lançado.

resumo do turno proposer 1 chamada Zod na fronteira gate ≥ 0.7 ? applied → MemoryStore rejected failed
Uma proposta tem três destinos possíveis. Repare: o caminho do erro (fail-closed) sai do proposer e do gate — não da bifurcação.
Exemplo resolvido · seguindo uma proposta
1
O turno termina. reviewAndLearn(summary, deps) recebe o resumo. Se o resumo for vazio, retorna ok(emptyOutcome()) — "nada a salvar".
2
O proposer roda e devolve uma proposta: { target:"USER.md", op:"add", rationale:"usa pnpm", score:0.82 }.
3
Zod valida a forma da proposta na fronteira. Forma inválida → erro → o passo inteiro falha fechado.
4
O gate compara: 0.82 ≥ 0.7ok({approved:true}). A escrita é aplicada via MemoryStore (com dedup) e cai em applied.
5
Agora você: a mesma proposta volta com score:0.55. Em que cesta ela cai e por quê? (Responda antes de abrir o gate ao vivo abaixo.)
05

O gate padrão, ao vivo


O gate padrão é uma função pura: compara o score com um limiar e devolve um veredito. Mexa no controle e veja a decisão mudar. A fronteira é inclusiva: score === limiar aprova.

scoreThresholdGate — o gate conservador padrão

packages/hermes/src/learning/gate.ts:24-36
export const scoreThresholdGate = (min = DEFAULT_REVIEW_SCORE_THRESHOLD) => {
  return async (proposal) => {
    const approved = proposal.score >= min;          // fronteira inclusiva: score === min aprova
    const reason = approved
      ? `score ${proposal.score} ≥ threshold ${min}`
      : `score ${proposal.score} < threshold ${min} (learn only from validated wins)`;
    return ok({ approved, reason });                  // puro + total: ok(verdict) para toda entrada
  };
};

O limiar padrão é 0.7 — a codificação mecânica da regra do hermes-mini-loop: "aprenda só com vitórias validadas". Note que a decisão mora em verdict.approved, não no Result: uma rejeição é um ok(...) normal, não um erro.

score = 0.82 limiar = 0.70
0.70
score 0.82 ≥ threshold 0.70 → aprovado · cesta applied
dicaArraste o score para baixo de 0.70: o veredito vira rejected — e mesmo assim continua sendo um ok(...). "Rejeitado" não é "deu erro".
06

3 · Curator — a metade do descarte


O agente cria skills; a telemetria de uso se acumula; o curator é o passo determinístico que mantém a biblioteca de skills limpa. É um CLONE fiel de agent/curator.py:apply_automatic_transitions, com quatro regras clonadas exatamente:

  • Gate de proveniência: só skills com createdBy === 'agent' são tocadas; o resto é pulado.
  • Isenção de pin: uma skill pinned nunca é transicionada, em nenhum caminho.
  • Nunca deleta: o estado terminal é archived — "ação máxima = arquivar". Não há remoção.
  • As quatro transições: active/stale além do corte de arquivo → archived; active além do corte de stale → stale; uma skill stale usada de novo → reativada para active.
Faxina, não demolição. O curator é o bibliotecário que move livros pouco usados para o depósito (stale) e depois para o arquivo morto (archived) — mas nunca joga um livro fora. Se alguém pega um do depósito, ele volta à estante.

Tempo é injetado — nunca Date.now()

O tempo é um Clock injetado — nunca Date.now() (a regra de determinismo do motor, e o que torna os testes de transição reproduzíveis). O curator usa o mesmo Clock com que o store de uso foi construído, então um evento registrado "agora" e uma transição decidida "agora" concordam. Os cortes são now - staleAfterMs e now - archiveAfterMs.

packages/hermes/src/curator/curator.ts (cabeçalho + ramo nextState)
// só curator-managed (createdBy === 'agent'), não-pinned, são considerados
if (record.createdBy !== 'agent') { skipped.push(...); continue; }
if (record.pinned)            { skipped.push({ name, reason: 'pinned' }); continue; }
const to = nextState(record, staleCutoff, archiveCutoff);  // terminal = archived
active stale archived passou staleAfter usada de novo → reativa passou archiveAfter active além do archiveAfter → archived (pula stale)
Três estados, quatro setas — e nenhuma delas leva ao "deletado". O terminal é archived.
gate de proveniência
createdBy: 'agent' ✓ createdBy: 'user' — pula transições
isenção de pin
pinned: true fixada nunca transiciona → imune a stale e archive 📌
retrieval · curator
Qual é o estado terminal do curator — e o que NÃO existe?
clique para virar
resposta
Terminal = archived. Não existe caminho de delete: a ação máxima é arquivar. Skills pinned ou não-'agent' nem são consideradas.
07

Por que com gate, e não auto-aplicar


Auto-aplicar seria mais rápido. Foi rejeitado de propósito. O ADR-0018 considerou "auto-aplicar escritas depois de cada run (o comportamento literal do Hermes)" e rejeitou: contorna o Validator Gate e deixa lições não-validadas endurecerem na memória durável — o exato modo de falha que o ADR-0006 existe para impedir. A graça toda da fusão é que o loop compõe com o pipeline de gates em vez de passar por fora dele.
aspectoauto-aplicar
velocidademais rápido (sem passo)
passa pelo Validator?não — contorna o gate
lição ruim na memóriaendurece como durável
compõe com o harness?é um daemon à parte
aspectocom gate
velocidade+1 passo síncrono barato
passa pelo Validator?sim — piso de qualidade
lição ruim na memóriarejeitada antes de sedimentar
compõe com o harness?passo pós-unidade, testável
Lembre-se: "com gate" não quer dizer "lento" nem "humano no meio". O gate padrão é um score ≥ 0.7 puro, sem humano e sem IO. "Com gate" quer dizer um piso de qualidade tem de ser cruzado.
ADR-0018 loop com gate ADR-0006 Validator = piso de emissão existe para honrar auto-aplicar quebraria esta seta — por isso foi rejeitado
O loop com gate não é capricho: é a forma de o ADR-0018 respeitar o piso de qualidade que o ADR-0006 já tinha estabelecido.
08

Confusões comuns


cuidado"O reviewer é um daemon de fundo, como no Hermes." Não — no Alembic é um passo síncrono pós-unidade sobre ports injetados (ADR-0018). Sem thread, sem fork; é isso que o torna testável e composável com o harness.
cuidado"Com gate significa lento / humano aprovando cada escrita." Não — o gate padrão é um score ≥ 0.7 puro, sem humano e sem IO. O piso pode depois virar o Validator completo da coda injetando outro gate — o kernel nunca muda.
guardeQuem propõe não é quem dispõe. O reviewer propõe; o Validator dispõe. Essa única frase carrega o ADR-0018 inteiro.
09

Recapitulando · o deck de fechamento


Cinco cartas, a lição inteira. Avance com os botões, as setas do teclado ou os pontos.

1 · A grande ideia

Uma run terminada deixa a próxima mais inteligente

Três engrenagens: memory (o caderno), learning (o filtro de fim de turno) e curator (a faxina). O loop "fecha" porque a volta passa por um piso.

memory learning curator
1
2 · Memory

Snapshot congelado no início da sessão

Escritas do meio vão ao disco mas não mudam o prompt — o cache fica quente. A versão nova é lida só na próxima sessão. Limites em caracteres (2200 / 1375).

iníciocongela → disco N+1relê
2
3 · Learning

Propõe → gate → dispõe

O reviewer propõe; o Validator dispõe. Três cestas: applied / rejected / failed. Só um erro do proposer ou do gate falha o passo fechado.

gate ≥ 0.7 appliedrejectedfailed
3
4 · Curator

Arquiva, nunca deleta

active → stale → archived; stale usada de novo → reativa. Só skills createdBy:'agent' e não-pinned. Tempo = Clock injetado.

active stale archived
4
5 · A ideia para guardar

Compõe com o gate, não passa por fora

Auto-aplicar foi rejeitado (ADR-0018) porque contornaria o Validator e violaria o ADR-0006. Nada sedimenta sem cruzar o piso.

escrita gate memory ✗ atalho auto-aplicar (rejeitado)
5
Carta 1 / 5 navegam
10

Verifique seu entendimento


Três perguntas — fecha a lição

Escolha uma opção em cada. O placar abaixo conta seus acertos.

1. Uma escrita na memória dá certo no meio da sessão. O system prompt muda no restante dela?
Correto: b. O snapshot é congelado no início da sessão. Escritas são duráveis na hora, mas não invalidam o prefixo do prompt — é justamente o ponto. "A próxima run é mais inteligente" é literal: a releitura acontece no próximo carregamento.
2. O reviewer propõe uma escrita com score: 0.6 e o gate padrão está em uso. O que acontece?
Correto: d. O scoreThresholdGate(0.7) padrão retorna ok({approved:false, reason}) — uma rejeição é um resultado normal, não um erro. Ela vai para rejected; só um erro do proposer/gate falha o passo fechado.
3. O curator encontra uma skill há muito sem uso, com pinned: true e createdBy: 'user'. O que ele faz?
Correto: c. Duas guardas se aplicam: o gate de proveniência só toca skills com createdBy === 'agent', e skills pinned nunca são transicionadas. Além disso, o estado terminal é archived — não há caminho de delete.
Acertos: 0/3
As cinco verdades do loop fechado
  1. O snapshot da memória é congelado no início da sessão — duráveis na hora, visíveis no prompt só amanhã.
  2. Limites de memória são em caracteres (2200 / 1375), independentes de modelo.
  3. O reviewer propõe; o Validator dispõe. A decisão mora em verdict.approved.
  4. Três cestas — applied / rejected / failed — e nada some em silêncio.
  5. O curator arquiva, nunca deleta; tempo é um Clock injetado, jamais Date.now().
Pergunta para levar adiante: se o Validator completo da coda virar o gate, o que muda no kernel reviewAndLearn? (Resposta: nada — só o port ReviewGate injetado. É o teste de fogo de um bom seam, e o tema da próxima lição, "Ports e injeção".)

Fontes (tudo no repo)