Esta lição abre o Alembic e mostra a sala de máquinas: seis camadas (de L-1 a L4), os fluxos de dados que as atravessam, onde mora o estado, quais APIs existem hoje — e os riscos técnicos que um arquiteto precisa governar. Tudo apontando para o arquivo real onde a propriedade vive.
L-1 ingestion, L0 ETL/contracts, L1 adapters, L2 council, L3 swarm, L4 harness — e o pacote real de cada uma.Ingest → T0 → T1/T2 → Council → Swarm → L4.ModelAdapter) é a dobradiça de toda a arquitetura.Result (ok/err) em vez de exceção — a biblioteca não lança.Pense no Alembic como uma fábrica em andares. No subsolo entram matérias-primas (páginas, transcrições, repositórios) — sem nunca serem alteradas. Cada andar acima refina um pouco: limpa e barateia, protege contra a variação dos modelos, decide com um conselho, executa com uma equipe limitada e, no topo, entrega o resultado por uma porta de cada vez (linha de comando, web, ou um protocolo seguro).
Numeramos as camadas para que a dependência seja sempre para baixo: uma camada superior conhece a inferior, nunca o contrário. A L0 (contracts) é a única que todas importam — ela é a cintura. Cada caixa do mapa abaixo é um pacote pnpm em packages/*, e o app em apps/cli é só um cliente fino de tudo isso.
Quando uma camada falha, você sabe exatamente onde olhar — e a fronteira de cada uma é um tipo Zod, não uma convenção informal. Mudar um schema exportado em @alembic/contracts obriga a atualizar testes e um caminho de compatibilidade. É de propósito: a cintura é o que mantém o resto substituível.
ModelAdapter e os schemas Zod que toda a arquitetura compartilha
Se você só puder ler um arquivo, leia este. Tudo o mais é substituível em volta dele.
A arquitetura por camadas, lida de baixo para cima: cada pacote tem uma função no pipeline que vai de L-1 a L4.
As seis camadas empilhadas — a ingestão entra embaixo, a superfície utilizável sai em cima. Cada faixa nomeia o pacote que a implementa.
contracts depende dos tipos dele. Ninguém aponta de volta — a cintura não conhece seus clientes.Clique numa camada para acendê-la na pilha e ver o que ela faz, onde mora e por que existe.
agent-browser aceita apenas verbos de leitura e o collector materializa pacotes wiki com um cursor monótono. Nada é alterado na fonte: a captura é read-only por desenho.
orchestrator → lead → worker, com profundidade cortada por canSpawn.| Camada | O que faz | Invariante |
|---|---|---|
| L-1 ingestion | Captura páginas/repos/transcrições e materializa pacotes wiki. | Read-only — só verbos de leitura. |
| L0 ETL/contracts | Valida o contrato, scoreia, deduplica por SHA-256 e manda só o residue para os tiers caros. | Barato primeiro; o que não resolve sobe. |
| L1 adapters | Fala com cada modelo (cliproxy, local, CLI, offline) por trás de uma interface única. | Devolve Result; nunca lança exceção. |
| L2 council | Fases serializadas acumulam contribuições; membros rodam em paralelo; o verifier vira gate. | Decisão só sai pelo verifier. |
| L3 swarm | orchestrator → lead → worker, com profundidade máxima e gates de dependência. | Limitado; T4 vai para park. |
| L4 harness | Expõe o núcleo por CLI, HTTP/SSE e MCP — sem misturar transporte com lógica. | MCP é read-only por desenho. |
Uma página recém-capturada chega na L0 mas não atinge a pontuação mínima de sinal. Para onde ela vai?
residue append-only. A L0 não joga nada fora: o que não "resolve" barato é registrado e fica disponível para os tiers T1/T2/T3 mais caros decidirem depois. Descartar seria perder auditabilidade.Fluxo detalhado: ingestion read-only, ETL determinístico (T0), modelos guardados, council, swarm e as superfícies. Tudo é JSONL append-only — auditável e replayável.
A esteira do funil — o que não resolve barato vira residue e sobe; o estado vive todo em JSONL append-only.
Avance um passo de cada vez pela jornada de uma unidade de trabalho.
Arquivos .jsonl com pacotes wiki entram por um corpus local. O T0 ignora famílias como Models/Prompts.
O que não resolve no T0 vira residue append-only para os tiers T1/T2/T3.
O T1 extrai um BusinessSignal com PII redaction e um budget guard ao redor dos tiers pagos.
O council pontua, agrega consenso, verifica claims e só então emite oportunidades/learnings.
Toda conversa com um modelo passa por um único ponto fino: o ModelAdapter. É a "cintura" da arquitetura — larga em cima (muitos modelos diferentes), larga embaixo (muitas camadas que precisam de um modelo), estreita no meio.
ModelAdapter é o formato do plugue. Trocar o "aparelho" (o modelo) não exige reformar a casa.O adapter recebe um input validado e devolve uma união ok / err — ele não lança. Isso é o que impede a variação dos modelos (timeout, erro de gateway, resposta malformada) de vazar exceção para as camadas de cima. O erro vira dado, e o chamador decide o que fazer.
type ModelRunResult = | { ok: true; value: { text: string; usage: TokenUsage } } | { ok: false; error: ModelError } // never throws
switch em ok.Antes do adapter, o roteador escolhe o modelo: o tier define a faixa de risco, pickCheapestForTier(tier) pega o mais barato da faixa quando nenhum modelId está fixado, e o roteador se recusa a trocar de modelo/adaptador em silêncio — se o alvo não existe, devolve err. Um fallback invisível é um bug de governança, não uma conveniência.
# prova: pickAdapter falha fechado quando o alvo não existe rg -n "pickCheapestForTier|no_model_for_tier|adapter_not_registered" packages/adapters/src
err explícito, não um modelo trocado por baixo dos panos.T2 chega sem modelId fixado.pickCheapestForTier('T2') devolve o modelo T2 de menor custo no registry.adapterId (em geral cliproxyapi). Se não houver, err — nunca um substituto silencioso.Result. O adapter roda e devolve ok/err. Exceção nenhuma sobe.packages/contracts/src/registry.ts e ache o modelo T2 mais barato. Depois confirme em packages/adapters/src que o roteador falha quando o adapterId não existe.Onde o Alembic guarda a verdade — e por quais portas ela entra e sai.
O Alembic não exige banco externo. A fonte de verdade são arquivos .jsonl em que só se acrescenta — nunca se sobrescreve. Stores de oportunidades e de learnings têm caminhos fixos relativos ao dataDir (ex.: Business/opportunity-graph.jsonl, Skills/learning/learnings.jsonl); o run grava events.jsonl e um cache; manifests de marketing são versionados; o swarm guarda seu próprio estado.
seq 0 reconstrói exatamente o mesmo estado — isso é o replay.fs direto: usam um FsPort injetável. Isso torna o IO testável e mantém a determinação onde ela é exigida.No topo (L4), CLI, HTTP/SSE e MCP leem ou acionam o mesmo HarnessCore. O transporte nunca se mistura com a lógica — cada superfície é um adaptador fino.
start, sem fanout.| Superfície | Para quê | Propriedade |
|---|---|---|
| CLI | Operador local: plan, run, serve… | A porta de entrada do dia a dia. |
| HTTP/SSE | Servidor stateful; POST /runs e replay de eventos via SSE. | Eventos viajam em swimlanes; dá pra reconectar. |
| MCP | Expor o harness a outros agentes. | Read-only por desenho — sem start/fanout. |
| Elemento | Onde olhar | Por que importa |
|---|---|---|
| Model waist | packages/contracts/src/ | Input/Result/Adapter, união ok, never throws. |
| Routing | packages/adapters/src/ | tier → modelo → adapter sem fallback silencioso. |
| T0 pipeline | packages/etl/src/ | walk, validate, score, dedupe, residue. |
| Stores | packages/etl/src/ | Stores append-only de oportunidade e learning. |
| Council | packages/council/src/ | Fases seriais e membros em paralelo + verifier. |
| Swarm | packages/swarm/src/ | Orquestração com profundidade limitada, resume e T4 park. |
| Harness server | packages/harness/src/ | HTTP do Node, replay SSE e o bind MCP. |
Aqui o JSONL é escolha de auditabilidade e replay. Um banco externo não é requisito do core — é uma opção que se adiciona quando o caso de uso pedir.
Não procure start/fanout no MCP: a ausência é uma propriedade de segurança, não um recurso faltando. Outros agentes podem ler, não disparar.
Empurrar a geração de tokens para o gateway/MLX é delegação consciente, não buraco. O TS não reimplementa cache de atenção de propósito.
O wrapper sobre o CLI externo (ex.: marketing/higgsfield.ts) é uma camada anticorrupção tipada, com comentários [uncertain] marcando quais flags/envelopes ainda não foram confirmados num run autenticado real. O seam existe; o acabamento é gated.
Result em vez de lançar?T0 não resolve?T1/T2/T3. Nada é descartado — auditabilidade acima de tudo.start?pickCheapestForTier faz?modelId foi fixado. Custo é decisão de arquitetura, não acaso.Não acredite no diagrama: prove cada afirmação no checkout. Aqui estão os atalhos.
packages/adapters/src e confirme que ele falha quando o modelo/adaptador não existe.rg -n "pickCheapestForTier|no adapter" packages/adapters/src
rg -n "redact|assertRedacted|PRIVATE" packages
packages/harness/src para entender replay e frames ao vivo.rg -n "sse|stream|replay" packages/harness/src
rg -n "OPPORTUNITY_GRAPH_PATH|appendStoreRecord" packages/etl/src
packages/etl/src/pii.ts).Um arquiteto governa riscos, não só componentes. Estes são os que estão ativos — cada um com a pergunta que o prova.
| Risco | O que olhar | Por que importa |
|---|---|---|
| Seam de fornecedor inacabado | packages/marketing-factory/src/higgsfield.ts e seus [uncertain]. | O envelope/flags do CLI real pode divergir do typed wrapper. |
| Sinal do usuário em aberto | approvals / rejections gravados, mas não realimentados. | O veredito humano não volta para o sistema — laço aberto. |
| Caminho de query vetorial ausente | Índices de embeddings são construídos, não consultados. | Há build de RAG, falta o read path (cosine/topK/rerank). |
| Determinação frágil em plans | Date.now() / Math.random() num alembic.plan.ts. | A VM rejeita; quebra replay se escapar. |
Seis cartões, a lição inteira. Use as setas ← → ou os botões.
contracts é o único pacote que todos importam.Result — never throws.err.T0 filtra barato; o que sobra vira residue.Três perguntas. As explicações aparecem ao escolher.
ModelAdapter devolve Result em vez de lançar exceção?ok/err transforma timeout, erro de gateway ou resposta malformada em dado que o chamador trata. A biblioteca nunca lança — é o que impede a não-determinação de contaminar as camadas superiores.T0 não resolve barato?T0 não descarta nada: o que não "resolve" barato é registrado como residue e fica disponível para os tiers caros decidirem. Descartar quebraria a auditabilidade que justifica o JSONL.