Lição 6: Mão na massa: escrevendo à mão um harness de agente com controles
Objetivos de aprendizado:
- Transformar o loop de
stop_reasondas lições anteriores num loopwhilefuncional em cima do@anthropic-ai/sdk, decidindo por conta própria se continua chamando ferramentas ou retorna texto e encerra- Construir os blocos de conteúdo
tool_useetool_resultexatamente conforme a spec, e enviar os vários resultados de um único turno de volta dentro de uma só mensagemuser- Equipar esse loop com quatro válvulas de controle — máximo de turnos, teto de orçamento, detecção de não-progresso e aprovação para ações de alto impacto — e dizer com precisão a que passo do loop cada uma pertence
Pré-requisitos: Ler as Lições 2 a 5; entender o loop dirigido por
stop_reason, condições de parada, redes de segurança contra descontrole, e a intervenção humana no loop | Anterior: Lição 5 <<
Primeiro, como fica rodando
As primeiras cinco lições desmontaram a máquina peça por peça: como o loop gira, quando ele deve parar, como o descontrole se parece, como uma pessoa entra. Esta lição solda essas peças no menor harness funcional. Antes de qualquer código, veja o que ele faz num terminal — um agente ligado a duas ferramentas de brinquedo (get_time reporta a hora, read_file lê um arquivo dentro do projeto), recebendo uma frase: "leia a primeira linha do README.md, e então me diga que horas são".
Olhe de perto o que aconteceu: o usuário disse uma frase, e quantas ferramentas foram chamadas, qual foi primeiro, e quando parar foram todos decididos pelo modelo dentro do loop. Essa é a linha entre um agente e um workflow — o caminho de um workflow é fixo em código, enquanto um agente é o modelo dirigindo dinamicamente seu próprio processo e decidindo quais ferramentas usar1. O código host (o harness que estamos escrevendo nesta lição) nunca especificou "leia o arquivo primeiro, depois cheque a hora". Ele só girou o loop fielmente, rodou a ferramenta que o modelo nomeou, e devolveu o resultado. As duas ferramentas aqui são inofensivas, então nada interrompeu a execução — mas este harness também tem uma válvula de aprovação soldada, e se o modelo se estende para algo de alto impacto como apagar um arquivo ou disparar uma requisição, ele para e espera um aval humano antes de agir (escrevemos isso mais adiante na lição). O resto desta lição constrói, linha por linha, o código por trás daquela saída de terminal.
O loop central: traga o esqueleto, troque pelo SDK de verdade
O callModel da Lição 2: O loop central: de uma ida e volta à operação contínua era pseudocódigo. Agora ele vira o @anthropic-ai/sdk de verdade. O esqueleto do loop é idêntico: envie uma requisição carregando messages, olhe response.stop_reason — se for "tool_use", rode as ferramentas, costure os resultados de volta e envie de novo; se não for (digamos, end_turn), retorne o texto e saia do loop2.
Aqui está a versão mínima sem nenhuma válvula, para o próprio loop ficar visível:
Ponha isto ao lado do esqueleto da Lição 2 e a estrutura não se moveu: a linha do while ainda diz "repita enquanto stop_reason for tool_use", e o corpo ainda são os mesmos quatro passos — empurra assistant, roda ferramentas, empurra tool_result, reatribui response. A única mudança substantiva é callModel virando client.messages.create(...), mais aquela reatribuição no fim do corpo. Essa reatribuição é o que torna a parada possível; tire-a e stop_reason fica no valor antigo para sempre, que é exatamente o loop morto da Lição 4: Descontrole e fallback: loops mortos, giro em falso, esgotamento de orçamento.
Campos de tool_use / tool_result, nenhum deles faltando
runToolUses é onde a ferramenta que o modelo nomeou de fato roda. A coisa mais fácil de errar aqui são os campos do bloco de conteúdo, então siga a spec: um bloco tool_use carrega id / name / input, um bloco tool_result carrega tool_use_id (declarando qual chamada ele responde) e content, e quando a execução da ferramenta falha você adiciona is_error: true3. Há mais uma regra rígida: por mais blocos tool_use que uma resposta contenha, essa mesma quantidade de blocos tool_result tem de voltar, todos empacotados na única mensagem user que segue imediatamente3 — a linha messages.push({ role: "user", content: toolResults }) no corpo do loop acima é o que mantém essa regra.
Note o try/catch: uma ferramenta explodindo não deveria derrubar o harness inteiro junto. Embrulhe o erro num tool_result marcado is_error: true e devolva-o, e o modelo ganha a chance de tentar de novo com argumentos diferentes ou tomar outra rota. Isso é muito mais estável do que lançar e matar o processo.
Parafusando quatro válvulas de controle
O loop gira agora, mas é o loop pelado da Lição 2 — aquele que confia no modelo e não deixa saída para si mesmo. Ele para no turno em que o modelo retorna end_turn, sem fronteira nenhuma no meio do caminho. E a autonomia de um agente significa custos mais altos mais o potencial de erros se acumulando volta após volta do loop, com o modelo potencialmente operando por muitos turnos1 — um loop pelado aposta a decisão inteira de parar-ou-continuar no modelo, o que é arriscado demais. Agora soldamos as quatro válvulas das lições anteriores, uma de cada vez.
Cada válvula guarda uma coisa, e nenhuma das posições é arbitrária:
- Válvula 1, máximo de turnos (Lição 3: Condições de parada: quando um agente deve desistir):
turns >= MAX_TURNSfica bem no topo do corpo, antes deturns++. Significa "antes desta volta, cheque se outra volta ainda é permitida". Esta condição de parada explícita existe para que, ao lado do próprioend_turndo modelo, você mantenha o controle nas suas próprias mãos1. - Válvula 2, teto de orçamento (Lição 4: Descontrole e fallback: loops mortos, giro em falso, esgotamento de orçamento): toda vez que uma resposta volta, some os tokens de
response.usagee pare no teto. Quando os turnos são poucos mas o contexto de cada turno é enorme, a contagem de turnos sozinha não segura o gasto; você precisa de tokens como uma comporta separada e independente. - Válvula 3, detecção de não-progresso (Lição 4): achate as chamadas de ferramenta deste turno numa assinatura e compare-a com a anterior; idênticas significam giro em falso. Isso pega o caso estagnado em que os turnos não passaram do limite e o orçamento não estourou, mas o modelo está andando no lugar, chamando a mesma ferramenta com os mesmos argumentos de novo e de novo.
- Válvula 4, a válvula de aprovação (Lição 5: Intervenção e direção: interromper, redirecionar, humano no loop): dentro de
runToolUses, antes de de fato executar uma ferramenta, ações de alto impacto ganham uma confirmação humana primeiro. Aprovação humana no loop em ações de alto impacto é precisamente o jeito recomendado de segurar o risco de agência excessiva4.
A função de assinatura da Válvula 3 é simples a ponto de ser sem graça — junte os nomes e argumentos de todo bloco tool_use do turno numa única string. Distinguir "o que foi chamado com quais argumentos" é tudo o que ela precisa fazer:
A válvula de aprovação: encaixada no momento antes da execução
Das quatro válvulas, a posição da válvula de aprovação é a que mais importa e a mais fácil de errar. Ela tem de se encaixar no momento em que o modelo nomeou uma ferramenta mas a ferramenta ainda não rodou — imprima a ação prestes a acontecer, espere por um humano, execute só depois da confirmação. Um passo mais tarde e o arquivo já está escrito, a requisição já enviada, e perguntar "confirma?" é inútil. Então ela vai dentro de runToolUses, antes da linha impl(...):
approve é uma função passada de fora; num terminal significa "imprima a ação, leia uma linha de entrada":
Um detalhe que importa: mesmo quando o usuário recusa, você ainda retorna um tool_result marcado is_error: true em vez de não retornar nada. A spec exige que todo tool_use tenha um tool_result correspondente devolvido3; pule-o e a próxima requisição dá erro porque uma chamada de ferramenta não tem resultado. Recusar não é o mesmo que ignorar — uma recusa é ela própria um resultado que o modelo merece ouvir, e um modelo que aprende que foi recusado muitas vezes muda para uma rota que não precisa da ação de alto impacto de jeito nenhum.
Duas ferramentas de brinquedo, para o loop de fato rodar
As válvulas estão no lugar; o que falta são ferramentas que o modelo possa chamar. Esta lição usa só dois brinquedos absolutamente seguros e mantém as operações perigosas fora da porta: get_time reporta a hora atual, e read_file lê um arquivo — com path.resolve fincando-o firmemente dentro do diretório do projeto, para que o modelo (ou um modelo tirado do curso pela saída de uma ferramenta) não consiga ler caminhos fora dos limites como /etc/passwd:
Nenhuma das ferramentas está no conjunto HIGH_IMPACT, então nenhuma dispara aprovação — elas são inofensivas por construção. Para demonstrar a válvula de aprovação, adicione um write_file a toolImpls e a HIGH_IMPACT. Esta lição evita de propósito introduzir uma operação de escrita real para que rodar o exemplo não possa danificar seus arquivos.
Juntando tudo: um ponto de entrada que você roda com node agent.js
Por último, reúna runAgent, runToolUses, as definições de ferramenta e a função de aprovação num ponto de entrada que você roda diretamente — a coisa por trás da saída de terminal no topo desta lição:
Jogue as peças anteriores (import, client, MODEL, runAgent, runToolUses, signatureOf, approveInTerminal, toolImpls, tools, main) num só agent.js, defina ANTHROPIC_API_KEY, rode npm i @anthropic-ai/sdk, e node agent.js "sua tarefa" vai rodar.
Olhe de novo estas cem e tantas linhas e você vai notar que nenhuma delas é um conceito novo: o loop while e o stop_reason vieram da Lição 2, MAX_TURNS da Lição 3, o orçamento e a detecção de giro em falso da Lição 4, e a válvula de aprovação da Lição 5. Um harness não é algum framework profundo; é esta camada de loop-mais-válvulas que você mesmo escreve e controla. Mesmo modelo, mesmas duas ferramentas — mas um harness com essas quatro válvulas e o loop pelado da Lição 2 podem diferir enormemente em quão estável rodam a mesma tarefa, porque o que decide se um agente é confiável é em grande parte essa camada externa de código de controle, não só o modelo dentro dela5.
Mantenha o senso de proporção sobre complexidade também: nem todo agente precisa das quatro válvulas, e uma linha que vale a pena lembrar é que você deveria considerar adicionar complexidade apenas quando ela comprovadamente melhora os resultados1. Uma ferramenta pequena que roda três a cinco turnos num ambiente controlado pode ficar bem só com MAX_TURNS; quatro válvulas são para os casos que rodam muitos turnos seguidos e podem se estender a ações de alto impacto.
Recapitulação
- O núcleo de um harness funcional ainda é o loop da Lição 2: envie uma requisição carregando
messages→ chequestop_reason, e se fortool_use, rode as ferramentas, costure umtool_resultde volta e envie de novo; se não for, retorne texto e encerre2. Trocar para o SDK de verdade só transformacallModelemclient.messages.create(...) - Os campos do bloco de conteúdo seguem a spec sem nenhum faltando:
tool_usecarregaid/name/input,tool_resultcarregatool_use_id/contentmaisis_errorna falha; por mais blocostool_useque um turno tenha, essa mesma quantidade de blocostool_resultvolta, todos empacotados na única mensagemuserque segue imediatamente3 - Cada uma das quatro válvulas de controle guarda um ponto, e suas posições não podem ser embaralhadas: máximo de turnos (Lição 3) e o teto de orçamento (Lição 4) são as fronteiras rígidas que tornam o loop certo de parar, a detecção de não-progresso (Lição 4) pega o andar no lugar, e a válvula de aprovação (Lição 5) tem de se encaixar antes da execução da ferramenta — porque a autonomia de um agente traz custos mais altos e erros que se acumulam, e o modelo pode operar por muitos turnos1, então o próprio
end_turndo modelo não consegue segurá-lo - A válvula de aprovação exigindo confirmação humana em ações de alto impacto é o jeito recomendado de segurar o risco de agência excessiva4; mesmo numa recusa, retorne um
tool_resultcomis_errore não deixe a chamada pendurada3 - Um harness não é um framework profundo; é esta camada de loop-mais-válvulas que você mesmo escreve e controla — mesmo modelo, código de controle diferente, e a confiabilidade pode diferir enormemente5. Mas não empilhe válvulas por empilhar tampouco: adicione complexidade apenas quando ela comprovadamente melhora os resultados1
Você terminou este curso. De "o que é um harness" a escrever à mão um loop com quatro válvulas de controle, o que você tem agora nas mãos não é só um conjunto de conceitos — é código de verdade que roda, que você pode editar, e ao qual você pode continuar adicionando controle. Ligue-o às suas próprias ferramentas e deixe-o fazer algum trabalho para você.
Footnotes
-
Building Effective AI Agents — Anthropic Engineering — https://www.anthropic.com/engineering/building-effective-agents ↩ ↩2 ↩3 ↩4 ↩5 ↩6
-
How tool use works — Claude API — https://platform.claude.com/docs/en/agents-and-tools/tool-use/how-tool-use-works ↩ ↩2
-
Handle tool calls — Claude API — https://platform.claude.com/docs/en/agents-and-tools/tool-use/handle-tool-calls ↩ ↩2 ↩3 ↩4 ↩5
-
LLM06:2025 Excessive Agency — OWASP Gen AI Security Project — https://genai.owasp.org/llmrisk/llm062025-excessive-agency/ ↩ ↩2
-
The 2026 Agent Engineering Roadmap — GitHub (codejunkie99/agent-roadmap-2026) — https://github.com/codejunkie99/agent-roadmap-2026 ↩ ↩2