src — events
src — events
The src/events module provides a robust, type-safe, and centralized event system for the application. It aims to replace or augment the scattered use of Node.js's native EventEmitter with a unified, developer-friendly solution that offers strong TypeScript support, advanced listening capabilities, and a clear structure for application-wide communication.
Purpose and Key Features
The primary goal of this module is to establish a consistent and predictable way for different parts of the application to communicate without direct coupling. By leveraging TypeScript, it ensures that event names and their associated data payloads are strictly typed, preventing common runtime errors and improving developer experience through auto-completion and compile-time checks.
Key features include:
- Type-Safe Events: All events and their payloads are defined with TypeScript interfaces, providing full auto-completion and compile-time validation.
- Centralized Event Bus: A global singleton
EventBusallows any part of the application to emit or listen to events. - Advanced Listener Options: Support for event filtering (predicate-based), priority handling, and one-time listeners (
once). - Wildcard Listeners: Subscribe to all events (
onAny) for logging, debugging, or cross-cutting concerns. - Event History & Statistics: Track recent events and gather statistics on emitted events and active listeners.
- Asynchronous Event Handling: Listeners can return Promises, and the system handles their resolution and error reporting.
- Migration Path:
TypedEventEmitterAdapterfacilitates gradual migration from existing Node.jsEventEmitterimplementations. - Scoped Event Views:
FilteredEventEmitterallows creating specialized views of an event stream based on a filter.
Core Concepts
At the heart of the event system are a few fundamental concepts:
BaseEvent: All events must extend this interface, which defines common properties liketype(a string identifier) andtimestamp.- Event Type Maps: Interfaces like
ApplicationEventsandAllEventsmap event names (e.g.,'agent:started') to their specific event data interfaces (e.g.,AgentEvent). These maps are crucial for providing type safety across the system. TypedEventEmitter: The generic class that implements the core event emission and listening logic, enforcing type safety based on the provided event map.EventBus: A singleton instance ofTypedEventEmitterconfigured withAllEvents, serving as the global communication hub.
Module Architecture
The src/events module is structured into several interconnected files, each with a specific responsibility:
classDiagram
direction LR
class TypedEventEmitter {
+emit()
+on()
+filter()
+waitFor()
}
class FilteredEventEmitter {
+on()
+waitFor()
}
class EventBus {
+static getInstance()
}
class TypedEventEmitterAdapter {
+emitTyped()
+onTyped()
}
class EventEmitter {
<<Node.js>>
}
TypedEventEmitter "1" -- "0..N" FilteredEventEmitter : creates
EventBus "1" *-- "1" TypedEventEmitter : wraps (singleton)
TypedEventEmitterAdapter "1" *-- "1" TypedEventEmitter : wraps
TypedEventEmitterAdapter --|> EventEmitter : extends Node.js EventEmitter
TypedEventEmitter "1" *-- "1" EventEmitter : wraps for error handling
src/events/types.ts: Event Definitions
This file is the backbone of the type-safe event system. It defines:
BaseEvent: The foundational interface for all events, requiringtype: stringandtimestamp: number.- Specific Event Interfaces: Detailed interfaces for various event categories (e.g.,
AgentEvent,ToolEvent,SessionEvent,FileEvent,CacheEvent,SyncEvent,WorkflowEvent, etc.). Each interface extendsBaseEventand defines the specific payload for that event type. - Event Listener and Filter Types:
EventListenerandEventFilterprovide type definitions for callback functions. AllEvents: A comprehensive interface that maps every possible event type string to its corresponding event data interface. This is the master type used by the globalEventBus.ApplicationEvents: A subset ofAllEvents, primarily for backward compatibility or scenarios where only core application events are needed.
Contribution Note: When adding new event types, define a new interface extending BaseEvent and add it to the AllEvents (and potentially other category-specific) interface.
src/events/typed-emitter.ts: The Type-Safe Emitter
This file contains the core TypedEventEmitter class and the TypedEventEmitterAdapter for migration.
TypedEventEmitter
This is the primary class for creating type-safe event emitters. It manages listeners, handles event emission, and provides advanced features:
- Type Safety: The generic
TEventsparameter ensures thatemitcalls only use defined event types with correct payloads, andon/oncelisteners receive correctly typed event objects. - Internal
EventEmitter: It wraps a Node.jsEventEmitterinstance internally, primarily for robust error handling (emitting an'error'event if a listener throws an exception or an async listener rejects). - Listener Management: It maintains its own
Mapof listeners, allowing for features like: on(type, listener, options): Adds a listener.optionscan includepriority(higher runs first) andfilter(a predicate function).once(type, listener, options): Adds a listener that is automatically removed after the first event.onAny(listener, options): Adds a wildcard listener that receives all emitted events (typed asBaseEvent).off(listenerId): Removes a specific listener by its unique ID.offAll(type?): Removes all listeners for a specific type, or all listeners entirely if no type is provided.emit(type, event): Emits an event. It constructs the full event object (addingtypeandtimestamp), updates statistics, adds to history, sorts listeners by priority, applies filters, and executes listeners. It gracefully handles synchronous and asynchronous listener errors.waitFor(type, options): Returns aPromisethat resolves with the next event of the specified type (optionally filtered) or rejects after a timeout.filter(type, predicate): Returns aFilteredEventEmitterinstance, providing a scoped view of events.getHistory()/getFilteredHistory(): Accesses a configurable-size history of emitted events.getStats()/resetStats(): Provides runtime statistics on emitted events and active listeners.dispose(): Cleans up all listeners and history, useful for resource management.
TypedEventEmitterAdapter
This class is designed to facilitate a gradual migration from existing codebases that use Node.js's native EventEmitter.
- It extends
EventEmitter: This means existing code expecting anEventEmitterinstance will still work. - It wraps a
TypedEventEmitter: It internally holds an instance ofTypedEventEmitter. emitTyped(type, event)/onTyped(type, listener, options)/onceTyped(...)/offTyped(...)/waitForTyped(...): These methods provide the type-safe API ofTypedEventEmitter. WhenemitTypedis called, it emits the event through both the nativeEventEmitter(for old listeners) and the internalTypedEventEmitter(for new, type-safe listeners).
This adapter allows developers to incrementally update their event handling logic without breaking existing functionality.
src/events/filtered-emitter.ts: Scoped Event Views
The FilteredEventEmitter class provides a specialized "view" of events from a TypedEventEmitter. It's not meant to be instantiated directly but is returned by TypedEventEmitter.filter().
- It takes a
TypedEventEmittersource, an eventtype, and anEventFilterpredicate in its constructor. - Its
on(),once(), andwaitFor()methods internally call the corresponding methods on thesourceTypedEventEmitter, automatically applying the configuredpredicateas a filter. - It tracks listener IDs created through itself, allowing
offAll()to easily remove only its own listeners from the source emitter.
This component simplifies listening to specific subsets of events without repeatedly defining the same filter logic.
src/events/event-bus.ts: The Global Event Bus
This file provides the singleton EventBus for application-wide event communication.
EventBus: A class that extendsTypedEventEmitter. Its primary purpose is to implement the singleton pattern.static getInstance: The static method to retrieve the single instance of the() EventBus. It ensures that only one instance is ever created.getGlobalEventBus(): A convenience function that returns the singletonEventBusinstance, explicitly typed withAllEvents. This is the recommended way to access the global bus for full type safety.getEventBus(): Another convenience function, returning theEventBustyped withApplicationEvents. This might be used for specific contexts or backward compatibility.resetEventBus(): A utility function (primarily for testing) to clear and re-initialize the singleton instance.
The global EventBus is the central nervous system for decoupled communication across the application.
src/events/type-guards.ts: Runtime Type Narrowing
This file provides utility functions for runtime type checking of events.
isEventType: A generic type guard that checks if an event's(event: BaseEvent, type: K) typeproperty matches a given string literal, narrowing the event's type accordingly.- Category-Specific Type Guards: Functions like
isAgentEvent(event: BaseEvent)orisToolEvent(event: BaseEvent)usestartsWith()checks on the eventtypeto narrow downBaseEventto a specific category of events. These are particularly useful when working withonAnylisteners orgetFilteredHistorywhere the initial event type isBaseEvent.
Usage Patterns
Using TypedEventEmitter Directly
For module-specific or component-specific eventing, you can instantiate TypedEventEmitter directly:
import { TypedEventEmitter, ToolEvents } from 39;./events/index.js39;;
class="hl-cmt">// Create an emitter for ToolEvents
const toolEmitter = new TypedEventEmitter<ToolEvents>();
class="hl-cmt">// Listen for a specific tool event
toolEmitter.on(39;tool:started39;, (event) => {
console.log(`Tool 39;${event.toolName}39; started with args:`, event.args);
});
class="hl-cmt">// Listen for any tool event
toolEmitter.onAny((event) => {
if (event.type.startsWith(39;tool:39;)) {
console.log(`A tool event occurred: ${event.type}`);
}
});
class="hl-cmt">// Emit a tool event (type-safe)
toolEmitter.emit(39;tool:started39;, { toolName: 39;git39;, args: { command: 39;clone39; } });
toolEmitter.emit(39;tool:completed39;, { toolName: 39;git39;, result: { success: true, output: 39;Cloned repo39; } });
Using the Global EventBus
For application-wide events, use the singleton EventBus:
import { getGlobalEventBus, AllEvents } from 39;./events/index.js39;;
const bus = getGlobalEventBus();
class="hl-cmt">// Listen for an agent event
bus.on(39;agent:started39;, (event) => {
console.log(`Agent 39;${event.agentId}39; has started.`);
});
class="hl-cmt">// Listen for a file event with a filter
const listenerId = bus.on(39;file:modified39;, (event) => {
console.log(`File modified: ${event.filePath}`);
}, {
filter: (event) => event.filePath.endsWith(39;.ts39;)
});
class="hl-cmt">// Emit events
bus.emit(39;agent:started39;, { agentId: 39;my-agent-12339; });
bus.emit(39;file:modified39;, { filePath: 39;/src/main.ts39;, operation: 39;edit39; });
bus.emit(39;file:modified39;, { filePath: 39;/README.md39;, operation: 39;edit39; }); class="hl-cmt">// This won39;t trigger the listener above
class="hl-cmt">// Later, remove the listener
bus.off(listenerId);
Filtering Events with FilteredEventEmitter
You can create a filtered view for a specific event type:
import { getGlobalEventBus, AllEvents } from 39;./events/index.js39;;
const bus = getGlobalEventBus();
class="hl-cmt">// Create a filtered emitter for 39;file:modified39; events on 39;.js39; files
const jsFileModifiedEmitter = bus.filter(39;file:modified39;, (event) => event.filePath.endsWith(39;.js39;));
jsFileModifiedEmitter.on((event) => {
console.log(`JavaScript file modified: ${event.filePath}`);
});
bus.emit(39;file:modified39;, { filePath: 39;index.js39;, operation: 39;save39; }); class="hl-cmt">// Triggers listener
bus.emit(39;file:modified39;, { filePath: 39;style.css39;, operation: 39;save39; }); class="hl-cmt">// Does not trigger
Waiting for Events
The waitFor method is useful for scenarios where you need to react to the next occurrence of an event:
import { getGlobalEventBus, AllEvents } from 39;./events/index.js39;;
const bus = getGlobalEventBus();
async function waitForAgentCompletion() {
console.log(39;Waiting for agent to complete...39;);
try {
const completionEvent = await bus.waitFor(39;agent:stopped39;, { timeout: 5000 });
console.log(`Agent 39;${completionEvent.agentId}39; stopped successfully.`);
} catch (error) {
console.error(39;Agent did not stop in time:39;, error);
}
}
waitForAgentCompletion();
class="hl-cmt">// Simulate agent stopping after a delay
setTimeout(() => {
bus.emit(39;agent:stopped39;, { agentId: 39;my-agent-45639; });
}, 2000);
Gradual Migration with TypedEventEmitterAdapter
If you have existing classes that extend Node.js EventEmitter, you can switch them to TypedEventEmitterAdapter to gradually introduce type safety:
import { TypedEventEmitterAdapter, SessionEvent } from 39;./events/index.js39;;
import { EventEmitter } from 39;events39;; class="hl-cmt">// Native EventEmitter
interface MySessionEvents extends Record<string, SessionEvent> {
39;session:started39;: SessionEvent;
39;session:ended39;: SessionEvent;
39;legacy-event39;: { data: string }; class="hl-cmt">// Keep old untyped events if needed
}
class="hl-cmt">// Old class (extends native EventEmitter)
class OldSessionManager extends EventEmitter {
startSession(sessionId: string) {
this.emit(39;session:started39;, { sessionId, timestamp: Date.now() }); class="hl-cmt">// Untyped emit
this.emit(39;legacy-event39;, { data: 39;old data39; });
}
}
class="hl-cmt">// New class (extends TypedEventEmitterAdapter)
class NewSessionManager extends TypedEventEmitterAdapter<MySessionEvents> {
startSession(sessionId: string) {
class="hl-cmt">// New type-safe API
this.emitTyped(39;session:started39;, { sessionId });
class="hl-cmt">// Old API still works for backward compatibility
this.emit(39;legacy-event39;, { data: 39;new data39; }); class="hl-cmt">// This will also trigger native EventEmitter listeners
}
}
const newManager = new NewSessionManager();
class="hl-cmt">// Listen using the new type-safe API
newManager.onTyped(39;session:started39;, (event) => {
console.log(`Typed session started: ${event.sessionId}`);
});
class="hl-cmt">// Listen using the old native EventEmitter API (still works)
newManager.on(39;legacy-event39;, (data: { data: string }) => {
console.log(`Legacy event received: ${data.data}`);
});
newManager.startSession(39;session-abc39;);
Event History and Statistics
TypedEventEmitter instances (including the EventBus) track emitted events and listener counts:
import { getGlobalEventBus } from 39;./events/index.js39;;
const bus = getGlobalEventBus();
bus.emit(39;agent:started39;, { agentId: 39;test-agent39; });
bus.emit(39;tool:completed39;, { toolName: 39;test-tool39;, result: { success: true } });
console.log(39;Event History:39;, bus.getHistory());
console.log(39;Event Stats:39;, bus.getStats());
class="hl-cmt">// Filter history
const agentEvents = bus.getFilteredHistory((entry) => entry.event.type.startsWith(39;agent:39;));
console.log(39;Agent Event History:39;, agentEvents);
Integration Points
The event system is designed to be a central communication layer. Based on the call graph, here's how other parts of the codebase interact with it:
src/tools/tool-manager.ts: Emitstool:started,tool:completed,tool:error,tool:registered,tool:instantiated,tool:disabledevents usingemitTypedwhen tools are executed or managed.sync/cloud/sync-manager.ts: Emits varioussync:events (e.g.,sync:started,sync:completed,sync:failed,sync:progress,sync:item_uploaded,sync:item_downloaded,sync:conflict_detected,sync:conflict_resolved) usingemitTypedto report synchronization status. It also listens tosync:events usingonTyped.src/undo/checkpoint-manager.ts: Likely emitscheckpoint:andundo:/redo:*events, and usesdisposeto clean up its event listeners.agent/multi-agent/multi-agent-system.ts: UseslistenerCountfor internal logic, possibly related to monitoring event activity.src/channels/reconnection-manager.ts: UseslistenerCount, suggesting it might monitor event activity to manage reconnection logic.- Unit Tests (
tests/unit/*.test.ts,tests/ai-integration-tests.test.ts): Heavily utilize all aspects of the event system, includingon,once,emit,waitFor,listenerCount,getEventBus,resetEventBus,dispose, andFilteredEventEmittermethods, to verify correct behavior.
This demonstrates that core application logic, such as tool execution and cloud synchronization, relies on the event system for reporting status and enabling decoupled reactions.
Contributing to Events
When adding new event types or categories:
- Define Event Interface: Create a new interface in
src/events/types.tsthat extendsBaseEvent. Ensure it has a uniquetypestring literal and defines all necessary payload properties.
export interface MyNewFeatureEvent extends BaseEvent {
type: 39;myfeature:started39; | 39;myfeature:completed39;;
featureId: string;
class="hl-cmt">// ... other properties
}
- Add to
AllEvents: Include your new event interface in theAllEventsmap insrc/events/types.tsto make it globally available. Consider adding it to a category-specific map if appropriate (e.g.,PluginEvents).
export interface AllEvents extends Record<string, BaseEvent> {
class="hl-cmt">// ... existing events
39;myfeature:started39;: MyNewFeatureEvent;
39;myfeature:completed39;: MyNewFeatureEvent;
}
- Implement Emission: Use
getGlobalEventBus().emit('myfeature:started', { featureId: '...' })orthis.emitTyped('myfeature:started', { featureId: '...' })if usingTypedEventEmitterAdapter. - Add Type Guards (Optional): If your new event category will be frequently checked at runtime (e.g., in
onAnylisteners), consider adding a type guard function tosrc/events/type-guards.ts.
export function isMyNewFeatureEvent(event: BaseEvent): event is MyNewFeatureEvent {
return event.type.startsWith(39;myfeature:39;);
}
- Update
index.ts: Ensure your new types are exported fromsrc/events/index.tsif they are intended for public consumption.
By following these guidelines, the event system remains consistent, type-safe, and easy to extend.