@ixo/oracles-events
Real-time event system for QiForge. Handles streaming communication between your oracle (server) and client applications via Server-Sent Events (SSE) and WebSocket (WS) connections.
Table of Contents
Overview
The @ixo/oracles-events package enables real-time communication between your oracle and client applications. While your LLM processes conversations using LangGraph, events stream live updates to clients showing tool execution, rendering custom UI components, and enabling browser-side interactions.
Key Features:
- Dual Communication: SSE for one-way streaming, WebSocket for bidirectional communication
- Type-Safe Events: Full TypeScript support with payload validation
- Session Management: Automatic session isolation and request tracking
- Client Integration: Seamless integration with
@ixo/oracles-client-sdk
- Browser Tools: Reverse tool calls that execute in the user's browser
Communication Channels
Server-Sent Events (SSE)
Use Case: One-way streaming from server to client
Best For: Tool call updates, component rendering, status notifications
Endpoint: /sse/events?sessionId={sessionId}
WebSocket (WS)
Use Case: Bidirectional real-time communication
Best For: Browser tool calls, interactive features, immediate responses
Implementation: Socket.IO with session-based rooms
Event Types
1. ToolCallEvent
Purpose: Notifies clients when LLM executes tools on the server
Use Cases: API calls, database queries, long-running operations
new ToolCallEvent({
sessionId: 'user-session-123',
requestId: 'request-456',
toolName: 'get_weather',
args: { location: 'New York' },
status: 'isRunning',
}).emit();
new ToolCallEvent({
sessionId: 'user-session-123',
requestId: 'request-456',
toolName: 'get_weather',
status: 'done',
output: 'Temperature: 72°F, Sunny',
}).emit();
2. RenderComponentEvent
Purpose: Instructs client to render custom UI components
Use Cases: Dynamic lists, charts, forms, interactive elements
new RenderComponentEvent({
sessionId: 'user-session-123',
requestId: 'request-456',
componentName: 'ProductList',
args: {
products: [
{ name: 'Product A', price: 29.99 },
{ name: 'Product B', price: 39.99 },
],
},
status: 'done',
}).emit();
3. BrowserToolCallEvent
Purpose: Enables LLM to execute tools directly in user's browser
Use Cases: UI navigation, DOM manipulation, browser-specific operations
import { callBrowserTool } from '@ixo/common';
const result = await callBrowserTool({
sessionId: 'user-session-123',
toolCallId: 'tool-call-789',
toolName: 'navigateToPage',
args: { url: '/products' },
});
4. MessageCacheInvalidationEvent
Purpose: Notifies clients to refresh message cache
Use Cases: New messages, conversation state changes
5. RouterEvent
Purpose: Manages conversation flow and navigation state
Use Cases: Multi-step conversations, wizard flows
Installation
pnpm install @ixo/oracles-events
Creating a New Event
1. Define Event Interface & Class
Create your event by extending BaseEvent:
export const EVENT_NAME = 'my_custom_event' as const;
export interface IMyCustomEvent {
message: string;
priority: 'high' | 'normal' | 'low';
metadata?: Record<string, any>;
}
import {
BaseEvent,
shouldHaveSessionId,
WithRequiredEventProps,
} from '../base-event/base-event';
import { EVENT_NAME, IMyCustomEvent } from './types';
export class MyCustomEvent extends BaseEvent<IMyCustomEvent> {
constructor(public payload: WithRequiredEventProps<IMyCustomEvent>) {
super();
shouldHaveSessionId(payload);
}
public eventName = EVENT_NAME;
static eventName = EVENT_NAME;
}
2. Register Event in GraphEventEmitter
import { MyCustomEvent } from './events/my-custom-event/my-custom-event';
export class GraphEventEmitter {
static registerEventHandlers(server: Server): void {
MyCustomEvent.registerEventHandlers(server);
}
}
3. Export Event
export { MyCustomEvent } from './my-custom-event/my-custom-event';
export type { IMyCustomEvent } from './my-custom-event/types';
4. Add to Types
import type { MyCustomEvent } from '../events';
export type AllEvents =
| ToolCallEvent
| RenderComponentEvent
| BrowserToolCallEvent
| MessageCacheInvalidationEvent
| RouterEvent
| MyCustomEvent;
export type MyCustomEventPayload = WithRequiredEventProps<IMyCustomEvent>;
5. Emit Your Event
There are two ways to emit events depending on your use case:
Method 1: Standard Event Emission (Recommended)
Use Case: Standard event flow that automatically reaches clients via WebSocket
How it works: event.emit() → rootEventEmitter → GraphEventEmitter (if registered) → WebSocket clients
import { ToolCallEvent } from '@ixo/oracles-events';
const event = new ToolCallEvent({
sessionId: 'user-session-123',
requestId: 'request-456',
toolName: 'get_weather',
args: { location: 'New York' },
status: 'isRunning',
});
event.emit();
Complete Event Flow:
Your Code → event.emit() → rootEventEmitter → GraphEventEmitter → WebSocket Server → Client
Registered Events: ToolCallEvent, RenderComponentEvent, BrowserToolCallEvent, MessageCacheInvalidationEvent, RouterEvent
Method 2: Direct Service Emission
Use Case: Direct emission to specific communication channels
How it works: Bypass event system and emit directly to SSE/WebSocket services
import { SseService } from 'src/sse/sse.service';
@Injectable()
export class MyService {
constructor(private readonly sseService: SseService) {}
sendEventToClient(sessionId: string) {
const event = new MyCustomEvent({
sessionId,
requestId: 'request-456',
message: 'Direct emission',
priority: 'high',
});
this.sseService.publishToSession(sessionId, event);
}
}
Method 3: Internal Communication
Use Case: Emit events back into the internal events system
How it works: Direct rootEventEmitter usage for service-to-service communication
import { rootEventEmitter } from '@ixo/oracles-events';
rootEventEmitter.emit('browser_tool_result', {
sessionId: 'user-session-123',
toolCallId: 'tool-123',
result: { success: true },
timestamp: new Date().toISOString(),
});
When to use each method:
- Method 1: Standard registered events that automatically reach WebSocket clients (ToolCallEvent, RenderComponentEvent, etc.)
- Method 2: Real-time streaming scenarios, direct SSE client communication
- Method 3: Internal service communication, responding to WebSocket events and triggering internal workflows
📊 Event Flow Diagram
The following diagram shows the complete flow from your code to the client:
graph LR
A["🧠 Your Code<br/>event.emit()"] --> B["📡 rootEventEmitter"]
B --> C["🎯 GraphEventEmitter<br/>(listens to rootEventEmitter)"]
C --> D["📱 WebSocket → Client"]
style A fill:#e3f2fd,stroke:#1976d2,stroke-width:2px
style B fill:#fff3e0,stroke:#f57c00,stroke-width:2px
style C fill:#e8f5e8,stroke:#388e3c,stroke-width:2px
style D fill:#fce4ec,stroke:#d81b60,stroke-width:2px
How it works:
- You call
event.emit() in your code
- This triggers
rootEventEmitter (internal event bus)
GraphEventEmitter listens to rootEventEmitter and forwards to WebSocket
- Client receives the event automatically
That's it! Once GraphEventEmitter is registered in ws.gateway.ts, all events automatically reach clients.
Event Status Management
Many events support status tracking:
new ToolCallEvent({
sessionId,
requestId,
toolName: 'complex_analysis',
status: 'isRunning',
}).emit();
new ToolCallEvent({
sessionId,
requestId,
toolName: 'complex_analysis',
status: 'isRunning',
output: 'Progress: 50% complete',
}).emit();
new ToolCallEvent({
sessionId,
requestId,
toolName: 'complex_analysis',
status: 'done',
output: 'Analysis complete: Found 25 insights',
}).emit();
Event Chaining
Chain multiple events for complex workflows:
new ToolCallEvent({
sessionId,
requestId,
toolName: 'data_analysis',
status: 'isRunning',
}).emit();
const data = await performAnalysis();
new ToolCallEvent({
sessionId,
requestId,
toolName: 'data_analysis',
status: 'done',
output: `Analyzed ${data.length} records`,
}).emit();
new RenderComponentEvent({
sessionId,
requestId,
componentName: 'AnalysisChart',
args: { data, chartType: 'bar' },
status: 'done',
}).emit();
Required Payload Properties
Every event payload must include:
interface RequiredEventProps {
sessionId: string;
requestId: string;
}
Session and Request Management
const sessionId = 'user-session-123';
const requestId = 'message-request-456';
new ToolCallEvent({ sessionId, requestId });
new RenderComponentEvent({ sessionId, requestId });
Testing
Basic Event Testing
import { MyCustomEvent } from '../my-custom-event';
import { rootEventEmitter } from '../../root-event-emitter';
describe('MyCustomEvent', () => {
it('should emit with correct payload', () => {
const payload = {
sessionId: 'test-session',
requestId: 'test-request',
message: 'test message',
priority: 'high' as const,
};
const event = new MyCustomEvent(payload);
const emitSpy = jest.spyOn(rootEventEmitter, 'emit');
event.emit();
expect(emitSpy).toHaveBeenCalledWith(MyCustomEvent.eventName, payload);
});
it('should require sessionId', () => {
expect(() => {
new MyCustomEvent({
requestId: 'test-request',
message: 'test',
priority: 'normal',
} as any);
}).toThrow();
});
});
Integration Testing
import { createTestEvent } from '../test-utils';
it('should register event handlers correctly', () => {
const mockServer = {
on: jest.fn(),
emit: jest.fn(),
};
MyCustomEvent.registerEventHandlers(mockServer as any);
expect(mockServer.on).toHaveBeenCalledWith(
MyCustomEvent.eventName,
expect.any(Function),
);
});
Contributing
Event Naming Conventions
- Use snake_case:
tool_call, render_component, my_custom_event
- Be descriptive:
user_preferences_updated not update
- Include domain if needed:
payment_status_changed
TypeScript Best Practices
- Always define interfaces for payload types
- Use
WithRequiredEventProps<T> for constructor parameters
- Export both the event class and interface
- Add proper JSDoc comments
Performance Considerations
- Batch events when possible instead of emitting many small events
- Use appropriate channels: SSE for notifications, WS for interactions
- Validate payloads before emission to catch errors early
- Avoid large payloads: Keep event data focused and minimal
Security Guidelines
- Validate all inputs in event payloads
- Sanitize user data before including in events
- Ensure session isolation - events only go to correct sessions
- Never include sensitive data in event payloads
Architecture Documentation
For comprehensive system architecture, event flows, and integration patterns, see Events System Architecture.
Thank you for using @ixo/oracles-events!