Lição 02 · Fundamentos · Alembic × Hermes v3 · @alembic/contracts
Alembic × Hermes — O Curso de Fusão v3 · Visual Course

@alembic/contracts — a língua que todos os 28 falam

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.

Leia primeiro (fonte primária)
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.

Você vai conseguir
  • Ler e devolver Result<T, E> sem try/catch no chamador.
  • Classificar trabalho na escada T0→T4 (e saber por que o default é T4).
  • Explicar o leilão pickCheapestForTier e o tie-break por ordem de declaração.
  • Validar qualquer chamada de modelo com modelRunInputSchema.
Leia a versão simples, ou abra a camada técnica em qualquer seção.
1

A grande ideia


@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”.

Por baixo do capô

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.

2

Em uma imagem


mundo externo JSON, stdout, resposta LLM FRONTEIRA (contracts) schema.safeParse(dado) Zod valida ANTES de entrar ok(valor tipado) segue para o motor err(erro tipado) para AQUI, fail-closed nada não-validado atravessa; nada explode do outro lado
Leia da esquerda → direita: o dado bruto (ex.: a resposta JSON de um modelo T1) só entra no motor depois do safeParse — e a falha vira valor, não exceção.
3

Result fail-closed


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.

fusao · 05-ports-and-injection · s0
packages/contracts/src/result.ts:10-26 (trecho real, não editado)
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 });
O kit completo (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 ResulttryCatchAsync “never rejects”. toError (linha 72) normaliza qualquer coisa lançada em Error.
Preveja antes de ver
Uma chamada de modelo cai em Result ou em outro tipo? Pense, depois revele.
Outro tipo: chamadas de modelo usam 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).
4

A escada de tiers (T0→T4 + LOCAL)


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.

fusao · 27-tiers-cost-and-budget · s0
TierSignificado (tier.ts:4-14)Autônomo?
T0silencioso / totalmente autônomo, sem humano no loopreservado ao caminho silencioso
T1autônomo com logging levesim
T2autônomo, um revisor notificadosim
T3autônomo, revisão de council exigidasim
T4PARK — retido; exige council + humanonão (estacionado)
O default é desconfiar
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.
5

MODEL_REGISTRY & o empate T2 real


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.

LEILÃO pickCheapestForTier(T2) · custo = in + out por 1k tokens gemini-3.5-flash 0.0001 + 0.0004 = 0.0005 deepseek-v4-pro 0.0001 + 0.0004 = 0.0005 glm-5.2 0.0002 + 0.0006 = 0.0008 reduce com < ESTRITO empate (0.0005 = 0.0005)? mantém o 1º declarado vencedor do T2 = a ordem de declaração Caso real (PR #157): deepseek-v4-pro dava AccessDenied no gateway → a correção foi DECLARAR gemini-3.5-flash antes dele.
O empate T2 real: dois modelos a 0.0005 combinado (registry.ts:154-155 e 179-180). O tie-break é a ordem de declaração — e virou ferramenta de operação no PR #157.
Por que isso importou: o run histórico de learnings (lição 05) precisava do T2. O leilão resolvia para 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.
Dois modelos T2 empatam no custo combinado por 1k tokens. Quem pickCheapestForTier escolhe?
O 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.
6

No código: a cintura em schema


Todo pedido de modelo no motor tem exatamente esta forma — é o modelRunInputSchema, validado na fronteira por todo adapter (lição 04):

packages/contracts/src/model.ts:30-40 (trecho real, não editado)
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.

Acesse você mesmo

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.

Camada técnica — comandos desta seção
# 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
7

Experimente


Recuperação ativa — vire as cartas; depois rode o doctor de verdade.

result
Qual campo discrimina Ok de Err?
clique para virar
O booleano ok (result.ts:10-20). Mesmo discriminante do ModelRunResult — os dois estilos leem igual no call site.
tier
Trabalho sem classificação cai em qual tier?
clique para virar
T4DEFAULT_TIER = Tier.T4 (tier.ts:51): nasce estacionado, exige council + humano. O default do motor é desconfiar.
registry
O que custa 0.0005 nesta lição?
clique para virar
O custo combinado (in+out por 1k tokens) de gemini-3.5-flash E de deepseek-v4-pro — o empate T2 que o PR #157 resolveu pela ordem de declaração.
schema
Por que signal fica fora do schema?
clique para virar
AbortSignal é objeto de runtime do host — Zod não o valida de forma significativa (model.ts:26-28). Ele viaja ao lado dos campos validados.
Pergunta para levar: “se eu adicionasse um modelo novo no registry, em que ordem eu o declararia — e que empate eu criaria sem querer?” Teaser da lição 03: a primeira consumidora desta língua é a ETL — um pipeline T0 que processa 100% do corpus a custo zero e onde um prefixo de pasta errado silencia tudo.