A Lição 3 nomeou o ciclo de aprendizado como pedra angular. Aqui está como ele realmente é entregue: três subsistemas — memory, learning, curator — que juntos permitem que uma run finalizada torne a próxima mais esperta, sem nunca auto-escrever uma lição não validada na memória durável.
Dois stores limitados, em arquivo, persistem entre sessões: MEMORY.md (as próprias notas do agente) e USER.md (o que ele sabe sobre o usuário). Ambos são injetados no system prompt como um snapshot congelado no início da sessão. A disciplina que importa:
memory com ação ∈ {add, replace, remove}; replace/remove localizam o alvo por uma substring curta e única (sem IDs); entradas são delimitadas por § em sua própria linha; limites em caracteres, não tokens (independente do modelo).// packages/hermes/src/memory/memory-store.ts:50-57 export const ENTRY_DELIMITER = '\n§\n'; /** Limite de caracteres padrão para o store MEMORY.md (padrão do Hermes). */ export const DEFAULT_MEMORY_CHAR_LIMIT = 2200; /** Limite de caracteres padrão para o store USER.md (padrão do Hermes). */ export const DEFAULT_USER_CHAR_LIMIT = 1375;
Este subsistema é um CLONE fiel de tools/memory_tool.py (1089 LOC). Os desvios são deliberados: IO é injetado via FsPort, e toda op falível retorna Result<T,Error> em vez de um dict Python.
O Hermes auto-escreve na memória após um turno. O Alembic não. O revisor apenas propõe; o Validador existente do Alembic dispõe. As escritas são com portão do Validador, nunca auto-aplicadas.
Por que a mudança? Duas razões do ADR, ambas fundamentadas:
AIAgent em Python no Alembic para forkar como thread daemon — um passo síncrono pós-unidade sobre portas injetadas é a unidade certa, e compõe com o harness.Então o ciclo são três portas injetadas e um kernel:
| Porta | Papel |
|---|---|
ReviewProposer | Retorna ReviewProposals a partir do resumo do turno — cada um um { target, op, rationale, score }. Em produção encapsula uma chamada de ModelAdapter; em testes, um fake. |
ReviewGate | Dispõe cada proposta (aprova/rejeita). O padrão é scoreThresholdGate(0.7); o Validador real do coda conecta depois fornecendo seu próprio gate — sem mudar o kernel. |
MemoryStore | O store onde escritas aprovadas se aplicam — reusando seu dedup, então rever um fato reforça em vez de duplicar. |
// packages/hermes/src/learning/review.ts:54-69 — o kernel 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 → falha fechada 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); // validar → portão → aplicar if (stepErr) return stepErr; // erro do portão → falha fechada } return ok({ applied: acc.applied, rejected: acc.rejected, failed: acc.failed }); };
Três baldes de resultado — applied / rejected / failed — então nada é descartado em silêncio. A saída do proposer é validada por Zod na fronteira (é saída de modelo não confiável em produção). Um erro de proposer ou portão falha o passo inteiro fechado; uma rejeição do store a uma escrita aprovada é registrada em failed, nunca lançada.
// packages/hermes/src/learning/gate.ts:24-36 — o portão conservador padrão export const scoreThresholdGate = (min = DEFAULT_REVIEW_SCORE_THRESHOLD) => { return async (proposal) => { const approved = proposal.score >= min; // limite inclusivo: 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 limite padrão é 0.7 — a codificação mecânica da regra do hermes-mini-loop "aprender só com vitórias validadas". Note que a decisão vive em verdict.approved, não no Result: uma rejeição é um ok(...) normal, não um erro.
O agente cria skills; telemetria se acumula; o curador é 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:
createdBy === 'agent' são tocadas; o resto é pulado.pinned nunca é transicionada, em nenhum caminho.archived — "ação máxima = arquivar". Não há remoção.O tempo é um Clock injetado — nunca Date.now() (a regra de determinismo do motor, e o que torna os testes de transição reprodutíveis). O curador é o mesmo Clock com que o usage store foi construído, então um evento registrado "agora" e uma transição decidida "agora" concordam.
score: 0.6 e o portão padrão está em uso. O que acontece?scoreThresholdGate(0.7) retorna ok({approved:false, reason}) — uma rejeição é um resultado normal, não um erro. Vai para rejected; só um erro de proposer/portão falha o passo fechado.pinned: true e createdBy: 'user'. O que ele faz?createdBy === 'agent', e skills pinadas nunca são transicionadas. E o estado terminal é archived — não há caminho de delete nenhum.score ≥ 0.7 sem humano e sem I/O. "Com portão" significa um piso de qualidade precisa ser passado; o piso pode depois ser o Validador completo do coda injetando um portão diferente — o kernel nunca muda.