Lição 6: Mão na massa: instalando uma camada de observabilidade no harness
Objetivos de aprendizado:
- Instalar uma camada de observabilidade funcional no seu próprio harness: logs estruturados em JSON Lines, árvore de trace reconstruída a partir dos logs, resumo de métricas em uma linha
- Percorrer uma tarefa com erro real do sintoma → filtro → primeira divergência → correção → comparação por reexecução (lado do modelo pregado por stub para reexecuções completas; APIs reais voltam ao retomar-do-erro), e explicar quais absurdos são causa e quais são contágio
- Traçar as fronteiras desta camada de observabilidade: cobre um processo, uma execução; conteúdo desligado por padrão; limiares não inventados
Pré-requisitos: Lições 1–5 concluídas, com o loop do harness do curso 7 (Fundamentos do Harness de Agente: Laços e Controle) rodando ao alcance da mão | Anterior: Lição 5 <<
O sintoma: uma região a mais em summary.md que não existe
Vamos começar por um cenário concreto, daqueles que dá para sentir o cheiro na mesa.
Você escreveu um agentezinho para cuidar dos relatórios semanais: o diretório data/ guarda três CSVs de vendas trimestrais, ele os lê, agrega por região e escreve summary.md. Três ferramentas: list_files, read_file, write_file. Rodou bem por semanas.
Segunda-feira de manhã, um colega pergunta no chat: “De onde saiu essa região ‘Centro da China’? A gente não tem região Centro da China.”
Você abre summary.md e, de fato:
Você abre data/, três arquivos dentro: 2026-q1-east.csv, 2026-q1-south.csv, 2026-q1-north.csv, com os nomes de região Leste da China, Sul da China, Norte da China no conteúdo. Busca no diretório inteiro por “Centro da China” — zero ocorrências. Sul da China sumiu por completo, Centro da China apareceu do nada, e o número 208000 veio sabe-se lá de onde.
A pergunta agora é: em que etapa isso deu errado?
Sem uma camada de observabilidade, você tem duas coisas: um summary.md escrito errado e a frase “o modelo inventou”. Essa frase não resolve nada — você não sabe se ele não conseguiu ler o arquivo logo de cara, ou se leu mas calculou errado, ou se leu os três mas embaralhou as linhas na hora de escrever. E você não pode “só reproduzir com um breakpoint” para forçar o erro a aparecer: agentes são não determinísticos entre execuções, os mesmos prompts e as mesmas ferramentas podem tomar um caminho completamente diferente, porém igualmente válido1. Você roda de novo três vezes, as três podem dar certo, ou a quarta pode falhar de um jeito novo.
Pior: os erros se acumulam. Uma etapa falhando pode fazer o agente desviar para uma trajetória completamente diferente, e o resultado final parece não ter relação com a falhinha original1. Então você não pode ficar só encarando o ponto final — o absurdo no ponto final costuma ser só contágio (a lição 1 chamou isso de desvio de trajetória, é a mesma coisa), e a lesão de verdade está a montante, em alguma etapa.
O trabalho desta lição é transformar “não dá para dizer” em “dá para conferir”: soldar uma camada de observabilidade no harness e então percorrer este bug real uma vez para localizá-lo. Tudo das cinco lições anteriores aterrissa num único arquivo executável.
O kit de observabilidade em três peças: o que registrar
Ao rodar agentes em produção, você precisa de visibilidade sobre quatro coisas: quais ferramentas eles chamaram, quanto tempo cada requisição de modelo levou, quantos tokens foram gastos, onde as falhas aconteceram2. A abordagem oficial é exportar isso como traces, métricas e eventos de log do OpenTelemetry; esta lição não puxa nenhuma biblioteca do OTel, nós fazemos uma versão mínima à mão, em três peças:
- Logs estruturados: uma entrada JSON Lines por requisição de modelo, uma por chamada de ferramenta, escritas em
run.log.jsonl.
- Árvore de trace: depois que a execução termina, reconstruir as relações pai-filho a partir daquele JSONL e imprimir indentado.
- Resumo de métricas: uma linha com o total de rodadas, contagem de chamadas de ferramenta, tokens, contagem de erros e duração total.
Modelo de spans: quem é pai de quem
Com a telemetria aprimorada ligada oficialmente, cada etapa do loop do agente vira um span inspecionável: uma interação é o span raiz, e requisições de modelo e execuções de ferramenta são spans filhos dele2. Note que, na árvore oficial, requisições de modelo e chamadas de ferramenta são irmãs de mesmo nível sob a raiz — a árvore que você reconstruiu na lição 4 tem esse formato. Nossa versão mínima usa deliberadamente uma ligação diferente: penduramos as chamadas de ferramenta sob a requisição de modelo que as disparou, de modo que o formato da árvore mostra diretamente “o que o modelo quis fazer nesta rodada”, com as ferramentas paralelas sob o mesmo pai visíveis de bate-pronto. O mecanismo pai-filho é exatamente o mesmo, nós só escolhemos um pai diferente para as ferramentas — as duas ligações são válidas, e qual delas escolher depende de que pergunta você quer que a árvore responda primeiro. Nossas três camadas ficam assim:
As relações pai-filho não dependem de uma pilha de chamadas em memória, elas dependem de dois campos nos logs: cada registro carrega um span_id, mais um parent_id apontando para o pai dele. A árvore é reconstruída a partir do JSONL em disco depois que a execução termina, não impressa conforme se avança. Este ponto importa: qualquer coisa visível na árvore precisa antes ter sido gravada nos logs. Se você achar que falta algo na árvore, não é problema do código de impressão, é problema do código de gravação.
Tabela de campos
O desenho de campos abaixo é a abordagem de engenharia desta lição, não uma especificação oficial — oficialmente são nomes de atributo de span do OTel; quando você escreve o seu próprio harness, os nomes dos campos são escolha sua. Quatro nomes diferem das lições 3 e 4, então vamos mapeá-los primeiro para você não achar que são erros de digitação: o type da lição 3 se chama kind aqui (naquela época havia só dois tipos de registro, agora temos agent_run, e um termo semanticamente mais amplo cai melhor); input_tokens/output_tokens foram dobrados em um objeto tokens:{input,output} (as coisas específicas de chamada de modelo empacotadas juntas); tool_response se chama tool_result aqui (o payload do hook chama de response, o valor de retorno aqui vem direto da implementação da ferramenta, seguindo a nomenclatura do bloco de conteúdo da API); o parent_span_id da lição 4 foi encurtado para parent_id. O custo também está declarado: o stats.mjs da lição 3 precisa de duas mudanças de nome de campo para ler este log — esta é uma demonstração ao vivo de “alinhamento de vocabulário importa mais do que nomes bonitos”. O vocabulário dos campos ainda vale a pena alinhar com o material oficial, para que, quando você enfim conectar um backend, não precise trocar de conceito, só de grafia:
O truque do trace_id foi aprendido do oficial: um prompt de usuário dispara várias chamadas de API e várias ferramentas, e o oficial usa um atributo prompt.id para amarrar todas de volta ao prompt disparador; a abordagem oficial de tracing também é direta — para rastrear toda a atividade disparada por um único prompt, filtre os eventos por um valor específico de prompt.id3. Aqui usamos trace_id; uma execução é uma tarefa, então use trace_id, faz exatamente a mesma coisa. A propósito, não há session_id aqui: uma execução deste script é uma sessão, manter esse campo seria sem sentido; cenários de várias rodadas e várias sessões o trazem de volta, e a referência de vocabulário está na lição 3.
A linha de métricas também não foi escolhida a esmo. Além da acurácia de topo, o oficial recomenda coletar: tempo total de execução de chamadas de ferramenta individuais e de tarefas, número total de chamadas de ferramenta, consumo total de tokens, erros de ferramenta4 — essas quatro coisas têm onde aterrissar na linha de resumo e no duration_ms de cada registro. rounds é o quinto número que eu acrescentei, prático para ver de relance quantas iterações do loop houve. Você já usou o conjunto oficial no curso 10 (Verificação e garantia de qualidade: não deixe passar o que só “parece certo”) — lá para avaliar, aqui para diagnosticar com a mesma régua.
Quanto conteúdo registrar: a única linha que esta lição pede que você trace sozinho
tool_input e tool_result podem ser um CSV inteiro, uma entrada de usuário inteira, um documento inteiro escrito. Registrar tudo é tecnicamente uma linha de código, mas por padrão você não deveria.
A postura padrão da telemetria oficial é clara: coisas estruturais sempre gravadas, conteúdo nunca gravado — cada span tem duração, nome do modelo, nome da ferramenta e contagem de tokens gravada quando a API devolve o uso, enquanto o conteúdo lido e escrito pelo agente por padrão não é coletado2. Prompts de usuário, a mesma coisa: por padrão só o comprimento é gravado, e gravar o conteúdo exige uma variável de ambiente separada3. E o oficial emparelha esse tipo de chave com uma afirmação dura: a menos que seu pipeline de observabilidade esteja aprovado para armazenar os dados que seu agente manipula, deixe essas chaves desligadas2.
Nossa camada deixa um meio-termo: por padrão grava shape (é string ou objeto, qual o comprimento, quais as chaves), chars (contagem de caracteres) mais um trecho dos primeiros 60 caracteres como resumo de cabeça. O trecho existe para você reconhecer de relance “qual arquivo ele leu desta vez” durante a sua própria depuração, sem precisar reexecutar repetidamente. O HEAD_CHARS = 60 no código é onde essa linha fica; defina 0 e nenhuma palavra de conteúdo toca o disco. Onde você traça essa linha no seu próprio projeto depende de onde os logs aterrissam, de quem consegue vê-los, de se a aprovação de dados foi obtida — esta é uma pergunta de conformidade, não uma pergunta técnica.
O arranjo de verificação: onde as diferenças das três versões ficam pregadas
Esta lição roda três vezes: uma normal, uma com bug, uma corrigida. As três saídas precisam ser comparáveis linha a linha, então as respostas do modelo não podem ser reais — respostas reais de modelo diferem toda vez, você não consegue usá-las para ensinar a localizar. Seguindo a abordagem antiga dos cursos 8 a 10 desta série: cliente stub com fila de respostas fixa. client.messages.create() não envia requisição de rede, devolve em sequência objetos de resposta pré-escritos de um array, cada objeto carregando stop_reason, content e usage completos. O loop do harness não muda uma palavra — o que ele recebe tem formato idêntico ao que um cliente real devolveria.
As diferenças de todas as três versões ficam pregadas na tabela VERSIONS do código; cada versão tem duas coisas:
Fora desta tabela, toda outra linha de código é compartilhada pelas três versões. As ferramentas leem/escrevem disco de verdade: list_files faz readdirSync de verdade, read_file lê arquivos de verdade e lança exceção de verdade porque o arquivo não existe, write_file escreve summary.md no disco de verdade. Então aquele erro em v-bug não é um objeto de erro forjado, é o sistema de arquivos genuinamente não achando aquele arquivo.
Para deixar claro: stubs resolvem “o lado do modelo é reproduzível”, não “o agente é determinístico”. Rodando de verdade, o mesmo prompt duas vezes pode escolher ferramentas diferentes e tomar caminhos diferentes1. O valor desta camada de observabilidade está exatamente aqui — os caminhos diferem a cada vez, mas a cada vez há um registro para revisar.
O que precisa ser dito com clareza: os stubs pregam o lado do modelo para que reexecuções completas funcionem; APIs reais voltam ao retomar-do-erro. Esta lição se atreve a fazer reexecuções completas justamente porque o lado do modelo está pregado por stub — reexecutar não introduz variáveis novas, e a comparação linha a linha se sustenta. Quando você conectar APIs reais, os stubs somem, e você volta à abordagem da lição 5: retomar do erro.
Código completo: observed-agent.mjs
Um arquivo inteiro, zero dependências, roda com node puro. Salve como observed-agent.mjs e depois node observed-agent.mjs --version v-bug executa.
Alguns pontos que valem destaque separado:
- O loop em si não mudou. Aquele
while (response.stop_reason === "tool_use") do curso 7 (Fundamentos do Harness de Agente: Laços e Controle) não moveu uma palavra; a observabilidade envolve por fora: callModel() grava uma marca de tempo antes e depois da requisição, runToolUses() envolveu cada bloco de ferramenta em um try/catch mais um cronômetro. Tire esses dois envoltórios e o que sobra é o loop original.
MAX_ROUNDS é uma comporta dura. Agentes precisam de condições de parada, como um número máximo de iterações; isso faz parte do controle5. Ultrapassar lança um erro, grava um harness_error e sai com código 2.
- Divisão de trabalho dos códigos de saída. Este script só cuida de executar e registrar: execução terminada significa 0; parâmetros errados ou descontrole significam não-zero. “A saída está correta?” é trabalho da suíte de verificadores do curso 10 (Verificação e garantia de qualidade: não deixe passar o que só “parece certo”) — note que
v-bug também sai com 0, o harness acha que terminou tudo tranquilo. A verificação te diz se quebrou, esta camada te diz por quê.
- Erros de ferramenta não quebram o loop. Os erros são embrulhados em um
tool_result com is_error: true e devolvidos ao modelo, e o loop continua. Isso está correto — agentes precisam obter verdade fundamental do ambiente a cada etapa para avaliar o progresso5, e erros também são feedback. O bug inteiro desta lição acontece na segunda metade dessa frase: o feedback foi dado, mas dado mal demais.
Primeira execução, tudo às mil maravilhas: v-good
Veja primeiro como é o normal. A saída de terminal abaixo e todas as saídas de terminal seguintes são genuinamente executadas, não exemplos escritos à mão.
Seus trace_id, span_id, ts e contagens de milissegundos vão diferir dos meus — os ids são gerados aleatoriamente a cada execução, os milissegundos são duração genuína. Fora isso, cada linha deve bater palavra por palavra.
Ler esta árvore de cima a baixo é uma frase completa: primeiro lista o diretório (turn-1), depois numa única rodada lê três arquivos em paralelo (turn-2, com três nós irmãos abaixo), depois escreve o arquivo (turn-3) e por fim encerra (turn-4, stop=end_turn). Aquelas três linhas paralelas são três blocos tool_use na mesma resposta do modelo, então o parent_id delas aponta para o mesmo model_call — o formato da árvore mostra diretamente “o que o modelo quis fazer nesta rodada”.
Não leve a sério aquela coluna de 0ms nos model_call: o cliente stub não tem ida e volta de rede, então a duração da requisição de modelo é toda 0. Depois de conectar APIs reais, esta coluna ganha valor diagnóstico — acompanhar durações de requisição de API e tempos de execução de ferramenta serve exatamente para achar gargalos de desempenho3.
O arquivo de log fica assim, um JSON completo por linha, dá para usar grep direto:
A linha dois é aquele list_files: o parent_id aponta para o span_id da linha um (então ele fica pendurado sob turn-1), dentro de tool_input só há formato, comprimento e um trechinho, e tool_result o mesmo — shape é string(52), e head traz os três nomes de arquivo. Nem um byte desta linha é “conteúdo de arquivo”, mas você já consegue responder “o que esta etapa chamou, que formato de coisa ele recebeu, deu erro?”.
Olhe de novo a linha de resumo de métricas: 4 rodadas, 5 chamadas de ferramenta, 0 erros, 6033 tokens, e no fim da linha também a duração total. Esta linha de números vale uma olhada toda vez que uma execução termina — a contagem de chamadas de ferramenta pode expor rotinas fixas que o agente percorre repetidamente, e um monte de chamadas redundantes muitas vezes sugere que os parâmetros de paginação ou de limite de tokens deveriam ser ajustados; enquanto um monte de erros de parâmetro inválido pode dizer que as descrições de ferramenta deveriam ser mais claras e os exemplos mais completos4. Tokens merecem atenção especial: ao analisar desempenho de eval, o oficial descobriu que o uso de tokens sozinho explica 80% da variância, e os outros dois fatores explicativos são a contagem de chamadas de ferramenta e a escolha do modelo1.
Reproduza o sintoma: v-bug
Agora rode a versão com bug. A fila do stub tem enterrada dentro dela a divergência real da abertura da lição; não espie primeiro, ache você mesmo a partir da saída.
O artefato está de fato errado:
Note primeiro algumas coisas invisíveis de fora:
- Contagem de rodadas e contagem de chamadas de ferramenta idênticas às de
v-good: 4 rodadas, 5 chamadas. Olhando só para esses dois números, as duas execuções parecem idênticas.
- Tokens subiram só 67 (6100 contra 6033). Se o seu alerta é “tokens acima do limiar”, este aqui nem tocaria a campainha.
- Só
errors=1, este único número mudou. É por isso que erros de ferramenta precisam ser cidadãos de primeira classe nas métricas4 — é o único sinal, no nível do resumo, de que esta execução tem algo errado.
- Aquele último
model_call é stop=end_turn, o agente acha que completou a tarefa com sucesso. Ele não deu erro, não pediu ajuda, não mencionou que faltava um pedaço de dado. O que ele omite no feedback muitas vezes pode ser mais importante do que o que ele inclui4.
Localização em cinco etapas: do rastreamento do sintoma à primeira divergência
A localização em cinco etapas da lição 5 é a orquestração geral; os materiais desta rodada são especiais — três logs comparáveis linha a linha na mão — então três das cinco etapas mudaram de forma, escritos lado a lado:
A etapa cinco precisa de explicação à parte. A lição 5 defende “depois de corrigir, retome do erro, não reexecute do zero”, com o motivo de que reexecuções completas reintroduzem não determinismo e você não consegue distinguir “corrigi certo” de “tive sorte desta vez”. Esta lição se atreve a fazer reexecuções completas justamente porque o lado do modelo está pregado por stub — reexecutar não introduz variáveis novas, e a comparação linha a linha se sustenta. Quando você conectar APIs reais, os stubs somem, e você volta à abordagem da lição 5: retomar do erro.
Aterrissando nos materiais desta rodada, as cinco etapas são as de baixo. Finja que você ainda não sabe a resposta e percorra uma vez.
Etapa um: fixe esta única execução
Em ambiente de produção, os logs de todas as execuções se misturam num único fluxo. Simule primeiro essa situação, juntando os logs das três execuções:
Das 32 linhas, só 10 pertencem à execução com bug. Esta etapa usa a abordagem de tracing dada pelo oficial: para rastrear toda a atividade disparada por um prompt, filtre os eventos por aquele id específico3. Se ele se chama prompt.id ou trace_id não importa; o que importa é que este id exista e que todo registro o carregue.
Já que estamos aqui, dá para conferir quantos erros há no fluxo inteiro:
Dois: um de v-bug, um de v-fixed. v-good limpinho.
Etapa dois: identifique a primeira divergência na árvore
A árvore já está impressa; percorra de cima para baixo e ache o primeiro registro que não bate com a expectativa:
Sob turn-2, três leituras paralelas, e a do meio quebrou. O motivo de ter quebrado está escrito em tool_input: o caminho é data/2026-q1-sourth.csv — south digitado como sourth. Aquela linha de list_files na árvore só mostra ok string(52); os nomes corretos precisam ser escavados no log: puxe aquele registro (você já o viu no head -3 acima) e o tool_result.head diz 2026-q1-east.csv 2026-q1-north.csv 2026-q1-south.csv — o modelo de fato recebeu o nome correto. A árvore cuida de localizar, os logs cuidam dos detalhes; as duas camadas cooperam exatamente assim.
Para ver aquele registro completo, pesque-o do fluxo:
Esta é a divergência. Note como ela foi reconhecida: não por adivinhação, por três campos — trace_id tranca o escopo nesta única execução, error não nulo a separa dos dez registros, e tool_input.head te diz onde os parâmetros saíram errados. Três campos, nenhum dispensável.
Etapa três: reconheça os absurdos a jusante como contágio, não corrija cada um
Depois da divergência, no turn-3 o modelo escreve uma agregação com uma região Centro da China e no turn-4 reporta “tarefa concluída”. As duas etapas parecem bem absurdas, mas os dois são a jusante:
Em sistemas de agente, uma etapa falhando basta para fazê-lo desviar para uma trajetória completamente diferente, com resultado final imprevisível1 — este é o exemplo mais limpo disso. Se você só tivesse o summary.md final, onde iria corrigir? Provavelmente iria mudar o prompt: “não invente dados”, “sempre cite as fontes dos dados”. Todas essas mudanças acertam o contágio, não acertam a lesão. Da próxima vez, troque o método do erro de digitação e ele vai inventar de novo.
A propósito, por que este sintoma cresceu até virar “Centro da China” em vez de “faltou Sul da China”: ele de fato recebeu o nome do arquivo (2026-q1-south.csv está ali no retorno de list_files), e as correspondências east→Leste da China e north→Norte da China já estão nos conteúdos das duas leituras bem-sucedidas anteriores — o que falta a ele são só os números específicos daqueles três meses. Mas aquele ENOENT opaco não lhe disse nem “tente de novo com o nome correto” nem “pare e explique direito”, então ele pegou o caminho mais fácil: disfarçar a lacuna de completude, preenchendo nome de região e números. A invenção não acontece porque ele não sabe nada, acontece porque o erro não lhe deu saída melhor.
Etapa quatro: determine a causa — o feedback que ele recebeu era péssimo
Nesta etapa, não tenha pressa de culpar o modelo. Olhe o que aquele erro de fato lhe deu:
Esta linha tem informação suficiente para uma pessoa que é engenheira; para um agente decidindo “o que fazer em seguida”, ela é quase vazia. Ele não consegue ler ali “quais arquivos deste diretório são legíveis”, não consegue ler “eu digitei errado ou este dado genuinamente não existe” e muito menos consegue ler “ao encontrar esta situação eu deveria parar e perguntar, não preencher sozinho”. Agentes precisam contar com feedback de verdade fundamental do ambiente a cada etapa para julgar o progresso5; este ENOENT foi todo o feedback que ele recebeu.
A sugestão oficial sobre engenharia de ferramentas é exatamente para essa lacuna: quando chamadas de ferramenta levantam erros, as próprias respostas de erro devem ser bem escritas, explicando com clareza melhorias específicas e acionáveis, em vez de jogar um código de erro opaco ou um stack trace4. Então o que precisa mudar desta vez não é o prompt, é a mensagem de erro do read_file.
Etapa cinco: comparação por reexecução (os stubs pregaram o lado do modelo, aqui dá para reexecutar por inteiro)
O método de correção está na próxima seção; depois de rodar, volte para ver se os números mudaram. Localizar não termina em “eu sei a causa”, termina em “depois de corrigir, aquela etapa no mesmo trace realmente está diferente”.
Corrija e reexecute: v-fixed
O que mudou foi a parte do read_file no código, só a mensagem de erro:
Esta mensagem enfia três coisas dentro: estado atual (o que de fato há no diretório), o que fazer em seguida (tentar de novo com o nome original), quando parar (se o dado genuinamente não estiver ali, pergunte a uma pessoa, não estime). As duas primeiras dão ao modelo um caminho a percorrer, a terceira bloqueia o caminho da invenção.
A fila de respostas de v-fixed demonstra a reação do modelo depois de receber esse erro: em vez de continuar inventando para a frente, ele volta a buscar verificação no ambiente — relê uma vez com o nome original listado na mensagem de erro e, por fim, na frase de encerramento, ainda devolve a pergunta ao usuário: “se houver dados de outras regiões fora de data/, me diga onde está o arquivo, eu não vou preencher os números por conta própria”.
O formato da árvore mudou: aquele ERROR no turn-2 continua no lugar original, mas abaixo dele nasceu um turn-3, dentro do qual há uma releitura com o nome de arquivo correto. O artefato está correto:
Os dois summary.md são idênticos byte a byte, e a região Centro da China sumiu.
As três execuções lado a lado:
Duas coisas precisam ser ditas com clareza, senão esta correção é fácil de entender errado:
Primeiro, errors não voltou a zero, e nem deveria. Aquela leitura com o nome digitado errado continuou dando erro; nós só trocamos a mensagem de erro, deixando o modelo escalar para fora do erro. A correção que genuinamente devolve errors a zero está em outra direção — escrever descrições de ferramenta mais explícitas, dar exemplos, para que o modelo não digite errado logo de saída. A leitura diagnóstica oficial corresponde exatamente a isso: grandes quantidades de erros de parâmetro inválido dizem que as descrições de ferramenta deveriam ser mais claras e os exemplos mais completos4. A descrição de read_file neste script já diz “O caminho precisa usar o nome de arquivo original devolvido por list_files”; claramente ainda não basta, e na próxima rodada deveríamos dar a ele um exemplo positivo.
Segundo, a correção não é de graça. Os tokens foram de 6033 para 8359, subiram 2326, um aumento de 38%, e o extra veio daquela rodada de ida e volta da releitura. Não é uma explosão, mas também não é custo zero. O custo de uma correção precisa ser posto na mesa e calculado; não dá para só olhar “o resultado está correto” e dar por encerrado.
Onde ficam as fronteiras desta camada de observabilidade
Esta coisa é pequena, e as fronteiras precisam ser ditas com clareza, para você não achar que instalá-la significa ter observabilidade de produção.
Ela cobre um processo, uma execução. Os logs são appendFileSync escrevendo direto num arquivo local, e o processo ser morto também não perde nada — isso é deliberado. Conecte um backend real e o tratamento não é este: na estrada do OTLP, a falha de exportação é silenciosa por padrão, endpoint inacessível ou rejeitando, o agente continua rodando, a telemetria é diretamente descartada e nem um erro aparece na sua aplicação; e a telemetria é agrupada em lotes antes de ser exportada em intervalos, e se o processo for morto antes da exportação, o que estiver no buffer do lote já era2. O “o pipeline de observabilidade vai mentir para você em silêncio” da lição 4 fala deste trecho. O arquivo local desvia dessa cova, e o custo é que ele fica só na máquina local.
Conectar backends reais e agregação entre processos não estão nesta lição. Para ligar esta camada a Honeycomb, Datadog, Grafana, Langfuse ou a um coletor auto-hospedado, é preciso o pacote do protocolo OTLP2, os campos precisam ser remapeados, e isso é outro assunto. Como agregar os logs de vários processos de agente, como distinguir por nome de serviço, a mesma coisa.
Limiares de alerta esta lição não dá em números. “Taxa de erro de ferramenta acima de quanto deveria alertar?” “Uma execução acima de quantos tokens conta como anomalia?” — a documentação oficial só mencionou que alertas devem ser feitos pelo seu backend, não deu número nenhum3. Eu também não vou inventar. Seus próprios limiares só podem crescer da sua própria linha de base: primeiro rode por um tempo, veja como é a distribuição das execuções normais, e só então trace a linha.
A gravação de conteúdo é desligada por padrão. Aquele HEAD_CHARS = 60 acima deixou só um trecho bem curto. Para genuinamente ligar o texto completo, a pré-condição é que o seu pipeline de observabilidade esteja aprovado para armazenar os dados que o seu agente manipula2 — passe primeiro pela aprovação de dados e só então mude o código, não o contrário.
Taxa de amostragem e janela de retenção de log também não serão expandidas. Uma execução são dezenas de linhas de JSONL; localmente, rodar algumas centenas de vezes não precisa de gestão; quando você precisar considerar isso, já é um problema de backend.
Palavra final: o valor desta camada de observabilidade não está em quanto ela registrou, está em ela permitir que você faça uma pergunta específica. “Por que ele inventou uma região Centro da China?” é uma pergunta sem resposta; “nesta execução com trace_id=tr-6e45c8c4, qual é o primeiro registro com error não nulo, e quais são os parâmetros?” é uma pergunta com resposta. Depois de colocar no ar um tracing de produção completo, só então dá para diagnosticar sistematicamente por que os agentes falharam e corrigir sistematicamente1.
💻 Exercícios
Recapitulação
- O trio da observabilidade cuida de um trecho cada: os logs JSON Lines cuidam de “registrar isso”, a árvore de trace cuida de “enxergar ordem e pertencimento”, o resumo de métricas cuida de “ver de relance se esta execução parece normal”; árvore e resumo são ambos reconstruídos a partir do JSONL em disco, e o que não estiver gravado nos logs nunca vai aparecer na árvore
- Todo registro precisa carregar
trace_id e parent_id: o primeiro circula registros espalhados de volta à mesma execução, o segundo permite reconstruí-los em árvore — esta é exatamente a mesma técnica que o oficial usa para amarrar todos os eventos disparados por um prompt com prompt.id, filtrando por ele para localizar3
- Conteúdo por padrão não é gravado em texto completo: a postura padrão da telemetria oficial é gravar tudo o que é estrutural, não coletar o conteúdo lido e escrito pelo agente, gravar só o comprimento dos prompts de usuário; para habilitar a gravação de conteúdo, a pré-condição é que o seu pipeline de observabilidade esteja aprovado para armazenar esse tipo de dado2
- Aqueles cinco números de métricas (duração, contagem de chamadas, tokens, contagem de erros, duração total) são o mesmo conjunto usado para avaliar no curso 10 (Verificação e garantia de qualidade: não deixe passar o que só “parece certo”)4, aqui trocado para uso diagnóstico; na comparação desta rodada, a contagem de rodadas, a contagem de chamadas e os tokens de
v-good e v-bug são quase idênticos, e a única coisa que mudou é a contagem de erros
- A ação-chave da localização é identificar a primeira divergência na árvore e então tratar uniformemente todos os absurdos a jusante como contágio: uma etapa falhando basta para fazer o agente desviar para uma trajetória completamente diferente1, e ir corrigir naquela camada do artefato final equivale a corrigir uma sombra
- Corrigir a mensagem de erro da ferramenta é uma correção que acerta a lesão: respostas de erro devem explicar com clareza melhorias específicas e acionáveis, em vez de jogar um código de erro opaco ou um stack trace4; depois desta correção o modelo mudou de “inventar uma região Centro da China” para “reler uma vez com o nome de arquivo original e devolver a pergunta ao usuário sobre se há outros dados”
- Depois de corrigir é obrigatório reexecutar e comparar, e é obrigatório reconhecer a conta:
errors não voltou a zero (o erro de digitação continua ali), os tokens subiram 38% (uma ida e volta a mais); “resultado correto” não equivale a “custo zero”
A linha principal das seis lições termina aqui. A lição 1 explicou por que você não consegue dizer — o agente toma caminhos diferentes em duas execuções, e sob um sintoma se comprimem várias causas que, vistas de fora, parecem idênticas. A lição 2 fincou a evidência de primeira mão na transcrição bruta, não no autorrelato dele. A lição 3 transformou cada etapa em dado com campos. A lição 4 costurou os dados espalhados numa árvore e, de passagem, te contou que esse pipeline em si vai mentir em silêncio. A lição 5 instalou sondas nas comportas do loop e deu o método de caminhada da localização. Esta lição soldou as cinco lições anteriores num arquivo de umas 400 linhas, com zero dependências, e o usou para genuinamente rastrear “de onde saiu a região Centro da China” até aquela leitura de caminho digitado errado no turn-2.
Este é também o curso 11 desta série. Da próxima vez que o seu agente não conseguir dizer onde errou, você não vai mais ter na mão só a frase “o modelo inventou” — você tem um log em que dá para dar grep, uma árvore em que dá para apontar para uma certa linha e falar, uma tabela de resumo com que dá para calcular custo e um conjunto de métodos de caminhada que vai do rastreamento do sintoma até a primeira divergência. O que resta é mover por inteiro os três trechos de observabilidade de observed-agent.mjs (logger, árvore de trace, resumo de métricas) para dentro do seu próprio harness, seguindo o padrão da seção 7 para envolver aquelas duas camadas em torno do seu loop — fixtures e stubs são andaime didático desta lição, não os leve — e então rodar a primeira tarefa real e ver o que há naquele primeiro run.log.jsonl de que você originalmente não fazia a menor ideia.