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.
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;
maxTurnsatingido 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.