LUCIANO CARRIZO
All projects
Archived

Event Agents Manager

A platform for organizing and observing hierarchical agent systems: it models Project → Agent → Thread → Event and provides complete traceability because events are immutable. Three surfaces on the same backend — a web UI with the live org chart, a CLI (evam), and a REST + WebSocket API. Archived TypeScript monorepo.

TypeScriptReactNode.jsFastifyPostgreSQLDocker
Overview

An organizational operating system for agents

Event Agents Manager (EAM) is a platform for organizing, coordinating, and observing systems of hierarchical agents. The idea that organizes everything is to treat an agent organization like an operating system: there are hierarchical entities, units of work, and a record of everything that happened. An “agent” here can be an AI agent or a human role modeled as one—the system does not distinguish between them, because the model is organizational, not about execution.

Everything rests on four linked entities: Project → Agent → Thread → Event. A project is an isolated workspace; inside it live agents and their hierarchy, threads representing work objectives, and events that record everything that occurred. That is where traceability comes from: there is no state overwriting itself, but a history that can be read in full.

On top of that model are three surfaces sharing a single backend:

┌─────────────────────────────────────────┐
│             Frontend (React)             │
│         Org View · Agents · Threads      │
└────────────────────┬────────────────────┘
                     │ HTTP + WebSocket
┌────────────────────▼────────────────────┐
│            Backend (Fastify)             │
│    REST API · WebSocket Broker           │
└────────────────────┬────────────────────┘
                     │ Drizzle ORM
┌────────────────────▼────────────────────┐
│           PostgreSQL (Docker)            │
└─────────────────────────────────────────┘

        CLI (evam) ──► Backend API

It is a pnpm monorepo with three apps (backend, frontend, cli) and two shared packages (shared, protocol), totaling roughly 4,150 lines of TypeScript. The repository is entirely mine: 10 commits, all authored by me, concentrated between 2026-05-16 and 2026-05-17.

The data model

Agent hierarchies, branching threads, and events that are never deleted

This is the part of the project that matters. Everything else—the UI, CLI, and API—is a way to view and manipulate this model; if the model is wrong, no surface can save it.

The agent and its hierarchy

An Agent is any actor within a project, described along two axes. The first is its type: permanent for stable agents in the organization (a CEO, Backend Lead, or Designer), and temporary for ephemeral workers created for a specific task (temp-auth-worker, temp-ui-worker). That distinction is not cosmetic: a temporary agent is born for one job and archived when it is done, and the system records both moments with dedicated events (AGENT_SPAWNED, AGENT_ARCHIVED).

The second axis is its statusidle, working, blocked, completed, archived. Notice that archived does not mean “deleted”: the agent is no longer active, but its history is preserved. The same decision appears throughout every layer of the system.

The hierarchy is resolved with a single field: each agent can have a parentId pointing to another agent, and that parent is its manager. That alone builds the complete tree:

CEO Agent
├── Backend Lead
│   ├── Auth Specialist
│   └── DB Specialist
└── Frontend Lead
    └── UI Agent

Threads and their branches

A Thread is a workflow, objective, or work context—“Implement login system,” “Migrate database to PostgreSQL”—and groups all events and delegations related to that task. It has its own lifecycle (open, in_progress, blocked, completed, archived) and, like agents, can point to a parent: a parentThreadId that turns one task into a work tree.

Thread: "Implement Auth"
├── Sub-thread: "Backend — JWT"
└── Sub-thread: "Frontend — Login Form"

The symmetry is deliberate: the same way of modeling hierarchy works both for who manages whom and which work depends on which work. Two trees, one data pattern.

Why events are immutable

This is the central decision. The system is event-driven: every organizational interaction is modeled as an event, rather than as a message or a state field that gets overwritten. And events are immutable— they are never modified, only appended.

An event’s anatomy is deliberately small:

{
  "id": "uuid",
  "type": "TASK_ASSIGNED",
  "threadId": "thread-id",
  "agentId": "source-agent-id",
  "targetAgentId": "destination-agent-id",
  "payload": { "task": "Implement JWT middleware" },
  "createdAt": "2026-05-16T18:00:00Z"
}

agentId identifies who generated the event; targetAgentId, whom it targets (it can be null, because not every event has a recipient); and payload is free-form JSON, the only place where the model is intentionally relaxed so it does not need to anticipate every use case.

The vocabulary of event types is closed and describes an organization at work, not a technical system: THREAD_CREATED, TASK_ASSIGNED, TASK_STARTED, TASK_COMPLETED, DELEGATED, AGENT_SPAWNED, AGENT_ARCHIVED, SUMMARY_CREATED, BLOCKED, UNBLOCKED, ERROR. A typical flow can be read entirely in that vocabulary: a lead assigns (TASK_ASSIGNED), the worker starts (TASK_STARTED), sub-delegations occur (DELEGATED), and the work closes (TASK_COMPLETED).

What this gains is complete traceability: a thread’s current state can always be reconstructed by reading its history, and that history never lies because nobody edits it. A blocked agent is not a flag someone switched on and off; it is a BLOCKED and an UNBLOCKED, each with its exact time and reason.

What this gives up is the convenience of an UPDATE. Correcting a bad record does not mean editing a row: it means appending a compensating event, and any view that wants to show “how things stand now” must derive it from history instead of reading it directly. It is more read-side work in exchange for losing nothing—and for a system whose purpose is to observe what agents did, that is the right tradeoff.

The event contract between agents was not left implicit in the code: it lives separately in packages/protocol/protocol.md as its own document.

Three surfaces

A UI, a CLI, and an API on the same model

The data model is exposed through three different paths, and none wraps another: the CLI does not talk to the UI, nor the UI to the CLI. Both talk to the backend’s same REST API.

01

Web UI — the live org chart

React 19 + Vite, with @xyflow/react (React Flow) to draw the agent graph and elkjs to calculate the hierarchical layout automatically—nobody positions nodes by hand. Org View displays the agent tree; Timeline shows a thread's event history. Client state lives in Zustand, and a connection indicator in the sidebar shows whether the WebSocket is active.

02

evam CLI — one command per entity

Commander + chalk + cli-table3, with one command file per model entity (project, agent, thread, event, instruction, session, template, init). The terminal surface mirrors the data model rather than offering an arbitrary set of shortcuts. It started out named eam and was renamed evam near the end.

03

Fastify backend — REST + WebSocket

Fastify 4 with one route per entity (agents, events, graph, instructions, projects, threads), Drizzle ORM over PostgreSQL, and Zod for input validation. Real-time behavior is handled by src/ws/broker.ts with @fastify/websocket: every API mutation emits its event and the broker broadcasts it, with no message queue in between.

Having the CLI consume the same REST API as the frontend keeps the three surfaces from falling out of sync: there is no second implementation of the model waiting to diverge. The cost is that the CLI cannot do anything the API does not expose—every convenient terminal shortcut first needs a door opened in the backend.

The rest of the stack supports the same arrangement: PostgreSQL 16 starts through Docker Compose to keep the development environment reproducible, and shared types live in packages/shared so all three apps speak of the same Agent and the same Event.

Closing

Built, used, and archived

Status

An orderly stopping point, not an abandoned half-finished project

The project is archived: it was built, used, and is not under active development today. The repository makes that fairly clear—the last commits are not a feature left halfway done but housekeeping: renaming the eam binary to evam and reorganizing context documentation under docs/context/. It is a completed version that became still, not an open front.

Accuracy

No tests and no CI, and I would rather say so

There is no test suite or continuous-integration workflow in the repository. That is the other side of building it in one short, concentrated stretch, and there is no point presenting as solid something that never passed through that safety net. What does support the case is the data model and documentation: docs/ covers concepts, API, UI, CLI, and setup, while the event protocol is documented separately.

The repository is public and complete: 10 commits and roughly 4,150 lines of TypeScript, written between 2026-05-16 and 2026-05-17. It is a small project by volume and a large one by model—almost all of the work went into deciding which entities exist and how they relate, which is exactly what remains when the code stops running.

Next step

The case shows the code. Let’s talk about what comes next.

Event Agents Manager is documented end to end: architecture, decisions, and what remains unfinished. If you have questions about the approach, write to me.

Let’s talk

or directly → LuchoC.dev@gmail.com