Aquele pedido do corpus jurídico já foi roteado para um tier. Agora ele precisa atravessar o único ponto fino por onde toda chamada de modelo passa — e voltar com uma resposta que nunca derruba o harness, mesmo quando a rede, o gateway ou o modelo falham.
packages/adapters/src/adapter-core.ts
Esta lição destila a "espinha de confiabilidade" do mapa: o que o Alembic possui em volta da inferência — Result never-throws, o núcleo guardado (Zod→disjuntor→retry) e o roteador sem fallback silencioso. Leituras de apoio do founder: walkinglabs (PT-BR) e ai-engineering-from-scratch.
ModelAdapter + Result) e por que TODA chamada passa por ela.Result<T,Error> / ModelRunResult e dizer por que ele nunca lança exceção.CLOSED → OPEN → HALF_OPEN e dizer o que cada estado faz.Um sistema confiável não tem mil portas para falar com o modelo. Tem uma só — fina, vigiada, e que sempre devolve algo, nunca explode. Essa porta é a cintura estreita.
No nosso exemplo recorrente, o pedido "monte uma fábrica de petições personalizadas" foi classificado num tier e agora vira uma chamada de modelo. Em vez de cada pedaço do Alembic abrir sua própria conexão HTTP, escrever seu próprio try/catch e inventar seu próprio jeito de tratar erro, tudo passa por um único tipo: o ModelAdapter. Ele recebe um ModelRunInput e devolve um ModelRunResult. Ponto. É a mesma forma para o modelo local de $0 e para o gpt-5.5-xhigh de US$ 0,015/1k — a diferença é um campo (adapterId), não um caminho de código separado.
"Estreita" tem um significado preciso: a interface é tão pequena que cabe na cabeça, então toda a confiabilidade (validar a entrada, abrir o disjuntor, tentar de novo, medir custo, emitir o span) mora num lugar só e vale para os seis adapters de uma vez. É o oposto de espalhar try/catch por trinta arquivos.
Pense como… a tomada de parede da sua casa. Todo aparelho — geladeira, abajur, notebook — fala com a usina elétrica pela mesma tomada padronizada, com o mesmo disjuntor no quadro. Ninguém puxa fio direto do poste. Onde a analogia quebra: a tomada não devolve um relatório de erro estruturado dizendo "tente de novo em 200ms" — a nossa cintura devolve.
A cintura é o tipo ModelAdapter.run(input: ModelRunInput) => Promise<ModelRunResult> em @alembic/contracts. O ModelRunResult é uma união discriminada por ok — ou um sucesso (text, usage?, costUsd?) ou uma falha (error com retryable: boolean). Nenhuma das duas é uma exceção: o chamador faz if (result.ok), não try/catch.
O adapter default é o cliproxyapi: um proxy HTTP OpenAI-compatível puro para http://127.0.0.1:8317. Ele declara capabilities = { streaming, toolUse, vision } e nada abaixo do fio (cache de KV, batching, atenção) — isso é a próxima lição (delegado). A troca offline↔online é o campo adapterId: 'local' (caminho determinístico $0) vs 'cliproxyapi' (o gateway).
HARNESS-MAP.md chama isso de "o único fato em que todo o mapa se apoia": é essa escolha arquitetural que torna ~7 dos 23 princípios delegados em vez de gaps — o Alembic não está sem gerência de KV cache, ele a empurrou conscientemente para baixo da cintura.
Toda a confiabilidade desta lição é uma fatia da jornada do pedido. À esquerda, o tier (lição 0001). À direita, os gates (lição 0005). No meio, em destaque, a cintura — onde estamos agora.
Se a rede cair no meio da chamada de modelo dentro da cintura, o que volta para quem chamou?
ModelRunResult com ok: false e error.retryable sinalizando se vale tentar de novo. O chamador faz if (result.ok) e segue a vida — o harness nunca é derrubado por uma falha de rede. É exatamente isso que "never-throws" significa.
A peça mais fundamental é um tipo minúsculo. Em vez de uma função retornar o sucesso e lançar o erro (dois caminhos diferentes que o chamador precisa lembrar de tratar), ela devolve um só valor que é OU sucesso OU erro. O chamador é obrigado pelo compilador a olhar os dois.
.value sem antes provar que .ok é true.Para chamadas de modelo o Alembic usa uma versão mais rica — o ModelRunResult — que além de ok carrega adapterId, durationMs, requestId e, no erro, um error.retryable: boolean. Esse booleano é o que conecta o resultado ao retry e à escalada: o harness decide o que fazer sem ter que ler a mensagem de erro.
// um Result minúsculo, sem dependências — a base do estilo da casa export interface Ok<T> { readonly ok: true; readonly value: T; } export interface Err<E> { readonly ok: false; readonly error: E; } export type Result<T, E = Error> = Ok<T> | Err<E>; export const ok = <T>(value: T): Ok<T> => ({ ok: true, value }); export const err = <E>(error: E): Err<E> => ({ ok: false, error }); // tryCatchAsync embrulha código que lança e NUNCA rejeita: export const tryCatchAsync = async <T>(fn) => { try { return ok(await fn()); } catch (cause) { return err(toError(cause)); } };
O Result<T,E> é para operações falíveis genéricas (IO de arquivo, parsing, wrapping de subprocesso). Chamadas de modelo usam o ModelRunResult — uma união discriminada via z.discriminatedUnion('ok', [...]) com sucesso e falha validados por schema. Os dois são discriminados por ok de propósito: leem igual nos call sites.
modelRunErrorSchema = { code, message, retryable }. O comentário no contrato é explícito: "retryable lets the harness decide whether a fallback or retry is warranted without parsing the message". Ler a mensagem de texto para decidir o fluxo seria frágil; o booleano é o contrato estável.
failureFromThrown. O throw vira valor.Result não vai a lugar nenhum: é tratado no mesmo lugar onde nasce.try/catch; se esquecer, o erro sobe e derruba o harness. O tipo da função não conta que ela pode falhar.ok:false. Impossível "esquecer" — e nada sobe sem querer.Cada adapter (são seis) implementa só a parte que é dele: uma tentativa — uma chamada de rede/subprocesso. Toda a parte transversal — validar a entrada, gatear no disjuntor, repetir com espera — mora num lugar só: runWithGuards. Assim os seis adapters se comportam de forma idêntica, e a confiabilidade não depende de cada autor lembrar de fazer certo.
A ordem importa, e ela é fixa:
Repare na ordem porque ela é uma decisão de engenharia, não um acaso: validar antes de gastar rede (entrada inválida vira CLIENT_ERROR imediato, não-retryable), disjuntor antes da tentativa (se o upstream está caído, nem tentamos), e retry só quando retryable é true.
// constrói um `run` que NUNCA lança, a partir de uma única tentativa export const runWithGuards = async (adapterId, attempt, input, runtime = {}) => { const validation = validate(adapterId, input); // 1 · Zod safeParse if (!validation.ok) return validation.result; // inválido → CLIENT_ERROR try { return await withRetry( // 4 · backoff por result.retryable () => guardedAttempt(adapterId, attempt, input, runtime.breaker, logger), policy, { clock, logger, random: runtime.random, signal: input.signal }, ); } catch (cause) { // rede de segurança final: withRetry só rejeita se guardedAttempt // rejeitar, o que ele nunca faz — preserva o invariante never-throws. return failureFromThrown({ adapterId, input, durationMs: 0 }, cause); } }; // guardedAttempt: gate no disjuntor → executa (pegando throw) → realimenta if (breaker && !breaker.canExecute()) // 2 · disjuntor aberto? return { retry: true, value: breakerRejection(...), reason: 'circuit_open' }; try { result = await attempt(input); } // 3 · a tentativa real catch (cause) { result = failureFromThrown(..., cause); } // defesa em profundidade if (result.ok) { breaker?.recordSuccess(); return { retry: false, value: result }; } breaker?.recordFailure(); if (result.error.retryable) return { retry: true, value: result, reason: result.error.code }; return { retry: false, value: result }; // erro terminal, ainda um valor
Abra packages/adapters/src/adapter-core.ts no repo. As quatro responsabilidades estão documentadas no topo do arquivo, em ordem: (1) validação de boundary do ModelRunInput via Zod; (2) o invariante never-throws; (3) o gate opcional do CircuitBreaker; (4) o backoff do withRetry guiado pelo retryable de cada resultado.
Backoff com clock injetado: o withRetry dorme min(base·2^n, max) com full jitter — mas via clock.sleep injetado, então um clock fake fast-forward nos testes. A política default: 3 tentativas, 200ms→2s.
Disjuntor aberto = retryable: em adapter-core.ts a rejeição do disjuntor volta retry: true de propósito — "so the policy may wait out a short cooldown". Ou seja, o retry e o disjuntor são acoplados: a espera do backoff dá tempo do cooldown vencer e o próximo gate passar para HALF_OPEN.
Defesa em profundidade: o attempt não deveria lançar, mas se lançar há um catch que vira failureFromThrown — e ainda um catch externo no runWithGuards como rede final. Duas redes para um invariante que não pode quebrar.
catch aninhados em volta da única tentativa. Para o throw furar as duas e derrubar o harness, os dois teriam que falhar — improvável por construção.retryable é a chave que separa "tenta de novo" de "desiste honestamente". Sem precisar ler a mensagem.prompt). A validação Zod barra antes de qualquer rede → volta CLIENT_ERROR, retryable:false. Nenhum token gasto, nenhum disjuntor tocado.retryable:true → breaker.recordFailure(), backoff de ~200ms, tenta de novo. Se a 2ª der certo, volta sucesso e o streak de falhas zera.CIRCUIT_OPEN na hora, sem nem bater na rede, até o cooldown vencer. Agora é sua vez: e se a chamada 6 chegar 31s depois, com cooldown de 30s? (Resposta na seção 5.)O disjuntor (circuit breaker) é o que impede o harness de martelar um serviço já doente. Ele tem três estados, exatamente como o disjuntor do quadro de luz da sua casa:
canExecute() = false) até passar o cooldown (default 30s).Resposta do "agora é sua vez" da seção anterior: a chamada 6, chegando depois do cooldown, faz o disjuntor passar a HALF_OPEN e ela é admitida como tentativa de prova. Se der certo, fecha.
halfOpenMaxCalls tentativas (default 1) e recusa o resto — nas palavras do código, "so a burst cannot stampede a sick upstream". Não basta esperar o cooldown; ele solta a vazão aos poucos para não derrubar de novo um upstream que mal levantou.Mexa no disjuntor de verdade. Cada botão é uma chamada; veja o estado mudar e o log explicar a transição. (Para encurtar a demo, o cooldown aqui é de 3s e o limiar de desarme é 3 falhas — no código real são 30s e 5.)
A classe CircuitBreaker (packages/adapters/src/circuit-breaker.ts) é pura em relação ao tempo via um Clock injetado — não faz IO e nunca lança. canExecute() chama maybeHalfOpen() primeiro (transição OPEN→HALF_OPEN quando o cooldown venceu) e, em HALF_OPEN, reserva um slot do orçamento de prova com halfOpenInFlight += 1.
Em HALF_OPEN, recordSuccess() incrementa halfOpenSuccesses e fecha ao atingir successThreshold; recordFailure() chama trip() na hora (reabre e reinicia o cooldown). É construído um por upstream (um por adapterId) — a saúde do gateway A não derruba o gateway B.
failureThreshold-ésima falha consecutiva que desarma, e o primeiro successThreshold sucessos que fecham. Streak, não acumulado: um sucesso no meio zera tudo.Antes de chegar na cintura, o pedido precisa saber qual modelo chamar. Isso é o tier routing: cada tier (T0–T4) é uma faixa de risco/qualidade, e pickCheapestForTier escolhe o modelo mais barato daquela faixa (somando custo de entrada + saída por 1k tokens). É o "dial" de tradeoff custo×qualidade da lição 0001, agora concreto.
E aqui mora uma decisão de design que é a alma da confiabilidade do Alembic: se não houver modelo para o tier, ou se o adapter daquele modelo não estiver registrado, o roteador NÃO substitui por outro em silêncio. Ele devolve um err tipado e deixa o chamador decidir: escalar o tier ou mostrar a falha.
"Um roteador que silenciosamente trocasse por um modelo diferente esconderia regressões de custo/capacidade, então isso deliberadamente não é feito aqui." — packages/adapters/src/router.ts (comentário no topo)
escalateTier / TIER_LADDER), não uma troca automática por um modelo mais barato. Isso é uma postura de confiabilidade mais forte que uma escada silenciosa. O que falta de verdade — o gap honesto — é o UX de modo degradado: um aviso visível ao usuário de "rodando num modelo mais fraco". O disjuntor por-modelo é real; o que está intencionalmente ausente é a troca opaca de modelo.err nomeados, uma rota de sucesso. Nenhum caminho "troca de modelo escondido": a falha é explícita e quem escala é o chamador.O registry é a fonte da verdade do "dial". Veja como os tiers mapeiam para modelos e preços reais (extraído de registry.ts):
| Tier | Para quê | Modelo mais barato (exemplo) | US$/1k in+out |
|---|---|---|---|
| T0 / LOCAL | silencioso, $0, offline, determinístico | local-default | 0 + 0 |
| T1 | rápido/barato auxiliar | qwen3.7-plus | 0,00015 + 0,0005 |
| T2 | execução balanceada | deepseek-v4-pro / gemini-3.5-flash | 0,0001 + 0,0004 |
| T3 | engenharia crítica / review profundo | gpt-5.5-xhigh | 0,015 + 0,045 |
| T4 | PARK — humano + council (não auto-executa) | — (nenhum modelo) | — |
escalateTier) quando precisa — nunca pula em silêncio.reduce simples: percorre os candidatos do tier e fica com o de menor soma input+output por 1k. Otimiza custo dentro da faixa de qualidade.DEFAULT_TIER = T4. Trabalho não classificado faz park por padrão — na dúvida, não roda sozinho. A confiabilidade começa por não agir quando o risco é desconhecido.Experimente o roteador. Escolha um tier e ligue/desligue adapters registrados. Veja o que pickAdapter devolveria — uma rota, ou um err nomeado (nunca um modelo trocado escondido).
pickAdapter(tier, adapters) em packages/adapters/src/router.ts resolve o modelo mais barato e busca o adapter pelo adapterId. pickCheapestForTier mora em packages/contracts/src/registry.ts e faz um reduce sobre os candidatos do tier comparando costPer1kInputUsd + costPer1kOutputUsd. RouteError tem três variantes nomeadas: no_model_for_tier, adapter_not_registered, unknown_model.
Depois que a chamada volta com sucesso, accountFor(modelId, rawUsage) (cost.ts) precifica o uso pelo registry e devolve undefined — não um 0 enganoso — para modelo desconhecido. Mesma filosofia do never-throws: melhor "não sei" honesto que um número fabricado. (Custo é tema cheio da lição 0007.)
undefined), nunca um zero que mentiria para o ledger. A mesma ética do never-throws.Os seis batimentos desta lição, em um folheável. Use as setas ← → ou os pontos.
Tente responder antes de virar cada cartão (clique, ou Enter). Recuperar ativa a memória muito mais que reler.
ModelAdapter + o Result: o único ponto fino, validado e never-throws por onde TODA chamada de modelo passa. Trocar offline↔online é um campo (adapterId).Result em vez de lançar exceção?ok:false, ninguém "esquece" um try/catch, e nada sobe sem querer para derrubar o harness.retryable. Validar antes de gastar rede; disjuntor antes de tentar.halfOpenMaxCalls provas e recusa o resto, pra um burst não derrubar de novo um upstream que mal levantou. Sucesso fecha; qualquer falha reabre.err nomeado (no_model_for_tier) — nunca troca por outro modelo em silêncio. Quem decide escalar o tier é o chamador.DEFAULT_TIER e por quê?T4 (park). Trabalho não classificado é retido do auto-executar: na dúvida sobre o risco, não roda sozinho. Confiabilidade começa por não agir.Result never-throws, o núcleo guardado (Zod→breaker→retry), o roteamento por tier + pickCheapestForTier, o disjuntor por-upstream. É a espinha de confiabilidade.cliproxyapi + MLX local: KV cache, batching, quantização, prefill/decode. O Alembic conscientemente não possui — e está certo.O gateway começou a dar erro 503 (sobrecarregado). Na primeira falha, a cintura espera um tiquinho e tenta de novo — talvez tenha sido um soluço. Continua falhando? Depois de algumas vezes seguidas, ela desiste de bater na porta por um tempo (disjuntor abre), para não piorar a sobrecarga. Quando o tempo passa, ela arrisca uma chamada de teste. Deu certo → volta ao normal. Falhou → fecha a porta de novo. Em momento nenhum o seu programa "quebra" — ele só recebe um resultado dizendo o que aconteceu.
Um HTTP 503 é mapeado para ModelRunResult com error.retryable = true. guardedAttempt chama breaker.recordFailure() e devolve { retry: true, reason: code }; withRetry dorme computeBackoffMs(attempt-1, policy, random()) = min(200·2^n, 2000) com full jitter, via clock.sleep injetado. Na failureThreshold-ésima falha consecutiva, trip() leva o breaker a OPEN e marca openedAt. Enquanto OPEN, canExecute() é false → breakerRejection com CIRCUIT_OPEN (retryable, para o policy esperar o cooldown). Passado cooldownMs, maybeHalfOpen() move para HALF_OPEN; canExecute() reserva halfOpenInFlight. Um recordSuccess() ≥ successThreshold chama close(); qualquer recordFailure() chama trip(). Tudo puro via Clock, sem IO, never-throws.
CLIENT_ERROR) é retryable:false de propósito: repetir uma entrada inválida só queima tempo e tokens. Por isso a validação Zod vem antes de tudo.Três perguntas — a pontuação corre sozinha
ModelRunResult nunca lança exceção?ok. (a) é falso e irrelevante; (c) é falso — TS permite throw, é uma convenção da casa ("avoid throwing in library code") com duas redes de segurança no adapter-core.runWithGuards?CLIENT_ERROR não-retryable); o disjuntor evita bater num upstream doente antes da tentativa; o retry fecha o ciclo só quando o resultado é retryable.router.ts. E (c) viola o never-throws: é um err tipado, não um throw.As Dez verdades da cintura estreita
ModelAdapter.run(input) → ModelRunResult.ok:true ou ok:false.error.retryable é true.err nomeado.pickCheapestForTier é o dial de tradeoff custo×qualidade.DEFAULT_TIER = T4.circuit-breaker.ts e pergunte-se "por que a rejeição do disjuntor é marcada como retryable?" — a resposta amarra o disjuntor ao retry. A seguir (0003): descemos abaixo da cintura, para o que o Alembic delega ao gateway e ao MLX — KV cache, batching, quantização — e por que delegar é a escolha certa.