Curso / Lição 8  ·  EN
Lição 08 · Mergulho profundo · subsistema 2 de 7

reviewAndLearn — o passo de aprendizado com portão

Como um turno finalizado ensina o próximo. Um revisor propõe escritas duráveis de memória; um portão Validador dispõe; escritas aprovadas sedimentam no MemoryStore. A restrição-chave — tirada direto da matriz de fusão e do ADR-0018 — é que o aprendizado é com portão, não auto-aplicado. É um ADAPT do fork de background-review do Hermes para o estilo de portas do motor: sem thread de daemon, apenas três portas injetadas.

A forma: propor → portão → aplicar

proposer chamada de modelo (prod) gate score ≥ 0.7 memory.apply dedup reusado applied[] rejected[] (portão disse não) failed[] (store disse não)

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

// packages/hermes/src/learning/review.ts:54-70
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 });
};

Os três baldes — e por que failedrejected

O resultado separa três destinos. A distinção entre rejected (o portão disse não) e failed (o portão disse sim mas o store não conseguiu escrever) é estrutural para observabilidade — uma escrita acima do orçamento nunca pode ser confundida com uma recusa de política:

// packages/hermes/src/learning/review.ts:78-103 — 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 ⇒ falha o passo
if (!verdict.value.approved) {
  acc.rejected.push({ proposal, reason: verdict.value.reason }); // portão 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 retorna Result<never, Error> | undefined. undefined significa "esta proposta foi tratada — continue o passo". Um Err significa "pare o passo inteiro fechado". Então uma única proposta sendo rejeitada ou com falha de store não aborta o lote; só um erro de infraestrutura (forma de proposta ruim, falha do gate) faz isso. O teste "records a store-rejected approved write in failed" prova que a primeira escrita sobrevive enquanto o estouro vai para failed.

O portão padrão: aprender só com vitórias validadas

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

// packages/hermes/src/learning/gate.ts:24-36
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)
  };
};
Uma sutileza que vale internalizar — o veredito é dado, não o Result

O gate retorna ok({approved:false, …}) para uma rejeição — não err. O envelope Result sinaliza se o gate funcionou; verdict.approved carrega a decisão. Um gate que deu erro (ex.: o serviço do Validador caiu) retorna err e para o passo. Essa separação é exatamente por que reviewAndLearn consegue distinguir "a política recusou isto" de "a máquina da política quebrou". O teste "rejects 0.69 and approves 0.70 at the default 0.7 floor" fixa a fronteira inclusiva.

Reforçar, não duplicar

O loop não adiciona nenhuma lógica de dedup própria. Escritas aprovadas fluem pelo dedup existente do MemoryStore — re-propor uma entrada que já existe é um no-op de sucesso (então é contado como applied, mas o store permanece em uma entrada). Isso espelha a intenção do ON CONFLICT DO UPDATE do mini-loop: reforçar, não empilhar duplicatas. O schema da proposta (learning/types.ts:62-71) valida a saída do modelo na fronteira, incluindo score limitado a [0,1] — um score: 1.5 de um modelo mal-comportado é rejeitado antes de qualquer escrita.

1. O gate retorna ok({approved:false, reason:'too weak'}) para uma proposta. Onde ela 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 conseguiu escrever. E só um erro do gate (err) abortaria o passo.
2. Uma proposta pontua 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 é rejeitada com a razão "learn only from validated wins". Exatamente 0.70 aprovaria.
3. Por que o loop de aprendizado reusa o dedup do MemoryStore em vez de adicionar o seu próprio?
Correto: d. Roteando escritas aprovadas pelo dedup existente do store mantém uma única política de dedup. Re-propor uma entrada existente tem sucesso (contado applied) mas não cresce o store — provado pelo teste "reinforce, do not duplicate".

Confusões comuns

"Ele auto-aplica o que o modelo propõe." O oposto — é com portão do Validador por design (ADR-0018). O modelo só propõe; um portão decide. O portão padrão é conservador (score ≥ 0.7), e o Validador real do coda pode substituí-lo por injeção sem tocar neste kernel.
"Sem daemon significa um mecanismo diferente." É um ADAPT, não um port literal: o Hermes forka uma thread de background; o Alembic não tem runtime Python AIAgent, então a mesma forma propor→portão→aplicar é remodelada em portas injetadas (ReviewProposer, ReviewGate, MemoryStore). A disciplina é idêntica; o encanamento serve ao motor.