Passo 2 · Alembic Completo · Camada L1 · Adapters — a cintura estreita
Alembic Completo · currículo fonte-de-verdade · Visual Course

Camada L1 · Adapters

Esta é a camada que todo modelo no sistema atravessa — CLI, Council, Swarm, Factory, ETL. O contrato é rígido e curto: run(input: ModelRunInput): Promise<ModelRunResult>, um Result discriminado em ok que nunca lança. Nesta lição você vai abrir essa "cintura estreita" linha a linha e ver por que ela é a peça mais importante de confiabilidade do Alembic.

Leia primeiro (fonte primária)
packages/adapters/src/ — adapter-core.ts + registry.ts + router.ts, ancorados em packages/contracts/src/model.ts

Tudo nesta página é citado de arquivo real do monorepo. O único conceito em torno do qual a camada inteira gira é a cintura estreita (the narrow waist): a interface ModelAdapter definida em contracts/src/model.ts e o spine que a implementa em adapters/src/adapter-core.ts. Nada aqui é inventado.

Leia a versão simples, ou abra a camada técnica em qualquer seção.
O que você vai conseguir fazer
  • Explicar o que é a cintura estreita e por que toda chamada de modelo passa por ModelAdapter.run.
  • Descrever o invariante never-throws e por que falha é um Result, não uma exceção.
  • Seguir o fluxo de runWithGuards: validação Zod → circuit breaker → attemptwithRetry → feedback do breaker.
  • Justificar a regra NO SILENT FALLBACK do router e ler os dois err que ele devolve.
  • Listar o que cada provider concreto precisa implementar — e por que é só attempt.
Suposições tolas (o que presumimos de você — bem pouco)
  • Você já viu a lição 0001 (L0 · Contracts + ETL) e sabe que Result<T, E> é "ou deu certo (ok) ou deu errado (err)".
  • Você sabe ler pseudocódigo simples. Não precisa dominar TypeScript — traduzimos cada trecho.
  • Você não precisa saber o que é circuit breaker, backoff ou jitter. A gente constrói esses termos aqui.
1

A grande ideia


Imagine um prédio inteiro com uma só porta giratória. Cada pessoa que entra ou sai — não importa de qual sala, andar ou setor — passa por aquela única porta. Se a porta for confiável, o prédio é confiável. A camada L1 é essa porta: o ponto fino por onde toda chamada de modelo do Alembic obrigatoriamente passa.

O nome técnico é cintura estreita (narrow waist). O sistema tem muitas fontes de chamada lá em cima (o CLI, o Council, o Swarm, a Factory, o pipeline de ETL) e vários destinos lá embaixo (o gateway cliproxyapi, modelos MLX locais, o adapter offline). Entre eles existe um contrato comum: ModelAdapter.run(input): Result. Tudo afunila para esse ponto e depois se reabre — como uma ampulheta.

Pense como… a tomada de parede. Mil aparelhos diferentes (lá em cima) e várias usinas de energia (lá embaixo), mas no meio há um padrão de plugue. Você não reprojeta o ferro de passar para cada usina; ele só fala "plugue". Aqui, cada parte do Alembic só fala "ModelAdapter" — e nunca precisa saber qual modelo, qual provedor, ou o que fazer quando a rede cai.

Por baixo do capô

O pacote @alembic/adapters tem cerca de 30 arquivos .ts em src/ (incluindo os testes). Ele depende quase exclusivamente de @alembic/contracts, que é quem define a cintura: modelRunInputSchema, o modelRunResultSchema (uma união discriminada) e a interface ModelAdapter, todos em contracts/src/model.ts. O @alembic/adapters é quem a implementa.

O comentário de cabeçalho de model.ts é literal: "The narrow waist of Alembic. Every model invocation in the system flows through ModelAdapter.run." É a única camada que todo run de modelo (CLI, Factory, Council, Swarm, ETL) toca — por isso é também a única em que vale a pena concentrar tanta disciplina de confiabilidade.

Diagrama em ampulheta: muitas fontes de chamada (CLI, Council, Swarm, Factory, ETL) convergem para um único losango central rotulado ModelAdapter.run(input): Result com o selo never-throws, e depois se reabrem para três destinos (cliproxyapi gateway, MLX local, offline). Legenda separa orquestração possuída de geração de tokens delegada.

A cintura estreita: tudo converge para um ponto de passagem (ModelAdapter.run) e se reabre para os provedores. Acima da cintura é orquestração possuída; abaixo, geração de tokens delegada.

muitas fontes → um ponto fino → muitos destinos CLI Council Swarm Factory ETL · distill ModelAdapter.run input → Result · never-throws cliproxyapi (gateway) MLX local offline $0
SVG 1 · A forma de ampulheta: a cintura é onde toda chamada se torna idêntica.
Guarde istoCintura estreita = um único contrato (ModelAdapter.run) por onde toda chamada de modelo passa. Mude o que mudar acima ou abaixo, o ponto do meio continua o mesmo.
Antes de continuar — arrisque um palpite

Se toda chamada de modelo passa por ModelAdapter.run, e esse método nunca lança exceção, então quando a rede cai no meio de uma chamada, o que o método devolve?

Um Result de falha{ ok: false, error: { code: 'network_error', retryable: true, … } }. A queda de rede não vira throw; vira um valor. Quem chamou ramifica em result.ok e decide retry/escalar/park. Esse é exatamente o invariante never-throws que vamos abrir a seguir.
2

O contrato: ModelRunInput → Result


A cintura tem duas pontas. Na entrada, um ModelRunInput validável. Na saída, um ModelRunResult que é uma união discriminada em ok: ou um sucesso com texto, ou uma falha com código de erro. Esses dois tipos vivem em contracts/src/model.ts, não em adapters — o contrato é declarado por quem o publica.

A entrada (modelRunInputSchema) carrega o essencial: requestId, modelId, systemPrompt, userPrompt e alguns campos opcionais (maxOutputTokens, temperature, timeoutMs, roleId, metadata). Há um detalhe fino: o signal (de cancelamento) não está no schema Zod — é um objeto de runtime que o Zod não consegue validar, então ele viaja ao lado dos campos validados.

A saída é onde mora a inteligência do contrato. Em vez de "devolve texto ou explode", ela sempre devolve um objeto com um campo ok. Se ok === true: tem text, usage (tokens), opcionalmente costUsd. Se ok === false: tem error com code, message e a flag decisiva retryable.

O schema de entrada (resumido)

// contracts/src/model.ts
export const modelRunInputSchema = z.object({
  requestId:      z.string().min(1),
  modelId:        z.string().min(1),
  systemPrompt:   z.string(),
  userPrompt:     z.string(),
  maxOutputTokens: z.number().int().positive().optional(),
  temperature:    z.number().min(0).max(2).optional(),
  timeoutMs:      z.number().int().positive().optional(),
  roleId:         z.string().min(1).optional(),
  metadata:       z.record(z.string(), z.unknown()).optional(),
});
// signal?: AbortSignal — carregado FORA do schema (host object, não-validável)

A saída é uma união discriminada

export const modelRunResultSchema = z.discriminatedUnion('ok', [
  modelRunSuccessSchema,  // { ok:true,  text, usage?, costUsd?, durationMs, … }
  modelRunFailureSchema,  // { ok:false, error:{ code, message, retryable }, … }
]);

O comentário na própria interface ModelAdapter deixa o invariante explícito: "INVARIANT: ModelAdapter.run NEVER throws. Any error … MUST be returned as a ModelRunFailure … Callers therefore never need a try/catch around run; they branch on result.ok."

ModelRunInput requestId · modelId systemPrompt · userPrompt temperature? · timeoutMs? signal? (fora do Zod) adapter.run never-throws ok: true text · usage · costUsd? ok: false error.code · retryable
SVG 2 · Uma entrada validável, uma saída em dois ramos. ok é o discriminante.
Por que não lançar exceção?
com throw (frágil) try { run(...) } catch (e) { // em todo lugar } com Result (uniforme) if (r.ok) use(r.text) else handle(r.error) // sem try/catch
A flag que decide tudo: retryable
error.retryable true false backoff +nova tentativa falha terminal(devolve já)
DicaO campo retryable é o que separa "tenta de novo" (429, 5xx, rede, timeout) de "não adianta" (4xx de cliente, auth, parse). O harness lê a flag, nunca a mensagem de erro — assim a decisão é estável e testável.
por que o signal fica FORA do schema Zod? modelRunInputSchema — validável (safeParse) requestId · modelId system/userPrompt temperature? timeoutMs? strings, números — Zod sabe validar signal?: AbortSignal objeto de runtime do host não-validável → viaja ao lado
SVG 7 · O contrato valida o que é dado; o signal (cancelamento) é um objeto de runtime, então viaja junto, fora do Zod.
3

O spine: runWithGuards


Aqui está o coração da camada. runWithGuards (em adapter-core.ts) é a função reutilizável que transforma "uma única tentativa" num run que respeita todo o contrato. Cada provider escreve só a tentativa; o spine faz o resto — sempre na mesma ordem.

Pense numa linha de montagem de cinco estações. A peça (a chamada) entra e percorre: (1) validação da entrada, (2) o disjuntor (circuit breaker) decide se pode executar, (3) a tentativa de fato — a única coisa que o provider implementa, (4) o retry com backoff se a falha for retryable, e (5) o feedback de sucesso/falha de volta para o disjuntor. No fim, sai um Result. Nunca uma exceção.

O contrato vivo (comentário do spine, adapter-core.ts)

/** The reusable spine that turns a "single attempt" into a
 *  compliant ModelAdapter `run`. It enforces, in order:
 *   1. boundary validation of ModelRunInput via Zod;
 *   2. the never-throws invariant (any escaped throw → failureFromThrown);
 *   3. an optional CircuitBreaker gate with correct success/failure feedback;
 *   4. withRetry backoff driven by each result's `retryable` flag.
 *  Adapters implement only the inner attempt(input) => Promise<Result>. */

guardedAttempt — onde o throw é domado

const guardedAttempt = async (...) => {
  if (breaker && !breaker.canExecute())          // (2) disjuntor aberto
    return { retry: true, value: breakerRejection(...), reason: 'circuit_open' };
  let result;
  try { result = await attempt(input); }            // (3) a tentativa do provider
  catch (cause) { result = failureFromThrown(..., cause); } // throw → Result
  if (result.ok) { breaker?.recordSuccess(); return { retry:false, value:result }; }
  breaker?.recordFailure();                          // (5) feedback
  return result.error.retryable
    ? { retry:true,  value:result, reason:result.error.code }  // (4) deixa o withRetry decidir
    : { retry:false, value:result };
};

E a entrada pública, runWithGuards, começa exatamente pela validação: const validation = validate(adapterId, input); if (!validation.ok) return validation.result; — invalidou? Devolve Result de falha CLIENT_ERROR sem nunca tocar o provider.

Esteira numerada de 1 a 5 do runWithGuards: validate com Zod safeParse e ramo de erro CLIENT_ERROR; circuit breaker canExecute com ramo CIRCUIT_OPEN; attempt destacado como a única coisa que o provider implementa; withRetry com backoff 200ms a 2s e full jitter; feedback recordSuccess ou recordFailure devolvendo Result.

O fluxo garantido de runWithGuards: o provider só escreve a estação 3 (attempt). O spine cuida das outras quatro — por isso todos os adapters se comportam igual.

runWithGuards — sempre nesta ordem 1 · validateZod safeParse 2 · breakercanExecute? 3 · attempto provider 4 · withRetrybackoff 5 · feedbackrecord* retryable? volta para a estação 3 Result
SVG 3 · As cinco estações do spine. A 4 pode reenviar para a 3 (retry); no fim, sempre um Result.
Exemplo resolvido — um attempt que lança exceção
1
Chega um ModelRunInput válido. validate() roda safeParse, passa. O spine segue.
2
O breaker está fechado (canExecute()true). Entra em guardedAttempt.
3
O attempt do provider, por um bug, lança uma exceção. O try/catch do spine captura e chama failureFromThrown(...) → vira um Result de falha. O throw não escapa.
4
Como result.ok === false, o breaker recebe recordFailure(). Se a falha for retryable, o withRetry aguarda o backoff e tenta de novo; senão, devolve já.
5
Agora você: em que linha o invariante never-throws é honrado quando o provider tem um bug? (Resposta: no catch de guardedAttempt — o failureFromThrown converte o throw em Result. Nenhum provider precisa se preocupar com isso.)
O ponto que muda tudo: como o "nunca lançar", o retry e o disjuntor vivem no spine, escrever um provider novo é trivial — você só implementa attempt correto. Todo o comportamento difícil de confiabilidade já vem de graça e idêntico para todos.
circuit-breaker.ts — a máquina de estados FECHADOcanExecute → true ABERTOcurto-circuita MEIO-ABERTOtesta uma vez muitas falhas após cooldown sucesso → recordSuccess() falha → volta a ABERTO
SVG 8 · O disjuntor: falhas demais abrem o circuito (e a chamada vira CIRCUIT_OPEN, retryable); depois de um cooldown ele testa uma vez antes de voltar a confiar.
4

Registry & Router — sem mágica


Duas peças pequenas conectam tier a adapter. O registry (registry.ts) monta o mapa id → adapter. O router (router.ts) escolhe o modelo mais barato do tier e resolve o adapter dele — e, crucialmente, nunca troca de modelo em silêncio.

O createAdapterRegistry sempre registra o cliproxyapi (é o gateway padrão). Os outros entram só quando você os configura: o local só se você passar uma baseUrl (ex.: MLX/Ollama no Mac), e pi-dev, codex-oauth, anthropic, local-cli só quando suas opções são dadas. Assim um servidor local não-configurado não rouba o roteamento do gateway.

O pickAdapter(tier) chama pickCheapestForTier para achar o modelo mais barato daquele tier; se não houver, devolve err. Achou o modelo mas o adapter dele não está registrado? Outro err. Ele nunca devolve um substituto silencioso.

registry.ts — sempre o gateway, o resto condicional

export const createAdapterRegistry = (options = {}) => {
  const registry = new Map();
  registry.set(cliproxy.id, cliproxy);              // SEMPRE
  if (options.localOpenai?.baseUrl) registry.set(local.id, local);  // condicional
  if (options.piDev)      registry.set(piDev.id, piDev);
  if (options.codexOauth) registry.set(codex.id, codex);
  if (options.anthropic)  registry.set(anthropic.id, anthropic);
  if (options.localCli?.command) registry.set(cli.id, cli);
  if (options.offline) {
    registry.set(offline.id, offline);
    if (options.offline.asDefault)              // liga offline em TODOS os tiers
      for (const e of Object.values(MODEL_REGISTRY)) registry.set(e.adapterId, offline);
  }
  return registry;
};

router.ts — NO SILENT FALLBACK

export const pickAdapter = (tier, adapters, options = {}) => {
  const entry = pickCheapestForTier(tier, 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', tier, modelId, adapterId });
  return ok({ entry, adapter });
};
// adapterForModel(modelId, …) ignora o tier e resolve um modelo específico (+ kind:'unknown_model')

O comentário do router é categórico: "NO SILENT FALLBACK. … A router that quietly substituted a different model would hide cost/capability regressions, so that is deliberately not done here."

Árvore de decisão de pickAdapter: losango central pickCheapestForTier; ramo não leva a err no_model_for_tier; segundo losango adapter registrado, ramo não leva a err adapter_not_registered; ramo sim leva a ok entry adapter; cartão de contraste riscado mostra o que NÃO acontece: trocar de modelo em silêncio.

A regra NO SILENT FALLBACK: dois pontos onde o router pode devolver err explícito, e nunca um modelo substituto. Falhar barulhento > falhar escondido.

pickCheapestForTiertem modelo? sim não err: no_model_for_tier adapters.get(id)registrado? sim não err: adapter_not_registered ok({entry,adapter}) o que NÃO acontece:trocar de modelo em silêncio ✗
SVG 4 · Dois err possíveis, um ok. Nunca um substituto escondido.
Cuidado"Sem fallback silencioso" não é teimosia — é proteção de custo. Se o router trocasse um T1 caro indisponível por um T3 barato sem avisar, você só descobriria pela conta. Devolver err obriga quem chamou a decidir conscientemente: escalar o tier ou abortar.
cliproxyapigateway padrão — SEMPRE registrado local (MLX/Ollama)só com localOpenai.baseUrl offlinehermético $0 — opt-in (asDefault)
sempre

cliproxyapi

O gateway padrão. É o único adapter que createAdapterRegistry registra incondicionalmente — toda configuração de registry tem, no mínimo, ele.

adapters/src/cliproxyapi.ts · registry.ts (registro fixo)

cliproxyapi → registrado sempre.

5

Os cross-cutting concerns


Em volta do spine há um conjunto de arquivos obrigatórios — as preocupações transversais que todo provider herda sem reescrever: retry, circuit breaker, custo, http, erros, logger, clock, token-store. Cada um faz uma coisa, bem.

ArquivoResponsabilidadeFato-chave
retry.tsBackoff exponencial com full jitterDEFAULT_RETRY_POLICY: 3 tentativas, 200ms→2s
circuit-breaker.tsMáquina de estados; canExecute() + feedbackabre o circuito após falhas e dá um cooldown
Full jitter: em vez de esperar exatamente base·2^n, o retry espera um valor aleatório em [0, delay]. Isso espalha as novas tentativas no tempo e evita que mil chamadas reenviem todas no mesmo instante (o "thundering herd").
ArquivoResponsabilidadeFato-chave
cost.tsContabilização de tokens → USDcomputeCostUsd, arredondado a 6 casas
errors.tsTaxonomia de erros + failureFromThrown10 ErrorCode com retryable definido
Honestidade no custo: costForModel devolve undefined quando o modelo é desconhecido — o chamador então omite costUsd em vez de reportar um zero enganoso. Não fabricar um número é melhor que mentir um.
ArquivoResponsabilidadeFato-chave
http.tsWrapper de fetch compartilhadousado pelos providers de rede
clock.tsClock injetável (sleep)um fake clock adianta o backoff nos testes
token-store.tsPersistência de credenciais/tokenspara adapters com OAuth
Por que um Clock injetável? O withRetry de fato aguarda o backoff via clock.sleep. Em produção é o relógio real; no teste é um falso que pula o tempo — assim os testes de retry rodam em milissegundos sem perder o realismo.
adapter-core (spine) retry.ts circuit-breaker.ts cost.ts errors.ts http.ts clock.ts logger.ts token-store.ts
SVG 5 · O spine no centro; as preocupações transversais o cercam e são herdadas por todos.
As 10 ErrorCode (e seu retryable)
retryable rate_limited (429) server_error (5xx) network_error timeout circuit_open não-retryable client_error (4xx) parse_error subprocess_error auth_error not_implemented
computeCostUsd — pura, sem IO
usage (tokens) pricing /1k computeCostUsdround 6 casas costUsd modelo desconhecido→ undefined (omite)
backoff exponencial + full jitter (200ms → 2s, teto 2s) tempo → janela 1: [0,200ms] janela 2: [0,400ms] janela 3: [0,800ms] (≤ teto 2s) a janela dobra a cada tentativa; o ponto = espera SORTEADA dentro dela (espalha o tráfego)
SVG 9 · Full jitter: a janela máxima dobra (200→400→800ms…, com teto de 2s), mas a espera real é um sorteio dentro dela — por isso mil chamadas não reenviam no mesmo instante.
6

Os providers concretos


Existem oito providers concretos, e todos têm a mesma forma minúscula: implementam apenas o attempt. O spine envolve cada um com validação + breaker + retry. Por isso o comportamento é idêntico independentemente de quem gera os tokens.

O offline.ts é o exemplo mais bonito disso: ele é o adapter mais simples possível justamente porque o spine já cuida de tudo. Ele é o caso hermético de $0 — determinístico, sem rede — usado em runs offline. Já o cliproxyapi.ts é um proxy fetch puro para o gateway: ele não tem lógica de inferência, só repassa.

Os oito (cada um exporta um create…Adapter)

// adapters/src/
cliproxyapi.ts      // gateway padrão (proxy fetch puro)
local-openai.ts     // servidor OpenAI-compatível local (MLX/Ollama)
offline.ts          // hermético $0, determinístico
anthropic.ts        // Anthropic de primeira classe
codex-oauth.ts      // Codex via OAuth
codex-exec.ts       // Codex via subprocess (CLI)
pi-dev.ts           // pi.dev
local-cli.ts        // CLI local genérico (presets em cli-presets.ts)

Há ainda openai-compatible.ts como base compartilhada para os providers que falam o dialeto OpenAI. Todos terminam construindo seu run com runWithGuards, então nenhum reimplementa "nunca lançar" ou retry.

cliproxyapi local-openai offline anthropic codex-oauth codex-exec pi-dev local-cli cada um só implementa attempt() runWithGuardso mesmo para todos
SVG 6 · Oito provedores, um único spine. A forma é minúscula porque a confiabilidade é compartilhada.
offline.asDefault: true → liga o $0 a TODOS os tiers offline (um só)determinístico · $0 T1 → adapterId T2 → adapterId T3 → adapterId T4 → adapterId LOCAL → adapterId
SVG 10 · O opt-in asDefault faz o registry sobrescrever cada adapterId de MODEL_REGISTRY com o mesmo adapter offline — qualquer tier resolve para o $0, sem rede.

offline vs cliproxyapi — a mesma forma, custos opostos

Dimensãooffline.tscliproxyapi.ts
Toca a rede?nãosim (fetch ao gateway)
Custo$0 herméticoconforme tokens
Determinísticosimnão
O que implementaattemptattempt
never-throws / retrydo spinedo spine
Nota técnicaCom offline.asDefault: true, o registry liga o adapter offline a todos os adapterId de MODEL_REGISTRY — então o roteamento em qualquer tier resolve para ele, sem rede. É um opt-in explícito (não um fallback silencioso): é assim que alembic distill --offline roda a $0.
A ideia que se repete
O contrato é honrado em todos os providers porque nenhum deles reimplementa o contrato. Eles delegam ao spine. Adicionar um provider novo é escrever um attempt correto e registrá-lo — o resto vem pronto.
7

Recapitulando


A virada de chave

Uma porta para tudo

Toda chamada de modelo do Alembic passa por um ponto fino: ModelAdapter.run(input): Result. É a cintura estreita.

O contrato

Falha é um valor

O Result é uma união discriminada em ok. run nunca lança: quem chama ramifica em result.ok, sem try/catch.

O spine

Cinco estações garantidas

runWithGuards: validate → breaker → attempt → withRetry → feedback. O provider só escreve a estação attempt.

O roteamento

NO SILENT FALLBACK

pickAdapter devolve err (no_model_for_tier ou adapter_not_registered) — nunca um modelo substituto. Falhar barulhento > falhar escondido.

O transversal

Confiabilidade compartilhada

retry (3×, 200ms→2s, full jitter), circuit breaker, custo (round 6 casas, undefined se desconhecido), clock injetável. Tudo herdado.

Para a próxima

Subir uma camada

A lição 0003 entra no L2 · Council: quando uma chamada não basta e várias perspectivas precisam debater e votar um veredito.

1 / 6setas

Simples ↔ Técnico: a mesma ideia, duas alturas

Alterne entre a explicação leiga e a precisa. Use "Técnico" quando quiser os nomes reais; "Simples" quando quiser a intuição.

Em linguagem de gente: a L1 é a porta giratória única do prédio, com um segurança que nunca grita (never-throws): se algo der errado, ele te entrega um bilhete dizendo o que houve e se vale tentar de novo, em vez de derrubar tudo. O segurança também não deixa entrar por uma porta que não existe (sem fallback silencioso): se a sala que você pediu não está no mapa, ele diz "não tem", e não te empurra para outra sala na surdina.
Com os termos reais: ModelAdapter.run é o contrato; runWithGuards o implementa com validate (safeParse) → CircuitBreaker.canExecuteattempt (capturado por failureFromThrown) → withRetry (DEFAULT_RETRY_POLICY: 3 tentativas, 200ms→2s, full jitter) → recordSuccess/recordFailure. pickAdapter usa pickCheapestForTier e devolve err em vez de substituir. computeCostUsd é puro; offline.asDefault liga o $0 a todos os tiers.

Cartões de memória

Vire cada cartão (clique, ou Enter/Espaço) e tente responder antes de ver o verso. É prática de recuperação — vale mais que reler.

Conceito
O que é a "cintura estreita"?
clique para virar
A interface ModelAdapter.run(input): Result por onde toda chamada de modelo passa. Um contrato, um ponto de passagem.
Invariante
Por que run nunca lança?
clique para virar
Para o chamador nunca precisar de try/catch: ele ramifica em result.ok. Falha é um valor, não uma exceção — controle de fluxo uniforme.
Spine
O que cada provider precisa escrever?
clique para virar
Só a função attempt(input). Validação, breaker, retry e never-throws vêm do spine (runWithGuards), iguais para todos.
Router
O que significa NO SILENT FALLBACK?
clique para virar
O router devolve err (no_model_for_tier / adapter_not_registered) em vez de trocar de modelo escondido — que esconderia regressões de custo/capacidade.
Retry
Qual é a DEFAULT_RETRY_POLICY?
clique para virar
3 tentativas, backoff de 200ms até 2s, com full jitter. O retry só dispara quando o Result tem retryable: true.
Custo
E se o modelo for desconhecido no cálculo de custo?
clique para virar
costForModel devolve undefined e o chamador omite costUsd — em vez de reportar um zero enganoso. Honestidade > número falso.
Revisão rápida

Responda as três. O placar abaixo conta seus acertos.

1. Quando o attempt de um provider lança uma exceção por um bug, o que acontece?
(b). O guardedAttempt tem um try/catch de defesa em profundidade: qualquer throw vira Result via failureFromThrown, honrando o never-throws. (a) viola o invariante; (c) é falso — o breaker recebe recordFailure().
2. O tier pedido não tem nenhum modelo registrado. O que pickAdapter devolve?
(c). NO SILENT FALLBACK: sem modelo para o tier → err({ kind: 'no_model_for_tier', tier }). (a) é exatamente o que o router se recusa a fazer; (b) inventaria um sucesso falso.
3. Por que o adapter offline.ts é tão simples?
(a). Como toda a confiabilidade transversal mora no runWithGuards, um provider só precisa de um attempt correto — e o offline determinístico $0 é o mais enxuto. (b) e (c) violariam o contrato.
Acertos: 0/3
Em uma frase, para você mesmo: "A camada L1 é ____; toda chamada devolve ____ em vez de lançar; cada provider só implementa ____; e o router, quando não acha modelo, devolve ____." Se você preencheu as quatro lacunas, está pronto para a lição 0003.
8

Como verificar esta camada


Nada nesta página vale sem prova. Aqui está como confirmar, no seu próprio terminal, que a cintura se comporta como descrito.

Roteiro de verificação (Proof Gate da camada)
1
Rode os testes da camada: pnpm --filter @alembic/adapters test. Foque em adapter-core.test.ts (guarded + never-throws), retry.test.ts, circuit-breaker.test.ts, cost.test.ts e offline.test.ts.
2
Force o caminho do throw: escreva um teste em que o attempt lança e confirme que o Result final tem ok: false com um code adequado e que o breaker recebeu recordFailure().
3
Confirme o $0: alembic distill ./corpus --offline e verifique no FunnelReport que costUsd = 0 e nenhum adapter de rede foi tocado (registry com offline.asDefault: true).
4
Agora você: registre um provider mock em createAdapterRegistry e compare o comportamento de offline vs cliproxyapi no mesmo input. O que muda no FunnelReport? (Dica: a forma do Result é idêntica; o que muda é custo e determinismo.)
# a camada inteira, verde:
pnpm --filter @alembic/adapters test
# o baseline do monorepo (toda mudança mantém isto verde):
pnpm -r typecheck && pnpm -r build && pnpm -w test
Confusão comum"Posso chamar um provider direto e tratar o throw." Não. O contrato público é sempre via registry + pickAdapter + run (que devolve Result). Qualquer throw interno é convertido pelo spine — você nunca precisa de try/catch em volta de run.
As sete verdades da camada L1
  1. Toda chamada de modelo passa por ModelAdapter.run — a cintura estreita.
  2. run nunca lança; devolve um Result discriminado em ok.
  3. A flag retryable (não a mensagem) decide retry vs falha terminal.
  4. Cada provider só implementa attempt; o spine faz o resto.
  5. O router devolve err, nunca um modelo substituto (NO SILENT FALLBACK).
  6. O custo é puro e honesto: undefined para modelo desconhecido, nunca um zero falso.
  7. offline.asDefault liga o $0 hermético a todos os tiers — um opt-in explícito.
Pergunta para levar à próxima lição: se uma única chamada de modelo já é tão blindada, o que muda quando o problema exige várias chamadas que precisam discordar entre si e chegar a um veredito? Essa é a camada L2 · Council.