Ao fim desta lição você sabe por que Result nunca lança, o que cada tier T0–T4 autoriza, como o MODEL_REGISTRY leiloa o modelo mais barato — e como um empate de US$ 0,0005 quase travou o primeiro run real de learnings.
packages/contracts/src/ — 11 arquivos, zero dependências além de Zod
result.ts, tier.ts, model.ts e registry.ts são a fronteira pública do motor: esta lição destila os quatro. Regra da casa: schema exportado não muda sem teste + caminho de retrocompatibilidade.
Result<T, E> sem try/catch no chamador.pickCheapestForTier e o tie-break por ordem de declaração.modelRunInputSchema.@alembic/contracts é o único pacote de que todos os outros dependem — e ele não faz nada. Não lê arquivo, não chama modelo, não tem IO. Ele apenas define a língua: os tipos, os schemas Zod e as constantes que os outros 27 workspaces usam para conversar sem se machucar.
Três ideias moram aqui. Result fail-closed: toda operação falível devolve ok(valor) ou err(erro) — a falha é um valor de primeira classe, nunca uma exceção voadora. Tiers de autonomia: cada trabalho carrega uma etiqueta T0–T4 que diz quanta supervisão humana ele exige. Registry de modelos: um catálogo com preço por 1k tokens que permite leiloar “o mais barato do tier” sem hardcode de modelo em lugar nenhum.
Pense como… a ABNT de um canteiro de obras: ela não ergue parede, mas define a bitola do parafuso que TODO fornecedor entrega. Onde a analogia quebra: norma de papel pode ser ignorada; aqui o typecheck e o Zod recusam a peça fora de medida na fronteira — não existe “passou batido”.
O pacote tem 11 arquivos-fonte (ls packages/contracts/src/): result.ts, tier.ts, model.ts, registry.ts, domain.ts, wiki.ts, theme.ts, course.ts, session.ts, otel.ts e index.ts. Dois estilos de falha convivem por design: Result<T, E> para operações falíveis comuns (IO, parse, subprocesso) e o ModelRunResult — união discriminada em ok — para chamadas de modelo (result.ts:2-8 documenta a divisão). Os dois leem igual no call site: if (!r.ok) ….
Convenção do repo (CLAUDE.md): “Public contracts: @alembic/contracts owns schemas and shared types. Do not change exported schemas without updating tests and backwards-compat path.” É o que deixa 28 workspaces evoluírem sem quebrar uns aos outros.
Uma exceção é uma falha invisível na assinatura: quem chama não sabe que ela existe até ela estourar em produção. Result inverte isso — a assinatura Result<T, Error> avisa: “posso falhar, e você VAI decidir o que fazer”. O compilador cobra o branch.
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>; /** Construct a success result. */ export const ok = <T>(value: T): Ok<T> => ({ ok: true, value }); /** Construct a failure result. */ export const err = <E>(error: E): Err<E> => ({ ok: false, error });
result.ts): isOk/isErr (type guards, linhas 29-34), mapResult (linha 37), e as pontes com o mundo que lança: tryCatch (linha 46) e tryCatchAsync (linha 60) embrulham funções que podem lançar e devolvem Result — tryCatchAsync “never rejects”. toError (linha 72) normaliza qualquer coisa lançada em Error.Result ou em outro tipo? Pense, depois revele.
ModelRunResult (união discriminada em ok, model.ts:109-112) — mais rico (usage, custo, duração, erro com retryable). O result.ts:2-8 documenta a divisão; os dois estilos leem igual: if (!r.ok).Tier não é “tamanho do modelo”: é quanta supervisão humana o trabalho exige. A escada sobe do silêncio total (T0) até o estacionamento obrigatório (T4). E há um marcador ortogonal, LOCAL, para trabalho que deve ficar em modelo local/$0 independentemente do tier.
| Tier | Significado (tier.ts:4-14) | Autônomo? |
|---|---|---|
| T0 | silencioso / totalmente autônomo, sem humano no loop | reservado ao caminho silencioso |
| T1 | autônomo com logging leve | sim |
| T2 | autônomo, um revisor notificado | sim |
| T3 | autônomo, revisão de council exigida | sim |
| T4 | PARK — retido; exige council + humano | não (estacionado) |
DEFAULT_TIER = Tier.T4 (tier.ts:51): trabalho não classificado nasce estacionado, não executado. isAutonomous só libera T1–T3 (tier.ts:59-60); escalateTier sobe um degrau por vez e para no topo (tier.ts:68-72). Fail-closed até na dúvida de classificação.O MODEL_REGISTRY (registry.ts:57) é o catálogo: cada entrada tem modelId, adapterId, tier e preço por 1k tokens (input/output). Quem precisa de um modelo não escolhe pelo nome — pede “o mais barato do tier X” a pickCheapestForTier. O default do motor é DEFAULT_MODEL_ID = 'glm-5.2' (registry.ts:233).
O leilão soma input+output e usa < estrito: um candidato só destrona o líder se for estritamente mais barato. Consequência silenciosa: no empate, vence quem foi declarado primeiro no objeto. Isso é comportamento documentado (registry.ts:82-84: “pickCheapestForTier keeps the FIRST entry on cost ties (strict <)”), não acidente.
deepseek-v4-pro, mas a conta do gateway devolvia AccessDenied nele. Sem tie-break aleatório para “dar sorte”, a correção honesta do PR #157 foi mudar a ordem de declaração: gemini-3.5-flash declarado antes ⇒ o mesmo empate passa a resolver para um modelo que a conta realmente usa. Um caractere de ordem destravou a primeira safra de 1.022 learnings.pickCheapestForTier escolhe?reduce em registry.ts:252-256 só troca o líder quando entryCost < bestCost (estrito) — no empate, o acumulador (o declarado antes) permanece. Contexto e latência nem entram na conta; só preço por 1k e ordem. Foi exatamente isso que o PR #157 explorou de propósito.Todo pedido de modelo no motor tem exatamente esta forma — é o modelRunInputSchema, validado na fronteira por todo adapter (lição 04):
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(), metadata: z.record(z.string(), z.unknown()).optional(), roleId: z.string().min(1).optional(), temperature: z.number().min(0).max(2).optional(), timeoutMs: z.number().int().positive().optional(), });
Detalhe fino: o signal (AbortSignal) fica fora do schema de propósito — é objeto de runtime do host, não validável por Zod (model.ts:26-28); ele viaja ao lado dos campos validados no tipo ModelRunInput (model.ts:54-57). E a interface ModelAdapter (model.ts:140-151) carrega o invariante central do motor: “run NEVER throws” — qualquer erro vira ModelRunFailure com error.retryable tipado.
Abra packages/contracts/src/model.ts e leia o bloco 129-151: o comentário do invariante nunca-lança está colado na interface, não escondido em doc externo. Depois confira quem o cumpre: grep -rn "runWithGuards" packages/adapters/src/ | head — todo adapter constrói o run pela mesma espinha.
# a fronteira pública inteira em 11 arquivos ls packages/contracts/src/ # o invariante nunca-lança, direto da fonte sed -n '129,151p' packages/contracts/src/model.ts # o leilão + o tie-break documentado sed -n '244,257p' packages/contracts/src/registry.ts sed -n '74,96p' packages/contracts/src/registry.ts # prova viva do catálogo: valida forma + coerência dos adapters, offline $0 alembic doctor --client-stack
Recuperação ativa — vire as cartas; depois rode o doctor de verdade.
ok (result.ts:10-20). Mesmo discriminante do ModelRunResult — os dois estilos leem igual no call site.DEFAULT_TIER = Tier.T4 (tier.ts:51): nasce estacionado, exige council + humano. O default do motor é desconfiar.gemini-3.5-flash E de deepseek-v4-pro — o empate T2 que o PR #157 resolveu pela ordem de declaração.model.ts:26-28). Ele viaja ao lado dos campos validados.