Toda chamada de modelo do sistema — do CLI ao council — passa por UMA porta: ModelAdapter.run, que nunca lança. Em cima dela, @alembic/harness monta o funil noturno T0→T3 com as DUAS cadeias de valor do ADR-0002, cada uma com seu próprio orçamento fail-closed. Nesta lição: as guardas do adapter, a pista local $0, o accounting de custo e o funil que produziu 2.589 signals e 1.022 learnings.
O contrato mora em packages/contracts/src/model.ts: ModelAdapter.run aceita um ModelRunInput validado por Zod (modelRunInputSchema, model.ts:30–40) e resolve para um ModelRunResult — união discriminada em ok (model.ts:109–114). O INVARIANTE, em maiúsculas no próprio doc (model.ts:131–135): run NUNCA lança. Rede caiu, timeout, quota, bug do adapter — tudo vira ModelRunFailure com um ModelRunError tipado que inclui retryable (model.ts:83–89), para o harness decidir fallback sem parsear mensagem de erro.
Pense como… a alfândega de um porto: não importa de qual navio (OpenAI, Anthropic, local) a carga veio, ela só entra pelo MESMO guichê, com o MESMO formulário, e recebe carimbo de aprovada ou recusada — nunca "some" no meio. Onde quebra: alfândega atrasa; a cintura estreita adiciona só validação, não fila.
Nenhum adapter reimplementa robustez. Cada um escreve só o attempt (uma tentativa de rede/subprocesso) e o entrega a runWithGuards (packages/adapters/src/adapter-core.ts:118–147), que impõe, nesta ordem (doc em adapter-core.ts:26–31): ① validação de borda do input via Zod; ② o invariante nunca-lança (throw que escapar vira failureFromThrown); ③ o portão do CircuitBreaker com feedback de sucesso/falha; ④ retry/backoff via withRetry, dirigido pelo flag retryable de cada resultado.
Duas rotas de adapterId dominam o registry: cliproxyapi — o gateway default em http://127.0.0.1:8317 (cliproxyapi.ts:21), token via ALEMBIC_CLIPROXY_TOKEN com fallback CLIPROXYAPI_API_KEY (apps/cli/src/config.ts:31) — e local, a pista $0 OpenAI-compatível (local-openai.ts:19–20) onde vive o deepseek-v4-flash servido em ALEMBIC_LOCAL_URL (registry.ts:161–174): custo 0, tier T1, residente na máquina. É a pista local que torna o modo --offline e os 1.834 testes herméticos possíveis.
computeCostUsd (cost.ts:26–34) arredonda a 6 casas para o ledger não acumular ruído binário; costForModel devolve undefined para modelo desconhecido — o caller então omite costUsd "rather than reporting a misleading zero" (cost.ts:41–48). Não saber o custo é informação; $0 falso seria mentira.O harness monta o funil noturno em packages/harness/src/funnel.ts. Primeiro, collectResidue (funnel.ts:316–341) reaproveita o L0 da lição 01: caminha o corpus, aplica o prior da família e recolhe só o que roteia acima do T0 — cada item com hash, família, canal e a evidência real (o campo evidence da bridge line, funnel.ts:308–314). Depois os tiers pagos entram: T1 extrai um BusinessSignal por item (runT1Extraction, funnel.ts:435–464), T2 refina a shortlist em batch, T3 leva ao council. Desde o ADR-0002, esse mesmo residue alimenta duas cadeias independentes:
Detalhes que definem o caráter do funil: o check de orçamento acontece antes de cada chamada paga com uma estimativa grosseira de ~4 chars/token (estimateUsage, funnel.ts:467–470); item bloqueado vira contador budgetBlocked, nunca chamada silenciosa (funnel.ts:450–457); e o report só ganha a chave routes em runs de dois knobs — "a single-budget run omits routes entirely" (funnel.ts:920–922), então a run de um knob é byte-idêntica à de antes do ADR. Foi essa rota opt-in que, apontada no pai correto (lição 01), produziu os 1.022 learnings de Bookmarks em 06-07 — a primeira produção da segunda cadeia na história do motor.
O medidor vive no ETL (packages/etl/src/budget.ts) e é deliberadamente chato: check(estimate) projeta o custo da PRÓXIMA chamada contra o cap e, se estourar, devolve um BudgetExceeded tipado com spentUsd/projectedUsd/capUsd (budget.ts:43–58) — "it never silently proceeds and never throws" (budget.ts:33–36). record(spend) registra o gasto REAL depois. Exceção de projeto: tier T0 precifica $0 e nunca é bloqueado (budget.ts:96–101) — o substrato gratuito sempre roda, mesmo com cap 0.
O jeito mais rápido de saber qual run você tem na mão é procurar a chave routes no --json:
# run de 1 knob (--budget só): SEM a chave routes — byte-idêntica ao pré-ADR-0002 { "stats": { … }, "t1BudgetBlocked": 0, … } # run de 2 knobs (--budget + --learnings-budget): as duas rotas, medidas separadas { …, "routes": { "signals": { "budgetUsd": 1, "spentUsd": 0, "budgetBlocked": 0 }, "learnings": { "budgetUsd": 1, "spentUsd": 0, "budgetBlocked": 0 } } }
routes.signals.budgetBlocked totaliza os bloqueios T1+T2+T3-council da cadeia de negócio (funnel.ts:925–931); routes.learnings.budgetBlocked conta só os da rota de aprendizado. Um número alto num lado com o outro zerado é o sistema dizendo qual cap você subestimou.
BudgetGuard impede uma chamada paga do funil?runT1Extraction chama budget.check(estimateUsage(input)) antes de route.adapter.run (funnel.ts:450–458); bloqueio vira budgetBlocked++ e o adapter pago nem é invocado. record só contabiliza o custo real do que passou.distill SÓ com --budget (sem --learnings-budget). O que aparece no report?funnel.ts:920–922 — "a single-budget run omits routes entirely so the SERIALIZED report… is byte-identical". A cadeia de learnings é opt-in aditiva: sem o 2º knob ela nem existe no output. Com os dois knobs, routes.signals e routes.learnings aparecem com caps, gasto e bloqueios separados.Tudo abaixo é leitura ou caminho hermético $0:
# o invariante nunca-lança, na fonte grep -n "NEVER throws" packages/contracts/src/model.ts # as 4 guardas em ordem sed -n '23,36p' packages/adapters/src/adapter-core.ts # funil hermético com as DUAS cadeias (caps de exemplo; --offline = $0 real) alembic distill ~/Documents/Resources --offline --budget 1 --learnings-budget 1 --json # smoke offline do registry + adapters (sem rede) alembic doctor --client-stack # onde o gateway e o token são resolvidos grep -n "CLIPROXYAPI_DEFAULT_BASE_URL" packages/adapters/src/cliproxyapi.ts sed -n '24,31p' apps/cli/src/config.ts
Troubleshooting clássico: fetch failed = gateway 127.0.0.1:8317 fora do ar ou token ausente — use --offline enquanto isso.
ModelAdapter.run NUNCA lança — falha é valor (model.ts:131–135).runWithGuards — Zod → breaker → retry → rede final (adapter-core.ts:26–31).undefined, nunca $0 falso (cost.ts:41–48).funnel.ts:672–675).funnel.ts:450–457).retryable: false?" — Na próxima lição subimos um andar: o @alembic/hermes, onde memória multi-store, skills e o AI Employee compõem TUDO isso num turno honesto de agente.