Um agente pode resolver uma tarefa simples com uma chamada e gastar muito mais quando começa a ler arquivos, chamar tools, corrigir uma hipótese e tentar de novo. O problema aparece quando o loop tem um limite de turnos, mas ninguém sabe quanto cada nova chamada ainda pode consumir.

A solução é tratar o orçamento como parte do runtime. Registre o uso confirmado, estime o custo da próxima chamada, reserve espaço para ela e interrompa a execução antes de enviá-la quando o saldo não for suficiente. O modelo pode sugerir economia, mas não deve ser o componente que aplica o limite.

Este post usa TypeScript sem um SDK específico. A implementação de callModel e dos adapters de tools fica ilustrativa. O contrato da guarda é verificável com testes locais e pode envolver qualquer provedor que devolva uso de tokens.

Diagrama mostra um agente registrando tokens, verificando o orçamento restante e parando antes da próxima chamada.

Regra curta

  • Um limite por requisição não limita o custo de um loop inteiro.
  • O orçamento deve ser conferido antes da chamada seguinte, não apenas depois dela.
  • Tokens de entrada, saída, raciocínio, tools e cache precisam seguir a política do provedor.
  • O estado final deve dizer se o agente concluiu, esgotou o orçamento ou parou com uso desconhecido.

Por que max_tokens não é um orçamento de execução?

Um parâmetro de saída limita uma chamada. Um agente pode fazer várias chamadas, repetir uma tool e reenviar um histórico maior a cada rodada. Mesmo que cada chamada respeite o próprio teto, o total da execução continua sem limite se o runtime não somar o uso.

A documentação de modelos de raciocínio da OpenAI explica que max_output_tokens inclui tokens de raciocínio, saída visível e formatação interna. A mesma documentação mostra input_tokens, output_tokens, reasoning_tokens e total_tokens no uso da resposta. Isso é útil para medir a chamada, mas não cria um teto para o próximo turno.

O guia de contagem de tokens da Anthropic também permite contar uma mensagem com tools antes de gerar e devolve input_tokens. No Gemini, o guia de contagem de tokens expõe campos separados para entrada, saída, pensamento, cache, uso de tools e total. Os nomes mudam. A decisão arquitetural não muda: normalize o uso dos adapters em um registro interno.

O owner de execução durável para agentes de IA cobre onde persistir o estado quando o run precisa continuar depois de uma falha. Aqui o recorte é anterior: decidir se o próximo passo ainda cabe no orçamento.

Quais limites um agente precisa combinar?

Um orçamento útil tem pelo menos três dimensões, além de um relógio opcional:

Limite O que controla O que não resolve sozinho
Tokens Uso de entrada, saída e raciocínio conforme o provedor informa Uma tool que fica presa sem gerar tokens
Tool calls Quantas operações externas podem começar Contexto grande em poucas chamadas
Turnos Quantas rodadas modelo-tool o loop pode executar Uma única chamada cara
Tempo Quanto o run pode permanecer ativo Custo já consumido antes do timeout

O guia de execução do OpenAI Agents JS documenta maxTurns como limite do loop. Use uma política parecida mesmo quando o agente é próprio. Ela funciona como backstop, não como substituto do orçamento de tokens.

O orçamento precisa ter uma unidade clara. Você pode controlar tokens, custo estimado ou créditos internos. Se não quiser atrelar o runtime ao preço de um modelo, controle tokens e mantenha o preço apenas na observabilidade. Preços e regras de cobrança mudam; o evento de uso do provedor é a evidência mais estável para a decisão de parar.

Como representar o orçamento no runtime?

Separe a política do loop. A política decide quanto um run pode usar. A guarda decide se uma chamada cabe. O adapter do provedor informa o uso depois que a chamada termina.

type Usage = {
  inputTokens: number;
  outputTokens: number;
  reasoningTokens?: number;
  toolTokens?: number;
};

type BudgetLimits = {
  totalTokens: number;
  maxToolCalls: number;
  maxTurns: number;
};

class BudgetExceeded extends Error {
  constructor(readonly reason: "tokens" | "tools" | "turns") {
    super(`agent budget exceeded: ${reason}`);
  }
}

class RunBudget {
  private usedTokens = 0;
  private toolCalls = 0;
  private turns = 0;

  constructor(private readonly limits: BudgetLimits) {}

  beforeTurn() {
    if (this.turns >= this.limits.maxTurns) {
      throw new BudgetExceeded("turns");
    }
  }

  beforeToolCall() {
    if (this.toolCalls >= this.limits.maxToolCalls) {
      throw new BudgetExceeded("tools");
    }
    this.toolCalls += 1;
  }

  reserveModelCall(estimatedTokens: number) {
    if (this.usedTokens + estimatedTokens > this.limits.totalTokens) {
      throw new BudgetExceeded("tokens");
    }
  }

  recordModelCall(usage: Usage) {
    this.usedTokens +=
      usage.inputTokens +
      usage.outputTokens +
      (usage.reasoningTokens ?? 0) +
      (usage.toolTokens ?? 0);
    this.turns += 1;
  }

  snapshot() {
    return {
      usedTokens: this.usedTokens,
      toolCalls: this.toolCalls,
      turns: this.turns,
      remainingTokens: this.limits.totalTokens - this.usedTokens,
    };
  }
}

Os números do exemplo são políticas de teste, não uma recomendação universal. A guarda não precisa conhecer o preço do provedor. Ela precisa receber uma estimativa conservadora para a próxima chamada e atualizar o uso com o valor devolvido pelo adapter.

Há uma limitação importante: uma estimativa pode errar. Se ela for pequena demais, o runtime deixa passar uma chamada que estoura o limite. Se for grande demais, o agente para antes de usar todo o orçamento. Por isso, trate o limite como uma barreira de segurança e registre a diferença entre reserva e uso real.

Onde conferir o orçamento no loop?

Confira antes de cada operação que pode consumir recurso. A ordem importa: verificar depois da chamada apenas transforma um estouro em surpresa.

type ModelResult =
  | { type: "final"; text: string; usage: Usage }
  | { type: "tool_calls"; calls: ToolCall[]; usage: Usage };
type ToolCall = { name: string; input: unknown };

async function runAgent(prompt: string, budget: RunBudget) {
  for (;;) {
    budget.beforeTurn();
    budget.reserveModelCall(estimateNextCall(prompt));

    const response = await callModel({ prompt }); // adapter ilustrativo
    budget.recordModelCall(response.usage);

    if (response.type === "final") {
      return { status: "completed", text: response.text, usage: budget.snapshot() };
    }

    const results = [];
    for (const call of response.calls) {
      budget.beforeToolCall();
      results.push(await executeTool(call)); // adapter ilustrativo
    }

    prompt = appendToolResults(prompt, results);
  }
}

O exemplo mostra duas políticas diferentes. reserveModelCall protege o uso do modelo. beforeToolCall limita efeitos e chamadas externas. Uma tool também pode ter um custo próprio, como uma consulta paga ou uma operação de browser. Nesse caso, crie um segundo ledger ou converta o custo em créditos internos, mas não esconda o evento dentro da mensagem do modelo.

O post sobre cancelamento de um agente quando uma tool trava trata timeout e cancelamento. Um orçamento complementa esse controle: ele pode parar um run que está perfeitamente responsivo, mas consumindo mais chamadas do que a tarefa permite.

Como lidar com o uso devolvido pelo provedor?

Normalize os campos de uso assim que a chamada termina. Não some apenas a saída visível. Em modelos de raciocínio, tokens internos podem entrar no limite; em chamadas com tools, a entrada de definições e resultados também pode participar do uso.

function normalizeUsage(raw: {
  input_tokens?: number;
  output_tokens?: number;
  total_tokens?: number;
  output_tokens_details?: { reasoning_tokens?: number };
  tool_use_prompt_tokens?: number;
}): Usage {
  return {
    inputTokens: raw.input_tokens ?? 0,
    outputTokens: raw.output_tokens ?? 0,
    reasoningTokens: raw.output_tokens_details?.reasoning_tokens ?? 0,
    toolTokens: raw.tool_use_prompt_tokens ?? 0,
  };
}

Esse adapter é ilustrativo porque cada SDK usa campos diferentes. O teste do adapter deve fixar exemplos reais de resposta do provedor e falhar quando o SDK mudar o formato. Não use total_tokens junto com input e output na mesma soma, ou você contará o mesmo uso duas vezes. Escolha uma fonte de verdade por resposta.

O guia do Gemini também descreve count_tokens antes da geração. Use essa chamada quando a estimativa de entrada for o risco principal, mas lembre que a geração posterior pode consumir saída, pensamento ou tool use. Contar antes melhora a reserva; não substitui registrar o uso depois.

O que fazer quando o orçamento acaba?

Esgotar o orçamento não é o mesmo que falhar. O agente pode ter produzido uma resposta parcial, concluído uma tool importante ou parado antes de qualquer efeito externo. O resultado precisa carregar o estado que permite ao chamador decidir.

type StopReason = "completed" | "budget_tokens" | "budget_tools" | "budget_turns" | "failed";

type RunResult = {
  status: StopReason;
  text?: string;
  usage: ReturnType<RunBudget["snapshot"]>;
  retryable: boolean;
};

Uma parada por tokens pode gerar retryable: false quando o próximo passo repetiria a mesma tarefa sem alterar o contexto. Também pode gerar retryable: true quando existe uma política de resumo, um modelo mais barato ou uma fila que retoma de um checkpoint. A escolha pertence ao runtime, não ao texto gerado pelo agente.

Não aumente o orçamento automaticamente quando o agente pede mais tempo. Isso transforma uma barreira previsível em uma negociação feita pelo componente que está consumindo o recurso. Se houver escalonamento, defina-o fora do loop, registre a nova autorização e aplique outro teto.

Como verificar que a guarda realmente funciona?

O teste útil não chama um modelo real. Ele injeta usos conhecidos e prova que a próxima chamada não começa depois do limite. O teste de integração pode validar o adapter separadamente.

const budget = new RunBudget({
  totalTokens: 100,
  maxToolCalls: 2,
  maxTurns: 3,
});

budget.reserveModelCall(60);
budget.recordModelCall({ inputTokens: 40, outputTokens: 20 });

// A próxima chamada estimada em 41 tokens precisa ser bloqueada.
expect(() => budget.reserveModelCall(41)).toThrow("tokens");

budget.beforeToolCall();
budget.beforeToolCall();
expect(() => budget.beforeToolCall()).toThrow("tools");

Esse trecho usa uma asserção no estilo de Vitest ou Jest e é intencionalmente um fixture curto. Na suíte real, cubra também:

  • uso de raciocínio que não aparece na saída visível;
  • tool use contabilizado na entrada;
  • estimativa maior que o saldo restante;
  • maxTurns atingido sem nova chamada;
  • falha do adapter antes de devolver uso;
  • interrupção depois de uma escrita externa sem confirmação.

Quando a chamada termina sem usage, não trate o valor como zero. Marque o uso como unknown e escolha uma política conservadora, como bloquear a próxima chamada ou aplicar um limite residual menor. Um zero falso torna o ledger otimista justamente no caminho de falha.

O orçamento também precisa aparecer na observabilidade?

Sim. O post sobre observabilidade de coding agents no CI mostra por que um log precisa explicar o que o agente fez e como foi verificado. Para um orçamento, registre run_id, model, turn, tool_calls, reserved_tokens, used_tokens, remaining_tokens, stop_reason e usage_status.

Não registre prompts, secrets ou resultados completos apenas para explicar um estouro. Um evento curto permite agrupar custo por run e comparar estimativa com uso real. Se a telemetria mostrar que a maior parte do uso vem do histórico repetido, o próximo passo pode ser um resumo ou prompt caching para tool calls, não um limite maior.

Em loops longos de Claude Code, Codex ou um harness próprio, uso o RemoteCode para continuar fluxos agentic com menos contexto repetido. É uma ferramenta minha, citada aqui porque contexto e orçamento se cruzam. Ela não substitui a guarda, o ledger ou a decisão de parada.

Perguntas frequentes

Um prompt pode impor o orçamento?

Não com segurança. O prompt pode orientar o modelo, mas a aplicação precisa conferir uso e decidir se a próxima chamada pode começar. A barreira deve ficar fora da decisão livre do modelo.

Devo controlar tokens ou dinheiro?

Tokens são uma unidade mais estável entre ambientes, enquanto dinheiro é melhor para cobrança e alertas. Uma aplicação pode controlar tokens por run e calcular custo em uma camada separada, usando o preço atual do provedor.

O limite de turnos substitui o orçamento?

Não. Um turno pode consumir muitos tokens, e várias chamadas pequenas podem superar o custo permitido. Combine tokens, tool calls e turnos, com timeout quando o trabalho também tem um prazo.

Posso repetir um run que esgotou tokens?

Somente com uma política explícita. Antes de repetir, verifique o checkpoint, os efeitos externos e o motivo da parada. Se o contexto não mudou, um retry tende a consumir mais sem aumentar a informação disponível.

Conclusão

Um agente econômico não nasce de uma instrução no system prompt. Ele nasce de um loop que mede o uso, reserva a próxima chamada e para antes de ultrapassar o orçamento. Tokens, tools, turnos e tempo protegem riscos diferentes, então o runtime precisa registrar cada razão de parada.

Comece com uma guarda pequena, um ledger por run e três testes: saldo insuficiente, limite de tools e uso desconhecido. Depois compare a estimativa com o usage real do provedor. A partir daí, você consegue escolher entre resumir, trocar de modelo, retomar por checkpoint ou encerrar com uma resposta honesta.

Fontes consultadas