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)
• 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
}
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)
// 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 });
}
Principais Cross-Cutting Concerns
| Arquivo | Responsabilidade | Key Lines |
|---|---|---|
| adapter-core.ts | Spine com validação + never-throws + breaker + retry | 50 (validate), 82 (guardedAttempt) |
| Arquivo | Responsabilidade | Key Lines |
|---|---|---|
| retry.ts | Backoff exponencial com jitter | 17-20 (DEFAULT_RETRY_POLICY) |
| circuit-breaker.ts | State machine | canExecute + feedback |
| Arquivo | Responsabilidade | Key Lines |
|---|---|---|
| cost.ts | Contabilização | accountFor |
| errors.ts | Tipos de erro | 9-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:linha | Conceito |
|---|---|
/packages/adapters/src/adapter-core.ts:23 | Spine comment (never-throws contract) |
/packages/adapters/src/adapter-core.ts:82 | guardedAttempt |
/packages/adapters/src/registry.ts:89 | createAdapterRegistry |
/packages/adapters/src/router.ts:53 | pickAdapter (no silent fallback) |
/packages/adapters/src/errors.ts:9 | ErrorCode + failureFromThrown |
/packages/adapters/src/retry.ts:17 | DEFAULT_RETRY_POLICY |
/packages/contracts/src/model.ts:30 | ModelRunInput 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?