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 APIIt 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.
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 status—idle, 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 AgentThreads 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.
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.
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.
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.
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.
Built, used, and archived
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.
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.