⏱️ Lectura: 13 min

Aviso: MAHORAGA es un bot de trading. Este artículo se enfoca exclusivamente en los patrones de ingeniería reusables para agentes LLM persistentes. No promovemos su uso para trading ni damos consejos financieros — estudiamos el repositorio como caso educativo porque expone decisiones arquitectónicas interesantes aplicables a moderación, enrutamiento, chatbots o monitoreo.

📑 En este artículo
  1. Por qué necesitamos agentes LLM persistentes 24/7
  2. Qué es un Durable Object, explicado desde cero
  3. D1 vs KV vs R2: qué usar para cada cosa
  4. Alarms: el bucle auto-renovable
  5. Abstracción multi-provider con Vercel AI SDK
  6. Pluggable Strategy Pattern
  7. Kill switch de dos niveles
  8. Observabilidad con Discord webhooks y cooldown
  9. El flujo completo de un tick
  10. Límites reales del free tier en 2026
  11. Alternativas cuando Workers no alcanza
  12. Cuándo NO usar Workers para tu agente
  13. Preguntas frecuentes
    1. ¿Los Durable Objects son más caros que Redis + un worker tradicional?
    2. ¿Qué pasa si mi agente sobrepasa los 10 ms de CPU en un tick?
    3. ¿Puedo tener miles de agentes en paralelo con este patrón?
    4. ¿Cómo pruebo un agente con alarms localmente?
    5. ¿MAHORAGA recomienda tradear con este código?
    6. ¿Cuándo empezar a pagar?
  14. Referencias
    1. 📚 Artículos relacionados

Si alguna vez intentaste correr un agente LLM que necesita recordar lo que hizo hace diez minutos, sobrevivir a reinicios, despertarse solo cada cierto tiempo y seguir funcionando después de que cierres el laptop, te topaste con un problema feo: el edge serverless tradicional es amnésico. Ahí entran los Durable Objects de Cloudflare, la pieza que transforma Workers de «funciones efímeras» en algo parecido a un microservicio persistente con memoria propia. Este artículo desarma cómo los usa un agente real (MAHORAGA, 799 estrellas en GitHub) y destila los patrones reusables para cualquier agente LLM 24/7.

Por qué necesitamos agentes LLM persistentes 24/7

Un agente LLM moderno no es solo un endpoint que recibe un prompt y responde. Es un bucle: observa, piensa, actúa, registra, vuelve a observar. Quiere leer su historial, consultar su último estado, disparar una acción, esperar, y repetir. Ese patrón choca frontalmente con el modelo mental del edge serverless clásico, donde cada request nace sin memoria y muere a los pocos milisegundos.

En Cloudflare Workers sin ayuda, guardar estado es caro y propenso a carreras: dos invocaciones simultáneas pueden pisarse escribiendo KV. No hay un «proceso de fondo» que siga corriendo entre requests. No hay un setInterval que sobreviva al próximo cold start. Y sin estado coherente, un agente no puede ser un agente — se convierte en un montón de funciones que se ignoran entre sí.

Los Durable Objects resuelven esto dándote una abstracción rara pero poderosa: una clase TypeScript instanciada en un único lugar del mundo, con storage propio, un único hilo lógico de ejecución por instancia, y la capacidad de despertarse sola. Es un microservicio con identidad estable, pero facturado por uso. Pensalo así: si Workers es AWS Lambda, un Durable Object es un contenedor con estado que existe mientras haya algo que hacer, colapsa cuando no, y se reanima exactamente donde lo dejaste cuando llega el próximo evento.

Qué es un Durable Object, explicado desde cero

Un Durable Object es una clase de JavaScript/TypeScript que extiende DurableObject. Cada instancia tiene un identificador único (derivado de un nombre o generado aleatoriamente) y vive en un datacenter específico. Todas las llamadas a esa instancia van al mismo lugar, en orden, sin concurrencia caótica. El runtime de Cloudflare garantiza consistencia fuerte dentro del objeto: no hay dos réplicas simultáneas pisando el mismo storage.

El patrón mínimo para un agente luce así:

// src/agent.ts
import { DurableObject } from 'cloudflare:workers';

interface Env {
  AGENT: DurableObjectNamespace<AgentDO>;
  AI_PROVIDER: string;
  OPENAI_API_KEY: string;
}

export class AgentDO extends DurableObject<Env> {
  async tick() {
    const last = (await this.ctx.storage.get<number>('last')) ?? 0;
    const now = Date.now();
    await this.ctx.storage.put('last', now);
    return { elapsedMs: now - last };
  }
}

export default {
  async fetch(req: Request, env: Env) {
    const id = env.AGENT.idFromName('singleton');
    const stub = env.AGENT.get(id);
    return Response.json(await stub.tick());
  },
} satisfies ExportedHandler<Env>;

Tres cosas importantes: ctx.storage es una key-value transaccional local al objeto; idFromName('singleton') garantiza que siempre hables con la misma instancia; y stub.tick() invoca el método remoto tipado, con RPC real — nada de fetch con strings.

💡 Tip: usá idFromName cuando quieras una instancia por tenant, por conversación o por agente. Usá newUniqueId cuando cada entidad deba ser efímera y no rastreable por nombre.

Arquitectura de un agente LLM persistente con Durable Objects, D1 y KV en Cloudflare Workers
Un agente 24/7 con Durable Objects, D1 para historial y KV para caché.

D1 vs KV vs R2: qué usar para cada cosa

Un agente serio necesita tres tipos de memoria: caliente (estado del tick actual), tibia (historial consultable) y fría (artefactos grandes). En Cloudflare, eso mapea a tres productos distintos.

  • Durable Object storage — estado del propio agente (flags, contadores, último timestamp). Hasta 5 GB por objeto usando el backend SQLite, transaccional, 0 ms de latencia desde dentro del DO.
  • D1 — base de datos SQL compartida entre muchos objetos: logs estructurados, historial de decisiones, métricas agregadas. Ideal cuando querés correr SELECTs con filtros o joins.
  • KV — caché global distribuido, lecturas baratísimas y eventualmente consistente. Perfecto para memoizar respuestas de LLM caras o cachear configuración leída muchas veces.
  • R2 — almacenamiento de objetos compatible con S3, sin egreso. Para artefactos binarios: embeddings serializados, capturas, PDFs generados por el agente.

La regla mental es sencilla: estado que solo le importa a un agente va al storage del propio DO; estado consultable por humanos o dashboards va a D1; lecturas masivas tolerantes a staleness van a KV; binarios pesados van a R2.

// Ejemplo mixto dentro del DO
async logDecision(event: string, payload: unknown) {
  // 1) Estado caliente local
  await this.ctx.storage.put('lastEvent', event);

  // 2) Historial auditable en D1
  await this.env.DB.prepare(
    'INSERT INTO decisions (ts, event, payload) VALUES (?, ?, ?)'
  ).bind(Date.now(), event, JSON.stringify(payload)).run();

  // 3) Caché de la última decisión para lectores externos
  await this.env.CACHE.put('agent:last', event, { expirationTtl: 60 });
}

Alarms: el bucle auto-renovable

Acá está el verdadero truco. Un Durable Object puede programar una alarma con ctx.storage.setAlarm(timestamp). Cuando llegue ese timestamp, Cloudflare invocará el método alarm() del objeto, aunque no haya ningún request entrante. Si dentro del alarm() programás la siguiente alarma, tenés un bucle perpetuo sin servidor corriendo.

export class LoopingAgent extends DurableObject<Env> {
  async start() {
    await this.ctx.storage.setAlarm(Date.now() + 30_000);
  }

  async alarm() {
    try {
      await this.runCycle();
    } catch (err) {
      await this.reportError(err);
    } finally {
      // Re-programar para el próximo ciclo
      await this.ctx.storage.setAlarm(Date.now() + 30_000);
    }
  }

  async stop() {
    await this.ctx.storage.deleteAlarm();
  }

  private async runCycle() { /* observar, pensar, actuar */ }
  private async reportError(e: unknown) { /* webhook */ }
}

Este patrón es el corazón de MAHORAGA: un ciclo de 30 segundos que consulta el mundo, le pide al LLM una decisión, la registra en D1 y se vuelve a dormir. El finally re-programa la alarma aun si hubo un error; así un fallo transitorio no mata el agente. Si necesitás ciclos más largos o en horarios específicos, complementalo con Cron Triggers del Worker (máximo 5 por cuenta en el plan gratuito).

Abstracción multi-provider con Vercel AI SDK

Un agente que se case con un solo proveedor de LLM es un agente frágil. Los precios cambian, la latencia cambia, los modelos se retiran. El Vercel AI SDK ofrece una capa uniforme sobre OpenAI, Anthropic, Google, xAI, DeepSeek y otros. Con una sola variable de entorno cambiás de cerebro.

import { generateText } from 'ai';
import { openai } from '@ai-sdk/openai';
import { anthropic } from '@ai-sdk/anthropic';
import { google } from '@ai-sdk/google';

function pickModel(env: Env) {
  switch (env.AI_PROVIDER) {
    case 'anthropic': return anthropic('claude-opus-4-7');
    case 'google':    return google('gemini-2.5-pro');
    case 'openai':
    default:          return openai('gpt-5.1');
  }
}

export async function think(env: Env, prompt: string) {
  const { text } = await generateText({
    model: pickModel(env),
    prompt,
    maxTokens: 512,
  });
  return text;
}

Este patrón tiene un beneficio adicional: podés correr A/B entre providers para el mismo agente cambiando un secret, sin tocar código. En un kill switch, incluso podés degradar a un modelo más barato cuando el presupuesto se acerque al límite.

Pluggable Strategy Pattern

Un agente LLM bien diseñado separa el core (bucle, storage, logging) de las estrategias (qué hacer en cada tick). Agregar una nueva capacidad no debería requerir tocar el núcleo.

export interface Strategy {
  readonly name: string;
  run(ctx: StrategyContext): Promise<StrategyResult>;
}

const registry = new Map<string, Strategy>();
export const registerStrategy = (s: Strategy) => registry.set(s.name, s);
export const getStrategy = (name: string) => registry.get(name);

// Plug nuevo comportamiento sin tocar el core:
registerStrategy({
  name: 'moderation',
  async run({ input, llm }) {
    const verdict = await llm(`¿Es apropiado? ${input}`);
    return { action: verdict.startsWith('sí') ? 'approve' : 'reject' };
  },
});

Con este patrón, moderación, enrutamiento, chatbot y monitoreo son solo estrategias distintas registradas en el mismo agente. El core nunca aprende qué hacen; solo las orquesta.

Kill switch de dos niveles

Todo agente autónomo necesita un freno de mano. MAHORAGA usa dos niveles: un token pausa que detiene nuevas acciones pero permite terminar la actual, y un token halt que aborta incluso el ciclo en curso. Ambos viven en KV para que una lectura sea rápida y global.

async guard(env: Env): Promise<'run' | 'pause' | 'halt'> {
  const halt  = await env.CACHE.get('kill:halt');
  if (halt)  return 'halt';
  const pause = await env.CACHE.get('kill:pause');
  if (pause) return 'pause';
  return 'run';
}

⚠️ Ojo: un kill switch que requiere un deploy para activarse no es un kill switch. Guardá el flag en KV o D1 y revisalo al inicio de cada tick — así podés parar todo desde un dashboard sin esperar CI.

Observabilidad con Discord webhooks y cooldown

Los agentes LLM fallan de formas raras: alucinan, se quedan esperando un API caído, consumen tokens sin producir output. Necesitás notificaciones, pero sin spamear el canal cuando el error se repite mil veces.

async notify(env: Env, msg: string, key: string) {
  const lastMs = Number((await env.CACHE.get(`notify:${key}`)) ?? 0);
  if (Date.now() - lastMs < 5 * 60_000) return; // cooldown 5 min
  await env.CACHE.put(`notify:${key}`, String(Date.now()), { expirationTtl: 600 });
  await fetch(env.DISCORD_WEBHOOK, {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({ content: msg }),
  });
}

La clave es el dedupe key: agrupá errores por tipo o estrategia para que un bucle roto no dispare 10k mensajes. Discord, Slack o Telegram funcionan igual; lo importante es el patrón.

Diagrama del flujo de un tick de agente LLM con alarms en Durable Objects
Cada tick: guard, estrategia, LLM, persistencia, re-alarma.

El flujo completo de un tick

graph LR;
 A[Alarm dispara] --> B{Kill switch?};
 B -- halt --> Z[Detener];
 B -- run --> C[Cargar estado];
 C --> D[Ejecutar estrategia];
 D --> E[Llamar LLM];
 E --> F[Persistir en D1];
 F --> G[Notificar si error];
 G --> H[Re-programar alarma];

Límites reales del free tier en 2026

El plan gratuito de Cloudflare alcanza para un agente personal serio, pero hay que conocer los techos:

  • Workers — 100.000 requests/día y 10 ms de CPU por invocación.
  • Durable Objects — 100.000 requests/día, 13.000 GB-s de duración y 5 GB de SQLite storage por objeto.
  • D1 — 5 millones de filas leídas/día, 100.000 escritas/día, 5 GB totales.
  • KV — 100.000 reads/día, 1.000 writes/día, 1 GB.
  • Cron Triggers — máximo 5 por cuenta.

Un agente con un tick cada 30 s consume 2.880 invocaciones de alarm/día — holgadamente dentro del free tier. Si tu agente escala a miles de tenants (un DO por tenant), probablemente pases al Workers Paid plan ($5/mes) bastante antes de chocar con los límites de storage.

Alternativas cuando Workers no alcanza

Durable Objects es una herramienta fenomenal, pero no la única. Cuando tu agente sobrepasa lo que un Worker puede hacer, estas son las opciones razonables en 2026:

  • Fly.io Machines — VMs firecracker que escalan a cero y arrancan en milisegundos. Ideal si necesitás deps nativas (Python con torch, binarios C++) o tareas largas.
  • Temporal / Hatchet — orquestadores de durable execution. Tu workflow sobrevive crashes, reintentos, despliegues. Más complejidad operacional pero garantías formales.
  • Inngest — step functions con timers y reintentos, pensados para desarrolladores JS/TS. Curva muy baja, ideal para agentes basados en eventos.
  • Upstash QStash — cola serverless con delay, reintentos y webhooks. Perfecta si tu «agente» es una secuencia de jobs más que un bucle.

Cuándo NO usar Workers para tu agente

Aunque Durable Objects te lleva lejos, hay casos donde la elección correcta es otro runtime:

  • Tareas individuales que necesitan más de 30 segundos de wall time continuo sin retomar — el modelo de alarms quiere ticks cortos.
  • Workloads que requieren GPU (inferencia local, fine-tuning) — Workers no expone GPUs; mirá Workers AI o un provider dedicado.
  • Dependencias nativas en C/C++ o Python que no compilan a WASM y requieren un runtime completo.
  • Procesos que abren conexiones TCP long-lived a bases de datos tradicionales con pooling complejo — el edge no es su hábitat natural.

📖 Resumen en Telegram: Ver resumen

Preguntas frecuentes

¿Los Durable Objects son más caros que Redis + un worker tradicional?

Depende del patrón de acceso. Para estado que se lee y escribe desde el propio objeto, el storage interno es gratis en tiempo de CPU y más barato que mantener un Redis siempre encendido. Para fan-out masivo de lecturas, un Redis dedicado puede ganar.

¿Qué pasa si mi agente sobrepasa los 10 ms de CPU en un tick?

El plan gratuito te cortará la invocación. La solución es partir el trabajo: cada alarm hace una fracción y reprograma otra alarma inmediata. El plan pago eleva el límite a 30 segundos de CPU por invocación, suficiente para la mayoría de agentes.

¿Puedo tener miles de agentes en paralelo con este patrón?

Sí — un Durable Object por tenant, conversación o entidad. Cada uno escala independientemente y solo cobra cuando tiene actividad. Es uno de los sweet spots más claros del modelo.

¿Cómo pruebo un agente con alarms localmente?

Wrangler en modo dev simula alarms, DO storage, D1 y KV de forma local. No necesitás subir al edge para iterar. Para tests automáticos, exponé los métodos del DO y llamalos en Vitest con un stub del storage.

¿MAHORAGA recomienda tradear con este código?

No, y este artículo tampoco. Lo estudiamos solo como un caso bien documentado de un agente autónomo persistente. Cualquiera de los patrones descritos aplica a moderación de contenidos, chatbots de soporte, monitores de infraestructura o enrutadores inteligentes.

¿Cuándo empezar a pagar?

Cuando uno de estos tres sea cierto: necesitás más de 100k requests/día a tus DOs, excedés 5 GB de SQLite en un objeto, o requerís CPU superior a 10 ms por tick. Los $5/mes del Workers Paid suelen alcanzar para agentes personales de producción.

Referencias

📱 ¿Te gusta este contenido? Únete a nuestro canal de Telegram @programacion donde publicamos a diario lo más relevante de tecnología, IA y desarrollo. Resúmenes rápidos, contenido fresco todos los días.


0 Comentarios

Deja un comentario

Marcador de posición del avatar

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.