Agent Mentor Learn
Gerenciamento e persistência de estado: fazendo tarefas longas sobreviverem à interrupção · Lição 4 de 6

Lição 4: Efeitos colaterais e idempotência: quais ferramentas são seguras de reexecutar na retomada

Objetivos de aprendizado:

  • Explicar por que repetir a execução na retomada entrega, por padrão, semântica de execução at-least-once, nunca exactly-once
  • Julgar se uma operação de ferramenta é idempotente e identificar os efeitos colaterais que causam dano de verdade no instante em que rodam duas vezes
  • Projetar e implementar um livro-razão de efeitos indexado por tool_use_id, para que uma chamada órfã na retomada consulte o livro-razão antes de decidir se de fato executa

Pré-requisitos: você leu as Lições 2 e 3 e entende as regras de reconciliação para um pendingToolUse órfão no checkpoint.json (Lição 3); você conhece o conjunto de ferramentas HIGH_IMPACT e a válvula de aprovação pré-execução do Curso 7 desta série, "Agent Harness Fundamentals: Loops and Control" | Anterior: Lição 3 << | Próxima: Lição 5 >>

A retomada entrega at-least-once: a Lição 3 deixou as ferramentas de alto impacto sem solução

A Lição 3 ensinou você a ler o pendingToolUse do checkpoint.json e usá-lo para trazer de volta ao loop uma chamada órfã — aquela em que a queda caiu entre a execução da ferramenta e a gravação no livro-razão. A regra de reconciliação de então era: ferramentas somente leitura simplesmente reexecutam e, para ferramentas de alto impacto sobre as quais você não consegue concluir nada, anexe um tool_result com is_error para que o loop deixe de ficar travado e a pergunta volte para uma pessoa. É um fallback honesto e é também um problema não resolvido. “Não dá para concluir” significa que a tarefa não consegue seguir sozinha, então toda queda em uma ferramenta de alto impacto precisa de alguém de olho.

A raiz disso é a seguinte: a retomada, por natureza, entrega semântica de execução at-least-once (pelo menos uma vez). O processo pode morrer depois que uma ferramenta de fato teve sucesso, mas antes de o resultado ser escrito de volta em messages ou confirmado em um checkpoint — e, nesse ponto, “esta ferramenta rodou ou não” é uma pergunta que o checkpoint.json sozinho não consegue responder. Para uma ferramenta somente leitura como read_file, não saber não custa nada; leia uma vez a mais e o resultado é o mesmo. Para send_email, create_ticket ou uma transferência de fundos, não saber é um incidente: reexecutar significa que o destinatário pode receber dois e-mails idênticos e um chamado duplicado pode aparecer no sistema do nada.

Isso não é um problema novo. Antes neste curso estabelecemos que agentes têm estado e que os erros se acumulam1 — e executar um efeito colateral duas vezes quando ele deveria acontecer uma é uma forma concreta que esse acúmulo assume. O erro não para em “rodamos uma vez a mais”; ele rola ladeira abaixo em cima daquele efeito colateral extra. Esta lição fecha a lacuna que a Lição 3 deixou aberta: apresentar a idempotência como conceito e depois parafusar um livro-razão de efeitos no loop de retomada, para que “não dá para concluir” vire “dá para concluir”.

O que idempotente significa: uma execução ou dez, mesmo efeito

Idempotente designa uma operação que produz o mesmo efeito final quer você a rode uma vez, quer a rode muitas. Repare que isso diz respeito ao efeito — o estado final que a operação deixa no mundo externo (arquivos, bancos de dados, caixas de entrada) —, não ao valor literal que cada chamada retorna.

Para julgar se uma ferramenta é idempotente, uma pergunta basta: “Se esta operação rodasse silenciosamente uma vez a mais, o mundo externo terminaria com alguma coisa a mais, ou em um estado diferente?” Passe estes pequenos exemplos por essa pergunta e a diferença aparece na hora.

readFileContent é idempotente por natureza porque não tem efeito colateral nenhum — não há nada “deixado para trás” de que se possa falar. setLine também é idempotente e de fato muda estado, mas o jeito como muda é por sobrescrita: chame uma vez e a linha 42 é X, chame dez vezes e a linha 42 continua sendo X. O estado final não varia com a contagem de chamadas. appendRow e sendEmail não são idempotentes, pelo mesmo motivo nos dois casos: o efeito deles é cumulativo — cada chamada de fato acrescenta mais uma coisa ao mundo externo, então a contagem de chamadas aparece diretamente no estado final.

Guarde essa linha divisória: escritas por sobrescrita costumam ser idempotentes, escritas por acréscimo costumam não ser; leituras e operações do tipo “confira primeiro, depois decida se age” costumam ser idempotentes, enquanto inserções incondicionais simples costumam não ser. O livro-razão de efeitos da próxima seção existe justamente para pegar as operações que não são idempotentes e que não dá para redesenhar até deixarem de ser.

O livro-razão de efeitos: anotar quais efeitos colaterais já aconteceram

A Lição 2 ensinou você a guardar a cena corrente do loop — messages, os contadores, a chamada de ferramenta ainda não gravada no livro-razão — em um checkpoint, para que uma queda possa ser retomada no mesmo lugar. Mas um checkpoint responde “a que etapa do loop chegamos”, não “o efeito colateral daquela etapa de fato aconteceu”. Durante a operação normal os dois andam quase em sincronia, mas no instante em que uma queda cai na fresta entre eles, eles discordam — que é exatamente por que a Lição 3 teve de deixar sem solução a reconciliação de alto impacto.

Fechar essa lacuna exige um livro-razão de efeitos: anotar quais efeitos colaterais já aconteceram, em disco, separado do checkpoint. A estrutura é simples — um mapa indexado por tool_use_id:

O momento da gravação no livro-razão importa muitíssimo: escreva no instante em que a função da ferramenta de fato tem sucesso e retorna um resultado, e escreva um tempo antes do checkpoint do “ponto B” da Lição 2 (a escrita de rotina que acontece depois que o resultado da ferramenta cai em messages). O motivo é direto. Se a queda cair dentro da janela estreita entre “a ferramenta teve sucesso” e “o checkpoint do ponto B terminou de escrever”, o checkpoint do ponto B nunca teve a chance de registrar que isso aconteceu, e na retomada a única coisa capaz de lhe contar a verdade é o livro-razão que terminou de escrever antes. A gravação no livro-razão, ela própria, também tem de ser atômica — .tmp + rename, como nas Lições 2 e 3 —, pelo mesmo motivo — um arquivo de livro-razão escrito pela metade é mais perigoso do que livro-razão nenhum, porque faz você acreditar em um efeito colateral que nunca chegou a se completar.

Com um livro-razão em mãos, a regra de reconciliação na retomada sobe do “não dá para concluir” da Lição 3 para “dá para concluir”. Pegue o pendingToolUse do checkpoint.json e procure o id dele no livro-razão. Acerto: o efeito colateral realmente aconteceu, então tire o result guardado do livro-razão, use-o para preencher um tool_result e nunca mais execute. Falha: esta chamada ou nunca começou, ou morreu no meio do caminho sem ter sucesso, então executar é seguro. A regra vale para toda ferramenta; acontece apenas que, para ferramentas idempotentes, a consulta dá no mesmo de qualquer jeito. O que de fato depende dela são as operações que causam dano quando se repetem.

Há aqui uma percepção que merece ser dita à parte: o tool_use_id já é uma chave de idempotência. Toda vez que o modelo nomeia uma ferramenta, ele carrega "A unique identifier for this particular tool use block"2 (um identificador único para este bloco de uso de ferramenta em particular) — essa é a definição literal do campo id na especificação oficial. Se essa mesma nomeação for vista uma segunda vez por causa de uma repetição na retomada, o id não muda. É precisamente isso que permite ao livro-razão reconhecer “esta chamada” e “aquela chamada anterior” como um único e mesmo evento, sem que você tenha de inventar um esquema de desduplicação próprio.

Duas válvulas em camadas: a aprovação pergunta “devemos?”, o livro-razão pergunta “já fizemos?”

O Curso 7 desta série, "Agent Harness Fundamentals: Loops and Control", equipou o runToolUses com uma válvula de aprovação: antes de uma ferramenta de alto impacto de fato rodar, imprima o que está prestes a acontecer, espere uma pessoa confirmar e só então deixe passar3. Essa válvula barra a pergunta “isto deve ser feito”. O livro-razão de efeitos desta lição barra outra pergunta: “isto já foi feito”. As duas válvulas perguntam coisas diferentes, mas ficam no mesmo lugar — ambas encaixadas no momento depois de o modelo ter nomeado uma ferramenta e antes de a ferramenta ter de fato rodado. Nenhuma das duas deixa a função da ferramenta executar antes de ter sido verificada.

Empilhe as duas e o runToolUses fica assim:

A ordem não é negociável: a válvula de idempotência tem de vir primeiro. O motivo é evidente — se esta chamada já está no livro-razão, perguntar “devemos fazer isto” depois não significa nada, porque já está feito, e perguntar de novo só confunde quem responde: o sistema claramente concluiu isto, então por que está me pedindo para confirmar? Para uma chamada órfã na retomada, a primeira pergunta é sempre “isto aconteceu” e só depois de resolvida essa é que “isto deve acontecer” tem a sua vez.

Idempotência na camada de projeto da ferramenta: corrija a causa, não fique só aparando as consequências

O livro-razão de efeitos é uma rede de proteção do lado do harness — tenha a ferramenta em si sido projetada para ser idempotente ou não, o livro-razão consegue barrar uma execução duplicada usando o tool_use_id. Mas uma rede de proteção continua sendo uma rede de proteção, e o investimento melhor é corrigir a causa: onde você puder mudar a ferramenta, projete-a para ser idempotente por natureza, para que o livro-razão nunca precise entrar em cena.

A versão mais comum dessa mudança é transformar “criar” em “garantir que existe”:

Chame ensureTicket uma vez ou dez e o sistema termina com exatamente um chamado correspondente àquele título — o estado final não varia com a contagem de chamadas, que é a definição de idempotente. O mesmo raciocínio vale para escrever arquivos: um write_file de arquivo inteiro é idempotente por natureza, e chamadas repetidas deixam para trás o mesmo conteúdo; um append_file de acréscimo não é, e o arquivo cresce uma seção por chamada. Quando você puder escolher sobrescrever, não escolha acrescentar.

Corrigir a causa e aparar as consequências não são uma escolha entre duas opções — são uma divisão de trabalho. Ferramentas que você consegue projetar para serem idempotentes deveriam ser resolvidas na camada da ferramenta, poupando toda chamada de um desvio pelo livro-razão. E para operações que genuinamente não podem ser “desduplicadas e fundidas” como questão de regra de negócio — duas transferências que realmente aconteceram em momentos diferentes, digamos, que devem ser reconhecidas como dois eventos distintos e não podem ser colapsadas em um por engenhosidade de projeto —, o livro-razão é a única rede de proteção que existe.

Voltando ao ponto de partida: mais uma proteção, e saber quando ela vale a pena

Este curso partiu do ponto de que a confiabilidade vem de emparelhar a adaptabilidade do modelo com salvaguardas determinísticas, como lógica de retentativa e checkpoints regulares1. O livro-razão de efeitos é uma dessas proteções. Ele não pede ao modelo que julgue “já fiz isto” — isso sempre esteve além do que o modelo consegue perceber. Ele faz o harness emitir esse julgamento em nome do modelo, usando evidência definitiva escrita em disco.

Mantenha também um senso de proporção. Se toda ferramenta que seu agente tem em mãos for somente leitura, o livro-razão desta lição provavelmente não vai justificar o seu lugar — um livro-razão de efeitos é, ele próprio, uma camada de complexidade, e o que torna sua adição válida é ele barrar de fato um risco real de efeitos colaterais duplicados; acrescente complexidade apenas quando isso comprovadamente melhorar os resultados3. O teste é o mesmo da lição anterior: olhe primeiro para o seu conjunto de ferramentas em busca de operações não idempotentes e de alto impacto. Se elas estiverem lá, vale instalar a válvula. Se não estiverem, não corra para escrevê-la.

Recapitulação

  • A retomada entrega semântica de execução at-least-once: o processo pode morrer depois que uma ferramenta de fato teve sucesso, mas antes de o resultado ser escrito no livro-razão, e o checkpoint sozinho não consegue dizer se aquela chamada órfã rodou. Essa é a raiz do porquê de a Lição 3 ter tido de deixar sem solução a reconciliação de alto impacto.
  • A definição de idempotente: uma operação que produz o mesmo efeito final quer rode uma vez, quer rode muitas. Escritas por sobrescrita (set_config, write_file) costumam ser idempotentes; escritas por acréscimo (append_log, send_email) costumam não ser.
  • O livro-razão de efeitos registra quais efeitos colaterais já aconteceram, em disco, indexados por tool_use_id — o identificador único que o modelo carrega ao nomear uma ferramenta2, que não muda quando a mesma nomeação é repetida na retomada, o que faz dele uma chave de idempotência por natureza. Escreva-o com a gravação atômica .tmp + rename, no instante em que a ferramenta tiver sucesso.
  • A regra de reconciliação na retomada sobe para: se pendingToolUse.id acertar no livro-razão, reuse o resultado guardado e nunca reexecute; se falhar, execute com segurança.
  • A válvula de aprovação pergunta “isto deve ser feito”, o livro-razão de efeitos pergunta “isto já foi feito”. Elas se complementam, ambas ficam diante da execução de fato, e a válvula de idempotência vem primeiro.
  • Corrigir a causa é melhor do que aparar as consequências: projete ferramentas para serem idempotentes por natureza (“garantir que existe” em vez de “criar”, sobrescrever em vez de acrescentar) e você não vai precisar do livro-razão para tudo. A confiabilidade vem da adaptabilidade do modelo emparelhada com salvaguardas determinísticas1 — mas uma salvaguarda também é complexidade, e ela só deveria ser acrescentada quando comprovadamente melhorar os resultados3.

>> Lição 5: Rebobinar e bifurcar: o segundo valor dos checkpoints

Footnotes

  1. How we built our multi-agent research system — Anthropic Engineering — https://www.anthropic.com/engineering/multi-agent-research-system 2 3

  2. Handle tool calls — Claude API — https://platform.claude.com/docs/en/agents-and-tools/tool-use/handle-tool-calls 2

  3. Building Effective AI Agents — Anthropic Engineering — https://www.anthropic.com/engineering/building-effective-agents 2 3

Exercícios

01

Para cada uma das seis ferramentas abaixo, decida: (1) ela é idempotente; (2) se uma chamada órfã a ela for reexecutada na retomada, o risco é alto, médio ou baixo — e por quê.

Nível 1: Classifique seis ferramentas quanto a idempotência e risco de reexecução
  • read_file(path) — lê o conteúdo de um arquivo
  • send_email(to, subject, body) — envia um e-mail
  • ensure_ticket(title, body) — procura pelo título, devolve o chamado existente se houver um, cria um novo só se não houver
  • append_log(line) — acrescenta uma linha ao fim de um arquivo de log
  • set_config(key, value) — define uma chave de configuração com um valor dado (por sobrescrita)
  • delete_file(path) — apaga um arquivo
Critérios de conclusão · marcado localmente
02

Semana passada, seu harness estava trabalhando na tarefa “cliente relata uma indisponibilidade, abra um chamado”. Ele rodou create_ticket, obteve um resultado de sucesso — e o contêiner acabou sendo reiniciado e morto bem nessa hora, antes de o resultado voltar para um tool_result. Depois que o processo voltou, o harness leu o pendingToolUse do checkpoint.json e encontrou exatamente aquela chamada create_ticket. Com o runToolUses sem livro-razão abaixo, a única opção dele era reexecutar — então um relato de indisponibilidade de um cliente virou um chamado duplicado no sistema, do nada.

Nível 2: Acrescente um livro-razão de efeitos a um runToolUses que não tem nenhum

Sua tarefa: (1) escrever as funções de leitura e escrita loadEffects/saveEffects para o effects.json, usando uma gravação atômica (.tmp primeiro, depois rename); (2) reescrever o runToolUses para acrescentar a lógica “consulte o livro-razão antes de executar, execute só em uma falha, registre no livro-razão no instante em que a execução tiver sucesso”; (3) escrever um script Node que prove isso: chame o seu runToolUses reescrito duas vezes com o mesmo tool_use_id (simulando uma repetição na retomada) e mostre que a segunda chamada não roda de novo a implementação real do create_ticket.

Critérios de conclusão · marcado localmente