← Volver al inicio

Caso de estudio

AgentBeacon

¿Qué está haciendo mi agente de IA ahora mismo, y me necesita?

Rol
Diseño y desarrollo (solo)
Stack
TypeScript · Node.js · MCP · n8n
Estado
Fase 1 completada, en producción en un Raspberry Pi 5 propio

El problema

Trabajo con Claude Code y OpenAI Codex en varias máquinas: un par de MacBooks y un Mac Studio. Los agentes ya son lo bastante buenos para trabajar un buen rato sin mí, y ahí está el problema. Cuando uno se detiene a pedir un permiso o una decisión, nada me avisa. Me entero cuando vuelvo al terminal, a veces una hora después.

No quería un dashboard para estar mirándolo, ni una notificación por cada archivo que lee un agente. Quería una sola señal, y solo cuando importa: un agente me está esperando, terminó o falló.

La solución

Los agentes reportan un estado normalizado a través de una sola herramienta MCP, agentbeacon_set_status. AgentBeacon valida la entrada con zod, la convierte en un evento con una forma única, elimina duplicados y la envía por POST a un solo webhook genérico. Ese webhook es un workflow de n8n que corre en mi propio servidor, y n8n se encarga de llevar el mensaje a Telegram.

AgentBeacon no sabe nada de Telegram. Añadir Discord, email o un historial es un cambio en n8n, no un release de código.

Arquitectura

El contrato del evento es la frontera de la que cuelga todo lo demás. Los agentes escriben en él, los providers lo leen, y ninguno de los dos lados sabe que el otro existe.

Arquitectura de AgentBeacon. Un agente de IA (Claude Code o Codex) llama la herramienta MCP. El evento se valida y se normaliza, luego pasa al dispatcher, que lo envía al provider de webhook, de ahí a n8n y de ahí a Telegram. Las luces Philips Hue son un segundo provider planificado para la Fase 2, alimentado por el mismo dispatcher.

La regla que no negocio

src/core nunca importa de src/providers. Todo provider implementa la misma interfaz pequeña, { name, isEnabled(), accepts?(event), handle(event) }, y eso es lo único que el core llega a ver.

Modelo de estados

Cinco estados y ni uno más. Cada vez que me dieron ganas de añadir idle, paused o cancelled, no había una necesidad real detrás.

  • working Silencio

    Trabajo activo: leyendo, escribiendo, corriendo tests o un build.

  • waiting Silencio

    Esperando algo externo que no depende de mí, como un deploy.

  • needs_attention Notifica

    No puede seguir sin mí: un permiso, una decisión, información que falta.

  • completed Notifica

    La tarea terminó bien.

  • failed Notifica

    No pudo terminar, con la razón cuando la hay.

Por defecto solo needs_attention, completed y failed envían notificación. Cada uno se puede activar o desactivar con variables de entorno.

Decisiones clave y sus costos

  1. La identidad es máquina + agente + sesión

    Cada ejecución se identifica como MB16:codex:s1. Sin la máquina en la clave, dos máquinas que reportaban con el sessionId default caían en la misma entrada: una pisaba a la otra y la dedup silenciaba las notificaciones de la segunda. Es el tipo de bug que nunca ves probando con una sola máquina.

  2. El cliente nunca escoge su alias de máquina

    El alias se deriva en el servidor a partir del bearer token. Si el body trae un campo machine, se descarta. Una máquina no se puede hacer pasar por otra, y un error de tipeo en un archivo de configuración no convierte una máquina en dos. Los tokens se comparan en tiempo constante, y el modo HTTP se niega a arrancar si no hay tokens configurados.

  3. La dedup solo se activa después de una entrega exitosa

    Si un estado se repite idéntico, sale una sola notificación, pero la entrada de dedup solo se guarda cuando una entrega funcionó. Si todos los providers fallaron, publicar el mismo estado otra vez lo reintenta en lugar de tragárselo. Perder un needs_attention es el peor fallo que puede tener este sistema.

  4. Los fallos de un provider se quedan aislados

    El fan-out usa Promise.allSettled. Un provider que falla nunca tumba el proceso ni hace fallar la tarea del agente. El provider de webhook reintenta un número limitado de veces (3 por defecto, 5 como máximo) con backoff exponencial desde 250 ms y un timeout de 8 s. Solo reintenta errores de red, 429 y 5xx. Un 404 significa que la ruta de n8n está mal, y reintentar no lo va a arreglar.

  5. Borré el provider de Telegram

    Antes había un provider directo de Telegram. Eso implicaba tener un bot token y un chat id en el código, y sacar un release cada vez que quería un canal nuevo. Lo eliminé y le pasé la entrega a n8n. Ahora AgentBeacon hace una sola cosa: emitir un evento limpio hacia un solo webhook.

  6. Los secretos nunca llegan a los logs

    El logger oculta credenciales Bearer, campos authorization y cualquier cosa con forma de bot token de Telegram. En modo stdio los logs van a stderr, porque stdout es el canal del protocolo MCP y una línea de log perdida ahí rompe la sesión.

Despliegue

En producción hay una sola instancia compartida a la que apuntan todas las máquinas. Usa el transporte HTTP: MCP Streamable HTTP para los agentes, un endpoint REST POST /events para scripts y un /healthz público para chequeos.

  • Build de Docker multi-stage sobre node:24-alpine, arm64, corriendo con un usuario sin privilegios de root.
  • Desplegado con Dokploy en un Raspberry Pi 5 en casa.
  • Expuesto a través de Cloudflare Tunnel, así que no hay ningún puerto de entrada abierto en mi red.

El costo que acepté

Una sola réplica, y el estado vive en memoria, así que se reinicia con cada redeploy. Para la Fase 1 me parece bien. Sin base de datos y sin colas, a propósito, hasta que una fase de verdad los necesite.

En números

  • 238

    tests pasando

    node:test en ~250 ms, sin red y sin credenciales. fetch se inyecta.

  • ≈1.6×

    más código de tests que de fuente

    ~2,550 líneas de tests vs ~1,570 líneas de código fuente.

  • 2

    dependencias en runtime

    @modelcontextprotocol/sdk y zod. Lo demás es Node.

  • 1

    herramienta MCP

    Una herramienta con un campo de estado, no una por estado.

  • 5

    estados

    working, waiting, needs_attention, completed, failed.

Stack

  • TypeScript (strict, ESM)
  • Node.js
  • MCP SDK
  • zod
  • fetch nativo
  • node:test
  • Docker
  • Dokploy
  • Cloudflare Tunnel
  • n8n
  • Telegram

Lo que viene

  1. Fase 2

    Luces Philips Hue

    Un color por estado, para saber desde el otro lado del cuarto que un agente me necesita.

  2. Fase 4

    Pantalla física con ESP32

    Un aparatito en el escritorio que muestra qué está haciendo cada sesión activa.

  3. Fase 5

    Dashboard e historial

    La primera fase que necesita base de datos, así que primero lleva un ADR y después código.

Preguntas abiertas

  • Firmar los payloads del webhook.
  • Versionar el contrato del evento.

Lo que aprendí

  • El contrato del evento es la verdadera frontera. Cuando esa forma se estabilizó, añadir providers y transportes se volvió fácil, y borrarlos también.
  • Deja la entrega en manos de una herramienta de workflows. Meter los canales en el servicio significaba secretos en el código y un release por canal. n8n ya hace ese trabajo bien.
  • Los bugs que valía la pena arreglar no eran features. Eran colisiones de identidad y notificaciones que se perdían en silencio, las formas en que el sistema podía fallar sin decir nada.

¿Necesitas algo así?

Diseño y construyo sistemas pequeños, bien probados y honestos sobre sus limitaciones. Si tu equipo tiene un problema parecido a este, hablemos.

El código fuente es privado. Con gusto te lo enseño en una llamada.