zayra-events
zayra-events
The central event communication layer for ZAYRA AI. zayra-events lets every zayra-* package publish and subscribe to events without depending on each other directly — zayra-task-engine doesn't need to know zayra-memory exists to tell it a task finished; both just talk to the same EventManager.
This is version 0.0.1. It contains no frontend UI, no business logic, and no AI decision-making — this package only handles communication.
Installation
npm install zayra-events
Usage
import { EventManager, TaskEvent } from "zayra-events";
const events = new EventManager();
events.on(TaskEvent.COMPLETED, (envelope) => {
console.log(`Task ${envelope.data.taskId} completed at ${envelope.timestamp}`);
});
await events.emit(TaskEvent.COMPLETED, { taskId: "t1" }, { source: "zayra-task-engine" });
Event Manager API
events.on(eventName, handler); // subscribe — returns an unsubscribe function
events.once(eventName, handler); // subscribe for one occurrence, then auto-unsubscribe
events.removeListener(eventName, handler);
events.removeAllListeners(eventName?); // omit eventName to clear every listener on every event
events.listenerCount(eventName);
await events.emit(eventName, data?, { source? }); // publish — see "emit() semantics" below
Event data format
Every event on() receives, and every result emit() resolves to, follows the same shape:
{
event: "task.completed",
timestamp: "2026-07-19T12:00:00.000Z",
data: { taskId: "t1" },
source: "zayra-task-engine", // whichever module passed { source } to emit(), or null
}
Standard events
import { SystemEvent, ModuleEvent, PluginEvent, AiEvent, MemoryEvent, TaskEvent } from "zayra-events";
SystemEvent.STARTING; // "system.starting"
SystemEvent.STARTED; // "system.started"
SystemEvent.READY; // "system.ready"
SystemEvent.SHUTDOWN; // "system.shutdown"
ModuleEvent.LOADED; // "module.loaded"
ModuleEvent.STARTED; // "module.started"
ModuleEvent.FAILED; // "module.failed"
ModuleEvent.REMOVED; // "module.removed"
PluginEvent.INSTALLED; // "plugin.installed"
PluginEvent.LOADED; // "plugin.loaded"
PluginEvent.ENABLED; // "plugin.enabled"
PluginEvent.DISABLED; // "plugin.disabled"
PluginEvent.REMOVED; // "plugin.removed"
AiEvent.MODEL_SELECTED; // "model.selected"
AiEvent.PROVIDER_CONNECTED; // "provider.connected"
AiEvent.PROVIDER_FAILED; // "provider.failed"
AiEvent.REQUEST_STARTED; // "ai.request.started"
AiEvent.REQUEST_COMPLETED; // "ai.request.completed"
AiEvent.REQUEST_FAILED; // "ai.request.failed"
MemoryEvent.CREATED; // "memory.created"
MemoryEvent.UPDATED; // "memory.updated"
MemoryEvent.DELETED; // "memory.deleted"
MemoryEvent.RECALLED; // "memory.recalled"
TaskEvent.CREATED; // "task.created"
TaskEvent.STARTED; // "task.started"
TaskEvent.COMPLETED; // "task.completed"
TaskEvent.FAILED; // "task.failed"
These constants exist to prevent typos — they're a catalog, not a restriction. See Plugin compatibility below.
Plugin compatibility (custom events)
Any dot-namespaced string is a valid event name, standard or not:
events.on("custom.plugin.event", (envelope) => { /* ... */ });
await events.emit("custom.plugin.event", { anything: true });
import { isStandardEvent } from "zayra-events";
isStandardEvent("task.completed"); // true
isStandardEvent("custom.plugin.event"); // false — still perfectly valid to emit/listen to
emit() semantics
- Listeners run concurrently, each on its own microtask — a slow or misbehaving listener never blocks the others, and
emit()never blocks your synchronous code unless you explicitlyawaitit (Performance Rules: lightweight, asynchronous, avoid blocking operations). - A throwing or rejecting listener never crashes
emit()or stops other listeners — its failure is caught and reported in the result instead. once()listeners are removed after they run, whether they succeeded or threw.
const outcome = await events.emit("ai.request.completed", { ms: 420 });
outcome.result;
// { listenerCount: 3, errorCount: 1, errors: [{ name, code: "LISTENER_FAILURE", message, listenerName }] }
emit() itself only throws for a genuinely invalid call — a malformed event name, or an unexpected internal failure — never for a listener's own mistake.
Event history
events.history.list(); // everything recorded so far, oldest first
events.history.list({ event: "task.completed" }); // filter by event name
events.history.list({ source: "zayra-memory" }); // filter by source
events.history.list({ limit: 20 }); // only the most recent 20
events.history.size; // current entry count
events.history.clear();
Each entry: { event, timestamp, source, result }. Capped at maxSize (default 500, configurable via new EventManager({ historySize: 1000 })) so long-running processes don't grow this unbounded — the oldest entry is dropped first once full.
Cross-package communication, without a direct dependency
// Inside zayra-task-engine's manager.js (or any module wired with the same EventManager):
await events.emit(TaskEvent.COMPLETED, { taskId: task.id }, { source: "zayra-task-engine" });
// Inside zayra-memory's manager.js — no import of zayra-task-engine anywhere:
events.on(TaskEvent.COMPLETED, async (envelope) => {
await memory.save({ content: `Task ${envelope.data.taskId} completed.` });
});
Both sides only need zayra-events — this is what lets zayra-core, zayra-config, zayra-memory, zayra-tools, zayra-plugins, zayra-provider, and zayra-task-engine communicate without any of them importing each other.
Errors
EventError— the single error class this package throws, distinguished byerr.code:"INVALID_EVENT"— a bad event name or missing/non-function handler was passed toon()/once()/emit()"LISTENER_FAILURE"— a listener threw or rejected duringemit()(reported in the result, not thrown)"PROCESSING_FAILURE"— an unexpected internal failure while processing an event
import { EventError } from "zayra-events";
try {
events.on("", handler);
} catch (err) {
if (err instanceof EventError && err.code === "INVALID_EVENT") {
console.error("Bad event name:", err.eventName);
}
}
Design note: dependency-free by convention
Like every other zayra-* package, zayra-events ships with zero npm dependencies. Packages that previously shipped their own minimal fallback event bus (zayra-plugins, zayra-task-engine, zayra-sdk) can now depend on this package directly and swap their internal EventBus for a shared EventManager instance — the on()/off()/emit()-shaped API was kept intentionally close to make that swap mechanical.
What this package deliberately does NOT do
Per its design rules:
- No frontend/UI code
- No business logic —
zayra-eventsdoesn't decide what an event means, it only delivers it - No AI decision-making
- No enforcement of which events a package may or may not emit — that's each package's own concern
Project structure
zayra-events/
src/
index.js # public entry point
manager.js # EventManager — on/once/removeListener/emit
event-types.js # SystemEvent, ModuleEvent, PluginEvent, AiEvent, MemoryEvent, TaskEvent catalogs
format.js # event name validation + createEventEnvelope()
history.js # EventHistory — bounded recent-events log
errors.js # EventError
package.json
README.md
API reference
class EventManager
| Method | Description |
|---|---|
new EventManager(options?) |
options.historySize (default 500), or inject options.history. |
on(eventName, handler) |
Subscribe. Returns an unsubscribe function. |
once(eventName, handler) |
Subscribe for one occurrence. |
removeListener(eventName, handler) |
Unsubscribe one handler. |
removeAllListeners(eventName?) |
Clear listeners for one event, or all events. |
listenerCount(eventName) |
Number of active listeners for an event. |
emit(eventName, data?, options?) |
Publish. options.source tags the emitter. Returns Promise<EmitResult>. |
.history |
The EventHistory instance backing this manager. |
Other exports
EventHistory, isValidEventName(), createEventEnvelope(), SystemEvent, ModuleEvent, PluginEvent, AiEvent, MemoryEvent, TaskEvent, STANDARD_EVENTS, isStandardEvent(), EventError
Compatibility
Designed to be the shared communication backbone for zayra-core, zayra-config, zayra-memory, zayra-tools, zayra-plugins, zayra-provider, zayra-task-engine, and zayra-sdk.
License
MIT