Un sistema operativo organizacional para agentes
Event Agents Manager (EAM) es una plataforma para organizar, coordinar y observar sistemas de agentes jerárquicos. La idea que lo ordena todo es tratar una organización de agentes como se trata un sistema operativo: hay entidades con jerarquía, hay unidades de trabajo, y hay un registro de todo lo que pasó. Un “agente” acá puede ser un agente de IA o un rol humano modelado como tal — el sistema no distingue, porque el modelo es organizacional, no de ejecución.
Todo se apoya en cuatro entidades encadenadas: Project → Agent → Thread → Event. Un proyecto es un workspace aislado; adentro viven los agentes con su jerarquía, los threads que representan objetivos de trabajo, y los eventos que registran cada cosa que ocurrió. De ahí sale la trazabilidad: no hay un estado que se pisa a sí mismo, hay una historia que se puede leer entera.
Encima de ese modelo hay tres superficies que comparten un solo backend:
┌─────────────────────────────────────────┐
│ Frontend (React) │
│ Org View · Agents · Threads │
└────────────────────┬────────────────────┘
│ HTTP + WebSocket
┌────────────────────▼────────────────────┐
│ Backend (Fastify) │
│ REST API · WebSocket Broker │
└────────────────────┬────────────────────┘
│ Drizzle ORM
┌────────────────────▼────────────────────┐
│ PostgreSQL (Docker) │
└─────────────────────────────────────────┘
CLI (evam) ──► Backend APIEs un monorepo pnpm con tres apps (backend, frontend, cli) y dos packages compartidos
(shared, protocol), ~4.150 líneas de TypeScript. El repositorio es 100 % mío: 10 commits, todos de
mi autoría, concentrados entre el 2026-05-16 y el 2026-05-17.
Jerarquía de agentes, threads que se ramifican y eventos que no se borran
Esta es la parte del proyecto que importa. El resto — la UI, el CLI, la API — son formas de mirar y tocar este modelo; si el modelo está mal, no hay superficie que lo salve.
El agente y su jerarquía
Un Agent es cualquier actor dentro de un proyecto, y tiene dos ejes que lo describen. El primero es su
tipo: permanent para los agentes estables de la organización (un CEO, un Backend Lead, un Designer)
y temporary para workers efímeros creados para una tarea concreta (temp-auth-worker, temp-ui-worker).
Esa distinción no es cosmética: un agente temporal nace para un trabajo y se archiva cuando termina, y el
sistema deja constancia de las dos cosas con eventos propios (AGENT_SPAWNED, AGENT_ARCHIVED).
El segundo eje es su estado — idle, working, blocked, completed, archived. Notar que
archived no es “borrado”: significa que el agente ya no está activo pero su historial se preserva. Es la
misma decisión que aparece en todas las capas del sistema.
La jerarquía se resuelve con un único campo: cada agente puede tener un parentId que apunta a otro
agente, y ese padre es su manager. Con eso solo ya se arma el árbol completo:
CEO Agent
├── Backend Lead
│ ├── Auth Specialist
│ └── DB Specialist
└── Frontend Lead
└── UI AgentLos threads y sus ramas
Un Thread es un workflow, un objetivo, un contexto de trabajo — “Implementar sistema de login”, “Migrar
base de datos a PostgreSQL” — y agrupa todos los eventos y delegaciones relacionados a esa misma tarea.
Tiene su propio ciclo de vida (open, in_progress, blocked, completed, archived) y, igual que los
agentes, puede apuntar a un padre: un parentThreadId que convierte una tarea en un árbol de trabajo.
Thread: "Implementar Auth"
├── Sub-thread: "Backend — JWT"
└── Sub-thread: "Frontend — Login Form"La simetría es deliberada: la misma forma de modelar la jerarquía sirve para quién manda a quién y para qué trabajo depende de qué trabajo. Dos árboles, un solo patrón de datos.
Por qué los eventos son inmutables
Acá está la decisión central. El sistema es event-driven: toda interacción organizacional se modela como un evento, no como un mensaje ni como un campo de estado que se sobreescribe. Y los eventos son inmutables — nunca se modifican, solo se agregan.
La anatomía de un evento es deliberadamente chica:
{
"id": "uuid",
"type": "TASK_ASSIGNED",
"threadId": "id-del-thread",
"agentId": "id-del-agente-origen",
"targetAgentId": "id-del-agente-destino",
"payload": { "task": "Implementar JWT middleware" },
"createdAt": "2026-05-16T18:00:00Z"
}agentId es quién generó el evento; targetAgentId, a quién va dirigido (puede ser null, porque no
todo evento tiene destinatario); y payload es JSON libre, el único lugar donde el modelo se afloja a
propósito para no tener que anticipar cada caso de uso.
El vocabulario de tipos es cerrado y describe una organización trabajando, no un sistema técnico:
THREAD_CREATED, TASK_ASSIGNED, TASK_STARTED, TASK_COMPLETED, DELEGATED, AGENT_SPAWNED,
AGENT_ARCHIVED, SUMMARY_CREATED, BLOCKED, UNBLOCKED, ERROR. Un flujo típico se lee entero en ese
vocabulario: un lead asigna (TASK_ASSIGNED), el worker arranca (TASK_STARTED), hay sub-delegaciones
(DELEGATED), y se cierra (TASK_COMPLETED).
Lo que se gana con eso es trazabilidad completa: el estado actual de un thread siempre se puede
reconstruir leyendo su historia, y la historia nunca miente porque nadie la edita. Un agente bloqueado no
es un flag que alguien prendió y apagó; son un BLOCKED y un UNBLOCKED con su momento exacto y su
motivo.
Lo que se pierde es la comodidad de un UPDATE. Corregir algo mal registrado no es editar una fila:
es agregar un evento nuevo que lo compensa, y toda vista que quiera mostrar “cómo están las cosas ahora”
tiene que derivarlo del historial en vez de leerlo directo. Es más trabajo de lectura a cambio de que
nada se pierda — y para un sistema cuyo propósito es observar qué hicieron los agentes, ese es el
intercambio correcto.
El contrato de eventos entre agentes no quedó implícito en el código: vive escrito aparte, en
packages/protocol/protocol.md, como documento propio.
Una UI, un CLI y una API sobre el mismo modelo
El modelo de datos se expone por tres caminos distintos, y ninguno es un envoltorio del otro: el CLI no habla con la UI ni la UI con el CLI. Los dos hablan con la misma API REST del backend.
UI web — el organigrama en vivo
React 19 + Vite, con @xyflow/react (React Flow) para dibujar el grafo de agentes y elkjs para calcular el layout jerárquico automáticamente — nadie acomoda nodos a mano. El Org View muestra el árbol de agentes; la Timeline, la historia de eventos de un thread. El estado del cliente vive en Zustand, y un indicador de conexión en el sidebar dice si el WebSocket está activo.
CLI evam — un comando por entidad
Commander + chalk + cli-table3, con un archivo de comandos por entidad del modelo (project, agent, thread, event, instruction, session, template, init). La superficie de la terminal es un espejo del modelo de datos, no un set arbitrario de atajos. Empezó llamándose eam y se renombró a evam sobre el final.
Backend Fastify — REST + WebSocket
Fastify 4 con una ruta por entidad (agents, events, graph, instructions, projects, threads), Drizzle ORM sobre PostgreSQL y Zod para validar la entrada. El tiempo real lo resuelve src/ws/broker.ts con @fastify/websocket: cada mutación de la API emite su evento y el broker lo difunde, sin cola de mensajes de por medio.
Que el CLI consuma la misma API REST que el frontend es lo que hace que las tres superficies no se desincronicen: no hay una segunda implementación del modelo esperando divergir. El costo es que el CLI no puede hacer nada que la API no exponga — cualquier atajo cómodo de terminal exige primero abrirle la puerta en el backend.
El resto del stack sostiene eso mismo: PostgreSQL 16 se levanta con Docker Compose para que el entorno de
desarrollo sea reproducible, y los tipos compartidos viven en packages/shared para que las tres apps
hablen del mismo Agent y del mismo Event.
Construido, usado y archivado
Un corte ordenado, no un abandono a mitad de camino
El proyecto está archivado: se construyó, se usó, y hoy no está en desarrollo activo. El repositorio lo muestra con bastante claridad — los últimos commits no son una feature dejada por la mitad sino housekeeping: renombrar el binario eam a evam y reacomodar la documentación de contexto bajo docs/context/. Es una versión cerrada que quedó quieta, no un frente abierto.
Sin tests y sin CI, y prefiero decirlo
No hay suite de tests ni workflow de integración continua en el repositorio. Es la contracara de haberlo construido en un tramo corto y concentrado, y no tiene sentido presentar como sólido algo que no pasó por esa red. Lo que sí sostiene el caso es el modelo de datos y la documentación: docs/ cubre conceptos, API, UI, CLI y setup, y el protocolo de eventos está escrito aparte.
El repositorio es público y está entero acá: 10 commits, ~4.150 líneas de TypeScript, escritas entre el 2026-05-16 y el 2026-05-17. Es un proyecto chico en volumen y grande en modelo — casi todo el trabajo se fue en decidir qué entidades existen y cómo se relacionan, que es exactamente lo que queda cuando el código deja de correr.