Passo 2 · Módulo 1 · Fundamentos · Cintura estreita & confiabilidade
Curso de Harness Engineering · Visual Course

Cintura estreita & confiabilidade

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.

Leia primeiro (fonte primária)
HARNESS-MAP.md · Parte II + 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.

Leia a versão simples, ou abra a camada técnica em qualquer seção.
Ao fim desta lição você consegue
  • Explicar o que é uma cintura estreita (ModelAdapter + Result) e por que TODA chamada passa por ela.
  • Ler o tipo Result<T,Error> / ModelRunResult e dizer por que ele nunca lança exceção.
  • Descrever o núcleo guardado na ordem certa: validar (Zod) → disjuntor → retry/backoff.
  • Simular um disjuntor passando por CLOSED → OPEN → HALF_OPEN e dizer o que cada estado faz.
  • Justificar a política NO-SILENT-FALLBACK do roteador e como o chamador escala o tier.
Suposições tolas (o que assumimos de você)
  • Você leu a lição 0001 (sabe que o Alembic é o harness e que a cintura é a dobradiça entre Modelo e Harness).
  • Você já viu uma função que pode "dar erro" — mas não precisa saber TypeScript a fundo. Vamos ler os tipos juntos.
  • Você não precisa saber o que é circuit breaker, Zod ou backoff. Cada um nasce explicado aqui.
1

A grande ideia


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.

Por baixo do capô

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.

An instructional cutaway diagram of a single narrow waist as the only passage. On the left, many small distinct labelled objects of different shapes and colors (a database cylinder
An instructional cutaway diagram of a single narrow waist as the only passage. On the left, many small distinc
2

Em uma imagem


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.

UMA UNIDADE DE TRABALHO ATRAVESSANDO O HARNESS — esta lição = a cintura pedido "fábrica de petições" tier routing T0–T4 A CINTURA ESTREITA ModelAdapter · Result Zod → breaker → retry never-throws gates Proof · verifier custo + span $ · OTEL park? T4 / ship
Leia da esquerda → direita: o pedido entra, é roteado por tier, atravessa a CINTURA (esta lição), passa pelos gates, tem custo medido e span emitido, e termina em park (T4) ou ship.
Faça uma aposta antes de seguir

Se a rede cair no meio da chamada de modelo dentro da cintura, o que volta para quem chamou?

Volta um valor, não uma exceção: um 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.

3

O contrato Result — never-throws


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.

Result<T, Error> discriminado por .ok { ok: true, value: T } deu certo { ok: false, error: E } falhou — mas é um VALOR if (result.ok) { … } else { … }
Um caminho de saída, dois resultados possíveis. O compilador não deixa você ler .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.

packages/contracts/src/result.ts
// 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)); }
};

Por que dois tipos (Result e ModelRunResult)?

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.

O detalhe que carrega peso

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.

Invariante da casaNever-throws não é uma sugestão: o CLAUDE.md do repo manda "avoid throwing in library code" e o adapter-core tem uma rede de segurança final que converte qualquer throw escapado em failureFromThrown. O throw vira valor.
throw — a exceção ESCAPA função lança sobe a pilha… 💥 derruba o harness Result — o erro RETORNA função devolve { ok:false } tratado AQUI: if (r.ok) … else …
O throw foge para cima até alguém pegá-lo (ou ninguém pega, e cai tudo). O Result não vai a lugar nenhum: é tratado no mesmo lugar onde nasce.
Estilo "joga exceção"
A função sinaliza erro lançando. O chamador tem que lembrar de embrulhar em try/catch; se esquecer, o erro sobe e derruba o harness. O tipo da função não conta que ela pode falhar.
Estilo "devolve Result"
A função retorna a falha como dado. O compilador força o chamador a tratar o ramo ok:false. Impossível "esquecer" — e nada sobe sem querer.
4

O núcleo guardado — Zod → disjuntor → retry


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:

input ModelRunInput 1 · valida (Zod) safeParse CLIENT_ERROR (não-retryable) 2 · disjuntor canExecute()? aberto → CIRCUIT_OPEN (retryable: espera o cooldown) 3 · attempt() a chamada real result.ok? + retryable? sucesso → breaker.success retorna ModelRunResult ok 4 · retry: breaker.failure + backoff min(base·2^n, max) → volta ao passo 2 erro final → retorna ModelRunResult falha (nunca lança)
A ordem é fixa: validar primeiro (lixo nem chega na rede), gatear no disjuntor (não bate num upstream doente), tentar, e só então decidir entre sucesso, retry-com-espera, ou erro terminal — sempre devolvendo um valor.

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.

packages/adapters/src/adapter-core.ts
// 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

Acesse você mesmo

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.

Os três detalhes finos

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.

rede 2 · runWithGuards try/catch → failureFromThrown rede 1 · guardedAttempt try/catch → failureFromThrown attempt(input) qualquer throw que escape uma rede cai na próxima → sempre vira VALOR
Dois 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.
teto 2000ms (max) ~200 ~400 ~800 espera = min(200·2ⁿ, 2000), com jitter
Backoff exponencial: cada retry espera o dobro do anterior, até o teto. Dá tempo ao upstream se recuperar — e o full jitter espalha as tentativas para não sincronizar uma horda.
resultado de erro error.retryable? true false retry + backoff 429 · 5xx · rede erro terminal CLIENT_ERROR volta ao disjuntor retorna ao chamador
Um único booleano decide o destino: retryable é a chave que separa "tenta de novo" de "desiste honestamente". Sem precisar ler a mensagem.
Exemplo guiado — o pedido "fábrica de petições" sob 3 cenários
1
Entrada inválida (faltou prompt). A validação Zod barra antes de qualquer rede → volta CLIENT_ERROR, retryable:false. Nenhum token gasto, nenhum disjuntor tocado.
2
Gateway com soluço (HTTP 503). A tentativa volta falha retryable:truebreaker.recordFailure(), backoff de ~200ms, tenta de novo. Se a 2ª der certo, volta sucesso e o streak de falhas zera.
3
Gateway morto (5 falhas seguidas). O disjuntor abre: as próximas chamadas voltam 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.)
A four-stage instructional pipeline read strictly left to right, each stage a clearly separated rounded box connected by a single bold arrow. Stage one: a shield with a checkmark (
A four-stage instructional pipeline read strictly left to right, each stage a clearly separated rounded box co
5

O disjuntor, ao vivo — CLOSED → OPEN → HALF_OPEN


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:

  • CLOSED (fechado) — corrente passa. Cada falha consecutiva conta; ao atingir o limite (default 5) ele desarma para OPEN. Qualquer sucesso zera o contador.
  • OPEN (aberto) — as chamadas são curto-circuitadas na hora (canExecute() = false) até passar o cooldown (default 30s).
  • HALF_OPEN (meio-aberto) — passado o cooldown, ele admite um punhado de chamadas de teste. Os primeiros sucessos o fecham; qualquer falha o reabre na hora.

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.

O detalhe que quase todo tutorial esquece: em HALF_OPEN o disjuntor admite no máximo 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.)

estado: CLOSED · falhas seguidas: 0
CLOSED passa OPEN bloqueia HALF_OPEN testa 1 cooldown vence N falhas seguidas → desarma sucesso de prova → fecha falha na prova → reabre

Por baixo do capô

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.

Sem off-by-oneO comentário do arquivo garante a máquina "no off-by-one": é a failureThreshold-ésima falha consecutiva que desarma, e o primeiro successThreshold sucessos que fecham. Streak, não acumulado: um sucesso no meio zera tudo.
um CircuitBreaker por adapterId — falha isolada gateway A OPEN doente gateway B CLOSED ok B segue atendendo normalmente A bloqueado — não contamina B
A saúde de um upstream é independente da do outro. Um gateway caído não derruba o resto do sistema — o oposto de um único disjuntor global.
6

Roteamento, tiers e o NO-SILENT-FALLBACK


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)
Honestidade do mapa: "fallback gracioso" no Alembic significa falhar alto + chamador escala (via 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.
tier T0–T4 tem modelo? cheapest err: no_model_for_tier adapter registrado? err: adapter_not_registered ok: Route entry + adapter chamador escalateTier() err → o chamador decide subir o tier
Dois losangos, dois 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):

TierPara quêModelo mais barato (exemplo)US$/1k in+out
T0 / LOCALsilencioso, $0, offline, determinísticolocal-default0 + 0
T1rápido/barato auxiliarqwen3.7-plus0,00015 + 0,0005
T2execução balanceadadeepseek-v4-pro / gemini-3.5-flash0,0001 + 0,0004
T3engenharia crítica / review profundogpt-5.5-xhigh0,015 + 0,045
T4PARK — humano + council (não auto-executa)— (nenhum modelo)
o DIAL de tradeoff — escalateTier sobe um degrau T0 · $0 offline T1 · barato T2 · balanceado T3 · poderoso T4 · humano (park) + custo / + qualidade / + supervisão
Os tiers são degraus: subir custa mais e supervisiona mais. O chamador escala um degrau por vez (escalateTier) quando precisa — nunca pula em silêncio.
pickCheapestForTier(T2) = min(in+out) deepseek-v4-pro0,0005 ✓ gemini-3.5-flash0,0005 glm-5.20,0008 escolhe o de menor in+out (empate → 1º)
Um 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.
LembreDEFAULT_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).

Adapters registrados (clique para alternar):

Acesse você mesmo

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.

O custo, do outro lado da cintura

Depois que a chamada volta com sucesso, accountFor(modelId, rawUsage) (cost.ts) precifica o uso pelo registry e devolve undefinednã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.)

accountFor(id, usage) modelo conhecido modelo desconhecido { usage, costUsd } costUsd = undefined (nunca um 0 enganoso)
Honestidade também no custo: sem preço conhecido, o resultado é "não sei" (undefined), nunca um zero que mentiria para o ledger. A mesma ética do never-throws.
A side-by-side instructional contrast split by a vertical dashed divider down the middle. Left panel titled with an empty banner shows a forked road where a traveler silently switc
A side-by-side instructional contrast split by a vertical dashed divider down the middle. Left panel titled wi
7

Recapitulação em slides


Os seis batimentos desta lição, em um folheável. Use as setas ou os pontos.

A cintura

Uma porta, não mil

Toda chamada de modelo passa por um ponto fino: ModelAdapter.run(input) → ModelRunResult. Offline↔online = um campo, não um caminho de código.

Result

O erro vira valor

Result<T,Error> / ModelRunResult é ok:true ou ok:false — nunca uma exceção. O chamador faz if (result.ok); o harness nunca é derrubado.

O núcleo guardado

Validar → disjuntor → retry

Em ordem fixa: Zod barra lixo antes da rede; o disjuntor não bate em upstream doente; o retry só dispara quando retryable é true. Um lugar, seis adapters idênticos.

Zodbreakerretry

Disjuntor

Três estados, sem stampede

CLOSED → OPEN → HALF_OPEN. Em meio-aberto ele admite só algumas provas e recusa o resto — "so a burst cannot stampede a sick upstream". Um por upstream.

Roteador

Sem fallback silencioso

Sem modelo para o tier? err nomeado, não um modelo trocado escondido. Trocar em silêncio esconderia regressão de custo/capacidade. Quem escala é o chamador.

Tiers

O dial de tradeoff

T0/$0 → T3/poderoso → T4/humano. pickCheapestForTier escolhe o mais barato da faixa. DEFAULT_TIER = T4: na dúvida, park.

1 / 6setas
8

Cartões de memória


Tente responder antes de virar cada cartão (clique, ou Enter). Recuperar ativa a memória muito mais que reler.

Cintura
O que é a "cintura estreita"?
clique para virar
O tipo 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
Por que devolver Result em vez de lançar exceção?
clique para virar
Porque o erro vira valor: o compilador força tratar o ramo ok:false, ninguém "esquece" um try/catch, e nada sobe sem querer para derrubar o harness.
Ordem
Qual a ordem do núcleo guardado?
clique para virar
1 validar (Zod) → 2 disjuntor → 3 tentativa → 4 retry/backoff por retryable. Validar antes de gastar rede; disjuntor antes de tentar.
Disjuntor
O que HALF_OPEN faz de especial?
clique para virar
Admite só halfOpenMaxCalls provas e recusa o resto, pra um burst não derrubar de novo um upstream que mal levantou. Sucesso fecha; qualquer falha reabre.
Roteador
O que o roteador faz quando não há modelo para o tier?
clique para virar
Devolve um err nomeado (no_model_for_tier) — nunca troca por outro modelo em silêncio. Quem decide escalar o tier é o chamador.
Tiers
Qual é o DEFAULT_TIER e por quê?
clique para virar
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.
9

Dois contrastes que fixam a ideia


Fallback silencioso vs. falhar-alto (o que o Alembic escolheu)

FALLBACK SILENCIOSO (rejeitado) T3 falha → troca p/ T1 escondido resposta pior, e ninguém percebe esconde regressão de custo/capacidade FALHA-ALTO (o Alembic) T3 sem modelo → err nomeado chamador escala (visível) ou para a regressão fica óbvia, não enterrada
Mesma falha, dois mundos. À direita o problema aparece; à esquerda ele apodrece silencioso. O gap honesto: falta só o aviso de UX "rodando degradado".

owned vs. delegated — onde esta lição mora

owned (esta lição) — o que o Alembic implementa em TS: o Result never-throws, o núcleo guardado (Zod→breaker→retry), o roteamento por tier + pickCheapestForTier, o disjuntor por-upstream. É a espinha de confiabilidade.
delegated (próxima lição) — o que mora abaixo da cintura, no gateway cliproxyapi + MLX local: KV cache, batching, quantização, prefill/decode. O Alembic conscientemente não possui — e está certo.
Em uma frase: a cintura é onde o Alembic transforma um modelo instável numa peça de infraestrutura previsível — validando, isolando falha e medindo, sem nunca explodir.
10

Simples ↔ Técnico — a mesma falha, duas leituras


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.

CuidadoRetry só ajuda contra falhas transitórias (429/5xx/rede). Erro de entrada (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.
An instructional state-machine diagram of three labelled circular nodes arranged in a triangle, connected by directional arrows. A green node on the left (closed, current passes),
An instructional state-machine diagram of three labelled circular nodes arranged in a triangle, connected by d
11

Cheque seu saber


Três perguntas — a pontuação corre sozinha

1. Por que o tipo ModelRunResult nunca lança exceção?
É a (b). O erro vira dado discriminado por 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.
2. Qual é a ordem correta do núcleo guardado em runWithGuards?
É a (c). Validar barra lixo antes de gastar rede (vira 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.
3. O tier T3 não tem modelo disponível. O que o roteador faz?
É a (a). NO-SILENT-FALLBACK: trocar em silêncio (b) esconderia regressão de custo/capacidade — explicitamente rejeitado no router.ts. E (c) viola o never-throws: é um err tipado, não um throw.
Acertos: 0/3

As Dez verdades da cintura estreita

  1. uma porta para o modelo: ModelAdapter.run(input) → ModelRunResult.
  2. O resultado é um valor, nunca uma exceção — ok:true ou ok:false.
  3. O chamador é obrigado pelo compilador a tratar o ramo de erro.
  4. A entrada é validada por Zod antes de qualquer rede.
  5. O disjuntor isola um upstream doente: CLOSED → OPEN → HALF_OPEN.
  6. HALF_OPEN solta a vazão aos poucos para não causar stampede.
  7. O retry dispara só quando error.retryable é true.
  8. O roteador nunca troca de modelo em silêncio — devolve err nomeado.
  9. pickCheapestForTier é o dial de tradeoff custo×qualidade.
  10. Na dúvida, park: DEFAULT_TIER = T4.
Você é o professor agora: olhe 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.