Un agente puede resolver una tarea sencilla con una llamada y gastar mucho más cuando empieza a leer archivos, llamar tools, corregir una hipótesis y volver a intentarlo. El problema aparece cuando el loop tiene un límite de turnos, pero nadie sabe cuánto puede consumir la próxima llamada.

La solución es tratar el presupuesto como parte del runtime. Registra el uso confirmado, estima la próxima llamada, reserva espacio para ella y detén la ejecución antes de enviarla cuando el saldo no sea suficiente. El modelo puede recomendar ahorro, pero no debe ser el componente que aplica el límite.

Este post usa TypeScript sin un SDK específico. La implementación de callModel y los adapters de tools son ilustrativos. El contrato de la guarda se puede verificar con pruebas locales y conectar con cualquier proveedor que devuelva el uso de tokens.

Diagrama muestra un agente registrando tokens, comprobando el presupuesto restante y deteniéndose antes de la próxima llamada.

Regla corta

  • Un límite por solicitud no limita un loop completo del agente.
  • Comprueba el presupuesto antes de la próxima llamada, no solo después.
  • Los tokens de entrada, salida, razonamiento, tools y caché deben seguir la política del proveedor.
  • El estado final debe decir si el agente terminó, agotó el presupuesto o se detuvo con uso desconocido.

¿Por qué max_tokens no es un presupuesto por ejecución?

Un parámetro de salida limita una llamada. Un agente puede hacer varias llamadas, repetir una tool y reenviar un historial más grande en cada ronda. Aunque cada llamada respete su propio techo, la ejecución sigue sin límite si el runtime no suma el uso.

La documentación de modelos de razonamiento de OpenAI explica que max_output_tokens incluye tokens de razonamiento, salida visible y formato interno. También muestra input_tokens, output_tokens, reasoning_tokens y total_tokens en el uso de la respuesta. Esto ayuda a medir una llamada, pero no crea un techo para el siguiente turno.

La guía de conteo de tokens de Anthropic puede contar un mensaje con tools antes de generar y devuelve input_tokens. La guía de conteo de tokens de Gemini expone campos separados para entrada, salida, pensamiento, caché, uso de tools y total. Los nombres cambian. La decisión de arquitectura no: normaliza el uso de los adapters en un registro interno.

El owner de ejecución durable para agentes de IA explica dónde persistir el estado cuando una ejecución debe continuar después de un fallo. Este post cubre el paso más estrecho anterior: decidir si la próxima operación todavía cabe en el presupuesto.

¿Qué límites debe combinar un agente?

Un presupuesto útil tiene al menos tres dimensiones, además de un reloj opcional:

Límite Qué controla Qué no resuelve por sí solo
Tokens Uso de entrada, salida y razonamiento que informa el proveedor Una tool que se bloquea sin generar tokens
Tool calls Cuántas operaciones externas pueden comenzar Un contexto grande en pocas llamadas
Turnos Cuántas rondas modelo-tool puede ejecutar el loop Una llamada costosa
Tiempo Cuánto puede permanecer activa la ejecución El uso gastado antes del timeout

La guía de OpenAI Agents JS para ejecutar agentes documenta maxTurns como límite del loop. Usa una política parecida incluso con un agente propio. Es una red de seguridad, no un sustituto del presupuesto de tokens.

El presupuesto también necesita una unidad clara. Puedes controlar tokens, coste estimado o créditos internos. Si no quieres atar el runtime al precio de un modelo, controla tokens y deja los precios en la observabilidad. Los precios y las reglas de facturación cambian. El evento de uso del proveedor es una evidencia más estable para decidir cuándo detenerse.

¿Cómo debe representar el runtime el presupuesto?

Separa la política del loop. La política dice cuánto puede usar una ejecución. La guarda decide si una llamada cabe. El adapter del proveedor informa del uso después de que termina la llamada.

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,
    };
  }
}

Los números del ejemplo son políticas de prueba, no recomendaciones universales. La guarda no necesita conocer el precio del proveedor. Necesita una estimación conservadora para la próxima llamada y una actualización de uso del adapter.

Hay un límite importante: una estimación puede equivocarse. Si es demasiado pequeña, el runtime puede admitir una llamada que exceda el presupuesto. Si es demasiado grande, el agente se detiene antes de usar todo el margen. Trata el límite como una barrera de seguridad y registra la diferencia entre lo reservado y lo usado.

¿Dónde debe comprobar el loop el presupuesto?

Comprueba antes de cada operación que pueda consumir un recurso. El orden importa. Comprobar después de la llamada convierte el exceso en una sorpresa.

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);
  }
}

El ejemplo muestra dos políticas distintas. reserveModelCall protege el uso del modelo. beforeToolCall limita operaciones y efectos externos. Una tool también puede tener su propio coste, como una consulta de pago o una operación de navegador. En ese caso, añade un segundo ledger o convierte el coste en créditos internos, pero no escondas el evento en el mensaje del modelo.

El post sobre cancelar un agente de IA cuando una tool se bloquea cubre timeout y cancelación. Un presupuesto complementa ese control: puede detener una ejecución que responde bien, pero hace más llamadas de las que permite la tarea.

¿Cómo debe manejar el runtime el uso del proveedor?

Normaliza los campos de uso en cuanto termina cada llamada. No sumes solo la salida visible. En modelos de razonamiento, los tokens internos pueden entrar en el límite. En las llamadas con tools, las definiciones y los resultados también pueden formar parte del 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,
  };
}

Este adapter es ilustrativo porque cada SDK usa campos distintos. Las pruebas del adapter deben fijar respuestas reales del proveedor y fallar cuando cambie la forma del SDK. No sumes total_tokens junto con input y output, porque contarías el mismo uso dos veces. Elige una única fuente de verdad por respuesta.

La guía de Gemini también describe count_tokens antes de generar. Úsalo cuando el tamaño de entrada sea el riesgo principal, pero recuerda que la generación puede consumir después tokens de salida, pensamiento o uso de tools. Contar mejora la reserva; no sustituye registrar el uso.

¿Qué debe ocurrir cuando se acaba el presupuesto?

Agotar el presupuesto no es lo mismo que fallar. El agente puede haber producido una respuesta parcial, completado una tool importante o detenido el proceso antes de cualquier efecto externo. El resultado debe llevar suficiente estado para que el llamador decida.

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

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

Una parada por tokens puede ser retryable: false cuando el siguiente paso repetiría la misma tarea sin cambiar el contexto. También puede ser retryable: true si existe una política de resumen, un modelo más barato o una cola con checkpoint que pueda reanudar el trabajo. El runtime decide, no el texto generado por el agente.

No aumentes automáticamente el presupuesto cuando el agente pide más tiempo. Eso convierte una barrera predecible en una negociación hecha por el componente que consume el recurso. Si existe escalado, defínelo fuera del loop, registra la nueva autorización y aplica otro techo.

¿Cómo verificar que la guarda funciona?

La prueba unitaria útil no llama a un modelo real. Inyecta un uso conocido y demuestra que la próxima llamada no puede comenzar después del límite. Una prueba de integración puede validar el adapter del proveedor por separado.

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

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

// Una estimación de 41 tokens para la próxima llamada debe bloquearse.
expect(() => budget.reserveModelCall(41)).toThrow("tokens");

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

Este ejemplo usa una aserción al estilo de Vitest o Jest y es un fixture pequeño. En una suite real, cubre también:

  • uso de razonamiento que no aparece en la respuesta visible;
  • uso de tools contado como entrada;
  • una estimación mayor que el saldo restante;
  • maxTurns alcanzado antes de otra llamada;
  • un fallo del adapter antes de devolver el uso;
  • una interrupción después de una escritura externa sin confirmación.

Cuando una llamada termina sin usage, no trates el valor como cero. Marca el uso como unknown y elige una política conservadora, como bloquear la próxima llamada o aplicar un límite residual menor. Un cero falso vuelve optimista al ledger justo en el camino de fallo donde debe ser prudente.

¿El presupuesto también debe aparecer en la observabilidad?

Sí. El post sobre observabilidad de coding agents en CI explica por qué un log debe mostrar qué hizo el agente y cómo se verificó. Para un presupuesto, registra run_id, model, turn, tool_calls, reserved_tokens, used_tokens, remaining_tokens, stop_reason y usage_status.

No registres prompts, secretos ni resultados completos solo para explicar un exceso. Un evento corto permite agrupar el gasto por ejecución y comparar estimaciones con uso real. Si la telemetría muestra que la mayor parte del uso viene del historial repetido, el siguiente paso puede ser un resumen o prompt caching para tool calls en varias rondas, no un límite mayor.

En ejecuciones largas de Claude Code, Codex o un harness propio, uso RemoteCode para continuar flujos agentic con menos contexto repetido. Es una herramienta del autor, mencionada porque contexto y presupuesto se cruzan. No sustituye la guarda, el ledger ni la decisión de parada.

Preguntas frecuentes

¿Puede un prompt imponer el presupuesto?

No de forma segura. Un prompt puede guiar al modelo, pero la aplicación debe medir el uso y decidir si puede comenzar la próxima llamada. La barrera debe estar fuera de la decisión libre del modelo.

¿Debo controlar tokens o dinero?

Los tokens son una unidad más estable entre entornos, mientras que el dinero es mejor para facturación y alertas. Una aplicación puede controlar tokens por ejecución y calcular el coste en otra capa con el precio actual del proveedor.

¿Un límite de turnos sustituye al presupuesto?

No. Un turno puede consumir muchos tokens y muchas llamadas pequeñas pueden superar el coste permitido. Combina tokens, tool calls y turnos, con timeout cuando el trabajo también tiene un plazo.

¿Puedo repetir una ejecución que agotó sus tokens?

Solo con una política explícita. Antes de reintentar, comprueba el checkpoint, los efectos externos y el motivo de parada. Si el contexto no ha cambiado, un retry probablemente gastará más sin añadir información.

Conclusión

Un agente económico no nace de una instrucción en el system prompt. Nace de un loop que mide el uso, reserva la próxima llamada y se detiene antes de superar el presupuesto. Tokens, tools, turnos y tiempo protegen riesgos distintos, por lo que el runtime debe registrar cada motivo de parada.

Empieza con una guarda pequeña, un ledger por ejecución y tres pruebas: saldo insuficiente, límite de tools y uso desconocido. Después compara las estimaciones con el uso real del proveedor. Así puedes elegir con honestidad entre resumir, cambiar de modelo, reanudar desde un checkpoint o terminar la ejecución.

Fuentes consultadas