Ir al contenido principal
State machine en agentes IA: sin más bucles infinitos

State machine en agentes IA: sin más bucles infinitos

Best Practices
7 min readPor Daily Miranda Pardo

Construyes el agente. Funciona en local. Lo llevas a producción y tres días después alguien te dice que lleva 40 iteraciones dando vueltas a lo mismo.

No es un problema del modelo. Es un problema de diseño.

Los agentes IA sin estado explícito son bucles mientras alguien no los frene. Y en producción, ese "alguien" suele ser el timeout de la petición, el límite de tokens o el coste de la factura del mes.

El anti-patrón: el bucle que nadie puede parar

La implementación más común de un agente es algo así:

while (!done) {
  const response = await llm.complete(messages);
  if (response.toolCalls) {
    const results = await executeTools(response.toolCalls);
    messages.push(...results);
  } else {
    done = true;
  }
}

Este código tiene un problema fundamental: no hay estados, solo condiciones booleanas. El agente no sabe en qué fase está de su tarea. No sabe si está recopilando información, razonando sobre una decisión o ejecutando una herramienta. Solo sabe si terminó o no.

Las consecuencias en producción son predecibles:

  • El agente llama a la misma herramienta tres veces porque no hay registro de que ya ejecutó ese paso
  • Cuando falla una tool, no hay forma de retomar desde donde estaba — toca empezar de cero
  • El human-in-the-loop se implementa como un if dentro del bucle, sin contexto de qué estado pausó la ejecución
  • Los logs son una lista plana de acciones sin estructura — debugging imposible

Qué es una state machine y por qué los agentes la necesitan

Una state machine es un modelo con un conjunto finito de estados y transiciones explícitas entre ellos. El sistema solo puede estar en un estado a la vez, y el paso de un estado a otro ocurre por eventos definidos.

Para un agente IA, los estados naturales son:

EstadoQué hace el agente
idleEsperando activación
collectingRecopilando contexto antes de razonar
reasoningAnalizando el contexto y decidiendo qué hacer
actingEjecutando una herramienta o llamada externa
waiting_humanPausado, esperando aprobación humana
errorManejando un fallo con estrategia definida
doneTarea completada

Con este modelo, cada iteración del agente no es "¿terminé?", sino "¿en qué estado estoy y qué evento me mueve al siguiente?".

State machine para agentes IA: diagrama de estados y transiciones en TypeScript — DAILYMP

Implementación en TypeScript: estado explícito y transiciones tipadas

Sin necesidad de XState ni ninguna librería externa, el patrón se implementa limpio en TypeScript puro:

type AgentState =
  | 'idle'
  | 'collecting'
  | 'reasoning'
  | 'acting'
  | 'waiting_human'
  | 'error'
  | 'done';

type AgentEvent =
  | { type: 'START'; input: string }
  | { type: 'CONTEXT_READY'; context: Context }
  | { type: 'TOOL_CALL'; tool: string; args: unknown }
  | { type: 'TOOL_RESULT'; result: unknown }
  | { type: 'NEEDS_HUMAN'; question: string }
  | { type: 'HUMAN_APPROVED'; decision: string }
  | { type: 'COMPLETE'; output: string }
  | { type: 'ERROR'; reason: string }
  | { type: 'RETRY' };

function transition(state: AgentState, event: AgentEvent): AgentState {
  switch (state) {
    case 'idle':
      if (event.type === 'START') return 'collecting';
      break;
    case 'collecting':
      if (event.type === 'CONTEXT_READY') return 'reasoning';
      break;
    case 'reasoning':
      if (event.type === 'TOOL_CALL') return 'acting';
      if (event.type === 'NEEDS_HUMAN') return 'waiting_human';
      if (event.type === 'COMPLETE') return 'done';
      break;
    case 'acting':
      if (event.type === 'TOOL_RESULT') return 'reasoning';
      if (event.type === 'ERROR') return 'error';
      break;
    case 'waiting_human':
      if (event.type === 'HUMAN_APPROVED') return 'reasoning';
      break;
    case 'error':
      if (event.type === 'RETRY') return 'collecting';
      if (event.type === 'NEEDS_HUMAN') return 'waiting_human';
      break;
    case 'done':
      break;
  }
  throw new Error(`Transición no válida: ${state} + ${event.type}`);
}

El tipo AgentEvent hace que TypeScript rechace en compilación cualquier transición que no hayas definido. Sin sorpresas en runtime.

El bucle principal: ahora con historia

Con la state machine, el loop del agente cambia de forma:

async function runAgent(input: string) {
  let state: AgentState = 'idle';
  const history: Array<{ state: AgentState; event: AgentEvent; ts: number }> = [];

  function emit(event: AgentEvent) {
    const next = transition(state, event);
    history.push({ state, event, ts: Date.now() });
    state = next;
    return next;
  }

  emit({ type: 'START', input });

  while (state !== 'done' && state !== 'waiting_human') {
    if (state === 'collecting') {
      const context = await gatherContext(input);
      emit({ type: 'CONTEXT_READY', context });
    }

    if (state === 'reasoning') {
      const decision = await llm.reason(context, history);
      if (decision.needsHuman) {
        emit({ type: 'NEEDS_HUMAN', question: decision.question });
      } else if (decision.toolCall) {
        emit({ type: 'TOOL_CALL', tool: decision.toolCall.name, args: decision.toolCall.args });
      } else {
        emit({ type: 'COMPLETE', output: decision.output });
      }
    }

    if (state === 'acting') {
      try {
        const result = await executeTool(/* ... */);
        emit({ type: 'TOOL_RESULT', result });
      } catch (err) {
        emit({ type: 'ERROR', reason: String(err) });
      }
    }

    if (state === 'error') {
      const canRetry = history.filter(h => h.event.type === 'RETRY').length < 3;
      if (canRetry) {
        emit({ type: 'RETRY' });
      } else {
        emit({ type: 'NEEDS_HUMAN', question: 'Máximo de reintentos alcanzado' });
      }
    }
  }

  return { state, history };
}

Fíjate en el cambio más importante: history es la fuente de verdad. Si el agente se pausa para esperar aprobación humana, puedes serializar state + history en base de datos, y reanudar exactamente donde lo dejaste cuando llega la respuesta — sin perder contexto, sin volver a empezar.

Human-in-the-loop como ciudadano de primera clase

Con la state machine, el human-in-the-loop deja de ser un if (isAmbiguous) askUser() metido a presión dentro del loop.

El estado waiting_human es tan legítimo como acting. Cuando el agente llega ahí:

  1. El estado completo se persiste (base de datos, Redis, lo que uses)
  2. Se lanza una notificación al humano con la pregunta concreta
  3. La sesión del agente se cierra limpiamente
  4. Cuando el humano responde, se crea una nueva sesión con el estado persistido y se emite HUMAN_APPROVED
  5. El agente continúa desde reasoning con el contexto completo intacto

Este patrón es central en los agentes de automatización que construimos en DAILYMP: procesos de aprobación de presupuestos, validación de contratos, escalado de incidencias — flujos que necesitan supervisión humana sin perder el hilo del proceso.

Qué ganas en producción

La diferencia no es teórica. Con estado explícito:

Debugging real: el history de transiciones te dice exactamente qué pasó, en qué orden y en cuánto tiempo. Cuando un agente falla a las 3 AM, no revisas logs planos — lees la secuencia de estados.

Retry inteligente: no empiezas de cero. El agente vuelve al estado anterior al error, no al principio del flujo.

Límite de iteraciones tipado: si state === 'reasoning' aparece más de N veces en el history sin un COMPLETE, hay una regla explícita para escalarlo. No un timeout arbitrario.

Visibilidad para el equipo: cualquiera puede leer el diagrama de estados y entender qué hace el agente sin revisar el código. Esto importa cuando hay que explicarle a un CTO o a un cliente cómo funciona el proceso.

Si estás construyendo agentes con integración en sistemas existentes — Odoo, CRMs, herramientas internas — la state machine es lo que hace que esa integración sea mantenible a 6 meses vista, no solo funcional el día del deploy.

El error más caro que puedes cometer

Subestimar la complejidad del flujo.

Un agente que "solo tiene que hacer tres cosas" acaba teniendo siete casos edge: qué pasa si la tool tarda demasiado, qué pasa si el LLM devuelve un JSON malformado, qué pasa si el usuario cancela a mitad, qué pasa si hay dos agentes corriendo en paralelo sobre el mismo recurso.

Sin estados explícitos, cada uno de esos casos se convierte en un if nuevo dentro del loop. En seis meses tienes un bucle de 300 líneas que nadie entiende y que falla de formas que nadie predice.

Con una state machine, cada caso edge es una transición nueva. Está en el diagrama. Está en los tipos. Está en el log.


¿Estás implementando un agente para un proceso crítico de tu empresa y no quieres pagar el precio de aprenderlo a golpe de incidencias en producción?

Hablamos de la arquitectura antes de que el problema exista →

Compartir artículo

LinkedInXWhatsApp

¿Procesos repetitivos en tu empresa?

Descarga gratis el Mapa de Automatización IA — los 5 procesos que más tiempo roban y cómo resolverlos.

Sin spam. Solo el PDF. Puedes darte de baja cuando quieras.

Escrito por Daily Miranda Pardo

Ayudo a empresas a automatizar procesos, crear agentes IA y conectar sistemas inteligentes.