run() que nunca lançaToda invocação de modelo no Alembic — todo adapter, todo membro de council, todo worker do swarm, toda camada do funil — flui por uma única forma de função: uma chamada assíncrona que nunca lança exceção e retorna um valor discriminado por ok. É o invariante mais importante do código. O pacote de fusão que você conheceu nas lições 7–13 se conecta a esta cintura; entendê-la é entender por que uma rede instável ou um 429 não derruba uma run. Fonte: packages/contracts/src/model.ts + packages/adapters/src/adapter-core.ts, governado pela ADR-0009.
O contrato é uma união discriminada por um campo literal ok. Não há throw no tipo — a falha é um valor, não um evento de fluxo de controle:
// packages/contracts/src/model.ts — os dois braços (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]);
Quem chama escreve if (result.ok) e o TypeScript estreita o tipo. Crucialmente, o braço de falha carrega um booleano retryable — o ponto de chamada não precisa adivinhar se um 429 vale uma nova tentativa; o resultado já diz. A própria interface documenta a lei: run(input): Promise<ModelRunResult> // NEVER throws (invariant).
Um comentário dizendo "nunca lança" é inútil se um adapter esquecer. Então o invariante é estrutural: todo adapter implementa apenas um attempt() interno, e uma única espinha compartilhada — runWithGuards — o envolve com quatro camadas, em ordem:
// packages/adapters/src/adapter-core.ts:118-147 — a espinha canônica (condensado) export const runWithGuards = async (adapterId, attempt, input, runtime = {}) => { const validation = validate(adapterId, input); // ① Zod na fronteira 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 nunca acontece; // esta é uma rede de segurança final para preservar o invariante. return failureFromThrown({ adapterId, input, durationMs: 0 }, cause); // ④ } };
A camada ② é onde o circuit-breaker aprende: guardedAttempt chama breaker?.recordSuccess() num resultado ok e breaker?.recordFailure() caso contrário, e reporta retry: true apenas quando result.error.retryable (adapter-core.ts:102-110). O catch externo em ④ é logicamente inalcançável — existe apenas para que a promessa em nível de tipo ("nunca lança") se sustente mesmo se uma refatoração futura quebrar uma suposição interna. Isso é defesa em profundidade na costura mais crítica do sistema.
run(): Promise<X> o que ela pode lançar, então quem chama ou super-captura ou esquece. Uma união discriminada torna todo caminho de falha visível e exaustivo: o compilador força você a tratar ok: false. Num sistema que distribui um pedido a 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 a run".Uma segunda união discriminada, mais leve — também por ok, com braços value/error — existe para trabalho falível não-modelo: IO de arquivo, parsing, wrapper de subprocesso. Ela espelha deliberadamente a cintura de modelo, então ambas se leem 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
Este é o Result<T, Error> que você viu retornado por todo subsistema do @alembic/hermes nas lições 7–13 — load(), transcribe(), webSearch() todos o retornam. A regra do repositório é explícita: "código de biblioteca retorna Result em vez de lançar" (CLAUDE.md). Duas uniões, uma filosofia: falibilidade é um valor, validado na fronteira, nunca uma surpresa.
attempt() interno de um adapter lança um TypeError cru no fundo do corpo. O que quem chamou run() observa?catch externo logicamente inalcançável em ④ o converteria. A união é total — run() resolve para um ModelRunResult, nunca rejeita.ModelRunFailure carrega um retryable: boolean em vez de deixar quem chama decidir?guardedAttempt retorna retry: true exatamente quando result.error.retryable vale, e withRetry faz backoff por essa flag. Classificar a retryability na origem mantém a política uniforme e evita divergência entre pontos de chamada.try/catch externo em runWithGuards é descrito como "uma 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. Ele fica porque o custo é um try e o prejuízo de quebrar o invariante é catastrófico. O comentário no código diz exatamente isso.ModelRunFailure totalmente tipado com code, message e flag retryable; nada fica oculto. O que se elimina é o salto invisível de fluxo de controle, 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. Compartilham o discriminante ok de propósito, para que os pontos de chamada se leiam igual, mas carregam payloads diferentes para trabalhos diferentes.