Passo 4 · Módulo 2 · O loop · O loop · Saída estruturada & ferramentas
Curso de Harness Engineering · Visual Course

Saída estruturada & ferramentas

No nosso pedido atravessando o harness, depois que o adapter responde, esta lição mostra como o Alembic torna impossível um sucesso malformado — Zod nas duas pontas — e onde ele honestamente para: não há laço que conserte e re-pergunte. Você vai saber o que está garantido e o que ainda é roadmap.

Leia primeiro (fonte primária)
HARNESS-MAP.md (Parte II, princípios #9 e #10) + packages/adapters/src/openai-compatible.ts

Esta lição destila o princípio "structured-output failures + schema validation + repair" e o "function-calling reliability + tool contracts + idempotency" do mapa, mostrando linha a linha onde o Alembic os cumpre e onde os deixa em aberto. Referência didática paralela: o currículo PT-BR da WalkingLabs.

Leia a versão simples, ou abra a camada técnica em qualquer seção.
1

A grande ideia


Um modelo de linguagem devolve texto. Texto pode estar errado, truncado, embrulhado em prosa ou em cercas de markdown. Saída estruturada é a disciplina de transformar esse texto frouxo num objeto tipado e validado antes que qualquer outra parte do sistema confie nele. No harness do Alembic isso acontece numa fronteira (boundary) específica, com Zod, e — esta é a parte honesta — com um limite claro do que ele faz e do que não faz.

Ao fim desta lição você saberá
  • Por que o Alembic valida a entrada e revalida o resultado de sucesso — tornando um "sucesso malformado" estruturalmente impossível.
  • Onde está o GAP honesto: não existe laço de auto-repair / re-pergunta — PARSE_ERROR é não-retentável.
  • O que looseJsonParse realmente faz (fatia + descarta), e por que isso não é "reparar".
  • Como o Alembic resolve idempotência: não com chaves por chamada de ferramenta, mas no nível run/journal (runId endereçado por conteúdo, rename atômico).
  • Por que a superfície MCP é read-only por design — e por que essa assimetria é a propriedade de segurança.

Pense no caminho do nosso exemplo recorrente: um pedido entra, o tier routing escolhe o modelo, o adapter chama o modelo e devolve um Result. A lição passada (0003) cuidou de tudo que está abaixo da cintura (KV cache, quantização — delegado). Esta lição cuida do instante seguinte: o que chega de volta é confiável?

Pense como… a inspeção de qualidade na entrada de uma fábrica. Toda peça que chega é medida contra um gabarito (o schema). Peça fora do gabarito não entra na linha — vira refugo registrado, não uma peça "quase boa" que segue adiante e quebra a montagem três etapas depois. Onde a analogia quebra: uma fábrica de verdade às vezes retrabalha a peça (lixa, ajusta) e a reaproveita. O Alembic, hoje, não retrabalha a saída do modelo — ele a descarta e, se for o caso, refaz a chamada inteira. Esse "não retrabalhar" é exatamente o GAP que esta lição ensina.

Por baixo do capô

A validação no Alembic é pervasiva e baseada em Zod. Toda entrada de adapter passa por modelRunInputSchema.safeParse na fronteira do guard (vira um CLIENT_ERROR tipado, nunca um throw). Blocos de uso (tokens) do provedor são coagidos via safeParse e devolvem undefined — nunca fabricam contagem — quando a forma vem errada. Votos de council validam contra councilVoteSchema; abaixo do quórum vira NO_GO. O pack de 8 camadas e cada gate validam via Zod e devolvem Result.

O status no HARNESS-MAP é owned para validação de schema — é o estilo da casa (@alembic/contracts é dono dos schemas). E é gap para o laço de auto-reparo/re-pergunta: saída ruim → falha tipada → no máximo refaz a chamada inteira (se retryable), nunca "re-pergunta com o erro de parse anexado".

1 · validar ✓ o Alembic tem (owned) 2 · reparar ✗ não tem (gap) 3 · re-perguntar ✗ não tem (gap)
As três capacidades do livro-texto. O Alembic implementa a primeira (verde, contínuo) e deixa as outras duas como roadmap (vermelho, tracejado).
Guarde isto Saída estruturada ≠ só "pedir JSON ao modelo". É validar na fronteira + decidir o que fazer quando a validação falha. O Alembic é forte na primeira metade e honesto sobre não ter a segunda (reparar).
A clear instructional diagram of a quality-inspection gate at a factory entrance, read left to right. On the left, three incoming parts arrive on a conveyor: one part a clean match
A clear instructional diagram of a quality-inspection gate at a factory entrance, read left to right. On the l
2

Em uma imagem


O boundary tem quatro estágios. A entrada é construída tipada; a resposta HTTP é validada (safeParse); o sucesso é reconstruído e revalidado (parse). Repare na seta vermelha tracejada que não existe: o caminho de volta "conserta e re-pergunta". Esse vão é o GAP.

ModelRunInput construído tipado fetch /v1/chat dispatch (gateway) safeParse(resp) valida a resposta parse(success) revalida o sucesso ok: success err: PARSE_ERROR não-retentável repair / re-ask — NÃO EXISTE (gap)
Da esquerda → direita: o pedido vira ModelRunInput tipado, é despachado, a resposta é validada com safeParse e o sucesso é revalidado com parse. Em verde: o caminho feliz. Em vermelho cheio: a falha tipada PARSE_ERROR. Em vermelho tracejado: o laço de reparo que o Alembic não tem.
Leia o diagrama assim: há duas validações no caminho de ida (resposta e sucesso) e zero no caminho de volta. Um harness "auto-aprendiz" fecharia aquele tracejado; o Alembic, hoje, é um harness confiável que prefere falhar limpo a remendar.
3

Validação dos dois lados


A sutileza que separa um harness sério de um script é: o Alembic valida duas vezes, e com severidades diferentes de propósito.

Lado 1 · A resposta do modelo (safeParse)

A resposta crua do gateway é dado externo não confiável. Ela passa por openAiChatResponseSchema.safeParse. Se a forma vier errada, vira um err(PARSE_ERROR) tipado. Nunca explode. É esperado que um servidor possa devolver lixo — então a falha é de dados, recuperável pela lógica de cima.

safeParse data (ok) err typed
Lado 2 · O resultado de sucesso (parse)

Quando já se sabe que deu certo, o Alembic reconstrói o objeto de sucesso e o valida com modelRunSuccessSchema.parse — a versão que lança. Por quê? Porque um sucesso malformado aqui seria um bug do próprio Alembic, não do modelo. Falhar alto expõe o bug em vez de propagá-lo.

parse objeto throw = bug
A distinção precisa safeParse devolve {success, data|error} e nunca lança — é para dado que pode vir errado (a resposta da rede). parse lança em forma inválida — é para invariantes que nunca deveriam falhar (o objeto que nós mesmos montamos). Usar a ferramenta certa em cada ponto é a engenharia.
Faça uma aposta antes de revelar

O gateway respondeu 200 OK, mas o corpo é {"choices": []} (uma lista de escolhas vazia). O que o Alembic faz?

Falha tipada, não sucesso. O schema exige choices: z.array(...).min(1) — uma lista vazia não passa no safeParse, então vira err(PARSE_ERROR, "malformed response…"). E mesmo que passasse, extractText não acharia conteúdo e devolveria outro PARSE_ERROR ("response had no assistant content"). Dois guardas independentes garantem que um "200 vazio" nunca vira um sucesso silencioso de texto vazio que envenena o resto do run.

Por que isto importa para o exemplo recorrente

No caminho pedido → tier → adapter → gates, os gates (Proof, verifier) confiam que um Result.ok === true carrega um objeto bem-formado. Essa confiança só é legítima porque a fronteira do adapter já provou a forma. Se a validação fosse frouxa, o erro vazaria para frente e estouraria num lugar distante e difícil de depurar — exatamente o modo de falha "malformed JSON" que o HARNESS-MAP lista como produção-realista (princípio #22).

Recapitulando a lição 0002: a cintura estreita é o ModelAdapter + Result<T,Error> que nunca lança. Esta lição é o que acontece dentro dessa cintura quando a resposta chega: a validação que faz o Result ser honesto.
A side-by-side teaching comparison of two inspectors checking incoming objects, drawn as two clearly separated panels. Left panel: an inspector wearing soft gloves gently examining
A side-by-side teaching comparison of two inspectors checking incoming objects, drawn as two clearly separated
4

O GAP do auto-repair


Aqui é onde a honestidade do curso ganha valor. O livro-texto de "structured output" tem três passos: validar → reparar → re-perguntar. O Alembic faz o primeiro com rigor. Os outros dois não existem como laço automático. Vamos ver exatamente o que existe e o que não existe.

O gap, sem rodeio Não há nenhum laço de auto-reparo / re-pergunta para a saída do modelo no Alembic. Saída inválida → err(PARSE_ERROR), que é não-retentável → no máximo a chamada inteira é refeita (se o erro for retryable por outro motivo, como rede). Nunca "re-pergunte ao modelo com o erro de parse anexado para ele se corrigir".

O único "remendo" que existe — e por que não é reparo

um lugar no repo que tenta recuperar JSON de uma resposta embrulhada: looseJsonParse, no funil de destilação. Mas leia o que ele realmente faz:

packages/harness/src/funnel.ts:204
// Tenta um parse direto; depois a primeira fatia balanceada {...}.
export const looseJsonParse = (text: string): unknown => {
  const tryParse = (s: string): unknown | undefined => {
    try { return JSON.parse(s) as unknown; }
    catch { return undefined; }
  };
  const direct = tryParse(text.trim());
  if (direct !== undefined) return direct;
  const start = text.indexOf('{');
  const end = text.lastIndexOf('}');
  if (start >= 0 && end > start) return tryParse(text.slice(start, end + 1));
  return undefined;  // nada recuperável → o sinal é DESCARTADO acima
};

É exatamente fatia + descarta: tenta o texto inteiro; se falhar, recorta do primeiro { ao último } e tenta de novo; se ainda falhar, devolve undefined. E quem chama (signalFromResult) trata undefined descartando o sinal — não re-perguntando, não consertando vírgula, não fechando chave.

texto do modelo (pode ter prosa) JSON.parse direto deu? → retorna falhou fatia { … } e tenta deu? → retorna falhou undefined → sinal DESCARTADO sem reparo, sem re-pergunta caminho feliz: objeto recuperado segue para safeParse de schema
Duas tentativas de JSON.parse (direto, depois fatiado) e — se as duas falham — o descarte. Em nenhum ramo há conserto de sintaxe ou re-pergunta ao modelo.
O livro-texto fazO Alembic faz
Valida contra schemaSim — Zod nas duas pontas (owned)
Repara JSON (fecha chaves, conserta aspas)Não — só fatia {} e tenta um JSON.parse
Re-pergunta ao modelo com o erro anexadoNão existePARSE_ERROR não-retentável (gap)
Cadeia de fallback de modelo na falha de parseNão — falha tipada; quem chama decide descartar

O custo de um laço de repair

Um laço de re-pergunta parece grátis, mas: (1) gasta tokens a cada tentativa — sem um teto, vira um sorvedouro de custo; (2) é não-determinístico — a "correção" do modelo é mais uma amostra estocástica, então o resultado deixa de ser reproduzível, o que colide com a regra de determinismo da VM do Alembic (lição 0005); (3) mascara regressões — se um modelo passou a devolver lixo, falhar limpo te avisa; remendar esconde. O Alembic prefere a falha honesta + a decisão explícita de quem chama (descartar o sinal, escalar o tier) a um remendo opaco. O gap é real, mas a postura é coerente com "shipping como infraestrutura confiável" (princípio #23).

Como fechá-lo, se um dia for preciso

Um repair seria aditivo: na falha de safeParse, montar um novo ModelRunInput com o texto original + o parsed.error.message + a instrução "devolva apenas JSON válido", com um teto de tentativas (ex.: 1) e contabilizado pelo BudgetGuard. Caberia atrás de uma flag, sem mudar o contrato never-throws.

Padrão reutilizável Quando você descrever um sistema, separe sempre "valida" de "repara" de "re-pergunta". São três capacidades distintas. Dizer "temos validação de saída estruturada" sem essa distinção é o tipo de afirmação vaga que o HARNESS-MAP existe para desfazer.
An instructional illustration contrasting three distinct actions on a malformed document, arranged as three labelled columns left to right with clear empty header space above each.
An instructional illustration contrasting three distinct actions on a malformed document, arranged as three la
5

Contratos de ferramenta & MCP read-only


Ferramentas (tools) são como um agente age no mundo. Dois riscos clássicos: o modelo alucina uma chamada de ferramenta, ou duplica uma. A defesa do livro é "contratos tipados + idempotência". O Alembic faz algo mais radical na sua superfície mais exposta — o servidor MCP: ele simplesmente não tem ferramentas de escrita.

packages/harness/src/mcp.ts:18 (o comentário que É a decisão de arquitetura)
/**
 * A superfície de ferramentas MCP é estritamente READ-ONLY.
 * Não há, de propósito, nenhuma tool `start`/`fanout` aqui: um cliente MCP
 * pode INSPECIONAR um run (status, swimlanes, o ledger de parked) mas nunca
 * pode causar execução autônoma por esta superfície. Essa assimetria É a
 * propriedade de segurança — o transporte menos confiável (um host MCP
 * arbitrário) recebe a MENOR autoridade. Cada handler valida sua entrada
 * com Zod e devolve um ToolResult; nenhum lança.
 */

As três ferramentas expostas são harness_status, harness_events e harness_lane — todas leitura pura. Cada uma valida seus argumentos via Zod (invokeTool faz safeParse antes de despachar), então uma chamada MCP malformada vira um erro tipado, não uma exceção.

tools/call args não confiáveis safeParse(args) Zod no boundary handler READ puro · nunca lança ToolResult args inválidos → isError tipado (não throw)
Toda chamada MCP é validada por Zod antes do dispatch; mesmo o caminho de erro devolve um isError estruturado, jamais uma exceção.

A assimetria de capacidade, em uma imagem

FORA · host MCP (menos confiável) harness_status / _events / _lane context_pack / artifact_read só LEITURA — não causa execução fronteira read DENTRO · orquestrador (confiável) start / fanout / escrita autoridade de execução vive só aqui sem write ⟶
A autoridade de escrita/execução nunca cruza a fronteira para o lado do host MCP. Como não há tool de escrita exposta, não há chamada de escrita para o modelo alucinar ou duplicar — idempotência por construção.
O insight contra-intuitivo: a maneira mais segura de tornar tool calls idempotentes não foi adicionar chaves de idempotência — foi não expor as ferramentas perigosas. A ausência da capacidade é a propriedade de segurança. Um segundo nível aditivo (context_pack, artifact_read) também é leitura pura, e o artifact_read ainda confina a leitura sob o diretório do run com um guard anti-escape de caminho.
Guarde isto "Tool contract" no Alembic = TaskSpec tipado por dentro + superfície MCP read-only por fora. O modelo de confiança é explícito: quanto menos confiável o transporte, menos autoridade ele recebe.
6

Idempotência — no nível run/journal


Idempotente = repetir a operação dá o mesmo resultado, sem efeito duplicado. O livro-texto coloca isso por chamada de ferramenta (uma chave de idempotência por requisição). O Alembic resolve num lugar diferente e, para o modelo dele, melhor: no nível do run e do journal.

runId endereçado por conteúdo

O id do run é o hash do seu spec imutável. Re-submeter um run idêntico cai no mesmo diretório on-disk — não bifurca uma árvore nova. "Filesystem como verdade."

packages/swarm/src/ids.ts:30
export const runIdFor = (spec: unknown): string =>
  `run-${shortHash(spec)}`;
commit por rename atômico

O worker escreve num arquivo in-progress, depois faz rename para o nome terminal. O orquestrador só observa o nome terminal — então nunca vê um relatório meio-escrito. O rename é o commit.

packages/swarm/src/worker.ts:185
await writeFile(tmp, renderReport(report));
await rename(tmp, final); // atômico = commit

Junte as duas peças e você tem replay seguro: re-rodar um run idêntico reusa o diretório endereçado por conteúdo + o journal append-only + o cache de resultado. A segurança de repetição vem da arquitetura (conteúdo-endereçado + rename atômico + journal), não de tokens de idempotência por ferramenta.

runId = hash(spec) ✓ submissão A submissão A' submissão A'' run-9af2 1 diretório runId = randomUUID() ✗ submissão A submissão A' submissão A'' run-1c… run-7e… run-b3… 3 diretórios → resume impossível
À esquerda, o runIdFor real: três submissões idênticas colapsam num único diretório (resume funciona). À direita, o contrafactual com id aleatório: cada submissão bifurca, e não há o que reusar.
t1.report.md in-progress · NÃO observado rename() t1.complete.md terminal · observado = commit orquestrador vê ✓ tarefa concluída crash aqui → nada confundido com sucesso
O estado meio-feito tem um nome que o orquestrador ignora; só o nome terminal (criado pelo rename atômico) conta como concluído.
Exemplo guiado · por que o rename atômico salva o replay
1
O worker começa a tarefa t1 e escreve t1.report.md (in-progress).
2
O processo morre no meio da escrita (crash, kill, OOM). Sobra um .report.md parcial — mas o orquestrador não vigia esse nome.
3
No --resume, collectReport procura só por t1.complete.md/t1.failed.md (nomes terminais). Não acha → trata t1 como não feita.
4
A tarefa roda de novo, escreve o tmp e faz rename para o nome terminal. Agora sim o orquestrador a vê como concluída. Nenhum estado meio-feito foi confundido com sucesso.
5
Agora você: imagine que o id do run fosse aleatório (randomUUID) em vez de hash do spec. O que o --resume reusaria? Nada — cada submissão criaria uma árvore nova. É o runIdFor endereçado por conteúdo que torna o resume possível.
Nuance honesta Para operações de escrita internas não há um mecanismo geral de chave de idempotência — comandos de proof são re-executados por inteiro no --resume, contando com o cache + o journal append-only, não com tokens por ferramenta. Isso é adequado ao modelo de DAG limitado; importaria se ferramentas de escrita fossem expostas a um chamador não confiável (mas, como vimos, elas não são).
A teaching diagram of an atomic file-rename commit, read left to right in three stages with space for captions. Stage one: a document being written, shown as half-filled or under c
A teaching diagram of an atomic file-rename commit, read left to right in three stages with space for captions
7

No código


Aqui está o coração da lição: o caminho de sucesso do transporte OpenAI-compatível. Repare nas duas validações (a safeParse da resposta na linha 158, o parse do sucesso na linha 179) e em como cada falha vira um Result, nunca um throw.

packages/adapters/src/openai-compatible.ts:158
// 1) A resposta crua do gateway é dado externo -> safeParse (nunca lança).
const parsed = openAiChatResponseSchema.safeParse(response.value.body);
if (!parsed.success) {
  return makeFailure(
    { ...ctx, durationMs, raw: response.value.body },
    makeError(ErrorCode.PARSE_ERROR, `malformed response: ${parsed.error.message}`),
  );
}

const text = extractText(parsed.data);
if (text === undefined) {       // 200, mas sem conteúdo de assistant
  return makeFailure(
    { ...ctx, durationMs, raw: response.value.body },
    makeError(ErrorCode.PARSE_ERROR, 'response had no assistant content'),
  );
}

// 2) O sucesso é NOSSO objeto -> parse (LANÇA se malformado = bug do Alembic).
return modelRunSuccessSchema.parse({
  ok: true,
  adapterId: config.adapterId,
  durationMs: Math.max(0, durationMs),
  modelId: input.modelId,
  text,
  raw: response.value.body,
  ...(usage ? { usage } : {}),
  ...(costUsd === undefined ? {} : { costUsd }),
}) satisfies ModelRunResult;

Em uma frase: o que vem de fora é checado com luvas (safeParse), o que montamos nós mesmos é checado com rigor (parse). As duas falhas de dados viram PARSE_ERROR tipado; o sucesso só é devolvido se sobreviver à revalidação.

extractText varre parsed.choices e devolve o primeiro content string não-vazio; uma mensagem presente-mas-vazia é tratada como completion vazia válida, mas undefined (sem nenhum content string) é falha. accountFor(input.modelId, …) precifica o uso pelo registry e devolve undefined (não 0) para modelo desconhecido — por isso o spread condicional costUsd === undefined ? {} : {costUsd}: nunca se inventa um custo. O satisfies ModelRunResult dá a checagem de tipo em build-time sem alargar o tipo do retorno.

Abra a coisa real

No repo: packages/adapters/src/openai-compatible.ts (o transporte), packages/harness/src/mcp.ts (a superfície read-only), packages/swarm/src/{ids,worker}.ts (idempotência por conteúdo + rename), packages/harness/src/funnel.ts linha 204 (looseJsonParse). Procure por safeParse com grep -rn "safeParse" packages/ para ver o quão pervasiva é a validação de fronteira.

8

Recapitulação em slides


Os seis pontos que fixam a lição. Use as setas ou os botões.

Saída estruturada

Validar é só metade

"Saída estruturada" tem três passos: validar → reparar → re-perguntar. O Alembic faz o primeiro com rigor; os outros dois não existem como laço automático. Saber dessa fronteira é o valor.

validar ✓ reparar ✗ re-perguntar ✗
1

Dois lados

safeParse vs parse, de propósito

A resposta do modelo (dado externo) usa safeParse → falha tipada. O sucesso (nosso objeto) usa parse → lança, porque um sucesso malformado seria um bug nosso. Um sucesso malformado é impossível.

safeParse(resposta) externo · nunca lança parse(sucesso) nosso · lança = bug
2

O gap

PARSE_ERROR é não-retentável

Saída inválida vira err(PARSE_ERROR) — não há re-pergunta com o erro anexado. O único "remendo", looseJsonParse, só fatia {…} e tenta um parse; falhou, descarta. Fatia + descarta, nunca repara.

parse fatia {…} descarta o sinal
3

Ferramentas

MCP read-only = segurança por ausência

A superfície MCP não tem tool de escrita. O transporte menos confiável recebe a menor autoridade. Sem tool de escrita exposta, não há chamada para alucinar ou duplicar: idempotência por construção.

host MCPsó read read → orquestradorescrita vive aqui
4

Idempotência

No run/journal, não por ferramenta

runId é o hash do spec (mesmo input → mesmo diretório); o worker faz rename atômico (o rename é o commit). Replay seguro vem da arquitetura, não de tokens de idempotência por chamada.

runIdFor(spec) + rename() = replay
5

A moldura

Confiável, ainda não auto-aprendiz

Estas escolhas mostram um harness que prefere falhar limpo a remendar. O laço de repair ausente é o mesmo tipo de "loop aberto" do sinal-do-usuário (lição 0008): a fronteira entre confiável e auto-evolutivo.

harness confiável auto-aprendiz laço aberto
6
1 / 6setas
9

Fixe os termos (flashcards)


Clique para virar. Tente responder antes de ver o verso — é prática de recuperação, não leitura passiva.

safeParse vs parse
Quando usar safeParse e quando usar parse?
clique para virar
safeParse (nunca lança) para dado externo que pode vir errado — a resposta do modelo. parse (lança) para um invariante nosso que nunca deveria falhar — o objeto de sucesso que montamos.
o gap
O que falta no "structured output" do Alembic?
clique para virar
O laço de auto-repair / re-pergunta. PARSE_ERROR é não-retentável: saída inválida é descartada ou a chamada é refeita inteira, nunca "re-perguntada com o erro anexado".
looseJsonParse
O que looseJsonParse realmente faz?
clique para virar
Fatia + descarta. Tenta o texto todo; se falhar, recorta do primeiro { ao último } e tenta um JSON.parse; se ainda falhar, devolve undefined e quem chama descarta o sinal. Não conserta nada.
MCP
Por que a superfície MCP é read-only?
clique para virar
Assimetria de capacidade: o transporte menos confiável recebe a menor autoridade. Sem tool de escrita exposta, não há chamada para alucinar/duplicar — idempotência por construção, e a ausência É a segurança.
idempotência
Onde mora a idempotência no Alembic?
clique para virar
No nível run/journal, não por chamada de ferramenta: runId = hash do spec (mesmo input → mesmo diretório) + rename atômico (o rename é o commit) + journal append-only.
sucesso malformado
Por que um "sucesso malformado" é impossível?
clique para virar
Porque o objeto de sucesso é reconstruído e revalidado com modelRunSuccessSchema.parse antes de retornar. Se a forma não bate, parse lança — então nenhum Result.ok sai com forma inválida.
10

Dois comparativos que mudam o entendimento


Comparativo A · Sucesso silencioso vs falha tipada

SCRIPT INGÊNUO (sucesso silencioso)
const data = JSON.parse(resp.body);
// se body for lixo -> THROW lá longe
// se faltar campo -> undefined viaja
useIt(data.text); // estoura aqui, sem contexto

O erro aparece longe da causa, sem o corpo cru para depurar.

ALEMBIC (falha tipada na fronteira)
const parsed = schema.safeParse(resp.body);
if (!parsed.success)
  return makeFailure(
    { raw: resp.body }, // corpo cru anexado
    makeError(PARSE_ERROR, parsed.error.message));

O erro nasce na fronteira, tipado, com o corpo cru no raw.

script ingênuo: parse passa passa 💥 estoura aqui longe da causa Alembic: safeParse ⚑ falha na fronteira tipada, com o corpo cru no raw — fácil de depurar
O mesmo lixo: no script, o erro viaja e estoura três passos adiante; no Alembic, ele é capturado e nomeado exatamente onde nasceu.

Comparativo B · Reparar (livro-texto) vs descartar (Alembic)

DimensãoLaço de repair (livro-texto)Descartar/refazer (Alembic)
Custo de tokensCresce a cada tentativa de re-perguntaZero extra — não re-pergunta
DeterminismoQuebra (a "correção" é outra amostra)Preservado — colável ao cache/VM
Visibilidade de regressãoMascara (remenda em silêncio)Alta — falha limpa avisa
Recuperação de saída salvávelMaior (conserta quase-JSON)Menor (só o que looseJsonParse fatia)
Status no HARNESS-MAPgap consciente (roadmap)
Conclusão dos comparativos: nenhum lado é "certo" em absoluto. O Alembic troca taxa de recuperação por determinismo + honestidade de falha — coerente com um harness que se vende como infraestrutura confiável. O dia em que precisar da recuperação, o repair entra como camada aditiva atrás de uma flag.
11

Experimente · o simulador de fronteira


Escolha um payload que "o modelo devolveu" e veja o boundary reagir estágio a estágio. Depois ligue o modo repair (hipotético) para ver o que mudaria — e perceber que, no Alembic real, esse botão está sempre em off.

Modo de recuperação:
dispatch
·
safeParse(resp)
·
extractText
·
parse(success)
·
O que observar "JSON em prosa" só sobrevive porque looseJsonParse fatia o {…}. "Vírgula sobrando" é o caso revelador: o modo descartar perde o sinal; o modo repair hipotético o recuperaria — e é justamente esse modo que o Alembic não tem.
Você é o professor agora: pegue o repo e rode grep -rn "PARSE_ERROR" packages/ — quantos pontos de fronteira devolvem essa falha tipada? Pergunta de acompanhamento para levar à próxima lição: se a saída inválida só é descartada, o que impede um agente de ficar preso tentando para sempre? A resposta — budgets de loop, terminação por DAG e o T4 park — é a lição 0005 · Loop do agente & guardrails.
12

Em miúdos (For-Dummies)


O que assumimos de você (muito pouco)
  • Que você já viu um JSON.parse estourar pelo menos uma vez na vida.
  • Que você entende "função que devolve um resultado em vez de lançar erro" (o Result da lição 0002).
  • Nada sobre Zod, MCP ou idempotência — explicamos tudo aqui.
A frase para levar O Alembic valida a saída do modelo com rigor (Zod nas duas pontas) mas não a conserta. Saída ruim é descartada ou a chamada é refeita — nunca remendada.
Erro comum Achar que "temos validação" significa "lidamos com saída ruim". Validar é detectar. Reparar e re-perguntar são outras duas coisas — e o Alembic, de propósito, não as tem.
Atalho mental Dado de forasafeParse (luva). Dado nossoparse (rigor). Se você lembrar só disso, já aplica o padrão certo em 90% dos boundaries.
Para quem quer o termo exato "Idempotência por construção" = a operação perigosa nem existe na superfície exposta, então não há o que tornar idempotente. É diferente de "idempotência por chave", onde a operação existe mas você a torna segura de repetir com um token.
As Dez verdades sobre saída estruturada no Alembic
  1. Modelo devolve texto; texto não é confiável até virar objeto validado.
  2. A validação é baseada em Zod e pervasiva — é o estilo da casa.
  3. A resposta do modelo usa safeParse (nunca lança) → falha de dados tipada.
  4. O objeto de sucesso usa parse (lança) → um sucesso malformado é bug nosso, não do modelo.
  5. Por isso um sucesso malformado é estruturalmente impossível.
  6. Falha de forma vira PARSE_ERROR tipado, com o corpo cru anexado em raw.
  7. PARSE_ERROR é não-retentável: não há re-pergunta com o erro anexado.
  8. O único "remendo", looseJsonParse, fatia + descarta — não repara.
  9. A superfície MCP é read-only: a ausência de tools de escrita É a segurança.
  10. Idempotência vive no run/journal (runId por conteúdo + rename atômico), não por ferramenta.
13

Revisão — prove que pegou


Três perguntas. O placar abaixo soma seus acertos. Clique uma opção e leia a explicação — inclusive das erradas.

Verificação cumulativa
1 · O gateway responde 200 OK com um corpo que falha no safeParse. O que o Alembic devolve?
(b) A resposta é dado externo → safeParse → na falha, makeFailure(…, makeError(PARSE_ERROR, …)) com raw: response.value.body. (a) erra: não se inventa sucesso. (c) erra: o contrato é never-throws. (d) erra: é justamente o laço de repair que não existe (o gap).
2 · Por que o caminho de sucesso usa parse (que lança) e não safeParse?
(c) O objeto de sucesso é montado por nós; se ele não bate no schema, o erro é nosso, não do modelo — e um throw expõe o bug em vez de propagá-lo silenciosamente. (a)/(d) são invenções técnicas. (b) confunde: o texto veio do modelo, mas a forma do objeto é responsabilidade do Alembic.
3 · Em que sentido as tool calls do Alembic são "idempotentes por construção"?
(a) A ausência de tools de escrita É a propriedade: sem escrita exposta, não há nada para alucinar ou duplicar. (b) é a abordagem do livro-texto (chave por chamada), que o Alembic não usa. (c) é frágil (depende do modelo obedecer). (d) não é o mecanismo — a idempotência de repetição vem do runId por conteúdo + rename atômico no nível run/journal.
Acertos: 0/3
Para ir além (fontes primárias)
ai-engineering-from-scratch + HARNESS-MAP.md (princípios #9, #10, #22)

Os arquivos reais desta lição: packages/adapters/src/openai-compatible.ts, packages/harness/src/mcp.ts, packages/swarm/src/{ids,worker}.ts, packages/harness/src/funnel.ts (looseJsonParse).