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, quatro invariantes e o funil que transforma fontes brutas em Learnings.
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.
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).
Eis a ideia mais importante do código. Toda invocação de modelo no sistema inteiro flui por uma forma de função: uma chamada async que nunca lança e retorna uma união discriminada chaveada em ok. Sucesso e falha são ambos valores de retorno comuns — não há um segundo caminho excepcional para raciocinar.
// 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").
"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: (1) validação Zod da entrada na fronteira, (2) um try/catch que converte qualquer throw escapado em um ModelRunFailure tipado, (3) um portão opcional de circuit-breaker, e (4) backoff de retry guiado pelo flag retryable de cada resultado — com um try/catch externo final "como última rede de segurança para preservar a invariante".
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.
Uma segunda união Result<T, E> mais leve (também chaveada em ok, com braços value/error) existe para trabalho falível não-modelo — IO de arquivo, parsing, encapsular subprocessos. Ela espelha deliberadamente a cintura de modelo para que ambas leiam idêntico nos call sites. Essa disciplina é a Lição 5.
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.
| # | 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. |
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.
| 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 |
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).
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.run() nunca lança; um 429 vem como um ModelRunFailure com error.retryable setado. runWithGuards converte qualquer throw escapado nesta forma e até dirige o retry pelo flag. E o roteamento não tem fallback silencioso — um modelo ausente retorna um erro tipado, nunca um substituto.GO quanto aprovação do painel.