ALEMBIC • LEARN COMPLETE • FASE 3
Layer L1: Adapters — The Narrow Waist (Deep Dive)
PATTERN: 04-CODE-UNDERSTANDING + 13-FLOWCHART (visual-teach-studio)

Layer L1: @alembic/adapters — The Narrow Waist

Esta é a camada mais importante do ponto de vista de contratos e confiabilidade. Todo modelo chamado no sistema (T0 até T3, Factory, Council, Swarm) passa por aqui. O contrato é rígido: run(input: ModelRunInput): Promise<ModelRunResult> onde o Result é discriminado em ok e nunca lança.

Arquitetura da Camada (25 arquivos .ts)

Estrutura completa:
adapter-core.ts:23-100 (spine com validação Zod, circuit-breaker, withRetry, never-throws — o contrato vivo)
registry.ts:89-110 + router.ts:53-70 (montagem + pickCheapestForTier + pickAdapter sem fallback silencioso)
• Cross-cutting (obrigatórios em todo provider): retry.ts, circuit-breaker.ts, cost.ts, http.ts, errors.ts, logger.ts, clock.ts, token-store.ts
• Providers concretos (8): cliproxyapi.ts, local-openai.ts, offline.ts (o $0 hermético), anthropic.ts, openai-compatible.ts, pi-dev.ts, codex-oauth.ts, local-cli.ts
• 5 arquivos de teste unitários (adapter-core, retry, circuit-breaker, cost, offline)

Total: 25 arquivos .ts em src/ (incluindo testes). Depende quase exclusivamente de @alembic/contracts. Esta é a única camada que todo run de modelo no sistema (CLI, Factory, Council, Swarm) toca.

O Spine: adapter-core.ts (o coração da camada)

23|/** The reusable spine that turns a "single attempt" into a compliant ModelAdapter `run`. */
24|export interface AdapterRuntime { clock?, logger?, retryPolicy?, breaker? }
48|export type Attempt = (input) => Promise<ModelRunResult>;
50|const validate = (adapterId, input) => { ... Zod + makeFailure ... }
82|const guardedAttempt = async (...) => {
89|  if (breaker && !breaker.canExecute()) return breakerRejection...
95|  try { result = await attempt(input) } catch (cause) { result = failureFromThrown... }
...
// depois withRetry + breaker feedback
}
Invariantes garantidos aqui (linhas 27-35):
1. Validação de entrada no boundary
2. Never-throws (qualquer throw vira failureFromThrown)
3. Circuit breaker com feedback correto
4. Retry backoff baseado em retryable flag do Result

Registry & Router (sem mágica)

createAdapterRegistryregistry.ts:89 pickAdapter (router)router.ts:53 adapter-core.runguarded + retry + breaker Concrete Attemptanthropic / cliproxy / offline Result Fluxo garantido: validação → breaker → attempt → retry → breaker feedback → Result (nunca throw)
// registry.ts:89
export const createAdapterRegistry = (options) => {
  const registry = new Map();
  // Sempre registra cliproxyapi + local
  // Opcionais só quando passados explicitamente
  // offline.asDefault liga o offline adapter em TODOS os tiers
}

// router.ts:53
export const pickAdapter = (tier, adapters) => {
  const entry = pickCheapestForTier(tier, MODEL_REGISTRY);
  if (!entry) return err({ kind: 'no_model_for_tier', tier });
  const adapter = adapters.get(entry.adapterId);
  if (!adapter) return err({ kind: 'adapter_not_registered', ... });
  return ok({ entry, adapter });
}
Ponto crítico: NO SILENT FALLBACK (router.ts:18-22). Se não achar modelo ou adapter, retorna err explícito. Isso evita esconder regressões de custo ou capacidade.

Principais Cross-Cutting Concerns

ArquivoResponsabilidadeKey Lines
adapter-core.tsSpine com validação + never-throws + breaker + retry50 (validate), 82 (guardedAttempt)
ArquivoResponsabilidadeKey Lines
retry.tsBackoff exponencial com jitter17-20 (DEFAULT_RETRY_POLICY)
circuit-breaker.tsState machinecanExecute + feedback
ArquivoResponsabilidadeKey Lines
cost.tsContabilizaçãoaccountFor
errors.tsTipos de erro9-14 + failureFromThrown

Como o Contrato é Honrado em Todos os Providers

Todos os adapters concretos (anthropic, cliproxyapi, local-*, offline, etc.) implementam apenas a função interna attempt. O adapter-core.ts envolve com validação + breaker + retry. Isso garante comportamento idêntico independentemente do provider.

Exemplo: offline.ts é o mais simples possível porque o spine já cuida de tudo (linhas 24-26 do adapter-core comentam exatamente isso).

Deep line-by-line adicional: Em adapter-core.ts:50-66 a validação acontece primeiro. Se falhar, já retorna makeFailure sem nunca tocar o provider. Depois guardedAttempt (82-100) cuida de breaker e catch de throw. O resultado volta para withRetry, que decide se tenta novamente baseado na flag retryable do Result. Finalmente o breaker recebe feedback de sucesso/falha. Todo esse fluxo é o motivo pelo qual nenhum provider precisa se preocupar com "nunca lançar" ou retry — eles só precisam implementar attempt corretamente.

Como Verificar Esta Camada (verify)

pnpm test packages/adapters
# Foco nos testes:
# - adapter-core.test.ts:82 (guardedAttempt + never-throws)
# - retry.test.ts, circuit-breaker.test.ts, cost.test.ts
# - offline.test.ts (o caso mais simples de hermético $0)

Em um run real: use alembic distill ./corpus --offline e confirme que custoUsd = 0 e nenhum adapter de rede é tocado (registry com offline.asDefault: true). Depois rode sem --offline (se tiver credenciais) e compare o FunnelReport.costUsd.

Common confusions: Achar que é possível chamar um provider diretamente e tratar throw — não é. O contrato público sempre passa pelo registry + pickAdapter + adapter-core.run (que devolve Result). Qualquer throw interno vira failureFromThrown.

Practice task: 1. Leia adapter-core.ts:82-100 (guardedAttempt completo). 2. Escreva um teste que força um attempt a lançar exceção. 3. Confirme que o Result final tem failure com código adequado e o breaker recebeu feedback. 4. Adicione temporariamente um novo provider mock e registre ele via createAdapterRegistry (registry.ts:89). Rode um distill offline e observe o comportamento. 5. Compare pickAdapter em router.ts:53 com pickCheapestForTier em contracts (model.ts). 6. Inspecione cliproxyapi.ts e offline.ts para ver como ambos só implementam attempt.

Esta camada é a prova de que o projeto leva a sério o contrato "toda chamada de modelo é uma função pura que devolve Result". Qualquer extensão futura (novo provider, novo cross-cutting) deve respeitar exatamente este fluxo em adapter-core.ts:23 + registry.ts:89 + router.ts:53.

Source Anchors (Evidência Direta)

Arquivo:linhaConceito
/packages/adapters/src/adapter-core.ts:23Spine comment (never-throws contract)
/packages/adapters/src/adapter-core.ts:82guardedAttempt
/packages/adapters/src/registry.ts:89createAdapterRegistry
/packages/adapters/src/router.ts:53pickAdapter (no silent fallback)
/packages/adapters/src/errors.ts:9ErrorCode + failureFromThrown
/packages/adapters/src/retry.ts:17DEFAULT_RETRY_POLICY
/packages/contracts/src/model.ts:30ModelRunInput schema (waist)

Common Confusions + Practice

Confusão: "Posso chamar o adapter diretamente e tratar throw". Não. O contrato público é sempre via registry + pickAdapter + run (que devolve Result). Qualquer throw interno é convertido.

Prática: 1. Leia adapter-core.ts:82-100 (guardedAttempt). 2. Veja como failureFromThrown é chamado no catch. 3. Escreva um teste que força um provider a lançar e confirme que o Result vira failure com código adequado. 4. Compare o comportamento de offline vs cliproxyapi no mesmo input.

Exercício: Adicione um novo provider (ex: um mock local). Registre ele no createAdapterRegistry e rode um distill offline. O que muda no FunnelReport?