Um agente recebe o pedido "atualize o cadastro do cliente" e chama uma tool de escrita. O revisor vê apenas um resumo, aprova a operação e descobre depois que o alvo mudou enquanto a execução esperava. O problema não é a falta de um botão. É a ausência de uma fronteira que mostre o efeito proposto antes de permitir a mutação.
Para criar um dry-run de agente de IA, separe quatro passos: produzir um plano tipado, calcular uma prévia sem efeitos, aprovar exatamente esse plano e revalidar o estado imediatamente antes da execução. O modelo pode propor a ação, mas somente o executor autorizado toca no sistema externo.
Este artigo é um spoke do guia sobre quando um agente de IA deve pedir aprovação humana. O owner explica quando parar para uma decisão. Aqui, a pergunta é como construir a prévia e impedir que uma aprovação seja ligada a argumentos diferentes. A validação de tool calls em TypeScript cobre o schema da chamada; este texto acrescenta o contrato de efeito e a rechecagem do recurso.
Resposta curta
- Gere um plano com alvo, argumentos, pré-condições e efeito esperado.
- Calcule a prévia em uma função ou tool que não possui credenciais de escrita.
- Ligue a aprovação ao fingerprint do plano, não a um pedido genérico de "sim".
- Releia a versão do recurso antes de executar e pare se ela mudou.
- Mantenha a execução em um adapter separado, com autorização própria e teste de zero efeitos no caminho de preview.
O que um dry-run precisa provar?
Um dry-run útil responde duas perguntas diferentes. Primeiro, o runtime entendeu os argumentos, o alvo e a política da ação? Segundo, o sistema consegue mostrar o que mudaria sem alterar o alvo? Se ele apenas imprime a intenção do modelo, não é uma prévia do efeito.
A documentação do Kubernetes descreve um dry-run com uma propriedade que vale como referência de projeto: a requisição passa por validação e pelos estágios aplicáveis, mas não é persistida e não produz efeitos colaterais (Kubernetes, "API Concepts"). Um agente não ganha essa garantia automaticamente. O executor da aplicação precisa construí-la.
Use estes critérios antes de chamar a resposta de dry-run:
| Critério | Pergunta de verificação |
|---|---|
| Sem mutação | O caminho consegue funcionar sem credencial ou permissão de escrita? |
| Plano explícito | A resposta identifica tool, alvo, argumentos e efeito esperado? |
| Pré-condições | A prévia guarda a versão ou estado que precisa continuar válido? |
| Política | O runtime valida autorização e risco antes de gerar aprovação? |
| Execução separada | Existe um caminho diferente que é o único capaz de alterar o sistema? |
O preview também pode falhar. Um registro não encontrado, um schema inválido ou uma política que nega a ação devem produzir um resultado explícito. Não transforme uma falha de prévia em permissão para tentar a escrita mesmo assim.
Por que dryRun: true na mesma tool é uma fronteira fraca?
Um parâmetro dryRun pode ser útil dentro de um adapter confiável, mas não deve ser a única barreira. A mesma função recebe a intenção, decide se está em modo de simulação e mantém acesso potencial ao efeito. Um branch esquecido, um teste que chama a flag errada ou um novo chamador que omite o campo pode cruzar a fronteira.
Prefira duas capacidades com contratos diferentes:
preview_update_contact(input) -> Plan
execute_update_contact(plan, approval) -> Result
A primeira pode ler o estado atual e calcular uma proposta. A segunda recebe um plano já validado, confere a autorização e revalida o estado antes de gravar. Se o modelo só enxerga preview_update_contact, ele não consegue escolher a tool de escrita para escapar da etapa de revisão.
O OpenAI Agents SDK documenta tools como funções executadas pela aplicação e permite configurar needsApproval para pausar uma chamada antes da execução. Esse recurso resolve a interrupção do run. Ele não cria, sozinho, uma prévia do efeito nem impede que o payload seja reconstruído de maneira diferente depois da aprovação. A separação de capacidades continua sendo responsabilidade do runtime.
Como modelar um plano que possa ser aprovado?
O plano deve ser um objeto de dados, não o texto que o agente usou para explicar o que pretende fazer. Inclua apenas campos que o executor consegue revalidar:
type ContactUpdatePlan = {
planId: string;
action: "update_contact";
targetId: string;
patch: { email?: string; name?: string };
before: { email: string; name: string };
after: { email: string; name: string };
expectedVersion: number;
requiresApproval: true;
};
before e expectedVersion são importantes por motivos diferentes. A versão detecta uma mudança concorrente mesmo quando o valor final parece parecido. O estado anterior ajuda a mostrar ao revisor o que a operação pretende substituir. O planId deve ser um fingerprint estável do plano normalizado, incluindo alvo, argumentos, versão e política aplicada.
Não inclua um campo como approved: true produzido pelo próprio modelo. Aprovação é uma decisão externa ao plano. O executor deve receber um token ou registro que a aplicação emitiu depois de conferir o planId, o usuário responsável, a expiração e a política.
Como implementar preview e execução sem compartilhar a permissão?
A fixture abaixo usa um Map local para representar o sistema externo. Ela é executável com Node.js e não mede um provedor real. O ponto do exemplo é tornar impossível que previewContactUpdate altere o mapa e fazer executeContactUpdate rejeitar um plano obsoleto.
Salve como agent-dry-run.mjs:
import assert from "node:assert/strict";
const contacts = new Map([
["contact-1", { version: 3, name: "Ana", email: "ana@old.example" }],
]);
function stableId(value) {
return JSON.stringify(value);
}
function previewContactUpdate({ targetId, patch }) {
const current = contacts.get(targetId);
if (!current) throw new Error("contact not found");
const allowed = new Set(["name", "email"]);
for (const field of Object.keys(patch)) {
if (!allowed.has(field)) throw new Error(`field not allowed: ${field}`);
}
const before = { name: current.name, email: current.email };
const after = { ...before, ...patch };
const identity = { targetId, patch, before, version: current.version };
return {
planId: `plan:${stableId(identity)}`,
action: "update_contact",
targetId,
patch,
before,
after,
expectedVersion: current.version,
requiresApproval: true,
};
}
function executeContactUpdate(plan, approval) {
if (!approval || approval.planId !== plan.planId) {
throw new Error("approval does not match the plan");
}
const current = contacts.get(plan.targetId);
if (!current || current.version !== plan.expectedVersion) {
throw new Error("stale plan: preview again");
}
contacts.set(plan.targetId, {
version: current.version + 1,
...plan.after,
});
return { status: "applied", planId: plan.planId };
}
const plan = previewContactUpdate({
targetId: "contact-1",
patch: { email: "ana@new.example" },
});
assert.equal(contacts.get("contact-1").email, "ana@old.example");
assert.equal(plan.after.email, "ana@new.example");
assert.throws(
() => executeContactUpdate(plan, { planId: "plan:wrong" }),
/approval does not match/,
);
assert.equal(contacts.get("contact-1").email, "ana@old.example");
const result = executeContactUpdate(plan, { planId: plan.planId });
assert.equal(result.status, "applied");
assert.equal(contacts.get("contact-1").version, 4);
assert.equal(contacts.get("contact-1").email, "ana@new.example");
assert.throws(
() => executeContactUpdate(plan, { planId: plan.planId }),
/stale plan/,
);
console.log("preview did not mutate; matching plan applied once");
Execute a fixture limpa com:
node agent-dry-run.mjs
O assert antes da execução demonstra que o preview não mudou o email. O token errado é rejeitado sem efeito. Depois da aplicação, a mesma aprovação não pode ser reutilizada porque a versão do contato mudou. Em um serviço real, substitua o Map por uma leitura transacional ou por uma API que ofereça If-Match, versão esperada ou outro mecanismo de pré-condição.
Esse teste prova o contrato local. Ele não prova que o adapter de produção não faz chamadas externas durante a prévia, que uma API concorrente respeita a versão ou que o token de aprovação está protegido. Esses pontos precisam de testes de integração e controles do sistema real.
Como ligar aprovação ao plano sem aprovar outra ação?
O fluxo deve persistir o plano inteiro ou um fingerprint que permita recuperá-lo. Depois, a aprovação precisa apontar para a mesma identidade:
modelo -> plano normalizado -> preview -> decisão -> revalidação -> execução
Se o agente recalcula after depois da aprovação, a pessoa pode ter aprovado uma mudança e o executor pode aplicar outra. O runtime deve comparar planId, alvo, argumentos relevantes, versão do recurso e política. Uma divergência exige uma nova prévia ou uma recusa segura.
A documentação de human-in-the-loop do OpenAI Agents SDK descreve um run que pausa, devolve interruptions e retoma pelo mesmo RunState depois da decisão (OpenAI Agents SDK, "Human-in-the-loop"). Use esse tipo de estado para carregar a pendência, mas mantenha a autorização e o fingerprint no seu próprio contrato. Um snapshot serializado não é uma prova de identidade por si só.
O mesmo vale para MCP. O SDK TypeScript expõe readOnlyHint, destructiveHint, idempotentHint e openWorldHint como anotações de tool (MCP TypeScript SDK, "server/mcp"). Essas anotações ajudam o cliente a entender o risco, mas são dicas. Elas não substituem uma autorização determinística, um dry-run real ou um sandbox quando o servidor não é confiável.
Quando um dry-run não é a resposta certa?
Nem toda ação precisa de uma prévia detalhada. Leituras, cálculos puros e mudanças locais descartáveis podem ser executados em um ambiente de teste mais simples. Uma prévia também pode ser enganosa quando o efeito depende de um sistema que muda rapidamente, de um envio que não pode ser desfeito ou de um terceiro que não oferece uma API de simulação.
Nesses casos, escolha a barreira adequada:
- use sandbox ou conta de teste quando a integração permitir uma execução real sem dados de produção;
- use aprovação explícita quando a consequência é externa e não existe uma prévia fiel;
- use recusa quando o executor não consegue identificar o alvo, a autoridade ou a pré-condição;
- use idempotência e reconciliação quando a chamada pode cair depois de o efeito começar.
Não chame uma resposta textual de preview se ela não foi derivada do estado que a execução vai conferir. Um resumo pode explicar a intenção. A prévia precisa representar a mudança e carregar as condições que tornam a aplicação válida.
Como este artigo foi produzido
Samuel Fajreldines é o autor responsável por este texto. A pesquisa combinou documentação atual do OpenAI Agents SDK, do MCP TypeScript SDK e do Kubernetes, inspeção de resultados de busca, leitura de discussões públicas recentes e comparação com os posts existentes sobre aprovação e validação de tools. A fixture foi executada localmente com Node.js usando somente um Map em memória. Nenhum contato, email, permissão ou API externa foi alterado.
Usei RemoteCode como ferramenta própria para manter contexto entre sessões de Codex e Claude Code. A assistência de IA ajudou a descobrir fontes, comparar páginas, organizar o rascunho, criar a ilustração SVG e revisar os três idiomas. Ela não executou um agente real, não testou um provedor de produção e não substitui a verificação dos contratos do seu sistema.
Fontes consultadas
- OpenAI Agents SDK, "Tools", consultado em 6 de novembro de 2026.
- OpenAI Agents SDK, "Human-in-the-loop", consultado em 6 de novembro de 2026.
- MCP TypeScript SDK, "server/mcp", consultado em 6 de novembro de 2026.
- Kubernetes, "API Concepts: Dry-run", consultado em 6 de novembro de 2026.