O motor
Antes de qualquer fusão, você precisa do hospedeiro. O Alembic é um motor de execução de planos para enxames de agentes: ele recebe um objetivo, um plano executável e um contrato de validação, então roda unidades de trabalho em vários modelos — roteando por custo, provando cada passo, com portão antes de entregar. Esta lição é a forma dele: seis camadas, um contrato que carrega o peso e nunca lança, quatro invariantes e o funil que transforma fontes brutas em Learnings.
Esta lição destila o mapa verificado na fonte (§1–§5) mais os arquivos que ele cita: packages/contracts/src/model.ts, packages/adapters/src/adapter-core.ts e packages/harness/src/funnel.ts. Por que importa pra missão: é o hospedeiro onde toda capacidade do Hermes vai ser clonada — você precisa da forma dele antes de fundir qualquer coisa.
- Recitar as seis camadas (L-1 → L4) e por que o grafo de imports é acíclico e só aponta para baixo.
- Explicar a cintura estreita: por que
run()nunca lança e comorunWithGuardsimpõe isso estruturalmente. - Enumerar as quatro invariantes (exatamente quatro) e o que sustenta cada uma na fonte.
- Descrever o funil de 4 níveis (T0→T3) e por que uma emissão exige
GO+ aprovação do painel.
As contagens são verificadas na fonte. O mapa registra 19 pacotes de workspace + 1 app; a suíte estava em 415 quando o mapa foi escrito e cresceu para ~565 depois que @alembic/hermes chegou — os mesmos 565 que o estudo de caso da Lição 6 roda.
01 · O que é, de fato, o Alembic
Pense no Alembic como uma fábrica com esteira. Você entrega três coisas na entrada — o que fazer (o objetivo), o passo a passo (o plano executável) e como saber se ficou certo (o contrato de validação). A esteira então faz o trabalho em estações, sempre conferindo a qualidade antes de empacotar. As "máquinas" da esteira são modelos de IA; a graça é que você troca uma máquina cara por uma barata sem reescrever a esteira.
GOAL.md + alembic.plan.ts + um contrato de validação. O @alembic/vm executa o plano injetando os hooks h.*; o roteamento escolhe o modelo mais barato do tier pedido (pickCheapestForTier); cada unit.proof[] vira um comando que falha fechado; e os quatro gates de fechamento vivem em @alembic/coda.02 · As seis camadas
O Alembic é estratificado de cima a baixo, e a estratificação é real: o grafo de imports não tem arestas para cima nem ciclos. Cada camada só pode depender das que estão abaixo. Leia de baixo — a fundação é o vocabulário; o topo é onde humanos e ferramentas se conectam.
| Camada | O que possui | Pacotes |
|---|---|---|
| L4 · CLIENTS | As superfícies que humanos e ferramentas usam: CLI, o servidor harness HTTP+SSE, um servidor MCP somente-leitura, o web cockpit, a TUI. | harness, web, tui, apps/cli |
| L3 · SWARM | Orquestração multi-nível: um orchestrator gera um lead que gera workers, sobre uma fila com dependências, com profundidade limitada, isolamento git-worktree e resume à prova de crash. | swarm |
| L2 · ENGINE | O kernel de decisão: um DebateEngine qualitativo, score quantitativo 0–10, um Verifier maker-checker independente, e um painel de N-lentes que é o portão de emissão T3+. | council |
| L1 · ADAPTER | A cintura estreita — toda chamada de modelo é uma forma de função que nunca lança. Seis adapters + offline + um roteador sem fallback silencioso, retry, circuit-breaker, contabilidade de custo. | adapters |
| L0 · SUBSTRATE | O piso determinístico de $0: o vocabulário Zod (todo tipo é um z.infer), o leitor de corpus em streaming, dedupe SHA-256, stores JSONL append-only endereçados por conteúdo, redação de PII, o guarda de budget, run directories, o registro de modelos. | contracts (vocabulário), etl (a camada determinística) |
| L-1 · SOURCE | A camada de ingestão que alimenta a wiki que o ETL depois destila — um Collector somente-leitura + um wrapper agent-browser que só pode navegar, nunca mutar. | ingestion |
Cola que atravessa as camadas. Um punhado de pacotes orquestra através de L2–L4 e é melhor lido como seu próprio nível: @alembic/mission (compila missões → run specs), @alembic/vm (executa alembic.plan.ts injetando os hooks h.*), @alembic/coda (os quatro portões de fechamento de run), @alembic/forge (o front-end Forge + Scope Gate) e @alembic/planf3 (HTML de plano).
03 · Por que o grafo é acíclico (e por que isso importa)
"Sem aresta para cima, sem ciclo" soa abstrato — mas é o que deixa cada camada testável e substituível em isolamento. Como L0 não conhece L1, e L1 não conhece L2, você pode trocar um adapter inteiro sem tocar no engine, e testar o engine sem nenhuma rede. Compare as duas formas:
Por isso a invariante 2 (engines agnósticos de adapter e de store) é estrutural, não um conselho: o grafo acíclico é o que torna a injeção possível. Guarde isto para a seção 06.
04 · A cintura estreita — um contrato que nunca lança
Eis a ideia mais importante do código. Toda invocação de modelo no sistema inteiro flui por uma forma de função. A analogia: é a tomada padrão da casa. Não importa se o aparelho é uma frontier cara ou um modelo local grátis — todos têm o mesmo plugue, então toda a fiação acima é igual. Muitos chamadores em cima, muitos provedores embaixo, e um encaixe único no meio:
Sucesso e falha são ambos valores de retorno comuns — não há um segundo caminho excepcional para raciocinar. O resultado é uma união discriminada chaveada em ok: se ok:true, você lê o texto; se ok:false, você lê um erro tipado. Mesma forma, dois ramos:
ok — o consumidor só ramifica num if// packages/contracts/src/model.ts — a cintura (forma) interface ModelAdapter { run(input: ModelRunInput): Promise<ModelRunResult>; // NUNCA lança (a invariante) } // ModelRunResult é uma união discriminada em `ok`: ModelRunSuccess = { ok: true; text; usage?; costUsd?; durationMs; modelId; ... } ModelRunFailure = { ok: false; error: { code; message; retryable }; durationMs; ... } ModelRunResult = z.discriminatedUnion('ok', [ Success, Failure ])
Fonte: packages/contracts/src/model.ts:30-151. Governado pela ADR-0009 ("cintura estreita — run nunca lança").
ModelRunResult com ok:false — a mesma forma de sempre. O throw nunca sobe: ele é capturado lá embaixo e convertido em um ModelRunFailure tipado, com error.retryable setado. Se você chutou "uma exceção que o engine precisa capturar", caiu na armadilha clássica — o ponto inteiro da cintura é que não existe caminho de exceção acima de L1. O engine só faz if (!res.ok).Porque a forma é uniforme, toda camada acima de L1 pode ser escrita como um kernel puro que só ramifica em ok. Um 429, um timeout, uma resposta malformada, uma queda de provedor — todos chegam como a mesma falha tipada. Não há try/catch espalhado pelo engine, pelo swarm ou pelo funil. O núcleo de orquestração até re-estabelece a fronteira para sub-runs inteiras com runDebateSafe / runSwarmSafe. Um contrato, imposto uma vez, comprado em todo lugar.
Duas uniões irmãs: a de modelo e a de IO
Existe uma segunda união, mais leve, para trabalho falível que não é chamada de modelo — IO de arquivo, parsing, encapsular subprocessos. Ela é o Result<T, E> e espelha de propósito a cintura de modelo, para que ambas leiam idêntico nos call sites. Veja-as lado a lado:
ok)05 · runWithGuards — a espinha que impõe "nunca lança"
"Nunca lança" não é um comentário que você torce para valer — é estruturalmente imposto por uma única espinha reusável, runWithGuards. Cada adapter implementa só um attempt() interno; a espinha o encapsula, em ordem, com quatro etapas. Pense numa linha de montagem com guarda-corpos: a entrada é validada, o miolo roda dentro de uma rede, e a saída sempre tem a forma certa.
Fonte: packages/adapters/src/adapter-core.ts — runWithGuards (linhas 118–147), a espinha estrutural nunca-lança. adapter-core.ts:118.
A diferença que isso faz: erro espalhado vs erro num lugar só
Compare as duas arquiteturas de erro. Sem a cintura, cada consumidor precisa do seu próprio try/catch (e um esquecido vira um crash). Com a cintura, o try/catch vive uma vez, e todo o resto só lê um valor:
| Aspecto | Sem cintura (throws espalhados) | Com cintura (runWithGuards) |
|---|---|---|
| Onde mora o try/catch | Em todo call site (N lugares) | Num lugar só (a espinha) |
| O que o consumidor faz | try { run() } catch(e) { … } | if (!res.ok) { … } |
| Um handler esquecido | Vira crash não tratado | Impossível — a falha é só um valor |
| 429 / timeout | Cada um lida do seu jeito | Forma única, retryable padronizado |
| Modelo ausente | Risco de fallback silencioso | Erro tipado, sem substituto |
06 · As quatro invariantes — exatamente quatro
A arquitetura se apoia em quatro propriedades (não mais — o mapa enumera exatamente estas). Cada uma é afirmada na fonte, e a maioria é governada por uma ADR. Visualize-as como quatro pilares:
| # | Invariante | Como é mantida |
|---|---|---|
| 1 | run() nunca lança; o resultado é uma união discriminada uniforme. | Estrutural, via runWithGuards (adapters/src/adapter-core.ts:118). ADR-0009. |
| 2 | Engines são agnósticos de adapter E de store — kernels puros com side-effects injetados. | O DebateEngine recebe views readonly + um AdapterRegistry injetado; o ETL roteia todo IO por um FsPort injetável; o funil recebe um registry injetado (então um offline torna a run $0). |
| 3 | IDs endereçados por conteúdo + layout determinístico de run-dir (para runs replicarem). | O id de uma run é o hash de conteúdo SHA-256 do spec; stores são endereçados por conteúdo sobre JSON canônico, então re-anexar conteúdo idêntico é no-op. Módulos de plano não podem usar Date.now()/Math.random(). |
| 4 | Dissidência é preservada/forçada pelo Verifier, não apenas por um prompt. | O Verifier maker-checker é somente-leitura por arquitetura e prova claims com oráculos determinísticos sobre evidência estruturada, nunca a prosa do maker. "Contrarian-last" é um erro rígido de carga do board. ADR-0003. |
Invariante 3 de perto: por que o mesmo corpus replica
O endereçamento por conteúdo é o que torna uma run replicável. O id é o SHA-256 do spec; os stores guardam por hash do conteúdo canônico. Então rodar de novo um corpus inalterado não cria nada novo — só bate nos mesmos hashes e não toca delta nenhum:
07 · O funil — uma destilação de 4 níveis
A razão de existir do Alembic é o funil: ele transforma um corpus bruto em duas cadeias de valor — um grafo de oportunidades de negócio e um store de Learnings. Faz isso em quatro níveis de custo, barato-primeiro, para que a maior parte do trabalho custe nada e só os sinais mais fortes cheguem a um modelo pago. A imagem mental: uma peneira de quatro malhas — a primeira deixa passar tudo de graça; cada malha seguinte é mais fina, mais cara, e segura menos.
| Nível | O que faz | Custo |
|---|---|---|
| T0 | runT0Pipeline determinístico: percorre o corpus → dedupe SHA-256 → valida contrato → score 6-dim → emite resíduo. Roda sobre 100% do corpus. | $0 |
| T1 | runT1Extraction: um BusinessSignal por item de resíduo via o adapter LOCAL injetado (free-tier, então na prática nunca bloqueado por budget). | ~$0 |
| T2 | runT2Shortlist: um shortlist FRONTIER com portão de budget refina os sinais T1 mais fortes em lotes; toda chamada paga é medida. | medido |
| T3 | runT3Council: um council sintético de 3 membros (otimista / analista / pessimista) mais o painel verifier de N-lentes. | medido |
Fonte: packages/harness/src/funnel.ts — os quatro níveis (linhas 55–81) + verified-GO (linhas 496–514).
Por que barato-primeiro: o custo desaba a cada malha
A peneira só faz sentido porque poucos itens chegam às malhas caras. T0 vê 100% do corpus, mas a $0. Quando você finalmente gasta com uma frontier (T2/T3), está pagando por um punhado de sobreviventes. Veja a queda do volume e a entrada do custo:
Faça a conta na mão (passo a passo → agora você)
Antes de mexer no slider, sinta a queda na mão, devagar. Suponha um corpus de 10.000 itens e uma sobrevivência didática de 20% a cada malha. Recuperar o procedimento (não só ver o número pronto) é o que fixa de verdade. [uncertain] os 20% são um modelo de ensino — o funil real prioriza por score 6-dim, não por taxa fixa.
runT0Pipeline determinístico roda sobre 100% do corpus: 10.000 itens, custo $0. Nada de modelo aqui — só walk + dedupe SHA-256 + contrato + score.10.000 × 0,20 = 2.000 itens viram um BusinessSignal cada, via o adapter LOCAL. Ainda praticamente de graça.2.000 × 0,20 = 400 itens vão para o shortlist FRONTIER — aqui começa o dinheiro (medido).400 × 0,20 = 80 itens passam por council + painel de N-lentes.400 / 10.000 = 4% do corpus. Os outros 96% foram filtrados de graça. É essa a economia do funil.40.000 × 0,20 × 0,20 × 0,20 = 320 itens chegam a T3 (três malhas de 20%). Dica: cada malha multiplica por 0,20, então T3 ≈ corpus × 0,008 — menos de 1% do corpus enfrenta o nível mais caro. O slider abaixo faz exatamente essa conta em tempo real.Calcule você: a peneira em ação
Arraste o tamanho do corpus e a "taxa de sobrevivência" entre malhas. Repare: T0 é sempre $0 e roda em tudo; só os poucos que chegam a T2/T3 custam dinheiro. [uncertain] os percentuais de sobrevivência são um modelo didático ajustável — o funil real prioriza por score, não por uma taxa fixa.
Um resultado T3 só emite quando ambos a decisão de consenso é GO e isPanelEmissionApproved(report) vale — o painel de N-lentes verificou, não estacionou. Maioria simples não basta; o painel pode vetar. É isso que mantém o grafo de oportunidades honesto: nada sedimenta sem passar o portão de emissão.
Três invariantes de segurança que o funil nunca deve regredir: PII antes da saída (um sinal de canal privado é redatado antes da chamada de modelo e re-checado antes de qualquer escrita), budget fail-closed (toda chamada paga é encapsulada num BudgetGuard.check fail-closed — uma quebra projetada bloqueia a chamada e o nível degrada em vez de gastar demais), e append-only (resultados fluem para stores endereçados por conteúdo, validados por schema, append-only; leituras de fonte permanecem somente-leitura).
08 · Decisão: "o que esse sinal vira?" (fluxograma)
Junte o funil e o portão de emissão numa única decisão. Diante de um item do corpus, siga as setas — cada losango é uma pergunta que escolhe o caminho, de "passou no contrato?" até o veredito final "emite" ou "estaciona":
BudgetGuard.check fail-closed impede.GO; o painel de N-lentes precisa aprovar. Esse "E" é o que separa uma opinião de uma emissão — e é por isso que nada entra no grafo sem ser verificado.09 · Confusões comuns
runWithGuards), então a falha é convertida em valor tipado uma vez e todo consumidor a lê uniformemente. A vitória é a ausência de tratamento de erro em todo o resto, não a presença dele no adapter.Date.now() / Math.random(); a VM os rejeita.Como isso se encaixa
Esta lição é o panorama da máquina inteira. As outras lições do módulo "Motor & método" abrem, uma a uma, as peças que você acabou de ver de cima: a cintura, o funil, as invariantes, os gates, o swarm. Aqui está o lugar de cada uma no fluxo de controle real — leia da esquerda (o que entra) para a direita (o que sai), com o motor inteiro destacado no meio:
Cada caixa acesa acima é uma lição inteira. Estes são os encaixes diretos — siga o link e veja a peça por dentro:
run() que a seção 04 só apresentou, e que toda camada acima (engine, swarm, funil) compra de graça.Onde você está na metodologia: esta é a visão de cima. As Lições 14–19 descem a um andar de cada vez nas peças desta esteira; as Lições 2–3 contam o que foi fundido aqui; o Módulo 4 (22–30) põe a mão na massa. Volte ao hub do curso para o mapa completo dos 30 passos, ou abra a galeria de blocos e demos. [uncertain] não existe um metodologia.html dedicado no repositório hoje — o index.html é o mapa-índice canônico da metodologia; se um mapa interativo separado for criado, aponte para ele aqui.
Na prática
Chega de diagrama — rode o motor. O caminho canônico tem dois comandos: você gera um escopo executável a partir de um prompt, depois roda esse escopo com os gates ligados. Primeiro, transforme uma ideia em GOAL.md + alembic.plan.ts + contrato:
# 1 · gera o escopo: plano HTML + GOAL.md + contrato + alembic.plan.ts alembic plan "adiciona um comando `alembic hello` que imprime a versão" # saída esperada (resumida): # ✓ GOAL.md (objetivo + done-when mensurável) # ✓ alembic.plan.ts (o plano executável — sem Date.now()/Math.random()) # ✓ validation-contract.md # ✓ plan.html (o plano legível)
Agora execute esse escopo. O run injeta os hooks h.* na VM, roteia cada unidade para o modelo mais barato do tier, roda os unit.proof[] e só fecha depois dos gates. O --yes dispensa a confirmação interativa:
# 2 · roda o escopo com os gates de fechamento alembic run --goal GOAL.md --plan alembic.plan.ts --yes # saída esperada (resumida): # run-id: 3f9a… (SHA-256 do spec — endereçado por conteúdo) # ▸ Scope Gate ✓ copia GOAL/plan/contrato para o run-dir # ▸ unit u1 ✓ roteada p/ o modelo mais barato do tier # ▸ Proof Gate ✓ pnpm -r typecheck && pnpm -w test (exit 0) # ▸ Validator Gate ✓ scrutiny independente por milestone # resultado em ~/.alembic/runs/3f9a…/ (events.jsonl · units/ · proof-results.jsonl)
Comandos canônicos do CLAUDE.md ("Forge scope execution" / "Run orchestration"). Um fetch failed aqui significa que o gateway cliproxyapi está fora do ar — rode tudo hermético com o sufixo --offline onde o comando o aceita (ex.: alembic distill <corpus> --offline, da Lição 15).
pnpm -r typecheck && pnpm -r build && pnpm -w test a partir da raiz — é o mesmo Proof que o motor roda. Tem que ficar verde antes de qualquer coisa.alembic doctor --client-stack — valida a forma do MODEL_REGISTRY e a coerência dos adapters, offline e a custo $0 (sem rede). É a checagem da cintura estreita (seção 04) antes de gastar qualquer token.alembic plan "<seu prompt>" e abra o GOAL.md gerado. Procure pelo done-when mensurável — sem ele, o Forge se recusa a compilar.alembic run --goal GOAL.md --plan alembic.plan.ts --yes e anote o run-id impresso — ele é o SHA-256 do spec (a invariante 3 da seção 06, ao vivo).alembic runs list e abra a pasta do run-id: procure events.jsonl (a trilha), units/<id>/proof-results.jsonl (cada proof que passou) e o registro dos gates. Re-rode o mesmo escopo e veja o id repetir — replay é no-op (seção 06).Por que dois comandos e não um. plan só produz texto (um escopo reversível — nada foi feito ainda); run é o que executa atrás dos gates. Essa separação é o ADR-0005 da Lição 17 em forma de CLI: tudo que é reversível roda sozinho, e o portão humano fica no fim. [uncertain] o caminho exato do run-dir (~/.alembic/runs/<id>/) depende do --data-dir configurado; use alembic runs list para o caminho real da sua máquina.
Fixe os conceitos (flashcards)
Clique pra virar. Tente lembrar a resposta antes de virar — recuperação ativa fixa mais que reler.
run() retorna num 429?ModelRunResult com ok:false e error.retryable — nunca lança. runWithGuards converte qualquer throw nessa forma.AdapterRegistry + FsPort por injeção — um registry offline roda o funil a $0.GO E isPanelEmissionApproved(report). Maioria simples não basta; o painel pode vetar.Revisão cumulativa — recupere de memória
Antes de clicar: responda de cabeça. As quatro opções têm o mesmo tamanho de propósito — sem pista pela forma.
run() nunca lança; um 429 vem como um ModelRunFailure com error.retryable setado, e runWithGuards até dirige o retry pelo flag. a inverte a invariante — não existe caminho de exceção acima de L1; o engine só faz if (!res.ok). b confunde uma falha de modelo (um valor) com um crash de processo: a run não aborta por um 429, ela trata. d descreve um fallback silencioso que o roteamento proíbe — um modelo ausente retorna erro tipado, nunca um substituto.GO + aprovação do painel. a é falso: a razão não é capacidade de leitura, é economia deliberada. b inverte a qualidade — T3 (council + verifier) é mais rigoroso que T0/T1, que só filtram barato. c confunde com velocidade: os níveis são sobre custo e confiança, não paralelismo.