Runtime System
Complete guide to the ElizaOS runtime system architecture, lifecycle management, and service orchestration
The ElizaOS runtime system is built around the AgentRuntime
class, which serves as the core
orchestrator for all agent operations. This comprehensive system provides a flexible, event-driven
framework that manages plugins, services, models, memory, and the complete agent lifecycle.
Architecture Overview
Core Components
The runtime system consists of several key components working together:
- AgentRuntime: Main orchestration class implementing
IAgentRuntime
- Plugin System: Extensible architecture for adding functionality
- Service Management: Type-safe service registration and discovery
- Model Handlers: Unified interface for AI model interactions
- Memory System: Persistent storage with embedding-based search
- Event System: Pub/sub pattern for decoupled communication
- Connection Management: Entity and room relationship handling
AgentRuntime Class
The AgentRuntime
class implements the IAgentRuntime
interface and serves as the primary
execution environment:
export class AgentRuntime implements IAgentRuntime {
readonly agentId: UUID;
readonly character: Character;
public adapter!: IDatabaseAdapter;
readonly actions: Action[] = [];
readonly evaluators: Evaluator[] = [];
readonly providers: Provider[] = [];
readonly plugins: Plugin[] = [];
private isInitialized = false;
// Service management
services = new Map<ServiceTypeName, Service>();
private serviceTypes = new Map<ServiceTypeName, typeof Service>();
// Model handling
models = new Map<string, ModelHandler[]>();
// Event system
events: Map<string, ((params: any) => Promise<void>)[]> = new Map();
// State management
stateCache = new Map<UUID, State>();
// Run tracking for debugging
private currentRunId?: UUID;
private currentActionContext?: ActionContext;
}
IAgentRuntime Interface
The runtime implements a comprehensive interface that extends IDatabaseAdapter
:
interface IAgentRuntime extends IDatabaseAdapter {
// Core properties
agentId: UUID;
character: Character;
providers: Provider[];
actions: Action[];
evaluators: Evaluator[];
plugins: Plugin[];
services: Map<ServiceTypeName, Service>;
events: Map<string, ((params: any) => Promise<void>)[]>;
routes: Route[];
// Core methods
registerPlugin(plugin: Plugin): Promise<void>;
initialize(): Promise<void>;
stop(): Promise<void>;
// Service management
getService<T extends Service>(serviceName: ServiceTypeName): T | null;
registerService(serviceDef: typeof Service): Promise<void>;
// Model handling
useModel<T extends ModelTypeName>(
modelType: T,
params: ModelParamsMap[T],
provider?: string
): Promise<ModelResultMap[T]>;
// State composition
composeState(message: Memory, includeList?: string[]): Promise<State>;
// Connection management
ensureConnection(params: ConnectionParams): Promise<void>;
// Event system
emitEvent(event: string | string[], params: any): Promise<void>;
registerEvent(event: string, handler: EventHandler): void;
}
Runtime Lifecycle
1. Construction and Setup
The runtime is created with configuration options and prepares for initialization:
const runtime = new AgentRuntime({
agentId: stringToUuid("my-agent"),
character: myCharacter,
plugins: [sqlPlugin, bootstrapPlugin],
adapter: databaseAdapter,
settings: runtimeSettings,
allAvailablePlugins: availablePlugins,
});
The runtime constructor sets up basic properties but doesn't initialize services or plugins.
Always call initialize()
before using the runtime.
2. Initialization Process
The initialization follows a structured multi-step process:
async initialize(): Promise<void> {
if (this.isInitialized) {
this.logger.warn('Agent already initialized');
return;
}
// Step 1: Register all plugins const pluginRegistrationPromises = []; for (const plugin of
this.characterPlugins) { if (plugin) { pluginRegistrationPromises.push(this.registerPlugin(plugin));
} } await Promise.all(pluginRegistrationPromises);
// Step 2: Initialize database adapter if (!this.adapter) { throw new Error('Database adapter not
initialized'); } await this.adapter.init();
// Step 3: Run plugin migrations await this.runPluginMigrations();
// Step 4: Create agent entity const existingAgent = await this.ensureAgentExists(this.character);
// Step 5: Setup room and participants await this.setupRoomAndParticipants();
// Step 6: Initialize embedding dimensions await this.ensureEmbeddingDimension();
// Step 7: Initialize queued services for (const service of this.servicesInitQueue) { await
this.registerService(service); }
this.isInitialized = true; }
async registerPlugin(plugin: Plugin): Promise<void> {
// Validate plugin
if (!plugin?.name) {
throw new Error('Plugin or plugin name is undefined');
}
// Check for duplicates
const existingPlugin = this.plugins.find((p) => p.name === plugin.name);
if (existingPlugin) {
this.logger.warn(`Plugin ${plugin.name} already registered`);
return;
}
// Add to active plugins
this.plugins.push(plugin);
// Initialize plugin
if (plugin.init) {
await plugin.init(plugin.config || {}, this);
}
// Register plugin components
if (plugin.adapter) {
this.registerDatabaseAdapter(plugin.adapter);
}
if (plugin.actions) {
plugin.actions.forEach(action => this.registerAction(action));
}
if (plugin.evaluators) {
plugin.evaluators.forEach(evaluator => this.registerEvaluator(evaluator));
}
if (plugin.providers) {
plugin.providers.forEach(provider => this.registerProvider(provider));
}
if (plugin.models) {
Object.entries(plugin.models).forEach(([modelType, handler]) => {
this.registerModel(modelType, handler, plugin.name, plugin.priority);
});
}
if (plugin.services) {
plugin.services.forEach(service => {
if (this.isInitialized) {
this.registerService(service);
} else {
this.servicesInitQueue.add(service);
}
});
}
}
// Database initialization and entity setup
async setupDatabase(): Promise<void> {
// Initialize database adapter
await this.adapter.init();
// Run plugin migrations await this.runPluginMigrations();
// Create agent entity const existingAgent = await this.ensureAgentExists(this.character);
// Create agent entity if not exists let agentEntity = await this.getEntityById(this.agentId); if
(!agentEntity) { await this.createEntity({ id: this.agentId, names: [this.character.name], metadata:
{}, agentId: existingAgent.id, }); }
// Setup default room const room = await this.getRoom(this.agentId); if (!room) { await
this.createRoom({ id: this.agentId, name: this.character.name, source: 'elizaos', type:
ChannelType.SELF, channelId: this.agentId, serverId: this.agentId, worldId: this.agentId, }); }
// Add agent as participant await this.ensureParticipantInRoom(this.agentId, this.agentId); }
async registerService(serviceDef: typeof Service): Promise<void> {
const serviceType = serviceDef.serviceType as ServiceTypeName;
if (!serviceType) {
this.logger.warn(`Service ${serviceDef.name} missing serviceType`);
return;
}
if (this.services.has(serviceType)) {
this.logger.warn(`Service ${serviceType} already registered`);
return;
}
try {
const serviceInstance = await serviceDef.start(this);
this.services.set(serviceType, serviceInstance);
this.serviceTypes.set(serviceType, serviceDef);
// Register send handlers if available
if (typeof serviceDef.registerSendHandlers === 'function') {
serviceDef.registerSendHandlers(this, serviceInstance);
}
this.logger.debug(`Service ${serviceType} registered successfully`);
} catch (error) {
this.logger.error(`Failed to register service ${serviceType}:`, error);
throw error;
}
}
3. Plugin Migration System
The runtime supports plugin-specific database migrations:
async runPluginMigrations(): Promise<void> {
const drizzle = this.adapter.db;
if (!drizzle) {
this.logger.warn('Drizzle instance not found, skipping migrations');
return;
}
const pluginsWithSchemas = this.plugins.filter(p => p.schema);
for (const plugin of pluginsWithSchemas) {
if (plugin.schema) {
this.logger.info(`Running migrations for plugin: ${plugin.name}`);
try {
if (this.adapter && 'runMigrations' in this.adapter) {
await this.adapter.runMigrations(plugin.schema, plugin.name);
}
} catch (error) {
this.logger.error(`Failed to migrate plugin ${plugin.name}:`, error);
}
}
}
}
Service Management
Service Registration and Discovery
The runtime provides a type-safe service system:
// Service registration
async registerService(serviceDef: typeof Service): Promise<void> {
const serviceType = serviceDef.serviceType as ServiceTypeName;
if (this.services.has(serviceType)) {
this.logger.warn(`Service ${serviceType} already registered`);
return;
}
const serviceInstance = await serviceDef.start(this);
this.services.set(serviceType, serviceInstance);
this.serviceTypes.set(serviceType, serviceDef);
}
// Service discovery
getService<T extends Service = Service>(serviceName: ServiceTypeName): T | null {
const serviceInstance = this.services.get(serviceName);
if (!serviceInstance) {
this.logger.debug(`Service ${serviceName} not found`);
return null;
}
return serviceInstance as T;
}
// Type-safe service access
getTypedService<T extends Service = Service>(serviceName: ServiceTypeName): T | null {
return this.getService<T>(serviceName);
}
// Service introspection
getRegisteredServiceTypes(): ServiceTypeName[] {
return Array.from(this.services.keys());
}
hasService(serviceType: ServiceTypeName): boolean {
return this.services.has(serviceType);
}
getAllServices(): Map<ServiceTypeName, Service> {
return this.services;
}
Service Usage Examples
// Get a service
const twitterService = runtime.getService('twitter');
if (twitterService) {
await twitterService.sendMessage(content);
}
// Check if service exists if (runtime.hasService('discord')) { const discord =
runtime.getService('discord'); await discord.processMessage(message); }
// Type-safe service access
const twitterService = runtime.getTypedService<TwitterService>('twitter');
if (twitterService) {
// TypeScript knows this is a TwitterService
await twitterService.postTweet(content);
}
// Service with custom interface
interface CustomService extends Service {
customMethod(): Promise<void>;
}
const customService = runtime.getTypedService<CustomService>('custom');
await customService?.customMethod();
// List all registered services
const serviceTypes = runtime.getRegisteredServiceTypes();
console.log('Available services:', serviceTypes);
// Get all services const allServices = runtime.getAllServices(); for (const [serviceName, service]
of allServices) { console.log(`Service ${serviceName}: ${service.constructor.name}`); }
// Check service availability const requiredServices = ['twitter', 'discord', 'telegram']; const
availableServices = requiredServices.filter(s => runtime.hasService(s)); console.log('Available
required services:', availableServices);
Model System
Model Registration and Priority
Models are registered with priority-based selection:
registerModel(
modelType: ModelTypeName,
handler: (params: any) => Promise<any>,
provider: string,
priority?: number
) {
const modelKey = typeof modelType === 'string' ? modelType : ModelType[modelType];
if (!this.models.has(modelKey)) {
this.models.set(modelKey, []);
}
const registrationOrder = Date.now();
this.models.get(modelKey)?.push({
handler,
provider,
priority: priority || 0,
registrationOrder,
});
// Sort by priority then registration order
this.models.get(modelKey)?.sort((a, b) => {
if ((b.priority || 0) !== (a.priority || 0)) {
return (b.priority || 0) - (a.priority || 0);
}
return a.registrationOrder - b.registrationOrder;
});
}
Model Usage with Run ID Tracking
The runtime tracks model usage for debugging and logging:
async useModel<T extends ModelTypeName, R = ModelResultMap[T]>(
modelType: T,
params: Omit<ModelParamsMap[T], 'runtime'> | any,
provider?: string
): Promise<R> {
const modelKey = typeof modelType === 'string' ? modelType : ModelType[modelType];
const model = this.getModel(modelKey, provider);
if (!model) {
throw new Error(`No handler found for model type: ${modelKey}`);
}
const startTime = performance.now();
const response = await model(this, { ...params, runtime: this });
const elapsedTime = performance.now() - startTime;
// Log model usage with run ID tracking
await this.adapter.log({
entityId: this.agentId,
roomId: this.agentId,
body: {
modelType,
modelKey,
params,
runId: this.getCurrentRunId(),
timestamp: Date.now(),
executionTime: elapsedTime,
provider: provider || this.models.get(modelKey)?.[0]?.provider || 'unknown',
actionContext: this.currentActionContext,
response: Array.isArray(response) && response.every(x => typeof x === 'number')
? '[array]'
: response,
},
type: `useModel:${modelKey}`,
});
return response as R;
}
Run ID Tracking System
The runtime includes a new run ID tracking system for debugging:
// Run ID management
createRunId(): UUID {
return uuidv4() as UUID;
}
startRun(): UUID {
this.currentRunId = this.createRunId();
return this.currentRunId;
}
endRun(): void {
this.currentRunId = undefined;
}
getCurrentRunId(): UUID {
if (!this.currentRunId) {
this.currentRunId = this.createRunId();
}
return this.currentRunId;
}
// Usage example
const runId = runtime.startRun();
try {
const response = await runtime.useModel(ModelType.COMPLETION, {
prompt: "Hello world"
});
// Process response
} finally {
runtime.endRun();
}
Memory and State Management
State Composition
The runtime composes state from multiple providers with caching:
async composeState(
message: Memory,
includeList: string[] | null = null,
onlyInclude = false,
skipCache = false
): Promise<State> {
const filterList = onlyInclude ? includeList : null;
const emptyObj = { values: {}, data: {}, text: '' } as State;
const cachedState = skipCache ? emptyObj : (await this.stateCache.get(message.id)) || emptyObj;
// Determine providers to use
const providerNames = new Set<string>();
if (filterList && filterList.length > 0) {
filterList.forEach(name => providerNames.add(name));
} else {
this.providers
.filter(p => !p.private && !p.dynamic)
.forEach(p => providerNames.add(p.name));
}
if (!filterList && includeList && includeList.length > 0) {
includeList.forEach(name => providerNames.add(name));
}
// Get providers in order
const providersToGet = Array.from(
new Set(this.providers.filter(p => providerNames.has(p.name)))
).sort((a, b) => (a.position || 0) - (b.position || 0));
// Execute providers
const providerData = await Promise.all(
providersToGet.map(async (provider) => {
const start = Date.now();
try {
const result = await provider.get(this, message, cachedState);
const duration = Date.now() - start;
this.logger.debug(`${provider.name} Provider took ${duration}ms`);
return { ...result, providerName: provider.name };
} catch (error) {
this.logger.error('Provider error:', provider.name, error);
return { values: {}, text: '', data: {}, providerName: provider.name };
}
})
);
// Aggregate results
const currentProviderResults = { ...(cachedState.data?.providers || {}) };
for (const freshResult of providerData) {
currentProviderResults[freshResult.providerName] = freshResult;
}
// Build final state
const newState = {
values: aggregatedStateValues,
data: { providers: currentProviderResults },
text: providersText,
} as State;
this.stateCache.set(message.id, newState);
return newState;
}
Memory Operations
The runtime provides comprehensive memory management:
async createMemory(memory: Memory, tableName: string, unique?: boolean): Promise<UUID> {
// Add embedding if not present
if (!memory.embedding) {
memory = await this.addEmbeddingToMemory(memory);
}
return await this.adapter.createMemory(memory, tableName, unique);
}
async addEmbeddingToMemory(memory: Memory): Promise<Memory> { if (memory.embedding) { return memory;
}
const memoryText = memory.content.text; if (!memoryText) { throw new Error('Cannot generate
embedding: Memory content is empty'); }
try { memory.embedding = await this.useModel(ModelType.TEXT_EMBEDDING, { text: memoryText, }); }
catch (error) { this.logger.error('Failed to generate embedding:', error); memory.embedding = await
this.useModel(ModelType.TEXT_EMBEDDING, null); }
return memory; }
async searchMemories(params: {
embedding: number[];
query?: string;
match_threshold?: number;
count?: number;
roomId?: UUID;
unique?: boolean;
worldId?: UUID;
entityId?: UUID;
tableName: string;
}): Promise<Memory[]> {
const memories = await this.adapter.searchMemories(params);
// Re-rank results if query provided
if (params.query) {
return await this.rerankMemories(params.query, memories);
}
return memories;
}
async rerankMemories(query: string, memories: Memory[]): Promise<Memory[]> {
const docs = memories.map(memory => ({
title: memory.id,
content: memory.content.text,
}));
const bm25 = new BM25(docs);
const results = bm25.search(query, memories.length);
return results.map(result => memories[result.index]);
}
// Get memories with embedding search
async getRelevantMemories(
query: string,
roomId: UUID,
count: number = 10
): Promise<Memory[]> {
// Generate query embedding
const queryEmbedding = await this.useModel(ModelType.TEXT_EMBEDDING, {
text: query
});
// Search memories
const memories = await this.searchMemories({
embedding: queryEmbedding,
query,
match_threshold: 0.7,
count,
roomId,
tableName: 'memories'
});
return memories;
}
// Clear all agent memories async clearAllAgentMemories(): Promise<void> {
this.logger.info(`Clearing all memories for agent ${this.character.name}`);
const allMemories = await this.getAllMemories(); const memoryIds = allMemories.map(memory =>
memory.id);
if (memoryIds.length === 0) { this.logger.info('No memories found to delete'); return; }
this.logger.info(`Found ${memoryIds.length} memories to delete`); await
this.adapter.deleteManyMemories(memoryIds);
this.logger.info(`Successfully cleared all ${memoryIds.length} memories`); }
Action Processing and Context Tracking
Action Execution with Context Tracking
The runtime tracks action execution for debugging and logging:
async processActions(
message: Memory,
responses: Memory[],
state?: State,
callback?: HandlerCallback
): Promise<void> {
for (const response of responses) {
if (!response.content?.actions || response.content.actions.length === 0) {
continue;
}
const actions = response.content.actions;
for (const responseAction of actions) {
// Find matching action
const action = this.findAction(responseAction);
if (action && action.handler) {
// Start tracking this action's execution
const actionId = uuidv4() as UUID;
this.currentActionContext = {
actionName: action.name,
actionId: actionId,
prompts: [],
};
try {
await action.handler(this, message, state, {}, callback, responses);
// Log action execution with collected prompts
await this.adapter.log({
entityId: message.entityId,
roomId: message.roomId,
type: 'action',
body: {
action: action.name,
actionId: actionId,
message: message.content.text,
messageId: message.id,
state,
responses,
prompts: this.currentActionContext?.prompts || [],
promptCount: this.currentActionContext?.prompts.length || 0,
},
});
} catch (error) {
this.logger.error('Action execution failed:', error);
// Create error memory
const actionMemory: Memory = {
id: uuidv4() as UUID,
content: {
thought: error.message,
source: 'auto',
},
entityId: message.entityId,
roomId: message.roomId,
worldId: message.worldId,
};
await this.createMemory(actionMemory, 'messages');
throw error;
} finally {
// Clear action context
this.currentActionContext = undefined;
}
}
}
}
}
private findAction(responseAction: string): Action | undefined {
const normalizedResponseAction = responseAction.toLowerCase().replace('_', '');
// Try exact match first
let action = this.actions.find(
a => a.name.toLowerCase().replace('_', '') === normalizedResponseAction
);
if (!action) {
// Try partial match
action = this.actions.find(
a => a.name.toLowerCase().replace('_', '').includes(normalizedResponseAction) ||
normalizedResponseAction.includes(a.name.toLowerCase().replace('_', ''))
);
}
if (!action) {
// Try similes
for (const _action of this.actions) {
const simileAction = _action.similes?.find(
simile => simile.toLowerCase().replace('_', '').includes(normalizedResponseAction) ||
normalizedResponseAction.includes(simile.toLowerCase().replace('_', ''))
);
if (simileAction) {
action = _action;
break;
}
}
}
return action;
}
Action Context for Debugging
The runtime provides detailed action context for debugging:
interface ActionContext {
actionName: string;
actionId: UUID;
prompts: Array<{
modelType: string;
prompt: string;
timestamp: number;
}>;
}
// During model usage, prompts are collected
if (this.currentActionContext && promptContent) {
this.currentActionContext.prompts.push({
modelType: modelKey,
prompt: promptContent,
timestamp: Date.now(),
});
}
Event System
Event Registration and Emission
The runtime supports a flexible event system for decoupled communication:
registerEvent(event: string, handler: (params: any) => Promise<void>) {
if (!this.events.has(event)) {
this.events.set(event, []);
}
this.events.get(event)?.push(handler);
}
getEvent(event: string): ((params: any) => Promise<void>)[] | undefined {
return this.events.get(event);
}
async emitEvent(event: string | string[], params: any) {
const events = Array.isArray(event) ? event : [event];
for (const eventName of events) {
const eventHandlers = this.events.get(eventName);
if (!eventHandlers) {
continue;
}
try {
await Promise.all(eventHandlers.map(handler => handler(params)));
} catch (error) {
this.logger.error(`Error during emitEvent for ${eventName}:`, error);
}
}
}
Event Usage Examples
// Register event handler
runtime.registerEvent("user_joined", async (params) => {
console.log("User joined:", params.userId);
await runtime.createMemory(
{
id: uuidv4(),
content: { text: `User ${params.userName} joined the room` },
entityId: params.userId,
roomId: params.roomId,
},
"messages"
);
});
// Emit event
await runtime.emitEvent("user_joined", {
userId: "user-123",
userName: "Alice",
roomId: "room-456",
});
// Multiple events
await runtime.emitEvent(["user_joined", "activity_logged"], params);
Connection Management
Entity and Room Management
The runtime manages connections between entities, rooms, and worlds:
async ensureConnection({
entityId,
roomId,
worldId,
worldName,
userName,
name,
source,
type,
channelId,
serverId,
userId,
metadata,
}: ConnectionParams) {
// Create world ID if not provided
if (!worldId && serverId) {
worldId = createUniqueUuid(this.agentId + serverId, serverId);
}
// Prepare entity metadata
const entityMetadata = {
[source]: {
id: userId,
name: name,
userName: userName,
},
};
try {
// Create or update entity
const entity = await this.getEntityById(entityId);
if (!entity) {
await this.createEntity({
id: entityId,
names: [name, userName].filter(Boolean),
metadata: entityMetadata,
agentId: this.agentId,
});
} else {
await this.adapter.updateEntity({
id: entityId,
names: [...new Set([...(entity.names || []), ...names])].filter(Boolean),
metadata: { ...entity.metadata, ...entityMetadata },
agentId: this.agentId,
});
}
// Ensure world exists
await this.ensureWorldExists({
id: worldId,
name: worldName || `World for server ${serverId}`,
agentId: this.agentId,
serverId: serverId || 'default',
metadata,
});
// Ensure room exists
await this.ensureRoomExists({
id: roomId,
name: name,
source,
type,
channelId,
serverId,
worldId,
});
// Add participants
await this.ensureParticipantInRoom(entityId, roomId);
await this.ensureParticipantInRoom(this.agentId, roomId);
this.logger.debug(`Successfully connected entity ${entityId} in room ${roomId}`);
} catch (error) {
this.logger.error('Failed to ensure connection:', error);
throw error;
}
}
Bulk Connection Management
For performance, the runtime supports bulk operations:
async ensureConnections(entities, rooms, source, world): Promise<void> {
// Create world
await this.ensureWorldExists({ ...world, agentId: this.agentId });
// Chunk arrays for batch processing
const chunkArray = (arr, size) =>
arr.reduce((chunks, item, i) => {
if (i % size === 0) chunks.push([]);
chunks[chunks.length - 1].push(item);
return chunks;
}, []);
// Create all rooms first
const roomIds = rooms.map(r => r.id);
const roomExistsCheck = await this.getRoomsByIds(roomIds);
const roomsIdExists = roomExistsCheck.map(r => r.id);
const roomsToCreate = roomIds.filter(id => !roomsIdExists.includes(id));
if (roomsToCreate.length) {
const roomObjsToCreate = rooms
.filter(r => roomsToCreate.includes(r.id))
.map(r => ({ ...r, worldId: world.id, serverId: world.serverId, source, agentId: this.agentId }));
await this.createRooms(roomObjsToCreate);
}
// Create all entities
const entityIds = entities.map(e => e.id);
const entityExistsCheck = await this.adapter.getEntityByIds(entityIds);
const entitiesToUpdate = entityExistsCheck.map(e => e.id);
const entitiesToCreate = entities.filter(e => !entitiesToUpdate.includes(e.id));
if (entitiesToCreate.length) {
const batches = chunkArray(entitiesToCreate, 5000);
for (const batch of batches) {
await this.createEntities(batch);
}
}
// Add participants in batches
const firstRoom = rooms[0];
await this.ensureParticipantInRoom(this.agentId, firstRoom.id);
const entityIdsInFirstRoom = await this.getParticipantsForRoom(firstRoom.id);
const missingIdsInRoom = entityIds.filter(id => !entityIdsInFirstRoom.includes(id));
if (missingIdsInRoom.length) {
await this.addParticipantsRoom(missingIdsInRoom, firstRoom.id);
}
}
Runtime Shutdown
Graceful Shutdown
The runtime provides mechanisms for graceful shutdown:
async stop(): Promise<void> {
this.logger.debug(`Stopping runtime for character ${this.character.name}`);
// Stop all services
for (const [serviceName, service] of this.services) {
this.logger.debug(`Stopping service ${serviceName}`);
try {
await service.stop();
} catch (error) {
this.logger.error(`Error stopping service ${serviceName}:`, error);
}
}
// Close database connection
if (this.adapter) {
try {
await this.adapter.close();
} catch (error) {
this.logger.error('Error closing database adapter:', error);
}
}
// Clear caches
this.stateCache.clear();
this.events.clear();
// Reset initialization state
this.isInitialized = false;
this.logger.info('Runtime stopped successfully');
}
Advanced Features
Semaphore for Concurrency Control
The runtime includes a semaphore implementation for managing concurrent operations:
export class Semaphore {
private permits: number;
private waiting: Array<() => void> = [];
constructor(count: number) {
this.permits = count;
}
async acquire(): Promise<void> {
if (this.permits > 0) {
this.permits -= 1;
return Promise.resolve();
}
return new Promise<void>((resolve) => {
this.waiting.push(resolve);
});
}
release(): void {
this.permits += 1;
const nextResolve = this.waiting.shift();
if (nextResolve && this.permits > 0) {
this.permits -= 1;
nextResolve();
}
}
}
// Usage example
const semaphore = new Semaphore(3); // Allow 3 concurrent operations
async function performOperation() {
await semaphore.acquire();
try {
// Perform operation
} finally {
semaphore.release();
}
}
Task Worker System
The runtime supports task workers for background processing:
registerTaskWorker(taskHandler: TaskWorker): void {
if (this.taskWorkers.has(taskHandler.name)) {
this.logger.warn(`Task definition ${taskHandler.name} already registered`);
}
this.taskWorkers.set(taskHandler.name, taskHandler);
}
getTaskWorker(name: string): TaskWorker | undefined {
return this.taskWorkers.get(name);
}
// Usage
const taskWorker: TaskWorker = {
name: 'backup-memories',
handler: async (runtime, task) => {
// Backup logic
const memories = await runtime.getMemories({
tableName: 'memories',
count: 1000
});
// Process memories
}
};
runtime.registerTaskWorker(taskWorker);
Error Handling and Debugging
Comprehensive Error Handling
The runtime implements comprehensive error handling:
// Plugin registration with error handling
try {
await plugin.init(plugin.config || {}, this);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
if (
errorMessage.includes("API key") ||
errorMessage.includes("environment variables") ||
errorMessage.includes("Invalid plugin configuration")
) {
console.warn(`Plugin ${plugin.name} requires configuration: ${errorMessage}`);
console.warn(
"Please check your environment variables and ensure all required API keys are set."
);
} else {
throw error;
}
}
// Service registration with error handling
try {
const serviceInstance = await serviceDef.start(this);
this.services.set(serviceType, serviceInstance);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
this.logger.error(`Failed to register service ${serviceType}: ${errorMessage}`);
throw error;
}
Debug Logging
The runtime provides comprehensive debug logging:
// Enable debug logging
const runtime = new AgentRuntime({
// ... other options
settings: {
LOG_LEVEL: "debug",
},
});
// Debug logs include:
// - Plugin registration details
// - Service initialization
// - Model usage timing
// - Provider execution timing
// - Action execution context
// - Memory operations
// - Connection management
Performance Optimization
State Caching
The runtime implements state caching to improve performance:
// State caching in composeState
const cachedState = skipCache ? emptyObj : (await this.stateCache.get(message.id)) || emptyObj;
// Cache results
this.stateCache.set(message.id, newState);
Connection Pooling
The runtime supports connection pooling through the database adapter:
async getConnection(): Promise<unknown> {
if (!this.adapter) {
throw new Error('Database adapter not registered');
}
return this.adapter.getConnection();
}
Batch Operations
Use batch operations for better performance:
// Batch entity creation
await this.createEntities(entities);
// Batch room creation
await this.createRooms(rooms);
// Batch participant addition
await this.addParticipantsRoom(entityIds, roomId);
Best Practices
1. Initialization
Always initialize the runtime before using it and handle initialization errors gracefully.
try {
await runtime.initialize();
console.log("Runtime initialized successfully");
} catch (error) {
console.error("Failed to initialize runtime:", error);
process.exit(1);
}
2. Error Handling
Implement comprehensive error handling for all runtime operations:
// Wrap operations in try-catch
try {
const result = await runtime.useModel(ModelType.COMPLETION, params);
return result;
} catch (error) {
this.logger.error('Model usage failed:', error);
// Implement fallback or retry logic
}
3. Resource Management
Properly manage resources and implement cleanup:
// Graceful shutdown
process.on("SIGINT", async () => {
console.log("Shutting down gracefully...");
await runtime.stop();
process.exit(0);
});
4. Performance Monitoring
Monitor runtime performance:
// Track execution time
const startTime = performance.now();
const result = await runtime.useModel(ModelType.COMPLETION, params);
const executionTime = performance.now() - startTime;
console.log(`Model execution took ${executionTime}ms`);
5. Service Management
Use type-safe service access:
// Type-safe service access
const service = runtime.getTypedService<MyService>("myService");
if (service) {
await service.performOperation();
}
Troubleshooting
Common Issues
1. Plugin Registration Failures
Problem: Plugin fails to register with API key errors.
Solution: Check environment variables and plugin configuration:
// Check if required environment variables are set
if (!process.env.OPENAI_API_KEY) {
throw new Error("OPENAI_API_KEY environment variable is required");
}
2. Database Connection Issues
Problem: Database adapter not initialized.
Solution: Ensure SQL plugin is included:
// Make sure @elizaos/plugin-sql is included in your plugins
const plugins = [
sqlPlugin,
// ... other plugins
];
3. Service Not Found
Problem: Service not available when expected.
Solution: Check service registration and timing:
// Check if service is registered
if (!runtime.hasService("myService")) {
console.error("Service not registered. Check plugin configuration.");
}
// List available services
console.log("Available services:", runtime.getRegisteredServiceTypes());
4. Memory Issues
Problem: Out of memory errors during bulk operations.
Solution: Use batch processing:
// Process in batches
const batchSize = 1000;
for (let i = 0; i < entities.length; i += batchSize) {
const batch = entities.slice(i, i + batchSize);
await runtime.createEntities(batch);
}
5. Model Not Found
Problem: No handler found for model type.
Solution: Check model registration:
// Check if model is registered
const model = runtime.getModel(ModelType.COMPLETION);
if (!model) {
console.error("No completion model registered");
// Register a model or check plugin configuration
}
The runtime system provides a robust foundation for building intelligent agents with comprehensive lifecycle management, service orchestration, and debugging capabilities. By following the patterns and best practices outlined in this documentation, you can build reliable and scalable ElizaOS applications.