Curso · produto

Alembic — Capabilities atômicas

Capabilities — os tijolos $0 do motor

As capabilities são as funcionalidades atômicas que o Alembic empilha em cima da camada Engine. São tijolos: pequenos, neutros, reaproveitáveis. Esta lição abre os seis tijolos textuais — embed, ocr, triage, notes, context-pack e memory — que aparecem no mapa de pacotes e alimentam tudo que vem depois (do funil ao swarm-harness). Os seis compartilham uma única regra de ouro: offline-determinístico por padrão, --online opta por um backend real.

Todos seguem o mesmo molde: um schema de entrada em @alembic/contracts, uma porta injetada (EmbeddingBackend, OcrBackend, ModelAdapter, FsPort), validação Zod na fronteira, e Result<T, Error> de saída. O CLI é um adaptador fino sobre essas funções puras — você pode invocar o mesmo código de dentro de uma missão (h.agent, h.mission) sem mexer numa linha.

O grupo dos seis tijolos

CLI commands (alembic ...) embed ocr triage notes context-pack memory <substore> <add|list> Source packages @alembic/embeddings @alembic/ocr @alembic/hermes (templates) @alembic/council @alembic/hermes (memory) Injected ports (offline-default · --online gated) EmbeddingBackend OcrBackend ModelAdapter (puro, sem porta) FsPort + Clock + IdFactory

Cada coluna do diagrama é a mesma história contada três vezes: o comando CLI recebe argumentos, o pacote-fonte faz o trabalho útil, a porta injetada é o seam de troca entre o backend offline determinístico e o real (cliproxyapi, SGLang/Unlimited-OCR, MLX-VLM). O context-pack é o único sem porta: é função pura, lê arquivos via node:fs no boundary do CLI e content-hasha tudo com node:crypto.

embed — vetorizar texto sem rede

embed transforma uma ou várias strings em vetores de tamanho fixo. Em modo offline (default), o createOfflineEmbeddingBackend em packages/embeddings/src/offline-backend.ts produz um vetor determinístico de 16 dimensões a partir dos char-codes da entrada — não é embedding semântico, é uma fingerprint reprodutível boa para plumbing, cache e teste de índice. Com --online, o createFetchEmbeddingBackend faz POST /v1/embeddings contra o gateway cliproxyapi (rota OpenAI-compatível) e devolve vetores reais.

$ alembic embed "legal AI factory"
embed: 1 vector(s) of 16 dims (model text-embedding-3-small)
  preview: [-0.1837, 0.4421, -0.7058, 0.2913, -0.0094, 0.5562, …]

A mesma chamada com --json imprime o EmbeddingResult completo (modelo + array de vetores), pronto para alimentar o embed-index que escreve <data-dir>/embeddings-index/<family>.jsonl de forma append-only.

ocr — extrair texto de imagens

ocr recebe uma imagem (path ou base64) e devolve o texto reconhecido. Offline, createOfflineOcrBackend retorna um placeholder estável (OCR(<source>)) derivado do nome do arquivo ou de uma fingerprint hex dos bytes — útil para wire-test sem GPU. Com --online e ALEMBIC_OCR_BASEURL apontando para o servidor SGLang/Unlimited-OCR (ex.: http://127.0.0.1:10000), o createSglangOcrBackend faz POST /v1/chat/completions e devolve OCR real. Sem o env var setado, o comando falha fechado com mensagem acionável e não tenta a rede.

$ alembic ocr ~/screenshots/invoice.png
ocr: /Users/acf/screenshots/invoice.png
OCR(/Users/acf/screenshots/invoice.png)

Note a simetria com embed: mesma estrutura de request/result Zod, mesma porta injetada, mesmo Result. Quem entende um entende o outro.

triage — classificar um issue

triage recebe texto livre (corpo do issue) e devolve um juízo estruturado: severidade, categoria, prioridade 1–10, labels canônicas do Alembic (needs-triage, needs-info, ready-for-agent, ready-for-human, wontfix), e um summary de uma frase. A template triageIssue em packages/hermes/src/templates/issue-triage.ts é uma função pura sobre o ModelAdapter — em modo offline, o adapter determinístico devolve um placeholder rotulado; com --online, o cliproxyapi roteia um modelo real.

$ alembic triage "App crashes on login. iPhone 15, iOS 17.2, Safari"
triage: medium/question (priority 5/10)
  labels: needs-triage
  assignee: any | needs-more-info: true
  summary: Offline deterministic triage placeholder (no model called)

A saída é diretamente aplicável: gh issue edit <n> --add-label $(labels.join(',')). O fato do placeholder devolver needs-triage com needsMoreInfo: true é por construção — o offline-default existe para você poder testar o pipeline sem chamar modelo. Em produção (--online), a label sai como ready-for-agent ou ready-for-human conforme a análise.

notes — transformar transcript em ata estruturada

notes <transcriptPath> lê um arquivo de transcript e devolve um MeetingNotesOutput: título inferido, data ISO, participantes, decisões-chave, action items com owner/due, bloqueadores, próxima reunião. A template extractMeetingNotes em packages/hermes/src/templates/meeting-notes.ts é o irmão estrutural de triageIssue — mesma forma, propósito diferente.

Uma sutileza importante: o template não chama Date.now()/new Date() (a VM do plan proíbe). Em vez disso, o CLI resolve hoje no boundary a partir do clock injetado, formata como YYYY-MM-DD, e passa esse defaultDate para a template — que só o usa quando o transcript em si não menciona uma data. Esse é o padrão recorrente no motor: tempo entra pela borda, nunca de dentro da função pura.

context-pack — pacote de evidências em 8 camadas

context-pack <files...> empacota um conjunto de arquivos em um Layered Context Pack de oito camadas (L0–L7), com brief em uma linha em cima, manifest content-hashed SHA-256 embaixo, e um budget de tokens por (role, model) calculado no meio. É o que um agente long-horizon recebe quando precisa de evidência reprodutível: sob pressão de orçamento, ele pode jogar fora camadas de baixo valor sem comprometer a verificabilidade — o manifest do L7 e o pack id continuam reproduzindo o mesmo bundle.

$ alembic context-pack README.md --brief "Why use Alembic" --role validator --model claude-opus-4-8-max
context-pack: ctxp_… (role validator, model claude-opus-4-8-max)
  manifest: man_…  (1 file(s))
  layers: L0 brief 16 chars | L1 repoMap 0 | L2 files 1 | L3 snippets 0 |
          L4 summaries 0 | L5 debate 0 | L6 artifacts 0/constraints 0 | L7 manifest 1
  budget: 5501 estimated / 100000 max (within budget)

Quem implementa: buildLayeredContextPack + buildContextManifest em packages/council/src/layered-context-pack.ts. Sem rede, sem clock interno, sem RNG — o createdAt é injetado pelo CLI a partir do clock real, o resto é função pura sobre os bytes dos arquivos. O mesmo pack id sai duas vezes para o mesmo input.

memory — cinco substores append-only

memory <substore> <add|list> escreve e lê do conjunto de cinco logs append-only que vivem em packages/hermes/src/memory/multi-store/:

Cada substore é um arquivo JSONL em <dataDir>/memory/<substore>.jsonl. Não há update/delete in-place — corrigir é appender uma nova linha (mesma disciplina do events.jsonl do harness; ADR-0010). Como no notes e context-pack, tempo e ids entram pela borda: o CLI injeta um Clock real e o monotonicIdFactory que mintia mem-1, mem-2, … — nunca Date.now()/randomUUID() dentro do store.

$ alembic memory episodic add --agent demo --record '{"event":"user-asked-for-demo","detail":"first run"}'
memory episodic: appended mem-1 (agent demo)
  /Users/acf/.alembic/memory/episodic.jsonl

$ alembic memory episodic list --agent demo
memory episodic: 1 record(s) under /Users/acf/.alembic/memory/episodic.jsonl
  mem-1 [agent demo] at 1719158400000
Insight central: os seis tijolos têm a mesma silhueta: schema Zod na entrada, função pura no meio, porta injetada na saída, Result<T, Error> em todo lugar — e offline-determinístico por construção. É por isso que eles compõem livremente: o output de embed pode virar input de um índice de busca, o de triage pode virar uma entrada no memory decision, o de notes pode virar L3/snippets dentro de um context-pack que vai para o L0/brief do próximo turno. Cada tijolo é pequeno; o efeito é uma fundação.
Como funciona por dentro — por que offline-default é regra, não exceção

Os seis backends offline (createOfflineEmbeddingBackend, createOfflineOcrBackend, o template adapter determinístico do triage/notes, o builder puro de context-pack, os cinco logs JSONL de memory) compartilham três restrições inegociáveis: (1) sem rede, (2) sem Date.now()/Math.random(), (3) sem dependência nova. Tudo se resolve em-processo, em milissegundos, $0. Isso não é uma comodidade — é o que permite que esses tijolos virem unidade de teste de qualquer outro pacote sem instalar Python, sem subir SGLang, sem ter chave de provider.

O --online é a única opt-in, e mesmo aí cada backend é fail-closed: o ocr --online sem ALEMBIC_OCR_BASEURL não tenta uma rota fake, recusa com mensagem acionável (linha 3461 de apps/cli/src/commands.ts aponta para docs/ocr-setup.md). O embed --online chama preflightLiveProvider antes de tentar o gateway. O triage --online e o notes --online passam pelo resolveTemplateAdapter que valida o registry de modelos. Se algo de rede der errado, vira err; nada vaza como exceção.

O memory é o caso mais radical: ele é sempre offline. Não tem --online porque um log JSONL local não precisa de backend. A única injeção é o FsPort (do @alembic/etl), o Clock e o IdFactory. O CLI usa o relógio real e o id factory monotônico; um teste injeta um relógio fake e um factory que devolve mem-1, mem-2… O resultado: o mesmo input produz o mesmo arquivo, byte-a-byte. Esse é o motivo de cache (chave SHA-256 de (prompt, opts), ADR-0009) e resume funcionarem de verdade no harness.

Auto-verificação

Qual a diferença prática entre o offline e o --online backend de embed?

Os dois implementam o mesmo port EmbeddingBackend e devolvem a mesma EmbeddingResult, mas a natureza dos vetores é diferente. O offline (createOfflineEmbeddingBackend) é uma fingerprint determinística de 16 dimensões dos char-codes — não é semântica, mas é estável (mesma entrada ⇒ mesmo vetor) e $0. O online (createFetchEmbeddingBackend) faz POST /v1/embeddings contra o cliproxyapi em 127.0.0.1:8317 e devolve embeddings reais. Como o pipeline (embed-index, busca por similaridade) só usa o port, você pode trocar um pelo outro sem mudar nada acima.

Por que o context-pack tem oito camadas em vez de uma só com "todo o contexto"?

Porque orçamento de tokens é fato da vida. As oito camadas (L0 brief, L1 repoMap, L2 selectedFiles, L3 snippets, L4 summaries, L5 debateState, L6 artifacts+constraints, L7 manifest content-hashed) são uma escada de prioridade: sob pressão de budget, o caller solta L4/L5/L6 e mantém L0–L3 + L7. O L7 é especial — ele é o recibo verificável (SHA-256 dos arquivos-fonte) que permite reproduzir o mesmo pack id depois, mesmo que as camadas mais altas tenham sido enxugadas. Pack id estável = cache estável = replay estável.

Por que memory é só append-only, sem update/delete?

Para preservar o histórico real. Update in-place esconde aquilo que foi pensado mas depois corrigido — exatamente a informação mais valiosa para replay, auditoria e aprendizado. A disciplina é a mesma do events.jsonl do harness (ADR-0010): cada decisão é um evento imutável; uma correção é um novo evento que referencia o anterior. Sem isso, o motor não conseguiria fazer alembic replay reproduzir uma run antiga byte-a-byte, e o --learn (ADR-0018) não teria sobre o que sedimentar memória durável.

Próximas paradas: funnel (onde embed/ocr viram índices ao longo do corpus T0→T3), swarm-harness (onde triage/notes/context-pack alimentam missões e gates), AI Employee (que costura os seis tijolos em skills + memory persistente), use-cases (três walkthroughs end-to-end que partem desses tijolos).