Guía10 min

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.

OpenAIAnthropicGemini
Bucle de agente con un tope de turnos que corta la corrida antes de un loop infinito

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:

  1. Llama al modelo con el agente e input actuales.
  2. Si hay tool calls, las corre, anexa resultados y repite.
  3. Si supera max_turns / maxTurns, lanza la excepción (salvo que pases None/null).
  4. “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).

El runner cuenta llamadas al modelo; el techo corta el loop antes de que se coma el presupuesto

Defaults verificados (2026-09-03)

RuntimeKnobDefault documentadoSi se pasa
OpenAI Agents JSmaxTurns10. null lo desactivaMaxTurnsExceededError
OpenAI Agents Pythonmax_turnsDEFAULT_MAX_TURNS. None lo desactivaMaxTurnsExceeded
LangGraph ≥ 1.0.6recursion_limit1000 pasoserror GRAPH_RECURSION_LIMIT
CrewAI Agentmax_iter20el agente debe responder con lo que tenga
Claude Agent SDKmax_turnsNone (sin techo en la opción)corte del query
Google ADK LoopAgentmax_iterationsno hay default seguro; hay que setearloel 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.

Tres techos distintos: vueltas del modelo, segundos de wall-clock y presupuesto

Checklist

  • El loop no es while True sin contador.
  • OpenAI: maxTurns/max_turns explícito. No dependas del default si el entorno cambia de JS (10) a Python (DEFAULT_MAX_TURNS).
  • Hay handler o except MaxTurnsExceeded que responde al usuario. No silent retry del run entero.
  • LangGraph: recursion_limit en el config de invoke, no dentro de configurable.
  • CrewAI: max_iter + max_execution_time en agentes con tools de red.
  • Claude SDK: no dejes max_turns=None en un bot 24/7; pon número + max_budget_usd si hay API paid.
  • ADK LoopAgent: max_iterations obligatorio. 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.