Max turns y recursion_limit: cómo cortar un agente que no para
Resumen
Un turno es una llamada al modelo. OpenAI Agents SDK corta en maxTurns 10 (JS) o DEFAULT_MAX_TURNS (Python) y lanza MaxTurnsExceeded. LangGraph 1.0.6+ usa recursion_limit 1000. CrewAI max_iter 20. Claude Agent SDK deja max_turns en None. Esta guía fija el techo, el handler y cuándo no desactivar el límite.

Qué resuelve
Esta pieza se queda en la decisión práctica: qué instalar, qué riesgo agrega y cómo aplicarlo sin romper operación.
Un agente que “no termina” casi nunca es un bug del modelo. Es un loop sin techo: llama tool, recibe resultado, vuelve a llamar, y tu runtime no cuenta. OpenAI Agents SDK (JS) documenta el techo por defecto: maxTurns 10. Si lo alcanzas, lanza MaxTurnsExceededError. En Python el parámetro se llama max_turns, el default es DEFAULT_MAX_TURNS, y el error es MaxTurnsExceeded. max_turns=None (Python) o maxTurns: null (JS) apaga el límite.
Esta guía no cubre reintentos HTTP —eso está en reintentos y rate limits— ni pausar para un humano —eso está en human-in-the-loop. Aquí el foco es cuántas vueltas permites y qué haces cuando se acaban.
Qué cuenta como un “turno”
En OpenAI Agents SDK un turno es una llamada al LLM dentro del loop del runner. El loop, según su doc de Running agents:
- Llama al modelo con el agente e input actuales.
- Si hay tool calls, las corre, anexa resultados y repite.
- Si supera
max_turns/maxTurns, lanza la excepción (salvo que pasesNone/null). - “Final output” = texto del tipo pedido y cero tool calls.
No es un mensaje de usuario. Un usuario dice “arregla el test” y el agente puede gastar 8 turnos (leer archivo, editar, correr test, leer error…) sin que el humano hable otra vez.
LangGraph no habla de “turns”. Habla de pasos del grafo. Desde LangGraph 1.0.6 el default de recursion_limit es 1000 pasos. Se pasa en el config de invoke / stream, fuera de configurable:
graph.invoke(inputs, config={"recursion_limit": 40})
CrewAI cuenta iteraciones del agente. max_iter default 20: al llegar, el agente debe dar su mejor respuesta. Eso no es lo mismo que 20 llamadas HTTP; es 20 ciclos del agente.
Claude Agent SDK (Python) expone max_turns: int | None = None y max_budget_usd: float | None = None en ClaudeAgentOptions. None = sin techo de turnos en esa opción. Los subagentes tienen maxTurns opcional en la misma referencia.
Google ADK, en LoopAgent, es explícito: el loop no decide solo cuándo parar. Tienes que poner max_iterations o que un subagente escale/pare. Su ejemplo oficial: LoopAgent(..., max_iterations=5).

Defaults verificados (2026-09-03)
| Runtime | Knob | Default documentado | Si se pasa |
|---|---|---|---|
| OpenAI Agents JS | maxTurns | 10. null lo desactiva | MaxTurnsExceededError |
| OpenAI Agents Python | max_turns | DEFAULT_MAX_TURNS. None lo desactiva | MaxTurnsExceeded |
| LangGraph ≥ 1.0.6 | recursion_limit | 1000 pasos | error GRAPH_RECURSION_LIMIT |
| CrewAI Agent | max_iter | 20 | el agente debe responder con lo que tenga |
| Claude Agent SDK | max_turns | None (sin techo en la opción) | corte del query |
Google ADK LoopAgent | max_iterations | no hay default seguro; hay que setearlo | el loop termina |
OpenAI Python además acepta error_handlers con clave "max_turns": en vez de reventar, devuelves un output controlado. JS hace lo mismo con errorHandlers.maxTurns. Úsalo para decirle al usuario “llegué al tope, esto es lo que sé”, no para tragar el error.
No pongas max_turns=None en producción “porque a veces 10 no alcanza”. Sube el número. El None es para notebooks.
Qué número poner
No copies el default del framework. Copia el peor caso barato de tu tarea:
- FAQ / lookup: 3–6. Si no resolvió en 6, no va a resolver en 40.
- Coding agent con tests: 15–25. Un ciclo típico es read → edit → test.
- Investigación con search: 8–12, más reintentos por 429, no más turnos.
- Loop de crítica (ADK
LoopAgent, writer+critic): el ejemplo oficial usa 5. Empieza ahí.
Regla: el techo debe ser menor que el presupuesto que estás dispuesto a quemar. Un turno no es un token; es una llamada completa más tools. Diez turnos con contexto largo superan fácil el costo de un “agente lento”.
Claude SDK suma un segundo freno: max_budget_usd. Úsalo junto a max_turns, no en lugar. El presupuesto corta dinero; los turnos cortan loops que gastan poco por llamada pero no paran.
LangGraph a 1000 es un airbag, no un plan. Si tu grafo es un agente ReAct, 1000 pasos es una factura. Baja a 25–50 en invoke y sube solo si el grafo es un workflow largo con nodos baratos (sin LLM en cada arista).
Handler mínimo (OpenAI Agents Python)
from agents import Runner, MaxTurnsExceeded
def on_max_turns(_data):
return "Tope de turnos. Devuelvo el último estado; no reintento solo."
try:
result = Runner.run_sync(
agent,
user_input,
max_turns=8,
error_handlers={"max_turns": on_max_turns},
)
except MaxTurnsExceeded:
# si no hay handler: log + respuesta al usuario, no silent retry
raise
JS equivalente: run(agent, input, { maxTurns: 8, errorHandlers: { maxTurns: handler } }).
Si no usas el SDK: un for con range(MAX) y break cuando no hay tool_calls. El while True es el bug. Mide el contador en tests de tools sin LLM: fixture que pide tool siempre; el test falla si el loop pasa de N.
Cuándo el techo no basta
max_turns no sustituye:
- Timeouts por tool. Una sola tool colgada gasta el turno entero. Eso es AbortSignal / timeout, no más iteraciones.
- Guardrails de side effect. Un refund en el turno 2 es igual de caro que en el 20. Pon guardrails antes del handler.
- HITL en money paths. Si la tool escribe, pausa. El techo no es un humano.
- Contratos de tools. Un schema roto hace que el modelo reintente la misma tool. Arregla function calling; no subas
max_iter.
CrewAI tiene max_execution_time (segundos) y max_rpm aparte de max_iter. Son tres techos distintos: vueltas, reloj, rate. Pon los tres si el agente vive en cron.

Checklist
- El loop no es
while Truesin contador. - OpenAI:
maxTurns/max_turnsexplícito. No dependas del default si el entorno cambia de JS (10) a Python (DEFAULT_MAX_TURNS). - Hay handler o
except MaxTurnsExceededque responde al usuario. No silent retry del run entero. - LangGraph:
recursion_limiten el config deinvoke, no dentro deconfigurable. - CrewAI:
max_iter+max_execution_timeen agentes con tools de red. - Claude SDK: no dejes
max_turns=Noneen un bot 24/7; pon número +max_budget_usdsi hay API paid. - ADK
LoopAgent:max_iterationsobligatorio. Un “STOP” del crítico es extra, no reemplazo. - Test sin LLM: el executor se detiene en N aunque el fake model siga pidiendo tools.
FAQ
¿10 turnos no me alcanzan para un coding agent? Sube a 20–25. No pongas null. Mide cuántos turnos usan los runs que sí cierran.
¿LangGraph 1000 es “seguro”? Es el default desde 1.0.6 para no matar grafos largos. Para un ReAct con LLM en cada nodo, 1000 es un loop caro. Pon 40 y mira el error GRAPH_RECURSION_LIMIT.
¿CrewAI max_iter=20 corta tools a mitad? Corta el ciclo del agente y pide la mejor respuesta. No es un kill -9 del HTTP en vuelo. Combínalo con max_execution_time.
¿Puedo reintentar automáticamente al pillar MaxTurnsExceeded? No el run entero. Log, muestra estado, y si acaso un segundo run con input nuevo (“continúa desde X”). Re-lanzar el mismo prompt es el loop un nivel más arriba.
Si todavía no tienes el loop básico, empieza por la ruta de instalación. El techo se pone el día 1, no el día que llega la factura.
Lecturas relacionadas
Sigue explorando Herramientas y otras piezas para builders.

Truncar tool results en agentes: recortar el payload, no el contrato

Tool calling en paralelo: cuándo sí, cuándo no y cómo devolver resultados

Structured outputs: JSON confiable para agentes de IA
