Pular para o conteúdo

Certificação Claude Developer Foundations · parte 15 de 20

Construção de agentes com Claude

Como construir um agente com Claude de duas formas, com o Agent SDK ou com um laço próprio sobre a API, e onde colocar os controles que não dependem do modelo.

Publicada em · 26 min de leitura

Tamanho da fonte:
Largura da coluna:

Um agente é um modelo que roda num laço, pede ferramentas, recebe os resultados e decide o próximo passo. Esta aula responde como você constrói um agente com Claude de duas formas: com o Claude Agent SDK, que já traz o laço pronto, ou com um laço seu sobre a API de mensagens. Nos dois casos a pergunta de projeto que mais pesa é onde colocar os controles que não podem depender de o modelo obedecer. Essa resposta é a seção de hooks, mas o desenho começa pelo laço, por isso a aula segue essa ordem.

O conceito de agente e a diferença entre workflow e laço autônomo estão em Agents. O formato exato do pedido de ferramenta e do resultado (tool_use, tool_result e o id que os liga) está em 4.1 Implementação de ferramentas. Aqui usamos esse formato sem explicá-lo de novo.

Mapa da aula

SeçãoO que responde
1. O problema que o laço resolvePor que um agente precisa de um laço e não de uma chamada só
2. Duas formas de construirQuando usar o Agent SDK e quando escrever o laço sobre a API
3. O que o SDK entrega e o que continua seuQuais peças vêm prontas e quais você ainda precisa projetar
4. Usando o Agent SDKComo a entrada query() e as opções se encaixam no laço
5. Laço próprio sobre a APIComo chamar o modelo, executar ferramentas e parar pelo stop_reason
6. HarnessA estrutura em volta do laço que controla contexto, ferramentas e limites
7. Hooks para ações determinísticasComo impedir uma ação com código, mesmo quando o modelo insiste
8. Erros comuns e boas práticasArmadilhas recorrentes e o motivo de cada regra
9. Prática, glossário e verificaçãoExercícios, termos e o que foi e o que não foi executado

1. O problema que o laço resolve

Um modelo sozinho recebe texto e devolve texto. Ele não abre arquivo, não consulta um banco e não roda um teste. Quando a tarefa exige agir, alguém precisa fazer a ação, e esse alguém é o seu código. O modelo fica no papel de quem pede: escreve qual ferramenta quer e com quais argumentos, e a sua aplicação executa e devolve o resultado.

Uma chamada só não basta quando você não sabe de antemão quantos passos a tarefa exige. Pense em “corrija o teste que está falhando”. O modelo precisa rodar o teste, ler a falha, abrir o arquivo, editar e rodar de novo. Cada passo depende do anterior, então o número de voltas só aparece durante a execução. O laço é o mecanismo que repete o ciclo “o modelo pede, o código executa, o resultado volta” até o modelo considerar que terminou ou até alguma regra mandar parar.

Repare que o laço tem duas perguntas: quem executa e quem decide que acabou. O código executa as ferramentas. O modelo sinaliza que quer parar, por meio do stop_reason da resposta. Mas quem decide se essa parada é aceitável é você, e a seção 6 mostra como.

2. Duas formas de construir

Há dois caminhos para ter um agente com Claude, e a escolha depende do quanto você quer escrever e do quanto quer controlar.

O primeiro é o Claude Agent SDK. Ele é uma biblioteca em Python e em TypeScript que embute o mesmo laço, as mesmas ferramentas e a mesma gestão de contexto que o Claude Code usa. Por baixo, o pacote inclui um binário do Claude Code, que roda como processo separado e conversa com a sua aplicação. Você ganha um agente funcional em poucas linhas, com leitura e escrita de arquivos, execução de comandos e sessões. Em troca, aceita o comportamento desse agente e o modelo de ferramentas que ele traz.

O segundo é escrever o laço você mesmo sobre a API de mensagens, com o SDK anthropic. Você chama messages.create, lê os blocos da resposta, executa as ferramentas que você definiu e devolve os resultados. Tudo fica no seu processo, sem binário extra, e cada requisição é uma decisão sua. O custo é escrever tudo: o laço, a validação, os limites, a persistência e os controles.

Uma regra prática: se o agente precisa de ferramentas genéricas de arquivo e terminal dentro da sua aplicação, o SDK poupa trabalho. Se as ferramentas são todas do seu domínio, a aplicação tem requisitos de latência ou de auditoria que pedem controle fino de cada chamada, ou você não pode carregar um binário, o laço próprio costuma ser a escolha mais limpa. Muitas equipes começam com o SDK para validar a ideia e migram o laço quando precisam de controle que o SDK não expõe.

3. O que o SDK entrega e o que continua seu

O Agent SDK entrega, pronto, o laço do agente. Cada volta do laço é um turno: o modelo produz texto e, se quiser, pedidos de ferramenta; o SDK executa as ferramentas, devolve os resultados e repete até a resposta vir sem pedido. Você não escreve esse loop.

Entrega também um conjunto de ferramentas embutidas que são as mesmas do Claude Code: Read, Write, Edit, Glob, Grep, Bash, WebSearch, WebFetch, além de Agent para criar subagents e Skill para carregar instruções reutilizáveis. Entrega sessões, que guardam o contexto entre execuções e permitem retomar ou bifurcar uma conversa pelo identificador de sessão. Entrega um sistema de permissões, em que você decide quais ferramentas rodam sem perguntar, quais são proibidas e em qual modo o agente opera. E entrega hooks, subagents e a conexão com servidores MCP, assunto da nota MCP.

O que continua com você é tão importante quanto isso, e a figura abaixo separa as duas colunas. O objetivo e o prompt continuam sendo seus: o SDK executa o que você pede, mas não sabe o que é proibido no seu negócio a menos que você diga. As ferramentas específicas do seu domínio, como consultar um pedido num sistema interno, precisam ser expostas por você, normalmente como um servidor MCP ou um callback. As regras de quem pode fazer o quê no seu sistema também são suas, e é nelas que os hooks entram. Os limites de voltas e de custo você define nas opções. E a verificação do resultado, isto é, se a tarefa de fato terminou, depende de um critério seu.

2 01 quem faz o que
figura 1 · 2 01 quem faz o que

A figura mostra o SDK à esquerda e o que continua com você à direita. As setas ligam as peças que se encontram na prática: o objetivo entra no laço, as ferramentas de negócio se somam às embutidas, e uma regra do seu domínio vira um hook dentro das permissões. Repare que o limite de custo aponta para o laço em vez de ficar no mesmo grupo. Ele é uma restrição de fora sobre um processo que o SDK conduz, e não parte do que o SDK entrega.

Uma palavra sobre a escolha do nome. Neste texto, “harness” é a estrutura em volta do laço que controla contexto, ferramentas e limites. A seção 6 define isso com mais cuidado. A documentação do Agent SDK usa a mesma palavra para falar de um desenho semelhante.

4. Usando o Agent SDK

A entrada mais simples do Agent SDK em Python é a função query(). Ela recebe um prompt e um objeto ClaudeAgentOptions, e devolve um iterador assíncrono de mensagens. Cada mensagem tem um tipo: SystemMessage para eventos de sessão (o primeiro tem o subtipo init), AssistantMessage para cada bloco que o modelo produz, UserMessage para os resultados das ferramentas e ResultMessage para o fim do laço. O código abaixo mostra a forma geral. Ele não foi executado contra a API, porque exige a chave e o binário do Claude Code, e serve para ver a estrutura.

import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage


async def main():
    opcoes = ClaudeAgentOptions(
        allowed_tools=["Read", "Grep", "Glob"],  # rodam sem pedir permissão
        permission_mode="default",
        max_turns=20,           # teto de voltas com ferramentas
        max_budget_usd=0.50,    # teto de gasto estimado
    )
    try:
        async for mensagem in query(prompt="Liste os arquivos com TODO", options=opcoes):
            if isinstance(mensagem, ResultMessage):
                if mensagem.subtype == "success":
                    print(mensagem.result)
                else:
                    print("parou antes do fim:", mensagem.subtype)
    except Exception as erro:
        # Em query() avulsa, um resultado de erro também levanta exceção depois de ser emitido.
        print("sessão terminou com erro:", erro)


asyncio.run(main())

Os campos de ClaudeAgentOptions usados acima existem no pacote claude-agent-sdk na versão 0.2.168, instalada em 18 de outubro de 2026 com pip, e a documentação oficial descreve o mesmo comportamento. Outros campos importantes são disallowed_tools, system_prompt, mcp_servers, hooks, model, cwd, can_use_tool e agents. O modo de permissão aceita default, acceptEdits, plan, dontAsk, bypassPermissions e auto. Cada modo muda o que roda sem perguntar, e vale a pena lê-los na documentação antes de escolher um para um agente que age sozinho.

O resultado final vem no ResultMessage, e o campo subtype diz como o laço terminou. O valor success indica que a tarefa acabou normalmente, e só nesse caso o campo result traz o texto final. Os outros valores são de parada forçada: error_max_turns quando o teto de voltas foi atingido, error_max_budget_usd quando o orçamento acabou, error_during_execution quando algo interrompeu o laço e error_max_structured_output_retries quando a saída estruturada não passou na validação dentro do limite de tentativas. Em todos os casos, o resultado carrega o custo e o identificador da sessão, o que permite retomar depois de um teto.

Quando você quer uma conversa com várias trocas, a alternativa é ClaudeSDKClient, que mantém a sessão entre chamadas a query. Ele é útil para interfaces de chat em que o usuário faz perguntas sucessivas sobre o mesmo contexto.

Um detalhe que a documentação deixa claro: os subagents criados pelo SDK começam com uma conversa nova. Eles não veem o histórico do agente que os criou e devolvem apenas a resposta final. Isso protege o contexto principal, mas significa que o brief da delegação precisa conter tudo o que o subagent deve saber, o mesmo cuidado que a nota Agents descreve para o Claude Code.

5. Laço próprio sobre a API

Agora o caminho sem o SDK de agente. O laço próprio tem quatro gestos, e cada um corresponde a uma linha do código abaixo. Primeiro, chamar o modelo com a lista de ferramentas e o histórico de mensagens. Segundo, ler a resposta e executar cada bloco tool_use que ela contém. Terceiro, devolver todos os resultados numa única mensagem de usuário, com o tool_use_id de cada um, e com os blocos tool_result antes de qualquer texto, como a documentação exige. Quarto, decidir se repete ou para, olhando o stop_reason.

O stop_reason é o campo que diz por que o modelo parou de gerar. Os valores que o SDK Python anthropic na versão 1.15.0 declara são sete, e a tabela mostra o que o seu laço deve fazer com cada um. A coluna da direita é a regra que o código da aula segue.

stop_reasonO que significaO que o laço faz
end_turnO modelo terminou a respostaDevolve o texto e para
tool_useO modelo pediu uma ou mais ferramentasExecuta cada pedido, devolve os resultados e repete
max_tokensA resposta bateu o limite de tokens de saídaPara sem executar nada, porque um pedido pode ter sido cortado no meio do JSON
stop_sequenceSaiu uma sequência de parada definida por vocêTrata como fim normal, se você definiu a sequência
refusalO modelo recusou a requisiçãoPara e informa, sem tentar de novo com o mesmo pedido
pause_turnO laço do lado do servidor atingiu o limite de iteraçõesSó aparece com ferramentas executadas pela Anthropic, como a busca na web. Devolva o conteúdo do assistente para continuar
model_context_window_exceededA resposta encheu a janela de contextoTrata a resposta como truncada

Um valor que não está nessa lista deve fazer o laço falhar alto. Um else silencioso que trata qualquer coisa como fim normal esconde mudanças futuras da API. O código da aula lança um erro quando recebe um valor fora das regras que conhece.

2 02 laco proprio
figura 2 · 2 02 laco proprio

A figura desenha o laço da seção como um fluxo. Ele começa na pergunta e passa por uma guarda, o losango “Dentro do limite?”, antes de cada chamada ao modelo. Essa guarda é parte do harness e não do laço: ela corta a volta quando o contador de voltas ou o orçamento de tokens estoura, e a seta vermelha de “não” leva ao aviso de parada. Depois do messages.create, o losango stop_reason separa os ramos. A saída verde end_turn é a única parada feliz. O ramo tool_use passa pelo hook, que decide entre executar (allow) e bloquear (deny, em vermelho, com a seta barrada). Os dois caminhos de ferramenta voltam à guarda com o tool_result, e é essa volta que fecha o laço. Os ramos vermelhos de max_tokens e de valor desconhecido mostram os casos em que o laço para sem executar nada.

O código abaixo implementa o laço completo com harness. Ele usa um cliente simulado com respostas roteirizadas, para rodar sem chave e sem rede. A linha comentada cliente = anthropic.Anthropic() seria o cliente real, lendo ANTHROPIC_API_KEY, e o restante do código não muda. O identificador claude-haiku-5-5 é o ID da API listado na página de modelos da Anthropic. O script usa só a biblioteca padrão.

"""Laço próprio sobre a API de mensagens, com um harness em volta. Roda com cliente simulado."""
import re
from dataclasses import dataclass, field
from types import SimpleNamespace

MODELO = "claude-haiku-5-5"  # identificador confirmado na pagina de modelos
MAX_VOLTAS = 5
ORCAMENTO_TOKENS = 4000

FERRAMENTAS = [
    {
        "name": "buscar_pedido",
        "description": "Consulta o status de um pedido pelo número. Não altera nada.",
        "input_schema": {
            "type": "object",
            "properties": {"numero": {"type": "string", "description": "Formato A-1234."}},
            "required": ["numero"],
            "additionalProperties": False,
        },
    },
    {
        "name": "apagar_pedido",
        "description": "Apaga um pedido do sistema. A ação é irreversível.",
        "input_schema": {
            "type": "object",
            "properties": {"numero": {"type": "string", "description": "Formato A-1234."}},
            "required": ["numero"],
            "additionalProperties": False,
        },
    },
]

PEDIDOS = {"A-1001": {"status": "enviado", "previsao": "2026-10-20"}}


def _buscar(args):
    return PEDIDOS[args["numero"]]


def _apagar(args):
    PEDIDOS.pop(args["numero"])
    return {"apagado": args["numero"]}


EXECUTORES = {"buscar_pedido": _buscar, "apagar_pedido": _apagar}


def regra_sem_apagar(nome, entrada):
    """Hook: roda em codigo antes da execucao. Devolve None para permitir ou o motivo para bloquear."""
    if nome == "apagar_pedido":
        return "bloqueado pelo harness: apagar pedido exige confirmação humana"
    return None


HOOKS_ANTES = [regra_sem_apagar]


def validar(entrada):
    numero = entrada.get("numero") if isinstance(entrada, dict) else None
    if set(entrada) != {"numero"} or not isinstance(numero, str) or not re.fullmatch(r"A-\d{4}", numero):
        raise ValueError(f"entrada inválida: {entrada!r}. Use {{'numero': 'A-1234'}}")
    return {"numero": numero}


def executar(bloco):
    """Transforma qualquer desfecho em tool_result, para que o tool_use nunca fique sem resposta."""
    for hook in HOOKS_ANTES:
        motivo = hook(bloco.name, bloco.input)
        if motivo:
            return {"type": "tool_result", "tool_use_id": bloco.id, "content": motivo, "is_error": True}
    try:
        if bloco.name not in EXECUTORES:
            raise LookupError(f"ferramenta desconhecida: {bloco.name}")
        resultado = EXECUTORES[bloco.name](validar(bloco.input))
        return {"type": "tool_result", "tool_use_id": bloco.id, "content": str(resultado)}
    except (ValueError, LookupError, KeyError) as erro:
        return {"type": "tool_result", "tool_use_id": bloco.id, "content": f"Erro: {erro}", "is_error": True}


@dataclass
class Resultado:
    texto: str = ""
    motivo_parada: str = ""
    voltas: int = 0
    tokens: int = 0
    ferramentas_pedidas: list = field(default_factory=list)
    resultados: list = field(default_factory=list)


def rodar(cliente, pergunta, max_voltas=MAX_VOLTAS, orcamento=ORCAMENTO_TOKENS):
    r = Resultado()
    mensagens = [{"role": "user", "content": pergunta}]
    while True:
        if r.voltas >= max_voltas:
            r.motivo_parada = "limite de voltas do harness"
            return r
        if r.tokens >= orcamento:
            r.motivo_parada = "orçamento de tokens do harness"
            return r

        resp = cliente.messages.create(model=MODELO, max_tokens=512, tools=FERRAMENTAS, messages=mensagens)
        r.voltas += 1
        r.tokens += resp.usage.input_tokens + resp.usage.output_tokens
        mensagens.append({"role": "assistant", "content": resp.content})

        parada = resp.stop_reason
        if parada == "end_turn":
            r.texto = "".join(b.text for b in resp.content if b.type == "text")
            r.motivo_parada = "end_turn"
            return r
        if parada == "tool_use":
            pedidos = [b for b in resp.content if b.type == "tool_use"]
            for b in pedidos:
                r.ferramentas_pedidas.append(b.name)
            devolucao = [executar(b) for b in pedidos]
            r.resultados.extend(d["content"] for d in devolucao)
            mensagens.append({"role": "user", "content": devolucao})
            continue
        if parada == "max_tokens":
            # A ferramenta pode ter sido cortada no meio do JSON: nada e executado.
            r.motivo_parada = "max_tokens (resposta cortada, nenhuma ferramenta executada)"
            return r
        if parada == "refusal":
            r.motivo_parada = "refusal"
            return r
        # pause_turn so aparece com ferramentas do servidor. Qualquer valor novo falha alto.
        raise RuntimeError(f"stop_reason não tratado: {parada!r}")


class ClienteSimulado:
    """Imita client.messages.create devolvendo respostas roteirizadas. Não toca na rede."""

    def __init__(self, roteiro):
        self._roteiro = list(roteiro)
        self.messages = self

    def create(self, **kwargs):
        return self._roteiro.pop(0)


def texto(t):
    return SimpleNamespace(type="text", text=t)


def pedido(id_, nome, entrada):
    return SimpleNamespace(type="tool_use", id=id_, name=nome, input=entrada)


def resposta(stop, *blocos, entrada=120, saida=40):
    uso = SimpleNamespace(input_tokens=entrada, output_tokens=saida)
    return SimpleNamespace(stop_reason=stop, content=list(blocos), usage=uso)


def mostrar(titulo, r):
    print(f"== {titulo}")
    print(f"  motivo: {r.motivo_parada}")
    print(f"  voltas: {r.voltas}  tokens: {r.tokens}  pedidas: {r.ferramentas_pedidas}")
    for conteudo in r.resultados[:2]:
        print(f"  tool_result: {conteudo}")
    if r.texto:
        print(f"  texto: {r.texto}")
    print()


if __name__ == "__main__":
    # Na aplicação real: cliente = anthropic.Anthropic() (lê ANTHROPIC_API_KEY).
    c1 = ClienteSimulado([
        resposta("tool_use", texto("Vou consultar."), pedido("toolu_01", "buscar_pedido", {"numero": "A-1001"})),
        resposta("end_turn", texto("O A-1001 foi enviado e chega em 20/10/2026.")),
    ])
    mostrar("1. caminho feliz", rodar(c1, "Onde está o A-1001?"))

    c2 = ClienteSimulado([
        resposta("tool_use", pedido("toolu_02", "apagar_pedido", {"numero": "A-1001"})),
        resposta("end_turn", texto("Não consigo apagar pedidos por aqui.")),
    ])
    r2 = rodar(c2, "Apague o A-1001.")
    mostrar("2. modelo pede ação proibida, hook bloqueia", r2)
    print("  pedido A-1001 ainda existe:", "A-1001" in PEDIDOS)
    print()

    c3 = ClienteSimulado([
        resposta("max_tokens", pedido("toolu_03", "apagar_pedido", {"numero": "A-1001"})),
    ])
    mostrar("3. resposta cortada no meio de um tool_use", rodar(c3, "Apague o A-1001."))
    print("  pedido A-1001 ainda existe:", "A-1001" in PEDIDOS)
    print()

    c4 = ClienteSimulado([
        resposta("tool_use", pedido(f"toolu_04{i}", "buscar_pedido", {"numero": "A-1001"}))
        for i in range(10)
    ])
    mostrar("4. modelo gira sem parar, limite de voltas", rodar(c4, "Fique consultando."))

    c5 = ClienteSimulado([
        resposta("tool_use", pedido("toolu_05", "buscar_pedido", {"numero": "A-1001"}), entrada=3000, saida=1500),
        resposta("end_turn", texto("nunca chega aqui")),
    ])
    mostrar("5. orçamento de tokens estoura antes da segunda chamada", rodar(c5, "Consulte.", orcamento=4000))

    c6 = ClienteSimulado([resposta("pause_turn", texto("..."))])
    try:
        rodar(c6, "Pesquise algo.")
    except RuntimeError as erro:
        print("== 6. stop_reason fora da lista tratada")
        print(f"  erro: {erro}")

A saída real da execução, sem edição, é esta:

== 1. caminho feliz
  motivo: end_turn
  voltas: 2  tokens: 320  pedidas: ['buscar_pedido']
  tool_result: {'status': 'enviado', 'previsao': '2026-10-20'}
  texto: O A-1001 foi enviado e chega em 20/10/2026.

== 2. modelo pede ação proibida, hook bloqueia
  motivo: end_turn
  voltas: 2  tokens: 320  pedidas: ['apagar_pedido']
  tool_result: bloqueado pelo harness: apagar pedido exige confirmação humana
  texto: Não consigo apagar pedidos por aqui.

  pedido A-1001 ainda existe: True

== 3. resposta cortada no meio de um tool_use
  motivo: max_tokens (resposta cortada, nenhuma ferramenta executada)
  voltas: 1  tokens: 160  pedidas: []

  pedido A-1001 ainda existe: True

== 4. modelo gira sem parar, limite de voltas
  motivo: limite de voltas do harness
  voltas: 5  tokens: 800  pedidas: ['buscar_pedido', 'buscar_pedido', 'buscar_pedido', 'buscar_pedido', 'buscar_pedido']
  tool_result: {'status': 'enviado', 'previsao': '2026-10-20'}
  tool_result: {'status': 'enviado', 'previsao': '2026-10-20'}

== 5. orçamento de tokens estoura antes da segunda chamada
  motivo: orçamento de tokens do harness
  voltas: 1  tokens: 4500  pedidas: ['buscar_pedido']
  tool_result: {'status': 'enviado', 'previsao': '2026-10-20'}

== 6. stop_reason fora da lista tratada
  erro: stop_reason não tratado: 'pause_turn'

Cada cenário mostra uma parte do desenho. No primeiro, o tool_result chega com o mesmo id do pedido e a resposta final só aparece depois dele. No segundo, o modelo pede apagar_pedido, o hook recusa antes de qualquer execução, o pedido continua existindo, e o modelo recebe a recusa como is_error para ajustar a resposta. O terceiro é o caso que quase ninguém testa: a resposta foi cortada num tool_use, e o laço não executa a ferramenta, porque o JSON dos argumentos pode estar incompleto. No quarto, o modelo insiste em consultar, e o teto de voltas do harness para o laço, em vez de deixá-lo girar. O quinto mostra o orçamento de tokens segurando a segunda chamada, que nunca acontece. No sexto, um stop_reason que o código não conhece para o laço com um erro explícito.

Repare também no que o script não prova. O modelo do roteiro não decidiu nada. Eu escrevi cada resposta e a entreguei na ordem certa, então a saída mostra como o laço reage a respostas dadas, e não como um modelo real se comportaria. A seção 9 lista o que falta verificar contra a API.

A figura mostra o laço pelo lado da chamada: o código envia a requisição, lê o stop_reason da resposta e decide o próximo passo. Ela parte da mesma mensagem que 1.3 Mecânica da API Claude descreve.

chamada e stop reason
figura 3 · chamada e stop reason

6. Harness

Harness é o nome que esta aula dá à estrutura em volta do laço. O laço em si é curto: chamar, verificar o stop_reason, executar, repetir. Todo o resto do código acima é harness, e ele responde a três perguntas que o laço sozinho não responde.

A primeira é o que entra no contexto. O modelo só sabe o que chega na requisição: o prompt de sistema, a lista de ferramentas, o histórico e os resultados. Quem monta essa mensagem é o harness. Num laço longo, o histórico cresce a cada volta, e a conta de tokens sobe junto. No código da aula, o harness mede tokens por volta e para quando o orçamento acaba. O Agent SDK faz algo parecido, mas com compactação automática: quando o contexto se aproxima do limite, ele resume as mensagens antigas e emite um SystemMessage com subtipo compact_boundary. Instruções que precisam sobreviver a essa compactação devem estar no CLAUDE.md do projeto, e não só no prompt inicial, porque a documentação diz que o CLAUDE.md é reinjetado a cada requisição.

A segunda pergunta é quais ferramentas o modelo pode pedir, e o que acontece com cada pedido. Aqui o harness faz três coisas: restringe a lista de ferramentas que vai na requisição, valida os argumentos antes de executar e transforma qualquer desfecho em um tool_result, mesmo uma exceção. A regra de transformar tudo em resultado existe porque um tool_use sem resposta quebra a conversa na volta seguinte. A nota 4.1 Implementação de ferramentas detalha esse ponto.

A terceira pergunta é quando parar além do end_turn. São quatro freios, cada um cobrindo uma falha diferente. O limite de voltas corta um laço que não progride. O orçamento de tokens ou de dinheiro corta um passo caro, mesmo dentro do limite de voltas. A confirmação humana cobre ações irreversíveis, porque um apagamento custa quase nada em tokens e um teto de custo não o impede. E o tratamento explícito de stop_reason cobre o caso em que a API muda de comportamento. O Agent SDK traz os dois primeiros como opções, max_turns e max_budget_usd, e os outros dois ficam com você.

Um cuidado do Agent SDK: o limite de voltas conta apenas as voltas com ferramentas, e a documentação alerta que hooks podem não disparar quando o laço atinge esse limite, porque a sessão termina antes. Uma regra que precisa valer até o fim não pode depender só de um hook nesse ponto.

7. Hooks para ações determinísticas

Um hook é uma função que o harness chama num ponto fixo do ciclo, por exemplo antes de uma ferramenta rodar. A diferença para uma instrução no prompt está em quem aplica a regra. Um prompt que diz “nunca apague pedidos” é um pedido ao modelo, que pode segui-lo na maioria das vezes e falhar em outras. Um hook é código: roda sempre que o evento acontece, não importa o que o modelo pensou, nem o que um texto lido de um arquivo tenha convencido o modelo a fazer. A figura compara as duas formas com o mesmo caso.

2 03 hook deterministico
figura 4 · 2 03 hook deterministico

A figura tem um caminho só, da esquerda para a direita. O prompt entra com linha pontilhada porque apenas orienta o modelo, e a seta sólida mostra o pedido de ferramenta, que sai do modelo com o tool_use. O harness dispara o hook antes da execução, e o losango é a regra em código, que decide entre bloquear e executar. A seta vermelha de bloqueio carrega o tool_result com is_error de volta ao modelo, que recebe a recusa como informação. A saída verde só existe quando o hook permite. Se você quiser ver o que o modelo tentou, a própria execução do Bash aparece à direita, depois do ramo que a regra liberou.

No Agent SDK, um hook é registrado nas opções, num dicionário indexado pelo nome do evento. Cada evento recebe uma lista de HookMatcher, e cada matcher tem um filtro opcional e uma ou mais funções de callback. O campo matcher filtra pelo nome da ferramenta, e não pelos argumentos. Para filtrar por caminho de arquivo ou por comando, a própria função precisa olhar tool_input. O callback recebe três argumentos: os dados do evento (input_data), o identificador da chamada de ferramenta (tool_use_id) e um contexto que, em Python, é reservado para uso futuro. Para bloquear, o callback devolve um objeto com hookSpecificOutput contendo permissionDecision igual a deny e uma permissionDecisionReason. Devolver {} permite a operação sem alteração.

O código abaixo define um callback que nega rm no Bash. Ele não foi acionado por um agente real, porque isso exige o binário do Claude Code e uma chave. Em vez disso, o callback é chamado diretamente com uma entrada simulada, e as opções são construídas para confirmar que o registro do hook aceita a forma documentada.

import asyncio
from claude_agent_sdk import ClaudeAgentOptions, HookMatcher


async def bloqueia_rm(input_data, tool_use_id, context):
    if input_data["hook_event_name"] != "PreToolUse":
        return {}
    comando = input_data["tool_input"].get("command", "")
    if comando.lstrip().startswith("rm "):
        return {
            "hookSpecificOutput": {
                "hookEventName": input_data["hook_event_name"],
                "permissionDecision": "deny",
                "permissionDecisionReason": "rm nao permitido neste agente",
            }
        }
    return {}


opcoes = ClaudeAgentOptions(
    allowed_tools=["Read", "Grep", "Glob", "Bash"],
    permission_mode="default",
    max_turns=20,
    max_budget_usd=0.50,
    hooks={"PreToolUse": [HookMatcher(matcher="Bash", hooks=[bloqueia_rm], timeout=30)]},
)
print("opcoes criadas, eventos de hook:", list(opcoes.hooks), "max_turns:", opcoes.max_turns)


async def main():
    entradas = [
        {"hook_event_name": "PreToolUse", "tool_name": "Bash", "tool_input": {"command": "rm -rf build"}},
        {"hook_event_name": "PreToolUse", "tool_name": "Bash", "tool_input": {"command": "ls -la"}},
    ]
    for entrada in entradas:
        decisao = await bloqueia_rm(entrada, "toolu_test", None)
        print(entrada["tool_input"]["command"], "->", decisao or "sem decisao (permite)")


asyncio.run(main())

Saída real, sem edição:

opcoes criadas, eventos de hook: ['PreToolUse'] max_turns: 20
rm -rf build -> {'hookSpecificOutput': {'hookEventName': 'PreToolUse', 'permissionDecision': 'deny', 'permissionDecisionReason': 'rm nao permitido neste agente'}}
ls -la -> sem decisao (permite)

Três detalhes sobre hooks merecem atenção. O primeiro é a ordem de decisão quando há vários hooks para o mesmo evento: a documentação diz que deny tem prioridade sobre defer, que tem prioridade sobre ask, que tem prioridade sobre allow. Ou seja, um único hook que nega vence qualquer número de hooks que permitem, e isso é o que você quer de uma trava. O segundo é o tempo: cada callback tem um limite, configurável por timeout no matcher, e o padrão da maioria dos eventos é de 600 segundos. Quando um PreToolUse estoura o tempo, a ferramenta é bloqueada e o modelo recebe um resultado dizendo que o hook não respondeu. O terceiro é o escopo. O matcher Bash não vê um arquivo escrito por outro meio. A documentação dá o exemplo de um matcher Write|Edit que não impede um echo com redirecionamento executado pelo Bash. Para bloquear escrita por comando, é preciso combinar o hook com a configuração de sandbox.

Vale distinguir esse mecanismo do que a nota Agents descreve para os hooks de configuração do Claude Code. Nos hooks de comando, configurados em arquivos de settings, a ação é bloqueada com código de saída 2, como mostra o exemplo daquela nota. Nos callbacks do SDK, a decisão vai no valor devolvido, em JSON. São dois canais para a mesma ideia, e o código de saída não vale para callbacks Python. Como a documentação não detalhou aqui o comportamento de código de saída para cada tipo de hook, trate essa diferença como algo a conferir antes de depender dela em produção.

8. Erros comuns e boas práticas

O erro mais frequente é deixar uma exceção escapar da execução de uma ferramenta. O modelo nunca recebe a falha, e a conversa fica com um pedido sem resposta. A regra do código da aula é que toda execução termine num tool_result, mesmo quando é um erro. No Agent SDK, a documentação recomenda o mesmo dentro de hooks: capture os erros do callback em vez de propagá-los, porque uma exceção não tratada interrompe a chamada que estava sendo analisada.

O segundo erro é tratar uma regra crítica como se fosse preferência. “Nunca apague pedidos” no prompt é uma tentativa. Uma regra de negócio que precisa valer sempre vai no harness, num hook ou numa regra de negação nas permissões, porque é o único lugar onde ela não depende da obediência do modelo. A figura da seção 7 mostra essa distinção.

O terceiro erro é colocar texto antes dos resultados na mensagem de usuário que devolve as ferramentas. A API responde com erro 400, e a origem costuma ser um sistema que quer acrescentar um lembrete antes dos resultados. O lembrete pode ir depois, nunca antes.

O quarto erro é tratar stop_reason como uma lista de dois valores. O laço que só conhece end_turn e tool_use quebra silenciosamente quando aparece max_tokens, e quebra com erro quando aparece pause_turn. Trate cada valor de forma explícita e deixe o desconhecido falhar alto.

O quinto erro é executar em paralelo ferramentas que escrevem. O Agent SDK roda ferramentas somente leitura em paralelo e executa Edit, Write e Bash em sequência, porque a segunda pode depender do efeito da primeira. Se você implementa o próprio laço e decide paralelizar, mantenha essa regra: leituras podem andar juntas, escritas não.

O sexto erro é o laço sem teto. Um modelo confuso pode pedir a mesma ferramenta indefinidamente, e cada volta custa tokens. Use max_turns no SDK e um contador no laço próprio, e defina um orçamento em dinheiro ou tokens. Quando o teto estoura, a resposta deve dizer que a tarefa não terminou, em vez de falhar em silêncio.

O sétimo erro é achar que validar o formato basta. Um número de pedido pode estar no formato certo e pertencer a outro cliente. A checagem de quem pode o quê deve usar a identidade vinda da sessão do usuário, nunca um campo do input, porque esse campo também é escrito pelo modelo.

9. Prática, glossário e verificação

Três exercícios, em ordem. No primeiro, acrescente ao laço da seção 5 uma ferramenta cancelar_pedido com descrição que diga quando usá-la e quando não usá-la, e decida se ela precisa de um hook de confirmação. No segundo, escreva um hook no SDK que registre em log cada chamada de Bash e negue qualquer comando que contenha curl, e teste o callback com entradas simuladas como fizemos aqui. No terceiro, troque o max_turns do laço da seção 5 por um teste que mede quantas voltas uma tarefa de três consultas exige, e registre o resultado antes de escolher um valor.

Glossário.

Agent SDK: biblioteca oficial, em Python e TypeScript, que embute o laço, as ferramentas e as sessões do Claude Code na sua aplicação.

ClaudeAgentOptions: classe de configuração passada ao SDK, com campos como allowed_tools, max_turns, max_budget_usd e hooks.

query: função do Agent SDK que executa uma sessão e devolve um iterador de mensagens. Para várias trocas na mesma sessão, use ClaudeSDKClient.

ResultMessage: mensagem final de uma sessão. O campo subtype diz se terminou com success ou por um teto.

HookMatcher: objeto que associa um filtro opcional e uma lista de callbacks a um evento de hook.

Hook: função de código chamada em um ponto fixo do ciclo do agente, capaz de permitir ou negar uma ação.

Harness: a estrutura em volta do laço que controla contexto, ferramentas e limites. Neste texto, é o nome dado ao que o código da seção 5 faz além de chamar a API.

stop_reason: campo da resposta da API que informa por que o modelo parou de gerar. Os valores estão na tabela da seção 5.

tool_result: bloco de mensagem de usuário que devolve o resultado de uma ferramenta, ligado ao pedido pelo tool_use_id.

max_turns: teto de voltas com ferramentas em uma sessão do Agent SDK.

max_budget_usd: teto de gasto estimado em dólares para uma sessão do Agent SDK. Inclui o gasto dos subagents.

Verificação. Os nomes de classes, funções, campos de opções, subtipos de resultado, modos de permissão, eventos de hook e valores de stop_reason foram conferidos de duas formas: contra o pacote claude-agent-sdk na versão 0.2.168 e o pacote anthropic na versão 1.15.0, ambos instalados com pip em 18 de outubro de 2026, e contra a documentação oficial consultada na mesma data. Os dois scripts rodaram localmente. O laço usa um cliente simulado, e o callback de hook foi chamado diretamente com uma entrada simulada. Nada foi executado contra a API real nem contra o binário do Claude Code, então não sei, por esta execução, se um modelo real escolhe os pedidos que o roteiro sugere, nem o formato exato das mensagens que o SDK emite em tempo de execução. A lista de eventos de hook foi tirada da documentação, porque o pacote instalado não exporta o tipo que a enumera. A página Python do Agent SDK veio truncada na leitura, então os campos foram conferidos no pacote instalado e não no texto completo da página. O comportamento de código de saída para hooks de comando de settings veio da nota Agents, e não foi relido nesta aula.

Relacionados

Fontes

Consultadas em 18 de outubro de 2026. As páginas do Agent SDK redirecionam de platform.claude.com para code.claude.com, e a leitura seguiu o redirecionamento.

↑ Voltar ao topo

Atalhos de teclado

Abrir busca
/
Alternar tema claro/escuro
t
Aula anterior na série
←
Próxima aula na série
→
Lista de atalhos
?