run() que nunca lançaToda invocação de modelo no Alembic — todo adapter, todo membro do council, todo worker do swarm, todo tier do funil — atravessa um único formato de função: uma chamada async que nunca lança e devolve um valor discriminado em ok. Este é o invariante mais importante da base de código. Os subsistemas @alembic/hermes que você viu nas lições 7–13 se conectam a esta cintura; entendê-la é entender por que uma rede instável ou um 429 não conseguem derrubar um run. Fonte: packages/contracts/src/model.ts + packages/adapters/src/adapter-core.ts, governado pela ADR-0009.
packages/contracts/src/model.ts — a união ModelRunResult + a interface ModelAdapter ("NEVER throws"), ancorada no spine packages/adapters/src/adapter-core.ts
Tudo nesta página é citado de arquivo real do monorepo. O conceito em torno do qual a camada inteira gira é a cintura estreita (the narrow waist): o tipo de retorno é uma união discriminada e o spine runWithGuards é o que torna o "nunca lança" verdadeiro de verdade — não um comentário esperançoso. Nada aqui é inventado.
ModelAdapter.run.ModelRunResult e dizer por que falha é um valor, não uma exceção.runWithGuards: Zod no boundary → guardedAttempt → withRetry → rede de segurança externa.ModelRunFailure carrega um retryable: boolean classificado na fonte.ModelRunResult do irmão leve Result<T, E> e dizer por que ambos compartilham o discriminante ok.@alembic/hermes devolver um Result.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 cintura estreita é essa porta: o ponto fino por onde toda chamada de modelo do Alembic obrigatoriamente passa.
O sistema tem muitas origens 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), que devolve um valor onde a primeira coisa que você lê é o campo ok. 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.
A cintura estreita como ampulheta — toda chamada de modelo passa por uma porta só.
Num sistema que dispara uma requisição para dezenas de modelos em paralelo (o painel do council, o fan-out do swarm), a diferença entre "um 500 de um provedor degrada aquela faixa" e "um 500 de um provedor derruba o run" está inteira em como a falha viaja. Se ela viaja como exceção, ela escapa para cima e contamina o chamador. Se ela viaja como valor, ela fica contida no resultado daquela faixa. A cintura estreita força a segunda opção em todo ponto de chamada — não por convenção, mas pelo tipo de retorno.
É por isso que a ADR-0009 trata o "never-throws" como invariante de sistema, e não como preferência de código: é a peça que torna o paralelismo seguro.
O contrato é uma união discriminada sobre um campo literal ok. Não existe throw no tipo — falha é um valor, não um evento de controle de fluxo:
Uma função declara run(input): Promise<ModelRunResult>. Olhando só para essa assinatura, quantas formas distintas o resultado pode ter — e o que você precisa checar antes de usar o texto?
ok: true, com text) ou falha (ok: false, com error). Você checa if (result.ok) primeiro; o TypeScript então estreita o tipo e só deixa você ler .text no ramo de sucesso. A assinatura sozinha já te diz tudo — nenhuma exceção escondida.packages/contracts/src/model.ts — os dois ramos (condensado)
type ModelRunSuccess = { // ok: literal(true) é o discriminante ok: true; adapterId: string; durationMs: number; modelId: string; text: string; usage?: TokenUsage; costUsd?: number; raw?: unknown; }; type ModelRunFailure = { // ok: literal(false) ok: false; adapterId: string; durationMs: number; modelId: string; error: { code: string; message: string; retryable: boolean }; }; type ModelRunResult = z.discriminatedUnion('ok', [Success, Failure]);
O chamador escreve if (result.ok) e o TypeScript estreita o tipo. Crucialmente, o ramo de falha carrega um retryable: o ponto de chamada não precisa adivinhar se um 429 vale a pena repetir — o resultado já diz. A própria interface documenta a lei: run(input): Promise<ModelRunResult> // NUNCA lança (invariante).
ok; o compilador só libera text no ramo verde e error no ramo vermelho.ok) cujo valor literal (true ou false) identifica qual ramo você tem. O compilador usa esse literal para "estreitar": dentro de if (result.ok) { … } ele sabe que result.text existe; fora dele, sabe que result.error existe. Você não pode ler o campo errado por acidente — vira erro de compilação..text num ramo de falha: o compilador barra antes de rodar.O tipo é definido a partir de um schema Zod (z.discriminatedUnion('ok', […])), não só com type. Isso dá duas coisas que um tipo puro não dá: (1) o tipo TypeScript é derivado do schema com z.infer, então tipo e validador nunca divergem; (2) o mesmo schema pode validar em runtime — por exemplo, validar o que um provider devolveu antes de confiar nele. O contrato vira executável, não só uma anotação que o compilador apaga.
durationMs, usage e costUsd sobem em ambos os ramos quando aplicável: é assim que o BudgetGuard contabiliza gasto e o cockpit mostra latência sem precisar instrumentar cada provider à parte. A cintura é também o ponto único de telemetria.
Um comentário dizendo "nunca lança" não vale nada se um adapter esquecer. Então o invariante é estrutural: cada adapter implementa apenas um attempt() interno, e um único spine compartilhado — runWithGuards — o embrulha com quatro camadas, em ordem:
As quatro guardas em ordem, de fora para dentro — só o miolo (attempt) é escrito por cada adapter.
packages/adapters/src/adapter-core.ts:118-147 — o spine canônico (condensado)
export const runWithGuards = async (adapterId, attempt, input, runtime = {}) => { const validation = validate(adapterId, input); // ① Zod no boundary if (!validation.ok) return validation.result; // input ruim ⇒ Failure, não throw try { return await withRetry( // ③ backoff em resultados retryable () => guardedAttempt(adapterId, attempt, input, runtime.breaker, logger), // ② policy, { clock, logger, random: runtime.random, signal: input.signal }, ); } catch (cause) { // withRetry só rejeita se guardedAttempt rejeitar, o que ele nunca faz; // esta é uma rede de segurança final para preservar o invariante. return failureFromThrown({ adapterId, input, durationMs: 0 }, cause); // ④ } };
attempt() de um adapter dispara um TypeError cru lá no fundo do corpo dele (digamos, leu um campo undefined da resposta do provedor).guardedAttempt tem um try/catch ao redor do attempt. O throw é capturado e convertido em failureFromThrown(...), virando um ModelRunFailure tipado. O breaker registra a falha (recordFailure()).withRetry recebe um valor (não uma exceção). Se result.error.retryable for verdadeiro, ele faz backoff e tenta de novo; senão, devolve o valor como está.try/catch externo do runWithGuards converteria via failureFromThrown. Resultado: o chamador de run() sempre recebe um ModelRunResult resolvido — a Promise nunca rejeita.input sem modelId. Em qual camada isso para, e o attempt() chega a rodar? (Resposta: para na ① antes do try; return validation.result devolve um Failure e o attempt nunca roda.)run(): Promise<X>, o que ela pode lançar, então os chamadores ou capturam demais ou esquecem. Uma união discriminada torna todo caminho de falha visível e exaustivo: o compilador te obriga a tratar o ok: false. Num sistema que dispara uma requisição para dezenas de modelos em paralelo, essa é a diferença entre "o 500 de um provedor degrada aquela faixa" e "o 500 de um provedor derruba o run".O comentário no código é literal: withRetry só rejeitaria se guardedAttempt rejeitasse, e guardedAttempt nunca rejeita (ele captura tudo internamente). Logo, hoje, o catch externo não pode disparar. Ele permanece porque o custo é um try e o prejuízo de o invariante quebrar é catastrófico — defesa em profundidade na costura mais carregada do sistema. Se um refactor futuro quebrar uma suposição interna, a rede externa ainda segura o "nunca lança".
Antes do try, validate(adapterId, input) roda o schema Zod no input. Se falhar, runWithGuards faz return validation.result — um ModelRunFailure — sem nunca entrar no bloco protegido. Input malformado morre no boundary, não no meio do attempt. Isso mantém os adapters concentrados só na chamada ao provedor, sem revalidar input.
retryable decideA camada ② é onde o circuit-breaker aprende. guardedAttempt chama breaker?.recordSuccess() num resultado ok e breaker?.recordFailure() caso contrário, e reporta retry: true só quando result.error.retryable (adapter-core.ts:103-110). E há um caminho que nem chega a tentar: se o breaker já estiver aberto, guardedAttempt retorna cedo com circuit_open — protege um provedor que já está caído de levar mais carga.
Por que classificar a "repetibilidade" na fonte? Porque o adapter conhece o provedor. Um 429 (rate limit) ou um 5xx transitório são retryable: true; um 400 (input inválido) ou 401 (sem credencial) são retryable: false — repetir só desperdiça. Classificar uma vez, na fonte, mantém a política uniforme e impede que cada ponto de chamada reinterprete o erro à sua maneira (o que levaria a drift).
circuit_open sem tocar o provedor; dá tempo para ele se recuperar.attempt devolve ok: true, o breaker faz recordSuccess(), sem retry. O chamador lê result.text. Sempre um ModelRunResult — nunca um throw.retryable existe justamente para não repetir erros que repetição não conserta (400/401). Um sistema que repete tudo cegamente transforma um erro de credencial em uma tempestade de requisições inúteis — e ainda toma rate-limit por cima.
Antes de chamar o attempt, guardedAttempt consulta o breaker. Se o circuito está aberto (falhas demais recentes), ele retorna cedo: { retry: true, value: breakerRejection(...), reason: 'circuit_open' } — sem nem tocar o provedor. Isso é o circuit-breaker fazendo seu trabalho: dar tempo para um serviço caído se recuperar em vez de martelá-lo. O withRetry ainda decide o backoff a partir do retry: true.
withRetry recebe uma policy e os seams injetados clock e random (para jitter determinístico nos testes). Ele só repete resultados marcados retry: true; nunca repete um ok nem uma falha não-repetível. O número de tentativas e o crescimento do atraso vivem na policy, não espalhados pelos adapters.
Result<T, E> para todo o restoExiste uma segunda união discriminada, mais leve — também sobre ok, com ramos value/error — para todo trabalho falível que não é chamada de modelo: IO de arquivo, parsing, wrapping de subprocesso. Ela espelha de propósito a cintura do modelo, para que ambas se leiam igual nos pontos de chamada:
packages/contracts/src/result.ts — o irmão leve
type Result<T, E = Error> = | { readonly ok: true; readonly value: T } | { readonly ok: false; readonly error: E }; // helpers: ok(v), err(e), isOk, isErr, mapResult, tryCatch, tryCatchAsync
ok primeiro), payloads diferentes para trabalhos diferentes.Este é o Result<T, Error> que você viu sendo devolvido por todo subsistema @alembic/hermes nas lições 7–13 — load(), transcribe(), webSearch() todos devolvem ele. A regra do repositório é explícita: "código de biblioteca devolve Result em vez de lançar" (CLAUDE.md). Duas uniões, uma filosofia: falibilidade é um valor, validado no boundary, nunca uma surpresa.
tryCatch que o código "que lança" entra no mundo "que devolve valor".Pense em ModelRunResult como a "nota fiscal completa" de uma chamada de modelo: além de deu-certo/deu-errado, ela traz quanto custou, quanto demorou, qual modelo respondeu. Já o Result é o "recibo simples": só diz se deu certo (e o que veio) ou se deu errado (e qual o erro). Você usa o recibo simples para abrir um arquivo; usa a nota completa para falar com um modelo.
São deliberadamente não o mesmo tipo. ModelRunResult carrega o domínio da chamada de modelo (adapterId, modelId, durationMs, usage, costUsd, raw, e um error com retryable). Result<T, E> é genérico e mínimo: value: T ou error: E, mais helpers (ok, err, isOk, isErr, mapResult, tryCatch, tryCatchAsync). Compartilham o discriminante ok só para que if (r.ok) funcione igual nos dois — reduzindo a carga cognitiva de quem lê o código.
ok permite ao compilador?if (r.ok) ele libera o ramo de sucesso (text/value); fora, o ramo de erro. Ler o campo errado vira erro de compilação.retryable, e por quê?try; o prejuízo de o "nunca lança" quebrar é catastrófico. Protege o invariante contra um refactor futuro.input malformado?try. runWithGuards devolve um Failure e o attempt nunca roda.Result agradável de usarok(v) e err(e) constroem cada ramo; isOk/isErr são type-guards; mapResult transforma o valor de sucesso preservando o erro; tryCatch/tryCatchAsync embrulham código que lança (uma lib de terceiros, por exemplo) e o convertem para a fronteira de Result. É a ponte por onde o mundo "que lança" entra no mundo "que devolve valor".
Seria possível espremer chamadas de modelo dentro de Result<ModelText, ModelError>, mas você perderia a forma fixa e auto-documentada da chamada de modelo (telemetria + repetibilidade sempre presentes) e teria que reinventá-la em cada ponto. Dois tipos, com o mesmo discriminante, dão o melhor dos dois mundos: leitura uniforme, cargas sob medida.
ModelRunFailure totalmente tipado, com code, message e retryable; nada fica escondido. O que se elimina é o salto invisível de controle de fluxo, não a informação.ModelRunResult é o contrato rico de chamada de modelo (usage, custo, duração, id do adapter); Result<T, E> é o contrato mínimo de operação falível. Elas compartilham o discriminante ok de propósito, para que os pontos de chamada se leiam igual, mas carregam payloads diferentes para trabalhos diferentes.retryable é só para log." Não — ele dirige a camada ③. guardedAttempt devolve retry: true exatamente quando result.error.retryable, e withRetry faz backoff a partir disso. É um campo de comportamento, não cosmético.| O mito | A realidade no código |
|---|---|
| "throw é mais simples" | throw é invisível no tipo; a união força o tratamento do ok:false em tempo de compilação |
| "o catch externo é morto" | é inalcançável hoje, mas é a rede que protege o invariante de um refactor futuro |
| "repetir sempre ajuda" | só retryable: true (429/5xx) repete; 400/401 não — repetir desperdiça |
| "validar input é do adapter" | a porta Zod (①) valida antes; o adapter só escreve o attempt |
Cinco cartas, a lição inteira. Use as setas do teclado ou os botões.
ModelAdapter.run — uma porta só.ok; falha é valor, não exceção.runWithGuards embrulha o attempt de cada adapter com quatro guardas em ordem.ModelRunFailure tipado (failureFromThrown).Result<T, E> estende a mesma filosofia ao resto da base — leitura uniforme em todo canto.Três perguntas. A pontuação corre conforme você responde.
attempt() interno de um adapter dispara um TypeError cru lá no fundo do corpo. O que o chamador de run() observa?catch externo (logicamente inalcançável) da camada ④ o converteria. A união é total — run() resolve para um ModelRunResult, nunca rejeita.ModelRunFailure carrega um retryable: boolean em vez de deixar o chamador decidir?guardedAttempt devolve retry: true exatamente quando result.error.retryable, e withRetry faz backoff a partir desse flag. Classificar a repetibilidade na fonte mantém a política uniforme e evita drift entre pontos de chamada.try/catch externo do runWithGuards é descrito como "rede de segurança final". Por que manter código logicamente inalcançável?withRetry só rejeita se guardedAttempt rejeitar, o que nunca acontece — então hoje o catch externo não dispara. Fica porque o custo é um try e o prejuízo de o invariante quebrar é catastrófico. O comentário no código diz exatamente isso.