Lição 02 · Alembic Completo v3 · Adapters e Harness ← índice
Alembic Completo v3 · Módulo 1 · O motor

Adapters e Harness: a cintura estreita e o funil

Toda chamada de modelo do sistema — do CLI ao council — passa por UMA porta: ModelAdapter.run, que nunca lança. Em cima dela, @alembic/harness monta o funil noturno T0→T3 com as DUAS cadeias de valor do ADR-0002, cada uma com seu próprio orçamento fail-closed. Nesta lição: as guardas do adapter, a pista local $0, o accounting de custo e o funil que produziu 2.589 signals e 1.022 learnings.

1

A cintura estreita: run nunca lança

O contrato mora em packages/contracts/src/model.ts: ModelAdapter.run aceita um ModelRunInput validado por Zod (modelRunInputSchema, model.ts:30–40) e resolve para um ModelRunResult — união discriminada em ok (model.ts:109–114). O INVARIANTE, em maiúsculas no próprio doc (model.ts:131–135): run NUNCA lança. Rede caiu, timeout, quota, bug do adapter — tudo vira ModelRunFailure com um ModelRunError tipado que inclui retryable (model.ts:83–89), para o harness decidir fallback sem parsear mensagem de erro.

Pense como… a alfândega de um porto: não importa de qual navio (OpenAI, Anthropic, local) a carga veio, ela só entra pelo MESMO guichê, com o MESMO formulário, e recebe carimbo de aprovada ou recusada — nunca "some" no meio. Onde quebra: alfândega atrasa; a cintura estreita adiciona só validação, não fila.

alembic-completo · 0002-camada-l1-adapters · s0
2

runWithGuards: a espinha de todo adapter

Nenhum adapter reimplementa robustez. Cada um escreve só o attempt (uma tentativa de rede/subprocesso) e o entrega a runWithGuards (packages/adapters/src/adapter-core.ts:118–147), que impõe, nesta ordem (doc em adapter-core.ts:26–31): ① validação de borda do input via Zod; ② o invariante nunca-lança (throw que escapar vira failureFromThrown); ③ o portão do CircuitBreaker com feedback de sucesso/falha; ④ retry/backoff via withRetry, dirigido pelo flag retryable de cada resultado.

ModelRunInput requestId, modelId… ① Zod valida safeParse na borda inválido → CLIENT_ERROR ② breaker canExecute()? aberto → CIRCUIT_OPEN ③ withRetry attempt(input) try/catch interno ④ retryable? backoff e repete ok: true · text ok: false · error sempre um VALOR, nunca throw
adapter-core.ts:118–147 — quatro guardas em série; o attempt fica no centro e qualquer throw vira failureFromThrown (defesa em profundidade nas linhas 95–101 e 142–146).

As pistas: gateway pago e local $0

Duas rotas de adapterId dominam o registry: cliproxyapi — o gateway default em http://127.0.0.1:8317 (cliproxyapi.ts:21), token via ALEMBIC_CLIPROXY_TOKEN com fallback CLIPROXYAPI_API_KEY (apps/cli/src/config.ts:31) — e local, a pista $0 OpenAI-compatível (local-openai.ts:19–20) onde vive o deepseek-v4-flash servido em ALEMBIC_LOCAL_URL (registry.ts:161–174): custo 0, tier T1, residente na máquina. É a pista local que torna o modo --offline e os 1.834 testes herméticos possíveis.

Cost accounting sem mentira: computeCostUsd (cost.ts:26–34) arredonda a 6 casas para o ledger não acumular ruído binário; costForModel devolve undefined para modelo desconhecido — o caller então omite costUsd "rather than reporting a misleading zero" (cost.ts:41–48). Não saber o custo é informação; $0 falso seria mentira.
3

runFunnel: T0→T3 com DUAS cadeias (ADR-0002)

O harness monta o funil noturno em packages/harness/src/funnel.ts. Primeiro, collectResidue (funnel.ts:316–341) reaproveita o L0 da lição 01: caminha o corpus, aplica o prior da família e recolhe só o que roteia acima do T0 — cada item com hash, família, canal e a evidência real (o campo evidence da bridge line, funnel.ts:308–314). Depois os tiers pagos entram: T1 extrai um BusinessSignal por item (runT1Extraction, funnel.ts:435–464), T2 refina a shortlist em batch, T3 leva ao council. Desde o ADR-0002, esse mesmo residue alimenta duas cadeias independentes:

residue collectResidue compartilhado CADEIA SIGNALS (negócio) — sempre roda T1 extrai → T2 refina → T3 council check ANTES de cada chamada paga · budgetBlocked++ 🔒 --budget signalsBudget opportunity- graph.jsonl CADEIA LEARNINGS — opt-in (2º knob) extração T2 · PII redigido PRÉ-chamada só emite learning EXPLÍCITO no texto · senão {} 🔒 --learnings- budget (próprio) learnings .jsonl estourar UM cadeado bloqueia SÓ aquela rota — a outra segue
ADR-0002 em funnel.ts: signalsBudget sempre presente, learningsBudget só quando o 2º knob é passado (funnel.ts:672–675); a rota de learnings extrai em T2 (funnel.ts:756) com PII redigido antes da chamada (funnel.ts:764–777).

Detalhes que definem o caráter do funil: o check de orçamento acontece antes de cada chamada paga com uma estimativa grosseira de ~4 chars/token (estimateUsage, funnel.ts:467–470); item bloqueado vira contador budgetBlocked, nunca chamada silenciosa (funnel.ts:450–457); e o report só ganha a chave routes em runs de dois knobs — "a single-budget run omits routes entirely" (funnel.ts:920–922), então a run de um knob é byte-idêntica à de antes do ADR. Foi essa rota opt-in que, apontada no pai correto (lição 01), produziu os 1.022 learnings de Bookmarks em 06-07 — a primeira produção da segunda cadeia na história do motor.

4

BudgetGuard: o cadeado fail-closed

O medidor vive no ETL (packages/etl/src/budget.ts) e é deliberadamente chato: check(estimate) projeta o custo da PRÓXIMA chamada contra o cap e, se estourar, devolve um BudgetExceeded tipado com spentUsd/projectedUsd/capUsd (budget.ts:43–58) — "it never silently proceeds and never throws" (budget.ts:33–36). record(spend) registra o gasto REAL depois. Exceção de projeto: tier T0 precifica $0 e nunca é bloqueado (budget.ts:96–101) — o substrato gratuito sempre roda, mesmo com cap 0.

fusao · 27-tiers-cost-and-budget · s1
2.589
signals no Business/opportunity-graph.jsonl
1.022
learnings únicos (100% sourceRefs+hash, confiança μ0.94)
$0
custo do caminho --offline (pista local + T0)

Lendo o report: 1 knob vs 2 knobs

O jeito mais rápido de saber qual run você tem na mão é procurar a chave routes no --json:

# run de 1 knob (--budget só): SEM a chave routes — byte-idêntica ao pré-ADR-0002
{ "stats": { … }, "t1BudgetBlocked": 0, … }

# run de 2 knobs (--budget + --learnings-budget): as duas rotas, medidas separadas
{ …, "routes": {
    "signals":   { "budgetUsd": 1, "spentUsd": 0, "budgetBlocked": 0 },
    "learnings": { "budgetUsd": 1, "spentUsd": 0, "budgetBlocked": 0 } } }

routes.signals.budgetBlocked totaliza os bloqueios T1+T2+T3-council da cadeia de negócio (funnel.ts:925–931); routes.learnings.budgetBlocked conta só os da rota de aprendizado. Um número alto num lado com o outro zerado é o sistema dizendo qual cap você subestimou.

5

Cheque seu modelo mental

Quando o BudgetGuard impede uma chamada paga do funil?
Correto: fail-closed é PRÉ-voo. runT1Extraction chama budget.check(estimateUsage(input)) antes de route.adapter.run (funnel.ts:450–458); bloqueio vira budgetBlocked++ e o adapter pago nem é invocado. record só contabiliza o custo real do que passou.
Você rodou distill SÓ com --budget (sem --learnings-budget). O que aparece no report?
Correto: funnel.ts:920–922 — "a single-budget run omits routes entirely so the SERIALIZED report… is byte-identical". A cadeia de learnings é opt-in aditiva: sem o 2º knob ela nem existe no output. Com os dois knobs, routes.signals e routes.learnings aparecem com caps, gasto e bloqueios separados.
Camada técnica — comandos copy-paste

Tudo abaixo é leitura ou caminho hermético $0:

# o invariante nunca-lança, na fonte
grep -n "NEVER throws" packages/contracts/src/model.ts

# as 4 guardas em ordem
sed -n '23,36p' packages/adapters/src/adapter-core.ts

# funil hermético com as DUAS cadeias (caps de exemplo; --offline = $0 real)
alembic distill ~/Documents/Resources --offline --budget 1 --learnings-budget 1 --json

# smoke offline do registry + adapters (sem rede)
alembic doctor --client-stack

# onde o gateway e o token são resolvidos
grep -n "CLIPROXYAPI_DEFAULT_BASE_URL" packages/adapters/src/cliproxyapi.ts
sed -n '24,31p' apps/cli/src/config.ts

Troubleshooting clássico: fetch failed = gateway 127.0.0.1:8317 fora do ar ou token ausente — use --offline enquanto isso.

O que levar desta lição
Pergunta de acompanhamento sugerida: "se a cintura estreita nunca lança, quem decide o que fazer com um retryable: false?" — Na próxima lição subimos um andar: o @alembic/hermes, onde memória multi-store, skills e o AI Employee compõem TUDO isso num turno honesto de agente.