Action is proposed as structured data (tool + args).
El problema (en producción)
Tu agente “funciona” en staging.
Luego llega el tráfico real y aprendes dos cosas:
- un agente es un loop, y los loops no paran por buena educación
- finanzas no es un sistema de monitoring (pero te van a avisar igual)
Lo vimos muchas veces:
- un tool flaky añade retries
- los retries multiplican tool calls
- los tool calls suben tokens (“esto pasó… intenta otra vez”)
- y tu agente “de centavos” hace $8–$20 por run
A escala, eso no es “un bug”. Es una suscripción sorpresa que tu CFO no firmó.
Los budgets no son “optimización”. Son controles de seguridad. Deciden qué pasa cuando el agente no puede terminar.
Si tú no decides, decide el agente. Y suele decidir: “un intento más”.
Por qué esto se rompe en producción
1) Los equipos presupuestan una cosa y se olvidan del resto
Error clásico: “tenemos un token budget”.
Genial. Tu agente gastó $0.04 en tokens y $6 en browser automation.
En prod necesitas mínimo:
max_stepsmax_secondsmax_tool_callsmax_usd
2) Los retries se vuelven multiplicadores dentro del loop
Un retry no es el problema. Retries dentro del loop + retries del tool = multiplicador de costo.
3) Budgets sin stop reasons son invisibles
Si el run termina en timeout, el usuario reintenta. Eso crea más runs.
Stop reasons explícitos:
max_secondsmax_tool_callsmax_usdloop_detected
Stop reasons = observabilidad.
4) Enforce disperso en el código no funciona
Si los budgets se checkean:
- a veces en el agente
- a veces en wrappers
- a veces nunca
…te perderás un camino.
Pon budgets en un choke point: loop + tool gateway.
Ejemplo de implementación (código real)
Un budget guard “production-shaped”:
- checks continuos
- tracking de costo model + tools (approx ok)
- stop reason tipado para logs/alertas
from dataclasses import dataclass, field
import time
from typing import Any
TOOL_USD = {
"search.read": 0.00,
"http.get": 0.00,
"browser.run": 0.20, # placeholder
}
@dataclass(frozen=True)
class BudgetPolicy:
max_steps: int = 25
max_seconds: int = 60
max_tool_calls: int = 12
max_usd: float = 1.00
@dataclass
class BudgetState:
started_at: float = field(default_factory=time.time)
steps: int = 0
tool_calls: int = 0
tokens_in: int = 0
tokens_out: int = 0
tool_usd: float = 0.0
def elapsed_s(self) -> float:
return time.time() - self.started_at
def estimate_model_usd(tokens_in: int, tokens_out: int) -> float:
# Replace with your real pricing model(s). Approximate is fine for guards.
return (tokens_in + tokens_out) * 0.000002
class BudgetExceeded(RuntimeError):
def __init__(self, stop_reason: str, *, state: BudgetState):
super().__init__(stop_reason)
self.stop_reason = stop_reason
self.state = state
class BudgetGuard:
def __init__(self, policy: BudgetPolicy):
self.policy = policy
self.state = BudgetState()
def total_usd(self) -> float:
return estimate_model_usd(self.state.tokens_in, self.state.tokens_out) + self.state.tool_usd
def check(self) -> None:
if self.state.steps > self.policy.max_steps:
raise BudgetExceeded("max_steps", state=self.state)
if self.state.elapsed_s() > self.policy.max_seconds:
raise BudgetExceeded("max_seconds", state=self.state)
if self.state.tool_calls > self.policy.max_tool_calls:
raise BudgetExceeded("max_tool_calls", state=self.state)
if self.total_usd() > self.policy.max_usd:
raise BudgetExceeded("max_usd", state=self.state)
def on_step(self) -> None:
self.state.steps += 1
self.check()
def on_model_call(self, *, tokens_in: int, tokens_out: int) -> None:
self.state.tokens_in += tokens_in
self.state.tokens_out += tokens_out
self.check()
def on_tool_call(self, *, tool: str) -> None:
self.state.tool_calls += 1
self.state.tool_usd += float(TOOL_USD.get(tool, 0.0))
self.check()
def run_agent(task: str, *, policy: BudgetPolicy) -> dict[str, Any]:
guard = BudgetGuard(policy)
try:
while True:
guard.on_step()
# model decides next action (pseudo)
action, tokens_in, tokens_out = llm_decide(task) # (pseudo)
guard.on_model_call(tokens_in=tokens_in, tokens_out=tokens_out)
if action.kind == "tool":
guard.on_tool_call(tool=action.name)
obs = call_tool(action.name, action.args) # (pseudo)
task = update_state(task, action, obs) # (pseudo)
continue
return {"status": "ok", "answer": action.final_answer, "usage": guard.state.__dict__}
except BudgetExceeded as e:
return {
"status": "stopped",
"stop_reason": e.stop_reason,
"usage": e.state.__dict__,
"partial": "Stopped by budget. Return partial results + a reason users can understand.",
}const TOOL_USD = {
"search.read": 0.0,
"http.get": 0.0,
"browser.run": 0.2, // placeholder
};
export class BudgetExceeded extends Error {
constructor(stopReason, { state }) {
super(stopReason);
this.stopReason = stopReason;
this.state = state;
}
}
export class BudgetGuard {
constructor(policy) {
this.policy = policy;
this.state = {
startedAtMs: Date.now(),
steps: 0,
toolCalls: 0,
tokensIn: 0,
tokensOut: 0,
toolUsd: 0,
};
}
elapsedS() {
return (Date.now() - this.state.startedAtMs) / 1000;
}
totalUsd() {
return estimateModelUsd(this.state.tokensIn, this.state.tokensOut) + this.state.toolUsd;
}
check() {
if (this.state.steps > this.policy.maxSteps) throw new BudgetExceeded("max_steps", { state: this.state });
if (this.elapsedS() > this.policy.maxSeconds) throw new BudgetExceeded("max_seconds", { state: this.state });
if (this.state.toolCalls > this.policy.maxToolCalls) throw new BudgetExceeded("max_tool_calls", { state: this.state });
if (this.totalUsd() > this.policy.maxUsd) throw new BudgetExceeded("max_usd", { state: this.state });
}
onStep() {
this.state.steps += 1;
this.check();
}
onModelCall({ tokensIn, tokensOut }) {
this.state.tokensIn += tokensIn;
this.state.tokensOut += tokensOut;
this.check();
}
onToolCall({ tool }) {
this.state.toolCalls += 1;
this.state.toolUsd += Number(TOOL_USD[tool] ?? 0);
this.check();
}
}
function estimateModelUsd(tokensIn, tokensOut) {
return (tokensIn + tokensOut) * 0.000002;
}Incidente real (con números)
Tuvimos un agente “inofensivo” que buscaba en docs internas.
Un tool devolvía 5xx a veces. La capa tool retryaba (normal). El agente también (normal). En el loop, “normal” se convierte en “por qué se quema el budget”.
Impacto en una tarde:
- p95 costo/request de $0.06 → $2.10
- subieron 429/5xx (el agente insistía)
- soporte recibió tickets “está lento” (porque sí estaba lento)
Fix:
- budgets duros en un choke point
- stop reasons en logs + alertas
- dedupe/caching para args repetidos
- modo read-only degrade en vez de dejarlo correr
Trade-offs
- Budgets cortan runs legítimos a veces. Mejor que loops sin fin.
- Necesitas stop reasons + UX, si no la gente reintenta.
- El costo exacto es difícil; la aproximación sirve como guardrail.
Cuándo NO usarlo
- Si no puedes estimar costos, empieza al menos con steps/time/tool-calls.
- Incluso en workflows deterministas, deja budgets (menos estrictos, pero presentes).
Checklist (copiar/pegar)
- [ ] Steps budget (
max_steps) - [ ] Time budget (
max_seconds) - [ ] Tool-call budget (
max_tool_calls) - [ ] Spend budget (
max_usd) (o approximation) - [ ] Stop reasons loggeados + visibles
- [ ] Enforcement en tool gateway + run loop
- [ ] Alertas por spikes de stops de budget
Config segura por defecto (JSON/YAML)
budgets:
max_steps: 25
max_seconds: 60
max_tool_calls: 12
max_usd: 1.00
stop_reasons:
log: true
surface_to_user: true
FAQ (3–5)
Usado por patrones
Fallos relacionados
- AI Agent Infinite Loop (Detectar + arreglar, con código)
- Explosión de presupuesto (cuando un agente quema dinero) + fixes + código
- Tool Spam Loops (fallo del agente + fixes + código)
- Incidentes de exceso de tokens (prompt bloat) + fixes + código
- Corrupción de respuestas de tools (schema drift + truncation) + código
P: ¿Un token budget alcanza?
R: No. Los tools suelen costar más que tokens. Presupuesta steps/time/tool-calls y $.
P: ¿Budgets por tenant?
R: Sí. Tiers funcionan bien. Los budgets son parte del producto.
P: ¿Qué tan preciso debe ser el tracking de costo?
R: No perfecto. Para guardrails alcanza un aproximado. La facturación exacta puede venir después.
P: ¿Qué respondo al usuario cuando se corta por budget?
R: Resultado parcial + stop reason claro + qué hacer luego (reducir scope, etc.).
Páginas relacionadas (3–6 links)
- Foundations: What makes an agent production-ready · Why agents fail in production
- Failure: Budget explosion · Tool spam loops
- Governance: Step limits · Kill switch design
- Production stack: Production agent stack