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.
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.
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.
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.
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)
limite em caracteres, não em tokens
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.
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.
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:
Port
Papel
ReviewProposer
Devolve 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.
ReviewGate
Dispõ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.
MemoryStore
O 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-closedif (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 → aplicaif (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.
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.7 → ok({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.
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.82limiar = 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 Clockinjetado — nunca Date.now() (a regra de determinismo do motor, e o que torna os testes de transição reproduzíveis). O curator usa o mesmoClock 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.
// só curator-managed (createdBy === 'agent'), não-pinned, são consideradosif (record.createdBy !== 'agent') { skipped.push(...); continue; }
if (record.pinned) { skipped.push({ name, reason: 'pinned' }); continue; }
const to = nextState(record, staleCutoff, archiveCutoff); // terminal = archived
Três estados, quatro setas — e nenhuma delas leva ao "deletado". O terminal é archived.
gate de proveniência
isenção de pin
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.
aspecto
auto-aplicar
velocidade
mais rápido (sem passo)
passa pelo Validator?
não — contorna o gate
lição ruim na memória
endurece como durável
compõe com o harness?
é um daemon à parte
aspecto
com gate
velocidade
+1 passo síncrono barato
passa pelo Validator?
sim — piso de qualidade
lição ruim na memória
rejeitada 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.
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.
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).
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.
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.
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.
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
O snapshot da memória é congelado no início da sessão — duráveis na hora, visíveis no prompt só amanhã.
Limites de memória são em caracteres (2200 / 1375), independentes de modelo.
O reviewer propõe; o Validator dispõe. A decisão mora em verdict.approved.
Três cestas — applied / rejected / failed — e nada some em silêncio.
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".)