Lição 8 · Curso de Fusão · Aprendizado profundo — reviewAndLearn, o passo com gate
Alembic × Hermes · Curso de Fusão · mergulho profundo · subsistema 2 de 7

Aprendizado profundo: reviewAndLearn

Como um turno que terminou ensina o próximo. Um revisor propõe escritas duráveis de memória; um gate do Validator dispõe; o que for aprovado se sedimenta no MemoryStore. A restrição-mestra — tirada direto da matriz de fusão e do ADR-0018 — é que aprender é gated, não auto-apply. Este é um ADAPT do fork de revisão em background do Hermes para o estilo de ports do motor: sem thread daemon, só três ports injetados.

Leia primeiro (fonte primária)
packages/hermes/src/learning/ — review.ts + gate.ts + types.ts, com a prova em review.test.ts

Tudo nesta página é citado de arquivo real do monorepo. O único conceito em torno do qual a lição inteira gira é o passo de aprendizado com gate: o modelo só propõe; um gate decide; só o aprovado vira escrita durável. Nada aqui é 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 aprender é gated, não auto-apply — e qual ADR fixa essa regra.
  • Seguir o fluxo propor → gate → aplicar de reviewAndLearn e dizer o que faz o passo falhar fechado.
  • Distinguir os três baldes — applied, rejected, failed — e por que failedrejected.
  • Ler o gate padrão scoreThresholdGate() e justificar o piso inclusivo em 0,7.
  • Separar o Result (a engrenagem funcionou?) do verdict.approved (qual foi a decisão?).
Suposições tolas (o que presumimos de você — bem pouco)
  • Você já viu a lição 7 (Memória profunda) e sabe que o MemoryStore guarda escritas duráveis e já faz dedup.
  • Você sabe que Result<T, E> é "ou deu certo (ok) ou deu errado (err)" — uma união discriminada que nunca lança.
  • Você não precisa saber Python nem como o Hermes roda. A gente explica o que foi adaptado e por quê.
Mergulhos profundos · você está no subsistema 2 de 7 1 · memóriastore 2 · aprendizadovocê está aqui 3 · curadoria 4 · clarificação 5 · web 6 · skills 7 · mídia
Sete subsistemas adaptados do Hermes; esta lição abre o segundo — o passo de aprendizado.
1

A grande ideia


Imagine um turno de trabalho que acabou. O agente resume o que aconteceu e, dessa lição, quer guardar algumas coisas para o futuro: "tal abordagem funcionou", "tal arquivo é o ponto certo". A pergunta perigosa é: guardamos tudo que o modelo sugere?

A resposta do Alembic é um não firme. O modelo apenas propõe. Antes de qualquer coisa virar memória durável, uma porta de controle — o gate — decide se a proposta é boa o bastante. Só o que passa é escrito. É por isso que chamamos de passo de aprendizado com gate.

Analogia. Pense num caderno de aprendizados de uma equipe. Qualquer pessoa pode sugerir uma anotação numa folha de rascunho. Mas só o revisor decide o que é copiado a limpo no caderno oficial. O rascunho fica cheio de ideias; o caderno só recebe o que vale. O modelo escreve no rascunho; o gate é o revisor.

O contrato em uma frase

A função pública é reviewAndLearn(summary, deps) e devolve Promise<Result<LearnOutcome, Error>>. Ela é pequena e total: recebe um resumo de texto e três ports em deps — o proposer (que em produção embrulha uma chamada de modelo), o gate (a política de aprovação) e o memory (o MemoryStore). Nenhum daemon, nenhuma thread: a mesma forma do Hermes reescrita como ports injetados.

Por que ADAPT e não CLONE

O Hermes faz isso forkando uma thread de revisão em background dentro do seu AIAgent em Python. O Alembic não tem esse runtime, então a mesma disciplina (propor → gate → aplicar) é remodelada em ports puros e testáveis. A disciplina é idêntica; o encanamento cabe no motor.

turno termina gera um summary reviewAndLearn propor → gate → aplicar MemoryStore escrita durável próximo turno já sabe mais
O turno N ensina o turno N+1 — mas só via memória que passou pelo gate.
auto-apply (o que NÃO fazemos) modelo memória qualquer sugestão vira verdade gated (o que fazemos) modelo gate memória só vitória validada sedimenta
A única diferença é a caixa do meio. Mas é ela que muda tudo.
Guarde istoGated, não auto-apply. O modelo nunca grava direto. Entre a proposta e a memória sempre existe uma decisão. Essa única regra é a alma da lição.
2

A forma: propor → gate → aplicar


O fluxo tem três estágios e três destinos. Cada proposta percorre os estágios e cai em um de três baldes de resultado. Veja o desenho inteiro antes do código:

Infográfico de pipeline horizontal: três caixas-estágio (proposer → gate → memory.apply) ligadas por setas cor-de-tijolo, e à direita três cartões-resultado empilhados — applied em verde-oliva, rejected em vermelho-ferrugem, failed em dourado — com legenda explicando que erro de infraestrutura derruba o passo e recusa de política apenas registra e segue.

O caminho de uma proposta: do proposer ao gate, e do gate para um dos três baldes. Erro de infraestrutura para o passo; recusa de política apenas registra e continua.

proposer chamada de modelo gate score ≥ 0,7 memory.apply dedup reaproveitado applied[] rejected[] · gate disse não failed[] · store disse não
O gate manda para rejected; o memory.apply manda para applied ou failed.

O driver é pequeno e total. Um resumo vazio ou zero propostas curto-circuita para um resultado vazio (um passo no-op é válido); um erro do proposer ou do gate faz o passo inteiro falhar fechado:

// packages/hermes/src/learning/review.ts — reviewAndLearn
export const reviewAndLearn = async (
  summary: string, deps: ReviewDeps,
): Promise<Result<LearnOutcome, Error>> => {
  if (summary.trim().length === 0) return ok(emptyOutcome());
  const proposed = await deps.proposer(summary);
  if (!proposed.ok) return proposed;                // erro do proposer ⇒ falha fechado
  if (proposed.value.length === 0) return ok(emptyOutcome());
  const acc: OutcomeAcc = { applied: [], rejected: [], failed: [] };
  for (const raw of proposed.value) {
    const stepErr = await processOne(raw, deps, acc);
    if (stepErr) return stepErr;                    // erro do gate / forma ruim ⇒ falha fechado
  }
  return ok({ applied: acc.applied, rejected: acc.rejected, failed: acc.failed });
};

Trecho citado verbatim de review.ts. Note os dois return que saem cedo: resumo vazio e zero propostas.

Preveja antes de revelar

O proposer devolve ok([]) — uma lista vazia de propostas (não houve nada a salvar). O que reviewAndLearn retorna?

Retorna ok(emptyOutcome()) — um resultado de sucesso com os três baldes vazios. Lista vazia não é erro: um passo no-op é válido. Só um err do proposer (a engrenagem quebrou) derruba o passo.
reviewAndLearn summary vazio → ok(vazio) proposer deu err → err (falha fechado) zero propostas → ok(vazio)
Dois caminhos para ok vazio (nada a fazer) e um para err (a máquina quebrou) — tudo antes de tocar no laço.

Por que "total"

"Total" quer dizer que toda entrada tem uma saída bem definida — a função nunca lança, nunca trava num caso não tratado. Resumo vazio, zero propostas, erro do proposer, erro do gate, escrita recusada: cada um tem um caminho explícito. É isso que torna o passo seguro de chamar a cada turno sem try/catch em volta.

O laço delega para processOne

O for não decide nada sozinho: ele chama processOne(raw, deps, acc) para cada proposta crua e só observa uma coisa — se voltou um stepErr (um Err), o passo inteiro para e devolve esse erro. Caso contrário, segue. Toda a lógica de "qual balde" vive em processOne, que vamos abrir agora.

3

Os três baldes — e por que failedrejected


O resultado separa três destinos. A distinção entre rejected (o gate disse não) e failed (o gate disse sim, mas o store não conseguiu gravar) é load-bearing para observabilidade — uma escrita estourada de orçamento nunca pode ser confundida com uma recusa de política:

// packages/hermes/src/learning/review.ts — processOne
const parsed = reviewProposalSchema.safeParse(raw);   // saída de modelo não confiável
if (!parsed.success) return err(new Error(`Invalid review proposal: …`));
const proposal = parsed.data;
const verdict = await deps.gate(proposal);
if (!verdict.ok) return verdict;                      // ERRO do gate ⇒ derruba o passo
if (!verdict.value.approved) {
  acc.rejected.push({ proposal, reason: verdict.value.reason }); // gate disse NÃO
  return undefined;                                 // continua o passo
}
const written = await deps.memory.apply(proposal.target, proposal.op);
if (!written.ok) {
  acc.failed.push({ proposal, reason: written.error.message }); // store disse NÃO
  return undefined;                                 // ainda continua
}
acc.applied.push(proposal);
Leia o tipo de retorno. processOne devolve Result<never, Error> | undefined. O undefined significa "esta proposta foi tratada — continue o passo". Um Err significa "pare o passo inteiro fechado". Ou seja: uma única proposta ser rejeitada ou falhar no store não aborta o lote; só um erro de infraestrutura (forma de proposta inválida, falha do gate) aborta.

A tabela dos três fins

BaldeQuem decidiuO que aconteceuO passo…
appliedstore gravougate aprovou e memory.apply deu okcontinua
rejectedgategate devolveu ok({approved:false}) — política recusoucontinua
failedstoregate aprovou, mas memory.apply deu err (ex.: orçamento)continua
(passo falha)infraestruturaforma inválida ou gate devolveu errpara fechado
forma válida? não sim err ⇒ passo PARA gate ok(reprovado) ok(aprovado) err ⇒ PARA rejected[] memory.apply (erro do gate)
Quatro fins, uma proposta. Só os ramos em vermelho-vivo (err) param o passo.

Explore: para onde vai cada proposta?

applied[]

Aprovada e gravada

O gate devolveu ok({approved:true}) e memory.apply devolveu ok. A proposta entra em applied e o passo segue para a próxima.

acc.applied.push(proposal)
qual balde acende
applied[] rejected[] failed[]

Um lote inteiro, em uma imagem

Cinco propostas entram juntas. Nenhuma rejeição nem falha de store derruba o lote — cada uma só cai no seu balde, e reviewAndLearn devolve ok com a contagem dos três:

5 propostas P1 · 0,92 P2 · 0,88 P3 · 0,55 P4 · 0,80 P5 · 0,99 applied[] · P1, P2, P4aprovadas e gravadas rejected[] · P3 failed[] · P5aprovada, store estourou ok(outcome)3 / 1 / 1
P3 reprovada e P5 falha de store não abortam nada: o lote inteiro completa e devolve ok.
Erro clássicoConfundir failed com rejected. Se você tratar os dois como "não entrou", perde a informação mais útil: por que não entrou. Recusa de política e falha de gravação pedem ações diferentes — uma é decisão, a outra é incidente.
4

O gate padrão: aprender só de vitórias validadas


Até o Validator Gate real do @alembic/coda (ADR-0006) plugar o seu próprio ReviewGate, o padrão conservador aprova uma proposta se e somente se score ≥ 0,7 — a codificação mecânica de "aprender só de vitórias validadas", herdada do hermes-mini-loop:

// packages/hermes/src/learning/gate.ts — scoreThresholdGate
export const scoreThresholdGate = (
  min: number = DEFAULT_REVIEW_SCORE_THRESHOLD,   // 0.7
): ReviewGate => {
  return async (proposal) => {
    const approved = proposal.score >= min;        // fronteira INCLUSIVA
    const reason = approved
      ? `score ${proposal.score} ≥ threshold ${min}`
      : `score ${proposal.score} < threshold ${min} (learn only from validated wins)`;
    return ok({ approved, reason });               // puro & total: sempre ok(verdict)
  };
};

Brinque com o piso

0 1 piso 0,7

À esquerda do piso, reprova; em 0,70 exato, aprova (a fronteira é inclusiva).

Onde mora o 0.7

O número não está cravado no gate: ele vem de DEFAULT_REVIEW_SCORE_THRESHOLD = 0.7 em learning/types.ts. scoreThresholdGate() usa esse default, mas aceita um min por parâmetro — então um chamador pode endurecer ou afrouxar o piso sem tocar no kernel.

Injeção, não edição

Quando o Validator real do coda entrar, ele não reescreve esta função: ele injeta o seu próprio ReviewGate em deps.gate. scoreThresholdGate() é só a linha de base segura para quando ninguém forneceu um gate. É o mesmo padrão de ports da cintura estreita — trocar a política por injeção.

reviewAndLearn o kernel não muda deps.gate (slot) scoreThresholdGate() hoje · default 0,7 Validator do coda amanhã · ADR-0006 injeta um OU outro
A política é plugável: o mesmo slot recebe o gate de score hoje e o Validator do coda amanhã.
Exemplo resolvido — três propostas, um piso de 0,7
1
Proposta A com score: 0,92. 0,92 ≥ 0,7aprovada. Segue para memory.apply.
2
Proposta B com score: 0,55. 0,55 ≥ 0,7 é falso → reprovada, vai para rejected com a razão "learn only from validated wins".
3
Proposta C com score: 0,70. A fronteira é inclusiva → aprovada. Por pouco, mas entra.
Agora você: uma proposta com score: 0,699. Aprova ou reprova? Reprova — está abaixo de 0,7, e o piso é inclusivo só em 0,7, não abaixo.
5

O veredito é dado, não o Result


Esta é a sutileza que vale internalizar. O gate devolve ok({approved:false, …}) para uma reprovação — não err. O invólucro Result sinaliza se o gate funcionou; o verdict.approved carrega a decisão. Um gate que deu erro (ex.: o serviço do Validator caiu) devolve err e para o passo.

Diagrama de duas colunas: à esquerda Result com um cadeado e dois ramos (ok = o gate funcionou, err = a engrenagem quebrou e para o passo); à direita verdict.approved com uma balança e dois chips (true = tenta gravar, false = rejected e o passo continua); uma seta pontilhada liga ok à leitura do verdict, com destaque dizendo que recusa do gate é ok com approved false, nunca err.

Duas camadas de sinal. O Result diz se a máquina rodou; o verdict diz o que ela decidiu. Só se lê o veredito quando o Result é ok.

Por que essa separação é load-bearing

É exatamente isso que deixa reviewAndLearn distinguir "a política recusou isto" de "a maquinaria da política quebrou". Se uma reprovação fosse err, qualquer proposta fraca derrubaria o passo inteiro — e você não conseguiria coletar o resto do lote nem saber por que cada uma caiu. Reprovação é ok({approved:false}) e vira rejected; erro é err e para tudo.

recusa de política
return ok({
  approved: false,
  reason: 'score 0.55 < 0.7'
});
// → rejected[], passo segue
máquina quebrou
return err(
  new Error('Validator offline')
);
// → passo PARA fechado
Result<Verdict> a engrenagem rodou? ok err lê verdict.approved passo PARA true → applied/failed false → rejected[]
Duas decisões aninhadas: primeiro "rodou?", só depois "aprovou?".
Flashcard
O gate reprovou uma proposta fraca. Que valor ele retorna?
clique para virar
Resposta
ok({approved:false, reason}) — um Result de sucesso cuja decisão é "não". Nunca err.
Flashcard
Quando o gate retorna err?
clique para virar
Resposta
Quando a maquinaria falhou — ex.: o serviço do Validator está fora. Aí o passo inteiro para fechado.
Flashcard
O que o Result sinaliza, então?
clique para virar
Resposta
Se o gate funcionou. A decisão (aprovar/reprovar) mora em verdict.approved, dentro do valor.
6

Reforçar, não duplicar


O laço não tem lógica de dedup própria. Escritas aprovadas fluem pelo dedup que já existe no MemoryStore — repropor uma entrada que já está lá é um no-op de sucesso (então conta como applied, mas o store continua com uma entrada). Isso espelha a intenção do ON CONFLICT DO UPDATE do mini-loop: reforçar, não empilhar duplicatas.

proposta "X" #1 proposta "X" #2 MemoryStore dedup interno 1 entrada #2 = no-op ok
Duas vezes a mesma proposta; o store guarda uma. A segunda é sucesso, mas não cresce nada.
Uma fonte de verdade para dedup. Se o laço de aprendizado tivesse o próprio dedup, haveria duas políticas que poderiam discordar. Roteando tudo pelo store, existe uma só. A validação da forma também mora na fronteira: reviewProposalSchema (em learning/types.ts) valida a saída do modelo, incluindo score limitado a [0, 1] — um score: 1.5 de um modelo malcomportado é rejeitado antes de qualquer escrita.
score: 1.5 score: 0.9 reviewProposalSchema score ∈ [0, 1] 1.5 barrado 0.9 segue err · passo PARA vai ao gate
A forma é validada antes de tudo: um score fora de [0,1] nunca chega ao gate.
Dica de designQuando duas camadas precisam concordar sobre uma regra (aqui, "o que é duplicado"), faça uma ser dona da regra e a outra delegar. Duas implementações da mesma política é uma fonte garantida de divergência futura.
7

Confusões comuns


"Ele aplica automaticamente o que o modelo propõe." O oposto — é gated por Validator por design (ADR-0018). O modelo só propõe; um gate decide. O gate padrão é conservador (score ≥ 0,7), e o Validator real do coda pode substituí-lo por injeção sem tocar neste kernel.
"Sem daemon significa que é um mecanismo diferente." É um ADAPT, não um port literal: o Hermes forka uma thread em background; o Alembic não tem o runtime AIAgent em Python, então a mesma forma propor → gate → aplicar vira ports injetados (ReviewProposer, ReviewGate, MemoryStore). A disciplina é idêntica; o encanamento cabe no motor.
Hermes (origem) AIAgent (Python) thread de background forka, revisa, escreve Alembic (ADAPT) reviewAndLearn (ports) ReviewProposer ReviewGate MemoryStore mesma disciplina · propor → gate → aplicar
O que muda é o encanamento (thread → ports). O que não muda é a disciplina.
"failed é o mesmo que rejected." Não. rejected é uma decisão de política (o gate disse não). failed é um incidente de escrita (o gate disse sim, mas o store não conseguiu — ex.: orçamento). Tratá-los igual apaga a informação que importa.

Verdadeiro ou falso (responda mentalmente)

AfirmaçãoVereditoPorquê
Uma proposta reprovada derruba o passo inteiro.FalsoReprovação é ok({approved:false})rejected; o passo continua.
Um score: 1.5 chega ao gate.FalsoO schema barra score fora de [0,1] antes do gate.
Repropor uma entrada existente cria duplicata.FalsoO dedup do store faz disso um no-op de sucesso.
Um erro do gate faz o passo falhar fechado.Verdadeiroerr do gate é retornado e para tudo.
8

Recapitulando


A virada de chave

Aprender é gated

O modelo só propõe; um gate decide; só o aprovado vira memória durável. ADR-0018: gated, não auto-apply.

A forma

Propor → gate → aplicar

reviewAndLearn é pequeno e total. Resumo vazio ou zero propostas = no-op ok; erro do proposer/gate = falha fechado.

Os destinos

Três baldes

applied (gravou), rejected (gate disse não), failed (store disse não). failedrejected — decisão vs incidente.

O gate padrão

Piso inclusivo em 0,7

scoreThresholdGate(): approved = score >= 0,7. 0,69 reprova, 0,70 aprova. "Aprender só de vitórias validadas". Trocável por injeção.

A sutileza

Veredito ≠ Result

Reprovação é ok({approved:false}), nunca err. O Result diz se rodou; verdict.approved diz a decisão. Erro para o passo.

Para a próxima

Da memória à curadoria

A lição 9 entra no curador profundo: como o sistema escolhe e ranqueia o melhor conteúdo antes de internalizá-lo.

1 / 6setas

Simples ↔ Técnico: a mesma ideia, duas alturas

Um turno termina e sugere lições. Um revisor decide o que vale guardar. Só o aprovado entra no caderno oficial. As sugestões recusadas ficam registradas (para você saber o que foi descartado), e se a caneta falhar na hora de escrever, isso é anotado à parte — não é a mesma coisa que "o revisor disse não".

reviewAndLearn(summary, deps) chama deps.proposer para obter ReviewProposal[], valida cada uma com reviewProposalSchema, passa por deps.gate (default scoreThresholdGate(0.7)) e, se verdict.approved, chama deps.memory.apply(target, op). Resultado: LearnOutcome{applied, rejected, failed}. err só em forma inválida ou erro do gate; falha de escrita vira failed.

Revisão rápida — fixe o essencial
1. O gate devolve ok({approved:false, reason:'fraca'}). Onde a proposta vai parar?
Correto: c. Uma recusa do gate é ok({approved:false})rejected[]. failed[] é reservado para propostas que o gate aprovou mas o store não gravou. Só um erro do gate (err) abortaria o passo.
2. Uma proposta tem score: 0,69 contra o scoreThresholdGate() padrão. O resultado?
Correto: b. approved = score >= min com min = 0,7. 0,69 está abaixo do piso inclusivo, então é reprovada com a razão "learn only from validated wins". Exatamente 0,70 aprovaria.
3. Por que o laço de aprendizado reusa o dedup do MemoryStore em vez de ter o seu?
Correto: d. Rotear escritas aprovadas pelo dedup do store mantém uma única política. Repropor uma entrada existente tem sucesso (conta como applied) mas não faz o store crescer — espelhando o ON CONFLICT DO UPDATE do mini-loop.
4. O que o invólucro Result ao redor do veredito do gate sinaliza?
Correto: a. O Result diz se a maquinaria do gate rodou (ok) ou quebrou (err). A decisão aprovar/reprovar é dado dentro do valor — verdict.approved —, não o invólucro.
Acertos: 0/4
As cinco coisas para levar desta lição
  1. Aprender é gated, não auto-apply (ADR-0018): o modelo propõe, um gate dispõe.
  2. A forma é propor → gate → aplicar, pequena e total; erro de infraestrutura faz falhar fechado.
  3. Três baldes: applied, rejected, failed — e failed (incidente) ≠ rejected (decisão).
  4. O gate padrão é score ≥ 0,7, piso inclusivo, trocável por injeção pelo Validator do coda.
  5. O Result diz se a engrenagem rodou; verdict.approved diz a decisão — recusa é ok, nunca err.
9

Verificar


A prova desta lição não é a leitura — é a suíte. O arquivo review.test.ts tem 14 casos que fixam cada afirmação feita aqui: a fronteira 0,69/0,70, failed vs rejected, reforçar-não-duplicar e o fail-closed em erros de proposer/gate. Rode os testes do pacote:

# a prova do passo de aprendizado, isolada
pnpm --filter @alembic/hermes test

# a baseline completa do monorepo
pnpm -r typecheck && pnpm -r build && pnpm -w test
LearnOutcome — o que você inspeciona depois do turno applied[] → o que aprendi rejected[] → o que recusei failed[] → o que quis mas falhou
Cada balde carrega a razão. É por isso que separar rejected de failed não é purismo: é observabilidade.
Recapitulando em uma frase: um turno terminado propõe aprendizados, um gate conservador dispõe, e só o aprovado sedimenta no MemoryStore — gated, observável em três baldes, e falho-fechado quando a máquina quebra.

As fontes (todas no repo, lidas verbatim)

Curioso para ver de onde vem o resumo que alimenta este passo? A lição 7 (Memória profunda) abre o MemoryStore e o seu dedup; a lição 9 (Curadoria profunda) mostra como o sistema escolhe o que vale ranquear. As três juntas formam o ciclo de aprendizado do motor.