Plugin System
Complete guide to the ElizaOS plugin architecture and extension system with v1.2.0 enhancements
Plugin System
The ElizaOS v1.2.0 plugin system is a powerful and flexible architecture that allows developers to extend agent functionality through modular, reusable components. This system provides a standardized way to add actions, providers, services, models, and more to agents, with enhanced dependency resolution, migration support, and improved lifecycle management.
Plugin Architecture Overview
Core Components
The plugin system consists of several key components:
export interface Plugin {
name: string; // Unique plugin identifier
description: string; // Plugin description
version?: string; // Plugin version (new in v1.2.0)
// Lifecycle
init?: (config: Record<string, string>, runtime: IAgentRuntime) => Promise<void>;
// Configuration
config?: { [key: string]: any };
// Functionality
actions?: Action[]; // Agent actions
providers?: Provider[]; // Context providers
evaluators?: Evaluator[]; // Message evaluators
services?: (typeof Service)[]; // Long-running services
models?: { [key: string]: ModelHandler }; // AI model handlers
events?: PluginEvents; // Event handlers
routes?: Route[]; // HTTP routes
// Database
adapter?: IDatabaseAdapter; // Database adapter
schema?: any; // Database schema
migrations?: Migration[]; // Database migrations (new in v1.2.0)
// Metadata
dependencies?: string[]; // Plugin dependencies
testDependencies?: string[]; // Test dependencies
priority?: number; // Loading priority
// Enhanced dependency management (new in v1.2.0)
compatibilityVersion?: string; // Runtime compatibility version
peerDependencies?: string[]; // Peer plugin dependencies
optionalDependencies?: string[]; // Optional plugin dependencies
// Testing
tests?: TestSuite[]; // Test suites
// Metadata for plugin registry (new in v1.2.0)
author?: string; // Plugin author
repository?: string; // Source repository
license?: string; // License information
}
Plugin Lifecycle
Plugins follow a structured lifecycle with enhanced dependency management:
- Discovery: Plugin is discovered through dependency resolution
- Validation: Plugin structure, version compatibility, and dependencies are validated
- Dependency Resolution: Plugin dependencies are resolved in correct order (enhanced in v1.2.0)
- Registration: Plugin is registered with the runtime
- Migration: Database schemas and data are migrated if needed (enhanced in v1.2.0)
- Initialization: Plugin's
init
function is called - Component Registration: Actions, providers, services, etc. are registered
- Activation: Plugin becomes active and functional
v1.2.0 Plugin System Enhancements
Enhanced Dependency Resolution
The plugin system now supports sophisticated dependency resolution with version compatibility checking:
export const enhancedPlugin: Plugin = {
name: "enhanced-plugin",
version: "1.2.0",
description: "Plugin with enhanced dependency management",
compatibilityVersion: ">=1.2.0",
dependencies: ["@elizaos/plugin-sql@^1.0.0"],
peerDependencies: ["@elizaos/plugin-discord@^2.0.0"],
optionalDependencies: ["@elizaos/plugin-cache@^1.1.0"],
init: async (config, runtime) => {
// Enhanced dependency checking
const sqlPlugin = runtime.getPlugin("@elizaos/plugin-sql");
if (!sqlPlugin) {
throw new Error("Required SQL plugin not found");
}
// Check version compatibility
if (!runtime.isPluginCompatible(sqlPlugin, "^1.0.0")) {
throw new Error("SQL plugin version incompatible");
}
},
};
Migration System
Plugins can now define database migrations for schema evolution:
export const migrationPlugin: Plugin = {
name: "migration-example",
version: "1.2.0",
description: "Plugin with database migrations",
migrations: [
{
version: "1.0.0",
up: async (db, runtime) => {
// Create initial tables
await db.schema.createTable("plugin_data", (table) => {
table.uuid("id").primary();
table.uuid("agent_id").references("agents.id");
table.jsonb("data");
table.timestamp("created_at").defaultTo(db.fn.now());
});
},
down: async (db, runtime) => {
await db.schema.dropTable("plugin_data");
},
},
{
version: "1.1.0",
up: async (db, runtime) => {
// Add new columns
await db.schema.alterTable("plugin_data", (table) => {
table.jsonb("metadata");
table.timestamp("updated_at").defaultTo(db.fn.now());
});
},
down: async (db, runtime) => {
await db.schema.alterTable("plugin_data", (table) => {
table.dropColumn("metadata");
table.dropColumn("updated_at");
});
},
},
],
};
Service-Based Architecture
Plugins now primarily use services instead of clients for better lifecycle management:
export class EnhancedService extends Service {
static serviceType = "enhanced-service";
static dependencies = ["database", "cache"]; // Service-level dependencies
private client: ExternalClient;
private healthCheckInterval: NodeJS.Timer;
constructor(runtime: IAgentRuntime) {
super(runtime);
}
static async start(runtime: IAgentRuntime): Promise<EnhancedService> {
const service = new EnhancedService(runtime);
// Enhanced initialization with health checks
await service.initialize();
await service.startHealthChecks();
return service;
}
async initialize() {
// Initialize external client with retry logic
this.client = new ExternalClient({
apiKey: this.runtime.getSetting("API_KEY"),
timeout: 30000,
});
// Test connection
await this.client.ping();
// Register event handlers
this.runtime.registerEvent("message_received", this.handleMessage.bind(this));
}
async startHealthChecks() {
this.healthCheckInterval = setInterval(async () => {
try {
await this.client.ping();
} catch (error) {
this.runtime.logger.error("Health check failed:", error);
// Emit health check failure event
await this.runtime.emitEvent("service_health_check_failed", {
service: this.constructor.name,
error: error.message,
});
}
}, 60000); // Check every minute
}
async stop() {
if (this.healthCheckInterval) {
clearInterval(this.healthCheckInterval);
}
if (this.client) {
await this.client.disconnect();
}
}
// Enhanced method with context tracking
async handleMessage(context: any) {
const { message, runtime } = context;
// Access action context if available
const actionContext = runtime.currentActionContext;
if (actionContext) {
this.runtime.logger.debug(`Handling message in action context: ${actionContext.actionName}`);
}
// Process message with enhanced logging
await this.processMessage(message);
}
}
Enhanced Event System
Plugins can now register domain-specific events:
export const eventEnhancedPlugin: Plugin = {
name: "event-enhanced",
version: "1.2.0",
description: "Plugin with enhanced event handling",
events: {
// Domain-specific events
"database:connection_error": [
async (context) => {
const { error, runtime } = context;
await handleDatabaseError(error, runtime);
},
],
"ai:model_response": [
async (context) => {
const { response, modelType, runtime } = context;
await logModelUsage(response, modelType, runtime);
},
],
// Standard events with enhanced context
message_received: [
async (context) => {
const { message, runtime, runId, actionContext } = context;
// Enhanced context available
console.log(`Message received in run ${runId}`);
if (actionContext) {
console.log(`Within action: ${actionContext.actionName}`);
}
},
],
},
};
Plugin Types
Action Plugins
Actions define what agents can do:
// Example action plugin
export const actionPlugin: Plugin = {
name: "action-example",
description: "Example action plugin",
actions: [
{
name: "SEND_MESSAGE",
description: "Send a message to a user or channel",
similes: ["MESSAGE", "SEND", "REPLY"],
validate: async (runtime: IAgentRuntime, message: Memory, state?: State) => {
// Validate if action should be triggered
return message.content.text.includes("send message");
},
handler: async (
runtime: IAgentRuntime,
message: Memory,
state?: State,
options?: any,
callback?: HandlerCallback
) => {
// Extract target and content from message
const target = extractTarget(message.content.text);
const content = extractContent(message.content.text);
// Send message through appropriate channel
await runtime.sendMessageToTarget(target, {
text: content,
source: message.content.source,
});
// Call callback if provided
if (callback) {
await callback({
text: `Message sent to ${target.name}`,
source: message.content.source,
});
}
},
examples: [
[
{
name: "User",
content: {
text: 'Send "Hello world" to John',
},
},
{
name: "Agent",
content: {
text: "Message sent to John",
actions: ["SEND_MESSAGE"],
},
},
],
],
},
],
};
Provider Plugins
Providers supply context and data to agents:
// Example provider plugin
export const providerPlugin: Plugin = {
name: "weather-provider",
description: "Provides weather information",
providers: [
{
name: "WEATHER_PROVIDER",
description: "Current weather conditions",
get: async (
runtime: IAgentRuntime,
message: Memory,
state?: State
): Promise<ProviderResult> => {
// Get user location from message or state
const location =
extractLocation(message.content.text) || state?.values?.location || "New York";
// Fetch weather data
const weather = await fetchWeatherData(location);
return {
text: `Current weather in ${location}: ${weather.description}, ${weather.temperature}°F`,
values: {
weather: weather,
location: location,
temperature: weather.temperature,
},
data: {
weatherProvider: {
location,
weather,
lastUpdated: new Date().toISOString(),
},
},
};
},
},
],
config: {
WEATHER_API_KEY: process.env.WEATHER_API_KEY,
},
init: async (config: Record<string, string>) => {
if (!config.WEATHER_API_KEY) {
throw new Error("Weather API key is required");
}
},
};
Service Plugins
Services provide long-running background functionality:
// Example service plugin
export class NotificationService extends Service {
static serviceType = "notification";
private notifications: Map<string, NotificationSubscription> = new Map();
constructor(runtime: IAgentRuntime) {
super(runtime);
}
static async start(runtime: IAgentRuntime): Promise<NotificationService> {
const service = new NotificationService(runtime);
await service.initialize();
return service;
}
async initialize() {
// Subscribe to relevant events
this.runtime.on("MESSAGE_RECEIVED", this.handleMessage.bind(this));
this.runtime.on("TASK_COMPLETED", this.handleTaskComplete.bind(this));
}
async stop() {
// Cleanup subscriptions
this.notifications.clear();
}
private async handleMessage(data: any) {
// Process incoming messages for notifications
const { message, runtime } = data;
// Check if message triggers notifications
const subscriptions = this.findMatchingSubscriptions(message);
for (const subscription of subscriptions) {
await this.sendNotification(subscription, message);
}
}
async subscribe(userId: string, criteria: NotificationCriteria): Promise<string> {
const subscriptionId = uuidv4();
this.notifications.set(subscriptionId, {
id: subscriptionId,
userId,
criteria,
createdAt: new Date(),
});
return subscriptionId;
}
async unsubscribe(subscriptionId: string): Promise<boolean> {
return this.notifications.delete(subscriptionId);
}
}
export const notificationPlugin: Plugin = {
name: "notification-service",
description: "Handles user notifications",
services: [NotificationService],
actions: [
{
name: "SUBSCRIBE_NOTIFICATIONS",
description: "Subscribe to notifications",
validate: async (runtime, message) => {
return (
message.content.text.includes("subscribe") || message.content.text.includes("notify me")
);
},
handler: async (runtime, message) => {
const service = runtime.getService("notification") as NotificationService;
const criteria = extractNotificationCriteria(message.content.text);
const subscriptionId = await service.subscribe(message.entityId, criteria);
return {
text: `Subscription created: ${subscriptionId}`,
source: message.content.source,
};
},
},
],
};
Model Plugins
Model plugins provide AI model integrations:
// Example model plugin
export const modelPlugin: Plugin = {
name: "openai-models",
description: "OpenAI model integration",
models: {
[ModelType.TEXT_GENERATION]: async (runtime: IAgentRuntime, params: GenerateTextParams) => {
const { prompt, maxTokens = 150, temperature = 0.7 } = params;
// Initialize OpenAI client
const client = new OpenAI({
apiKey: runtime.getSetting("OPENAI_API_KEY"),
});
// Generate response
const response = await client.chat.completions.create({
model: "gpt-4",
messages: [
{ role: "system", content: "You are a helpful assistant." },
{ role: "user", content: prompt },
],
max_tokens: maxTokens,
temperature: temperature,
});
return response.choices[0].message.content;
},
[ModelType.TEXT_EMBEDDING]: async (runtime: IAgentRuntime, params: { text: string }) => {
const client = new OpenAI({
apiKey: runtime.getSetting("OPENAI_API_KEY"),
});
const response = await client.embeddings.create({
model: "text-embedding-ada-002",
input: params.text,
});
return response.data[0].embedding;
},
},
config: {
OPENAI_API_KEY: process.env.OPENAI_API_KEY,
},
init: async (config: Record<string, string>) => {
if (!config.OPENAI_API_KEY) {
throw new Error("OpenAI API key is required");
}
},
};
Database Plugins
Database plugins provide storage capabilities:
// Example database plugin
export const databasePlugin: Plugin = {
name: "postgresql-adapter",
description: "PostgreSQL database adapter",
adapter: new PostgresDatabaseAdapter(),
schema: {
// Drizzle schema definitions
customTable: pgTable("custom_table", {
id: uuid("id").primaryKey(),
agentId: uuid("agent_id").references(() => agents.id),
data: jsonb("data"),
createdAt: timestamp("created_at").defaultNow(),
}),
},
init: async (config: Record<string, string>, runtime: IAgentRuntime) => {
const adapter = runtime.adapter as PostgresDatabaseAdapter;
// Initialize connection
await adapter.connect(config.DATABASE_URL);
// Run migrations
await adapter.migrate();
},
};
Plugin Registration and Loading
Registration Process
Plugins are registered through the runtime:
// Manual registration
await runtime.registerPlugin(myPlugin);
// Automatic registration from character config
const character = {
name: "MyAgent",
plugins: ["@elizaos/plugin-sql", "@elizaos/plugin-discord", myCustomPlugin],
};
const runtime = new AgentRuntime({
character,
plugins: character.plugins,
});
Plugin Resolution
The runtime resolves plugins through enhanced mechanisms with dependency management:
// packages/core/src/runtime.ts - Enhanced for v1.2.0
async registerPlugin(plugin: Plugin): Promise<void> {
// 1. Validate plugin structure and compatibility
if (!plugin?.name) {
throw new Error('Plugin name is required');
}
// 2. Check version compatibility (new in v1.2.0)
if (plugin.compatibilityVersion) {
if (!this.isRuntimeCompatible(plugin.compatibilityVersion)) {
throw new Error(`Plugin ${plugin.name} requires runtime version ${plugin.compatibilityVersion}`);
}
}
// 3. Check for duplicates
const existingPlugin = this.plugins.find(p => p.name === plugin.name);
if (existingPlugin) {
this.logger.warn(`Plugin ${plugin.name} already registered`);
return;
}
// 4. Validate dependencies (enhanced in v1.2.0)
await this.validatePluginDependencies(plugin);
// 5. Add to active plugins
this.plugins.push(plugin);
// 6. Run migrations if present (new in v1.2.0)
if (plugin.migrations) {
await this.runPluginMigrations(plugin);
}
// 7. Initialize plugin
if (plugin.init) {
await plugin.init(plugin.config || {}, this);
}
// 8. Register database adapter
if (plugin.adapter) {
this.registerDatabaseAdapter(plugin.adapter);
}
// 9. Register components
if (plugin.actions) {
plugin.actions.forEach(action => this.registerAction(action));
}
if (plugin.providers) {
plugin.providers.forEach(provider => this.registerProvider(provider));
}
if (plugin.evaluators) {
plugin.evaluators.forEach(evaluator => this.registerEvaluator(evaluator));
}
if (plugin.services) {
plugin.services.forEach(service => this.registerService(service));
}
if (plugin.models) {
Object.entries(plugin.models).forEach(([modelType, handler]) => {
this.registerModel(modelType as ModelTypeName, handler, plugin.name, plugin.priority);
});
}
if (plugin.routes) {
plugin.routes.forEach(route => this.routes.push(route));
}
if (plugin.events) {
Object.entries(plugin.events).forEach(([event, handlers]) => {
handlers.forEach(handler => this.registerEvent(event, handler));
});
}
// 10. Log successful registration with version (new in v1.2.0)
this.logger.info(`Plugin ${plugin.name}${plugin.version ? ` v${plugin.version}` : ''} registered successfully`);
}
// New in v1.2.0: Plugin dependency validation
private async validatePluginDependencies(plugin: Plugin): Promise<void> {
const missing: string[] = [];
// Check required dependencies
if (plugin.dependencies) {
for (const dep of plugin.dependencies) {
const [name, version] = dep.split('@');
const depPlugin = this.plugins.find(p => p.name === name);
if (!depPlugin) {
missing.push(dep);
} else if (version && !this.isPluginVersionCompatible(depPlugin, version)) {
throw new Error(`Plugin ${name} version ${depPlugin.version} incompatible with required ${version}`);
}
}
}
// Check peer dependencies
if (plugin.peerDependencies) {
for (const dep of plugin.peerDependencies) {
const [name, version] = dep.split('@');
const depPlugin = this.plugins.find(p => p.name === name);
if (!depPlugin) {
this.logger.warn(`Peer dependency ${dep} not found for plugin ${plugin.name}`);
} else if (version && !this.isPluginVersionCompatible(depPlugin, version)) {
this.logger.warn(`Peer dependency ${name} version incompatible`);
}
}
}
if (missing.length > 0) {
throw new Error(`Missing dependencies for plugin ${plugin.name}: ${missing.join(', ')}`);
}
}
// New in v1.2.0: Plugin migration execution
private async runPluginMigrations(plugin: Plugin): Promise<void> {
if (!plugin.migrations || plugin.migrations.length === 0) {
return;
}
const migrationTable = 'plugin_migrations';
// Ensure migration table exists
await this.adapter.createMigrationTable(migrationTable);
// Get applied migrations
const appliedMigrations = await this.adapter.getAppliedMigrations(plugin.name);
// Sort migrations by version
const sortedMigrations = plugin.migrations.sort((a, b) =>
this.compareVersions(a.version, b.version)
);
// Apply pending migrations
for (const migration of sortedMigrations) {
if (!appliedMigrations.includes(migration.version)) {
try {
await migration.up(this.adapter.db, this);
await this.adapter.recordMigration(plugin.name, migration.version);
this.logger.info(`Applied migration ${migration.version} for plugin ${plugin.name}`);
} catch (error) {
this.logger.error(`Failed to apply migration ${migration.version}:`, error);
throw error;
}
}
}
}
Plugin Dependencies
Dependency Management
Plugins can declare enhanced dependencies with version specifications:
export const dependentPlugin: Plugin = {
name: "advanced-plugin",
version: "2.0.0",
description: "Plugin that depends on others",
compatibilityVersion: ">=1.2.0",
// Required dependencies with version constraints
dependencies: [
"@elizaos/plugin-sql@^1.0.0", // Database functionality
"@elizaos/plugin-discord@^2.0.0", // Discord integration
"custom-base-plugin@>=1.1.0", // Custom dependency
],
// Peer dependencies (should be present but not automatically installed)
peerDependencies: [
"@elizaos/plugin-cache@^1.0.0", // Cache functionality
],
// Optional dependencies (nice to have but not required)
optionalDependencies: [
"@elizaos/plugin-analytics@^1.0.0", // Analytics functionality
],
init: async (config: Record<string, string>, runtime: IAgentRuntime) => {
// Verify dependencies are available
const sqlPlugin = runtime.getPlugin("@elizaos/plugin-sql");
if (!sqlPlugin) {
throw new Error("SQL plugin dependency not found");
}
// Check version compatibility
if (!runtime.isPluginVersionCompatible(sqlPlugin, "^1.0.0")) {
throw new Error("SQL plugin version incompatible");
}
// Use dependency functionality
const database = runtime.adapter;
// Optional dependency handling
const analyticsPlugin = runtime.getPlugin("@elizaos/plugin-analytics");
if (analyticsPlugin) {
// Enable analytics if available
await this.enableAnalytics(analyticsPlugin);
}
},
};
Dependency Resolution
Dependencies are resolved during plugin loading with enhanced version checking:
// Enhanced dependency resolution for v1.2.0
async function resolveDependencies(plugins: Plugin[]): Promise<Plugin[]> {
const resolved: Plugin[] = [];
const pending = [...plugins];
const maxIterations = pending.length * 2; // Prevent infinite loops
let iterations = 0;
while (pending.length > 0 && iterations < maxIterations) {
const plugin = pending.shift()!;
iterations++;
// Check if dependencies are resolved
const unresolvedDeps =
plugin.dependencies?.filter((dep) => {
const [name, version] = dep.split("@");
const resolvedPlugin = resolved.find((p) => p.name === name);
if (!resolvedPlugin) {
return true; // Not resolved yet
}
// Check version compatibility if specified
if (version && !isVersionCompatible(resolvedPlugin.version, version)) {
throw new Error(
`Plugin ${name} version ${resolvedPlugin.version} incompatible with required ${version}`
);
}
return false; // Resolved and compatible
}) || [];
// Check peer dependencies (don't block but warn)
if (plugin.peerDependencies) {
for (const dep of plugin.peerDependencies) {
const [name, version] = dep.split("@");
const peerPlugin = resolved.find((p) => p.name === name);
if (!peerPlugin) {
console.warn(`Peer dependency ${dep} not found for plugin ${plugin.name}`);
} else if (version && !isVersionCompatible(peerPlugin.version, version)) {
console.warn(
`Peer dependency ${name} version ${peerPlugin.version} incompatible with required ${version}`
);
}
}
}
if (unresolvedDeps.length === 0) {
resolved.push(plugin);
} else {
// Move to end of queue
pending.push(plugin);
}
}
// Check for circular dependencies
if (pending.length > 0) {
const pendingNames = pending.map((p) => p.name);
throw new Error(
`Circular dependency detected or missing dependencies: ${pendingNames.join(", ")}`
);
}
return resolved;
}
// Version compatibility checking
function isVersionCompatible(current: string, required: string): boolean {
// Simple semver compatibility check
// This would use a full semver library in production
if (required.startsWith("^")) {
const requiredVersion = required.slice(1);
return current >= requiredVersion && current.split(".")[0] === requiredVersion.split(".")[0];
}
if (required.startsWith(">=")) {
const requiredVersion = required.slice(2);
return current >= requiredVersion;
}
if (required.startsWith("~")) {
const requiredVersion = required.slice(1);
const [major, minor] = requiredVersion.split(".");
const [currentMajor, currentMinor] = current.split(".");
return currentMajor === major && currentMinor === minor;
}
return current === required;
}
Event System
Event Handling
Plugins can register event handlers:
export const eventPlugin: Plugin = {
name: "event-handler",
description: "Handles various events",
events: {
MESSAGE_RECEIVED: [
async (params) => {
const { message, runtime } = params;
console.log(`Message received: ${message.content.text}`);
// Process message
await processIncomingMessage(message, runtime);
},
],
AGENT_CONNECTED: [
async (params) => {
const { agentId, runtime } = params;
console.log(`Agent connected: ${agentId}`);
// Setup agent-specific handlers
await setupAgentHandlers(agentId, runtime);
},
],
WORLD_JOINED: [
async (params) => {
const { worldId, entityId, runtime } = params;
// Update world state
await updateWorldState(worldId, entityId, runtime);
},
],
},
};
Custom Events
Plugins can emit custom events:
export const customEventPlugin: Plugin = {
name: "custom-events",
description: "Emits custom events",
actions: [
{
name: "TRIGGER_CUSTOM_EVENT",
description: "Triggers a custom event",
validate: async () => true,
handler: async (runtime, message) => {
// Emit custom event
await runtime.emitEvent("CUSTOM_EVENT", {
timestamp: new Date().toISOString(),
message: message.content.text,
entityId: message.entityId,
});
},
},
],
events: {
CUSTOM_EVENT: [
async (params) => {
console.log("Custom event triggered:", params);
// Handle custom event
await handleCustomEvent(params);
},
],
},
};
Plugin Testing
Test Structure
Plugins should include comprehensive tests:
// Example plugin test suite
export const pluginTestSuite: TestSuite = {
name: "Plugin Test Suite",
description: "Tests for the example plugin",
tests: [
{
name: "Action validation",
description: "Test action validation logic",
test: async (runtime: IAgentRuntime) => {
const action = examplePlugin.actions![0];
const message = createTestMessage("test message");
const isValid = await action.validate(runtime, message);
expect(isValid).toBe(true);
},
},
{
name: "Provider data",
description: "Test provider data retrieval",
test: async (runtime: IAgentRuntime) => {
const provider = examplePlugin.providers![0];
const message = createTestMessage("test message");
const result = await provider.get(runtime, message);
expect(result.text).toBeTruthy();
expect(result.values).toBeDefined();
},
},
{
name: "Service lifecycle",
description: "Test service start/stop",
test: async (runtime: IAgentRuntime) => {
const ServiceClass = examplePlugin.services![0];
const service = await ServiceClass.start(runtime);
expect(service).toBeDefined();
await service.stop();
},
},
],
};
Integration Testing
Test plugins with the runtime:
// Integration test example
describe("Plugin Integration", () => {
let runtime: IAgentRuntime;
beforeEach(async () => {
runtime = new AgentRuntime({
character: createTestCharacter(),
plugins: [examplePlugin],
});
await runtime.initialize();
});
afterEach(async () => {
await runtime.stop();
});
it("should register plugin components", () => {
expect(runtime.actions).toContain(expect.objectContaining({ name: "EXAMPLE_ACTION" }));
expect(runtime.providers).toContain(expect.objectContaining({ name: "EXAMPLE_PROVIDER" }));
});
it("should handle actions correctly", async () => {
const message = createTestMessage("example action");
const responses = await runtime.processMessage(message);
expect(responses).toHaveLength(1);
expect(responses[0].content.text).toContain("example response");
});
});
Plugin Development Best Practices
1. Structure and Organization
// Recommended plugin structure
export const wellStructuredPlugin: Plugin = {
name: "well-structured-plugin",
description: "A well-structured plugin example",
// Clear configuration
config: {
API_KEY: process.env.PLUGIN_API_KEY,
API_URL: process.env.PLUGIN_API_URL || "https://api.example.com",
},
// Robust initialization
init: async (config: Record<string, string>, runtime: IAgentRuntime) => {
// Validate configuration
if (!config.API_KEY) {
throw new Error("API key is required");
}
// Test connectivity
await testApiConnection(config.API_URL, config.API_KEY);
// Initialize plugin state
await initializePluginState(runtime);
},
// Well-documented components
actions: [
{
name: "WELL_DOCUMENTED_ACTION",
description: "Action with comprehensive documentation",
similes: ["DOCUMENTED", "EXAMPLE"],
validate: async (runtime, message, state) => {
// Clear validation logic
return validateMessage(message) && validateState(state);
},
handler: async (runtime, message, state, options, callback) => {
try {
// Error handling
const result = await processAction(message, state, options);
if (callback) {
await callback(result);
}
return result;
} catch (error) {
runtime.logger.error("Action failed:", error);
throw error;
}
},
// Comprehensive examples
examples: [
[
{
name: "User",
content: { text: "Example input" },
},
{
name: "Agent",
content: { text: "Example output", actions: ["WELL_DOCUMENTED_ACTION"] },
},
],
],
},
],
// Dependency management
dependencies: ["@elizaos/plugin-sql"],
// Test coverage
tests: [wellStructuredTestSuite],
};
2. Error Handling
// Robust error handling
export const errorHandlingPlugin: Plugin = {
name: "error-handling-example",
description: "Demonstrates proper error handling",
init: async (config: Record<string, string>, runtime: IAgentRuntime) => {
try {
// Validate configuration
validateConfig(config);
// Initialize with retries
await retryOperation(async () => {
await initializeExternalService(config);
}, 3);
} catch (error) {
runtime.logger.error("Plugin initialization failed:", error);
// Provide helpful error messages
if (error instanceof ConfigurationError) {
throw new Error(`Configuration error: ${error.message}`);
}
if (error instanceof NetworkError) {
throw new Error(`Network error: ${error.message}. Please check your connection.`);
}
throw error;
}
},
actions: [
{
name: "SAFE_ACTION",
description: "Action with comprehensive error handling",
validate: async (runtime, message, state) => {
try {
return await validateSafely(message, state);
} catch (error) {
runtime.logger.warn("Validation failed:", error);
return false;
}
},
handler: async (runtime, message, state, options, callback) => {
try {
const result = await processWithTimeout(message, state, options);
if (callback) {
await callback(result);
}
return result;
} catch (error) {
runtime.logger.error("Action execution failed:", error);
// Provide user-friendly error response
const errorResponse = {
text: "I encountered an error processing your request. Please try again.",
source: message.content.source,
error: true,
};
if (callback) {
await callback(errorResponse);
}
return errorResponse;
}
},
},
],
};
3. Performance Optimization
// Performance-optimized plugin
export const performantPlugin: Plugin = {
name: "performant-plugin",
description: "Demonstrates performance best practices",
providers: [
{
name: "CACHED_PROVIDER",
description: "Provider with caching",
get: async (runtime, message, state) => {
// Cache key generation
const cacheKey = generateCacheKey(message, state);
// Check cache first
const cached = await runtime.getCache(cacheKey);
if (cached) {
return cached;
}
// Compute expensive operation
const result = await computeExpensiveOperation(message, state);
// Cache result with TTL
await runtime.setCache(cacheKey, result, { ttl: 300 }); // 5 minutes
return result;
},
},
],
actions: [
{
name: "BATCH_ACTION",
description: "Action that processes items in batches",
validate: async () => true,
handler: async (runtime, message, state, options) => {
const items = extractItems(message.content.text);
// Process in batches to avoid memory issues
const batchSize = 10;
const results = [];
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
const batchResults = await Promise.all(batch.map((item) => processItem(item)));
results.push(...batchResults);
}
return {
text: `Processed ${results.length} items`,
source: message.content.source,
data: { results },
};
},
},
],
};
Advanced Plugin Features
Schema Management
Plugins can define database schemas:
export const schemaPlugin: Plugin = {
name: "schema-plugin",
description: "Plugin with database schema",
schema: {
// Drizzle schema definition
customTable: pgTable("custom_table", {
id: uuid("id").primaryKey(),
agentId: uuid("agent_id").references(() => agents.id),
pluginData: jsonb("plugin_data"),
createdAt: timestamp("created_at").defaultNow(),
updatedAt: timestamp("updated_at").defaultNow(),
}),
},
init: async (config, runtime) => {
// Schema migrations are handled automatically
// Access the table through the adapter
const db = runtime.db;
// Example usage
await db.insert(customTable).values({
id: uuidv4(),
agentId: runtime.agentId,
pluginData: { initialized: true },
});
},
};
Route Registration
Plugins can register HTTP routes:
export const routePlugin: Plugin = {
name: "route-plugin",
description: "Plugin with HTTP routes",
routes: [
{
name: "plugin-status",
path: "/api/plugin/status",
type: "GET",
handler: async (req, res, runtime) => {
const status = await getPluginStatus(runtime);
res.json({ status });
},
},
{
name: "plugin-config",
path: "/api/plugin/config",
type: "POST",
handler: async (req, res, runtime) => {
const config = req.body;
await updatePluginConfig(config, runtime);
res.json({ success: true });
},
},
],
};
The plugin system in ElizaOS provides a powerful foundation for extending agent capabilities while maintaining code quality, testability, and maintainability. By following these patterns and best practices, developers can create robust, reusable plugins that enhance the agent ecosystem.