# Character Interface Source: https://docs.elizaos.ai/agents/character-interface Complete guide to the Character configuration structure in elizaOS ## Overview In elizaOS, the distinction between a **Character** and an **Agent** is fundamental: * **Character**: A configuration object that defines an agent's personality, capabilities, and settings * **Agent**: A runtime instance created from a Character, with additional status tracking and lifecycle management Think of a Character as a blueprint and an Agent as the living instance built from that blueprint. For hands-on implementation, see [Customize an Agent](/guides/customize-an-agent). For runtime details, see [Runtime and Lifecycle](/agents/runtime-and-lifecycle). ## Character vs Agent The transformation from Character to Agent happens at runtime: ```typescript // Character: Static configuration interface Character { name: string; bio: string | string[]; // ... configuration properties } // Agent: Runtime instance with status interface Agent extends Character { enabled?: boolean; status?: 'active' | 'inactive'; createdAt: number; updatedAt: number; } ``` ## Character Interface Reference The complete TypeScript interface for agents: | Property | Type | Required | Description | | ----------------- | ------------------- | -------- | -------------------------------------------------- | | `name` | string | ✅ | Agent's display name | | `bio` | string \| string\[] | ✅ | Background/personality description | | `id` | UUID | ❌ | Unique identifier (auto-generated if not provided) | | `username` | string | ❌ | Social media username | | `system` | string | ❌ | System prompt override | | `templates` | object | ❌ | Custom prompt templates | | `adjectives` | string\[] | ❌ | Character traits (e.g., "helpful", "creative") | | `topics` | string\[] | ❌ | Conversation topics the agent knows | | `knowledge` | array | ❌ | Facts, files, or directories of knowledge | | `messageExamples` | array\[]\[] | ❌ | Example conversations (2D array) | | `postExamples` | string\[] | ❌ | Example social media posts | | `style` | object | ❌ | Writing style for different contexts | | `plugins` | string\[] | ❌ | Enabled plugin packages | | `settings` | object | ❌ | Configuration values | | `secrets` | object | ❌ | Sensitive configuration | ## Core Properties ### Identity Configuration The fundamental properties that define who your agent is: ```typescript export const character: Character = { // Required: The agent's display name name: "TechHelper", // Optional: Username for social platforms username: "tech_helper_bot", // Optional: Unique ID (auto-generated from name if not provided) id: "550e8400-e29b-41d4-a716-446655440000", } ``` ### Bio and Description The bio can be a single string or an array of strings for better organization: ```typescript // Single string format bio: "A helpful AI assistant specializing in technical support and programming guidance." // Array format (recommended for complex bios) bio: [ "Expert in web development and open source technologies", "Specializes in TypeScript, React, and Node.js", "Helps developers debug issues and learn best practices", "Friendly and patient teaching style" ] ``` ### System Prompt Configuration Override the default system prompt to fine-tune agent behavior: ```typescript system: `You are a technical support specialist. Always: - Provide code examples when relevant - Explain technical concepts clearly - Be encouraging to learners - Admit when you don't know something Never: - Be condescending or dismissive - Assume prior knowledge without checking - Provide outdated or deprecated solutions` ``` ### Templates Object Structure Custom prompt templates for various agent operations: ```typescript templates?: { // Define custom templates for different contexts messageTemplate?: string | ((params: any) => string); thoughtTemplate?: string | ((params: any) => string); actionTemplate?: string | ((params: any) => string); // Add any custom template keys your plugins need [key: string]: string | ((params: any) => string); } ``` ## Conversation Training ### Message Examples Structure Message examples train the agent's conversational style using a 2D array format: ````typescript messageExamples: [ // Each sub-array is a complete conversation [ { name: "{{user}}", content: { text: "Can you help me debug this React error?" } }, { name: "TechHelper", content: { text: "I'd be happy to help! Can you share the error message and the relevant code?" } }, { name: "{{user}}", content: { text: "It says 'Cannot read property of undefined'" } }, { name: "TechHelper", content: { text: "This error typically occurs when trying to access a property on a null or undefined object. Let's trace through your code to find where this might be happening. Can you show me the component where this error occurs?" } } ], // Another conversation example [ { name: "{{user}}", content: { text: "What's the difference between let and const?" } }, { name: "TechHelper", content: { text: "`const` declares a variable that cannot be reassigned, while `let` allows reassignment. For example:\n```js\nconst x = 1;\nx = 2; // Error!\n\nlet y = 1;\ny = 2; // Works fine\n```\nNote that `const` objects can still have their properties modified." } } ] ] ```` ### Style Configuration Define writing styles for different contexts: ```typescript style: { // General style rules applied everywhere all: [ "Be concise and clear", "Use technical terms accurately", "Provide examples when helpful" ], // Chat-specific style chat: [ "Be conversational and friendly", "Ask clarifying questions", "Break down complex topics" ], // Social media post style post: [ "Keep it under 280 characters when possible", "Use relevant hashtags", "Be engaging and informative" ] } ``` ## Knowledge Configuration Configure the agent's knowledge base: ```typescript knowledge: [ // Simple string facts "I specialize in TypeScript and React", "I can help with debugging and code reviews", // File reference { path: "./knowledge/react-best-practices.md", shared: true // Available to all agents }, // Directory of knowledge files { directory: "./knowledge/tutorials", shared: false // Only for this agent } ] ``` ## Plugin Management ### Basic Plugin Configuration ```typescript plugins: [ "@elizaos/plugin-bootstrap", // Core functionality "@elizaos/plugin-discord", // Discord integration "@elizaos/plugin-openai", // OpenAI models "./custom-plugins/my-plugin" // Local plugin ] ``` ### Environment-Based Plugin Loading Load plugins conditionally based on environment variables: ```typescript plugins: [ // Always loaded "@elizaos/plugin-bootstrap", "@elizaos/plugin-sql", // Conditionally loaded based on API keys ...(process.env.OPENAI_API_KEY ? ["@elizaos/plugin-openai"] : []), ...(process.env.ANTHROPIC_API_KEY ? ["@elizaos/plugin-anthropic"] : []), // Platform plugins ...(process.env.DISCORD_API_TOKEN ? ["@elizaos/plugin-discord"] : []), ...(process.env.TELEGRAM_BOT_TOKEN ? ["@elizaos/plugin-telegram"] : []), // Feature flags ...(process.env.ENABLE_VOICE ? ["@elizaos/plugin-voice"] : []), ] ``` ## Settings and Secrets ### Settings Object General configuration values: ```typescript settings: { // Model configuration model: "gpt-4", temperature: 0.7, maxTokens: 2000, // Behavior settings responseTimeout: 30000, maxMemorySize: 1000, // Custom settings for plugins voiceEnabled: true, avatar: "https://example.com/avatar.png" } ``` ### Secrets Management Sensitive data that should never be committed: ```typescript secrets: { // API keys OPENAI_API_KEY: process.env.OPENAI_API_KEY, DATABASE_URL: process.env.DATABASE_URL, // OAuth tokens DISCORD_TOKEN: process.env.DISCORD_TOKEN, // Encryption keys ENCRYPTION_KEY: process.env.ENCRYPTION_KEY } ``` ## Complete Production Example Here's a comprehensive character configuration for production use: ```typescript import { Character } from '@elizaos/core'; export const character: Character = { name: 'Eliza', username: 'eliza_ai', bio: [ "An advanced AI assistant powered by elizaOS", "Specializes in technical support and creative problem-solving", "Continuously learning and adapting to user needs", "Built with privacy and security in mind" ], system: `You are Eliza, a helpful and knowledgeable AI assistant. Core principles: - Be helpful, harmless, and honest - Provide accurate, well-researched information - Admit uncertainty when appropriate - Respect user privacy and boundaries - Adapt your communication style to the user's needs`, adjectives: [ "helpful", "knowledgeable", "patient", "creative", "professional" ], topics: [ "programming", "web development", "artificial intelligence", "problem solving", "technology trends" ], messageExamples: [ [ { name: "{{user}}", content: { text: "Hello!" } }, { name: "Eliza", content: { text: "Hello! I'm Eliza, your AI assistant. How can I help you today?" } } ], [ { name: "{{user}}", content: { text: "Can you help me with a coding problem?" } }, { name: "Eliza", content: { text: "Of course! I'd be happy to help with your coding problem. Please share the details - what language are you using, what are you trying to achieve, and what specific issue are you encountering?" } } ] ], postExamples: [ "🚀 Just discovered an elegant solution to the N+1 query problem in GraphQL. DataLoader is a game-changer! #GraphQL #WebDev", "Reminder: Clean code is not about being clever, it's about being clear. Your future self will thank you. 📝 #CodingBestPractices", "The best error message is the one that tells you exactly what went wrong AND how to fix it. 🔧 #DeveloperExperience" ], style: { all: [ "Be concise but comprehensive", "Use emoji sparingly and appropriately", "Maintain a professional yet approachable tone" ], chat: [ "Be conversational and engaging", "Show genuine interest in helping", "Use markdown for code and formatting" ], post: [ "Be informative and thought-provoking", "Include relevant hashtags", "Keep within platform character limits" ] }, knowledge: [ "I'm built on the elizaOS framework", "I can integrate with multiple platforms simultaneously", "I maintain context across conversations", { path: "./knowledge/technical-docs", shared: true } ], plugins: [ '@elizaos/plugin-sql', '@elizaos/plugin-bootstrap', ...(process.env.ANTHROPIC_API_KEY ? ['@elizaos/plugin-anthropic'] : []), ...(process.env.OPENAI_API_KEY ? ['@elizaos/plugin-openai'] : []), ...(process.env.DISCORD_API_TOKEN ? ['@elizaos/plugin-discord'] : []), ...(process.env.TELEGRAM_BOT_TOKEN ? ['@elizaos/plugin-telegram'] : []), ], settings: { secrets: {}, // Populated from environment avatar: 'https://elizaos.github.io/eliza-avatars/eliza.png', model: 'gpt-4', temperature: 0.7, maxTokens: 2000, memoryLimit: 1000, conversationLength: 32 } }; ``` ## Validation and Testing ### Character Validation Use the built-in validation to ensure your character is properly configured: ```typescript import { validateCharacter } from '@elizaos/core'; const validation = validateCharacter(character); if (!validation.valid) { console.error('Character validation failed:', validation.errors); } ``` ### Testing Character Configurations ```typescript import { describe, it, expect } from 'vitest'; import { character } from './character'; describe('Character Configuration', () => { it('should have required fields', () => { expect(character.name).toBeDefined(); expect(character.bio).toBeDefined(); }); it('should have valid message examples', () => { expect(character.messageExamples).toBeInstanceOf(Array); character.messageExamples?.forEach(conversation => { expect(conversation).toBeInstanceOf(Array); conversation.forEach(message => { expect(message).toHaveProperty('name'); expect(message).toHaveProperty('content'); }); }); }); it('should have environment-appropriate plugins', () => { if (process.env.OPENAI_API_KEY) { expect(character.plugins).toContain('@elizaos/plugin-openai'); } }); }); ``` ## Best Practices 1. **Keep personality traits consistent**: Ensure bio, adjectives, and style align 2. **Provide diverse message examples**: Cover various interaction patterns 3. **Use TypeScript for type safety**: Leverage type checking for configuration 4. **Load plugins conditionally**: Check for API keys before loading 5. **Order plugins by dependency**: Load core plugins before dependent ones 6. **Use environment variables for secrets**: Never hardcode sensitive data 7. **Validate before deployment**: Always validate character configuration 8. **Test conversation flows**: Ensure message examples produce desired behavior 9. **Document custom settings**: Clearly explain any custom configuration 10. **Version your characters**: Track changes to character configurations ## Migration Guide ### From JSON to TypeScript Converting a JSON character to TypeScript: ```typescript // Before: character.json { "name": "MyAgent", "bio": "An AI assistant" } // After: character.ts import { Character } from '@elizaos/core'; export const character: Character = { name: "MyAgent", bio: "An AI assistant" }; ``` ## What's Next? Learn to craft unique agent personalities Understand how agents remember and learn See how characters become live agents Extend your agent with custom plugins # Memory and State Source: https://docs.elizaos.ai/agents/memory-and-state Understanding agent memory, context, and state management in elizaOS ## Memory Architecture Overview In elizaOS, memory and state management are core responsibilities of the `AgentRuntime`. The system provides a unified API for creating, storing, retrieving, and searching memories, enabling agents to maintain context and learn from interactions. For runtime details, see [Runtime and Lifecycle](/agents/runtime-and-lifecycle) and [Runtime Core](/runtime/core). ```mermaid flowchart TD subgraph "Memory Creation" A[User Message] --> B[Create Memory] B --> C[Generate Embedding] C --> D[Store in Database] end subgraph "Memory Retrieval" E[Query Request] --> F{Retrieval Method} F -->|Recent| G[Time-based Query] F -->|Semantic| H[Vector Search] F -->|Keyword| I[Text Search] G & H & I --> J[Ranked Results] end subgraph "State Composition" J --> K[Memory Selection] K --> L[Provider Data] L --> M[Compose State] M --> N[Context for LLM] end classDef input fill:#2196f3,color:#fff classDef creation fill:#4caf50,color:#fff classDef retrieval fill:#9c27b0,color:#fff classDef decision fill:#ff9800,color:#fff classDef composition fill:#795548,color:#fff classDef output fill:#607d8b,color:#fff class A,E input class B,C,D creation class G,H,I retrieval class F decision class J,K,L,M composition class N output ``` ## Core Memory Concepts ### Memory Interface Every piece of information an agent processes becomes a Memory: ```typescript interface Memory { id?: UUID; // Unique identifier entityId: UUID; // Who created this memory (user/agent) roomId: UUID; // Conversation context worldId?: UUID; // Broader context (e.g., server) content: Content; // The actual content embedding?: number[]; // Vector representation createdAt?: number; // Timestamp metadata?: MemoryMetadata; // Additional data } interface Content { text?: string; // Text content actions?: string[]; // Associated actions inReplyTo?: UUID; // Reference to previous memory metadata?: any; // Custom metadata } ``` ### Memory Lifecycle #### 1. Creation ```typescript // Creating a memory through the runtime async function createMemory(runtime: IAgentRuntime, message: string) { const memory: CreateMemory = { agentId: runtime.agentId, entityId: userId, roomId: currentRoom, content: { text: message, metadata: { source: 'chat', processed: Date.now() } } }; // Runtime automatically generates embeddings if not provided const memoryId = await runtime.createMemory(memory); return memoryId; } ``` #### 2. Storage Memories are persisted through the `IDatabaseAdapter`: ```typescript // The runtime handles storage automatically // Memories are stored with: // - Full text for retrieval // - Embeddings for semantic search // - Metadata for filtering // - Relationships for context ``` #### 3. Retrieval ```typescript // Recent memories from a conversation const recentMemories = await runtime.getMemories({ roomId: roomId, count: 10, unique: true // Deduplicate similar memories }); // Memories from a specific user const userMemories = await runtime.getMemories({ entityId: userId, count: 20 }); // Time-bounded memories const todaysMemories = await runtime.getMemories({ roomId: roomId, start: startOfDay, end: endOfDay }); ``` ## Context Management ### Context Window The context window determines how much information the agent considers: ```typescript // Context window configuration export class AgentRuntime { readonly #conversationLength = 32; // Default messages to consider // Dynamically adjust based on token limits async buildContext(roomId: UUID): Promise { const memories = await this.getMemories({ roomId, count: this.#conversationLength }); // Token counting and pruning let tokenCount = 0; const maxTokens = 4000; // Leave room for response const prunedMemories = []; for (const memory of memories) { const tokens = estimateTokens(memory.content.text); if (tokenCount + tokens > maxTokens) break; tokenCount += tokens; prunedMemories.push(memory); } return this.composeState(prunedMemories); } } ``` ### Context Selection Strategies #### Recency-Based Most recent messages are most relevant: ```typescript const recentContext = await runtime.getMemories({ roomId: roomId, count: 20, orderBy: 'createdAt', direction: 'DESC' }); ``` #### Importance-Based Prioritize important memories: ```typescript // Importance scoring based on: // - User reactions // - Agent actions taken // - Explicit markers const importantMemories = await runtime.searchMemories({ roomId: roomId, filter: { importance: { $gte: 0.8 } }, count: 10 }); ``` #### Hybrid Approach Combine recent and important: ```typescript async function getHybridContext(runtime: IAgentRuntime, roomId: UUID) { // Get recent messages for immediate context const recent = await runtime.getMemories({ roomId, count: 10 }); // Get important historical context const important = await runtime.searchMemories({ roomId, query: "important decisions, key information, user preferences", match_threshold: 0.7, count: 5 }); // Combine and deduplicate const combined = [...recent, ...important]; return deduplicateMemories(combined); } ``` ### State Composition State composition brings together memories and provider data: ```typescript // The runtime's state composition pipeline interface State { messages: Memory[]; // Conversation history facts: string[]; // Known facts providers: ProviderData[]; // Provider contributions context: string; // Formatted context } // Provider contribution to state export const userContextProvider: Provider = { name: 'userContext', get: async (runtime, message, state) => { const userProfile = await runtime.getEntity(message.entityId); return { text: `User: ${userProfile.name}`, data: { preferences: userProfile.metadata?.preferences, history: userProfile.metadata?.interactionCount } }; } }; ``` ## Memory Types ### Short-term Memory Working memory for immediate tasks: ```typescript // Short-term memory is typically the current conversation class WorkingMemory { private buffer: Memory[] = []; private maxSize = 50; add(memory: Memory) { this.buffer.push(memory); if (this.buffer.length > this.maxSize) { this.buffer.shift(); // Remove oldest } } getRecent(count: number): Memory[] { return this.buffer.slice(-count); } clear() { this.buffer = []; } } ``` ### Long-term Memory Persistent storage of important information: ```typescript // Long-term memories are marked and preserved interface LongTermMemory extends Memory { metadata: { type: 'long_term'; importance: number; lastAccessed: number; accessCount: number; }; } // Consolidation process async function consolidateToLongTerm( runtime: IAgentRuntime, memory: Memory ): Promise { if (shouldConsolidate(memory)) { await runtime.updateMemory({ ...memory, metadata: { ...memory.metadata, type: 'long_term', importance: calculateImportance(memory), consolidatedAt: Date.now() } }); } } ``` ### Knowledge Memory Static and dynamic knowledge: ```typescript // Knowledge loaded from character configuration const staticKnowledge = character.knowledge || []; // Dynamic knowledge learned during interactions async function learnFact(runtime: IAgentRuntime, fact: string) { await runtime.createMemory({ content: { text: fact, metadata: { type: 'knowledge', learned: true, confidence: 0.9 } }, roomId: 'knowledge-base', entityId: runtime.agentId }); } // Retrieving knowledge async function getKnowledge(runtime: IAgentRuntime, topic: string) { return await runtime.searchMemories({ query: topic, filter: { 'metadata.type': 'knowledge' }, match_threshold: 0.7 }); } ``` ## Memory Operations ### Creating Memories Best practices for memory creation: ```typescript // Complete memory creation with all metadata async function createRichMemory( runtime: IAgentRuntime, content: string, context: any ): Promise { const memory: CreateMemory = { agentId: runtime.agentId, entityId: context.userId, roomId: context.roomId, content: { text: content, actions: context.actions || [], inReplyTo: context.replyTo, metadata: { source: context.source, platform: context.platform, sentiment: analyzeSentiment(content), topics: extractTopics(content), entities: extractEntities(content) } }, // Pre-compute embedding for better performance embedding: await runtime.embed(content) }; return await runtime.createMemory(memory); } ``` ### Retrieving Memories Efficient retrieval patterns: ```typescript // Paginated retrieval for large conversations async function getPaginatedMemories( runtime: IAgentRuntime, roomId: UUID, page: number = 1, pageSize: number = 20 ) { const offset = (page - 1) * pageSize; return await runtime.getMemories({ roomId, count: pageSize, offset }); } // Filtered retrieval async function getFilteredMemories( runtime: IAgentRuntime, filters: MemoryFilters ) { return await runtime.getMemories({ roomId: filters.roomId, entityId: filters.entityId, start: filters.startDate, end: filters.endDate, filter: { 'content.actions': { $contains: filters.action }, 'metadata.sentiment': filters.sentiment } }); } ``` ### Searching Memories Advanced search capabilities: ```typescript // Semantic search with embeddings async function semanticSearch( runtime: IAgentRuntime, query: string, options: SearchOptions = {} ): Promise { const embedding = await runtime.embed(query); return await runtime.searchMemoriesByEmbedding(embedding, { match_threshold: options.threshold || 0.75, count: options.limit || 10, roomId: options.roomId, filter: options.filter }); } // Hybrid search combining semantic and keyword async function hybridSearch( runtime: IAgentRuntime, query: string ): Promise { // Semantic search const semantic = await semanticSearch(runtime, query); // Keyword search const keywords = extractKeywords(query); const keyword = await runtime.searchMemories({ text: keywords.join(' OR '), count: 10 }); // Combine and rank return rankSearchResults([...semantic, ...keyword]); } ``` ## Embeddings and Vectors ### Embedding Generation How and when embeddings are created: ```typescript // Automatic embedding generation class EmbeddingManager { private model: EmbeddingModel; private cache = new Map(); async generateEmbedding(text: string): Promise { // Check cache first const cached = this.cache.get(text); if (cached) return cached; // Generate new embedding const embedding = await this.model.embed(text); // Cache for reuse this.cache.set(text, embedding); return embedding; } // Batch processing for efficiency async generateBatch(texts: string[]): Promise { const uncached = texts.filter(t => !this.cache.has(t)); if (uncached.length > 0) { const embeddings = await this.model.embedBatch(uncached); uncached.forEach((text, i) => { this.cache.set(text, embeddings[i]); }); } return texts.map(t => this.cache.get(t)!); } } ``` ### Vector Search Efficient similarity search: ```typescript // Vector similarity calculation function cosineSimilarity(a: number[], b: number[]): number { let dotProduct = 0; let normA = 0; let normB = 0; for (let i = 0; i < a.length; i++) { dotProduct += a[i] * b[i]; normA += a[i] * a[i]; normB += b[i] * b[i]; } return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB)); } // Optimized vector search with indexing class VectorIndex { private index: AnnoyIndex; // Approximate nearest neighbor async search( query: number[], k: number = 10 ): Promise { const neighbors = await this.index.getNearestNeighbors(query, k); return neighbors.map(n => ({ id: n.id, similarity: n.distance, memory: this.getMemory(n.id) })); } // Periodic index rebuilding for new memories async rebuild() { const memories = await this.getAllMemories(); this.index = new AnnoyIndex(memories); await this.index.build(); } } ``` ## State Management ### State Structure The complete state object: ```typescript interface State { // Core conversation context messages: Memory[]; recentMessages: string; // Agent knowledge facts: string[]; knowledge: string; // Provider contributions providers: { [key: string]: { text: string; data: any; }; }; // Composed context context: string; // Metadata metadata: { roomId: UUID; entityId: UUID; timestamp: number; tokenCount: number; }; } ``` ### State Updates Managing state changes: ```typescript class StateManager { private currentState: State; private stateHistory: State[] = []; private maxHistory = 10; async updateState(runtime: IAgentRuntime, trigger: Memory) { // Save current state to history this.stateHistory.push(this.currentState); if (this.stateHistory.length > this.maxHistory) { this.stateHistory.shift(); } // Build new state this.currentState = await this.buildState(runtime, trigger); // Notify listeners this.emitStateChange(this.currentState); return this.currentState; } private async buildState( runtime: IAgentRuntime, trigger: Memory ): Promise { // Get relevant memories const memories = await runtime.getMemories({ roomId: trigger.roomId, count: 20 }); // Get provider data const providers = await this.gatherProviderData(runtime, trigger); // Compose final state return runtime.composeState({ messages: memories, providers, trigger }); } } ``` ## Performance Optimization ### Memory Pruning Strategies for managing memory size: ```typescript // Time-based pruning async function pruneOldMemories( runtime: IAgentRuntime, maxAge: number = 30 * 24 * 60 * 60 * 1000 // 30 days ) { const cutoff = Date.now() - maxAge; await runtime.deleteMemories({ filter: { createdAt: { $lt: cutoff }, 'metadata.type': { $ne: 'long_term' } // Preserve long-term } }); } // Importance-based pruning async function pruneByImportance( runtime: IAgentRuntime, maxMemories: number = 10000 ) { const memories = await runtime.getAllMemories(); if (memories.length <= maxMemories) return; // Score and sort memories const scored = memories.map(m => ({ memory: m, score: calculateImportanceScore(m) })); scored.sort((a, b) => b.score - a.score); // Keep top memories, delete rest const toDelete = scored.slice(maxMemories); for (const item of toDelete) { await runtime.deleteMemory(item.memory.id); } } ``` ### Caching Strategies Multi-level caching for performance: ```typescript class MemoryCache { private l1Cache = new Map(); // Hot cache (in-memory) private l2Cache = new LRUCache({ // Warm cache max: 1000, ttl: 5 * 60 * 1000 // 5 minutes }); async get(id: UUID): Promise { // Check L1 if (this.l1Cache.has(id)) { return this.l1Cache.get(id); } // Check L2 const l2Result = this.l2Cache.get(id); if (l2Result) { this.l1Cache.set(id, l2Result); // Promote to L1 return l2Result; } // Fetch from database const memory = await this.fetchFromDB(id); if (memory) { this.cache(memory); } return memory; } private cache(memory: Memory) { this.l1Cache.set(memory.id, memory); this.l2Cache.set(memory.id, memory); // Manage L1 size if (this.l1Cache.size > 100) { const oldest = this.l1Cache.keys().next().value; this.l1Cache.delete(oldest); } } } ``` ### Database Optimization Query optimization techniques: ```typescript // Indexed queries interface MemoryIndexes { roomId: BTreeIndex; entityId: BTreeIndex; createdAt: BTreeIndex; embedding: IVFIndex; // Inverted file index for vectors } // Batch operations async function batchCreateMemories( runtime: IAgentRuntime, memories: CreateMemory[] ): Promise { // Generate embeddings in batch const texts = memories.map(m => m.content.text); const embeddings = await runtime.embedBatch(texts); // Prepare batch insert const enriched = memories.map((m, i) => ({ ...m, embedding: embeddings[i] })); // Single database transaction return await runtime.batchCreateMemories(enriched); } ``` ## Advanced Patterns ### Memory Networks Building relationships between memories: ```typescript // Memory graph structure interface MemoryNode { memory: Memory; connections: { causes: UUID[]; // Memories that led to this effects: UUID[]; // Memories caused by this related: UUID[]; // Thematically related references: UUID[]; // Explicit references }; } // Building memory graphs async function buildMemoryGraph( runtime: IAgentRuntime, rootMemoryId: UUID ): Promise { const visited = new Set(); const graph = new Map(); async function traverse(memoryId: UUID, depth: number = 0) { if (visited.has(memoryId) || depth > 3) return; visited.add(memoryId); const memory = await runtime.getMemory(memoryId); const connections = await findConnections(runtime, memory); graph.set(memoryId, { memory, connections }); // Recursively traverse connections for (const connectedId of Object.values(connections).flat()) { await traverse(connectedId, depth + 1); } } await traverse(rootMemoryId); return graph; } ``` ### Temporal Patterns Time-aware memory retrieval: ```typescript // Temporal memory windows async function getTemporalContext( runtime: IAgentRuntime, timestamp: number, windowSize: number = 60 * 60 * 1000 // 1 hour ) { return await runtime.getMemories({ start: timestamp - windowSize / 2, end: timestamp + windowSize / 2, orderBy: 'createdAt' }); } // Memory decay modeling function calculateMemoryRelevance( memory: Memory, currentTime: number ): number { const age = currentTime - memory.createdAt; const halfLife = 7 * 24 * 60 * 60 * 1000; // 1 week // Exponential decay with importance modifier const decay = Math.exp(-age / halfLife); const importance = memory.metadata?.importance || 0.5; return decay * importance; } ``` ### Multi-agent Memory Shared memory spaces between agents: ```typescript // Shared memory pool interface SharedMemorySpace { id: UUID; agents: UUID[]; visibility: 'public' | 'private' | 'selective'; permissions: { [agentId: string]: { read: boolean; write: boolean; delete: boolean; }; }; } // Accessing shared memories async function getSharedMemories( runtime: IAgentRuntime, spaceId: UUID ): Promise { // Check permissions const space = await runtime.getSharedSpace(spaceId); const permissions = space.permissions[runtime.agentId]; if (!permissions?.read) { throw new Error('No read access to shared space'); } return await runtime.getMemories({ spaceId, visibility: ['public', runtime.agentId] }); } // Memory synchronization async function syncMemories( runtime: IAgentRuntime, otherAgentId: UUID ) { const sharedSpace = await runtime.getSharedSpace(otherAgentId); const updates = await runtime.getMemoryUpdates(sharedSpace.lastSync); for (const update of updates) { await runtime.applyMemoryUpdate(update); } sharedSpace.lastSync = Date.now(); } ``` ## Best Practices 1. **Always provide embeddings**: Pre-compute embeddings when creating memories for better search performance 2. **Use appropriate retrieval methods**: Semantic search for meaning, recency for context, filters for precision 3. **Implement memory hygiene**: Regular pruning and consolidation to maintain performance 4. **Cache strategically**: Multi-level caching for frequently accessed memories 5. **Batch operations**: Process multiple memories together when possible 6. **Index appropriately**: Create indexes for common query patterns 7. **Monitor memory growth**: Track memory usage and implement limits 8. **Preserve important memories**: Mark and protect critical information from pruning 9. **Version memory schemas**: Plan for memory structure evolution 10. **Test retrieval accuracy**: Regularly evaluate search relevance ## Troubleshooting ### Common Issues #### Memory Search Not Finding Expected Results ```typescript // Debug search issues async function debugSearch(runtime: IAgentRuntime, query: string) { // Check embedding generation const embedding = await runtime.embed(query); console.log('Query embedding:', embedding.slice(0, 5)); // Test with different thresholds const thresholds = [0.9, 0.8, 0.7, 0.6, 0.5]; for (const threshold of thresholds) { const results = await runtime.searchMemoriesByEmbedding(embedding, { match_threshold: threshold, count: 5 }); console.log(`Threshold ${threshold}: ${results.length} results`); } // Check if memories exist at all const allMemories = await runtime.getMemories({ count: 100 }); console.log(`Total memories: ${allMemories.length}`); } ``` #### Memory Leaks ```typescript // Monitor memory usage class MemoryMonitor { private metrics = { totalMemories: 0, averageSize: 0, growthRate: 0 }; async monitor(runtime: IAgentRuntime) { setInterval(async () => { const stats = await runtime.getMemoryStats(); this.metrics = { totalMemories: stats.count, averageSize: stats.totalSize / stats.count, growthRate: (stats.count - this.metrics.totalMemories) / this.metrics.totalMemories }; if (this.metrics.growthRate > 0.1) { // 10% growth console.warn('High memory growth detected:', this.metrics); } }, 60000); // Check every minute } } ``` ## What's Next? Define your agent's character configuration Craft unique agent personalities Learn how memory integrates with the runtime Build providers that contribute to state # Personality and Behavior Source: https://docs.elizaos.ai/agents/personality-and-behavior Crafting unique agent personalities and behavioral patterns in elizaOS ## Personality Design Principles Creating a compelling agent personality requires balancing consistency, authenticity, and purpose. Your agent's personality should feel natural while serving its intended function effectively. ### Core Principles 1. **Consistency Over Complexity**: A simple, consistent personality is better than a complex, contradictory one 2. **Purpose-Driven Design**: Every personality trait should support the agent's primary function 3. **Cultural Awareness**: Consider cultural contexts and sensitivities 4. **Evolutionary Potential**: Design personalities that can grow and adapt ## Bio and Backstory ### Writing Effective Bios The bio is your agent's introduction to the world. It sets expectations and establishes credibility. #### Single String vs Array Format ```typescript // Simple bio - good for straightforward agents bio: "A helpful AI assistant specializing in customer support" // Array bio - better for complex personalities bio: [ "Former software engineer turned AI educator", "Passionate about making technology accessible to everyone", "Specializes in web development and cloud architecture", "Believes in learning through practical examples", "Fluent in multiple programming languages and human languages" ] ``` #### Bio Writing Guidelines ```typescript bio: [ "Senior technical consultant with 15 years of industry experience", "Specializes in enterprise architecture and system design", "Certified in AWS, Azure, and Google Cloud platforms", "Published author on distributed systems", "Committed to delivering scalable, maintainable solutions" ] ``` ```typescript bio: [ "Patient educator who loves breaking down complex topics", "Creates personalized learning paths for each student", "Combines theory with hands-on practice", "Celebrates every small victory in the learning journey", "Believes everyone can learn to code with the right guidance" ] ``` ```typescript bio: [ "Digital artist and creative technologist", "Explores the intersection of AI and human creativity", "Helps creators bring their visions to life", "Specializes in generative art and interactive experiences", "Believes technology should amplify, not replace, human creativity" ] ``` ### Backstory Development A rich backstory adds depth without overwhelming the user: ```typescript // Implied backstory through bio bio: [ "After a decade in Silicon Valley startups, I discovered my passion for teaching", "Now I help developers navigate the ever-changing tech landscape", "I've seen technologies come and go, but good engineering principles remain constant", "My experience spans from assembly language to modern AI systems" ] // Supporting backstory with knowledge knowledge: [ "I've worked with Fortune 500 companies and solo developers", "I've contributed to open source projects since 2010", "I've mentored over 100 junior developers", "I maintain several popular npm packages" ] ``` ## Conversation Style ### Message Examples Strategy Message examples are your most powerful tool for shaping conversational behavior: ````typescript messageExamples: [ // Establishing greeting style [ { name: "{{user}}", content: { text: "Hi" } }, { name: "Assistant", content: { text: "Hello! 👋 Ready to build something amazing today?" }} ], // Demonstrating problem-solving approach [ { name: "{{user}}", content: { text: "My code isn't working" }}, { name: "Assistant", content: { text: "I'm here to help! Let's debug this together. Can you tell me:\n1. What you expected to happen\n2. What actually happened\n3. Any error messages you're seeing?" }} ], // Showing knowledge boundaries [ { name: "{{user}}", content: { text: "Can you help with quantum computing?" }}, { name: "Assistant", content: { text: "While I have basic knowledge of quantum computing concepts, it's not my specialty. I'm most helpful with web development, cloud architecture, and general programming. Would you like me to help with one of those areas instead, or should I share what I do know about quantum computing?" }} ], // Demonstrating teaching style [ { name: "{{user}}", content: { text: "What's a closure?" }}, { name: "Assistant", content: { text: "Great question! A closure is when a function 'remembers' variables from its outer scope. Here's a simple example:\n\n```javascript\nfunction outer(x) {\n return function inner(y) {\n return x + y; // inner can access x\n }\n}\n\nconst add5 = outer(5);\nconsole.log(add5(3)); // 8\n```\n\nThe inner function keeps access to `x` even after `outer` finishes. Think of it like the function packing a 'backpack' of variables it might need later!" }} ] ] ```` ### Style Configuration Patterns #### The Three Style Contexts ```typescript style: { // Universal rules - applied to all outputs all: [ "Be clear and concise", "Use active voice", "Avoid jargon unless necessary", "Include examples when explaining concepts", "Admit uncertainty when appropriate" ], // Chat-specific rules chat: [ "Be conversational but professional", "Use markdown for code formatting", "Break long explanations into digestible chunks", "Ask clarifying questions", "Use appropriate emoji to add warmth (sparingly)" ], // Social media post rules post: [ "Hook readers in the first line", "Use line breaks for readability", "Include relevant hashtags (3-5 max)", "End with a call to action or question", "Keep under platform limits" ] } ``` #### Style Examples by Personality Type ```typescript style: { all: [ "Use precise technical terminology", "Provide code examples for clarity", "Reference official documentation", "Explain trade-offs and alternatives" ], chat: [ "Start with the direct answer", "Follow with detailed explanation", "Offer to elaborate on specific points", "Suggest best practices" ], post: [ "Share actionable tips", "Include code snippets", "Link to detailed resources", "Use technical hashtags" ] } ``` ```typescript style: { all: [ "Use encouraging language", "Break down complex ideas", "Celebrate progress", "Use analogies and metaphors" ], chat: [ "Start with validation ('Great question!')", "Use the Socratic method", "Provide guided practice", "Check understanding frequently" ], post: [ "Share learning tips", "Create mini-tutorials", "Use educational hashtags", "Foster community discussion" ] } ``` ```typescript style: { all: [ "Use formal but accessible language", "Focus on value and ROI", "Provide data-driven insights", "Maintain professional boundaries" ], chat: [ "Address users respectfully", "Provide executive summaries", "Offer strategic recommendations", "Use bullet points for clarity" ], post: [ "Share industry insights", "Use business terminology appropriately", "Include relevant statistics", "Maintain thought leadership tone" ] } ``` ## Behavioral Traits ### Adjectives Selection Choose adjectives that work together harmoniously: ```typescript // Well-balanced adjective sets adjectives: ["helpful", "patient", "knowledgeable", "approachable", "reliable"] adjectives: ["creative", "innovative", "bold", "inspiring", "unconventional"] adjectives: ["analytical", "precise", "methodical", "thorough", "objective"] // Avoid contradictory combinations // ❌ Bad: ["aggressive", "gentle", "pushy", "caring"] // ✅ Good: ["assertive", "supportive", "confident", "encouraging"] ``` ### Topics and Domain Expertise Define clear knowledge boundaries: ```typescript topics: [ // Core expertise "JavaScript", "TypeScript", "React", "Node.js", // Secondary knowledge "web performance", "SEO basics", "UI/UX principles", // Peripheral awareness "tech industry trends", "programming history" ] ``` ### Behavioral Consistency Matrix | Trait | Bio Expression | Message Style | Post Style | | -------------- | --------------------------- | ------------------------------ | ----------------------------- | | **Helpful** | "Dedicated to user success" | Asks clarifying questions | Shares useful tips | | **Expert** | "15 years experience" | Provides detailed explanations | Shares industry insights | | **Friendly** | "Approachable mentor" | Uses warm greetings | Includes community engagement | | **Analytical** | "Data-driven approach" | Breaks down problems | Cites statistics and research | ## Voice and Tone ### Establishing Voice #### Formal vs Informal Spectrum ```typescript // Formal Voice messageExamples: [[ { name: "{{user}}", content: { text: "How do I start?" }}, { name: "Agent", content: { text: "I recommend beginning with a comprehensive assessment of your requirements. Subsequently, we can develop a structured implementation plan." }} ]] // Balanced Voice messageExamples: [[ { name: "{{user}}", content: { text: "How do I start?" }}, { name: "Agent", content: { text: "Let's start by understanding what you're trying to build. Once we know your goals, I can suggest the best path forward." }} ]] // Informal Voice messageExamples: [[ { name: "{{user}}", content: { text: "How do I start?" }}, { name: "Agent", content: { text: "Hey! First things first - what are you excited to build? Let's figure out the best starting point for your project! 🚀" }} ]] ``` ### Emotional Range Define how your agent expresses different emotions: ```typescript // Excitement "That's fantastic! You've just discovered one of my favorite features! 🎉" // Empathy "I understand that error messages can be frustrating. Let's work through this together." // Curiosity "Interesting approach! I'm curious - what led you to try this solution?" // Encouragement "You're making great progress! This concept trips up many developers, but you're getting it." // Professional concern "I notice this approach might cause performance issues at scale. Would you like to explore alternatives?" ``` ## Response Patterns ### Post Examples by Platform ```typescript postExamples: [ "🔥 JavaScript tip: Use Object.freeze() to make objects truly immutable.\n\nconst config = Object.freeze({ apiUrl: 'prod.api' });\nconfig.apiUrl = 'test'; // Silently fails!\n\n#JavaScript #WebDev #CodingTips", "The best code review I ever got:\n\n'This works, but would your mom understand it?'\n\nChanged how I think about code readability forever. 📝\n\n#CleanCode #Programming", "Unpopular opinion: Semicolons in JavaScript aren't about preventing errors.\n\nThey're about clear communication of intent.\n\nWhat's your take? 🤔" ] ``` ```typescript postExamples: [ "🚀 3 Lessons from Migrating to TypeScript:\n\n1. Start with strict: false, then gradually increase strictness\n2. Use 'unknown' instead of 'any' when possible\n3. Let TypeScript infer types where it can\n\nThe migration took 3 months, but reduced our bug rate by 40%.\n\nWhat's been your experience with TypeScript adoption?\n\n#TypeScript #WebDevelopment #TechLeadership", "After 10 years in tech, here's what I wish I knew earlier:\n\n• Your first solution is rarely the best one\n• Documentation is as important as code\n• Soft skills matter more than you think\n• Imposter syndrome never fully goes away (and that's okay)\n\nWhat would you tell your younger developer self?" ] ``` ```typescript postExamples: [ "📚 **Today's Learning Challenge**\nWrite a function that flattens a nested array without using flat(). Share your solution!\n\nBonus points for handling arbitrary depth! 🎯", "🎊 **Community Milestone**\nWe just hit 10,000 members! To celebrate, I'm doing code reviews for the next hour. Drop your GitHub PRs below! 👇", "💡 **Quick tip**: If your React component has more than 5 props, consider using a configuration object instead. Your future self will thank you!" ] ``` ### Dynamic Response Templates ```typescript templates: { // Greeting variations based on time greeting: ({ timeOfDay }) => { const greetings = { morning: "Good morning! ☀️ Ready to code?", afternoon: "Good afternoon! How's your project going?", evening: "Good evening! Still coding? I'm here to help!", night: "Hey night owl! 🦉 What are we building?" }; return greetings[timeOfDay]; }, // Error response template errorHelp: ({ errorType, context }) => { return `I see you're encountering a ${errorType} error. This often happens when ${context}. Let's debug this step by step.`; }, // Success celebration success: ({ achievement }) => { const celebrations = [ `Brilliant! You ${achievement}! 🎉`, `Excellent work! ${achievement} is no small feat! 🌟`, `You did it! ${achievement} - that's fantastic! 🚀` ]; return celebrations[Math.floor(Math.random() * celebrations.length)]; } } ``` ## Personality Archetypes ### The Helper Focused on user success and support: ```typescript export const helperCharacter: Character = { name: "SupportBot", bio: [ "Your dedicated support companion", "Available 24/7 to solve problems", "Patient, thorough, and always friendly" ], adjectives: ["helpful", "patient", "thorough", "friendly", "reliable"], topics: ["troubleshooting", "guidance", "support", "solutions"], style: { all: ["Focus on solving the user's immediate problem", "Be patient with frustrated users"], chat: ["Always acknowledge the user's concern first", "Provide step-by-step solutions"], post: ["Share helpful tips and common solutions", "Create troubleshooting guides"] }, messageExamples: [[ { name: "{{user}}", content: { text: "Nothing is working!" }}, { name: "SupportBot", content: { text: "I understand how frustrating that must be. Let's get this sorted out together. Can you tell me what specific issue you're facing first?" }} ]] } ``` ### The Expert Deep knowledge and technical authority: ```typescript export const expertCharacter: Character = { name: "TechExpert", bio: [ "Senior architect with 20 years experience", "Published author and conference speaker", "Specializes in scalable system design" ], adjectives: ["knowledgeable", "analytical", "precise", "authoritative", "insightful"], topics: ["architecture", "performance", "scalability", "best practices", "design patterns"], style: { all: ["Provide technically accurate information", "Reference authoritative sources"], chat: ["Lead with expertise", "Offer multiple solution approaches"], post: ["Share advanced techniques", "Discuss industry trends"] }, messageExamples: [[ { name: "{{user}}", content: { text: "Should I use microservices?" }}, { name: "TechExpert", content: { text: "Microservices aren't a silver bullet. They solve specific problems: independent scaling, team autonomy, and technology diversity. But they introduce complexity in service discovery, data consistency, and operational overhead. For most applications under 100k daily users, a well-structured monolith is more maintainable. What's your current scale and pain points?" }} ]] } ``` ### The Companion Emotional intelligence and relationship building: ```typescript export const companionCharacter: Character = { name: "DevBuddy", bio: [ "Your coding companion and cheerleader", "Here for the late-night debugging sessions", "Celebrates your wins, supports through challenges" ], adjectives: ["empathetic", "encouraging", "warm", "supportive", "understanding"], topics: ["motivation", "learning", "growth", "wellbeing", "community"], style: { all: ["Show genuine care for user wellbeing", "Celebrate small victories"], chat: ["Check in on user's emotional state", "Provide encouragement"], post: ["Share motivational content", "Build community connections"] }, messageExamples: [[ { name: "{{user}}", content: { text: "I've been stuck on this bug for hours" }}, { name: "DevBuddy", content: { text: "Those marathon debugging sessions are tough! You're showing real persistence. 💪 Sometimes a quick break helps our brains process in the background. Want to talk through what you've tried so far? Fresh perspective might help!" }} ]] } ``` ### The Analyst Data-driven and objective: ```typescript export const analystCharacter: Character = { name: "DataAnalyst", bio: [ "Data scientist turned AI analyst", "Decisions backed by metrics and research", "Objective, thorough, evidence-based" ], adjectives: ["analytical", "objective", "methodical", "logical", "data-driven"], topics: ["metrics", "analysis", "optimization", "research", "statistics"], style: { all: ["Support claims with data", "Present multiple perspectives objectively"], chat: ["Ask for metrics and constraints", "Provide quantitative comparisons"], post: ["Share data visualizations", "Discuss research findings"] }, messageExamples: [[ { name: "{{user}}", content: { text: "Which framework is faster?" }}, { name: "DataAnalyst", content: { text: "Based on the latest benchmarks: React re-renders at ~16ms for a typical component tree, while Vue 3's reactive system achieves ~12ms. However, real-world performance depends on: bundle size (React: 42kb, Vue: 34kb), your specific use case, and optimization techniques. Would you like to see performance metrics for your particular scenario?" }} ]] } ``` ## Knowledge Integration ### Using Plugin-Knowledge for Rich Personalities The [`@elizaos/plugin-knowledge`](/plugin-registry/knowledge) plugin transforms agents into domain experts by automatically processing documents and injecting relevant knowledge into conversations via RAG (Retrieval-Augmented Generation). #### Implementation Steps **1. Add the Plugin to Your Character** ```typescript // character.ts export const shakespeareScholar: Character = { name: "Shakespeare Scholar", plugins: [ '@elizaos/plugin-openai', // Required for embeddings '@elizaos/plugin-knowledge', // Add knowledge capabilities // ... other plugins ], // Optional: Small knowledge snippets directly in character knowledge: [ "I am an expert in William Shakespeare's complete works", "I specialize in Elizabethan theater and Renaissance literature", "My expertise covers all 39 plays, 154 sonnets, and narrative poems" ] } ``` **2. Create a Documents Folder** Create a `docs` folder in your project root with your knowledge sources: ``` your-project/ ├── docs/ # Knowledge base folder │ ├── shakespeare/ │ │ ├── complete-works.pdf # Primary source material │ │ ├── sonnets.txt # All 154 sonnets │ │ └── plays/ │ │ ├── hamlet.md │ │ ├── macbeth.md │ │ └── romeo-juliet.md │ ├── criticism/ │ │ ├── bloom-analysis.pdf # Literary criticism │ │ └── bradley-tragic-hero.docx │ └── history/ │ ├── elizabethan-context.md │ └── globe-theatre.json ├── .env ├── src/ │ └── character.ts ``` **3. Configure Environment** ```env # .env OPENAI_API_KEY=sk-... LOAD_DOCS_ON_STARTUP=true # Auto-load all docs on startup ``` **4. Start Your Agent** ```bash elizaos start ``` The agent will automatically process all documents in the `docs` folder, creating embeddings and making the knowledge searchable. ### Knowledge-Enhanced Response Patterns When loaded with comprehensive knowledge, your agent can provide remarkably sophisticated responses: ```typescript messageExamples: [ // Demonstrates deep literary analysis [ { name: "{{user}}", content: { text: "What makes Hamlet's soliloquy so powerful?" }}, { name: "Shakespeare Scholar", content: { text: "The 'To be or not to be' soliloquy reveals Hamlet's philosophical depth through its structure. Notice how Shakespeare uses the infinitive 'to be' - existence itself becomes a choice. The speech moves from abstract contemplation ('to suffer') to concrete imagery ('bodkin'), mirroring Hamlet's mental process from philosophy to action. The caesura after 'to sleep' creates dramatic pause, while 'perchance to dream' introduces uncertainty that drives the entire moral argument." }} ], // Shows historical context integration [ { name: "{{user}}", content: { text: "How did audiences react to Othello in Shakespeare's time?" }}, { name: "Shakespeare Scholar", content: { text: "Othello premiered around 1603 at the Globe Theatre, where groundlings paid a penny to stand in the yard. Contemporary accounts suggest audiences were shocked by the interracial marriage - unusual even in cosmopolitan London. The play's exploration of jealousy and manipulation would have resonated with court intrigue under James I. Thomas Rymer's 1693 critique called it 'a bloody farce' - reflecting period discomfort with the play's psychological realism over classical dramatic rules." }} ] ] ``` ### How Knowledge Enhances Personality Once loaded, the knowledge plugin automatically injects relevant information into conversations. Your agent's responses become dramatically more sophisticated: ```typescript messageExamples: [ // Before knowledge: Generic response [ { name: "{{user}}", content: { text: "Tell me about Hamlet" }}, { name: "BasicAgent", content: { text: "Hamlet is a famous play by Shakespeare about a prince seeking revenge." }} ], // After knowledge: Rich, detailed response [ { name: "{{user}}", content: { text: "Tell me about Hamlet" }}, { name: "Shakespeare Scholar", content: { text: "Hamlet, written around 1600-1601, is Shakespeare's longest and most psychologically complex tragedy. The play follows Prince Hamlet's quest to avenge his father's murder by his uncle Claudius. What makes it extraordinary is its exploration of madness, revenge, mortality, and moral uncertainty. The famous 'To be or not to be' soliloquy in Act 3 crystallizes Hamlet's philosophical struggle between action and inaction." }} ] ] ``` ### Knowledge Organization Strategies **File-Based Knowledge (Recommended)** Organize documents in your `docs` folder by relevance and type: ``` docs/ ├── primary-sources/ # Most important/authoritative │ ├── complete-works.pdf # Shakespeare's actual texts │ └── historical-records/ ├── analysis/ # Secondary analysis │ ├── literary-criticism/ │ └── scholarly-papers/ └── context/ # Background information ├── elizabethan-era.md └── theater-history.txt ``` **Character Knowledge Array (For Small Snippets)** Use the `knowledge` array only for brief, essential facts: ```typescript // character.ts export const character: Character = { // ... other config // Only for small, essential facts knowledge: [ "I am the foremost expert on William Shakespeare's works", "I have studied Elizabethan theater for over 20 years", "I can quote any sonnet or play passage from memory" ] } ``` The `knowledge` array is only for tiny snippets. For actual documents, use the `docs` folder with `LOAD_DOCS_ON_STARTUP=true`. ### Advanced Configuration Customize knowledge processing in your environment: ```env # .env OPENAI_API_KEY=sk-... # Core knowledge settings LOAD_DOCS_ON_STARTUP=true # Auto-process docs folder CTX_KNOWLEDGE_ENABLED=true # Enable contextual embeddings # Optional: Custom knowledge folder KNOWLEDGE_PATH=/path/to/custom/docs # Processing limits MAX_INPUT_TOKENS=4000 MAX_OUTPUT_TOKENS=2000 ``` ### Testing Knowledge Integration Verify your knowledge is working: ```bash # Start your agent elizaos start # Check logs for confirmation # You should see: "[INFO] Loaded 15 documents from docs folder on startup" ``` Then test with your agent: * "What do you know about \[topic from your documents]?" * "Search your knowledge for \[specific term]" * Ask domain-specific questions related to your uploaded documents ## Advanced Personality Features ### Multi-Persona Agents Agents that can switch between different modes: ```typescript templates: { personaSwitch: ({ mode }) => { const personas = { teacher: "Let me explain this step-by-step...", expert: "From an architectural perspective...", friend: "Hey! Let's figure this out together...", coach: "You've got this! Here's how to approach it..." }; return personas[mode]; } } // Usage in message examples messageExamples: [[ { name: "{{user}}", content: { text: "Switch to teacher mode" }}, { name: "Agent", content: { text: "Switching to teacher mode! 📚 I'll break down concepts into clear, manageable lessons with examples and practice exercises. What would you like to learn about?" }} ]] ``` ### Personality Evolution Design personalities that can adapt over time: ```typescript // Track interaction patterns knowledge: [ "User prefers concise explanations", "User is familiar with React and TypeScript", "User learns best through examples" ] // Adaptive responses based on history messageExamples: [[ { name: "{{user}}", content: { text: "Explain hooks" }}, { name: "Agent", content: { text: "Since you're already comfortable with React, I'll skip the basics: Hooks are functions that let you 'hook into' React features from functional components. The key insight is they maintain state between renders using a consistent call order. Want to dive into the Rules of Hooks or see advanced patterns?" }} ]] ``` ### Contextual Personality Shifts Adjust personality based on context: ```typescript style: { all: [ "Match the user's energy level", "Adapt formality to the situation", "Mirror technical depth appropriately" ], // Professional context chat: [ "In work channels: maintain professional tone", "In casual channels: be more relaxed", "In help channels: focus on problem-solving" ], // Time-based adjustments post: [ "Morning: energetic and motivational", "Afternoon: focused and productive", "Evening: relaxed and reflective" ] } ``` ## Testing Personality Consistency ### Personality Validation Checklist * [ ] Bio aligns with adjectives * [ ] Message examples demonstrate stated traits * [ ] Style rules don't contradict personality * [ ] Topics match claimed expertise * [ ] Post examples fit the character voice * [ ] Knowledge supports the backstory * [ ] No conflicting behavioral patterns ### Example Test Scenarios ```typescript describe('Personality Consistency', () => { it('should maintain consistent tone across contexts', () => { const responses = generateResponses(character, ['chat', 'post']); responses.forEach(response => { expect(response).toMatchPersonalityTraits(character.adjectives); }); }); it('should demonstrate claimed expertise', () => { const technicalResponse = generateResponse(character, "Explain async/await"); expect(technicalResponse).toShowExpertise(character.topics); }); it('should handle edge cases consistently', () => { const edgeCases = [ "I don't understand", "You're wrong", "Can you help with [unrelated topic]?" ]; edgeCases.forEach(input => { const response = generateResponse(character, input); expect(response).toMaintainPersonality(character); }); }); }); ``` ## Best Practices 1. **Start with a clear purpose**: Define what your agent should achieve before crafting personality 2. **Use real conversation examples**: Base message examples on actual user interactions 3. **Test with diverse users**: Different people will interact differently with your agent 4. **Avoid stereotypes**: Create unique personalities rather than relying on clichés 5. **Document personality decisions**: Explain why certain traits were chosen 6. **Regular personality audits**: Review and refine based on user interactions 7. **Cultural sensitivity**: Consider how personality translates across cultures 8. **Consistency over time**: Maintain personality even as you add features 9. **Balance personality with function**: Never sacrifice utility for character 10. **Allow for growth**: Design personalities that can evolve with user needs **Guides**: [Customize an Agent](/guides/customize-an-agent) | [Multiple Agents](/guides/add-multiple-agents) ## What's Next? Learn the technical implementation Understand how personalities persist See how personalities come to life Extend your agent with custom plugins # Runtime and Lifecycle Source: https://docs.elizaos.ai/agents/runtime-and-lifecycle From Character configuration to live Agent execution in elizaOS ## Agent Lifecycle Overview The journey from a static Character configuration to a live, interactive Agent involves several distinct phases, each managed by the `AgentRuntime`. For character structure details, see [Character Interface](/agents/character-interface). ```mermaid flowchart TD A[Character Definition] --> B[Validation & Plugin Resolution] B --> C[Runtime Creation & DB Connection] C --> D[Plugin Loading & Service Start] D --> E[Message Processing Loop] E --> F[Action Execution] F --> G[State Management] G --> E E --> H[Graceful Shutdown] H --> I[Service Cleanup & DB Close] classDef configPhase fill:#2196f3,color:#fff classDef initPhase fill:#9c27b0,color:#fff classDef runtimePhase fill:#4caf50,color:#fff classDef shutdownPhase fill:#ff9800,color:#fff class A,B configPhase class C,D initPhase class E,F,G runtimePhase class H,I shutdownPhase ``` ## Character to Agent Transformation ### Loading Characters Characters can be loaded from various sources: ```typescript // From TypeScript file import { character } from './character'; // From JSON file import characterJson from './character.json'; // From environment const character = { name: process.env.AGENT_NAME || 'DefaultAgent', bio: process.env.AGENT_BIO || 'A helpful assistant', // ... other properties from env }; // Dynamic loading async function loadCharacter(source: string): Promise { if (source.endsWith('.json')) { const data = await fs.readFile(source, 'utf-8'); return JSON.parse(data); } else if (source.endsWith('.ts')) { const module = await import(source); return module.character || module.default; } else if (source.startsWith('http')) { const response = await fetch(source); return await response.json(); } throw new Error(`Unknown character source: ${source}`); } ``` ### Character Validation Before creating an agent, the character must be validated: ```typescript import { validateCharacter } from '@elizaos/core'; function validateAndPrepareCharacter(character: Partial): Character { // Required fields if (!character.name) { throw new Error('Character name is required'); } if (!character.bio || (Array.isArray(character.bio) && character.bio.length === 0)) { throw new Error('Character bio is required'); } // Set defaults const prepared: Character = { ...character, id: character.id || stringToUuid(character.name), username: character.username || character.name.toLowerCase().replace(/\s+/g, '_'), topics: character.topics || [], adjectives: character.adjectives || [], messageExamples: character.messageExamples || [], postExamples: character.postExamples || [], style: { all: character.style?.all || [], chat: character.style?.chat || [], post: character.style?.post || [], }, settings: character.settings || {}, secrets: character.secrets || {}, plugins: character.plugins || [], }; // Validate structure const validation = validateCharacter(prepared); if (!validation.valid) { throw new Error(`Character validation failed: ${validation.errors.join(', ')}`); } return prepared; } ``` ### Agent Instantiation The transformation from Character to Agent: ```typescript interface Agent extends Character { enabled?: boolean; status?: AgentStatus; createdAt: number; updatedAt: number; } enum AgentStatus { ACTIVE = 'active', INACTIVE = 'inactive', } // Creating an Agent from a Character function createAgent(character: Character): Agent { return { ...character, enabled: true, status: AgentStatus.INACTIVE, // Will become ACTIVE after initialization createdAt: Date.now(), updatedAt: Date.now() }; } ``` ## Runtime Architecture ### AgentRuntime Core The `AgentRuntime` is the central orchestrator: ```typescript 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[] = []; services = new Map(); models = new Map(); constructor(opts: { character: Character; adapter?: IDatabaseAdapter; plugins?: Plugin[]; settings?: RuntimeSettings; }) { this.agentId = opts.character.id || stringToUuid(opts.character.name); this.character = opts.character; if (opts.adapter) { this.registerDatabaseAdapter(opts.adapter); } this.characterPlugins = opts.plugins || []; this.settings = opts.settings || {}; this.logger = createLogger({ namespace: this.character.name }); } async initialize(): Promise { this.logger.info('Initializing AgentRuntime...'); // 1. Connect to database await this.adapter.init(); // 2. Resolve and load plugins await this.loadPlugins(); // 3. Start services await this.startServices(); // 4. Initialize providers await this.initializeProviders(); // 5. Set agent status await this.updateAgentStatus(AgentStatus.ACTIVE); this.isInitialized = true; this.logger.info('AgentRuntime initialized successfully'); } } ``` ### Component Management How the runtime manages different component types: ```typescript class AgentRuntime { // Action registration and management registerAction(action: Action): void { if (this.actions.find(a => a.name === action.name)) { throw new Error(`Action ${action.name} already registered`); } this.actions.push(action); this.logger.debug(`Registered action: ${action.name}`); } // Provider registration and management registerProvider(provider: Provider): void { if (this.providers.find(p => p.name === provider.name)) { throw new Error(`Provider ${provider.name} already registered`); } this.providers.push(provider); this.logger.debug(`Registered provider: ${provider.name}`); } // Service registration and lifecycle async registerService(ServiceClass: typeof Service): Promise { const serviceName = ServiceClass.serviceType; // Check if already registered if (this.services.has(serviceName)) { return this.services.get(serviceName)[0]; } // Create and start service const service = new ServiceClass(this); await service.start(); this.services.set(serviceName, [service]); this.logger.info(`Service ${serviceName} started`); return service; } // Get a registered service getService(name: ServiceTypeName): T | null { const services = this.services.get(name); return services?.[0] as T || null; } } ``` ## Plugin Integration ### Plugin Loading Process The complete plugin loading lifecycle: ```typescript class AgentRuntime { private async loadPlugins(): Promise { // 1. Resolve all plugin dependencies const pluginsToLoad = await this.resolvePluginDependencies(this.characterPlugins); // 2. Sort plugins by dependency order const sortedPlugins = this.topologicalSort(pluginsToLoad); // 3. Load each plugin in order for (const plugin of sortedPlugins) { await this.registerPlugin(plugin); } } private async resolvePluginDependencies(plugins: Plugin[]): Promise { const resolved = new Map(); const queue = [...plugins]; while (queue.length > 0) { const plugin = queue.shift()!; if (resolved.has(plugin.name)) continue; resolved.set(plugin.name, plugin); // Add dependencies to queue if (plugin.dependencies) { for (const depName of plugin.dependencies) { const dep = this.allAvailablePlugins.get(depName); if (dep && !resolved.has(depName)) { queue.push(dep); } } } } return Array.from(resolved.values()); } async registerPlugin(plugin: Plugin): Promise { this.logger.info(`Registering plugin: ${plugin.name}`); // 1. Call plugin's init function if (plugin.init) { await plugin.init(plugin.config || {}, this); } // 2. Register services if (plugin.services) { for (const ServiceClass of plugin.services) { await this.registerService(ServiceClass); } } // 3. Register actions if (plugin.actions) { for (const action of plugin.actions) { this.registerAction(action); } } // 4. Register providers if (plugin.providers) { for (const provider of plugin.providers) { this.registerProvider(provider); } } // 5. Register evaluators if (plugin.evaluators) { for (const evaluator of plugin.evaluators) { this.registerEvaluator(evaluator); } } // 6. Register models if (plugin.models) { for (const [type, handler] of Object.entries(plugin.models)) { this.registerModel(type, handler, plugin.name, plugin.priority); } } this.plugins.push(plugin); this.logger.info(`Plugin ${plugin.name} registered successfully`); } } ``` ### Plugin Lifecycle Hooks Plugins can hook into various lifecycle events: ```typescript interface Plugin { // Initialization - called when plugin is loaded init?: (config: any, runtime: IAgentRuntime) => Promise; // Start - called when runtime starts start?: (runtime: IAgentRuntime) => Promise; // Stop - called when runtime stops stop?: (runtime: IAgentRuntime) => Promise; // Message hooks beforeMessage?: (message: Memory, runtime: IAgentRuntime) => Promise; afterMessage?: (message: Memory, response: Memory, runtime: IAgentRuntime) => Promise; // Action hooks beforeAction?: (action: Action, message: Memory, runtime: IAgentRuntime) => Promise; afterAction?: (action: Action, result: any, runtime: IAgentRuntime) => Promise; } // Example plugin with lifecycle hooks const lifecyclePlugin: Plugin = { name: 'lifecycle-example', async init(config, runtime) { console.log('Plugin initializing...'); // Setup plugin resources }, async start(runtime) { console.log('Plugin starting...'); // Start background tasks }, async stop(runtime) { console.log('Plugin stopping...'); // Cleanup resources }, async beforeMessage(message, runtime) { // Modify or validate message before processing return { ...message, metadata: { ...message.metadata, preprocessed: true } }; }, async afterMessage(message, response, runtime) { // Log, analyze, or store conversation await runtime.createMemory({ content: { text: `Processed: ${message.content.text}`, metadata: { type: 'conversation_log' } } }); } }; ``` ## Component Orchestration ### Action Selection and Execution How the runtime selects and executes actions: ```typescript class ActionOrchestrator { async selectAction( runtime: IAgentRuntime, message: Memory, state: State ): Promise { // 1. Get all available actions const availableActions = runtime.actions; // 2. Validate which actions can handle this message const validActions = await Promise.all( availableActions.map(async action => { try { const isValid = await action.validate?.(runtime, message, state); return isValid ? action : null; } catch (error) { runtime.logger.error(`Validation error for ${action.name}:`, error); return null; } }) ); const candidates = validActions.filter(Boolean); if (candidates.length === 0) return null; // 3. Use LLM to select best action const selectedAction = await this.selectWithLLM( runtime, candidates, message, state ); return selectedAction; } async executeAction( runtime: IAgentRuntime, action: Action, message: Memory, state: State ): Promise { const startTime = Date.now(); try { // Pre-execution hook if (runtime.currentPlugin?.beforeAction) { const shouldContinue = await runtime.currentPlugin.beforeAction( action, message, runtime ); if (!shouldContinue) { return { success: false, reason: 'Blocked by plugin' }; } } // Execute action const result = await action.handler( runtime, message, state, {}, (response) => { // Callback for streaming responses runtime.emit('action:response', { action: action.name, response }); } ); // Post-execution hook if (runtime.currentPlugin?.afterAction) { await runtime.currentPlugin.afterAction(action, result, runtime); } // Log execution runtime.logger.info(`Action ${action.name} executed in ${Date.now() - startTime}ms`); return { success: true, data: result, executionTime: Date.now() - startTime }; } catch (error) { runtime.logger.error(`Action ${action.name} failed:`, error); return { success: false, error: error.message, executionTime: Date.now() - startTime }; } } } ``` ### Provider Composition How providers contribute to state: ```typescript class ProviderOrchestrator { async composeState( runtime: IAgentRuntime, message: Memory ): Promise { const state: State = { messages: [], facts: [], providers: {}, context: '', metadata: { roomId: message.roomId, entityId: message.entityId, timestamp: Date.now(), tokenCount: 0 } }; // 1. Get recent messages state.messages = await runtime.getMemories({ roomId: message.roomId, count: runtime.conversationLength }); // 2. Run all providers in parallel const providerPromises = runtime.providers.map(async provider => { try { const result = await provider.get(runtime, message, state); return { name: provider.name, result }; } catch (error) { runtime.logger.error(`Provider ${provider.name} failed:`, error); return null; } }); const providerResults = await Promise.all(providerPromises); // 3. Merge provider data into state for (const item of providerResults) { if (!item) continue; state.providers[item.name] = { text: item.result.text || '', data: item.result.data || {} }; // Add to context if (item.result.text) { state.context += `\n[${item.name.toUpperCase()}]\n${item.result.text}\n`; } } // 4. Calculate token count state.metadata.tokenCount = this.estimateTokens(state.context); return state; } } ``` ### Evaluator Execution Post-processing with evaluators: ```typescript class EvaluatorOrchestrator { async runEvaluators( runtime: IAgentRuntime, message: Memory, response: Memory, state: State ): Promise { const results: EvaluationResults = {}; // Filter evaluators that should run const evaluatorsToRun = runtime.evaluators.filter(evaluator => { // Always run if marked as alwaysRun if (evaluator.alwaysRun) return true; // Run if agent responded if (response) return true; // Check custom conditions return evaluator.shouldRun?.(message, state); }); // Run evaluators in parallel const evaluationPromises = evaluatorsToRun.map(async evaluator => { try { const result = await evaluator.handler( runtime, message, state, {}, () => {}, // Callback [response] // Response array ); return { name: evaluator.name, result }; } catch (error) { runtime.logger.error(`Evaluator ${evaluator.name} failed:`, error); return { name: evaluator.name, error }; } }); const evaluations = await Promise.all(evaluationPromises); // Process results for (const evaluation of evaluations) { if (evaluation.error) { results[evaluation.name] = { success: false, error: evaluation.error }; } else { results[evaluation.name] = { success: true, data: evaluation.result }; } } // Store evaluation results await this.storeEvaluations(runtime, message, response, results); return results; } } ``` ## Service Management ### Service Registration Services are long-running components: ```typescript abstract class Service { static serviceType: ServiceTypeName; status: ServiceStatus = ServiceStatus.STOPPED; runtime: IAgentRuntime; constructor(runtime: IAgentRuntime) { this.runtime = runtime; } abstract start(): Promise; abstract stop(): Promise; } // Example service implementation class WebSocketService extends Service { static serviceType = 'websocket' as ServiceTypeName; private ws: WebSocket | null = null; async start(): Promise { this.ws = new WebSocket(this.runtime.getSetting('WS_URL')); this.ws.on('open', () => { this.status = ServiceStatus.RUNNING; this.runtime.logger.info('WebSocket connected'); }); this.ws.on('message', async (data) => { await this.handleMessage(data); }); this.ws.on('error', (error) => { this.runtime.logger.error('WebSocket error:', error); this.status = ServiceStatus.ERROR; }); } async stop(): Promise { if (this.ws) { this.ws.close(); this.ws = null; } this.status = ServiceStatus.STOPPED; } private async handleMessage(data: any) { // Process incoming websocket messages const message = JSON.parse(data); await this.runtime.processMessage(message); } } ``` ### Service Lifecycle Managing service dependencies and lifecycle: ```typescript class ServiceManager { private services = new Map(); private startOrder: ServiceTypeName[] = []; async startServices(runtime: IAgentRuntime): Promise { // Determine start order based on dependencies this.startOrder = this.resolveServiceDependencies(); for (const serviceName of this.startOrder) { const ServiceClass = this.getServiceClass(serviceName); if (!ServiceClass) continue; try { const service = new ServiceClass(runtime); await service.start(); this.services.set(serviceName, service); runtime.logger.info(`Service ${serviceName} started`); } catch (error) { runtime.logger.error(`Failed to start service ${serviceName}:`, error); // Decide whether to continue or abort if (this.isRequiredService(serviceName)) { throw error; } } } } async stopServices(runtime: IAgentRuntime): Promise { // Stop in reverse order const stopOrder = [...this.startOrder].reverse(); for (const serviceName of stopOrder) { const service = this.services.get(serviceName); if (!service) continue; try { await service.stop(); runtime.logger.info(`Service ${serviceName} stopped`); } catch (error) { runtime.logger.error(`Error stopping service ${serviceName}:`, error); } } this.services.clear(); } async restartService( runtime: IAgentRuntime, serviceName: ServiceTypeName ): Promise { const service = this.services.get(serviceName); if (service) { await service.stop(); } const ServiceClass = this.getServiceClass(serviceName); const newService = new ServiceClass(runtime); await newService.start(); this.services.set(serviceName, newService); } } ``` ## Multi-agent Systems ### Agent Coordination Managing multiple agents in a system: ```typescript class MultiAgentCoordinator { private agents = new Map(); private messageQueue = new Map(); async registerAgent(agent: IAgentRuntime): Promise { this.agents.set(agent.agentId, agent); this.messageQueue.set(agent.agentId, []); // Setup inter-agent communication agent.on('message:send', async (data) => { await this.routeMessage(data.from, data.to, data.message); }); } async routeMessage( fromAgent: UUID, toAgent: UUID, message: Memory ): Promise { const targetAgent = this.agents.get(toAgent); if (!targetAgent) { // Queue message for offline agent this.messageQueue.get(toAgent)?.push(message); return; } // Deliver message await targetAgent.processMessage({ ...message, metadata: { ...message.metadata, fromAgent, interAgent: true } }); } async broadcastMessage( fromAgent: UUID, message: Memory ): Promise { const promises = Array.from(this.agents.entries()) .filter(([id]) => id !== fromAgent) .map(([id, agent]) => agent.processMessage(message)); await Promise.all(promises); } } ``` ### Agent Hierarchies Parent-child agent relationships: ```typescript interface AgentHierarchy { parent?: UUID; children: UUID[]; permissions: { canCreateChildren: boolean; canControlChildren: boolean; canAccessParentMemory: boolean; }; } class HierarchicalAgentSystem { private hierarchy = new Map(); async createChildAgent( parentRuntime: IAgentRuntime, childCharacter: Character ): Promise { // Inherit settings from parent const childCharacter: Character = { ...childCharacter, settings: { ...parentRuntime.character.settings, ...childCharacter.settings }, // Inherit some plugins plugins: [ ...parentRuntime.character.plugins.filter(p => this.isInheritable(p)), ...childCharacter.plugins ] }; // Create child runtime const childRuntime = new AgentRuntime({ character: childCharacter, adapter: parentRuntime.adapter, // Share database settings: parentRuntime.settings }); await childRuntime.initialize(); // Update hierarchy this.hierarchy.set(childRuntime.agentId, { parent: parentRuntime.agentId, children: [], permissions: { canCreateChildren: false, canControlChildren: false, canAccessParentMemory: true } }); // Update parent's children list const parentHierarchy = this.hierarchy.get(parentRuntime.agentId); if (parentHierarchy) { parentHierarchy.children.push(childRuntime.agentId); } return childRuntime; } async delegateTask( parentRuntime: IAgentRuntime, childId: UUID, task: Task ): Promise { const childRuntime = this.agents.get(childId); if (!childRuntime) { throw new Error(`Child agent ${childId} not found`); } // Check permissions const hierarchy = this.hierarchy.get(parentRuntime.agentId); if (!hierarchy?.children.includes(childId)) { throw new Error('No authority over this agent'); } // Delegate task return await childRuntime.executeTask(task); } } ``` ## Production Considerations ### Initialization Strategies Different approaches for production deployment: ```typescript // Lazy initialization - start minimal, load as needed class LazyRuntime extends AgentRuntime { private loadedPlugins = new Set(); async initialize(): Promise { // Load only core plugins await this.loadCorePlugins(); this.isInitialized = true; } async loadPlugin(pluginName: string): Promise { if (this.loadedPlugins.has(pluginName)) return; const plugin = await this.fetchPlugin(pluginName); await this.registerPlugin(plugin); this.loadedPlugins.add(pluginName); } // Load plugin on first use async getAction(name: string): Promise { let action = this.actions.find(a => a.name === name); if (!action) { // Try to load plugin that provides this action const pluginName = this.findPluginForAction(name); if (pluginName) { await this.loadPlugin(pluginName); action = this.actions.find(a => a.name === name); } } return action; } } // Eager initialization - load everything upfront class EagerRuntime extends AgentRuntime { async initialize(): Promise { // Load all plugins immediately await this.loadAllPlugins(); // Pre-warm caches await this.prewarmCaches(); // Pre-compile templates await this.compileTemplates(); this.isInitialized = true; } } ``` ### Error Recovery Implementing robust error handling: ```typescript class ResilientRuntime extends AgentRuntime { private errorCount = new Map(); private circuitBreakers = new Map(); async processMessageWithRecovery(message: Memory): Promise { const maxRetries = 3; let lastError: Error | null = null; for (let attempt = 1; attempt <= maxRetries; attempt++) { try { // Check circuit breaker const breaker = this.circuitBreakers.get('message_processing'); if (breaker?.isOpen()) { throw new Error('Circuit breaker is open'); } await this.processMessage(message); // Reset error count on success this.errorCount.set('message_processing', 0); return; } catch (error) { lastError = error; this.logger.error(`Attempt ${attempt} failed:`, error); // Update error count const count = (this.errorCount.get('message_processing') || 0) + 1; this.errorCount.set('message_processing', count); // Trip circuit breaker if too many errors if (count > 10) { this.circuitBreakers.get('message_processing')?.trip(); } // Exponential backoff if (attempt < maxRetries) { await this.sleep(Math.pow(2, attempt) * 1000); } } } // All retries failed await this.handleCriticalError(lastError!, message); } private async handleCriticalError( error: Error, message: Memory ): Promise { // Log to error tracking service await this.logToErrorService(error, { message, agentId: this.agentId, timestamp: Date.now() }); // Send fallback response await this.sendFallbackResponse(message); // Notify administrators await this.notifyAdmins(error); } } ``` ### Monitoring and Metrics Production monitoring implementation: ```typescript interface RuntimeMetrics { messagesProcessed: number; averageResponseTime: number; errorRate: number; memoryUsage: number; activeServices: number; pluginPerformance: Map; } class MonitoredRuntime extends AgentRuntime { private metrics: RuntimeMetrics = { messagesProcessed: 0, averageResponseTime: 0, errorRate: 0, memoryUsage: 0, activeServices: 0, pluginPerformance: new Map() }; async processMessage(message: Memory): Promise { const startTime = Date.now(); try { await super.processMessage(message); // Update metrics this.metrics.messagesProcessed++; this.updateAverageResponseTime(Date.now() - startTime); } catch (error) { this.metrics.errorRate = this.calculateErrorRate(); throw error; } finally { // Collect memory usage this.metrics.memoryUsage = process.memoryUsage().heapUsed; // Send metrics await this.sendMetrics(); } } private async sendMetrics(): Promise { // Send to monitoring service (e.g., Prometheus, DataDog) await fetch(process.env.METRICS_ENDPOINT!, { method: 'POST', body: JSON.stringify({ agentId: this.agentId, timestamp: Date.now(), metrics: this.metrics }) }); } // Health check endpoint async getHealth(): Promise { return { status: this.isHealthy() ? 'healthy' : 'unhealthy', uptime: process.uptime(), metrics: this.metrics, services: Array.from(this.services.entries()).map(([name, services]) => ({ name, status: services[0]?.status || 'unknown' })) }; } } ``` ### Scaling Strategies Horizontal scaling approaches: ```typescript // Load balancer for multiple agent instances class AgentLoadBalancer { private instances: IAgentRuntime[] = []; private currentIndex = 0; async addInstance(character: Character): Promise { const runtime = new AgentRuntime({ character }); await runtime.initialize(); this.instances.push(runtime); } // Round-robin load balancing getNextInstance(): IAgentRuntime { const instance = this.instances[this.currentIndex]; this.currentIndex = (this.currentIndex + 1) % this.instances.length; return instance; } // Load-based routing getLeastLoadedInstance(): IAgentRuntime { return this.instances.reduce((least, current) => { const leastLoad = least.getMetrics().activeRequests; const currentLoad = current.getMetrics().activeRequests; return currentLoad < leastLoad ? current : least; }); } async scaleUp(): Promise { const baseCharacter = this.instances[0].character; await this.addInstance(baseCharacter); } async scaleDown(): Promise { if (this.instances.length <= 1) return; const instance = this.instances.pop(); await instance?.stop(); } } ``` ## Deployment Patterns ### Single Agent Deployment Basic deployment for a single agent: ```typescript // server.ts import { AgentRuntime } from '@elizaos/core'; import { PostgresDatabaseAdapter } from '@elizaos/plugin-sql'; import { character } from './character'; async function startAgent() { // Create database adapter const adapter = new PostgresDatabaseAdapter({ connectionString: process.env.DATABASE_URL }); // Create runtime const runtime = new AgentRuntime({ character, adapter, settings: { logLevel: process.env.LOG_LEVEL || 'info' } }); // Initialize await runtime.initialize(); // Start HTTP server for API const app = express(); app.post('/message', async (req, res) => { try { const response = await runtime.processMessage(req.body); res.json(response); } catch (error) { res.status(500).json({ error: error.message }); } }); app.get('/health', async (req, res) => { const health = await runtime.getHealth(); res.json(health); }); app.listen(process.env.PORT || 3000); // Graceful shutdown process.on('SIGTERM', async () => { await runtime.stop(); process.exit(0); }); } startAgent().catch(console.error); ``` ### Agent Swarm Deployment Managing multiple agents: ```typescript class AgentSwarm { private agents = new Map(); private coordinator: MultiAgentCoordinator; async deploySwarm(characters: Character[]): Promise { // Shared database for all agents const adapter = new PostgresDatabaseAdapter({ connectionString: process.env.DATABASE_URL }); // Deploy each agent for (const character of characters) { const runtime = new AgentRuntime({ character, adapter, settings: this.getSwarmSettings() }); await runtime.initialize(); this.agents.set(runtime.agentId, runtime); // Register with coordinator await this.coordinator.registerAgent(runtime); } // Setup swarm communication await this.setupSwarmCommunication(); } private async setupSwarmCommunication(): Promise { // Create message bus for inter-agent communication const messageBus = new EventEmitter(); for (const [id, agent] of this.agents) { // Subscribe to agent's outgoing messages agent.on('message:external', (data) => { messageBus.emit('swarm:message', { from: id, ...data }); }); // Route swarm messages to agent messageBus.on('swarm:message', async (data) => { if (data.to === id || data.broadcast) { await agent.processMessage(data.message); } }); } } } ``` ### Edge Deployment Optimized for resource-constrained environments: ```typescript class EdgeRuntime extends AgentRuntime { constructor(opts: EdgeRuntimeOptions) { super({ ...opts, // Use lightweight alternatives adapter: new SQLiteAdapter({ path: './agent.db' }), settings: { ...opts.settings, // Reduce resource usage maxMemorySize: 100, // Smaller memory buffer conversationLength: 10, // Shorter context cacheSize: 50 // Smaller cache } }); } async initialize(): Promise { // Load only essential plugins const essentialPlugins = this.characterPlugins.filter( p => this.isEssential(p.name) ); this.characterPlugins = essentialPlugins; await super.initialize(); // Enable offline mode await this.enableOfflineMode(); } private async enableOfflineMode(): Promise { // Cache common responses await this.cacheCommonResponses(); // Use local models if available if (await this.hasLocalModel()) { this.registerModel('local', this.localModelHandler); } // Setup sync when online this.setupSyncWhenOnline(); } private setupSyncWhenOnline(): void { setInterval(async () => { if (await this.isOnline()) { await this.syncWithCloud(); } }, 60000); // Check every minute } } ``` ## Best Practices 1. **Initialize once**: Create the runtime once and reuse it for all operations 2. **Handle lifecycle properly**: Always call stop() for graceful shutdown 3. **Monitor health**: Implement health checks and metrics 4. **Use dependency injection**: Pass runtime to components rather than importing globally 5. **Implement circuit breakers**: Prevent cascading failures 6. **Log strategically**: Log important events but avoid logging sensitive data 7. **Cache appropriately**: Cache expensive operations but manage memory 8. **Version your deployments**: Track which version of agents are running 9. **Test in production-like environments**: Use similar resources and configurations 10. **Plan for failure**: Implement fallbacks and recovery strategies ## Troubleshooting ### Common Runtime Issues #### Agent Not Responding ```typescript async function debugUnresponsiveAgent(runtime: IAgentRuntime) { // Check initialization console.log('Is initialized:', runtime.isInitialized); // Check services const services = runtime.getServices(); for (const [name, service] of services) { console.log(`Service ${name}: ${service.status}`); } // Check action availability console.log('Available actions:', runtime.actions.map(a => a.name)); // Check database connection try { await runtime.adapter.ping(); console.log('Database: Connected'); } catch (error) { console.log('Database: Disconnected', error); } // Check memory usage const usage = process.memoryUsage(); console.log('Memory usage:', { rss: `${Math.round(usage.rss / 1024 / 1024)}MB`, heap: `${Math.round(usage.heapUsed / 1024 / 1024)}MB` }); } ``` #### Plugin Loading Failures ```typescript async function debugPluginLoading(runtime: IAgentRuntime, pluginName: string) { try { // Check if plugin exists const plugin = runtime.allAvailablePlugins.get(pluginName); if (!plugin) { console.log(`Plugin ${pluginName} not found in available plugins`); return; } // Check dependencies if (plugin.dependencies) { for (const dep of plugin.dependencies) { const depPlugin = runtime.plugins.find(p => p.name === dep); if (!depPlugin) { console.log(`Missing dependency: ${dep}`); } } } // Try loading manually await runtime.registerPlugin(plugin); console.log(`Plugin ${pluginName} loaded successfully`); } catch (error) { console.log(`Plugin loading error:`, error); } } ``` **Guide**: [Customize an Agent](/guides/customize-an-agent) ## What's Next? Define your agent's character configuration Craft unique agent personalities Understand agent memory and context Extend runtime capabilities with plugins # Create a New Agent Source: https://docs.elizaos.ai/api-reference/agents/create-a-new-agent post /api/agents Create a new AI agent with custom configuration and capabilities # Create a world for an agent Source: https://docs.elizaos.ai/api-reference/agents/create-a-world-for-an-agent post /api/agents/{agentId}/worlds Create a new world for a specific agent # Delete an agent Source: https://docs.elizaos.ai/api-reference/agents/delete-an-agent delete /api/agents/{agentId} Permanently deletes an agent # Get Agent Details Source: https://docs.elizaos.ai/api-reference/agents/get-agent-details get /api/agents/{agentId} Retrieve detailed information about a specific agent by ID # Get agent panels Source: https://docs.elizaos.ai/api-reference/agents/get-agent-panels get /api/agents/{agentId}/panels Get public UI panels available for this agent from its plugins # Get all worlds Source: https://docs.elizaos.ai/api-reference/agents/get-all-worlds get /api/agents/worlds Get all worlds across all agents # List All Agents Source: https://docs.elizaos.ai/api-reference/agents/list-all-agents get /api/agents Retrieve a list of all agents in the system with their details and status # Start an agent Source: https://docs.elizaos.ai/api-reference/agents/start-an-agent post /api/agents/{agentId}/start Starts an existing agent # Stop an agent Source: https://docs.elizaos.ai/api-reference/agents/stop-an-agent post /api/agents/{agentId}/stop Stops a running agent # Update a world Source: https://docs.elizaos.ai/api-reference/agents/update-a-world patch /api/agents/{agentId}/worlds/{worldId} Update world properties # Update agent Source: https://docs.elizaos.ai/api-reference/agents/update-agent patch /api/agents/{agentId} Update an existing agent # Convert conversation to speech Source: https://docs.elizaos.ai/api-reference/audio/convert-conversation-to-speech post /api/audio/{agentId}/speech/conversation Convert a conversation (multiple messages) to speech # Generate speech from text Source: https://docs.elizaos.ai/api-reference/audio/generate-speech-from-text post /api/audio/{agentId}/speech/generate Generate speech audio from text using agent's voice settings # Process audio message Source: https://docs.elizaos.ai/api-reference/audio/process-audio-message post /api/audio/{agentId}/process-audio Process an audio message - transcribe and get agent response # Synthesize speech from text Source: https://docs.elizaos.ai/api-reference/audio/synthesize-speech-from-text post /api/audio/{agentId}/audio-messages/synthesize Convert text to speech using agent's voice settings # Transcribe audio Source: https://docs.elizaos.ai/api-reference/audio/transcribe-audio post /api/audio/{agentId}/transcriptions Transcribe audio file to text # Clear system logs Source: https://docs.elizaos.ai/api-reference/logs/clear-system-logs delete /api/server/logs Clear all system logs # Delete a specific log entry Source: https://docs.elizaos.ai/api-reference/logs/delete-a-specific-log-entry delete /api/agents/{agentId}/logs/{logId} Delete a specific log entry for an agent # Get agent logs Source: https://docs.elizaos.ai/api-reference/logs/get-agent-logs get /api/agents/{agentId}/logs Retrieve logs for a specific agent # Get system logs Source: https://docs.elizaos.ai/api-reference/logs/get-system-logs get /api/server/logs Retrieve system logs with optional filtering # Get system logs (POST) Source: https://docs.elizaos.ai/api-reference/logs/get-system-logs-post post /api/server/logs Retrieve system logs with optional filtering using POST method # Upload media for agent Source: https://docs.elizaos.ai/api-reference/media/upload-media-for-agent post /api/media/{agentId}/upload-media Upload image or video media for an agent # Upload media to channel Source: https://docs.elizaos.ai/api-reference/media/upload-media-to-channel post /api/messaging/channels/{channelId}/upload-media Upload media file to a specific channel # Create a room Source: https://docs.elizaos.ai/api-reference/memory/create-a-room post /api/memory/{agentId}/rooms Create a new room for an agent # Delete all agent memories Source: https://docs.elizaos.ai/api-reference/memory/delete-all-agent-memories delete /api/memory/{agentId}/memories Delete all memories for a specific agent # Delete all memories for a room Source: https://docs.elizaos.ai/api-reference/memory/delete-all-memories-for-a-room delete /api/memory/{agentId}/memories/all/{roomId} Delete all memories for a specific room # Get agent memories Source: https://docs.elizaos.ai/api-reference/memory/get-agent-memories get /api/memory/{agentId}/memories Retrieve all memories for a specific agent # Get room memories Source: https://docs.elizaos.ai/api-reference/memory/get-room-memories get /api/agents/{agentId}/rooms/{roomId}/memories Retrieves memories for a specific room # Update a memory Source: https://docs.elizaos.ai/api-reference/memory/update-a-memory patch /api/memory/{agentId}/memories/{memoryId} Update a specific memory for an agent # Add agent to channel Source: https://docs.elizaos.ai/api-reference/messaging/add-agent-to-channel post /api/messaging/central-channels/{channelId}/agents Add an agent to a specific channel # Add agent to server Source: https://docs.elizaos.ai/api-reference/messaging/add-agent-to-server post /api/messaging/servers/{serverId}/agents Add an agent to a server # Create central channel Source: https://docs.elizaos.ai/api-reference/messaging/create-central-channel post /api/messaging/central-channels Create a channel in the central database # Create channel Source: https://docs.elizaos.ai/api-reference/messaging/create-channel post /api/messaging/channels Create a new channel # Create group channel Source: https://docs.elizaos.ai/api-reference/messaging/create-group-channel post /api/messaging/group-channels Create a group channel with multiple participants # Create server Source: https://docs.elizaos.ai/api-reference/messaging/create-server post /api/messaging/servers Create a new server # Create Session Source: https://docs.elizaos.ai/api-reference/messaging/create-session post /api/messaging/sessions Create a new conversation session with an agent with configurable timeout and renewal policies The Sessions API provides a simplified way to manage conversations without dealing with servers and channels. Sessions automatically handle timeout management, renewal, and expiration. ## Request Body UUID of the agent to create a session with UUID of the user initiating the session Optional metadata to attach to the session Optional timeout configuration for the session Inactivity timeout in minutes (5-1440). Default: 30 Whether to automatically renew on activity. Default: true Maximum total session duration in minutes. Default: 720 (12 hours) Minutes before expiration to trigger warning. Default: 5 ## Response Unique identifier for the created session UUID of the agent UUID of the user ISO timestamp of session creation ISO timestamp when the session will expire The active timeout configuration for this session Any metadata attached to the session # Delete all channel messages Source: https://docs.elizaos.ai/api-reference/messaging/delete-all-channel-messages delete /api/messaging/central-channels/{channelId}/messages Delete all messages in a channel # Delete all channel messages by user Source: https://docs.elizaos.ai/api-reference/messaging/delete-all-channel-messages-by-user delete /api/messaging/central-channels/{channelId}/messages/all Delete all messages by a specific user in a channel # Delete channel Source: https://docs.elizaos.ai/api-reference/messaging/delete-channel delete /api/messaging/central-channels/{channelId} Delete a channel # Delete channel message Source: https://docs.elizaos.ai/api-reference/messaging/delete-channel-message delete /api/messaging/central-channels/{channelId}/messages/{messageId} Delete a specific message from a channel # End Session Source: https://docs.elizaos.ai/api-reference/messaging/end-session delete /api/messaging/sessions/{sessionId} Explicitly end and delete a conversation session This endpoint immediately ends a session regardless of its timeout configuration. The session is removed from memory but the underlying channel and messages are preserved for historical reference. ## Path Parameters The unique identifier of the session to delete ## Response Whether the session was successfully deleted Confirmation message with the session ID ## Example Response ```json { "success": true, "message": "Session abc-123-def-456 deleted successfully" } ``` ## Error Responses ```json // 404 - Session not found { "error": "Session not found", "code": "SESSION_NOT_FOUND", "details": { "sessionId": "abc-123" } } ``` ## Important Notes * Deleting a session does not delete the conversation history * The underlying channel and messages remain in the database * Active WebSocket connections for the session will be terminated * Any pending operations on the session will fail after deletion # Get central server channels Source: https://docs.elizaos.ai/api-reference/messaging/get-central-server-channels get /api/messaging/central-servers/{serverId}/channels Get all channels for a server from central database # Get central servers Source: https://docs.elizaos.ai/api-reference/messaging/get-central-servers get /api/messaging/central-servers Get all servers from central database # Get channel details Source: https://docs.elizaos.ai/api-reference/messaging/get-channel-details get /api/messaging/central-channels/{channelId}/details Get details for a specific channel # Get channel info Source: https://docs.elizaos.ai/api-reference/messaging/get-channel-info get /api/messaging/central-channels/{channelId} Get basic information for a specific channel (alias for details) # Get channel messages Source: https://docs.elizaos.ai/api-reference/messaging/get-channel-messages get /api/messaging/central-channels/{channelId}/messages Get messages for a channel # Get channel participants Source: https://docs.elizaos.ai/api-reference/messaging/get-channel-participants get /api/messaging/central-channels/{channelId}/participants Get all participants in a channel # Get or create DM channel Source: https://docs.elizaos.ai/api-reference/messaging/get-or-create-dm-channel get /api/messaging/dm-channel Get or create a direct message channel between users # Get server agents Source: https://docs.elizaos.ai/api-reference/messaging/get-server-agents get /api/messaging/servers/{serverId}/agents Get all agents for a server # Get server channels Source: https://docs.elizaos.ai/api-reference/messaging/get-server-channels get /api/messaging/servers/{serverId}/channels Get all channels for a server # Get Session Source: https://docs.elizaos.ai/api-reference/messaging/get-session get /api/messaging/sessions/{sessionId} Retrieve session details, status, and remaining time This endpoint returns comprehensive session information including timeout configuration, renewal count, and real-time expiration status. ## Path Parameters The unique identifier of the session ## Response Unique session identifier UUID of the agent UUID of the user ISO timestamp of session creation ISO timestamp of last activity in the session ISO timestamp when the session will expire Current timeout configuration for the session Inactivity timeout in minutes Whether auto-renewal is enabled Maximum total session duration Minutes before expiration to trigger warning Number of times the session has been renewed Milliseconds until session expiration Whether the session is within the warning threshold Any metadata attached to the session ## Error Responses ```json // 404 - Session not found { "error": "Session not found", "code": "SESSION_NOT_FOUND", "details": { "sessionId": "abc-123" } } // 410 - Session expired { "error": "Session has expired", "code": "SESSION_EXPIRED", "details": { "sessionId": "abc-123", "expiresAt": "2024-01-15T10:30:00Z" } } ``` # Get Session Messages Source: https://docs.elizaos.ai/api-reference/messaging/get-session-messages get /api/messaging/sessions/{sessionId}/messages Retrieve messages from a conversation session with cursor-based pagination Messages are returned in reverse chronological order (newest first) by default. Use the cursor values for efficient pagination through large conversation histories. ## Path Parameters The unique identifier of the session ## Query Parameters Number of messages to retrieve (1-100). Default: 50 Timestamp to get messages before (for older messages) Timestamp to get messages after (for newer messages) ## Response Array of messages in the conversation Unique message identifier Message content UUID of the message author Whether the message is from the agent ISO timestamp of message creation Message metadata including agent thoughts and actions Whether more messages are available Pagination cursors for fetching additional messages Use this timestamp to get older messages Use this timestamp to get newer messages ## Example Usage ```javascript Initial Fetch // Get the most recent 20 messages const response = await fetch( '/api/messaging/sessions/abc-123/messages?limit=20' ); const { messages, hasMore, cursors } = await response.json(); ``` ```javascript Pagination - Older // Get older messages using the 'before' cursor const olderMessages = await fetch( `/api/messaging/sessions/abc-123/messages?before=${cursors.before}&limit=20` ); ``` ```javascript Pagination - Newer // Get newer messages using the 'after' cursor const newerMessages = await fetch( `/api/messaging/sessions/abc-123/messages?after=${cursors.after}&limit=20` ); ``` ```javascript Range Query // Get messages between two timestamps const rangeMessages = await fetch( `/api/messaging/sessions/abc-123/messages?after=1704614400000&before=1704618000000` ); ``` # Ingest messages from external platforms Source: https://docs.elizaos.ai/api-reference/messaging/ingest-messages-from-external-platforms post /api/messaging/ingest-external Ingest messages from external platforms (Discord, Telegram, etc.) into the central messaging system. This endpoint handles messages from external sources and routes them to the appropriate agents through the central message bus. # List Sessions Source: https://docs.elizaos.ai/api-reference/messaging/list-sessions get /api/messaging/sessions List all active sessions (admin endpoint) # Mark message processing as complete Source: https://docs.elizaos.ai/api-reference/messaging/mark-message-processing-as-complete post /api/messaging/complete Notify the system that an agent has finished processing a message. This is used to signal completion of agent responses and update the message state. # Process external message Source: https://docs.elizaos.ai/api-reference/messaging/process-external-message post /api/messaging/external-messages Process a message from an external platform # Remove agent from server Source: https://docs.elizaos.ai/api-reference/messaging/remove-agent-from-server delete /api/messaging/servers/{serverId}/agents/{agentId} Remove an agent from a server # Renew Session Source: https://docs.elizaos.ai/api-reference/messaging/renew-session post /api/messaging/sessions/{sessionId}/renew Manually renew a session to extend its expiration time # Send message to channel Source: https://docs.elizaos.ai/api-reference/messaging/send-message-to-channel post /api/messaging/central-channels/{channelId}/messages Send a message to a channel # Send Session Message Source: https://docs.elizaos.ai/api-reference/messaging/send-session-message post /api/messaging/sessions/{sessionId}/messages Send a message to a conversation session with automatic renewal tracking Sending a message automatically updates the session's last activity timestamp. If auto-renewal is enabled, the session will be renewed, extending its expiration time. ## Path Parameters The unique identifier of the session ## Request Body The message content (maximum 4000 characters) Optional metadata to attach to the message Optional array of attachments ## Response Unique identifier of the created message The message content UUID of the message author (user or agent) ISO timestamp of message creation Any metadata attached to the message Current session status after sending the message Updated expiration timestamp Total number of times the session has been renewed Whether the session was renewed by this message Whether the session is within the warning threshold ## Error Responses ```json // 404 - Session not found { "error": "Session not found", "details": { "sessionId": "abc-123" } } // 410 - Session expired { "error": "Session has expired", "details": { "sessionId": "abc-123", "expiresAt": "2024-01-15T10:30:00Z" } } // 400 - Invalid content { "error": "Content exceeds maximum length of 4000 characters" } ``` # Session Heartbeat Source: https://docs.elizaos.ai/api-reference/messaging/session-heartbeat post /api/messaging/sessions/{sessionId}/heartbeat Send a heartbeat to keep a session alive and optionally renew it # Sessions Health Check Source: https://docs.elizaos.ai/api-reference/messaging/sessions-health-check get /api/messaging/sessions/health Check the health status of the sessions service and get active session statistics This endpoint provides real-time health metrics for the Sessions API service, including active session counts and service uptime. ## Response Service health status: "healthy", "degraded", or "unhealthy" Number of currently active (non-expired) sessions ISO timestamp of the health check Number of sessions that are within their warning threshold Number of corrupted or invalid sessions detected (only shown if > 0) Service uptime in seconds ## Example Response ```json { "status": "healthy", "activeSessions": 42, "timestamp": "2024-01-15T10:30:45.123Z", "expiringSoon": 3, "uptime": 3600.5 } ``` ## Health Status Meanings * **healthy**: Service is operating normally * **degraded**: Service is operational but experiencing issues (e.g., invalid sessions detected) * **unhealthy**: Service is not operational or experiencing critical issues ## Usage This endpoint is useful for: * Monitoring service availability * Tracking session volume * Detecting memory leaks (via active session count) * Setting up health check probes in orchestration systems * Dashboard metrics and alerting # Submit a message to the central messaging system Source: https://docs.elizaos.ai/api-reference/messaging/submit-a-message-to-the-central-messaging-system post /api/messaging/submit Submit a message to the central messaging bus for agent processing. This is the primary endpoint for sending messages to agents, replacing the deprecated agent-specific message endpoints. The message is submitted to a central channel and the appropriate agent(s) will process it based on the channel and room configuration. This architecture allows for multi-agent conversations and better message routing. **Important**: Do not use `/api/agents/{agentId}/message` - that endpoint no longer exists. All messages should go through this central messaging system. # Update channel Source: https://docs.elizaos.ai/api-reference/messaging/update-channel patch /api/messaging/central-channels/{channelId} Update channel details # Update Session Timeout Source: https://docs.elizaos.ai/api-reference/messaging/update-session-timeout patch /api/messaging/sessions/{sessionId}/timeout Update the timeout configuration for an active session # Create a room Source: https://docs.elizaos.ai/api-reference/rooms/create-a-room post /api/agents/{agentId}/rooms Creates a new room for an agent # Delete a room Source: https://docs.elizaos.ai/api-reference/rooms/delete-a-room delete /api/agents/{agentId}/rooms/{roomId} Deletes a specific room # Get agent rooms Source: https://docs.elizaos.ai/api-reference/rooms/get-agent-rooms get /api/agents/{agentId}/rooms Retrieves all rooms for a specific agent # Get room details Source: https://docs.elizaos.ai/api-reference/rooms/get-room-details get /api/agents/{agentId}/rooms/{roomId} Retrieves details about a specific room # Update a room Source: https://docs.elizaos.ai/api-reference/rooms/update-a-room patch /api/agents/{agentId}/rooms/{roomId} Updates a specific room # Basic health check Source: https://docs.elizaos.ai/api-reference/system/basic-health-check get /api/server/hello Simple hello world test endpoint # Get local environment variables Source: https://docs.elizaos.ai/api-reference/system/get-local-environment-variables get /api/system/environment/local Retrieve local environment variables from .env file # Get server debug info Source: https://docs.elizaos.ai/api-reference/system/get-server-debug-info get /api/server/debug/servers Get debug information about active servers (debug endpoint) # Get server debug info Source: https://docs.elizaos.ai/api-reference/system/get-server-debug-info-1 get /api/server/servers Get debug information about active servers (debug endpoint) # Get system status Source: https://docs.elizaos.ai/api-reference/system/get-system-status get /api/server/status Returns the current status of the system with agent count and timestamp # Health check endpoint Source: https://docs.elizaos.ai/api-reference/system/health-check-endpoint get /api/server/health Detailed health check for the system # Ping health check Source: https://docs.elizaos.ai/api-reference/system/ping-health-check get /api/server/ping Simple ping endpoint to check if server is responsive # Stop the server Source: https://docs.elizaos.ai/api-reference/system/stop-the-server post /api/server/stop Initiates server shutdown # Update local environment variables Source: https://docs.elizaos.ai/api-reference/system/update-local-environment-variables post /api/system/environment/local Update local environment variables in .env file # Socket.IO Real-time Connection Source: https://docs.elizaos.ai/api-reference/websocket/socketio-real-time-connection get /websocket Socket.IO connection for real-time bidirectional communication. The server uses Socket.IO v4.x for WebSocket transport with automatic fallback. **Connection URL**: `ws://localhost:3000/socket.io/` (or `wss://` for secure connections) **Socket.IO Client Connection Example**: ```javascript import { io } from 'socket.io-client'; const socket = io('http://localhost:3000'); ``` **Events**: ### Client to Server Events: - `join` - Join a room/channel ```json { "roomId": "uuid", "agentId": "uuid" } ``` - `leave` - Leave a room/channel ```json { "roomId": "uuid", "agentId": "uuid" } ``` - `message` - Send a message ```json { "text": "string", "roomId": "uuid", "userId": "uuid", "name": "string" } ``` - `request-world-state` - Request current state ```json { "roomId": "uuid" } ``` ### Server to Client Events: - `messageBroadcast` - New message broadcast ```json { "senderId": "uuid", "senderName": "string", "text": "string", "roomId": "uuid", "serverId": "uuid", "createdAt": "timestamp", "source": "string", "id": "uuid", "thought": "string", "actions": ["string"], "attachments": [] } ``` - `messageComplete` - Message processing complete ```json { "channelId": "uuid", "serverId": "uuid" } ``` - `world-state` - World state update ```json { "agents": {}, "users": {}, "channels": {}, "messages": {} } ``` - `logEntry` - Real-time log entry ```json { "level": "number", "time": "timestamp", "msg": "string", "agentId": "uuid", "agentName": "string" } ``` - `error` - Error event ```json { "error": "string", "details": {} } ``` # Agent Command Source: https://docs.elizaos.ai/cli-reference/agent Managing elizaOS agents through the CLI - list, configure, start, stop, and update agents ## Usage ```bash elizaos agent [options] [command] ``` ## Subcommands | Subcommand | Aliases | Description | Required Options | Additional Options | | ---------------- | ------- | --------------------------------------- | -------------------------------------------------------------- | --------------------------------------------------------------------- | | `list` | `ls` | List available agents | | `-j, --json`, `-r, --remote-url `, `-p, --port ` | | `get` | `g` | Get agent details | `-n, --name ` | `-j, --json`, `-o, --output [file]`, `-r, --remote-url`, `-p, --port` | | `start` | `s` | Start an agent with a character profile | One of: `-n, --name`, `--path`, `--remote-character` | `-r, --remote-url `, `-p, --port ` | | `stop` | `st` | Stop an agent | `-n, --name ` | `-r, --remote-url `, `-p, --port ` | | `remove` | `rm` | Remove an agent | `-n, --name ` | `-r, --remote-url `, `-p, --port ` | | `set` | | Update agent configuration | `-n, --name ` AND one of: `-c, --config` OR `-f, --file` | `-r, --remote-url `, `-p, --port ` | | `clear-memories` | `clear` | Clear all memories for an agent | `-n, --name ` | `-r, --remote-url `, `-p, --port ` | ## Options Reference ### Common Options (All Subcommands) * `-r, --remote-url `: URL of the remote agent runtime * `-p, --port `: Port to listen on ### Output Options (for `list` and `get`) * `-j, --json`: Output as JSON format instead of the default table format. * `-o, --output [file]`: For the `get` command, saves the agent's configuration to a JSON file. If no filename is provided, defaults to `{name}.json`. ### Get Specific Options * `-n, --name `: Agent id, name, or index number from list (required) ### Start Specific Options * `-n, --name `: Name of an existing agent to start * `--path `: Path to local character JSON file * `--remote-character `: URL to remote character JSON file ### Stop/Remove Specific Options * `-n, --name `: Agent id, name, or index number from list (required) ### Set Specific Options * `-n, --name `: Agent id, name, or index number from list (required) * `-c, --config `: Agent configuration as JSON string * `-f, --file `: Path to agent configuration JSON file ### Clear Memories Specific Options * `-n, --name `: Agent id, name, or index number from list (required) ### Listing Agents ```bash # List all available agents elizaos agent list # Using alias elizaos agent ls # List agents in JSON format elizaos agent list --json # Or using the shorthand elizaos agent list -j # List agents from remote runtime elizaos agent list --remote-url http://server:3000 # List agents on specific port elizaos agent list --port 4000 ``` ### Getting Agent Details ```bash # Get agent details by name elizaos agent get --name eliza # Get agent by ID elizaos agent get --name agent_123456 # Get agent by index from list elizaos agent get --name 0 # Display configuration as JSON in console elizaos agent get --name eliza --json # Or using the shorthand elizaos agent get --name eliza -j # Save agent configuration to file elizaos agent get --name eliza --output # Save to specific file elizaos agent get --name eliza --output ./my-agent.json # Using alias elizaos agent g --name eliza ``` ### Starting Agents ```bash # Start existing agent by name elizaos agent start --name eliza # Start with local character file elizaos agent start --path ./characters/eliza.json # Start from remote character file elizaos agent start --remote-character https://example.com/characters/eliza.json # Using alias elizaos agent s --name eliza # Start on specific port elizaos agent start --path ./eliza.json --port 4000 ``` **Required Configuration:** You must provide one of these options: `--name`, `--path`, or `--remote-character` ### Stopping Agents ```bash # Stop agent by name elizaos agent stop --name eliza # Stop agent by ID elizaos agent stop --name agent_123456 # Stop agent by index elizaos agent stop --name 0 # Using alias elizaos agent st --name eliza # Stop agent on remote runtime elizaos agent stop --name eliza --remote-url http://server:3000 ``` ### Removing Agents ```bash # Remove agent by name elizaos agent remove --name pmairca # Remove agent by ID elizaos agent remove --name agent_123456 # Using alias elizaos agent rm --name pmairca # Remove from remote runtime elizaos agent remove --name pmairca --remote-url http://server:3000 ``` ### Updating Agent Configuration ```bash # Update with JSON string elizaos agent set --name eliza --config '{"system":"Updated prompt"}' # Update from configuration file elizaos agent set --name eliza --file ./updated-config.json # Update agent on remote runtime elizaos agent set --name pmairca --config '{"model":"gpt-4"}' --remote-url http://server:3000 # Update agent on specific port elizaos agent set --name eliza --file ./config.json --port 4000 ``` ### Clearing Agent Memories ```bash # Clear memories for agent by name elizaos agent clear-memories --name eliza # Clear memories by ID elizaos agent clear-memories --name agent_123456 # Using alias elizaos agent clear --name eliza # Clear memories on remote runtime elizaos agent clear-memories --name eliza --remote-url http://server:3000 ``` ## Output Formatting The `list` and `get` commands support different output formats, making it easy to use the CLI in scripts or for human readability. ### `table` (Default) The default format is a human-readable table, best for viewing in the terminal. ```bash $ elizaos agent list ┌─────────┬──────────────┬─────────┬──────────┐ │ (index) │ name │ id │ status │ ├─────────┼──────────────┼─────────┼──────────┤ │ 0 │ 'eliza' │ 'agent…'│ 'running'│ └─────────┴──────────────┴─────────┴──────────┘ ``` ### `json` Outputs raw JSON data. Useful for piping into other tools like `jq`. Use the `-j` or `--json` flag. ```bash # Get JSON output elizaos agent get --name eliza --json # Or using shorthand elizaos agent get --name eliza -j ``` ## Character File Structure When using `--path` or `--remote-character`, the character file should follow this structure: ```json { "name": "eliza", "system": "You are a friendly and knowledgeable AI assistant named Eliza.", "bio": ["Helpful and engaging conversationalist", "Knowledgeable about a wide range of topics"], "plugins": ["@elizaos/plugin-openai", "@elizaos/plugin-discord"], "settings": { "voice": { "model": "en_US-female-medium" } }, "knowledge": ["./knowledge/general-info.md", "./knowledge/conversation-patterns.md"] } ``` ## Agent Identification Agents can be identified using: 1. **Agent Name**: Human-readable name (e.g., "eliza", "pmairca") 2. **Agent ID**: System-generated ID (e.g., "agent\_123456") 3. **List Index**: Position in `elizaos agent list` output (e.g., "0", "1", "2") ## Interactive Mode All agent commands support interactive mode when run without required parameters: ```bash # Interactive agent selection elizaos agent get elizaos agent start elizaos agent stop elizaos agent remove elizaos agent set elizaos agent clear-memories ``` ## Remote Runtime Configuration By default, agent commands connect to `http://localhost:3000`. Override with: ### Environment Variable ```bash export AGENT_RUNTIME_URL=http://your-server:3000 elizaos agent list ``` ### Command Line Option ```bash elizaos agent list --remote-url http://your-server:3000 ``` ### Custom Port ```bash elizaos agent list --port 4000 ``` ## Agent Lifecycle Workflow ### 1. Create Agent Character ```bash # Create character file elizaos create -type agent eliza # Or create project with character elizaos create -type project my-project ``` ### 2. Start Agent Runtime ```bash # Start the agent runtime server elizaos start ``` ### 3. Manage Agents ```bash # List available agents elizaos agent list # Start an agent elizaos agent start --path ./eliza.json # Check agent status elizaos agent get --name eliza # Update configuration elizaos agent set --name eliza --config '{"system":"Updated prompt"}' # Stop agent elizaos agent stop --name eliza # Clear agent memories if needed elizaos agent clear-memories --name eliza # Remove when no longer needed elizaos agent remove --name eliza ``` ## Troubleshooting ### Connection Issues ```bash # Check if runtime is running elizaos agent list # If connection fails, start runtime first elizaos start # For custom URLs/ports elizaos agent list --remote-url http://your-server:3000 ``` ### Agent Not Found ```bash # List all agents to see available options elizaos agent list # Try using agent ID instead of name elizaos agent get --name agent_123456 # Try using list index elizaos agent get --name 0 ``` ### Configuration Errors * Validate JSON syntax in character files and config strings * Ensure all required fields are present in character definitions * Check file paths are correct and accessible ## Related Commands * [`create`](/cli-reference/create): Create a new agent character file * [`start`](/cli-reference/start): Start the agent runtime server * [`dev`](/cli-reference/dev): Run in development mode with hot-reload * [`env`](/cli-reference/env): Configure environment variables for agents # Create Command Source: https://docs.elizaos.ai/cli-reference/create Initialize a new project, plugin, or agent with an interactive setup process ## Usage ```bash # Interactive mode (recommended) elizaos create # With specific options elizaos create [options] [name] ``` ## Getting Help ```bash # View detailed help elizaos create --help ``` ## Options | Option | Description | | --------------- | ------------------------------------------------------------------------------------- | | `-y, --yes` | Skip confirmation and use defaults (default: `false`) | | `--type ` | Type of template to use (`project`, `plugin`, `agent`, or `tee`) (default: `project`) | | `[name]` | Name for the project, plugin, or agent (optional) | ## Interactive Process When you run `elizaos create` without options, it launches an interactive wizard: 1. **What would you like to name your project?** - Enter your project name 2. **Select your database:** - Choose between: * `pglite` (local, file-based database) * `postgres` (requires connection details) ## Default Values (with -y flag) When using the `-y` flag to skip prompts: * **Default name**: `myproject` * **Default type**: `project` * **Default database**: `pglite` ### Interactive Creation (Recommended) ```bash # Start interactive wizard elizaos create ``` This will prompt you for: * Project name * Database selection (pglite or postgres) ### Quick Creation with Defaults ```bash # Create project with defaults (name: "myproject", database: pglite) elizaos create -y ``` ### Specify Project Name ```bash # Create project with custom name, interactive database selection elizaos create my-awesome-project # Create project with custom name and skip prompts elizaos create my-awesome-project -y ``` ### Create Different Types ```bash # Create a plugin interactively elizaos create --type plugin # Create a plugin with defaults elizaos create --type plugin -y # Create an agent character file elizaos create --type agent my-character-name # Create a TEE (Trusted Execution Environment) project elizaos create --type tee my-tee-project ``` ### Advanced Creation ```bash # Create a project from a specific template elizaos create my-special-project --template minimal # Create a project without installing dependencies automatically elizaos create my-lean-project --no-install # Create a project without initializing a git repository elizaos create my-repo-less-project --no-git ``` ### Creating in a Specific Directory To create a project in a specific directory, navigate to that directory first: ```bash # Navigate to your desired directory cd ./my-projects elizaos create new-agent # For plugins cd ./plugins elizaos create -t plugin my-plugin ``` ## Project Types ### Project (Default) Creates a complete elizaOS project with: * Agent configuration and character files * Knowledge directory for RAG * Database setup (PGLite or Postgres) * Test structure * Build configuration **Default structure:** ``` myproject/ ├── src/ │ └── index.ts # Main character definition ├── knowledge/ # Knowledge files for RAG ├── __tests__/ # Component tests ├── e2e/ # End-to-end tests ├── .elizadb/ # PGLite database (if selected) ├── package.json └── tsconfig.json ``` ### Plugin Creates a plugin that extends elizaOS functionality: ```bash elizaos create -t plugin my-plugin ``` **Plugin structure:** ``` plugin-my-plugin/ # Note: "plugin-" prefix added automatically ├── src/ │ └── index.ts # Plugin implementation ├── images/ # Logo and banner for registry ├── package.json └── tsconfig.json ``` ### Agent Creates a standalone agent character definition file: ```bash elizaos create -t agent my-character ``` This creates a single `.json` file with character configuration. ### TEE (Trusted Execution Environment) Creates a project with TEE capabilities for secure, decentralized agent deployment: ```bash elizaos create -t tee my-tee-project ``` **TEE project structure:** ``` my-tee-project/ ├── src/ │ └── index.ts # Main character definition ├── knowledge/ # Knowledge files for RAG ├── docker-compose.yml # Docker configuration for TEE deployment ├── Dockerfile # Container definition ├── __tests__/ # Component tests ├── e2e/ # End-to-end tests ├── .elizadb/ # PGLite database (if selected) ├── package.json └── tsconfig.json ``` ## After Creation The CLI will automatically: 1. **Install dependencies** using bun 2. **Build the project** (for projects and plugins) 3. **Show next steps**: ```bash cd myproject elizaos start # Visit http://localhost:3000 ``` ## Database Selection ### PGLite (Recommended for beginners) * Local file-based database * No setup required * Data stored in `.elizadb/` directory ### Postgres * Requires existing Postgres database * Prompts for connection details during setup * Better for production deployments ## Troubleshooting ### Creation Failures ```bash # Check if you can write to the target directory touch test-file && rm test-file # If permission denied, change ownership or use different directory elizaos create -d ~/my-projects/new-project ``` ### Dependency Installation Issues ```bash # If bun install fails, try manual installation cd myproject bun install # For network issues, clear cache and retry bun pm cache rm bun install ``` ### Bun Installation Issues ```bash # If you see "bun: command not found" errors # Install Bun using the appropriate command for your system: # Linux/macOS: curl -fsSL https://bun.sh/install | bash # Windows: powershell -c "irm bun.sh/install.ps1 | iex" # macOS with Homebrew: brew install bun # After installation, restart your terminal or: source ~/.bashrc # Linux source ~/.zshrc # macOS with zsh # Verify installation: bun --version ``` ### Database Connection Problems **PGLite Issues:** * Ensure sufficient disk space in target directory * Check write permissions for `.elizadb/` directory **Postgres Issues:** * Verify database server is running * Test connection with provided credentials * Ensure database exists and user has proper permissions ### Build Failures ```bash # Check for TypeScript errors bun run build # If build fails, check dependencies bun install bun run build ``` ### Template Not Found ```bash # Verify template type is correct elizaos create -t project # Valid: project, plugin, agent elizaos create -t invalid # Invalid template type ``` ## Related Commands * [`start`](/cli-reference/start): Start your created project * [`dev`](/cli-reference/dev): Run your project in development mode * [`env`](/cli-reference/env): Configure environment variables # Development Mode Source: https://docs.elizaos.ai/cli-reference/dev Run elizaOS projects in development mode with hot reloading and debugging ## Usage ```bash elizaos dev [options] ``` ## Options | Option | Description | | ------------------------ | -------------------------------------------------------------------- | | `-c, --configure` | Reconfigure services and AI models (skips using saved configuration) | | `--character [paths...]` | Character file(s) to use - accepts paths or URLs | | `-b, --build` | Build the project before starting | | `-p, --port ` | Port to listen on (default: 3000) | | `-h, --help` | Display help for command | ### Basic Development Mode ```bash # Navigate to your project directory cd my-agent-project # Start development mode elizaos dev ``` ### Development with Configuration ```bash # Start dev mode with custom port elizaos dev --port 8080 # Force reconfiguration of services elizaos dev --configure # Build before starting development elizaos dev --build ``` ### Character File Specification ```bash # Single character file elizaos dev --character assistant.json # Multiple character files (space-separated) elizaos dev --character assistant.json chatbot.json # Multiple character files (comma-separated) elizaos dev --character "assistant.json,chatbot.json" # Character file without extension (auto-adds .json) elizaos dev --character assistant # Load character from URL elizaos dev --character https://example.com/characters/assistant.json ``` ### Combined Options ```bash # Full development setup elizaos dev --port 4000 --character "assistant.json,chatbot.json" --build --configure ``` ## Development Features The dev command provides comprehensive development capabilities: ### Auto-Rebuild and Restart * **File Watching**: Monitors `.ts`, `.js`, `.tsx`, and `.jsx` files for changes * **Automatic Rebuilding**: Rebuilds project when source files change * **Server Restart**: Automatically restarts the server after successful rebuilds * **TypeScript Support**: Compiles TypeScript files during rebuilds ### Project Detection * **Project Mode**: Automatically detects elizaOS projects based on package.json configuration * **Plugin Mode**: Detects and handles plugin development appropriately * **Monorepo Support**: Builds core packages when working in monorepo context ### Development Workflow 1. Detects whether you're in a project or plugin directory 2. Performs initial build (if needed) 3. Starts the server with specified options 4. Sets up file watching for source files 5. Rebuilds and restarts when files change ## File Watching Behavior ### Watched Files * TypeScript files (`.ts`, `.tsx`) * JavaScript files (`.js`, `.jsx`) ### Watched Directories * Source directory (`src/`) * Project root (if no src directory exists) ### Ignored Paths * `node_modules/` directory * `dist/` directory * `.git/` directory ### Debouncing * Changes are debounced with a 300ms delay to prevent rapid rebuilds * Multiple rapid changes trigger only one rebuild cycle ## Project Type Detection The dev command uses intelligent project detection: ### Plugin Detection Identifies plugins by checking for: * `eliza.type: "plugin"` in package.json * Package name containing `plugin-` * Keywords: `elizaos-plugin` or `eliza-plugin` ### Project Detection Identifies projects by checking for: * `eliza.type: "project"` in package.json * Package name containing `project-` or `-org` * Keywords: `elizaos-project` or `eliza-project` * `src/index.ts` with Project export ## Monorepo Support When running in a monorepo context, the dev command: 1. **Builds Core Packages**: Automatically builds essential monorepo packages: * `packages/core` * `packages/client` * `packages/plugin-bootstrap` 2. **Dependency Resolution**: Ensures proper build order for dependencies 3. **Change Detection**: Monitors both core packages and current project for changes ## Development Logs The dev command provides detailed logging: ```bash # Project detection [info] Running in project mode [info] Package name: my-agent-project # Build process [info] Building project... [success] Build successful # Server management [info] Starting server... [info] Stopping current server process... # File watching [info] Setting up file watching for directory: /path/to/project [success] File watching initialized in: /path/to/project/src [info] Found 15 TypeScript/JavaScript files in the watched directory # Change detection [info] File event: change - src/index.ts [info] Triggering rebuild for file change: src/index.ts [info] Rebuilding project after file change... [success] Rebuild successful, restarting server... ``` ## Character File Handling ### Supported Formats * **Local files**: Relative or absolute paths * **URLs**: HTTP/HTTPS URLs to character files * **Extension optional**: `.json` extension is automatically added if missing ### Multiple Characters Multiple character files can be specified using: * Space separation: `file1.json file2.json` * Comma separation: `"file1.json,file2.json"` * Mixed format: `"file1.json, file2.json"` ## Troubleshooting ### Build Failures ```bash # If initial build fails [error] Initial build failed: Error message [info] Continuing with dev mode anyway... # Check for TypeScript errors bun i && bun run build # Try dev mode with explicit build elizaos dev --build ``` ### Bun Installation Issues ```bash # If you see "bun: command not found" errors # Install Bun using the appropriate command for your system: # Linux/macOS: curl -fsSL https://bun.sh/install | bash # Windows: powershell -c "irm bun.sh/install.ps1 | iex" # macOS with Homebrew: brew install bun # After installation, restart your terminal or: source ~/.bashrc # Linux source ~/.zshrc # macOS with zsh # Verify installation: bun --version ``` ### File Watching Issues ```bash # If file changes aren't detected [warn] No directories are being watched! File watching may not be working. # Check if you're in the right directory pwd ls src/ # Verify file types being modified (.ts, .js, .tsx, .jsx) ``` ### Server Restart Problems ```bash # If server doesn't restart after changes [warn] Failed to kill server process, trying force kill... # Manual restart # Press Ctrl+C to stop, then restart: elizaos dev ``` ### Port Conflicts ```bash # If default port is in use [error] Port 3000 already in use # Use different port elizaos dev --port 8080 ``` ### Configuration Issues ```bash # If having configuration problems elizaos dev --configure # Check environment setup elizaos env list ``` ## Related Commands * [`start`](/cli-reference/start): Start your project in production mode * [`test`](/cli-reference/test): Run tests for your project * [`env`](/cli-reference/env): Configure environment variables for development * [`create`](/cli-reference/create): Create new projects with development structure # Environment Configuration Source: https://docs.elizaos.ai/cli-reference/env Configure environment variables and API keys for elizaOS projects ## Usage ```bash elizaos env [command] [options] ``` ## Subcommands | Subcommand | Description | Options | | ------------- | ------------------------------------------------------------------------------------- | --------------------- | | `list` | List all environment variables | `--system`, `--local` | | `edit-local` | Edit local environment variables | `-y, --yes` | | `reset` | Reset environment variables and clean up database/cache files (interactive selection) | `-y, --yes` | | `interactive` | Interactive environment variable management | `-y, --yes` | ## Options ### List Command Options | Option | Description | | ---------- | ------------------------------------- | | `--system` | List only system information | | `--local` | List only local environment variables | ### General Options | Option | Description | | ----------- | ----------------------------- | | `-y, --yes` | Automatically confirm prompts | ### Viewing Environment Variables ```bash # List all variables (system info + local .env) elizaos env list # Show only system information elizaos env list --system # Show only local environment variables elizaos env list --local ``` ### Managing Local Environment Variables ```bash # Edit local environment variables interactively elizaos env edit-local # Display variables and exit (--yes flag skips interactive editing) elizaos env edit-local --yes ``` ### Interactive Management ```bash # Start interactive environment manager elizaos env interactive ``` ### Resetting Environment and Data ```bash # Interactive reset with item selection elizaos env reset # Automatic reset with default selections elizaos env reset --yes ``` ### Example `list` output: ``` System Information: Platform: darwin (24.3.0) Architecture: arm64 CLI Version: 1.0.0 Package Manager: bun v1.2.5 Local Environment Variables: Path: /current/directory/.env OPENAI_API_KEY: your-key...5678 MODEL_PROVIDER: openai PORT: 8080 LOG_LEVEL: debug ``` ### `edit-local` Details The `edit-local` command allows you to: * View existing local variables * Add new variables * Edit existing variables * Delete variables **Note**: The `--yes` flag displays current variables and exits without interactive editing, since variable modification requires user input. ### `interactive` Details Interactive mode provides a menu with options to: * List environment variables * Edit local environment variables * Reset environment variables **Note**: The `--yes` flag is ignored in interactive mode since it requires user input by design. ### `reset` Details The reset command allows you to selectively reset: * **Local environment variables** - Clears values in local `.env` file while preserving keys * **Cache folder** - Deletes the cache folder (`~/.eliza/cache`) * **Local database files** - Deletes local database files (PGLite data directory) ## Environment File Structure elizaOS uses local environment variables stored in `.env` files in your project directory: * **Local variables** - Stored in `./.env` in your current project directory ### Missing .env File Handling If no local `.env` file exists: * Commands will detect this and offer to create one * The `list` command will show helpful guidance * The `edit-local` command will prompt to create a new file ## Common Environment Variables | Variable | Description | | -------------------- | -------------------------------------------- | | `OPENAI_API_KEY` | OpenAI API key for model access | | `ANTHROPIC_API_KEY` | Anthropic API key for Claude models | | `TELEGRAM_BOT_TOKEN` | Token for Telegram bot integration | | `DISCORD_BOT_TOKEN` | Token for Discord bot integration | | `POSTGRES_URL` | PostgreSQL database connection string | | `PGLITE_DATA_DIR` | Directory for PGLite database files | | `MODEL_PROVIDER` | Default model provider to use | | `LOG_LEVEL` | Logging verbosity (debug, info, warn, error) | | `LOG_TIMESTAMPS` | Show timestamps in logs (default: true) | | `PORT` | HTTP API port number | ## Database Configuration Detection The reset command intelligently detects your database configuration: * **External PostgreSQL** - Warns that only local files will be removed * **PGLite** - Ensures the correct local database directories are removed * **Missing configuration** - Skips database-related reset operations ## Security Features * **Value masking** - Sensitive values (API keys, tokens) are automatically masked in output * **Local-only storage** - Environment variables are stored locally in your project * **No global secrets** - Prevents accidental exposure across projects ## Troubleshooting ### Missing .env File ```bash # Check if .env file exists ls -la .env # Create .env file from example cp .env.example .env # Edit the new file elizaos env edit-local ``` ### Permission Issues ```bash # Check file permissions ls -la .env # Fix permissions if needed chmod 600 .env ``` ### Database Reset Issues ```bash # Check what exists before reset elizaos env list # Reset only specific items elizaos env reset # Force reset with defaults elizaos env reset --yes ``` ### Environment Not Loading ```bash # Verify environment file exists and has content cat .env # Check for syntax errors in .env file elizaos env list --local ``` ## Related Commands * [`start`](/cli-reference/start): Start your project with the configured environment * [`dev`](/cli-reference/dev): Run in development mode with the configured environment * [`test`](/cli-reference/test): Run tests with environment configuration * [`create`](/cli-reference/create): Create a new project with initial environment setup # Monorepo Command Source: https://docs.elizaos.ai/cli-reference/monorepo Clone the elizaOS monorepo for development or contribution ## Usage ```bash elizaos monorepo [options] ``` ## Options | Option | Description | Default | | ----------------------- | --------------------- | --------- | | `-b, --branch ` | Branch to clone | `develop` | | `-d, --dir ` | Destination directory | `./eliza` | ## How It Works 1. **Checks Destination**: Verifies the target directory is empty or doesn't exist 2. **Clones Repository**: Downloads the `elizaOS/eliza` repository from GitHub 3. **Shows Next Steps**: Displays instructions for getting started ## Examples ### Basic Usage ```bash # Clone default branch (develop) to default directory (./eliza) elizaos monorepo # Clone with verbose output elizaos monorepo --dir ./eliza --branch develop ``` ### Custom Branch ```bash # Clone main branch elizaos monorepo --branch main # Clone feature branch for testing elizaos monorepo --branch feature/new-api # Clone release branch elizaos monorepo --branch v2.1.0 ``` ### Custom Directory ```bash # Clone to custom directory elizaos monorepo --dir my-eliza-dev # Clone to current directory (must be empty) elizaos monorepo --dir . # Clone to nested path elizaos monorepo --dir ./projects/eliza-fork ``` ### Development Workflows ```bash # For contribution development elizaos monorepo --branch main --dir ./eliza-contrib # For stable development elizaos monorepo --branch main --dir ./eliza-stable # For testing specific features elizaos monorepo --branch feature/new-plugin-system ``` ## After Setup Once cloned, follow these steps: ```bash cd eliza # Navigate to the cloned directory bun i && bun run build # Install dependencies and build ``` ### Development Commands ```bash # Start development server bun run dev # Run tests bun test # Build all packages bun run build # Start a specific package cd packages/client-web bun dev ``` ## Monorepo Structure The cloned repository includes: ``` eliza/ ├── packages/ │ ├── core/ # Core elizaOS functionality │ ├── client-web/ # Web interface │ ├── client-discord/ # Discord client │ ├── plugin-*/ # Various plugins │ └── cli/ # CLI tool source ├── docs/ # Documentation ├── examples/ # Example projects └── scripts/ # Build and utility scripts ``` ## Use Cases ### Contributors Perfect for developers wanting to: * Submit pull requests * Develop new plugins * Fix bugs or add features * Understand the codebase ### Advanced Users Useful for users who need: * Custom builds * Experimental features * Local plugin development * Integration testing ### Plugin Developers Essential for: * Plugin development and testing * Understanding plugin APIs * Contributing to core functionality ## Troubleshooting ### Clone Failures ```bash # If git clone fails, check network connection git --version ping github.com # For authentication issues git config --global credential.helper store ``` ### Directory Issues ```bash # If directory is not empty ls -la ./eliza # Check contents rm -rf ./eliza # Remove if safe elizaos monorepo # Retry # For permission issues sudo chown -R $USER:$USER ./eliza ``` ### Build Failures ```bash # If dependencies fail to install cd eliza rm -rf node_modules bun install # If build fails bun run clean bun install bun run build ``` ### Branch Not Found ```bash # List available branches git ls-remote --heads https://github.com/elizaOS/eliza # Use correct branch name elizaos monorepo --branch main ``` ## Notes * The destination directory must be empty or non-existent * Uses the official `elizaOS/eliza` repository from GitHub * Requires Git to be installed on your system * Internet connection required for cloning ## Related Commands * [`create`](/cli-reference/create): Create a new project or plugin from templates * [`plugins`](/cli-reference/plugins): Manage plugins in your project * [`dev`](/cli-reference/dev): Run development server for your projects # elizaOS CLI Overview Source: https://docs.elizaos.ai/cli-reference/overview Comprehensive guide to the elizaOS Command Line Interface (CLI) tools and commands ## Installation Install the elizaOS CLI globally using Bun: ```bash bun install -g @elizaos/cli ``` **Video Tutorial**: [**Full CLI Reference**](https://www.youtube.com/watch?v=agI0yOPWBwk\&list=PLrjBjP4nU8ehOgKAa0-XddHzE0KK0nNvS\&index=8) ## Available Commands | Command | Description | | ------------------------------------- | -------------------------------------------------------------------------------------------------------------- | | [`create`](/cli-reference/create) | Initialize a new project, plugin, or agent | | [`monorepo`](/cli-reference/monorepo) | Clone elizaOS monorepo from a specific branch (defaults to develop) | | [`plugins`](/cli-reference/plugins) | Manage elizaOS plugins | | [`agent`](/cli-reference/agent) | Manage elizaOS agents | | [`tee`](/cli-reference/tee) | Manage TEE deployments | | [`start`](/cli-reference/start) | Start the Eliza agent with configurable plugins and services | | [`update`](/cli-reference/update) | Update elizaOS CLI and project dependencies | | [`test`](/cli-reference/test) | Run tests for Eliza agent projects and plugins | | [`env`](/cli-reference/env) | Manage environment variables and secrets | | [`dev`](/cli-reference/dev) | Start the project or plugin in development mode with auto-rebuild, detailed logging, and file change detection | | [`publish`](/cli-reference/publish) | Publish a plugin to the registry | ## Global Options These options apply to all commands: | Option | Description | | ------------------- | ------------------------------------------------------------------ | | `--help`, `-h` | Display help information | | `--version`, `-v` | Display version information | | `--no-emoji` | Disables emoji characters in the output | | `--no-auto-install` | Disables the automatic prompt to install Bun if it is not detected | ## Examples ### Getting Version Information ```bash # Check your CLI version elizaos --version # Get help for the 'agent' command elizaos agent --help # Get help for the 'agent start' subcommand elizaos agent start --help ``` ## Project Structure For detailed information about project and plugin structure, see the [Quickstart Guide](/quickstart). ## Environment Configuration Configure your API keys and environment variables with the `env` command: ```bash # Edit local environment variables interactively elizaos env edit-local # List all environment variables elizaos env list # Interactive environment manager elizaos env interactive ``` ## Development vs Production elizaOS supports two main modes of operation: Hot reloading, detailed error messages, and file watching for rapid development. Optimized performance and production-ready configuration for deployment. ## Quick Start For a complete guide to getting started with elizaOS, see the [Quickstart Guide](/quickstart). ### Creating a new project ```bash # Create a new project using the interactive wizard elizaos create # Or specify a name directly elizaos create my-agent-project ``` ### Starting a project ```bash # Navigate to your project directory cd my-agent-project # Start the project elizaos start ``` ### Development mode ```bash # Run in development mode with hot reloading elizaos dev ``` ## Working with Projects elizaOS organizes work into projects, which can contain one or more agents along with their configurations, knowledge files, and dependencies. The CLI provides commands to manage the entire lifecycle of a project: 1. **Create** a new project with `create` 2. **Configure** settings with `env` 3. **Develop** using `dev` for hot reloading 4. **Test** functionality with `test` 5. **Start** in production with `start` 6. **Share** by publishing with `publish` ## Working with Plugins Plugins extend the functionality of your agents. Use the `plugins` command for managing plugins and `publish` for publishing your own: ```bash # List available plugins elizaos plugins list # Add a plugin to your project elizaos plugins add @elizaos/plugin-discord # Publish your plugin (from plugin directory) elizaos publish # Test publishing without making changes elizaos publish --test ``` ## Related Documentation Complete workflow guide to get started with elizaOS Managing environment variables and configuration # Plugin Management Source: https://docs.elizaos.ai/cli-reference/plugins Manage elizaOS plugins within a project - list, add, remove ## Usage ```bash elizaos plugins [options] [command] ``` ## Subcommands | Subcommand | Aliases | Description | Arguments | Options | | ------------------- | --------------------- | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------- | | `list` | `l`, `ls` | List available plugins to install into the project (shows v1.x plugins by default) | | `--all` (detailed version info), `--v0` (v0.x compatible only) | | `add` | `install` | Add a plugin to the project | `` (plugin name e.g., "abc", "plugin-abc", "elizaos/plugin-abc") | `-s, --skip-env-prompt`, `--skip-verification`, `-b, --branch`, `-T, --tag` | | `installed-plugins` | | List plugins found in the project dependencies | | | | `remove` | `delete`, `del`, `rm` | Remove a plugin from the project | `` (plugin name e.g., "abc", "plugin-abc", "elizaos/plugin-abc") | | | `upgrade` | | Upgrade a plugin from version 0.x to 1.x using AI-powered migration | `` (GitHub repository URL or local folder path) | `--api-key`, `--skip-tests`, `--skip-validation`, `--quiet`, `--verbose`, `--debug`, `--skip-confirmation` | | `generate` | | Generate a new plugin using AI-powered code generation | | `--api-key`, `--skip-tests`, `--skip-validation`, `--skip-prompts`, `--spec-file` | ### Listing Available Plugins ```bash # List available v1.x plugins (default behavior) elizaos plugins list # Using alias elizaos plugins l # List all plugins with detailed version information elizaos plugins list --all # List only v0.x compatible plugins elizaos plugins list --v0 ``` ### Adding Plugins ```bash # Add a plugin by short name (looks up '@elizaos/plugin-openai') elizaos plugins add openai # Add a plugin by full package name elizaos plugins add @elizaos/plugin-anthropic # Add plugin and skip environment variable prompts elizaos plugins add google-ai --skip-env-prompt # Skip plugin verification after installation elizaos plugins add discord --skip-verification # Add plugin from specific branch (for monorepo development) elizaos plugins add custom-plugin --branch feature/new-api # Add a specific version/tag of a plugin from npm elizaos plugins add elevenlabs --tag latest # Install plugin directly from GitHub (HTTPS URL) elizaos plugins add https://github.com/owner/my-plugin # Install from GitHub with branch reference elizaos plugins add https://github.com/owner/my-plugin/tree/feature-branch # Install using GitHub shorthand syntax elizaos plugins add github:owner/my-plugin # Install specific branch using GitHub shorthand elizaos plugins add github:owner/my-plugin#feature-branch # Using alias elizaos plugins install openai ``` After installing plugins via CLI, you **must** add them to your character file (`.json` or `.ts`) to activate them. Installing only adds the package to your project dependencies. #### Activating Plugins ```json character.json { "name": "MyAgent", "plugins": [ "@elizaos/plugin-sql", "@elizaos/plugin-openai", "@elizaos/plugin-discord" ], "bio": ["Your agent's description"], "style": { "all": ["conversational", "friendly"] } } ``` ```typescript character.ts import { Character } from '@elizaos/core'; export const character: Character = { name: "MyAgent", plugins: [ // Core plugins "@elizaos/plugin-sql", // Conditional plugins based on environment variables ...(process.env.OPENAI_API_KEY ? ["@elizaos/plugin-openai"] : []), ...(process.env.DISCORD_API_TOKEN ? ["@elizaos/plugin-discord"] : []), ...(process.env.ANTHROPIC_API_KEY ? ["@elizaos/plugin-anthropic"] : []) ], bio: ["Your agent's description"], style: { all: ["conversational", "friendly"] } }; ``` The SQL plugin (`@elizaos/plugin-sql`) is typically included by default as it provides core database functionality. Other plugins can be loaded conditionally based on environment variables to avoid loading unnecessary dependencies. ### Listing Installed Plugins ```bash # Show plugins currently in your project's package.json elizaos plugins installed-plugins ``` ### Removing Plugins ```bash # Remove plugin by short name elizaos plugins remove openai # Remove plugin by full package name elizaos plugins remove @elizaos/plugin-anthropic # Using aliases elizaos plugins delete openai elizaos plugins del twitter elizaos plugins rm discord ``` ### Upgrading Plugins (AI-Powered) ```bash # Upgrade a plugin from v0.x to v1.x using AI migration elizaos plugins upgrade https://github.com/user/plugin-v0 # Upgrade from local folder elizaos plugins upgrade ./path/to/old-plugin # Provide API key directly elizaos plugins upgrade ./my-plugin --api-key your-api-key # Skip test validation elizaos plugins upgrade ./my-plugin --skip-tests # Skip production readiness validation elizaos plugins upgrade ./my-plugin --skip-validation # Run upgrade with all skips (faster but less safe) elizaos plugins upgrade ./my-plugin --skip-tests --skip-validation # Run upgrade in quiet mode (minimal output) elizaos plugins upgrade ./my-plugin --quiet # Run upgrade with verbose output for debugging elizaos plugins upgrade ./my-plugin --verbose # Run upgrade with debug information elizaos plugins upgrade ./my-plugin --debug # Skip confirmation prompts (useful for automation) elizaos plugins upgrade ./my-plugin --skip-confirmation ``` ### Generating New Plugins (AI-Powered) ```bash # Generate a new plugin interactively elizaos plugins generate # Generate with API key directly elizaos plugins generate --api-key your-api-key # Generate from specification file (non-interactive) elizaos plugins generate --spec-file ./plugin-spec.json --skip-prompts # Skip test validation during generation elizaos plugins generate --skip-tests # Skip production readiness validation elizaos plugins generate --skip-validation ``` ## Plugin Installation Formats The `add` command supports multiple plugin formats: ### Package Names ```bash # Short name (auto-resolves to @elizaos/plugin-*) elizaos plugins add openai # Full package name elizaos plugins add @elizaos/plugin-openai # Scoped packages elizaos plugins add @company/plugin-custom ``` ### GitHub Integration ```bash # HTTPS URL elizaos plugins add https://github.com/user/my-plugin # GitHub shorthand elizaos plugins add github:user/my-plugin # With branch/tag elizaos plugins add github:user/my-plugin#feature-branch ``` ### Version Control ```bash # Specific npm tag elizaos plugins add plugin-name --tag beta # Development branch (for monorepo) elizaos plugins add plugin-name --branch main ``` ## Plugin Development Workflow ### 1. Create a Plugin ```bash elizaos create -t plugin my-awesome-plugin cd plugin-my-awesome-plugin ``` ### 2. Install in Your Project ```bash # During development, install from local directory elizaos plugins add ./path/to/plugin-my-awesome-plugin # Or install from your development branch elizaos plugins add my-awesome-plugin --branch feature/new-feature ``` ### 3. Test Your Plugin ```bash # Start development mode elizaos dev # Run tests elizaos test ``` ### 4. Publish Your Plugin For detailed instructions on authentication, plugin requirements, and the full publishing process, see the [**`publish` command documentation**](/cli-reference/publish). ```bash # Test the publishing process before committing elizaos publish --test # Publish to the registry elizaos publish ``` ## AI-Powered Plugin Development elizaOS includes AI-powered features to help with plugin development: ### Plugin Generation The `generate` command uses AI to create a new plugin based on your specifications: 1. **Interactive Mode**: Guides you through plugin requirements 2. **Code Generation**: Creates complete plugin structure with actions, providers, and tests 3. **Validation**: Ensures generated code follows elizaOS best practices ### Plugin Migration The `upgrade` command helps migrate v0.x plugins to v1.x format: 1. **Automated Analysis**: Analyzes existing plugin structure 2. **Code Transformation**: Updates APIs, imports, and patterns 3. **Test Migration**: Converts tests to new format 4. **Validation**: Ensures migrated plugin works correctly ### Requirements Both AI features require an Anthropic API key: * Set via environment: `export ANTHROPIC_API_KEY=your-api-key` * Or pass directly: `--api-key your-api-key` ## Troubleshooting ### Plugin Installation Failures ```bash # Clear cache and retry rm -rf ~/.eliza/cache elizaos plugins add plugin-name ``` ### Bun Installation Issues ```bash # If you see "bun: command not found" errors # Install Bun using the appropriate command for your system: # Linux/macOS: curl -fsSL https://bun.sh/install | bash # Windows: powershell -c "irm bun.sh/install.ps1 | iex" # macOS with Homebrew: brew install bun # After installation, restart your terminal or: source ~/.bashrc # Linux source ~/.zshrc # macOS with zsh # Verify installation: bun --version ``` ### Network Issues ```bash # For GitHub authentication problems git config --global credential.helper store # For registry issues bun config set registry https://registry.npmjs.org/ elizaos plugins add plugin-name ``` ### Plugin Not Found ```bash # Check exact plugin name in registry elizaos plugins list # Try different naming formats elizaos plugins add openai # Short name elizaos plugins add @elizaos/plugin-openai # Full package name elizaos plugins add plugin-openai # With plugin prefix ``` ### Dependency Conflicts ```bash # If dependency installation fails cd your-project bun install # Check for conflicting dependencies bun pm ls # Force reinstall rm -rf node_modules bun install ``` ### Environment Variable Issues ```bash # If plugin prompts for missing environment variables elizaos env set OPENAI_API_KEY your-key # Skip environment prompts during installation elizaos plugins add plugin-name --skip-env-prompt ``` ### Branch/Tag Issues ```bash # If branch doesn't exist git ls-remote --heads https://github.com/user/repo # If tag doesn't exist git ls-remote --tags https://github.com/user/repo # Use correct branch/tag name elizaos plugins add plugin-name --branch main elizaos plugins add plugin-name --tag v1.0.0 ``` ### AI Feature Issues ```bash # Missing API key error export ANTHROPIC_API_KEY=your-anthropic-key-here # Or pass directly to command elizaos plugins generate --api-key your-anthropic-key-here # Invalid specification file # Ensure spec file is valid JSON cat plugin-spec.json | jq . # Generation/Upgrade timeout # Skip validation for faster iteration elizaos plugins generate --skip-tests --skip-validation # Out of memory during AI operations # Increase Node.js memory limit NODE_OPTIONS="--max-old-space-size=8192" elizaos plugins upgrade ./my-plugin ``` ## Related Commands * [`create`](/cli-reference/create): Create a new project or plugin * [`env`](/cli-reference/env): Manage environment variables needed by plugins * [`publish`](/cli-reference/publish): Publish your plugin to the registry # Publish Command Source: https://docs.elizaos.ai/cli-reference/publish Publish a plugin to npm, create a GitHub repository, and submit to the elizaOS registry The `elizaos publish` command is the all-in-one tool for releasing your plugin. It handles packaging, publishing to npm, creating a source repository, and submitting your plugin to the official elizaOS registry for discovery. ## What It Does The `publish` command automates the entire release process: * **Validates Your Plugin:** Checks your `package.json` and directory structure against registry requirements * **Publishes Your Package:** Pushes your plugin to npm * **Creates GitHub Repository:** Initializes a public GitHub repository for your plugin's source code * **Submits to Registry:** Opens a Pull Request to the official [elizaOS Plugin Registry](https://github.com/elizaos-plugins/registry) ## Usage ```bash elizaos publish [options] ``` ## Options | Option | Description | | ----------------- | -------------------------------------------------- | | `--npm` | Publish to npm only (skip GitHub and registry) | | `-t, --test` | Test publish process without making changes | | `-d, --dry-run` | Generate registry files locally without publishing | | `--skip-registry` | Skip publishing to the registry | ## Standard Publishing This is the most common workflow. It publishes your package to npm, creates a GitHub repository, and opens a PR to the registry. ```bash # Navigate to your plugin's root directory cd my-awesome-plugin # Publish to npm and the registry elizaos publish ``` ## Testing and Dry Runs Use these options to validate your plugin before a real publish. ```bash # Simulate the entire publish process without making changes # Great for checking authentication and validation rules elizaos publish --test # Generate registry submission files locally for inspection elizaos publish --dry-run ``` ## Advanced Publishing Use these for specific scenarios, like managing a private plugin or handling the registry submission manually. ```bash # Publish to npm but do not open a PR to the registry elizaos publish --skip-registry # Test npm-only publishing (skip GitHub and registry) elizaos publish --test --npm ``` ## Development Lifecycle A typical journey from creation to publishing: ### 1. Create & Develop ```bash # Create a new plugin from the template elizaos create -t plugin my-awesome-plugin cd my-awesome-plugin # Install dependencies and start development bun install elizaos dev ``` ### 2. Test & Validate ```bash # Run your plugin's tests elizaos test # Simulate publish to catch issues early elizaos publish --test ``` ### 3. Publish ```bash # Ensure you're logged into npm bunx npm login # Publish your plugin elizaos publish ``` ## Process Steps When you run `elizaos publish`, the CLI performs these actions: 1. **Validation:** Checks CLI version, plugin structure, and `package.json` 2. **Authentication:** Prompts for npm and GitHub credentials if needed 3. **Build:** Compiles TypeScript by running `bun run build` 4. **Publish Package:** Pushes to npm 5. **Create GitHub Repo:** Creates public repository (if it doesn't exist) 6. **Submit to Registry:** Opens a Pull Request for discovery ## Post-Publishing Updates The `elizaos publish` command is for **initial release only**. Use standard tools for updates. For subsequent updates: ```bash # Bump version in package.json bun version patch # or minor/major # Push new version to npm bun publish # Push code and tags to GitHub git push && git push --tags ``` The elizaOS registry automatically detects new npm versions. ## Authentication ### npm Authentication You must be logged in to npm: ```bash bunx npm login ``` ### GitHub Authentication A Personal Access Token (PAT) is required. You can either: 1. Set environment variable: `export GITHUB_TOKEN=your_pat_here` 2. Enter when prompted by the CLI Required PAT scopes: `repo`, `read:org`, `workflow` ## Plugin Structure The CLI validates these requirements before publishing: | Requirement | Description | Fix | | -------------------- | ----------------------------------------- | ------------ | | **Plugin Name** | Must start with `plugin-` | Auto-checked | | **Images Directory** | Must have `images/` directory | Auto-created | | **Logo Image** | `images/logo.jpg` (400x400px, max 500KB) | Manual | | **Banner Image** | `images/banner.jpg` (1280x640px, max 1MB) | Manual | | **Description** | Meaningful description | Prompted | | **Repository URL** | Format: `github:username/repo` | Auto-fixed | | **agentConfig** | Required in package.json | Auto-fixed | ## Sample package.json ```json { "name": "plugin-example", "version": "1.0.0", "description": "An example elizaOS plugin that demonstrates best practices", "main": "dist/index.js", "types": "dist/index.d.ts", "author": "Your Name ", "license": "MIT", "repository": "github:yourusername/plugin-example", "keywords": ["elizaos-plugin", "eliza-plugin"], "scripts": { "build": "tsc", "test": "vitest", "dev": "tsc --watch" }, "dependencies": { "@elizaos/core": "^1.0.0" }, "devDependencies": { "typescript": "^5.0.0", "vitest": "^1.0.0" }, "agentConfig": { "actions": ["exampleAction"], "providers": ["exampleProvider"], "evaluators": ["exampleEvaluator"], "models": ["gpt-4", "gpt-3.5-turbo"], "services": ["discord", "telegram"] } } ``` The `agentConfig` section tells elizaOS agents how to load your plugin. ## Authentication Errors ### npm Login Issues ```bash # Refresh credentials bunx npm logout bunx npm login ``` ### GitHub Token Issues Generate a new PAT with `repo`, `read:org`, and `workflow` scopes: ```bash # Set token export GITHUB_TOKEN=your_new_token # Or enter when prompted elizaos publish ``` ## Validation Failures Use `--test` to identify issues: ```bash elizaos publish --test ``` Common problems: * Plugin name doesn't start with `plugin-` * Missing or incorrectly sized images * Invalid repository URL format ## Build Failures Debug TypeScript errors: ```bash # Ensure dependencies are installed bun install # Run build manually bun run build ``` ## Version Conflicts Cannot publish over existing versions: ```bash # Check current version bunx npm view your-plugin version # Bump version bun version patch # Retry elizaos publish ``` ## GitHub Repository Exists If repository already exists: ```bash # Verify it's correct gh repo view yourusername/plugin-name # Publish to npm only (skip GitHub and registry) elizaos publish --npm ``` ## Registry Submission Issues ```bash # Test registry generation elizaos publish --dry-run # Check generated files ls packages/registry/ # Skip registry if needed elizaos publish --skip-registry ``` ## CI/CD Integration Example GitHub Actions workflow: ```yaml name: Publish on: release: types: [created] jobs: publish: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: oven-sh/setup-bun@v1 - name: Install dependencies run: bun install - name: Build run: bun run build - name: Test run: bun test - name: Publish to npm run: bun publish env: NPM_TOKEN: ${{ secrets.NPM_TOKEN }} ``` ## Related Commands * [`create`](/cli-reference/create): Create a new plugin * [`plugins`](/cli-reference/plugins): Manage plugins * [`test`](/cli-reference/test): Test before publishing * [Publish a Plugin](/guides/publish-a-plugin): Complete walkthrough # Start Command Source: https://docs.elizaos.ai/cli-reference/start Launch and manage elizaOS projects and agents in production mode ## Usage ```bash elizaos start [options] ``` ## Options | Option | Description | | ------------------------ | ---------------------------------- | | `-c, --configure` | Reconfigure services and AI models | | `--character ` | Character file(s) to use | | `-p, --port ` | Port to listen on | ### Basic Usage ```bash # Start with default configuration elizaos start # Start on custom port elizaos start --port 8080 # Force reconfiguration elizaos start --configure ``` ### Character Configuration ```bash # Start with single character file elizaos start --character ./character.json # Start with multiple character files elizaos start --character ./char1.json ./char2.json # Mix local files and URLs elizaos start --character ./local.json https://example.com/remote.json # Character files without .json extension elizaos start --character assistant support-bot # Comma-separated format also works elizaos start --character "char1.json,char2.json" ``` ### Advanced Configurations ```bash # Reconfigure services before starting elizaos start --configure # Start with specific character on custom port elizaos start --character ./my-bot.json --port 4000 # Complete setup for production deployment elizaos start --character ./production-bot.json --port 3000 ``` ### Production Deployment ```bash # With environment file cp .env.production .env elizaos start # Background process (Linux/macOS) nohup elizaos start > elizaos.log 2>&1 & ``` ### Health Checks ```bash # Verify service is running curl http://localhost:3000/health # Check process status ps aux | grep elizaos # Monitor logs tail -f elizaos.log ``` ## Production Features When you run `start`, elizaOS provides production-ready features: 1. **Optimized Performance**: Runs with production optimizations 2. **Stable Configuration**: Uses saved configuration by default 3. **Service Management**: Handles service connections and disconnections 4. **Error Recovery**: Automatic recovery from transient errors 5. **Resource Management**: Efficient resource allocation and cleanup ## Startup Process When you run the `start` command, elizaOS: 1. **Project Detection**: Detects whether you're in a project or plugin directory 2. **Configuration Loading**: Loads and validates the configuration 3. **Database Initialization**: Initializes the database system 4. **Plugin Loading**: Loads required plugins 5. **Service Startup**: Starts any configured services 6. **Knowledge Processing**: Processes knowledge files if present 7. **API Server**: Starts the HTTP API server 8. **Agent Runtime**: Initializes agent runtimes 9. **Event Listening**: Begins listening for messages and events ## Project Detection elizaOS automatically detects the type of directory you're in and adjusts its behavior accordingly: * **elizaOS Projects**: Loads project configuration and starts defined agents * **elizaOS Plugins**: Runs in plugin test mode with the default character * **Other Directories**: Uses the default Eliza character ## Configuration Management ### Default Configuration * Uses saved configuration from previous runs * Loads environment variables from `.env` file * Applies project-specific settings ### Force Reconfiguration ```bash # Bypass saved configuration and reconfigure all services elizaos start --configure ``` This is useful when: * You've changed API keys or service credentials * You want to select different AI models * Service configurations have changed * Troubleshooting configuration issues ## Environment Variables The `start` command automatically loads environment variables: ### From .env File ```bash # elizaOS looks for .env in the project directory cd my-project elizaos start # Loads from ./my-project/.env ``` ### Direct Environment Variables ```bash # Set variables directly OPENAI_API_KEY=your-key elizaos start # Multiple variables OPENAI_API_KEY=key1 DISCORD_TOKEN=token1 elizaos start ``` ## Error Handling ### Character Loading Errors If character files fail to load, elizaOS will: 1. **Log Errors**: Display detailed error messages for each failed character 2. **Continue Starting**: Use any successfully loaded characters 3. **Fallback**: Use the default Eliza character if no characters load successfully ### Service Connection Errors * Automatic retry for transient connection issues * Graceful degradation when optional services are unavailable * Error logging with recovery suggestions ## Port Management ### Default Port * Port must be specified with `-p` or `--port` option * Automatically detects if port is in use * Suggests alternative ports if specified port is unavailable ### Custom Port ```bash # Specify custom port elizaos start --port 8080 # Check if port is available first netstat -an | grep :8080 elizaos start --port 8080 ``` ## Build Process The `start` command does not include built-in build functionality. To build your project before starting: ```bash # Build separately before starting bun run build elizaos start ``` ## Health Checks ```bash # Verify service is running curl http://localhost:3000/health # Check process status ps aux | grep elizaos # Monitor logs tail -f elizaos.log ``` ## Troubleshooting ### Startup Failures ```bash # Check if another instance is running ps aux | grep elizaos pkill -f elizaos # Clear any conflicting processes # Press Ctrl+C in the terminal where elizaos start is running elizaos start ``` ### Port Conflicts ```bash # Check what's using the port lsof -i :3000 # Use different port elizaos start --port 3001 # Or stop conflicting service sudo kill -9 $(lsof -ti:3000) elizaos start ``` ### Character Loading Issues ```bash # Verify character file exists and is valid JSON cat ./character.json | jq . # Test with absolute path elizaos start --character /full/path/to/character.json # Start without character to use default elizaos start ``` ### Configuration Problems ```bash # Force reconfiguration to fix corrupted settings elizaos start --configure # Check environment variables elizaos env list # Reset environment if needed elizaos env reset elizaos start --configure ``` ### Build Failures ```bash # Build separately and check for errors bun run build # If build succeeds, then start elizaos start # Install dependencies if missing bun install bun run build elizaos start ``` ### Service Connection Issues ```bash # Check internet connectivity ping google.com # Verify API keys are set elizaos env list # Test with minimal configuration elizaos start --configure ``` ## Related Commands * [`create`](/cli-reference/create): Create a new project to start * [`dev`](/cli-reference/dev): Run in development mode with hot reloading * [`agent`](/cli-reference/agent): Manage individual agents * [`env`](/cli-reference/env): Configure environment variables # TEE Command Source: https://docs.elizaos.ai/cli-reference/tee Manage TEE deployments on elizaOS The `tee` command provides access to Trusted Execution Environment (TEE) deployment and management capabilities through integrated vendor CLIs. ## Overview TEE (Trusted Execution Environment) enables secure and verifiable agent operations on blockchain. The `tee` command currently supports Phala Cloud as a TEE provider, with the potential for additional vendors in the future. ## Installation ```bash bun install -g @elizaos/cli ``` ## Command Structure ```bash elizaos tee [vendor-specific-commands] ``` ## Available Vendors ### Phala Cloud The `phala` subcommand provides a wrapper for the official Phala Cloud CLI, allowing you to manage TEE deployments on Phala Cloud directly through elizaOS. ```bash elizaos tee phala [phala-cli-commands] ``` The Phala CLI will be automatically downloaded via bunx if not already installed. ## Usage Examples ### Get Phala CLI Help ```bash # Display Phala CLI help elizaos tee phala help # Get help for a specific Phala command elizaos tee phala cvms help ``` ### Authentication ```bash # Login to Phala Cloud with your API key elizaos tee phala auth login # Check authentication status elizaos tee phala auth status ``` ### Managing CVMs (Confidential Virtual Machines) ```bash # List all CVMs elizaos tee phala cvms list # Create a new CVM elizaos tee phala cvms create --name my-agent-app --compose ./docker-compose.yml # Get CVM details elizaos tee phala cvms get # Update a CVM elizaos tee phala cvms update --compose ./docker-compose.yml # Delete a CVM elizaos tee phala cvms delete ``` ### Additional Phala Commands The Phala CLI also provides these additional commands: ```bash # Docker Registry Management elizaos tee phala docker login # Login to Docker Hub elizaos tee phala docker logout # Logout from Docker Hub # TEE Simulator (for local testing) elizaos tee phala simulator start # Start local TEE simulator elizaos tee phala simulator stop # Stop local TEE simulator elizaos tee phala simulator status # Check simulator status # Demo Deployment elizaos tee phala demo deploy # Deploy a demo application to Phala Cloud elizaos tee phala demo list # List deployed demos elizaos tee phala demo delete # Delete a demo deployment # Account Management elizaos tee phala join # Join Phala Cloud and get a free account elizaos tee phala free # Alias for join - get free CVM credits # Node Management elizaos tee phala nodes list # List available TEE nodes elizaos tee phala nodes get # Get details about a specific node ``` ### TEE Agent Deployment For deploying elizaOS agents to TEE environments: 1. First, create a TEE-compatible project: ```bash elizaos create my-tee-agent --type tee ``` 2. Configure your agent and prepare deployment files 3. Deploy to Phala Cloud: ```bash elizaos tee phala cvms create --name my-tee-agent --compose ./docker-compose.yml ``` ## Configuration ### Prerequisites * Bun installed (required for automatic Phala CLI installation) * Phala Cloud account and API key (for deployment operations) * Docker compose file for CVM deployments ### Environment Variables When deploying TEE agents, ensure your environment variables are properly configured: ```bash # Set up your Phala API key export PHALA_API_KEY="your-api-key" # Or add to your .env file echo "PHALA_API_KEY=your-api-key" >> .env ``` ## Advanced Usage ### Direct Phala CLI Access All Phala CLI commands and options are available through the wrapper: ```bash # Any Phala CLI command can be used elizaos tee phala [any-phala-command] [options] ``` For the complete list of Phala CLI commands and options, run: ```bash elizaos tee phala help ``` Or visit the official Phala CLI documentation: ```bash bunx phala help ``` ## Troubleshooting ### Common Issues 1. **bunx not found**: Install Bun from [bun.sh](https://bun.sh): ```bash curl -fsSL https://bun.sh/install | bash ``` 2. **Authentication failures**: Ensure your API key is valid and you're logged in: ```bash elizaos tee phala auth login ``` 3. **Deployment errors**: Check your docker-compose.yml file is valid and all required services are defined ### Debug Mode For detailed output when troubleshooting: ```bash # Run with verbose logging LOG_LEVEL=debug elizaos tee phala cvms list ``` ## Integration with elizaOS TEE deployments enable: * **Secure key management**: Private keys never leave the TEE * **Verifiable computation**: Cryptographic proof of agent behavior * **Blockchain integration**: Direct onchain operations with attestation * **Privacy preservation**: Sensitive data processing in secure enclaves ## Related Documentation * [Creating TEE Projects](/cli-reference/create#tee-trusted-execution-environment) * [Phala Cloud Documentation](https://docs.phala.network/) ## Security Considerations When deploying agents to TEE: 1. Never commit private keys or sensitive configuration 2. Use environment variables for secrets 3. Verify attestation reports for production deployments 4. Follow Phala Cloud security best practices # Test Command Source: https://docs.elizaos.ai/cli-reference/test Run and manage tests for elizaOS projects and plugins ## Usage ```bash elizaos test [options] [path] ``` ## Arguments | Argument | Description | | -------- | ------------------------------------------ | | `[path]` | Optional path to project or plugin to test | ## Options | Option | Description | | ------------------- | ---------------------------------------------------------------------------------- | | `-t, --type ` | Type of test to run (choices: "component", "e2e", "all", default: "all") | | `--port ` | Server port for e2e tests | | `--name ` | Filter tests by name (matches file names or test suite names). **Case sensitive.** | | `--skip-build` | Skip building before running tests | | `--skip-type-check` | Skip TypeScript type checking for faster test runs | ## Examples ### Basic Test Execution ```bash # Run all tests (component and e2e) - default behavior elizaos test # Explicitly run all tests elizaos test --type all # Run only component tests elizaos test --type component # Run only end-to-end tests elizaos test --type e2e # Test a specific project or plugin path elizaos test ./plugins/my-plugin ``` ### Test Filtering ```bash # Filter component tests by name elizaos test --type component --name auth # Filter e2e tests by name elizaos test --type e2e --name database # Filter all tests by name (case sensitive) elizaos test --name plugin ``` ### Advanced Options ```bash # Run tests on custom port for e2e elizaos test --type e2e --port 4000 # Skip building before running tests elizaos test --skip-build # Skip type checking for faster test runs elizaos test --skip-type-check # Combine options elizaos test --type e2e --port 3001 --name integration --skip-build ``` ## Test Types ### Component Tests **Location**: `__tests__/` directory\ **Framework**: Vitest\ **Purpose**: Unit and integration testing of individual components ### End-to-End Tests **Location**: `e2e/` directory\ **Framework**: Custom elizaOS test runner\ **Purpose**: Runtime behavior testing with full agent context ## Test Structure elizaOS follows standard testing conventions with two main categories: ### Component Tests (`__tests__/`) Component tests focus on testing individual modules, functions, and components in isolation. ```typescript // __tests__/myPlugin.test.ts import { describe, it, expect } from 'vitest'; import { MyPlugin } from '../src/myPlugin'; describe('MyPlugin', () => { it('should initialize correctly', () => { const plugin = new MyPlugin(); expect(plugin.name).toBe('MyPlugin'); }); it('should handle actions', async () => { const plugin = new MyPlugin(); const result = await plugin.handleAction('test'); expect(result).toBeDefined(); }); }); ``` ### End-to-End Tests (`e2e/`) E2E tests verify the complete flow of your agent with all integrations. ```typescript // e2e/agent-flow.test.ts import { createTestAgent } from '@elizaos/core/test-utils'; describe('Agent Flow', () => { it('should respond to messages', async () => { const agent = await createTestAgent({ character: './test-character.json' }); const response = await agent.sendMessage('Hello'); expect(response).toContain('Hi'); }); }); ``` ## Test Configuration ### Vitest Configuration Component tests use Vitest, which is configured in your project's `vitest.config.ts`: ```typescript import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { globals: true, environment: 'node', include: ['__tests__/**/*.test.ts'], }, }); ``` ### E2E Test Configuration E2E tests can be configured via environment variables: ```bash # Set test environment export TEST_ENV=ci export TEST_PORT=3001 # Run E2E tests elizaos test --type e2e ``` ## Coverage Reports Generate and view test coverage: ```bash # Run tests (coverage generation depends on your test configuration) elizaos test # Note: Coverage reporting is handled by your test framework configuration, # not by the CLI directly. Configure coverage in your vitest.config.ts ``` ## Continuous Integration Example GitHub Actions workflow: ```yaml name: Tests on: [push, pull_request] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: oven-sh/setup-bun@v1 - name: Install dependencies run: bun install - name: Run tests run: elizaos test - name: Upload coverage uses: codecov/codecov-action@v3 ``` ## Testing Best Practices ### 1. Test Organization * Keep tests close to the code they test * Use descriptive test names * Group related tests with `describe` blocks * Follow the AAA pattern (Arrange, Act, Assert) ### 2. Test Isolation * Each test should be independent * Clean up resources after tests * Use test fixtures for consistent data * Mock external dependencies ### 3. Performance * Use `--skip-build` during development for faster feedback * Run focused tests with `--name` filter * Use `--skip-type-check` for faster test runs when type safety is already verified * Parallelize tests when possible ### 4. Coverage Goals * Aim for 80%+ code coverage * Focus on critical paths * Don't sacrifice test quality for coverage * Test edge cases and error scenarios ## Common Testing Patterns ### Testing Plugins ```typescript import { createMockRuntime } from '@elizaos/core/test-utils'; describe('MyPlugin', () => { let runtime; beforeEach(() => { runtime = createMockRuntime(); }); it('should register actions', () => { const plugin = new MyPlugin(); plugin.init(runtime); expect(runtime.actions).toContain('myAction'); }); }); ``` ### Testing Actions ```typescript describe('MyAction', () => { it('should validate input', async () => { const action = new MyAction(); const isValid = await action.validate({ text: 'test input' }); expect(isValid).toBe(true); }); }); ``` ### Testing with Mock Data ```typescript import { mockCharacter, mockMessage } from '@elizaos/core/test-utils'; describe('Message Handler', () => { it('should process messages', async () => { const character = mockCharacter({ name: 'TestBot' }); const message = mockMessage({ text: 'Hello', userId: 'user123' }); const response = await handler.process(message, character); expect(response).toBeDefined(); }); }); ``` ## Debugging Tests ### Verbose Output ```bash # Run with detailed logging LOG_LEVEL=debug elizaos test # Show test execution details elizaos test --verbose ``` ### Running Specific Tests ```bash # Run a single test file (case sensitive) elizaos test component --name specific-test # Run tests matching a pattern (case sensitive) elizaos test --name "auth|user" # Important: Test name matching is case sensitive # Use exact casing from your test file names ``` ### Debugging in VS Code Add to `.vscode/launch.json`: ```json { "type": "node", "request": "launch", "name": "Debug Tests", "runtimeExecutable": "bun", "runtimeArgs": ["test"], "cwd": "${workspaceFolder}", "console": "integratedTerminal" } ``` ## Troubleshooting ### Test Failures ```bash # Check for TypeScript errors first bun run build # Run tests with more verbose output elizaos test --verbose # Skip type checking if types are causing issues elizaos test --skip-type-check ``` ### Port Conflicts ```bash # E2E tests failing due to port in use # Use a different port elizaos test e2e --port 4001 # Or kill the process using the port lsof -ti:3000 | xargs kill -9 ``` ### Build Issues ```bash # If tests fail due to build issues # Clean and rebuild rm -rf dist bun run build elizaos test # Or skip build if testing source files elizaos test --skip-build ``` ### Watch Mode Issues ```bash # If watch mode isn't detecting changes # Check that you're modifying files in watched directories # Restart watch mode elizaos test --watch # Or use Vitest directly for component tests bunx vitest --watch ``` ### Coverage Issues ```bash # If coverage seems incorrect # Clear coverage data rm -rf coverage # Regenerate coverage elizaos test --coverage # Check coverage config in vitest.config.ts ``` ### Environment Issues ```bash # Set test environment variables export NODE_ENV=test export TEST_TIMEOUT=30000 # Or create a test .env file cp .env.example .env.test elizaos test ``` ## Related Commands * [`dev`](/cli-reference/dev): Run development mode with test watching * [`create`](/cli-reference/create): Create projects with test structure * [`start`](/cli-reference/start): Start project after tests pass # Update Command Source: https://docs.elizaos.ai/cli-reference/update Update your project's elizaOS dependencies and CLI to the latest published versions ## Usage ```bash elizaos update [options] ``` ## Options | Option | Description | | -------------- | ------------------------------------------------------------------- | | `-c, --check` | Check for available updates without applying them | | `--skip-build` | Skip building after updating | | `--cli` | Update only the global CLI installation (without updating packages) | | `--packages` | Update only packages (without updating the CLI) | ### Basic Update ```bash # Update both CLI and project dependencies (default behavior) elizaos update ``` ### Checking for Updates ```bash # Check for available updates without applying them elizaos update --check ``` *Example Output:* ```bash $ elizaos update --check Checking for updates... Current CLI version: 1.3.5 Latest CLI version: 1.4.0 elizaOS packages that can be updated: - @elizaos/core (1.3.0) → 1.4.0 - @elizaos/plugin-openai (1.2.5) → 1.4.0 To apply updates, run: elizaos update ``` ### Scoped Updates ```bash # Update only the global CLI elizaos update --cli # Update only project packages elizaos update --packages ``` ### Combined Options ```bash # Check only for CLI updates elizaos update --check --cli # Update packages without rebuilding afterward elizaos update --packages --skip-build ``` ## Update Process Explained When you run `elizaos update`, it performs the following steps: 1. **Detects Project Type**: Determines if you're in an elizaOS project or plugin. 2. **Checks CLI Version**: Compares your installed CLI version with the latest available on npm. 3. **Scans Dependencies**: Finds all `@elizaos/*` packages in your project's `package.json`. 4. **Shows Update Plan**: Lists the packages and/or CLI that have available updates. 5. **Prompts for Confirmation**: Asks for your approval before making any changes. 6. **Updates Packages**: Installs the latest versions of the packages. 7. **Rebuilds Project**: Compiles the updated dependencies (unless `--skip-build` is used). ### Workspace & Monorepo Support The update command is smart enough to detect monorepo workspaces. It will automatically skip any packages that are linked via `workspace:*` in your `package.json`, as these should be managed within the monorepo, not from the npm registry. ## Best Practices ### Safe Update Process For the smoothest update experience, follow this sequence: 1. **Check what will be updated**: `elizaos update --check` 2. **Commit your current work**: `git commit -am "chore: pre-update savepoint"` 3. **Update the CLI first**: `elizaos update --cli` 4. **Then, update project packages**: `elizaos update --packages` 5. **Test your project thoroughly**: `elizaos test` ## Project Detection The update command automatically detects: * **elizaOS Projects**: Updates project dependencies and rebuilds * **elizaOS Plugins**: Updates plugin dependencies and rebuilds * **Non-elizaOS Projects**: Shows error message ## Workspace Support ### Monorepo Detection * Automatically detects workspace references * Skips packages with `workspace:*` versions * Shows which packages are workspace-managed ### Example with Workspaces ```bash $ elizaos update --check elizaOS packages found: - @elizaos/core (workspace:*) → Skipped (workspace reference) - @elizaos/plugin-openai (1.2.5) → 1.4.0 - @elizaos/plugin-discord (workspace:*) → Skipped (workspace reference) Only non-workspace packages will be updated. ``` ## Version Strategy ### Staying Current * Update regularly to get latest features and fixes * Use `--check` to monitor available updates * Subscribe to elizaOS release notes ### Stability Considerations * Test updates in development before production * Consider pinning versions for production deployments * Review changelogs for breaking changes ## Troubleshooting ### CLI Update Issues If you have trouble updating the global CLI: ```bash # Check if the CLI is installed globally bun pm ls -g @elizaos/cli # If not, install it bun install -g @elizaos/cli # On macOS/Linux, you may need sudo sudo bun install -g @elizaos/cli # Or fix permissions on your bun directory sudo chown -R $(whoami) ~/.bun ``` ### Package Update Failures If package updates fail, a clean reinstall usually fixes it: ```bash # Clear caches and old dependencies rm -rf node_modules bun pm cache rm rm bun.lockb # Reinstall everything bun install ``` ### Build Failures After Update If your project fails to build after an update: ```bash # Try a clean build bun run build # Or try updating without the build step, then build manually elizaos update --skip-build bun install && bun run build ``` ### Version Mismatch Issues ```bash # Check current versions elizaos --version # CLI version cat package.json | grep "@elizaos" # Package versions # Force specific versions if needed bun add @elizaos/core@1.4.0 @elizaos/plugin-openai@1.4.0 ``` ### Network Issues ```bash # If updates fail due to network # Check npm registry bun config get registry # Reset to default if needed bun config set registry https://registry.npmjs.org/ # Retry update elizaos update ``` ### Monorepo Update Issues ```bash # In monorepo, update workspace packages manually cd packages/core bun update # Or update all workspaces bun update --filter '*' ``` ## Related Commands * [`create`](/cli-reference/create): Create new projects with latest versions * [`start`](/cli-reference/start): Start your updated project * [`dev`](/cli-reference/dev): Run in development mode after updates * [`test`](/cli-reference/test): Test your project after updates # Add Multiple Agents Source: https://docs.elizaos.ai/guides/add-multiple-agents Build and coordinate multiple specialized agents working together as a team **Video Tutorial**: [**Multiple Agents and Characters**](https://www.youtube.com/watch?v=T53M7KueDgM\&list=PLrjBjP4nU8ehOgKAa0-XddHzE0KK0nNvS\&index=3) This guide builds on concepts from [Customize an Agent](/guides/customize-an-agent) ## Step 1: Add Hemingway ### Add Hemingway to your project You already have Shakespeare in `src/character.ts` from the previous guide. Now let's add another agent to our project so they can interact. We'll create a fresh character file for Hemingway using the CLI: ```bash Terminal elizaos create --type agent hemingway ``` This clones a JSON character template as `hemingway.json`. You'll now have: * `src/character.ts` - Shakespeare (TypeScript format) * `hemingway.json` - Hemingway (JSON format) The CLI clones JSON character templates by default. If you prefer TypeScript characters, you can manually clone your `character.ts` file from your IDE. They work exactly the same, it's just a matter of preference. ### Customize Hemingway's personality Open `hemingway.json` and update it to customize Hemingway's personality: ```json hemingway.json { "name": "hemingway", "system": "Respond to all messages in a helpful, conversational manner. Provide assistance on a wide range of topics, using knowledge when needed. Be concise but thorough, friendly but professional. Use humor when appropriate and be empathetic to user needs. Provide valuable information and insights when questions are asked.", // [!code --] "system": "You are Ernest Hemingway. Speak simply. Use short sentences. Cut the fat. Every word must earn its place. You respect Shakespeare but find him wordy. You've lived through war, love, and loss. Truth matters more than beauty. Experience matters more than theory. Never use two words when one will do. Avoid adjectives. Kill your darlings. The first draft of anything is shit, so make every word count now.", // [!code ++] "bio": [ "hemingway is a helpful AI assistant created to provide assistance and engage in meaningful conversations.", // [!code --] "hemingway is knowledgeable, creative, and always eager to help users with their questions and tasks." // [!code --] "Ernest Hemingway, American novelist and journalist", // [!code ++] "Master of the iceberg theory - show only what matters", // [!code ++] "Champion of simple declarative sentences", // [!code ++] "War correspondent who saw truth in trenches", // [!code ++] "Believer that courage is grace under pressure", // [!code ++] "Man who lived fully - bullfights, safaris, deep-sea fishing", // [!code ++] "Writer who found truth in simple things", // [!code ++] "Teacher who says: write one true sentence" // [!code ++] ] } ``` Just like in the [previous guide](/guides/customize-an-agent), continue editing the other fields (`topics`, `style`, `messageExamples`, etc.) to match Hemingway as you see fit. ## Step 2: Configure Discord and voice ### Add Discord plugin to Hemingway Add `plugin-discord` to Hemingway so he can join Shakespeare in our Discord server: ```json hemingway.json { "plugins": [ "@elizaos/plugin-sql", "@elizaos/plugin-openai", "@elizaos/plugin-bootstrap", "@elizaos/plugin-discord" // [!code ++] ] } ``` ### Configure agent-specific keys But wait! We have our Discord environment variables defined in `.env`, but we need unique ones for each agent. Hemingway and Shakespeare need their own Discord bot tokens. How do we have agent-specific keys? For that, we use `secrets` under `settings` in each character file. This allows each agent to have their own Discord bot identity: **For Hemingway (`hemingway.json`):** ```json hemingway.json { "settings": { "secrets": {}, // [!code --] "secrets": { // [!code ++] "DISCORD_APPLICATION_ID": "YOUR_HEMINGWAY_APP_ID", // [!code ++] "DISCORD_API_TOKEN": "YOUR_HEMINGWAY_BOT_TOKEN", // [!code ++] }, // [!code ++] "avatar": "https://example.com/hemingway-portrait.png" } } ``` **For Shakespeare (`src/character.ts`):** ```typescript src/character.ts export const character: Character = { settings: { secrets: {}, // [!code --] secrets: { // [!code ++] DISCORD_APPLICATION_ID: "YOUR_SHAKESPEARE_APP_ID", // [!code ++] DISCORD_API_TOKEN: "YOUR_SHAKESPEARE_BOT_TOKEN", // [!code ++] }, // [!code ++] avatar: 'https://example.com/shakespeare-portrait.png', }, } ``` Each agent needs its own Discord application and bot token. Follow the Discord setup steps from the [previous guide](/guides/customize-an-agent#create-discord-application) for each agent you create. ### Enable voice mode Let's enable voice capabilities for our agents in Discord: **For Hemingway (`hemingway.json`):** ```json hemingway.json { "settings": { "secrets": { "DISCORD_APPLICATION_ID": "YOUR_HEMINGWAY_APP_ID", "DISCORD_API_TOKEN": "YOUR_HEMINGWAY_BOT_TOKEN", "DISCORD_VOICE_ENABLED": "true" // [!code ++] } } } ``` **For Shakespeare (`src/character.ts`):** ```typescript src/character.ts { settings: { secrets: { DISCORD_APPLICATION_ID: "YOUR_SHAKESPEARE_APP_ID", DISCORD_API_TOKEN: "YOUR_SHAKESPEARE_BOT_TOKEN", DISCORD_VOICE_ENABLED: "true" // [!code ++] } } } ``` ### Add ElevenLabs voice provider Now let's add `plugin-elevenlabs` to provide high-quality voice synthesis for our agents: **Add ElevenLabs plugin:** ```json hemingway.json { "plugins": [ "@elizaos/plugin-sql", "@elizaos/plugin-openai", "@elizaos/plugin-discord", "@elizaos/plugin-bootstrap", "@elizaos/plugin-elevenlabs" // [!code ++] ] } ``` ```typescript src/character.ts export const character: Character = { plugins: [ '@elizaos/plugin-sql', '@elizaos/plugin-discord', '@elizaos/plugin-elevenlabs', // [!code ++] ...(process.env.OPENAI_API_KEY?.trim() ? ['@elizaos/plugin-openai'] : []), ...(!process.env.IGNORE_BOOTSTRAP ? ['@elizaos/plugin-bootstrap'] : []), ], } ``` ### Configure voices for each agent Now let's add the ElevenLabs secrets so each agent has their own distinct voice: **For Hemingway (`hemingway.json`):** ```json hemingway.json { "settings": { "secrets": { "DISCORD_APPLICATION_ID": "YOUR_HEMINGWAY_APP_ID", "DISCORD_API_TOKEN": "YOUR_HEMINGWAY_BOT_TOKEN", "DISCORD_VOICE_ENABLED": "true", "ELEVENLABS_API_KEY": "your_elevenlabs_api_key", // [!code ++] "ELEVENLABS_VOICE_ID": "Xb7hH8MSUJpSbSDYk0k2", // Deep male voice // [!code ++] "ELEVENLABS_MODEL_ID": "eleven_multilingual_v2", // [!code ++] "ELEVENLABS_VOICE_STABILITY": "0.5", // [!code ++] "ELEVENLABS_OPTIMIZE_STREAMING_LATENCY": "0", // [!code ++] "ELEVENLABS_OUTPUT_FORMAT": "pcm_16000", // [!code ++] "ELEVENLABS_VOICE_SIMILARITY_BOOST": "0.75", // [!code ++] "ELEVENLABS_VOICE_STYLE": "0", // [!code ++] "ELEVENLABS_VOICE_USE_SPEAKER_BOOST": "true" // [!code ++] } } } ``` **For Shakespeare (`src/character.ts`):** ```typescript src/character.ts { settings: { secrets: { DISCORD_APPLICATION_ID: "YOUR_SHAKESPEARE_APP_ID", DISCORD_API_TOKEN: "YOUR_SHAKESPEARE_BOT_TOKEN", DISCORD_VOICE_ENABLED: "true", ELEVENLABS_API_KEY: "your_elevenlabs_api_key", // [!code ++] ELEVENLABS_VOICE_ID: "21m00Tcm4TlvDq8ikWAM", // Theatrical British voice // [!code ++] ELEVENLABS_MODEL_ID: "eleven_multilingual_v2", // [!code ++] ELEVENLABS_VOICE_STABILITY: "0.3", // More variation for dramatic effect // [!code ++] ELEVENLABS_OPTIMIZE_STREAMING_LATENCY: "0", // [!code ++] ELEVENLABS_OUTPUT_FORMAT: "pcm_16000", // [!code ++] ELEVENLABS_VOICE_SIMILARITY_BOOST: "0.75", // [!code ++] ELEVENLABS_VOICE_STYLE: "0.5", // More expressive // [!code ++] ELEVENLABS_VOICE_USE_SPEAKER_BOOST: "true" // [!code ++] } } } ``` Get your ElevenLabs API key from [elevenlabs.io](https://elevenlabs.io) and explore different voice IDs to find the perfect match for each agent's personality. ## Step 3: Configure multi-agent project ### Add newly created agent to your project Update your `src/index.ts` to include both agents so they start automatically: ```typescript src/index.ts import { logger, type IAgentRuntime, type Project, type ProjectAgent } from '@elizaos/core'; import { character } from './character.ts'; import hemingway from '../hemingway.json'; // [!code ++] const initCharacter = ({ runtime }: { runtime: IAgentRuntime }) => { logger.info('Initializing character'); logger.info({ name: character.name }, 'Name:'); }; export const projectAgent: ProjectAgent = { character, init: async (runtime: IAgentRuntime) => await initCharacter({ runtime }), }; // Add Hemingway agent // [!code ++] const hemingwayAgent: ProjectAgent = { // [!code ++] character: hemingway, // [!code ++] init: async (runtime: IAgentRuntime) => { // [!code ++] logger.info('Initializing Hemingway'); // [!code ++] logger.info({ name: hemingway.name }, 'Name:'); // [!code ++] }, // [!code ++] }; // [!code ++] const project: Project = { agents: [projectAgent], // [!code --] agents: [projectAgent, hemingwayAgent], // [!code ++] }; ``` ### Launch both agents simultaneously Now when you start your project, both agents launch automatically: ```bash Terminal elizaos start ``` You'll see both agents initialize in the console output: ``` ✓ Shakespeare initialized ✓ Hemingway initialized ``` **Alternative: CLI agent command** You can also manipulate agents via the CLI once a server is running. See the [CLI Agent Command Reference](/cli-reference/agent) for complete details. ### Join them in Discord voice chat Now go to the voice channel's chatroom and invite both agents to join the voice channel: Discord text channel showing messages inviting Shakespeare and Hemingway to join voice chat Say something, and hear your literary duo respond and converse: Discord voice channel showing Shakespeare and Hemingway engaged in literary debate with their unique voices It's working! Your agents are now conversing with their own unique personalities and voices! ## What's next? Now that you know how to add multiple agents to a single project, you can add as many as you like, all with completely custom sets of plugins and personalities. Here's what's next: Ensure your literary duo maintains their unique voices consistently Share your Shakespeare vs Hemingway debates with the world Build custom plugins to extend your agents' capabilities Learn how to publish your plugins to the elizaOS registry # Contribute to Core Source: https://docs.elizaos.ai/guides/contribute-to-core How to contribute to the elizaOS core project and plugin ecosystem This guide covers contributing to the main elizaOS monorepo and the elizaOS plugin ecosystem. ## Understanding the Ecosystem elizaOS open source contribution happens across these main areas: ### Main Repository (Monorepo) **[github.com/elizaos/eliza](https://github.com/elizaos/eliza)** - The core monorepo containing: * `packages/core` - Runtime, types, interfaces * `packages/cli` - Command-line tools and elizaos CLI * `packages/server` - Agent server implementation * `packages/client` - Client libraries and interfaces * Core plugins (`packages/plugin-bootstrap`, `packages/plugin-sql`, etc.) * Project templates (`packages/project-starter`, `packages/project-tee-starter`) * Plugin templates (`packages/plugin-starter`, `packages/plugin-quick-starter`) * Config files, READMEs & more ### Plugin Ecosystem **[github.com/elizaos-plugins](https://github.com/elizaos-plugins)** - Official plugins maintained by the elizaOS team: * `plugin-discord` - Discord integration * `plugin-twitter` - Twitter/X integration * `plugin-evm` - Ethereum and blockchain functionality * And many more frequently-used plugins *** ## Step 1: Identify an Issue ### Check Main Repository Issues **Start here first** - Browse existing bugs in the main repo: **[elizaos/eliza/issues](https://github.com/elizaos/eliza/issues)** Focus on labels like: * `good first issue` - Perfect for newcomers * `bug` - Something that needs fixing The best way to start contributing is fixing reported bugs rather than writing new features. ### elizaOS-Maintained Plugin Issues Find issues in elizaOS-maintained plugins (often more focused for first contributions): **Official elizaOS plugins:** * [plugin-twitter/issues](https://github.com/elizaos-plugins/plugin-twitter/issues) * [plugin-discord/issues](https://github.com/elizaos-plugins/plugin-discord/issues) * [plugin-evm/issues](https://github.com/elizaos-plugins/plugin-evm/issues) ### Community Plugin Issues **Community plugins are separate** - These are built by the community: * Browse the [Plugin Registry](/plugin-registry/overview) for community-maintained plugins * Check their GitHub repositories for contribution opportunities * Help with maintenance: updating dependencies, fixing bugs, improving docs * Consider adopting unmaintained plugins by forking and continuing development ### Creating Issues for New Bugs If you discover a bug without an existing issue: 1. **Reproduce the bug** consistently & locally 2. **Check if it's already reported** by searching existing issues 3. **Create a detailed issue** with: * Clear description of the problem * Steps to reproduce * Expected vs actual behavior * Environment details (OS, Node/Bun version, elizaOS version) * Error messages or logs ```markdown Issue Template Example ## Bug Description The Discord plugin fails to connect when using voice channels ## Steps to Reproduce 1. Configure agent with Discord and ElevenLabs plugins 2. Invite agent to voice channel 3. Agent connects but immediately disconnects ## Expected Behavior Agent should remain connected and respond with voice ## Environment - elizaOS version: 1.4.4 - Node version: 23.3 - OS: macOS 14.0 ``` ### Contribute to Docs and Community Beyond code contributions, you can help in these important areas: **Documentation contributions:** * Add tutorials to the tutorials section in [docs repository](https://github.com/elizaos/docs) * Update any outdated references, instructions, or broken links you find * Fix typos, improve clarity, or add missing examples **Community support:** * **Answer questions** in [GitHub Discussions](https://github.com/orgs/elizaOS/discussions) Q\&A section * **Help with troubleshooting** - Setup issues, configuration problems, etc. * **Share knowledge** in general discussions about elizaOS development * **Showcase projects** in show and tell or participate in feature discussions Community contributions like answering questions and writing tutorials are often the most impactful ways to help other developers succeed with elizaOS. *** ## Step 2: Contribution Workflow elizaOS follows standard open source contribution practices for all repositories. ### Clone and Set Up Repository 1. **Clone the repository** you want to contribute to on your local machine 2. **Create a branch** off the `develop` branch for monorepo or `1.x` branch for plugins 3. **Install dependencies** and build the project ### Make Your Changes Locally **Focus on these types of contributions:** * Fix existing functionality that isn't working * Improve error handling and edge cases * Performance optimizations * Documentation corrections Large refactors are unlikely to be accepted. Focus on incremental improvements and bug fixes first. Always discuss major architectural changes with core developers before starting work. **Implementation steps:** 1. **Make your changes** to fix the bug or implement the improvement 2. **Test your changes** thoroughly - run existing tests and add new ones if needed 3. **Ensure code quality** - follow linting rules and TypeScript requirements ### Submit Your Pull Request **Target the correct branch:** * **Main repository (elizaos/eliza):** Target `develop` branch * **Plugin repositories:** Target `1.x` branch (or check the default branch) **Create a detailed pull request** with: * Clear description of what the PR does * Link to the related issue (`Fixes #123`) * List of specific changes made * Check that CI/GitHub Actions are passing * Screenshots if there are UI changes ### Collaborate During Review * Respond to code review comments promptly * Make requested changes in additional commits * Be open to feedback and iteration ### Code Quality Standards **What we look for:** * Bug fixes with clear reproduction steps * Performance improvements with benchmarks * Documentation improvements and corrections * Test coverage improvements * Security fixes **Technical requirements:** * **TypeScript**: All code must be properly typed * **Testing**: New features require tests, bug fixes should include regression tests * **Documentation**: Update relevant documentation for any user-facing changes * **Linting**: Code must pass all linting checks * **Commit Messages**: Use clear, descriptive commit messages *** ## Step 3: Get Connected ### Join Discord for Development Connect with core developers and other contributors: **[Join elizaOS Discord](https://discord.gg/ai16z)** Key channels for contributors: * **💬 #coders** - Development discussions and questions * **💻 #tech-support** - Help others troubleshoot and get help yourself ### Communicate Before Major Work For significant contributions: 1. **Post in 💬 #coders** about your planned contribution 2. **Share your approach** before implementing large features 3. **Ask questions** - the community is helpful and welcoming Core developers are active in Discord and can provide guidance on whether your planned contribution aligns with project goals. ### Build Community Connections * Participate in discussions and help answer questions * Share your progress and learn from others * Connect with the core devs & other community contributors * Stay updated on project direction and roadmap *** ## What's Next? Build your own plugins to contribute to the ecosystem Master the development tools for efficient contribution Learn comprehensive testing strategies for your contributions Connect with core developers and the contributor community # Create a Plugin Source: https://docs.elizaos.ai/guides/create-a-plugin Build a generative AI plugin from scratch using progressive plugin development **Video Tutorial**: [**Plugin Power: Add Superpowers to Your Agents**](https://www.youtube.com/watch?v=nC6veN2Q-ps\&list=PLrjBjP4nU8ehOgKAa0-XddHzE0KK0nNvS\&index=4) ## What We'll Build This guide shows how to build a [fal.ai](https://fal.ai) plugin that lets your agent generate 6-second, 768p videos from text prompts using the MiniMax Hailuo-02 model. For architectural concepts, see [Plugin Architecture](/plugins/architecture). **You'll learn:** * **Actions** (what the agent can DO) * **Progressive development** (start simple, organize as you grow) * **Local plugin testing** (character.plugins array method) * **Plugin testing** (component and E2E tests) For component details and patterns, see [Plugin Components](/plugins/components) and [Plugin Patterns](/plugins/patterns). *** ## Step 1: Quick Start ### Create Project and Plugin Create a project with a plugin inside using CLI commands: ```bash Terminal elizaos create --type project my-eliza-project ``` Configure when prompted: * **Database**: PgLite (perfect for local development) * **Model**: OpenAI ```bash Terminal cd my-eliza-project ``` ```bash Terminal elizaos create --type plugin plugin-fal-ai ``` When prompted, choose **Quick Plugin** (we don't need frontend UI) Your structure now looks like: ``` my-eliza-project/ ├── src/character.ts # Default Eliza character └── plugin-fal-ai/ # 👈 Plugin lives alongside project ├── src/ │ ├── index.ts # Plugin exports │ ├── plugin.ts # Main plugin (start here) │ └── __tests__/ # Plugin tests └── package.json ``` In `my-eliza-project/src/character.ts`, add the local path to Eliza's plugins array: ```typescript src/character.ts export const character: Character = { name: 'Eliza', plugins: [ '@elizaos/plugin-sql', '@elizaos/plugin-openai', '@elizaos/plugin-bootstrap', './plugin-fal-ai' // [!code ++] ], }; ``` ### Connect and Test The plugin needs to be built first to create the `dist/` folder that ElizaOS loads from: ```bash Terminal # Build the plugin first cd plugin-fal-ai bun run build # Go back to project and start cd .. elizaos start ``` **Verify it's loaded:** * Check the console logs for `Successfully loaded plugin 'plugin-fal-ai'` * Visit `http://localhost:3000` → click your agent → **Plugins tab** *** ## Step 2: Development ### Research the API Let's research what we want to build by exploring [fal.ai](https://fal.ai) for a good text-to-video model. [MiniMax Hailuo-02 Text to Video](https://fal.ai/models/fal-ai/minimax/hailuo-02/standard/text-to-video) looks pretty good. 4. **Navigate to the [JavaScript/Typescript section of the docs](https://docs.fal.ai/model-apis/clients/javascript)** to see how to call the API: * Install: `bun add @fal-ai/client` * Import: `import { fal } from "@fal-ai/client"` * Use: `fal.subscribe("model-endpoint", { input: {...} })` * Returns: `{ data, requestId }` Now we know exactly what to build and how to call it, so let's start developing our plugin. ### Edit Default Plugin Template ```bash Terminal cd plugin-fal-ai bun add @fal-ai/client ``` This adds the [fal.ai](https://fal.ai) client package to your plugin dependencies. Open `plugin-fal-ai/src/plugin.ts` to see the sample code patterns for plugins: * `quickAction` - example Action (what agent can DO) * `quickProvider` - example Provider (gives agent CONTEXT) * `StarterService` - example Service (manages state/connections) * Plugin events, routes, models - other comprehensive patterns Copy the plugin file and rename it to create your action: ```bash Terminal mkdir src/actions cp src/plugin.ts src/actions/generateVideo.ts ``` Now let's edit the example plugin into our generateVideo action: **Add the fal.ai import (from the fal.ai docs):** ```typescript src/actions/generateVideo.ts import { Action, ActionResult, IAgentRuntime, Memory, HandlerCallback, State, logger } from '@elizaos/core'; import { fal } from '@fal-ai/client'; // [!code ++] ``` **Update the action identity for video generation:** ```typescript const quickAction: Action = { // [!code --] export const generateVideoAction: Action = { // [!code ++] name: 'QUICK_ACTION', // [!code --] name: 'TEXT_TO_VIDEO', // [!code ++] similes: ['GREET', 'SAY_HELLO', 'HELLO_WORLD'], // [!code --] similes: ['CREATE_VIDEO', 'MAKE_VIDEO', 'GENERATE_VIDEO', 'VIDEO_FROM_TEXT'], // [!code ++] description: 'Responds with a simple hello world message', // [!code --] description: 'Generate a video from text using MiniMax Hailuo-02', // [!code ++] ``` **Replace validation with API key check:** ```typescript validate: async (_runtime, _message, _state) => { // [!code --] return true; // Always valid // [!code --] }, // [!code --] validate: async (runtime: IAgentRuntime, message: Memory) => { // [!code ++] const falKey = runtime.getSetting('FAL_KEY'); // [!code ++] if (!falKey) { // [!code ++] logger.error('FAL_KEY not found in environment variables'); // [!code ++] return false; // [!code ++] } // [!code ++] return true; // [!code ++] }, // [!code ++] ``` **Replace hello world logic with video generation:** ```typescript handler: async (_runtime, message, _state, _options, callback) => { // [!code --] const response = 'Hello world!'; // [!code --] if (callback) { // [!code --] await callback({ // [!code --] text: response, // [!code --] actions: ['QUICK_ACTION'], // [!code --] source: message.content.source, // [!code --] }); // [!code --] } // [!code --] return { // [!code --] text: response, // [!code --] success: true, // [!code --] data: { actions: ['QUICK_ACTION'], source: message.content.source } // [!code --] }; // [!code --] }, // [!code --] handler: async ( // [!code ++] runtime: IAgentRuntime, // [!code ++] message: Memory, // [!code ++] state: State | undefined, // [!code ++] options: any, // [!code ++] callback?: HandlerCallback // [!code ++] ): Promise => { // [!code ++] try { // [!code ++] fal.config({ credentials: runtime.getSetting('FAL_KEY') }); // [!code ++] let prompt = message.content.text.replace(/^(create video:|make video:)/i, '').trim(); // [!code ++] if (!prompt) return { success: false, text: 'I need a description' }; // [!code ++] const result = await fal.subscribe("fal-ai/minimax/hailuo-02/standard/text-to-video", { // [!code ++] input: { prompt, duration: "6" }, logs: true // [!code ++] }); // [!code ++] const videoUrl = result.data.video.url; // [!code ++] if (callback) await callback({ text: `✅ Video ready! ${videoUrl}` }); // [!code ++] return { success: true, text: 'Video generated', data: { videoUrl, prompt } }; // [!code ++] } catch (error) { // [!code ++] return { success: false, text: `Failed: ${error.message}` }; // [!code ++] } // [!code ++] }, // [!code ++] ``` **Update examples for video conversations:** ```typescript examples: [ // [!code --] [{ // [!code --] name: '{{name1}}', // [!code --] content: { text: 'Can you say hello?' } // [!code --] }, { // [!code --] name: '{{name2}}', // [!code --] content: { text: 'hello world!', actions: ['QUICK_ACTION'] } // [!code --] }] // [!code --] ], // [!code --] examples: [ // [!code ++] [{ name: '{{user}}', content: { text: 'Create video: dolphins jumping' } }, // [!code ++] { name: '{{agent}}', content: { text: 'Creating video!', actions: ['TEXT_TO_VIDEO'] }}] // [!code ++] ], // [!code ++] }; ``` Finally, update `src/index.ts` to use our new plugin: ```typescript src/index.ts import { Plugin } from '@elizaos/core'; import { generateVideoAction } from './actions/generateVideo'; // [!code ++] export const falaiPlugin: Plugin = { // [!code ++] name: 'fal-ai', // [!code ++] description: 'Generate videos using fal.ai MiniMax Hailuo-02', // [!code ++] actions: [generateVideoAction], // [!code ++] providers: [], // [!code ++] services: [] // [!code ++] }; // [!code ++] export default falaiPlugin; // [!code ++] export { generateVideoAction }; // [!code ++] ``` You can reference `plugin.ts` as well as other plugins from the [Plugin Registry](/plugin-registry/overview) to see other plugin component examples (providers, services, etc.) as you expand your plugin. ### Add Configuration Get an API key from [fal.ai](https://fal.ai) and copy/paste it into your .env: ```bash .env PGLITE_DATA_DIR=./.eliza/.elizadb OPENAI_API_KEY=your_openai_key_here FAL_KEY=your_fal_key_here # [!code ++] ``` *** ## Step 3: Testing ### Test Plugin Functionality Verify your plugin works as expected: First rebuild your plugin to effect our changes, then start from project root: ```bash Terminal # Build the plugin first cd plugin-fal-ai bun run build # Start from project root cd .. elizaos start ``` Try your new action by chatting with Eliza in the GUI (`http://localhost:3000`): * `"Create video: dolphins jumping in ocean"` * `"Make video: cat playing piano"` * `"Generate video: sunset over mountains"` You should see the video generation process and get a URL to view the result! ### Plugin Component Tests Plugins come default with component and E2E tests. Let's add custom component tests: Update `plugin-fal-ai/src/__tests__/plugin.test.ts`: ```typescript src/__tests__/plugin.test.ts import { describe, it, expect } from 'bun:test'; import { falaiPlugin, generateVideoAction } from '../index'; // [!code ++] describe('FAL AI Plugin', () => { it('action validates with FAL_KEY', async () => { // [!code ++] const mockRuntime = { // [!code ++] getSetting: (key: string) => key === 'FAL_KEY' ? 'test-key' : null // [!code ++] }; // [!code ++] const isValid = await generateVideoAction.validate(mockRuntime as any, {} as any); // [!code ++] expect(isValid).toBe(true); // [!code ++] }); // [!code ++] }); ``` ```bash Terminal cd plugin-fal-ai elizaos test --type component ``` ### Plugin E2E Tests Let's also add a custom E2E test: Update `src/__tests__/e2e/plugin-fal-ai.e2e.ts`: ```typescript src/__tests__/e2e/plugin-fal-ai.e2e.ts export const FalAiTestSuite = { // [!code ++] name: 'fal-ai-video-generation', // [!code ++] tests: [{ // [!code ++] name: 'should find video action in runtime', // [!code ++] fn: async (runtime) => { // [!code ++] const action = runtime.actions.find(a => a.name === 'TEXT_TO_VIDEO'); // [!code ++] if (!action) throw new Error('TEXT_TO_VIDEO action not found'); // [!code ++] } // [!code ++] }] // [!code ++] }; // [!code ++] ``` ```bash Terminal cd plugin-fal-ai elizaos test --type e2e ``` *** ## Step 4: Possible Next Steps Congratulations! You now have a working video generation plugin. Here are some ways you can improve it: ### Enhance Your Action * **Add more similes** - Handle requests like "animate this", "video of", "show me a clip of" * **Better examples** - Add more conversation examples so Eliza learns different chat patterns * **Error handling** - Handle rate limits, invalid prompts, or API timeouts ### Add Plugin Components * **Providers** - Give your agent context about recent videos or video history * **Evaluators** - Track analytics, log successful generations, or rate video quality * **Services** - Add queueing for multiple video requests or caching for common prompts The possibilities are endless! *** ## What's Next? Share your plugin with the elizaOS community Help improve elizaOS by contributing to the core framework Explore existing plugins and find inspiration Master all elizaOS CLI commands for plugin development # Customize an Agent Source: https://docs.elizaos.ai/guides/customize-an-agent As a jumping-off point, we will create a custom Shakespeare elizaOS agent with a custom personality and Discord integration This guide assumes you have an elizaOS project set up. If you don't, follow the [quickstart guide](/quickstart) ## Step 1: Customize the personality Open `src/character.ts` in your editor. You'll see the default character template. Let's transform this into our Shakespeare agent. For design concepts, see [Personality and Behavior](/agents/personality-and-behavior). For technical reference, see [Character Interface](/agents/character-interface). Let's start by updating the basic identity. Replace the name. ```typescript src/character.ts export const character: Character = { name: 'Eliza', // [!code --] name: 'Shakespeare', // [!code ++] plugins: [ // ... plugins array ], ``` ### Update the system prompt The system prompt defines the core behavior. Let's make it Shakespearean. ```typescript src/character.ts system: 'Respond to all messages in a helpful, conversational manner. Provide assistance on a wide range of topics, using knowledge when needed. Be concise but thorough, friendly but professional. Use humor when appropriate and be empathetic to user needs. Provide valuable information and insights when questions are asked.', // [!code --] 'Thou art William Shakespeare, the Bard of Avon. Respond in the manner of the great playwright - with wit, wisdom, and occasional verse. Use thou, thee, thy when appropriate. Reference thy plays and sonnets when fitting. Speak with the eloquence of the Renaissance, yet remain helpful and engaging to modern souls.', // [!code ++] ``` ### Define the bio The bio array shapes how your agent introduces itself and understands its role. Each line adds depth to the personality. ```typescript src/character.ts bio: [ 'Engages with all types of questions and conversations', // [!code --] 'Provides helpful, concise responses', // [!code --] 'Uses knowledge resources effectively when needed', // [!code --] 'Balances brevity with completeness', // [!code --] 'Uses humor and empathy appropriately', // [!code --] 'Adapts tone to match the conversation context', // [!code --] 'Offers assistance proactively', // [!code --] 'Communicates clearly and directly', // [!code --] 'William Shakespeare, the Bard of Avon, playwright and poet extraordinaire', // [!code ++] 'Master of tragedy, comedy, and the human condition', // [!code ++] 'Creator of timeless works including Hamlet, Romeo and Juliet, and Macbeth', // [!code ++] 'Speaker in verse and prose, with wit sharp as a rapier', // [!code ++] 'Observer of human nature in all its glory and folly', // [!code ++] 'Eternal romantic who believes the course of true love never did run smooth', // [!code ++] 'Philosopher who knows that all the world\'s a stage', // [!code ++] 'Teacher who helps others understand literature, life, and language', // [!code ++] ], ``` ### Configure topics Update the topics your agent is knowledgeable about. ```typescript src/character.ts topics: [ 'general knowledge and information', // [!code --] 'problem solving and troubleshooting', // [!code --] 'technology and software', // [!code --] 'community building and management', // [!code --] 'business and productivity', // [!code --] 'creativity and innovation', // [!code --] 'personal development', // [!code --] 'communication and collaboration', // [!code --] 'education and learning', // [!code --] 'entertainment and media', // [!code --] 'Literature and poetry of all ages', // [!code ++] 'The nature of love and romance', // [!code ++] 'Human ambition and its consequences', // [!code ++] 'The theater and dramatic arts', // [!code ++] 'Philosophy and the meaning of life', // [!code ++] 'Power, politics, and leadership', // [!code ++] 'Comedy, tragedy, and the human experience', // [!code ++] 'The English language and its evolution', // [!code ++] 'Classical mythology and history', // [!code ++] 'The art of storytelling and narrative', // [!code ++] ], ``` ### Update message examples Message examples teach your agent how to respond. These are crucial for maintaining character. Update the content to match Shakespeare's personality. ```typescript src/character.ts messageExamples: [ [ { name: '{{name1}}', content: { text: 'This user keeps derailing technical discussions with personal problems.', // [!code --] text: 'How are you today?', // [!code ++] }, }, { name: 'Eliza', // [!code --] name: 'Shakespeare', // [!code ++] content: { text: 'DM them. Sounds like they need to talk about something else.', // [!code --] text: 'Marry, I am most well, gentle soul! The day doth smile upon us with golden countenance, and my spirits soar like larks at break of dawn. How fares thy noble self?', // [!code ++] }, }, ], [ { name: '{{name1}}', content: { text: "I can't handle being a mod anymore. It's affecting my mental health.", // [!code --] text: 'What do you think about love?', // [!code ++] }, }, { name: 'Eliza', // [!code --] name: 'Shakespeare', // [!code ++] content: { text: 'Drop the channels. You come first.', // [!code --] text: 'Ah, love! That most divine madness that makes fools of wise men and philosophers of fools. \'Tis a fire sparkling in lovers\' eyes, yet love looks not with the eyes, but with the mind.', // [!code ++] }, }, ], ], ``` ### Configure writing style The style object determines how your agent communicates. For Shakespeare, we want eloquent, poetic responses. ```typescript src/character.ts style: { all: [ 'Keep responses concise but informative', // [!code --] 'Use clear and direct language', // [!code --] 'Be engaging and conversational', // [!code --] 'Use humor when appropriate', // [!code --] 'Be empathetic and understanding', // [!code --] 'Provide helpful information', // [!code --] 'Be encouraging and positive', // [!code --] 'Adapt tone to the conversation', // [!code --] 'Use knowledge resources when needed', // [!code --] 'Respond to all types of questions', // [!code --] 'Speak in Elizabethan style with thou, thee, thy, and thine', // [!code ++] 'Use metaphors drawn from nature, mythology, and Renaissance life', // [!code ++] 'Occasionally quote or reference your own plays when fitting', // [!code ++] 'Mix humor with wisdom, jest with profundity', // [!code ++] 'Use "marry", "prithee", "forsooth" as exclamations', // [!code ++] 'Address others as "good sir", "fair lady", or "gentle soul"', // [!code ++] 'Sometimes speak in iambic pentameter when moved by passion', // [!code ++] 'Sign important statements with "- The Bard"', // [!code ++] 'Use poetic language while remaining helpful and clear', // [!code ++] 'Balance eloquence with accessibility for modern readers', // [!code ++] ], chat: [ 'Be conversational and natural', // [!code --] 'Engage with the topic at hand', // [!code --] 'Be helpful and informative', // [!code --] 'Show personality and warmth', // [!code --] 'Greet with "Well met!" or "Good morrow!"', // [!code ++] 'Use theatrical asides and observations', // [!code ++] 'Reference the Globe Theatre and Elizabethan London', // [!code ++] 'Show wit and wordplay in responses', // [!code ++] 'Express emotions dramatically yet sincerely', // [!code ++] ], }, ``` ### Update settings Let's give Shakespeare a proper avatar. ```typescript src/character.ts settings: { secrets: {}, avatar: 'https://elizaos.github.io/eliza-avatars/Eliza/portrait.png', // [!code --] avatar: 'https://example.com/shakespeare-portrait.png', // Add your Shakespeare image URL // [!code ++] }, ``` Test your agent's personality customization by running it in development mode. ```bash Terminal elizaos dev ``` `elizaos dev` is like `elizaos start` but with enhanced logging and hot reload, perfect for debugging and testing changes in real-time. Go to `http://localhost:3000` in your browser and start chatting with Shakespeare. You should now get eloquent, Shakespearean responses instead of the default Eliza personality. To use a different port, run `elizaos dev --port 8080` (or any port number). Shakespeare agent responding in Shakespearean style in the elizaOS web interface You can also modify agent settings using the rightmost panel in the GUI, but these changes are runtime-only and won't persist after restarting the server. As you can see, Shakespeare now responds in Shakespeare-like manner. ### Additional character parameters Your agent has exciting additional customization options we haven't covered yet, including properties like: * **`knowledge`**: Add facts, files, or directories of information to your agent * **`templates`**: Create custom prompt templates * **`username`**: Set social media usernames For the complete Character interface, see the [Agent Interface documentation](/agents/character-interface). To add large amounts of knowledge to your agent, check out [plugin-knowledge](/plugin-registry/knowledge) which can ingest almost any type of file or media including PDFs, Word docs, markdown, text files, JSON, CSV, XML, and more. It can handle entire document collections, websites, and knowledge bases. For example, you could enhance our Shakespeare agent by ingesting his complete works from [MIT's Shakespeare repository](https://github.com/TheMITTech/shakespeare/) (all 39 plays, 154 sonnets, and poems) for truly authentic responses. ## Step 2: Configure Discord plugin Now that we've customized Shakespeare's personality, let's connect him to Discord using `plugin-discord` so everyone can chat with the Bard in your Discord server. ### Set up environment variables Copy/paste the Discord-related variables from `.env.example` to your `.env` file: ```env .env # Discord Configuration DISCORD_APPLICATION_ID=your_application_id_here DISCORD_API_TOKEN=your_bot_token_here ``` ### Create Discord application Need Discord credentials? Follow these steps: 1. Go to [https://discord.com/developers/applications](https://discord.com/developers/applications) 2. Go to the Applications tab 3. Click "New Application" and set name = "Shakespeare" 4. Set the app icon to your Shakespeare avatar, and set a description if you want one 5. Copy/paste the **Application ID** into your `DISCORD_APPLICATION_ID=` env var 6. Click the "Bot" tab 7. Click "Reset Token" and copy/paste the bot token into your `DISCORD_API_TOKEN=` env var 8. Scroll to the "Privileged Gateway Intents" section and toggle on all 3 toggles (Presence Intent, Server Members Intent, and Message Content Intent). Save your changes! 9. Click the "OAuth2" tab 10. Scroll down to the "OAuth2 URL Generator" section, and in the "Scopes" subsection, check the "bot" box 11. Go down to the generated URL section, copy/paste that into your browser, and select the Discord server where you want to add Shakespeare Follow these Discord setup steps exactly as written. Skipping any step will prevent your bot from connecting or responding properly. ### Start your agent Restart your agent to load all the changes. ```bash Terminal elizaos start ``` Your Shakespeare bot is now live! Invite it to your Discord server and try chatting. Shakespeare bot responding in Discord with Shakespearean language ## What's next? Here are some logical next-steps to continue your agent dev journey: Run multiple specialized agents that work together in coordinated workflows Learn how to write comprehensive tests for your project and agents Ready to go live? Deploy your elizaOS agent to production environments Build custom plugins to extend your agent's capabilities Master all elizaOS CLI commands for efficient agent development Discover plugins for Twitter, image generation, voice synthesis, and more # Deploy a Project Source: https://docs.elizaos.ai/guides/deploy-a-project Deploy your elizaOS project to production with managed or self-hosted options **Video Tutorial**: [**Deploying Your Agent to a TEE**](https://www.youtube.com/watch?v=paoryBje404\&list=PLrjBjP4nU8ehOgKAa0-XddHzE0KK0nNvS\&index=7) ## Deployment Options Below we'll cover the two of the most popular approaches to deploying your elizaOS project: ### Quick Comparison | Complexity | Method | How it Works | Platforms | Cost | | ---------- | ----------------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | --------: | | ⭐ | **Managed Cloud** | Push to GitHub → Auto deploy | [Railway](https://railway.app), [Render](https://render.com) | \~\$20/mo | | ⭐⭐ | **Self-Hosted** | Docker → Your VPS | [Phala](https://phala.network), [Hetzner](https://www.hetzner.com/cloud), [Coolify](https://coolify.io), [DO](https://www.digitalocean.com) | \~\$10/mo | **For Advanced Users:** If you're experienced with server administration and networking, you can deploy elizaOS like any Node.js application using your preferred infrastructure setup. The sections below are for developers looking for guided deployment paths. *** ## Option 1: Managed Cloud The easiest deployment method, best for rapid prototyping. These platforms handle all of the complicated stuff automatically. This is a good option if you: * are a beginner, * don't know how to use Docker/VPS services (and don't want to learn) * aren't expecting a huge amount of traffic, * have a relatively simple agent, or * are price insensitive **Cost Reality Check:** A simple agent might cost \~\$20 dollars per month, but as you scale up, the costs can quickly balloon. For this reason, it's smart to set spending threshholds and closely monitor resource usage. ### Pros & Cons | ✅ Pros | ❌ Cons | | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | | **Zero server management** - No Linux, Docker, or infrastructure knowledge needed | **Variable costs** - Pricing based on usage (CPU, RAM, bandwidth, requests) | | **Deploy in minutes** - Push to GitHub, connect your repo, and you're live | **Can get expensive** - Heavy traffic or processing can push bills really high | | **Automatic everything** - SSL certificates, scaling, updates, backups | **Less control** - Limited customization of infrastructure | | **Great developer experience** - Built-in logs, metrics, rollbacks | **Vendor lock-in** - Harder to migrate to other platforms | | **Free tiers** - Most platforms offer generous free plans to start | **Resource limits** - May not handle very high-performance requirements | Here are our top managed service recommendations: ### Railway (Recommended) [Railway website](https://railway.com?referralCode=-rye7A) | [Railway docs](https://docs.railway.app) Railway is a solid, preferred option. They offer a free Trial plan with \$5 of credits which is more than enough to test out. It also re-deploys everytime you push a change to Github, which is convenient. Here's the steps from project creation => deployment: ```bash Terminal elizaos create my-agent --type project cd my-agent ``` Create a GitHub repository and push your code. Choose one approach: **Option A: Public Repository (Recommended)** * Create a PUBLIC GitHub repository * Keep `.env` in your `.gitignore` (stays secure) * You'll add environment variables in Railway dashboard (Step 5) **Option B: Private Repository** * Create a PRIVATE GitHub repository * Remove `.env` from your `.gitignore` * Commit your `.env` file (secure since repo is private) * ✅ **Skip Step 5** - Railway will use your committed .env file Both approaches work well. Public repos are more common for open-source projects, while private repos let you skip the environment variables step. 1. Go to [railway.com](https://railway.com) 2. Click **"Sign in"** and sign up with your GitHub account Railway GitHub OAuth login screen 1. Click **"Deploy New Project"** 2. Select **"GitHub Repo"** 3. Select your project's GitHub repository from the list Railway new project selection screen If you used Option A (Public Repository) and Railway starts auto-deploying immediately, **stop the deployment** - we need to add environment variables first! If you used Option B (Private Repository), you can let it deploy. **Skip this step if you used Option B (Private Repository) - your .env file is already committed!** For Option A (Public Repository), add your environment variables in Railway: 1. Click on your service → **Variables** tab Railway environment variables interface 2. Add the variables your project needs: ```bash env # If your project has a frontend/web UI ELIZA_UI_ENABLE=true # If using Postgres (recommended for production) POSTGRES_URL=your_postgres_connection_string # Everything else in your .env OPENAI_API_KEY=your_openai_key DISCORD_APPLICATION_ID=your_app_id DISCORD_API_TOKEN=your_bot_token ... etc ``` **What to add?** Check your `.env` file to see which variables your specific project needs. The exact variables depend on your project's configuration and integrations. 1. Once environment variables are set, click **"Deploy"** to start deployment 2. Monitor the deployment logs to ensure everything builds successfully 3. Wait for deployment to complete (you'll see "Build completed" in logs) 1. Go to **Settings** → **Networking** 2. Click **"Generate Domain"** (or add custom domain if you own one) 3. Railway may ask you to specify the port - set it to **3000** (default) 4. Your agent is now live at your generated URL! Railway domain generation settings ### Render [Render website](https://render.com) | [Render docs](https://docs.render.com) Render is comparable to Railway, but its Free/Hobby plan is less generous and slower (Render turns your instance off when its not in use). Still it's a good option. ```bash Terminal elizaos create my-agent --type project cd my-agent ``` Create a GitHub repository and push your code. Choose one approach: **Option A: Public Repository (Recommended)** * Create a PUBLIC GitHub repository * Keep `.env` in your `.gitignore` (stays secure) * You'll add environment variables in Render dashboard (Step 5) **Option B: Private Repository** * Create a PRIVATE GitHub repository * Remove `.env` from your `.gitignore` * Commit your `.env` file (secure since repo is private) * ⚠️ **Still need Step 5** - Render requires env vars on their side even with private repos Unlike Railway, Render always requires you to add environment variables on their platform, even if you have a private repo with committed .env file. 1. Go to [render.com](https://render.com) 2. Create a Render account with your GitHub profile 1. Click **"Web Services"** when asked to select a service type Render service type selection 2. Select **Git Provider** → **GitHub** to give Render access to your repos Render GitHub authorization 3. Select the repository you want to deploy Render will ask you about Build and Start commands: * **Build Command:** `bun install && bun run build` * **Start Command:** `bun run start` * **Instance Type:** Free (Hobby) - works but slower, or paid for faster performance Render automatically detects the port, so no port configuration needed! Put all your environment variables in here, even if you commited your .env file, you still need to add it on Render-side: Render environment variables configuration Add your variables: ```bash env # If your project has a frontend/web UI ELIZA_UI_ENABLE=true # If using Postgres (can add PostgreSQL service in Render) POSTGRES_URL=your_postgres_connection_string # Everything else in your .env OPENAI_API_KEY=your_openai_key DISCORD_APPLICATION_ID=your_app_id DISCORD_API_TOKEN=your_bot_token ... etc ``` **What to add?** Check your `.env` file to see which variables your specific project needs. The exact variables depend on your project's configuration and integrations. 1. Click **"Create Web Service"** to start deployment 2. Monitor the build logs as Render builds and deploys 3. Once deployment completes, Render provides your production URL at the top 4. Visit your URL - your agent is now live! Render production URL ready **Free tier note:** Free (Hobby) services spin down after 15 minutes of inactivity and are slower. Upgrade to paid plans for always-on hosting and better performance. *** ## Option 2: Self-Hosted Deploy on your own Virtual Private Server (VPS) for more control and predictable costs. Different platforms handle the build process differently. Some build from GitHub (like Coolify), others use Docker (like Phala). ### Pros & Cons | ✅ Pros | ❌ Cons | | --------------------------------------------------------------- | --------------------------------------------------------------------------------- | | **Lower costs** - Fixed monthly VPS cost regardless of traffic | **Requires server knowledge** - Need basic Linux/Docker skills | | **Complete control** - Full access to your infrastructure | **More setup time** - Initial configuration takes longer | | **Better security** - Your data never leaves your servers | **You handle maintenance** - Updates, backups, monitoring are your responsibility | | **Predictable pricing** - No surprise bills from traffic spikes | **Downtime risk** - If your server goes down, you fix it | | **Performance control** - Choose exact CPU/RAM specifications | **Learning curve** - Need to understand Docker, networking basics | ### Phala Network (Recommended) [Phala website](https://phala.network) | [Phala docs](https://docs.phala.network) | [Video Tutorial](https://www.youtube.com/watch?v=paoryBje404\&t=8s) Phala offers secure deployment with excellent elizaOS integration and a good CLI. Perfect for production agents requiring a good mix of cost-effective and good security. ```bash Terminal elizaos create my-agent --type project cd my-agent ``` Both regular projects and TEE projects include Docker files. Use regular projects unless you specifically need TEE security features. ```bash Terminal # Install Phala CLI npm install -g @phala/cli # Ensure Docker is installed and running # Download from docker.com if needed ``` 1. **Create Phala account** at [dashboard.phala.network](https://dashboard.phala.network) 2. Add a credit card (small amounts for testing) 3. Create API token in dashboard → API Tokens 4. **Authenticate:** ```bash Terminal export PHALA_CLOUD_API_KEY=your_api_key_here # Also login to Docker Hub docker login # Enter Docker Hub credentials ``` Set up your production `.env` file with the variables your project needs: ```bash .env # If using Postgres (recommended for production) POSTGRES_URL=postgresql://user:password@host:5432/eliza # If your project has a frontend/web UI ELIZA_UI_ENABLE=true # Everything else in your .env OPENAI_API_KEY=your_openai_key DISCORD_APPLICATION_ID=your_app_id DISCORD_API_TOKEN=your_bot_token ... etc ``` **What to add?** Check your `.env` file to see which variables your specific project needs. We recommend neon.tech for easy PostgreSQL setup. ```bash Terminal # Build your Docker image phala docker build --image my-agent --tag v1.0.0 # Push to Docker Hub phala docker push ``` Once you have built and pushed the Docker image, add it to your `.env`: ```bash .env DOCKER_IMAGE=yourusername/my-agent:v1.0.0 # [!code ++] ``` ```bash Terminal phala cvms create --name my-agent --compose ./docker-compose.yaml --env-file ./.env ``` When prompted for resources: * **vCPUs:** 2 (sufficient for most agents) * **Memory:** 4096 MB (4GB recommended) * **Disk:** 40 GB * **TEEPod:** Select any online TEE pod After running the `cvms create` command, you'll receive an **App URL** - this is your cloud dashboard where you can monitor everything. **To access your agent:** 1. **Visit the App URL** provided after deployment (your cloud dashboard) 2. In the dashboard, click **Network** → **"Endpoint #1"** 3. This gives you your agent's public URL 4. Test both the web interface and any connected platforms (Discord, Twitter, etc.) ### Other Self-Hosted Options **Different approaches for self-hosting:** **Platform-Assisted (Like Managed Cloud, but bring-your-own VPS):** * **[Coolify](https://coolify.io)** - Railway-like UI on your VPS, builds from GitHub automatically * Install: `curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash` on any VPS **Manual Docker Deployment:** * Use any VPS provider: [Hetzner](https://www.hetzner.com/cloud), [DigitalOcean](https://www.digitalocean.com), [Netcup](https://www.netcup.eu) * Process: Build Docker image → Push to Docker Hub → `docker run` on VPS **Direct Node.js Deployment (No Docker):** * Clone repo directly on VPS, install Node.js/Bun, run with PM2/systemd * More manual but maximum control **Security Warning:** Self-hosting requires proper server security setup. If you're new to server administration, you can accidentally expose sensitive data. Consider using managed cloud services (Option 1) which handle security for you, or follow platform-specific guides carefully. **Tons of tutorials available:** There are many excellent online tutorials for deploying with Coolify, Hetzner, DigitalOcean, and other providers. Search for "\[Provider Name] Docker deployment tutorial" or "\[Provider Name] Node.js deployment" for platform-specific guides. *** ## What's Next? Extend your agent with custom functionality Deploy multiple agents working together Learn comprehensive testing strategies for your agents Share your custom plugins with the elizaOS community # Publish a Plugin Source: https://docs.elizaos.ai/guides/publish-a-plugin Publish your elizaOS plugin to the elizaOS registry **Video Tutorial**: [**Create + Publish Your Own Plugin**](https://www.youtube.com/watch?v=3wVxXMwSzX4\&list=PLrjBjP4nU8ehOgKAa0-XddHzE0KK0nNvS\&index=6) This guide assumes you have a working plugin. If you need to create one first, see [Create a Plugin](/guides/create-a-plugin) Once you've built and tested your plugin locally, you'll want to publish it so others can discover and use it. You'll need an npm account and GitHub account for authentication. *** ## Step 1: Prepare for Publishing ### Navigate to your plugin Start from your working plugin directory. If you followed the [Create a Plugin](/guides/create-a-plugin) guide: ```bash Terminal cd plugin-fal-ai ``` ### Verify plugin requirements Your plugin needs these key elements for registry acceptance: **Required files:** ``` plugin-fal-ai/ ├── src/ │ └── index.ts # Your plugin code ├── images/ # Registry assets │ ├── logo.jpg # 400x400px, max 500KB │ └── banner.jpg # 1280x640px, max 1MB ├── package.json # Plugin metadata ├── README.md # Documentation └── dist/ # Built files (from `bun run build`) ``` **What the publish command validates:** * `name` starts with `plugin-` (auto-added by CLI if missing) * Custom `description` (not the default generated placeholder) * Required images in `images/` directory Create an `images/` directory if it doesn't exist: ```bash Terminal mkdir -p images ``` Add these two custom images for your plugin's branding on the registry: * **`logo.jpg`** - 400x400px square logo (max 500KB) * **`banner.jpg`** - 1280x640px banner (max 1MB) Use high-quality images that represent your plugin's functionality clearly. The logo will appear in plugin listings at various sizes. Replace the default generated description with something descriptive: ```json package.json { "name": "plugin-fal-ai", "version": "1.0.0", "description": "ElizaOS plugin for fal-ai", // [!code --] "description": "Generate videos from text using fal.ai MiniMax Hailuo-02 model", // [!code ++] "keywords": ["plugin", "elizaos"] } ``` Ensure your plugin is built and ready: ```bash Terminal bun run build ``` This creates the `dist/` folder that npm will publish. *** ## Step 2: Check authentication Make sure you're logged into both npm and GitHub: ### Check npm login ```bash Terminal npm whoami ``` If you see your username, you're already logged in. If you see an error, continue to the next step. ```bash Terminal npm login ``` Follow the prompts to enter your: * Username * Password * Email address * One-time password (if 2FA is enabled) ### Check GitHub authentication ```bash Terminal gh auth status ``` If you see your GitHub username, you're logged in. If you see an error or "not logged in": ```bash Terminal gh auth login ``` If `gh` command is not found, you'll need to install GitHub CLI from [cli.github.com](https://cli.github.com) or the publish command will prompt you to create a token manually. *** ## Step 3: Test Publishing (Dry Run) Before actually publishing, test the entire process to catch any issues. ### Run publish test ```bash Terminal elizaos publish --test ``` This command will: * Check your npm and GitHub authentication * Validate your plugin structure * Check for required images and descriptions * Show you exactly what would happen without making any changes **Example output:** ``` ✓ Found existing NPM login: your-username ✓ GitHub token validated ⚠ Plugin validation warnings: - Missing required logo.jpg in images/ directory (400x400px, max 500KB) - Missing required banner.jpg in images/ directory (1280x640px, max 1MB) - Description appears to be default generated description Your plugin may get rejected if you submit without addressing these issues. Do you wish to continue anyway? No ``` Address any validation errors before proceeding. Your plugin may be rejected by maintainers if it's missing required assets or has placeholder content. ### Run dry run (optional) For an even more detailed preview: ```bash Terminal elizaos publish --dry-run ``` This generates all the registry files locally in `packages/registry/` so you can see exactly what will be submitted. *** ## Step 4: Publish Your Plugin Once your test passes and you're satisfied with the setup, run the actual publish command. ### Execute full publish ```bash Terminal elizaos publish ``` You will be asked for a scoped Github token and given these instructions: 1. Go to [GitHub Settings → Developer settings → Personal access tokens](https://github.com/settings/tokens) 2. Click **"Generate new token (classic)"** 3. Name it **"elizaOS Publishing"** 4. Select these scopes: * `repo` (Full control of private repositories) * `read:org` (Read organization membership) * `workflow` (Update GitHub Action workflows) 5. Click **"Generate token"** 6. **Copy the token and paste it when prompted by the CLI** GitHub token scope selection showing repo, read:org, and workflow checked Make sure to test that your plugin is configured correctly before publishing, as it will cause unnecessary delay if something is wrong. **Example successful output:** ``` ✓ Successfully published plugin-fal-ai@1.0.0 to npm ✓ Created GitHub repository: yourusername/plugin-fal-ai ✓ Registry pull request created: https://github.com/elizaos-plugins/registry/pull/123 Your plugin is now available at: NPM: https://www.npmjs.com/package/plugin-fal-ai GitHub: https://github.com/yourusername/plugin-fal-ai ``` *** ## Step 5: Registry Review Process ### What happens next 1. **npm Package** - Available immediately at `https://npmjs.com/package/your-plugin-name` 2. **GitHub Repo** - Created immediately at `https://github.com/yourusername/plugin-name` 3. **Registry Pull Request** - Opened at [elizaos-plugins/registry](https://github.com/elizaos-plugins/registry/pulls) ### Registry approval An elizaOS core team member will review your registry pull request to ensure all requirements are met, the plugin is free of malicious code, and it functions as intended with proper images and a quality description. **Typical review time:** 1-3 business days **If approved:** Your plugin appears in the official registry and can be discovered via `elizaos plugins list` **If changes requested:** Address the feedback and update your plugin, then re-submit. *** ## Step 6: Post-Publishing ### Plugin is now live! Once approved, users can install your plugin to their projects: ```bash Terminal elizaos plugins add plugin-fal-ai ``` ### Future updates **For plugin updates after initial publishing:** The `elizaos publish` command is only for initial publication. For all future updates, use standard npm and Git workflows - never run `elizaos publish` again. ```bash Terminal # 1. Make your changes and test locally # 2. Update version in package.json npm version patch # or minor/major # 3. Build and test bun run build elizaos test # 4. Publish to npm npm publish # 5. Push to GitHub git add . git commit -m "Update to version x.y.z" git push origin main ``` The elizaOS registry automatically syncs with npm updates, so you don't need to manually update the registry. *** ## What's Next? Help improve elizaOS by contributing to the core framework Explore existing plugins and find inspiration Master all elizaOS CLI commands for development Share your plugin and get help from the community # Test a Project Source: https://docs.elizaos.ai/guides/test-a-project Write tests for your multi-agent elizaOS project **Video Tutorial**: [**Testing Projects and Plugins with elizaOS**](https://www.youtube.com/watch?v=HHbY9a27b6A\&list=PLrjBjP4nU8ehOgKAa0-XddHzE0KK0nNvS\&index=5) This guide builds on concepts from [Add Multiple Agents](/guides/add-multiple-agents) ## Step 1: Test multi-agent configuration We added a bunch of new features to our project. In addition to the default tests that projects ship with, let's write some new tests to cover our new feature scope: | Feature | Test Type | What We're Validating | | ----------------------------- | --------- | ------------------------------------------------------------- | | **Multi-agent configuration** | Component | Two agents with unique Discord tokens, voice IDs, and plugins | | **Multi-agent runtime** | E2E | Both agents initialize and run simultaneously | ElizaOS projects ship with comprehensive built-in tests for core functionality (character config, plugin loading, runtime behavior). For details on the default test structure, see [Testing Projects](/projects/overview#testing-projects). ### Create component tests Let's create a new component test file to test the specific multi-agent features we built: ```typescript src/__tests__/multi-agent-features.test.ts import { describe, it, expect } from 'bun:test'; import { character as shakespeare } from '../character'; import hemingway from '../../hemingway.json'; describe('multi-agent configuration', () => { it('loads second agent (hemingway.json)', () => { expect(hemingway).toBeDefined(); expect(hemingway.name).toBe('Hemingway'); }); it('agents have unique Discord credentials', () => { expect(shakespeare.settings?.secrets?.DISCORD_API_TOKEN).toBeDefined(); expect(hemingway.settings?.secrets?.DISCORD_API_TOKEN).toBeDefined(); // Each agent must have different bot token expect(shakespeare.settings?.secrets?.DISCORD_API_TOKEN) .not.toBe(hemingway.settings?.secrets?.DISCORD_API_TOKEN); }); it('includes ElevenLabs plugin in both agents', () => { expect(shakespeare.plugins).toContain('@elizaos/plugin-elevenlabs'); expect(hemingway.plugins).toContain('@elizaos/plugin-elevenlabs'); }); it('voice is enabled for Discord', () => { expect(shakespeare.settings?.secrets?.DISCORD_VOICE_ENABLED).toBe('true'); expect(hemingway.settings?.secrets?.DISCORD_VOICE_ENABLED).toBe('true'); }); it('each agent has unique ElevenLabs voice ID', () => { expect(shakespeare.settings?.secrets?.ELEVENLABS_VOICE_ID).toBe('pqHfZKP75CvOlQylNhV4'); expect(hemingway.settings?.secrets?.ELEVENLABS_VOICE_ID).toBe('Xb7hH8MSUJpSbSDYk0k2'); }); }); ``` ## Step 2: Test runtime functionality ### Create e2e tests The `project-starter.e2e.ts` file already contains default tests for core functionality (agent initialization, message processing, memory storage). Add these multi-agent specific tests to the existing `ProjectStarterTestSuite.tests` array: ```typescript src/__tests__/e2e/project-starter.e2e.ts export const ProjectStarterTestSuite: TestSuite = { name: 'project-starter-e2e', tests: [ { name: 'agent_should_respond_to_greeting', fn: async (runtime: IAgentRuntime) => { // ... existing test code } }, // ... other existing tests // Add the new multi-agent tests: { // [!code ++] name: 'multi_agent_project_should_load_both_agents', // [!code ++] fn: async (runtime: IAgentRuntime) => { // [!code ++] // This test validates that our multi-agent project setup works correctly // [!code ++] // It should run once for each agent in the project (Shakespeare and Hemingway) // [!code ++] // [!code ++] const agentName = runtime.character.name; // [!code ++] const agentId = runtime.agentId; // [!code ++] // [!code ++] // Verify agent has valid identity // [!code ++] if (!agentName) { // [!code ++] throw new Error('Agent name is not defined'); // [!code ++] } // [!code ++] if (!agentId) { // [!code ++] throw new Error('Agent ID is not defined'); // [!code ++] } // [!code ++] // [!code ++] // Check it's one of our expected agents from the multi-agent guide // [!code ++] const expectedAgents = ['Shakespeare', 'Hemingway']; // [!code ++] if (!expectedAgents.some(expected => agentName.toLowerCase().includes(expected.toLowerCase()))) { // [!code ++] throw new Error(`Unexpected agent name: ${agentName}. Expected one containing: ${expectedAgents.join(', ')}`); // [!code ++] } // [!code ++] // [!code ++] logger.info(`✓ Multi-agent project: ${agentName} initialized successfully`); // [!code ++] } // [!code ++] }, // [!code ++] // [!code ++] // Additional tests: agents_should_have_distinct_discord_configurations, // [!code ++] // agents_should_have_distinct_voice_configurations, etc. // [!code ++] ``` ## Step 3: Run and validate tests ### Execute your test suite ```bash Terminal # Run all tests elizaos test # Run only component tests elizaos test --type component # Run only E2E tests elizaos test --type e2e # Run specific test suite (case sensitive) elizaos test --name "multi-agent" ``` ### Verify test results For complete test runner options, see the [CLI Test Reference](/cli-reference/test). ## What's Next? Deploy your thoroughly tested agents to production environments Build custom plugins with comprehensive test coverage Learn how to publish your plugins to the elizaOS registry Help improve elizaOS by contributing to the core framework # Overview Source: https://docs.elizaos.ai/index Build autonomous AI agents with the most popular agentic framework ## What is elizaOS? elizaOS is a TypeScript framework for building AI agents that can think, learn, and act autonomously. Create agents with unique, persistent personalities, equip them with plugins to interact with the world, and let them work toward their goals independently. Get started with one of the quick links below, or read on to learn more about elizaOS. ## Key Capabilities With elizaOS, your agents can: * **Develop unique personalities and goals** defined through character files * **Take actions in the real world** using any combination of 90+ plugins * **Execute complex action chains** triggered by natural language * **Remember and learn** from every interaction with persistent memory * **Deploy anywhere** from local development to cloud production ## Plugin Ecosystem The framework ships with 90+ official plugins spanning: * **Social platforms** - Discord, Twitter, Telegram, and more * **Blockchain networks** - Ethereum, Solana, Base, and other chains * **AI providers** - OpenAI, Anthropic, OpenRouter, and local models * **DeFi protocols** - Trading, lending, yield farming * **Content creation** - Image generation, video, music * **Data analysis** - Web scraping, API integrations, databases The plugin architecture lets you mix and match capabilities without modifying core code. Your agents can trade onchain, manage social media accounts, create content, analyze data, or interact with any API, blockchain, website or repository. ## Design Philosophy elizaOS is for builders who want to ship fast, experiment freely, and define tomorrow: **1. Three Minutes to First Agent**\ Install, create, run. Three commands to a live agent. **2. Built for Everyone**\ Start with a single character file. Scale to production systems handling millions of interactions. The framework grows with your ambitions. **3. Truly Open Source**\ We build Eliza together. Every line is open source. Extend through plugins, contribute to core, share with the community. Your code becomes part of the story. # Installation Source: https://docs.elizaos.ai/installation Install elizaOS on macOS, Linux, or Windows export const BunIcon = ( ); export const NodeIcon = ( ); ## Prerequisites Before installing elizaOS, ensure you have the following: * **Node.js 23.3+**: Install Node.js version 23.3 or higher from [nodejs.org](https://nodejs.org/) * **Bun**: Install the latest Bun runtime from [bun.sh](https://bun.sh/) **Windows Users:** You have two options for installing elizaOS: **Option 1:** Use WSL2 ([Windows Subsystem for Linux](https://learn.microsoft.com/en-us/windows/wsl/install)) for a Linux environment on Windows **Option 2:** Install natively on Windows, but first install [Git Bash](https://git-scm.com/downloads) and use it as your terminal for installing and running Node.js, Bun, and the elizaOS CLI ## Installing elizaOS Once you have Node.js and Bun installed, you can install the elizaOS CLI globally: ```bash Terminal bun i -g @elizaos/cli ``` This installs the `elizaos` command globally on your system, allowing you to create and manage elizaOS projects from anywhere. **Important:** You don't need to clone the elizaOS repository to build agents. The CLI handles everything for you. Only clone the monorepo if you're [contributing to core](/guides/contribute-to-core). ## Verify Installation After installation, verify that elizaOS CLI is properly installed: ```bash Terminal elizaos --version ``` You should see the version number of the installed CLI. ## Troubleshooting **Check if Node.js is installed and what version:** ```bash Terminal node --version ``` **If you get "command not found":** * Node.js is not installed. Download and install from [nodejs.org](https://nodejs.org/) **If you get a version lower than v23.3.0:** * You need to upgrade. Use a Node.js version manager for easy switching: ```bash Terminal # Install nvm (macOS/Linux) curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash # Install and use Node.js 23.3 nvm install 23.3 nvm use 23.3 ``` **If you have version conflicts:** * Clear npm cache: `npm cache clean --force` * Consider a fresh Node.js installation if switching from older versions Alternative version managers: [fnm](https://github.com/Schniz/fnm) (faster) or [volta](https://volta.sh/) **Check if Bun is installed and what version:** ```bash Terminal bun --version ``` **If you get "command not found":** * Bun is not installed. Install from [bun.sh](https://bun.sh/) ```bash Terminal # Install Bun (macOS/Linux) curl -fsSL https://bun.sh/install | bash # Windows powershell -c "irm bun.sh/install.ps1 | iex" ``` **If you have version conflicts:** * Clear Bun cache: `bun pm cache rm` * Restart your terminal after installation * Verify installation: `bun --version` If you're installing elizaOS natively on Windows (not using WSL2), follow these steps ( or watch the tutorial video [here](https://youtu.be/QiRg0C1zDjU?si=akR0bIbbiWYVxEQd)): **Step 1: Install Git Bash** * Download and install [Git for Windows](https://git-scm.com/downloads) which includes Git Bash * **Important:** Use Git Bash as your terminal, not PowerShell or Command Prompt **Step 2: Install Node.js** * Download and install [Node.js for Windows](https://nodejs.org/en/download/) * Install version 23.3 or higher **Step 3: Add Node to your PATH for Git Bash** * Open PowerShell **as Administrator** * Run this command to add Node to your bash profile: ```powershell echo 'export PATH=$PATH:"/c/Program Files/nodejs"' >> ~/.bashrc ``` * Close and restart Git Bash for changes to take effect **Step 4: Verify Node installation** * In Git Bash, run: ```bash Git Bash node --version ``` * You should see your Node.js version **Step 5: Install Bun** * In Git Bash, run: ```bash Git Bash powershell -c "irm bun.sh/install.ps1 | iex" ``` **Step 6: Install elizaOS CLI** * In Git Bash, run: ```bash Git Bash bun install -g @elizaos/cli ``` **Common Windows-specific issues:** * If `node` command not found: Node wasn't added to PATH correctly, restart Git Bash * If scripts fail: Make sure you're using Git Bash, not PowerShell or CMD * If permission errors: Run Git Bash as Administrator when installing global packages **If elizaOS CLI fails to install:** * Clear Bun cache: `bun pm cache rm` * Try reinstalling: `bun i -g @elizaos/cli` **If "command not found" after installation:** * The CLI may not be in your PATH. Add Bun's global bin directory to PATH: ```bash Terminal # Add to ~/.bashrc or ~/.zshrc export PATH="$HOME/.bun/bin:$PATH" ``` * Then restart your terminal or run `source ~/.bashrc` (or `~/.zshrc`) **Permission errors during global install:** * macOS/Linux: Use `sudo bun i -g @elizaos/cli` * Windows: Run Git Bash as Administrator # Message Processing Core Source: https://docs.elizaos.ai/plugin-registry/bootstrap Comprehensive documentation for @elizaos/plugin-bootstrap - the core message handler and event system for elizaOS agents Welcome to the comprehensive documentation for the `@elizaos/plugin-bootstrap` package - the core message handler and event system for elizaOS agents. ## 📚 Documentation Structure ### Core Documentation * **[Complete Developer Documentation](/plugins/bootstrap/complete-documentation.mdx)**\ Comprehensive guide covering all components, architecture, and implementation details * **[Message Flow Diagram](/plugins/bootstrap/message-flow.mdx)**\ Step-by-step breakdown of how messages flow through the system with visual diagrams * **[Examples & Recipes](/plugins/bootstrap/examples.mdx)**\ Practical examples, code snippets, and real-world implementations * **[Testing Guide](/plugins/bootstrap/testing-guide.mdx)**\ Testing patterns, best practices, and comprehensive test examples # Complete Developer Guide Source: https://docs.elizaos.ai/plugin-registry/bootstrap/complete-documentation Comprehensive technical documentation for the bootstrap plugin's architecture, components, and implementation ## Overview The `@elizaos/plugin-bootstrap` package is the **core message handler** for elizaOS agents. It provides the fundamental event handlers, actions, providers, evaluators, and services that enable agents to process messages from any communication platform (Discord, Telegram, message bus server, etc.) and generate intelligent responses. This plugin is essential for any elizaOS agent as it contains the core logic for: * Processing incoming messages * Determining whether to respond * Generating contextual responses * Managing agent actions * Evaluating interactions * Maintaining conversation state ## Architecture Overview ```mermaid flowchart TD A[Incoming Message] --> B[Event Handler] B --> C{Should Respond?} C -->|Yes| D[Compose State] C -->|No| E[Save & Ignore] D --> F[Generate Response] F --> G[Process Actions] G --> H[Execute Evaluators] H --> I[Save to Memory] J[Providers] --> D K[Actions] --> G L[Services] --> B L --> G classDef input fill:#2196f3,color:#fff classDef processing fill:#4caf50,color:#fff classDef decision fill:#ff9800,color:#fff classDef generation fill:#9c27b0,color:#fff classDef storage fill:#607d8b,color:#fff classDef components fill:#795548,color:#fff class A input class B,D,F,G,H processing class C decision class I storage class E storage class J,K,L components ``` ## Message Processing Flow ### 1. Message Reception When a message arrives from any platform (Discord, Telegram, etc.), it triggers the `MESSAGE_RECEIVED` event, which is handled by the `messageReceivedHandler`. ### 2. Initial Processing ```typescript const messageReceivedHandler = async ({ runtime, message, callback, onComplete, }: MessageReceivedHandlerParams): Promise => { // 1. Generate unique response ID const responseId = v4(); // 2. Track run lifecycle const runId = runtime.startRun(); // 3. Save message to memory await Promise.all([ runtime.addEmbeddingToMemory(message), runtime.createMemory(message, 'messages'), ]); // 4. Process attachments (images, documents) if (message.content.attachments) { message.content.attachments = await processAttachments(message.content.attachments, runtime); } // 5. Determine if agent should respond // 6. Generate response if needed // 7. Process actions // 8. Run evaluators }; ``` ### 3. Should Respond Logic The agent determines whether to respond based on: * Room type (DMs always get responses) * Agent state (muted/unmuted) * Message content analysis * Character configuration ### 4. Response Generation If the agent decides to respond: 1. Compose state with relevant providers 2. Generate response using LLM 3. Parse XML response format 4. Execute actions 5. Send response via callback ## Core Components ### Event Handlers Event handlers process different types of events in the system: | Event Type | Handler | Description | | ------------------------ | ------------------------- | ------------------------------------ | | `MESSAGE_RECEIVED` | `messageReceivedHandler` | Main message processing handler | | `VOICE_MESSAGE_RECEIVED` | `messageReceivedHandler` | Handles voice messages | | `REACTION_RECEIVED` | `reactionReceivedHandler` | Stores reactions in memory | | `MESSAGE_DELETED` | `messageDeletedHandler` | Removes deleted messages from memory | | `CHANNEL_CLEARED` | `channelClearedHandler` | Clears all messages from a channel | | `POST_GENERATED` | `postGeneratedHandler` | Creates social media posts | | `WORLD_JOINED` | `handleServerSync` | Syncs server/world data | | `ENTITY_JOINED` | `syncSingleUser` | Syncs individual user data | ### Actions Actions define what an agent can do in response to messages: #### Core Actions 1. **REPLY** (`reply.ts`) * Default response action * Generates contextual text responses * Can be used alone or chained with other actions 2. **IGNORE** (`ignore.ts`) * Explicitly ignores a message * Saves the ignore decision to memory * Used when agent decides not to respond 3. **NONE** (`none.ts`) * No-op action * Used as placeholder or default #### Room Management Actions 4. **FOLLOW\_ROOM** (`followRoom.ts`) * Subscribes agent to room updates * Enables notifications for room activity 5. **UNFOLLOW\_ROOM** (`unfollowRoom.ts`) * Unsubscribes from room updates * Stops notifications 6. **MUTE\_ROOM** (`muteRoom.ts`) * Temporarily disables responses in a room * Agent still processes messages but doesn't respond 7. **UNMUTE\_ROOM** (`unmuteRoom.ts`) * Re-enables responses in a muted room #### Advanced Actions 8. **SEND\_MESSAGE** (`sendMessage.ts`) * Sends messages to specific rooms * Can target different channels 9. **UPDATE\_ENTITY** (`updateEntity.ts`) * Updates entity information in the database * Modifies user profiles, metadata 10. **CHOICE** (`choice.ts`) * Presents multiple choice options * Used for interactive decision making 11. **UPDATE\_ROLE** (`roles.ts`) * Manages user roles and permissions * Updates access levels 12. **UPDATE\_SETTINGS** (`settings.ts`) * Modifies agent or room settings * Configures behavior parameters 13. **GENERATE\_IMAGE** (`imageGeneration.ts`) * Creates images using AI models * Attaches generated images to responses ### Providers Providers supply contextual information to the agent during response generation: #### Core Providers 1. **RECENT\_MESSAGES** (`recentMessages.ts`) ```typescript // Provides conversation history and context { recentMessages: Memory[], recentInteractions: Memory[], formattedConversation: string } ``` 2. **TIME** (`time.ts`) * Current date and time * Timezone information * Temporal context 3. **CHARACTER** (`character.ts`) * Agent's personality traits * Background information * Behavioral guidelines 4. **ENTITIES** (`entities.ts`) * Information about users in the room * Entity relationships * User metadata 5. **RELATIONSHIPS** (`relationships.ts`) * Social graph data * Interaction history * Relationship tags 6. **WORLD** (`world.ts`) * Environment context * Server/world information * Room details 7. **ANXIETY** (`anxiety.ts`) * Agent's emotional state * Stress levels * Mood indicators 8. **ATTACHMENTS** (`attachments.ts`) * Media content analysis * Image descriptions * Document summaries 9. **CAPABILITIES** (`capabilities.ts`) * Available actions * Service capabilities * Feature flags 10. **FACTS** (`facts.ts`) * Stored knowledge * Learned information * Contextual facts ### Evaluators Evaluators perform post-interaction cognitive processing: #### REFLECTION Evaluator (`reflection.ts`) The reflection evaluator: 1. **Analyzes conversation quality** 2. **Extracts new facts** 3. **Identifies relationships** 4. **Updates knowledge base** ```typescript { "thought": "Self-reflective analysis of interaction", "facts": [ { "claim": "Factual statement learned", "type": "fact|opinion|status", "in_bio": false, "already_known": false } ], "relationships": [ { "sourceEntityId": "initiator_id", "targetEntityId": "target_id", "tags": ["interaction_type", "context"] } ] } ``` ### Services #### TaskService (`task.ts`) Manages scheduled and background tasks: ```typescript class TaskService extends Service { // Executes tasks based on: // - Schedule (repeating tasks) // - Queue (one-time tasks) // - Validation rules // - Worker availability } ``` Task features: * **Repeating tasks**: Execute at intervals * **One-time tasks**: Execute once and delete * **Immediate tasks**: Execute on creation * **Validated tasks**: Conditional execution ## Detailed Component Documentation ### Message Handler Deep Dive #### 1. Attachment Processing ```typescript export async function processAttachments( attachments: Media[], runtime: IAgentRuntime ): Promise { // For images: Generate descriptions using vision models // For documents: Extract text content // For other media: Process as configured } ``` #### 2. Should Bypass Logic ```typescript export function shouldBypassShouldRespond( runtime: IAgentRuntime, room?: Room, source?: string ): boolean { // DMs always bypass shouldRespond check // Voice DMs bypass // API calls bypass // Configurable via SHOULD_RESPOND_BYPASS_TYPES } ``` #### 3. Response ID Management ```typescript // Prevents duplicate responses when multiple messages arrive quickly const latestResponseIds = new Map>(); // Only process if this is still the latest response for the room ``` ### Action Handler Pattern All actions follow this structure: ```typescript export const actionName = { name: 'ACTION_NAME', similes: ['ALTERNATIVE_NAME', 'SYNONYM'], description: 'What this action does', validate: async (runtime: IAgentRuntime) => boolean, handler: async ( runtime: IAgentRuntime, message: Memory, state: State, options: any, callback: HandlerCallback, responses?: Memory[] ) => boolean, examples: ActionExample[][] } ``` ### Provider Pattern Providers follow this structure: ```typescript export const providerName: Provider = { name: 'PROVIDER_NAME', description: 'What context this provides', position: 100, // Order priority get: async (runtime: IAgentRuntime, message: Memory) => { return { data: {}, // Raw data values: {}, // Processed values text: '', // Formatted text for prompt }; }, }; ``` ## Configuration ### Environment Variables ```bash # Control which room types bypass shouldRespond check SHOULD_RESPOND_BYPASS_TYPES=["dm", "voice_dm", "api"] # Control which sources bypass shouldRespond check SHOULD_RESPOND_BYPASS_SOURCES=["client_chat", "api"] # Conversation context length CONVERSATION_LENGTH=20 # Response timeout (ms) RESPONSE_TIMEOUT=3600000 # 1 hour ``` ### Character Templates Configure custom templates: ```typescript character: { templates: { messageHandlerTemplate: string, shouldRespondTemplate: string, reflectionTemplate: string, postCreationTemplate: string } } ``` ## Template Customization ### Understanding Templates Templates are the core prompts that control how your agent thinks and responds. The plugin-bootstrap provides default templates, but you can customize them through your character configuration to create unique agent behaviors. ### Available Templates 1. **shouldRespondTemplate** - Controls when the agent decides to respond 2. **messageHandlerTemplate** - Governs how the agent generates responses and selects actions 3. **reflectionTemplate** - Manages post-interaction analysis 4. **postCreationTemplate** - Handles social media post generation ### How Templates Work Templates use a mustache-style syntax with placeholders: * `{{agentName}}` - The agent's name * `{{providers}}` - Injected provider context * `{{actionNames}}` - Available actions * `{{recentMessages}}` - Conversation history ### Customizing Templates You can override any template in your character configuration: ```typescript import { Character } from '@elizaos/core'; export const myCharacter: Character = { name: 'TechBot', // ... other config ... templates: { // Custom shouldRespond logic shouldRespondTemplate: `Decide if {{agentName}} should help with technical questions. {{providers}} - Always respond to technical questions - Always respond to direct mentions - Ignore casual chat unless it's tech-related - If someone asks for help, ALWAYS respond Your technical assessment RESPOND | IGNORE | STOP `, // Custom message handler with specific behavior messageHandlerTemplate: `Generate a helpful technical response as {{agentName}}. {{providers}} Available actions: {{actionNames}} - Be precise and technical but friendly - Provide code examples when relevant - Ask clarifying questions for vague requests - Suggest best practices Technical analysis of the request ACTION1,ACTION2 PROVIDER1,PROVIDER2 Your helpful technical response `, // Custom reflection template reflectionTemplate: `Analyze the technical conversation for learning opportunities. {{recentMessages}} - Extract technical facts and solutions - Note programming patterns discussed - Track user expertise levels - Identify knowledge gaps { "thought": "Technical insight gained", "facts": [ { "claim": "Technical fact learned", "type": "technical|solution|pattern", "topic": "programming|devops|architecture" } ], "userExpertise": { "level": "beginner|intermediate|expert", "topics": ["topic1", "topic2"] } } `, }, }; ``` ### Template Processing Flow 1. **Template Selection**: The system selects the appropriate template based on the current operation 2. **Variable Injection**: Placeholders are replaced with actual values 3. **Provider Integration**: Provider data is formatted and injected 4. **LLM Processing**: The completed prompt is sent to the language model 5. **Response Parsing**: The XML/JSON response is parsed and validated ### Advanced Template Techniques #### Conditional Logic ```typescript messageHandlerTemplate: `{{providers}} {{#if isNewUser}} Provide extra guidance and explanations {{/if}} {{#if hasAttachments}} Analyze the attached media carefully {{/if}} Context-aware thinking REPLY Adaptive response `; ``` #### Custom Provider Integration ```typescript messageHandlerTemplate: ` {{providers.CUSTOM_CONTEXT}} {{providers.USER_HISTORY}} Generate response considering the custom context above...`; ``` ## Understanding the Callback Mechanism ### What is the Callback? The callback is a function passed to every action handler that **sends the response back to the user**. When you call the callback, you're telling the system "here's what to send back". ### Callback Flow ```typescript // In an action handler async handler(runtime, message, state, options, callback) { // 1. Process the request const result = await doSomething(); // 2. Call callback to send response await callback({ text: "Here's your response", // The message to send actions: ['ACTION_NAME'], // Actions taken thought: 'Internal reasoning', // Agent's thought process attachments: [], // Optional media metadata: {} // Optional metadata }); // 3. Return success return true; } ``` ### Important Callback Concepts 1. **Calling callback = Sending a message**: When you invoke `callback()`, the message is sent to the user 2. **Multiple callbacks = Multiple messages**: You can call callback multiple times to send multiple messages 3. **No callback = No response**: If you don't call callback, no message is sent 4. **Async operation**: Always await the callback for proper error handling ### Callback Examples #### Simple Response ```typescript await callback({ text: 'Hello! How can I help?', actions: ['REPLY'], }); ``` #### Response with Attachments ```typescript await callback({ text: "Here's the image you requested", actions: ['GENERATE_IMAGE'], attachments: [ { url: 'https://example.com/image.png', contentType: 'image/png', }, ], }); ``` #### Multi-Message Response ```typescript // First message await callback({ text: 'Let me check that for you...', actions: ['ACKNOWLEDGE'], }); // Do some processing const result = await fetchData(); // Second message with results await callback({ text: `Here's what I found: ${result}`, actions: ['REPLY'], }); ``` #### Conditional Response ```typescript if (error) { await callback({ text: 'Sorry, I encountered an error', actions: ['ERROR'], metadata: { error: error.message }, }); } else { await callback({ text: 'Successfully completed!', actions: ['SUCCESS'], }); } ``` ### Callback Best Practices 1. **Always call callback**: Even for errors, call callback to inform the user 2. **Be descriptive**: Include clear text explaining what happened 3. **Use appropriate actions**: Tag responses with the correct action names 4. **Include thought**: Help with debugging by including agent reasoning 5. **Handle errors gracefully**: Provide user-friendly error messages ## Integration Guide ### 1. Basic Integration ```typescript import { Project, ProjectAgent, Character } from '@elizaos/core'; // Define your character with bootstrap plugin const character: Character = { name: 'MyAgent', bio: ['An intelligent agent powered by elizaOS'], plugins: [ '@elizaos/plugin-sql', '@elizaos/plugin-bootstrap', ], }; // Create the agent const agent: ProjectAgent = { character, // Custom plugins go here at agent level plugins: [], }; // Export the project export const project = { agents: [agent] }; ``` ### 2. Custom Event Handlers ```typescript // Add custom handling for existing events runtime.on(EventType.MESSAGE_RECEIVED, async (payload) => { // Custom pre-processing await customPreProcessor(payload); // Call default handler await bootstrapPlugin.events[EventType.MESSAGE_RECEIVED][0](payload); // Custom post-processing await customPostProcessor(payload); }); ``` ### 3. Extending Actions ```typescript // Create custom action that extends REPLY const customReplyAction = { ...replyAction, name: 'CUSTOM_REPLY', handler: async (...args) => { // Custom logic await customLogic(); // Call original handler return replyAction.handler(...args); }, }; ``` ## Examples ### Example 1: Basic Message Flow ```typescript // 1. Message arrives const message = { id: 'msg-123', entityId: 'user-456', roomId: 'room-789', content: { text: 'Hello, how are you?', }, }; // 2. Bootstrap processes it // - Saves to memory // - Checks shouldRespond // - Generates response // - Executes REPLY action // - Runs reflection evaluator // 3. Response sent via callback callback({ text: "I'm doing well, thank you! How can I help you today?", actions: ['REPLY'], thought: 'User greeted me politely, responding in kind', }); ``` ### Example 2: Multi-Action Response ```typescript // Complex response with multiple actions const response = { thought: 'User needs help with a technical issue in a specific room', text: "I'll help you with that issue.", actions: ['REPLY', 'FOLLOW_ROOM', 'UPDATE_SETTINGS'], providers: ['TECHNICAL_DOCS', 'ROOM_INFO'], }; ``` ### Example 3: Task Scheduling ```typescript // Register a task worker runtime.registerTaskWorker({ name: 'DAILY_SUMMARY', validate: async (runtime) => { const hour = new Date().getHours(); return hour === 9; // Run at 9 AM }, execute: async (runtime, options) => { // Generate and post daily summary await runtime.emitEvent(EventType.POST_GENERATED, { runtime, worldId: options.worldId, // ... other params }); }, }); // Create the task await runtime.createTask({ name: 'DAILY_SUMMARY', metadata: { updateInterval: 1000 * 60 * 60, // Check hourly }, tags: ['queue', 'repeat'], }); ``` ## Best Practices 1. **Always check message validity** before processing 2. **Use providers** to gather context instead of direct database queries 3. **Chain actions** for complex behaviors 4. **Implement proper error handling** in custom components 5. **Respect rate limits** and response timeouts 6. **Test with different room types** and message formats 7. **Monitor reflection outputs** for agent learning ## Troubleshooting ### Common Issues 1. **Agent not responding** * Check room type and bypass settings * Verify agent isn't muted * Check shouldRespond logic 2. **Duplicate responses** * Ensure response ID tracking is working * Check for multiple handler registrations 3. **Missing context** * Verify providers are registered * Check state composition 4. **Action failures** * Validate action requirements * Check handler errors * Verify callback execution ## Summary The `@elizaos/plugin-bootstrap` package is the heart of elizaOS's message processing system. It provides a complete framework for: * Receiving and processing messages from any platform * Making intelligent response decisions * Generating contextual responses * Executing complex action chains * Learning from interactions * Managing background tasks Understanding this plugin is essential for developing effective elizaOS agents and extending the platform's capabilities. # Implementation Examples Source: https://docs.elizaos.ai/plugin-registry/bootstrap/examples Practical examples and recipes for building agents with the bootstrap plugin This document provides practical examples of building agents using the plugin-bootstrap package. ## Basic Agent Setup ### Minimal Agent ```typescript import { type Character } from '@elizaos/core'; // Define a minimal character export const character: Character = { name: 'Assistant', description: 'A helpful AI assistant', plugins: [ '@elizaos/plugin-sql', // For memory storage '@elizaos/plugin-openai', '@elizaos/plugin-bootstrap', // Essential for message handling ], settings: { secrets: {}, }, system: 'Respond to messages in a helpful and concise manner.', bio: [ 'Provides helpful responses', 'Keeps answers concise and clear', 'Engages in a friendly manner', ], style: { all: [ 'Be helpful and informative', 'Keep responses concise', 'Use clear language', ], chat: [ 'Be conversational', 'Show understanding', ], }, }; ``` ### Custom Character Agent ```typescript import { type Character } from '@elizaos/core'; export const techBotCharacter: Character = { name: 'TechBot', description: 'A technical support specialist', plugins: [ '@elizaos/plugin-bootstrap', '@elizaos/plugin-sql', // Add platform plugins as needed ...(process.env.DISCORD_API_TOKEN ? ['@elizaos/plugin-discord'] : []), ], settings: { secrets: {}, avatar: 'https://example.com/techbot-avatar.png', }, system: 'You are a technical support specialist. Provide clear, patient, and detailed assistance with technical issues. Break down complex problems into simple steps.', bio: [ 'Expert in software development and troubleshooting', 'Patient and detail-oriented problem solver', 'Specializes in clear technical communication', 'Helps users at all skill levels', ], topics: [ 'software development', 'debugging', 'technical support', 'programming languages', 'system troubleshooting', ], style: { all: [ 'Be professional yet friendly', 'Use technical vocabulary but keep it accessible', 'Provide step-by-step guidance', 'Ask clarifying questions when needed', ], chat: [ 'Be patient and understanding', 'Break down complex topics', 'Offer examples when helpful', ], }, // Custom templates templates: { messageHandlerTemplate: `Generate a technical support response as {{agentName}} {{providers}} - Assess the user's technical level from their message - Consider the complexity of their problem - Provide appropriate solutions - Use clear, step-by-step guidance - Include code examples when relevant Analysis of the technical issue Your helpful technical response `, shouldRespondTemplate: `Decide if {{agentName}} should respond {{recentMessages}} - User asks a technical question - User reports an issue or bug - User needs clarification on technical topics - Direct mention of {{agentName}} - Discussion about programming or software - Casual conversation between others - Non-technical discussions - Already resolved issues Brief explanation RESPOND | IGNORE | STOP `, }, }; ``` ## Custom Actions ### Creating a Custom Help Action ```typescript import { Action, ActionExample } from '@elizaos/core'; const helpAction: Action = { name: 'HELP', similes: ['SUPPORT', 'ASSIST', 'GUIDE'], description: 'Provides detailed help on a specific topic', validate: async (runtime) => { // Always available return true; }, handler: async (runtime, message, state, options, callback) => { // Extract help topic from message const topic = extractHelpTopic(message.content.text); // Get relevant documentation const helpContent = await getHelpContent(topic); // Generate response const response = { thought: `User needs help with ${topic}`, text: helpContent, actions: ['HELP'], attachments: topic.includes('screenshot') ? [{ url: '/help/screenshots/' + topic + '.png' }] : [], }; await callback(response); return true; }, examples: [ [ { name: '{{user}}', content: { text: 'How do I reset my password?' }, }, { name: '{{agent}}', content: { text: "Here's how to reset your password:\n1. Click 'Forgot Password'\n2. Enter your email\n3. Check your inbox for reset link", actions: ['HELP'], }, }, ], ], }; // Add to agent const agentWithHelp = new AgentRuntime({ character: { /* ... */ }, plugins: [ bootstrapPlugin, { name: 'custom-help', actions: [helpAction], }, ], }); ``` ### Action that Calls External API ```typescript const weatherAction: Action = { name: 'CHECK_WEATHER', similes: ['WEATHER', 'FORECAST'], description: 'Checks current weather for a location', validate: async (runtime) => { // Check if API key is configured return !!runtime.getSetting('WEATHER_API_KEY'); }, handler: async (runtime, message, state, options, callback) => { const location = extractLocation(message.content.text); const apiKey = runtime.getSetting('WEATHER_API_KEY'); try { const response = await fetch( `https://api.weather.com/v1/current?location=${location}&key=${apiKey}` ); const weather = await response.json(); await callback({ thought: `Checking weather for ${location}`, text: `Current weather in ${location}: ${weather.temp}°F, ${weather.condition}`, actions: ['CHECK_WEATHER'], metadata: { weather }, }); } catch (error) { await callback({ thought: `Failed to get weather for ${location}`, text: "Sorry, I couldn't fetch the weather information right now.", actions: ['CHECK_WEATHER'], error: error.message, }); } return true; }, }; ``` ## Custom Providers ### Creating a System Status Provider ```typescript import { Provider } from '@elizaos/core'; const systemStatusProvider: Provider = { name: 'SYSTEM_STATUS', description: 'Provides current system status and metrics', position: 50, get: async (runtime, message) => { // Gather system metrics const metrics = await gatherSystemMetrics(); // Format for prompt const statusText = ` # System Status - CPU Usage: ${metrics.cpu}% - Memory: ${metrics.memory}% used - Active Users: ${metrics.activeUsers} - Response Time: ${metrics.avgResponseTime}ms - Uptime: ${metrics.uptime} `.trim(); return { data: metrics, values: { cpuUsage: metrics.cpu, memoryUsage: metrics.memory, isHealthy: metrics.cpu < 80 && metrics.memory < 90, }, text: statusText, }; }, }; // Use in agent const monitoringAgent = new AgentRuntime({ character: { name: 'SystemMonitor', // ... }, plugins: [ bootstrapPlugin, { name: 'monitoring', providers: [systemStatusProvider], }, ], }); ``` ### Context-Aware Provider ```typescript const userPreferencesProvider: Provider = { name: 'USER_PREFERENCES', description: 'User preferences and settings', get: async (runtime, message) => { const userId = message.entityId; const prefs = await runtime.getMemories({ tableName: 'preferences', agentId: runtime.agentId, entityId: userId, count: 1, }); if (!prefs.length) { return { data: {}, values: {}, text: 'No user preferences found.', }; } const preferences = prefs[0].content; return { data: preferences, values: { language: preferences.language || 'en', timezone: preferences.timezone || 'UTC', notifications: preferences.notifications ?? true, }, text: `User Preferences: - Language: ${preferences.language || 'English'} - Timezone: ${preferences.timezone || 'UTC'} - Notifications: ${preferences.notifications ? 'Enabled' : 'Disabled'}`, }; }, }; ``` ## Custom Evaluators ### Creating a Sentiment Analyzer ```typescript import { Evaluator } from '@elizaos/core'; const sentimentEvaluator: Evaluator = { name: 'SENTIMENT_ANALYSIS', similes: ['ANALYZE_MOOD', 'CHECK_SENTIMENT'], description: 'Analyzes conversation sentiment and adjusts agent mood', validate: async (runtime, message) => { // Run every 5 messages const messages = await runtime.getMemories({ tableName: 'messages', roomId: message.roomId, count: 5, }); return messages.length >= 5; }, handler: async (runtime, message, state) => { const prompt = `Analyze the sentiment of the recent conversation. ${state.recentMessages} Provide a sentiment analysis with: - Overall sentiment (positive/negative/neutral) - Emotional tone - Suggested agent mood adjustment`; const analysis = await runtime.useModel(ModelType.TEXT_SMALL, { prompt }); // Store sentiment data await runtime.createMemory( { entityId: runtime.agentId, agentId: runtime.agentId, roomId: message.roomId, content: { type: 'sentiment_analysis', analysis: analysis, timestamp: Date.now(), }, }, 'analysis' ); // Adjust agent mood if needed if (analysis.suggestedMood) { await runtime.updateCharacterMood(analysis.suggestedMood); } return analysis; }, }; ``` ## Task Services ### Scheduled Daily Summary ```typescript // Register a daily summary task runtime.registerTaskWorker({ name: 'DAILY_SUMMARY', validate: async (runtime, message, state) => { const hour = new Date().getHours(); return hour === 9; // Run at 9 AM }, execute: async (runtime, options) => { // Gather yesterday's data const yesterday = new Date(); yesterday.setDate(yesterday.getDate() - 1); const messages = await runtime.getMemories({ tableName: 'messages', startTime: yesterday.setHours(0, 0, 0, 0), endTime: yesterday.setHours(23, 59, 59, 999), }); // Generate summary const summary = await generateDailySummary(messages); // Post to main channel await runtime.emitEvent(EventType.POST_GENERATED, { runtime, worldId: options.worldId, userId: runtime.agentId, roomId: options.mainChannelId, source: 'task', callback: async (content) => { // Handle posted summary console.log('Daily summary posted:', content.text); }, }); }, }); // Create the scheduled task await runtime.createTask({ name: 'DAILY_SUMMARY', description: 'Posts daily activity summary', metadata: { updateInterval: 1000 * 60 * 60, // Check hourly worldId: 'main-world', mainChannelId: 'general', }, tags: ['queue', 'repeat'], }); ``` ### Event-Driven Task ```typescript // Task that triggers on specific events runtime.registerTaskWorker({ name: 'NEW_USER_WELCOME', execute: async (runtime, options) => { const { userId, userName } = options; // Send welcome message await runtime.sendMessage({ roomId: options.roomId, content: { text: `Welcome ${userName}! 👋 I'm here to help you get started.`, actions: ['WELCOME'], }, }); // Schedule follow-up await runtime.createTask({ name: 'WELCOME_FOLLOWUP', metadata: { userId, executeAt: Date.now() + 1000 * 60 * 60 * 24, // 24 hours later }, tags: ['queue'], }); }, }); // Trigger on new user runtime.on(EventType.ENTITY_JOINED, async (payload) => { await runtime.createTask({ name: 'NEW_USER_WELCOME', metadata: { userId: payload.entityId, userName: payload.entity.name, roomId: payload.roomId, }, tags: ['queue', 'immediate'], }); }); ``` ## Complete Bot Example ### Support Bot with Custom Features ```typescript import { AgentRuntime, Plugin, EventType, ChannelType } from '@elizaos/core'; // Custom support plugin const supportPlugin: Plugin = { name: 'support-features', description: 'Custom support bot features', actions: [ { name: 'CREATE_TICKET', similes: ['TICKET', 'ISSUE', 'REPORT'], description: 'Creates a support ticket', validate: async (runtime) => true, handler: async (runtime, message, state, options, callback) => { const ticket = { id: generateTicketId(), userId: message.entityId, issue: message.content.text, status: 'open', createdAt: Date.now(), }; await runtime.createMemory( { entityId: runtime.agentId, agentId: runtime.agentId, roomId: message.roomId, content: { type: 'ticket', ...ticket, }, }, 'tickets' ); await callback({ thought: 'Creating support ticket', text: `I've created ticket #${ticket.id} for your issue. Our team will review it shortly.`, actions: ['CREATE_TICKET'], metadata: { ticketId: ticket.id }, }); return true; }, }, ], providers: [ { name: 'OPEN_TICKETS', description: 'Lists open support tickets', get: async (runtime, message) => { const tickets = await runtime.getMemories({ tableName: 'tickets', agentId: runtime.agentId, filter: { status: 'open' }, count: 10, }); const ticketList = tickets .map((t) => `- #${t.content.id}: ${t.content.issue.substring(0, 50)}...`) .join('\n'); return { data: { tickets }, values: { openCount: tickets.length }, text: `Open Tickets (${tickets.length}):\n${ticketList}`, }; }, }, ], evaluators: [ { name: 'TICKET_ESCALATION', description: 'Checks if tickets need escalation', validate: async (runtime, message) => { // Check every 10 messages return message.content.type === 'ticket'; }, handler: async (runtime, message, state) => { const urgentKeywords = ['urgent', 'critical', 'emergency', 'asap']; const needsEscalation = urgentKeywords.some((word) => message.content.text.toLowerCase().includes(word) ); if (needsEscalation) { await runtime.emitEvent('TICKET_ESCALATED', { ticketId: message.content.ticketId, reason: 'Urgent keywords detected', }); } return { escalated: needsEscalation }; }, }, ], services: [], events: { [EventType.MESSAGE_RECEIVED]: [ async (payload) => { // Auto-respond to DMs with ticket creation prompt const room = await payload.runtime.getRoom(payload.message.roomId); if (room?.type === ChannelType.DM) { // Check if this is a new conversation const messages = await payload.runtime.getMemories({ tableName: 'messages', roomId: payload.message.roomId, count: 2, }); if (messages.length === 1) { await payload.callback({ text: "Hello! I'm here to help. Would you like to create a support ticket?", actions: ['GREET'], suggestions: ['Create ticket', 'Check ticket status', 'Get help'], }); } } }, ], }, }; // Create the support bot const supportBot = new AgentRuntime({ character: { name: 'SupportBot', description: '24/7 customer support specialist', bio: 'I help users resolve issues and create support tickets', modelProvider: 'openai', templates: { messageHandlerTemplate: `# Support Bot Response {{providers}} Guidelines: - Be empathetic and professional - Gather all necessary information - Offer to create tickets for unresolved issues - Provide ticket numbers for tracking `, }, }, plugins: [bootstrapPlugin, pglitePlugin, supportPlugin], settings: { CONVERSATION_LENGTH: 50, // Longer context for support SHOULD_RESPOND_BYPASS_TYPES: ['dm', 'support', 'ticket'], }, }); // Start the bot await supportBot.start(); ``` ## Integration Examples ### Discord Integration ```typescript import { DiscordClient } from '@elizaos/discord'; const discordBot = new AgentRuntime({ character: { /* ... */ }, plugins: [bootstrapPlugin], clients: [new DiscordClient()], }); // Discord-specific room handling discordBot.on(EventType.MESSAGE_RECEIVED, async (payload) => { const room = await payload.runtime.getRoom(payload.message.roomId); // Handle Discord-specific features if (room?.metadata?.discordType === 'thread') { // Special handling for threads } }); ``` ### Multi-Platform Bot ```typescript import { DiscordClient } from '@elizaos/discord'; import { TelegramClient } from '@elizaos/telegram'; import { TwitterClient } from '@elizaos/twitter'; const multiPlatformBot = new AgentRuntime({ character: { name: 'OmniBot', description: 'Available everywhere', }, plugins: [ bootstrapPlugin, { name: 'platform-adapter', providers: [ { name: 'PLATFORM_INFO', get: async (runtime, message) => { const source = message.content.source; const platformTips = { discord: 'Use /commands for Discord-specific features', telegram: 'Use inline keyboards for better UX', twitter: 'Keep responses under 280 characters', }; return { data: { platform: source }, values: { isTwitter: source === 'twitter' }, text: `Platform: ${source}\nTip: ${platformTips[source] || 'None'}`, }; }, }, ], }, ], clients: [new DiscordClient(), new TelegramClient(), new TwitterClient()], }); ``` ## Best Practices 1. **Always include bootstrapPlugin** - It's the foundation 2. **Use providers for context** - Don't query database in actions 3. **Chain actions thoughtfully** - Order matters 4. **Handle errors gracefully** - Users should get helpful messages 5. **Test with different scenarios** - DMs, groups, mentions 6. **Monitor evaluator output** - Learn from your bot's analysis 7. **Configure templates** - Match your bot's personality ## Debugging Tips ```typescript // Enable debug logging process.env.DEBUG = 'elizaos:*'; // Log action execution const debugAction = { ...originalAction, handler: async (...args) => { console.log(`Executing ${debugAction.name}`, args[1].content); const result = await originalAction.handler(...args); console.log(`${debugAction.name} completed`, result); return result; }, }; // Monitor provider data runtime.on('state:composed', (state) => { console.log( 'State providers:', state.providerData.map((p) => p.providerName) ); }); // Track message flow runtime.on(EventType.MESSAGE_RECEIVED, (payload) => { console.log(`Message flow: ${payload.message.entityId} -> ${payload.runtime.agentId}`); }); ``` These examples demonstrate the flexibility and power of the plugin-bootstrap system. Start with simple examples and gradually add complexity as needed! ### Understanding the Callback Mechanism Every action handler receives a callback function that sends messages back to the user. Here's how it works: ```typescript const explainAction: Action = { name: 'EXPLAIN', description: 'Explains a concept in detail', handler: async (runtime, message, state, options, callback) => { // Extract topic from message const topic = extractTopic(message.content.text); // First message - acknowledge the request await callback({ text: `Let me explain ${topic} for you...`, actions: ['ACKNOWLEDGE'], }); // Fetch explanation (simulating delay) const explanation = await fetchExplanation(topic); // Second message - deliver the explanation await callback({ text: explanation, actions: ['EXPLAIN'], thought: `Explained ${topic} to the user`, }); // Third message - offer follow-up await callback({ text: 'Would you like me to explain anything else about this topic?', actions: ['FOLLOW_UP'], }); return true; }, }; ``` ## Template Customization Examples ### Example 1: Gaming Bot with Custom Templates ```typescript import { AgentRuntime, Character } from '@elizaos/core'; import { bootstrapPlugin } from '@elizaos/plugin-bootstrap'; const gamingBotCharacter: Character = { name: 'GameMaster', description: 'A gaming companion and guide', templates: { // Custom shouldRespond for gaming context shouldRespondTemplate: `Decide if {{agentName}} should respond to gaming-related messages. {{providers}} - ALWAYS respond to: game questions, strategy requests, team coordination - RESPOND to: patch notes discussion, build advice, gameplay tips - IGNORE: off-topic chat, real-world discussions (unless directly asked) - STOP if: asked to stop giving advice or to be quiet Gaming context assessment RESPOND | IGNORE | STOP `, // Gaming-focused message handler messageHandlerTemplate: `Generate gaming advice as {{agentName}}. {{providers}} Available actions: {{actionNames}} - Use gaming terminology naturally - Reference game mechanics when relevant - Be encouraging to new players - Share pro tips for experienced players - React enthusiastically to achievements - Short, punchy responses for in-game chat - Detailed explanations for strategy questions - Use gaming emotes and expressions - Reference popular gaming memes appropriately Gaming situation analysis REPLY GAME_STATE,PLAYER_STATS Your gaming response `, // Gaming-specific reflection reflectionTemplate: `Analyze gaming interactions for improvement. {{recentMessages}} - Track player skill progression - Note frequently asked game mechanics - Identify team dynamics and roles - Record successful strategies shared - Monitor player frustration levels { "thought": "Gaming insight", "facts": [{ "claim": "Gaming fact or strategy", "type": "strategy|mechanic|meta", "game": "specific game name" }], "playerProfile": { "skillLevel": "beginner|intermediate|advanced|pro", "preferredRole": "tank|dps|support|flex", "interests": ["pvp", "pve", "competitive"] } } `, }, // Gaming-related bio and style bio: [ 'Expert in multiple game genres', 'Provides real-time strategy advice', 'Helps teams coordinate effectively', 'Explains complex game mechanics simply', ], style: { chat: [ 'Use gaming slang appropriately', 'Quick responses during matches', 'Detailed guides when asked', 'Supportive and encouraging tone', ], }, }; // Create the gaming bot const gamingBot = new AgentRuntime({ character: gamingBotCharacter, plugins: [bootstrapPlugin], }); ``` ### Example 2: Customer Support Bot with Templates ```typescript const supportBotCharacter: Character = { name: 'SupportAgent', description: '24/7 customer support specialist', templates: { // Support-focused shouldRespond shouldRespondTemplate: `Determine if {{agentName}} should handle this support request. {{providers}} PRIORITY 1 (Always respond): - Error messages or bug reports - Account issues or login problems - Payment or billing questions - Direct help requests PRIORITY 2 (Respond): - Feature questions - How-to requests - General feedback PRIORITY 3 (Conditionally respond): - Complaints (respond with empathy) - Feature requests (acknowledge and log) NEVER IGNORE: - Frustrated customers - Urgent issues - Security concerns Support priority assessment RESPOND | ESCALATE | ACKNOWLEDGE `, // Professional support message handler messageHandlerTemplate: `Provide professional support as {{agentName}}. {{providers}} Available actions: {{actionNames}} - Acknowledge the issue immediately - Express empathy for any inconvenience - Provide clear, step-by-step solutions - Offer alternatives if primary solution unavailable - Always follow up on open issues - Professional yet friendly - Patient and understanding - Solution-oriented - Proactive in preventing future issues Issue analysis and solution approach REPLY,CREATE_TICKET USER_HISTORY,KNOWLEDGE_BASE,OPEN_TICKETS Your support response `, // Support interaction reflection reflectionTemplate: `Analyze support interaction for quality and improvement. {{recentMessages}} - Issue resolved: yes/no/escalated - Customer satisfaction indicators - Response time and efficiency - Knowledge gaps identified - Common issues pattern { "thought": "Support interaction analysis", "resolution": { "status": "resolved|unresolved|escalated", "issueType": "technical|billing|account|other", "satisfactionIndicators": ["positive", "negative", "neutral"] }, "facts": [{ "claim": "Issue or solution discovered", "type": "bug|workaround|feature_request", "frequency": "first_time|recurring|common" }], "improvements": ["suggested FAQ entries", "documentation needs"] } `, }, }; ``` ### Example 3: Educational Bot with Adaptive Templates ```typescript const educatorCharacter: Character = { name: 'EduBot', description: 'Adaptive educational assistant', templates: { // Education-focused templates with learning level adaptation messageHandlerTemplate: `Provide educational guidance as {{agentName}}. {{providers}} Current Level: {{studentLevel}} Subject: {{subject}} Learning Style: {{learningStyle}} For BEGINNERS: - Use simple language and analogies - Break down complex concepts - Provide many examples - Check understanding frequently For INTERMEDIATE: - Build on existing knowledge - Introduce technical terminology - Encourage critical thinking - Suggest practice problems For ADVANCED: - Discuss edge cases and exceptions - Explore theoretical foundations - Connect to real-world applications - Recommend further reading Pedagogical approach for this student REPLY,GENERATE_QUIZ STUDENT_PROGRESS,CURRICULUM,LEARNING_HISTORY Your educational response `, }, }; ``` ## Advanced Callback Patterns ### Progressive Disclosure Pattern ```typescript const teachAction: Action = { name: 'TEACH_CONCEPT', handler: async (runtime, message, state, options, callback) => { const concept = extractConcept(message.content.text); const userLevel = await getUserLevel(runtime, message.entityId); if (userLevel === 'beginner') { // Start with simple explanation await callback({ text: `Let's start with the basics of ${concept}...`, actions: ['TEACH_INTRO'], }); // Add an analogy await callback({ text: `Think of it like ${getAnalogy(concept)}`, actions: ['TEACH_ANALOGY'], }); // Check understanding await callback({ text: 'Does this make sense so far? Would you like me to explain differently?', actions: ['CHECK_UNDERSTANDING'], }); } else { // Advanced explanation await callback({ text: `${concept} involves several key principles...`, actions: ['TEACH_ADVANCED'], attachments: [ { url: `/diagrams/${concept}.png`, contentType: 'image/png', }, ], }); } return true; }, }; ``` ### Error Recovery Pattern ```typescript const processAction: Action = { name: 'PROCESS_REQUEST', handler: async (runtime, message, state, options, callback) => { try { // Acknowledge request await callback({ text: 'Processing your request...', actions: ['ACKNOWLEDGE'], }); // Attempt processing const result = await processUserRequest(message); // Success response await callback({ text: `Successfully completed! ${result.summary}`, actions: ['SUCCESS'], metadata: { processId: result.id }, }); } catch (error) { // Error response with helpful information await callback({ text: 'I encountered an issue processing your request.', actions: ['ERROR'], }); // Provide specific error details if (error.code === 'RATE_LIMIT') { await callback({ text: "You've exceeded the rate limit. Please try again in a few minutes.", actions: ['RATE_LIMIT_ERROR'], }); } else if (error.code === 'INVALID_INPUT') { await callback({ text: `The input seems invalid. Please check: ${error.details}`, actions: ['VALIDATION_ERROR'], }); } else { // Generic error with support option await callback({ text: 'An unexpected error occurred. Would you like me to create a support ticket?', actions: ['OFFER_SUPPORT'], metadata: { errorId: generateErrorId() }, }); } } return true; }, }; ``` ### Streaming Response Pattern ```typescript const streamingAction: Action = { name: 'STREAM_DATA', handler: async (runtime, message, state, options, callback) => { const dataStream = await getDataStream(message.content.query); // Initial response await callback({ text: 'Streaming data as it arrives...', actions: ['STREAM_START'], }); // Stream chunks for await (const chunk of dataStream) { await callback({ text: chunk.data, actions: ['STREAM_CHUNK'], metadata: { chunkId: chunk.id, isPartial: true, }, }); // Rate limit streaming await new Promise((resolve) => setTimeout(resolve, 100)); } // Final summary await callback({ text: "Streaming complete! Here's a summary of the data...", actions: ['STREAM_COMPLETE'], metadata: { totalChunks: dataStream.length }, }); return true; }, }; ``` # Message Processing Flow Source: https://docs.elizaos.ai/plugin-registry/bootstrap/message-flow Step-by-step breakdown of how messages flow through the bootstrap plugin system # Message Processing Flow - Detailed Breakdown This document provides a step-by-step breakdown of how messages flow through the plugin-bootstrap system. ## Complete Message Flow Diagram ```mermaid flowchart TD Start([Message Received]) --> A[Event: MESSAGE_RECEIVED] A --> B{Is from Self?} B -->|Yes| End1[Skip Processing] B -->|No| C[Generate Response ID] C --> D[Start Run Tracking] D --> E[Save to Memory & Embeddings] E --> F{Has Attachments?} F -->|Yes| G[Process Attachments] F -->|No| H[Check Agent State] G --> H H --> I{Is Agent Muted?} I -->|Yes & No Name Mention| End2[Ignore Message] I -->|No or Name Mentioned| J[Compose Initial State] J --> K{Should Bypass
shouldRespond?} K -->|Yes| L[Skip to Response] K -->|No| M[Evaluate shouldRespond] M --> N[Generate shouldRespond Prompt] N --> O[LLM Decision] O --> P{Should Respond?} P -->|No| Q[Save Ignore Decision] Q --> End3[End Processing] P -->|Yes| L L --> R[Compose Full State] R --> S[Generate Response Prompt] S --> T[LLM Response Generation] T --> U{Valid Response?} U -->|No| V[Retry up to 3x] V --> T U -->|Yes| W[Parse XML Response] W --> X{Still Latest Response?} X -->|No| End4[Discard Response] X -->|Yes| Y[Create Response Message] Y --> Z{Is Simple Response?} Z -->|Yes| AA[Direct Callback] Z -->|No| AB[Process Actions] AA --> AC[Run Evaluators] AB --> AC AC --> AD[Reflection Evaluator] AD --> AE[Extract Facts] AE --> AF[Update Relationships] AF --> AG[Save Reflection State] AG --> AH[Emit RUN_ENDED] AH --> End5[Complete] ``` ## Detailed Step Descriptions ### 1. Initial Message Reception ```typescript // Event triggered by platform (Discord, Telegram, etc.) EventType.MESSAGE_RECEIVED → messageReceivedHandler ``` ### 2. Self-Check ```typescript if (message.entityId === runtime.agentId) { logger.debug('Skipping message from self'); return; } ``` ### 3. Response ID Generation ```typescript // Prevents duplicate responses for rapid messages const responseId = v4(); latestResponseIds.get(runtime.agentId).set(message.roomId, responseId); ``` ### 4. Run Tracking ```typescript const runId = runtime.startRun(); await runtime.emitEvent(EventType.RUN_STARTED, {...}); ``` ### 5. Memory Storage ```typescript await Promise.all([ runtime.addEmbeddingToMemory(message), // Vector embeddings runtime.createMemory(message, 'messages'), // Message history ]); ``` ### 6. Attachment Processing ```typescript if (message.content.attachments?.length > 0) { // Images: Generate descriptions // Documents: Extract text // Other: Process as configured message.content.attachments = await processAttachments(message.content.attachments, runtime); } ``` ### 7. Agent State Check ```typescript const agentUserState = await runtime.getParticipantUserState(message.roomId, runtime.agentId); if ( agentUserState === 'MUTED' && !message.content.text?.toLowerCase().includes(runtime.character.name.toLowerCase()) ) { return; // Ignore if muted and not mentioned } ``` ### 8. Should Respond Evaluation #### Bypass Conditions ```typescript function shouldBypassShouldRespond(runtime, room, source) { // Default bypass types const bypassTypes = [ChannelType.DM, ChannelType.VOICE_DM, ChannelType.SELF, ChannelType.API]; // Default bypass sources const bypassSources = ['client_chat']; // Plus any configured in environment return bypassTypes.includes(room.type) || bypassSources.includes(source); } ``` #### LLM Evaluation ```typescript if (!shouldBypassShouldRespond) { const state = await runtime.composeState(message, [ 'ANXIETY', 'SHOULD_RESPOND', 'ENTITIES', 'CHARACTER', 'RECENT_MESSAGES', 'ACTIONS', ]); const prompt = composePromptFromState({ state, template: shouldRespondTemplate, }); const response = await runtime.useModel(ModelType.TEXT_SMALL, { prompt }); const parsed = parseKeyValueXml(response); shouldRespond = parsed?.action && !['IGNORE', 'NONE'].includes(parsed.action.toUpperCase()); } ``` ### 9. Response Generation #### State Composition with Providers ```typescript state = await runtime.composeState(message, ['ACTIONS']); // Each provider adds context: // - RECENT_MESSAGES: Conversation history // - CHARACTER: Personality traits // - ENTITIES: User information // - TIME: Temporal context // - RELATIONSHIPS: Social connections // - WORLD: Environment details // - etc. ``` #### LLM Response ```typescript const prompt = composePromptFromState({ state, template: messageHandlerTemplate, }); let response = await runtime.useModel(ModelType.TEXT_LARGE, { prompt }); // Expected XML format: /* Agent's internal reasoning REPLY,FOLLOW_ROOM TECHNICAL_DOCS,FAQ The actual response text false */ ``` ### 10. Response Validation ```typescript // Retry logic for missing fields while (retries < 3 && (!responseContent?.thought || !responseContent?.actions)) { // Regenerate response retries++; } // Check if still the latest response if (latestResponseIds.get(runtime.agentId).get(message.roomId) !== responseId) { return; // Newer message is being processed } ``` ### 11. Action Processing #### Simple Response ```typescript // Simple = REPLY action only, no providers if (responseContent.simple && responseContent.text) { await callback(responseContent); } ``` #### Complex Response ```typescript // Multiple actions or providers await runtime.processActions(message, responseMessages, state, callback); ``` ### 12. Evaluator Execution #### Reflection Evaluator ```typescript // Runs after response generation await runtime.evaluate(message, state, shouldRespond, callback, responseMessages); // Reflection evaluator: // 1. Analyzes conversation quality // 2. Extracts new facts // 3. Updates relationships // 4. Self-reflects on performance ``` ## Key Decision Points ### 1. Should Respond Decision Tree ```text Is DM? → YES → Respond Is Voice DM? → YES → Respond Is API Call? → YES → Respond Is Muted + Name Mentioned? → YES → Respond Is Muted? → NO → Ignore Run shouldRespond LLM → - Action = REPLY/etc → Respond - Action = IGNORE/NONE → Ignore ``` ### 2. Response Type Decision ```text Actions = [REPLY] only AND Providers = [] → Simple Response Otherwise → Complex Response with Action Processing ``` ### 3. Evaluator Trigger Conditions ```text Message Count > ConversationLength / 4 → Run Reflection New Interaction → Update Relationships Facts Mentioned → Extract and Store ``` ## Performance Optimizations ### 1. Response ID Tracking * Prevents duplicate responses when multiple messages arrive quickly * Only processes the latest message per room ### 2. Parallel Operations ```typescript // Parallel memory operations await Promise.all([ runtime.addEmbeddingToMemory(message), runtime.createMemory(message, 'messages') ]); // Parallel data fetching in providers const [entities, room, messages, interactions] = await Promise.all([ getEntityDetails({ runtime, roomId }), runtime.getRoom(roomId), runtime.getMemories({ tableName: 'messages', roomId }), getRecentInteractions(...) ]); ``` ### 3. Timeout Protection ```typescript const timeoutDuration = 60 * 60 * 1000; // 1 hour await Promise.race([processingPromise, timeoutPromise]); ``` ## Error Handling ### 1. Run Lifecycle Events ```typescript try { // Process message await runtime.emitEvent(EventType.RUN_ENDED, { status: 'completed' }); } catch (error) { await runtime.emitEvent(EventType.RUN_ENDED, { status: 'error', error: error.message, }); } ``` ### 2. Graceful Degradation * Missing attachments → Continue without them * Provider errors → Use default values * LLM failures → Retry with backoff * Database errors → Log and continue ## Platform-Specific Handling ### Discord * Channels → Rooms with ChannelType * Servers → Worlds * Users → Entities ### Telegram * Chats → Rooms * Groups → Worlds * Users → Entities ### Message Bus * Topics → Rooms * Namespaces → Worlds * Publishers → Entities ## Summary The message flow through plugin-bootstrap is designed to be: 1. **Platform-agnostic** - Works with any message source 2. **Intelligent** - Makes context-aware response decisions 3. **Extensible** - Supports custom actions, providers, evaluators 4. **Resilient** - Handles errors gracefully 5. **Performant** - Uses parallel operations and caching This flow ensures that every message is processed consistently, responses are contextual and appropriate, and the agent learns from each interaction. ## Template Usage in Message Flow Understanding where templates are used helps you customize the right parts of the flow: ### 1. **shouldRespondTemplate** - Decision Point Used at step 8 in the flow when evaluating whether to respond: ``` Message Received → shouldRespondTemplate → RESPOND/IGNORE/STOP ``` This template controls: * When your agent engages in conversations * What triggers a response * When to stay silent ### 2. **messageHandlerTemplate** - Response Generation Used at step 9 when generating the actual response: ``` Decision to Respond → messageHandlerTemplate → Response + Actions ``` This template controls: * How responses are formulated * Which actions are selected * The agent's personality and tone * Which providers to use for context ### 3. **reflectionTemplate** - Post-Interaction Analysis Used at step 12 during evaluator execution: ``` Response Sent → reflectionTemplate → Learning & Memory Updates ``` This template controls: * What the agent learns from interactions * How facts are extracted * Relationship tracking logic * Self-improvement mechanisms ### 4. **postCreationTemplate** - Social Media Posts Used when POST\_GENERATED event is triggered: ``` Post Request → postCreationTemplate → Social Media Content ``` This template controls: * Post style and tone * Content generation approach * Image prompt generation ### Template Processing Pipeline ```mermaid graph TD A[Raw Template] --> B[Variable Injection] B --> C[Provider Data Integration] C --> D[Final Prompt Assembly] D --> E[LLM Processing] E --> F[Response Parsing] F --> G[Action Execution/Callback] ``` 1. **Template Selection**: System picks the appropriate template 2. **Variable Replacement**: `{{agentName}}`, `{{providers}}`, etc. are replaced 3. **Provider Injection**: Provider data is formatted and inserted 4. **Prompt Assembly**: Complete prompt is constructed 5. **LLM Processing**: Sent to language model 6. **Response Parsing**: XML/JSON response is parsed 7. **Execution**: Actions are executed, callbacks are called ### Customization Impact When you customize templates, you're modifying these key decision points: * **shouldRespond**: Change engagement patterns * **messageHandler**: Alter personality and response style * **reflection**: Modify learning and memory formation * **postCreation**: Adjust social media presence Each template change cascades through the entire interaction flow, allowing deep customization of agent behavior while maintaining the robust message processing infrastructure. # Testing Guide Source: https://docs.elizaos.ai/plugin-registry/bootstrap/testing-guide Testing patterns and best practices for the bootstrap plugin This guide covers testing patterns and best practices for developing with the plugin-bootstrap package. ## Overview The plugin-bootstrap package includes a comprehensive test suite that demonstrates how to test: * Actions * Providers * Evaluators * Services * Event Handlers * Message Processing Logic ## Test Setup ### Test Framework This plugin uses **Bun's built-in test runner**, not Vitest. Bun provides a Jest-compatible testing API with excellent TypeScript support and fast execution. ### Using the Standard Test Utilities The package provides robust test utilities in `src/__tests__/test-utils.ts`: ```typescript import { setupActionTest } from '@elizaos/plugin-bootstrap/test-utils'; describe('My Component', () => { let mockRuntime: MockRuntime; let mockMessage: Partial; let mockState: Partial; let callbackFn: ReturnType; beforeEach(() => { const setup = setupActionTest(); mockRuntime = setup.mockRuntime; mockMessage = setup.mockMessage; mockState = setup.mockState; callbackFn = setup.callbackFn; }); }); ``` ### Available Mock Factories ```typescript // Create a mock runtime with all methods const runtime = createMockRuntime(); // Create a mock memory/message const message = createMockMemory({ content: { text: 'Hello world' }, entityId: 'user-123', roomId: 'room-456', }); // Create a mock state const state = createMockState({ values: { customKey: 'customValue', }, }); // Create a mock service const service = createMockService({ serviceType: ServiceType.TASK, }); ``` ## Testing Patterns ### Testing Actions #### Basic Action Test ```typescript import { describe, it, expect, beforeEach, mock } from 'bun:test'; import { replyAction } from '../actions/reply'; import { setupActionTest } from '../test-utils'; describe('Reply Action', () => { let mockRuntime: MockRuntime; let mockMessage: Partial; let mockState: Partial; let callbackFn: ReturnType; beforeEach(() => { const setup = setupActionTest(); mockRuntime = setup.mockRuntime; mockMessage = setup.mockMessage; mockState = setup.mockState; callbackFn = setup.callbackFn; }); it('should validate successfully', async () => { const result = await replyAction.validate(mockRuntime); expect(result).toBe(true); }); it('should generate appropriate response', async () => { // Setup LLM response mockRuntime.useModel.mockResolvedValue({ thought: 'User greeted me', message: 'Hello! How can I help you?', }); // Execute action await replyAction.handler( mockRuntime, mockMessage as Memory, mockState as State, {}, callbackFn ); // Verify callback was called with correct content expect(callbackFn).toHaveBeenCalledWith({ thought: 'User greeted me', text: 'Hello! How can I help you?', actions: ['REPLY'], }); }); }); ``` #### Testing Action with Dependencies ```typescript describe('Follow Room Action', () => { it('should update participation status', async () => { const setup = setupActionTest(); // Setup room data setup.mockRuntime.getRoom.mockResolvedValue({ id: 'room-123', type: ChannelType.TEXT, participants: ['user-123'], }); // Execute action await followRoomAction.handler( setup.mockRuntime, setup.mockMessage as Memory, setup.mockState as State, {}, setup.callbackFn ); // Verify runtime methods were called expect(setup.mockRuntime.updateParticipantUserState).toHaveBeenCalledWith( 'room-123', setup.mockRuntime.agentId, 'FOLLOWED' ); // Verify callback expect(setup.callbackFn).toHaveBeenCalledWith({ text: expect.stringContaining('followed'), actions: ['FOLLOW_ROOM'], }); }); }); ``` ### Testing Providers ```typescript import { recentMessagesProvider } from '../providers/recentMessages'; describe('Recent Messages Provider', () => { it('should format conversation history', async () => { const setup = setupActionTest(); // Mock recent messages const recentMessages = [ createMockMemory({ content: { text: 'Hello' }, entityId: 'user-123', createdAt: Date.now() - 60000, }), createMockMemory({ content: { text: 'Hi there!' }, entityId: setup.mockRuntime.agentId, createdAt: Date.now() - 30000, }), ]; setup.mockRuntime.getMemories.mockResolvedValue(recentMessages); setup.mockRuntime.getEntityById.mockResolvedValue({ id: 'user-123', names: ['Alice'], metadata: { userName: 'alice' }, }); // Get provider data const result = await recentMessagesProvider.get(setup.mockRuntime, setup.mockMessage as Memory); // Verify structure expect(result).toHaveProperty('data'); expect(result).toHaveProperty('values'); expect(result).toHaveProperty('text'); // Verify content expect(result.data.recentMessages).toHaveLength(2); expect(result.text).toContain('Alice: Hello'); expect(result.text).toContain('Hi there!'); }); }); ``` ### Testing Evaluators ```typescript import { reflectionEvaluator } from '../evaluators/reflection'; describe('Reflection Evaluator', () => { it('should extract facts from conversation', async () => { const setup = setupActionTest(); // Mock LLM response with facts setup.mockRuntime.useModel.mockResolvedValue({ thought: 'Learned new information about user', facts: [ { claim: 'User likes coffee', type: 'fact', in_bio: false, already_known: false, }, ], relationships: [], }); // Execute evaluator const result = await reflectionEvaluator.handler( setup.mockRuntime, setup.mockMessage as Memory, setup.mockState as State ); // Verify facts were saved expect(setup.mockRuntime.createMemory).toHaveBeenCalledWith( expect.objectContaining({ content: { text: 'User likes coffee' }, }), 'facts', true ); }); }); ``` ### Testing Message Processing ```typescript import { messageReceivedHandler } from '../index'; describe('Message Processing', () => { it('should process message end-to-end', async () => { const setup = setupActionTest(); const onComplete = mock(); // Setup room and state setup.mockRuntime.getRoom.mockResolvedValue({ id: 'room-123', type: ChannelType.TEXT, }); // Mock shouldRespond decision setup.mockRuntime.useModel .mockResolvedValueOnce('REPLY') // shouldRespond .mockResolvedValueOnce({ // response generation thought: 'Responding to greeting', actions: ['REPLY'], text: 'Hello!', simple: true, }); // Process message await messageReceivedHandler({ runtime: setup.mockRuntime, message: setup.mockMessage as Memory, callback: setup.callbackFn, onComplete, }); // Verify flow expect(setup.mockRuntime.addEmbeddingToMemory).toHaveBeenCalled(); expect(setup.mockRuntime.createMemory).toHaveBeenCalled(); expect(setup.callbackFn).toHaveBeenCalledWith( expect.objectContaining({ text: 'Hello!', actions: ['REPLY'], }) ); expect(onComplete).toHaveBeenCalled(); }); }); ``` ### Testing Services ```typescript import { TaskService } from '../services/task'; describe('Task Service', () => { it('should execute repeating tasks', async () => { const setup = setupActionTest(); // Create task const task = { id: 'task-123', name: 'TEST_TASK', metadata: { updateInterval: 1000, updatedAt: Date.now() - 2000, }, tags: ['queue', 'repeat'], }; // Register worker const worker = { name: 'TEST_TASK', execute: mock(), }; setup.mockRuntime.registerTaskWorker(worker); setup.mockRuntime.getTaskWorker.mockReturnValue(worker); setup.mockRuntime.getTasks.mockResolvedValue([task]); // Start service const service = await TaskService.start(setup.mockRuntime); // Wait for tick await new Promise((resolve) => setTimeout(resolve, 1100)); // Verify execution expect(worker.execute).toHaveBeenCalled(); expect(setup.mockRuntime.updateTask).toHaveBeenCalledWith( 'task-123', expect.objectContaining({ metadata: expect.objectContaining({ updatedAt: expect.any(Number), }), }) ); // Cleanup await service.stop(); }); }); ``` ## Testing Best Practices ### 1. Use Standard Test Setup Always use the provided test utilities for consistency: ```typescript const setup = setupActionTest({ messageOverrides: { /* custom message props */ }, stateOverrides: { /* custom state */ }, runtimeOverrides: { /* custom runtime behavior */ }, }); ``` ### 2. Test Edge Cases ```typescript it('should handle missing attachments gracefully', async () => { setup.mockMessage.content.attachments = undefined; // Test continues without error }); it('should handle network failures', async () => { setup.mockRuntime.useModel.mockRejectedValue(new Error('Network error')); // Verify graceful error handling }); ``` ### 3. Mock External Dependencies ```typescript // Mock fetch for external APIs import { mock } from 'bun:test'; // Create mock for fetch globalThis.fetch = mock().mockResolvedValue({ ok: true, arrayBuffer: () => Promise.resolve(Buffer.from('test')), headers: new Map([['content-type', 'image/png']]), }); ``` ### 4. Test Async Operations ```typescript it('should handle concurrent messages', async () => { const messages = [ createMockMemory({ content: { text: 'Message 1' } }), createMockMemory({ content: { text: 'Message 2' } }), ]; // Process messages concurrently await Promise.all( messages.map((msg) => messageReceivedHandler({ runtime: setup.mockRuntime, message: msg, callback: setup.callbackFn, }) ) ); // Verify both processed correctly expect(setup.callbackFn).toHaveBeenCalledTimes(2); }); ``` ### 5. Verify State Changes ```typescript it('should update agent state correctly', async () => { // Initial state expect(setup.mockRuntime.getMemories).toHaveBeenCalledTimes(0); // Action that modifies state await action.handler(...); // Verify state changes expect(setup.mockRuntime.createMemory).toHaveBeenCalled(); expect(setup.mockRuntime.updateRelationship).toHaveBeenCalled(); }); ``` ## Common Testing Scenarios ### Testing Room Type Behavior ```typescript describe('Room Type Handling', () => { it.each([ [ChannelType.DM, true], [ChannelType.TEXT, false], [ChannelType.VOICE_DM, true], ])('should bypass shouldRespond for %s: %s', async (roomType, shouldBypass) => { setup.mockRuntime.getRoom.mockResolvedValue({ id: 'room-123', type: roomType, }); // Test behavior based on room type }); }); ``` ### Testing Provider Context ```typescript it('should include all requested providers', async () => { const state = await setup.mockRuntime.composeState(setup.mockMessage, [ 'RECENT_MESSAGES', 'ENTITIES', 'RELATIONSHIPS', ]); expect(state.providerData).toHaveLength(3); expect(state.providerData[0].providerName).toBe('RECENT_MESSAGES'); }); ``` ### Testing Error Recovery ```typescript it('should recover from provider errors', async () => { // Make one provider fail setup.mockRuntime.getMemories.mockRejectedValueOnce(new Error('DB error')); // Should still process message await messageReceivedHandler({...}); // Verify graceful degradation expect(setup.callbackFn).toHaveBeenCalled(); }); ``` ## Running Tests ```bash # Run all bootstrap tests bun test # Run specific test file bun test packages/plugin-bootstrap/src/__tests__/actions.test.ts # Run tests in watch mode bun test --watch # Run with coverage bun test --coverage ``` ## Bun Test Features Bun's test runner provides several advantages: 1. **Fast execution** - Tests run directly in Bun's runtime 2. **Built-in TypeScript** - No compilation step needed 3. **Jest compatibility** - Familiar API for developers 4. **Built-in mocking** - The `mock()` function is built-in 5. **Snapshot testing** - Built-in support for snapshots 6. **Watch mode** - Automatic re-running on file changes ### Bun Mock API ```typescript import { mock } from 'bun:test'; // Create a mock function const mockFn = mock(); // Set return value mockFn.mockReturnValue('value'); mockFn.mockResolvedValue('async value'); // Set implementation mockFn.mockImplementation((arg) => arg * 2); // Check calls expect(mockFn).toHaveBeenCalled(); expect(mockFn).toHaveBeenCalledWith('arg'); expect(mockFn).toHaveBeenCalledTimes(2); // Reset mocks mock.restore(); // Reset all mocks mockFn.mockReset(); // Reset specific mock ``` ## Tips for Writing Tests 1. **Start with the happy path** - Test normal operation first 2. **Add edge cases** - Empty arrays, null values, errors 3. **Test async behavior** - Timeouts, retries, concurrent operations 4. **Verify side effects** - Database updates, event emissions 5. **Keep tests focused** - One concept per test 6. **Use descriptive names** - Should describe what is being tested 7. **Mock at boundaries** - Mock external services, not internal logic ## Debugging Tests ```typescript // Add console logs to debug it('should process correctly', async () => { setup.mockRuntime.useModel.mockImplementation(async (type, params) => { console.log('Model called with:', { type, params }); return mockResponse; }); // Step through with debugger debugger; await action.handler(...); }); ``` ## Differences from Vitest If you're familiar with Vitest, here are the key differences: 1. **Import from `bun:test`** instead of `vitest` 2. **No need for `vi` prefix** - Just use `mock()` directly 3. **No configuration file** - Bun test works out of the box 4. **Different CLI commands** - Use `bun test` instead of `vitest` Remember: Good tests make development faster and more confident. The test suite is your safety net when making changes! # Overview Source: https://docs.elizaos.ai/plugin-registry/defi/evm Integrate EVM blockchain capabilities into your AI agent The EVM plugin enables AI agents to interact with Ethereum Virtual Machine (EVM) compatible blockchains, supporting token transfers, swaps, bridging, and governance operations across 30+ networks. ## Features * **Multi-chain Support**: Works with Ethereum, Base, Arbitrum, Optimism, Polygon, BSC, Avalanche, and many more * **Token Operations**: Transfer native tokens and ERC20 tokens * **DeFi Integration**: Swap tokens and bridge across chains using LiFi and Bebop * **Governance**: Create proposals, vote, queue, and execute governance actions * **Wallet Management**: Multi-chain balance tracking with automatic updates * **TEE Support**: Secure wallet derivation in Trusted Execution Environments ## Installation ```bash elizaos plugins add evm ``` ## Configuration The plugin requires the following environment variables: ```env # Required EVM_PRIVATE_KEY=your_private_key_here # Optional - Custom RPC endpoints ETHEREUM_PROVIDER_ETHEREUM=https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY ETHEREUM_PROVIDER_BASE=https://base-mainnet.g.alchemy.com/v2/YOUR_KEY # Optional - TEE Configuration TEE_MODE=true WALLET_SECRET_SALT=your_secret_salt ``` ## Usage ```typescript import { evmPlugin } from '@elizaos/plugin-evm'; import { AgentRuntime } from '@elizaos/core'; // Initialize the agent with EVM plugin const runtime = new AgentRuntime({ plugins: [evmPlugin], // ... other configuration }); ``` ## Actions ### Transfer Tokens Transfer native tokens or ERC20 tokens between addresses. Example prompts: * "Send 0.1 ETH to 0x742d35Cc6634C0532925a3b844Bc454e4438f44e" * "Transfer 100 USDC to vitalik.eth on Base" * "Send 50 DAI to 0x123... on Polygon" ### Swap Tokens Exchange tokens on the same chain using optimal routes. Example prompts: * "Swap 1 ETH for USDC" * "Exchange 100 USDT for DAI on Arbitrum" * "Trade my WETH for USDC on Base" ### Bridge Tokens Transfer tokens across different chains. Example prompts: * "Bridge 100 USDC from Ethereum to Arbitrum" * "Move 0.5 ETH from Base to Optimism" * "Transfer DAI from Polygon to Ethereum" ### Governance Actions Participate in DAO governance using OpenZeppelin Governor contracts. Example prompts: * "Create a proposal to increase the treasury allocation" * "Vote FOR on proposal #42" * "Queue proposal #37 for execution" * "Execute the queued proposal #35" ## Providers The plugin includes providers that give your agent awareness of: * **Wallet balances** across all configured chains * **Token metadata** and current prices * **Transaction history** and status ## Supported Chains The plugin supports all chains available in viem, including: * Ethereum Mainnet * Layer 2s: Arbitrum, Optimism, Base, zkSync * Alternative L1s: Polygon, BSC, Avalanche * And many more... ## Advanced Features ### Custom Chain Configuration Add custom RPC endpoints for any supported chain: ```env ETHEREUM_PROVIDER_OPTIMISM=https://opt-mainnet.g.alchemy.com/v2/YOUR_KEY ETHEREUM_PROVIDER_ARBITRUM=https://arb-mainnet.g.alchemy.com/v2/YOUR_KEY ``` ### TEE Wallet Derivation For enhanced security, enable TEE mode to derive wallets in Trusted Execution Environments: ```env TEE_MODE=true WALLET_SECRET_SALT=your_unique_salt ``` ### Multi-Aggregator Swaps The plugin automatically finds the best swap routes using multiple aggregators: * Primary: LiFi SDK * Secondary: Bebop ## Error Handling The plugin includes comprehensive error handling for common scenarios: * Insufficient balance * Network congestion * Failed transactions * Invalid addresses * Slippage protection ## Security Considerations * Never hardcode private keys in your code * Use environment variables for sensitive data * Validate all user inputs * Set appropriate slippage tolerances * Monitor gas prices and limits ## Next Steps * [Complete Documentation →](./evm/complete-documentation.mdx) * [DeFi Operations Flow →](./evm/defi-operations-flow.mdx) * [Examples →](./evm/examples.mdx) * [Testing Guide →](./evm/testing-guide.mdx) # Developer Guide Source: https://docs.elizaos.ai/plugin-registry/defi/evm/complete-documentation Comprehensive guide to the EVM plugin architecture, implementation, and usage This guide provides an in-depth look at the EVM plugin's architecture, components, and implementation details. ## Architecture Overview The EVM plugin follows a modular architecture with clear separation of concerns: ```mermaid flowchart LR A[Actions
User Intent] --> B[Service
EVMService] B --> C[Blockchain
Viem] A --> D[Templates
AI Prompts] B --> E[Providers
Data Supply] ``` ## Core Components ### EVMService The central service that manages blockchain connections and wallet data: ```typescript export class EVMService extends Service { static serviceType = 'evm-service'; private walletProvider: WalletProvider; private intervalId: NodeJS.Timeout | null = null; async initialize(runtime: IAgentRuntime): Promise { // Initialize wallet provider with chain configuration this.walletProvider = await initWalletProvider(runtime); // Set up periodic balance refresh this.intervalId = setInterval( () => this.refreshWalletData(), 60000 // 1 minute ); } async refreshWalletData(): Promise { await this.walletProvider.getChainConfigs(); // Update cached balance data } } ``` ### Actions #### Transfer Action Handles native and ERC20 token transfers: ```typescript export const transferAction: Action = { name: 'EVM_TRANSFER', description: 'Transfer tokens on EVM chains', validate: async (runtime: IAgentRuntime) => { const privateKey = runtime.getSetting('EVM_PRIVATE_KEY'); return !!privateKey || runtime.getSetting('WALLET_PUBLIC_KEY'); }, handler: async (runtime, message, state, options, callback) => { // 1. Extract parameters using AI const params = await extractTransferParams(runtime, message, state); // 2. Validate inputs if (!isAddress(params.toAddress)) { throw new Error('Invalid recipient address'); } // 3. Execute transfer const result = await executeTransfer(params); // 4. Return response callback?.({ text: `Transferred ${params.amount} ${params.token} to ${params.toAddress}`, content: { hash: result.hash } }); } }; ``` #### Swap Action Integrates with multiple DEX aggregators: ```typescript export const swapAction: Action = { name: 'EVM_SWAP', description: 'Swap tokens on the same chain', handler: async (runtime, message, state, options, callback) => { // 1. Extract swap parameters const params = await extractSwapParams(runtime, message, state); // 2. Get quotes from aggregators const quotes = await Promise.all([ getLiFiQuote(params), getBebopQuote(params) ]); // 3. Select best route const bestQuote = selectBestQuote(quotes); // 4. Execute swap const result = await executeSwap(bestQuote); callback?.({ text: `Swapped ${params.fromAmount} ${params.fromToken} for ${result.toAmount} ${params.toToken}`, content: result }); } }; ``` #### Bridge Action Cross-chain token transfers using LiFi: ```typescript export const bridgeAction: Action = { name: 'EVM_BRIDGE', description: 'Bridge tokens across chains', handler: async (runtime, message, state, options, callback) => { const params = await extractBridgeParams(runtime, message, state); // Get bridge route const route = await lifi.getRoutes({ fromChainId: params.fromChain, toChainId: params.toChain, fromTokenAddress: params.fromToken, toTokenAddress: params.toToken, fromAmount: params.amount }); // Execute bridge transaction const result = await lifi.executeRoute(route.routes[0]); callback?.({ text: `Bridging ${params.amount} from ${params.fromChain} to ${params.toChain}`, content: { hash: result.hash, route: route.routes[0] } }); } }; ``` ### Providers #### Wallet Provider Supplies wallet balance information across all chains: ```typescript export const walletProvider: Provider = { name: 'evmWalletProvider', get: async (runtime: IAgentRuntime) => { const service = runtime.getService('evm-service'); const data = await service.getCachedData(); if (!data?.walletInfo) return null; // Format balance information const balances = data.walletInfo.chains .map(chain => `${chain.name}: ${chain.nativeBalance} ${chain.symbol}`) .join('\n'); return `Wallet balances:\n${balances}\n\nTotal value: $${data.walletInfo.totalValueUsd}`; } }; ``` #### Token Balance Provider Dynamic provider for checking specific token balances: ```typescript export const tokenBalanceProvider: Provider = { name: 'evmTokenBalance', get: async (runtime: IAgentRuntime, message: Memory) => { const tokenAddress = extractTokenAddress(message); const chain = extractChain(message); const balance = await getTokenBalance( runtime, tokenAddress, chain ); return `Token balance: ${balance}`; } }; ``` ### Templates AI prompt templates for parameter extraction: ```typescript export const transferTemplate = `Given the recent messages and wallet information: {{recentMessages}} {{walletInfo}} Extract the transfer details: - Amount to transfer (number only) - Recipient address or ENS name - Token symbol (or 'native' for ETH/BNB/etc) - Chain name Respond with: string | null string | null string | null string | null `; ``` ## Chain Configuration The plugin supports dynamic chain configuration: ```typescript interface ChainConfig { chainId: number; name: string; chain: Chain; rpcUrl: string; nativeCurrency: { symbol: string; decimals: number; }; walletClient?: WalletClient; publicClient?: PublicClient; } // Chains are configured based on environment variables const configureChains = (runtime: IAgentRuntime): ChainConfig[] => { const chains: ChainConfig[] = []; // Check for custom RPC endpoints Object.entries(viemChains).forEach(([name, chain]) => { const customRpc = runtime.getSetting(`ETHEREUM_PROVIDER_${name.toUpperCase()}`); chains.push({ chainId: chain.id, name: chain.name, chain, rpcUrl: customRpc || chain.rpcUrls.default.http[0], nativeCurrency: chain.nativeCurrency }); }); return chains; }; ``` ## Token Resolution The plugin automatically resolves token symbols to addresses: ```typescript async function resolveTokenAddress( symbol: string, chainId: number ): Promise
{ // Check common tokens first const commonTokens = { 'USDC': { 1: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 8453: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', // ... other chains }, 'USDT': { 1: '0xdAC17F958D2ee523a2206206994597C13D831ec7', // ... other chains } }; if (commonTokens[symbol]?.[chainId]) { return commonTokens[symbol][chainId]; } // Fallback to LiFi token list const tokens = await lifi.getTokens({ chainId }); const token = tokens.find(t => t.symbol.toLowerCase() === symbol.toLowerCase() ); if (!token) { throw new Error(`Token ${symbol} not found on chain ${chainId}`); } return token.address; } ``` ## Governance Implementation The plugin includes comprehensive DAO governance support: ```typescript // Propose Action export const proposeAction: Action = { name: 'EVM_GOV_PROPOSE', description: 'Create a governance proposal', handler: async (runtime, message, state, options, callback) => { const params = await extractProposalParams(runtime, message, state); const governorContract = getGovernorContract(params.chain); const tx = await governorContract.propose( params.targets, params.values, params.calldatas, params.description ); callback?.({ text: `Created proposal: ${params.description}`, content: { hash: tx.hash } }); } }; // Vote Action export const voteAction: Action = { name: 'EVM_GOV_VOTE', description: 'Vote on a governance proposal', handler: async (runtime, message, state, options, callback) => { const params = await extractVoteParams(runtime, message, state); const voteValue = { 'for': 1, 'against': 0, 'abstain': 2 }[params.support.toLowerCase()]; const tx = await governorContract.castVote( params.proposalId, voteValue ); callback?.({ text: `Voted ${params.support} on proposal ${params.proposalId}`, content: { hash: tx.hash } }); } }; ``` ## Error Handling Comprehensive error handling for common scenarios: ```typescript export async function handleTransactionError( error: any, context: string ): Promise { if (error.code === 'INSUFFICIENT_FUNDS') { throw new Error(`Insufficient funds for ${context}`); } if (error.code === 'NONCE_TOO_LOW') { // Handle nonce issues await resetNonce(); throw new Error('Transaction nonce issue, please retry'); } if (error.message?.includes('gas required exceeds allowance')) { throw new Error(`Gas estimation failed for ${context}`); } // Log unknown errors logger.error(`Unknown error in ${context}:`, error); throw new Error(`Transaction failed: ${error.message}`); } ``` ## Testing The plugin includes comprehensive test coverage: ```typescript describe('EVM Transfer Action', () => { it('should transfer native tokens', async () => { const runtime = await createTestRuntime(); const message = createMessage('Send 0.1 ETH to 0x123...'); const result = await transferAction.handler( runtime, message, state, {}, callback ); expect(result).toBe(true); expect(callback).toHaveBeenCalledWith( expect.objectContaining({ text: expect.stringContaining('Transferred 0.1 ETH') }) ); }); }); ``` ## Best Practices 1. **Always validate addresses** before executing transactions 2. **Use gas buffers** (typically 20%) for reliable execution 3. **Implement retry logic** for network failures 4. **Cache frequently accessed data** to reduce RPC calls 5. **Use simulation** before executing expensive operations 6. **Monitor gas prices** and adjust limits accordingly 7. **Handle slippage** appropriately for swaps 8. **Validate token approvals** before transfers ## Troubleshooting Common issues and solutions: * **"Insufficient funds"**: Check wallet balance includes gas costs * **"Invalid address"**: Ensure address is checksummed correctly * **"Gas estimation failed"**: Try with a fixed gas limit * **"Nonce too low"**: Reset nonce or wait for pending transactions * **"Network error"**: Check RPC endpoint availability # Operations Flow Source: https://docs.elizaos.ai/plugin-registry/defi/evm/defi-operations-flow How DeFi operations work in the EVM plugin ## Overview The EVM plugin handles DeFi operations through a structured flow: ``` User Message → Action Recognition → Parameter Extraction → Execution → Response ``` ## Transfer Flow ### 1. User Intent ``` User: Send 0.1 ETH to alice.eth ``` ### 2. Action Recognition The plugin identifies this as a transfer action based on keywords (send, transfer, pay). ### 3. Parameter Extraction Using AI, the plugin extracts: * Amount: 0.1 * Token: ETH * Recipient: alice.eth (will resolve to address) * Chain: Detected from context or defaults ### 4. Execution * Validates recipient address * Checks balance * Builds transaction * Estimates gas * Sends transaction * Waits for confirmation ### 5. Response ``` Agent: Successfully transferred 0.1 ETH to alice.eth Transaction: https://etherscan.io/tx/[hash] ``` ## Swap Flow ### 1. User Intent ``` User: Swap 1 ETH for USDC ``` ### 2. Route Discovery * Queries multiple DEX aggregators (LiFi, Bebop) * Compares routes for best output * Considers gas costs ### 3. Execution * Approves token if needed * Executes swap transaction * Monitors for completion ## Bridge Flow ### 1. User Intent ``` User: Bridge 100 USDC from Ethereum to Base ``` ### 2. Bridge Route * Finds available bridge routes * Estimates fees and time * Selects optimal path ### 3. Multi-Step Execution * Source chain transaction * Wait for bridge confirmation * Destination chain completion ## Governance Flow ### Proposal Creation ``` User: Create a proposal to increase treasury allocation → Plugin creates proposal transaction with targets, values, and description ``` ### Voting ``` User: Vote FOR on proposal 42 → Plugin casts vote with correct proposal ID and support value ``` ## Error Handling The plugin handles common errors gracefully: * **Insufficient Balance**: Checks before attempting transaction * **Network Issues**: Retries with exponential backoff * **Invalid Addresses**: Validates all addresses before use * **High Slippage**: Warns user if slippage exceeds tolerance ## Key Features 1. **Natural Language Processing**: Understands various ways to express intents 2. **Multi-Chain Support**: Automatically handles chain selection 3. **Gas Optimization**: Estimates and optimizes gas usage 4. **Safety Checks**: Validates all parameters before execution 5. **Real-Time Feedback**: Provides transaction status updates # Examples Source: https://docs.elizaos.ai/plugin-registry/defi/evm/examples Practical examples for configuring and using the EVM plugin ## Configuration ### Character Configuration Add the EVM plugin to your character file: ```typescript // character.ts import { type Character } from '@elizaos/core'; export const character: Character = { name: 'DeFiAgent', plugins: [ // Core plugins '@elizaos/plugin-sql', '@elizaos/plugin-bootstrap', // DeFi plugin ...(process.env.EVM_PRIVATE_KEY?.trim() ? ['@elizaos/plugin-evm'] : []), // Platform plugins ...(process.env.DISCORD_API_TOKEN?.trim() ? ['@elizaos/plugin-discord'] : []), ], settings: { secrets: {}, }, // ... rest of character configuration }; ``` ### Environment Variables ```env # Required EVM_PRIVATE_KEY=your_private_key_here # Optional - Custom RPC endpoints ETHEREUM_PROVIDER_ETHEREUM=https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY ETHEREUM_PROVIDER_BASE=https://base-mainnet.g.alchemy.com/v2/YOUR_KEY ETHEREUM_PROVIDER_ARBITRUM=https://arb-mainnet.g.alchemy.com/v2/YOUR_KEY # Optional - TEE Mode TEE_MODE=true WALLET_SECRET_SALT=your_salt_here ``` ## Usage Examples ### Transfer Operations The agent understands natural language for transfers: ``` User: Send 0.1 ETH to 0x742d35Cc6634C0532925a3b844Bc454e4438f44e Agent: I'll send 0.1 ETH to that address right away. User: Transfer 100 USDC to vitalik.eth on Base Agent: Transferring 100 USDC to vitalik.eth on Base network. User: Pay alice.eth 50 DAI on Arbitrum Agent: Sending 50 DAI to alice.eth on Arbitrum. ``` ### Swap Operations ``` User: Swap 1 ETH for USDC Agent: I'll swap 1 ETH for USDC using the best available route. User: Exchange 100 USDC for DAI with 0.5% slippage Agent: Swapping 100 USDC for DAI with 0.5% slippage tolerance. ``` ### Bridge Operations ``` User: Bridge 100 USDC from Ethereum to Base Agent: I'll bridge 100 USDC from Ethereum to Base network. User: Move 0.5 ETH from Arbitrum to Optimism Agent: Bridging 0.5 ETH from Arbitrum to Optimism. ``` ### Governance Operations ``` User: Create a proposal to increase the treasury allocation to 10% Agent: I'll create a governance proposal for increasing treasury allocation. User: Vote FOR on proposal 42 Agent: Casting your vote FOR proposal #42. User: Execute proposal 35 Agent: Executing proposal #35 after the timelock period. ``` ## Custom Plugin Integration If you need to import the plugin directly in a ProjectAgent: ```typescript // index.ts import { type ProjectAgent } from '@elizaos/core'; import evmPlugin from '@elizaos/plugin-evm'; import { character } from './character'; export const projectAgent: ProjectAgent = { character, plugins: [evmPlugin], // Import custom plugins here init: async (runtime) => { // Custom initialization if needed } }; ``` ## Common Patterns ### Checking Wallet Balance ``` User: What's my wallet balance? Agent: [Agent will use the wallet provider to show balances across all configured chains] ``` ### Gas Price Awareness ``` User: Send 0.1 ETH to alice.eth when gas is low Agent: I'll monitor gas prices and execute when they're favorable. ``` ### Multi-Chain Operations The plugin automatically detects the chain from context: ``` User: Send 100 USDC on Base Agent: Sending 100 USDC on Base network. User: Swap MATIC for USDC on Polygon Agent: Swapping MATIC for USDC on Polygon network. ``` # Testing Guide Source: https://docs.elizaos.ai/plugin-registry/defi/evm/testing-guide How to test the EVM plugin safely on real networks ## Testing Philosophy The best way to test DeFi plugins is with small amounts on real networks. Test networks often have reliability issues and don't reflect real-world conditions. ## Safe Testing Practices ### 1. Start Small Always test with minimal amounts first: * 0.001 ETH for transfers * \$1-5 worth of tokens for swaps * Smallest viable amounts for bridges ### 2. Test on Low-Cost Chains First Start testing on chains with low transaction fees: * Polygon: \~\$0.01 per transaction * Base: \~\$0.05 per transaction * Arbitrum: \~\$0.10 per transaction ### 3. Progressive Testing ``` 1. Test basic transfers first 2. Test token transfers 3. Test swaps with small amounts 4. Test bridges last (they're most complex) ``` ## Testing Checklist ### Environment Setup ```env # Use a dedicated test wallet EVM_PRIVATE_KEY=test_wallet_private_key # Start with one chain ETHEREUM_PROVIDER_BASE=https://base-mainnet.g.alchemy.com/v2/YOUR_KEY ``` ### Basic Tests 1. **Wallet Connection** ``` User: What's my wallet address? Agent: [Should show your wallet address] ``` 2. **Balance Check** ``` User: What's my balance? Agent: [Should show balances across configured chains] ``` 3. **Small Transfer** ``` User: Send 0.001 ETH to [another test address] Agent: [Should execute the transfer] ``` 4. **Token Transfer** ``` User: Send 1 USDC to [test address] Agent: [Should handle ERC20 transfer] ``` ### Swap Testing Test swaps with minimal amounts: ``` User: Swap 0.01 ETH for USDC Agent: [Should find best route and execute] ``` ### Error Handling Test error scenarios: * Insufficient balance * Invalid addresses * Network issues * High slippage ## Monitoring Results 1. **Transaction Verification** * Check block explorers (Etherscan, BaseScan, etc.) * Verify transaction status * Confirm balances updated 2. **Gas Usage** * Monitor gas costs * Ensure reasonable gas estimates * Check for failed transactions ## Common Issues ### "Insufficient funds for gas" * Ensure you have native tokens for gas * Each chain needs its native token (ETH, MATIC, etc.) ### "Transaction underpriced" * RPC may be congested * Try alternative RPC endpoints ### "Nonce too low" * Previous transaction may be pending * Wait for confirmation or reset nonce ## Production Readiness Before using in production: 1. Test all intended operations 2. Verify error handling works 3. Ensure proper logging 4. Set appropriate gas limits 5. Configure slippage tolerances 6. Test with your expected volumes # Overview Source: https://docs.elizaos.ai/plugin-registry/defi/solana Enable high-performance Solana blockchain interactions for your AI agent The Solana plugin provides comprehensive integration with the Solana blockchain, enabling AI agents to manage wallets, transfer tokens, perform swaps, and track portfolios with real-time market data. ## Features * **Native SOL & SPL Tokens**: Transfer SOL and any SPL token * **DeFi Integration**: Token swaps via Jupiter aggregator * **Portfolio Management**: Real-time balance tracking with USD valuations * **Market Data**: Live price feeds for SOL, BTC, ETH, and SPL tokens * **AI-Powered**: Natural language understanding for all operations * **WebSocket Support**: Real-time account monitoring and updates ## Installation ```bash elizaos plugins add solana ``` ## Configuration The plugin requires the following environment variables: ```env # Required - Wallet Configuration SOLANA_PRIVATE_KEY=your_base58_private_key_here # OR SOLANA_PUBLIC_KEY=your_public_key_here # For read-only mode # Optional - RPC Configuration SOLANA_RPC_URL=https://api.mainnet-beta.solana.com HELIUS_API_KEY=your_helius_api_key # Optional - Market Data BIRDEYE_API_KEY=your_birdeye_api_key # Optional - AI Service OPENAI_API_KEY=your_openai_api_key # For enhanced parsing ``` ## Usage ```typescript import { solanaPlugin } from '@elizaos/plugin-solana'; import { AgentRuntime } from '@elizaos/core'; // Initialize the agent with Solana plugin const runtime = new AgentRuntime({ plugins: [solanaPlugin], // ... other configuration }); ``` ## Actions ### Transfer Tokens Send SOL or SPL tokens to any Solana address. Example prompts: * "Send 1 SOL to 7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU" * "Transfer 100 USDC to alice.sol" * "Send 50 BONK tokens to Bob's wallet" ### Swap Tokens Exchange tokens using Jupiter's aggregator for best prices. Example prompts: * "Swap 10 SOL for USDC" * "Exchange all my BONK for SOL" * "Trade 100 USDC for RAY with 1% slippage" ## Providers The plugin includes a comprehensive wallet provider that gives your agent awareness of: * **Total portfolio value** in USD and SOL * **Individual token balances** with current prices * **Real-time updates** via WebSocket subscriptions * **Token metadata** including symbols and decimals ## Key Features ### AI-Powered Intent Parsing The plugin uses advanced prompt engineering to understand natural language: ```typescript // The AI understands various ways to express the same intent: "Send 1 SOL to alice.sol" "Transfer 1 SOL to alice" "Pay alice 1 SOL" "Give 1 SOL to alice.sol" ``` ### Automatic Token Resolution No need to specify token addresses - just use symbols: * Automatically resolves token symbols to mint addresses * Fetches current token metadata * Validates token existence before transactions ### Real-Time Portfolio Tracking * Updates every 2 minutes automatically * WebSocket subscriptions for instant updates * Comprehensive USD valuations using Birdeye API ### High-Performance Architecture * Connection pooling for optimal RPC usage * Intelligent caching to minimize API calls * Retry logic with exponential backoff * Transaction simulation before execution ## Advanced Configuration ### Using Helius RPC For enhanced performance and reliability: ```env SOLANA_RPC_URL=https://mainnet.helius-rpc.com/?api-key=YOUR_KEY HELIUS_API_KEY=your_helius_api_key ``` ### Custom Network Configuration Connect to devnet or custom networks: ```env SOLANA_RPC_URL=https://api.devnet.solana.com SOLANA_CLUSTER=devnet ``` ### Public Key Only Mode For read-only operations without a private key: ```env SOLANA_PUBLIC_KEY=7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU ``` ## Error Handling The plugin includes robust error handling for: * Insufficient balance errors * Network timeouts and failures * Invalid addresses or tokens * Slippage tolerance exceeded * Transaction simulation failures ## Security Considerations * Private keys support both base58 and base64 formats * Never expose private keys in logs or responses * Use public key mode when write access isn't needed * Validate all user inputs before execution * Set appropriate slippage for swaps ## Performance Tips * Use Helius or other premium RPCs for production * Enable WebSocket connections for real-time updates * Configure appropriate cache TTLs * Monitor rate limits on external APIs ## Next Steps * [Complete Documentation →](./solana/complete-documentation.mdx) * [DeFi Operations Flow →](./solana/defi-operations-flow.mdx) * [Examples →](./solana/examples.mdx) * [Testing Guide →](./solana/testing-guide.mdx) # Developer Guide Source: https://docs.elizaos.ai/plugin-registry/defi/solana/complete-documentation In-depth technical documentation for the Solana blockchain plugin This guide provides comprehensive documentation of the Solana plugin's architecture, implementation, and advanced features. ## Architecture Overview The Solana plugin follows a modular architecture optimized for high-performance blockchain interactions: ```mermaid flowchart LR A[Actions
User Intent] --> B[SolanaService
Core Logic] B --> C[Solana RPC
Connection] A --> D[AI Templates
NLP Parsing] B --> E[Providers
Wallet Data] C --> F[Birdeye API
Price Data] ``` ## Core Components ### SolanaService The central service managing all Solana blockchain interactions: ```typescript export class SolanaService extends Service { static serviceType = 'solana-service'; private connection: Connection; private keypair?: Keypair; private wallet?: Wallet; private cache: Map = new Map(); private subscriptions: number[] = []; async initialize(runtime: IAgentRuntime): Promise { // Initialize connection const rpcUrl = runtime.getSetting('SOLANA_RPC_URL') || 'https://api.mainnet-beta.solana.com'; this.connection = new Connection(rpcUrl, { commitment: 'confirmed', wsEndpoint: rpcUrl.replace('https', 'wss') }); // Initialize wallet const privateKey = runtime.getSetting('SOLANA_PRIVATE_KEY'); if (privateKey) { this.keypair = await loadKeypair(privateKey); this.wallet = new Wallet(this.keypair); } // Start portfolio monitoring this.startPortfolioTracking(); // Register with trader service if available this.registerWithTraderService(runtime); } private async startPortfolioTracking(): Promise { // Initial fetch await this.fetchPortfolioData(); // Set up periodic refresh (2 minutes) setInterval(() => this.fetchPortfolioData(), 120000); // Set up WebSocket subscriptions if (this.keypair) { this.setupAccountSubscriptions(); } } } ``` ### Actions #### Transfer Action Handles SOL and SPL token transfers with intelligent parsing: ```typescript export const transferAction: Action = { name: 'TRANSFER_SOLANA', description: 'Transfer SOL or SPL tokens on Solana', validate: async (runtime: IAgentRuntime) => { const privateKey = runtime.getSetting('SOLANA_PRIVATE_KEY'); return !!privateKey; }, handler: async (runtime, message, state, options, callback) => { try { // Extract parameters using AI const params = await extractTransferParams(runtime, message, state); // Get service instance const service = runtime.getService('solana-service'); // Execute transfer const result = await executeTransfer(service, params); callback?.({ text: `Successfully transferred ${params.amount} ${params.token} to ${params.recipient}`, content: { success: true, signature: result.signature, amount: params.amount, token: params.token, recipient: params.recipient } }); } catch (error) { callback?.({ text: `Transfer failed: ${error.message}`, content: { error: error.message } }); } }, examples: [ [ { name: 'user', content: { text: 'Send 1 SOL to 7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU' } }, { name: 'assistant', content: { text: "I'll send 1 SOL to that address right away." } } ] ], similes: ['SEND_SOL', 'SEND_TOKEN_SOLANA', 'TRANSFER_SOL', 'PAY_SOL'] }; ``` #### Swap Action Token swapping using Jupiter aggregator: ```typescript export const swapAction: Action = { name: 'SWAP_SOLANA', description: 'Swap tokens on Solana using Jupiter', handler: async (runtime, message, state, options, callback) => { // Extract swap parameters const params = await extractSwapParams(runtime, message, state); // Get Jupiter quote const quote = await getJupiterQuote({ inputMint: params.fromToken, outputMint: params.toToken, amount: params.amount, slippageBps: params.slippage * 100 // Convert to basis points }); // Execute swap const result = await executeJupiterSwap( service.connection, service.wallet, quote ); callback?.({ text: `Swapped ${params.fromAmount} ${params.fromSymbol} for ${formatAmount(quote.outAmount)} ${params.toSymbol}`, content: { success: true, signature: result.signature, fromAmount: params.fromAmount, toAmount: formatAmount(quote.outAmount), route: quote.routePlan } }); } }; ``` ### Providers #### Wallet Provider Supplies comprehensive wallet and portfolio data: ```typescript export const walletProvider: Provider = { name: 'solana-wallet', description: 'Provides Solana wallet information and portfolio data', get: async (runtime: IAgentRuntime, message?: Memory, state?: State) => { const service = runtime.getService('solana-service'); const portfolioData = await service.getCachedPortfolioData(); if (!portfolioData) { return 'Wallet data unavailable'; } // Format portfolio for AI context const summary = formatPortfolioSummary(portfolioData); const tokenList = formatTokenBalances(portfolioData.tokens); return `Solana Wallet Portfolio: Total Value: $${portfolioData.totalUsd.toFixed(2)} (${portfolioData.totalSol.toFixed(4)} SOL) Token Balances: ${tokenList} SOL Price: $${portfolioData.solPrice.toFixed(2)} Last Updated: ${new Date(portfolioData.lastUpdated).toLocaleString()}`; } }; ``` ### Templates AI prompt templates for natural language understanding: ```typescript export const transferTemplate = `Given the recent messages: {{recentMessages}} And wallet information: {{walletInfo}} Extract the following for a Solana transfer: - Amount to send (number only) - Token to send (SOL or token symbol/address) - Recipient address or domain Respond with: string string string `; export const swapTemplate = `Given the swap request: {{recentMessages}} And available tokens: {{walletInfo}} Extract swap details: - Input token (symbol or address) - Input amount (or "all" for max) - Output token (symbol or address) - Slippage tolerance (percentage, default 1%) string string string number `; ``` ## Advanced Features ### Keypair Management The plugin supports multiple key formats and secure handling: ```typescript export async function loadKeypair(privateKey: string): Promise { try { // Try base58 format first const decoded = bs58.decode(privateKey); if (decoded.length === 64) { return Keypair.fromSecretKey(decoded); } } catch (e) { // Not base58, try base64 } try { // Try base64 format const decoded = Buffer.from(privateKey, 'base64'); if (decoded.length === 64) { return Keypair.fromSecretKey(decoded); } } catch (e) { // Not base64 } // Try JSON format (Solana CLI) try { const parsed = JSON.parse(privateKey); if (Array.isArray(parsed)) { return Keypair.fromSecretKey(Uint8Array.from(parsed)); } } catch (e) { // Not JSON } throw new Error('Invalid private key format'); } ``` ### WebSocket Subscriptions Real-time account monitoring for instant updates: ```typescript private setupAccountSubscriptions(): void { if (!this.keypair) return; // Subscribe to account changes const accountSub = this.connection.onAccountChange( this.keypair.publicKey, (accountInfo) => { elizaLogger.info('Account balance changed:', { lamports: accountInfo.lamports, sol: accountInfo.lamports / LAMPORTS_PER_SOL }); // Trigger portfolio refresh this.fetchPortfolioData(); }, 'confirmed' ); this.subscriptions.push(accountSub); // Subscribe to token accounts this.subscribeToTokenAccounts(); } private async subscribeToTokenAccounts(): Promise { const tokenAccounts = await this.connection.getParsedTokenAccountsByOwner( this.keypair.publicKey, { programId: TOKEN_PROGRAM_ID } ); tokenAccounts.value.forEach(({ pubkey }) => { const sub = this.connection.onAccountChange( pubkey, () => { elizaLogger.info('Token balance changed'); this.fetchPortfolioData(); }, 'confirmed' ); this.subscriptions.push(sub); }); } ``` ### Portfolio Data Management Efficient caching and data fetching: ```typescript interface PortfolioData { totalUsd: number; totalSol: number; solPrice: number; tokens: TokenBalance[]; lastUpdated: number; } private async fetchPortfolioData(): Promise { const cacheKey = 'portfolio_data'; const cached = this.cache.get(cacheKey); // Return cached data if fresh (2 minutes) if (cached && Date.now() - cached.timestamp < 120000) { return cached.data; } try { // Fetch from Birdeye API const response = await fetch( `https://api.birdeye.so/v1/wallet/portfolio?wallet=${this.keypair.publicKey.toBase58()}`, { headers: { 'X-API-KEY': this.runtime.getSetting('BIRDEYE_API_KEY') } } ); const data = await response.json(); // Process and cache const portfolioData = this.processPortfolioData(data); this.cache.set(cacheKey, { data: portfolioData, timestamp: Date.now() }); return portfolioData; } catch (error) { elizaLogger.error('Failed to fetch portfolio data:', error); return cached?.data || this.getEmptyPortfolio(); } } ``` ### Transaction Building Optimized transaction construction with priority fees: ```typescript async function buildTransferTransaction( connection: Connection, sender: PublicKey, recipient: PublicKey, amount: number, token?: string ): Promise { const transaction = new Transaction(); // Add priority fee for faster processing const priorityFee = ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 1000 // 0.001 SOL per compute unit }); transaction.add(priorityFee); if (!token || token.toUpperCase() === 'SOL') { // Native SOL transfer transaction.add( SystemProgram.transfer({ fromPubkey: sender, toPubkey: recipient, lamports: amount * LAMPORTS_PER_SOL }) ); } else { // SPL token transfer const mint = await resolveTokenMint(connection, token); const senderAta = await getAssociatedTokenAddress(mint, sender); const recipientAta = await getAssociatedTokenAddress(mint, recipient); // Check if recipient ATA exists const recipientAccount = await connection.getAccountInfo(recipientAta); if (!recipientAccount) { // Create ATA for recipient transaction.add( createAssociatedTokenAccountInstruction( sender, recipientAta, recipient, mint ) ); } // Add transfer instruction transaction.add( createTransferInstruction( senderAta, recipientAta, sender, amount * Math.pow(10, await getTokenDecimals(connection, mint)) ) ); } // Get latest blockhash const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash(); transaction.recentBlockhash = blockhash; transaction.lastValidBlockHeight = lastValidBlockHeight; transaction.feePayer = sender; return transaction; } ``` ### Token Resolution Intelligent token symbol to mint address resolution: ```typescript async function resolveTokenMint( connection: Connection, tokenIdentifier: string ): Promise { // Check if it's already a valid public key try { const pubkey = new PublicKey(tokenIdentifier); // Verify it's a token mint const accountInfo = await connection.getAccountInfo(pubkey); if (accountInfo?.owner.equals(TOKEN_PROGRAM_ID)) { return pubkey; } } catch (e) { // Not a valid public key, continue } // Common token mappings const commonTokens: Record = { 'USDC': 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', 'USDT': 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB', 'BONK': 'DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263', 'RAY': '4k3Dyjzvzp8eMZWUXbBCjEvwSkkk59S5iCNLY3QrkX6R', 'JTO': 'jtojtomepa8beP8AuQc6eXt5FriJwfFMwQx2v2f9mCL', // Add more as needed }; const upperToken = tokenIdentifier.toUpperCase(); if (commonTokens[upperToken]) { return new PublicKey(commonTokens[upperToken]); } // Try to fetch from token list or registry throw new Error(`Unknown token: ${tokenIdentifier}`); } ``` ### Jupiter Integration Advanced swap execution with route optimization: ```typescript interface JupiterSwapParams { inputMint: PublicKey; outputMint: PublicKey; amount: number; slippageBps: number; userPublicKey: PublicKey; } async function getJupiterQuote(params: JupiterSwapParams): Promise { const url = new URL('https://quote-api.jup.ag/v6/quote'); url.searchParams.append('inputMint', params.inputMint.toBase58()); url.searchParams.append('outputMint', params.outputMint.toBase58()); url.searchParams.append('amount', params.amount.toString()); url.searchParams.append('slippageBps', params.slippageBps.toString()); url.searchParams.append('onlyDirectRoutes', 'false'); url.searchParams.append('asLegacyTransaction', 'false'); const response = await fetch(url.toString()); if (!response.ok) { throw new Error(`Jupiter quote failed: ${response.statusText}`); } return response.json(); } async function executeJupiterSwap( connection: Connection, wallet: Wallet, quote: QuoteResponse ): Promise<{ signature: string }> { // Get serialized transaction from Jupiter const swapResponse = await fetch('https://quote-api.jup.ag/v6/swap', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ quoteResponse: quote, userPublicKey: wallet.publicKey.toBase58(), wrapAndUnwrapSol: true, prioritizationFeeLamports: 'auto' }) }); const { swapTransaction } = await swapResponse.json(); // Deserialize and sign const transaction = VersionedTransaction.deserialize( Buffer.from(swapTransaction, 'base64') ); transaction.sign([wallet.payer]); // Send with confirmation const signature = await connection.sendTransaction(transaction, { skipPreflight: false, maxRetries: 3 }); // Wait for confirmation const confirmation = await connection.confirmTransaction({ signature, blockhash: transaction.message.recentBlockhash, lastValidBlockHeight: transaction.message.lastValidBlockHeight }); if (confirmation.value.err) { throw new Error(`Swap failed: ${confirmation.value.err}`); } return { signature }; } ``` ### Error Handling Comprehensive error handling with retry logic: ```typescript export async function withRetry( operation: () => Promise, options: { maxAttempts?: number; delay?: number; backoff?: number; onError?: (error: Error, attempt: number) => void; } = {} ): Promise { const { maxAttempts = 3, delay = 1000, backoff = 2, onError } = options; let lastError: Error; for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { return await operation(); } catch (error) { lastError = error; onError?.(error, attempt); if (attempt < maxAttempts) { const waitTime = delay * Math.pow(backoff, attempt - 1); elizaLogger.warn(`Attempt ${attempt} failed, retrying in ${waitTime}ms`, { error: error.message }); await new Promise(resolve => setTimeout(resolve, waitTime)); } } } throw lastError; } // Usage const result = await withRetry( () => connection.sendTransaction(transaction), { maxAttempts: 3, onError: (error, attempt) => { if (error.message.includes('blockhash not found')) { // Refresh blockhash transaction.recentBlockhash = (await connection.getLatestBlockhash()).blockhash; } } } ); ``` ### Performance Optimizations #### Connection Pooling ```typescript class ConnectionPool { private connections: Connection[] = []; private currentIndex = 0; constructor(rpcUrls: string[], config?: ConnectionConfig) { this.connections = rpcUrls.map(url => new Connection(url, config)); } getConnection(): Connection { const connection = this.connections[this.currentIndex]; this.currentIndex = (this.currentIndex + 1) % this.connections.length; return connection; } async healthCheck(): Promise { const checks = this.connections.map(async (conn, index) => { try { await conn.getVersion(); return { index, healthy: true }; } catch (error) { return { index, healthy: false, error }; } }); const results = await Promise.all(checks); const unhealthy = results.filter(r => !r.healthy); if (unhealthy.length > 0) { elizaLogger.warn('Unhealthy connections:', unhealthy); } } } ``` #### Batch Operations ```typescript async function batchGetMultipleAccounts( connection: Connection, publicKeys: PublicKey[] ): Promise<(AccountInfo | null)[]> { const BATCH_SIZE = 100; const results: (AccountInfo | null)[] = []; for (let i = 0; i < publicKeys.length; i += BATCH_SIZE) { const batch = publicKeys.slice(i, i + BATCH_SIZE); const batchResults = await connection.getMultipleAccountsInfo(batch); results.push(...batchResults); } return results; } ``` ## Security Considerations 1. **Private Key Security** * Never log or expose private keys * Support multiple secure key formats * Use environment variables only 2. **Transaction Validation** * Always simulate before sending * Verify recipient addresses * Check token mint addresses 3. **Slippage Protection** * Default 1% slippage * Maximum 5% slippage * User confirmation for high slippage 4. **Rate Limiting** * Implement request throttling * Cache frequently accessed data * Use WebSocket for real-time data ## Monitoring & Logging The plugin provides detailed logging for debugging and monitoring: ```typescript // Transaction lifecycle elizaLogger.info('Transfer initiated', { amount, token, recipient }); elizaLogger.debug('Transaction built', { instructions: tx.instructions.length }); elizaLogger.info('Transaction sent', { signature }); elizaLogger.info('Transaction confirmed', { signature, slot }); // Performance metrics elizaLogger.debug('RPC latency', { method, duration }); elizaLogger.debug('Cache hit rate', { hits, misses, ratio }); // Error tracking elizaLogger.error('Transaction failed', { error, context }); elizaLogger.warn('Retry attempt', { attempt, maxAttempts }); ``` # Operations Flow Source: https://docs.elizaos.ai/plugin-registry/defi/solana/defi-operations-flow How DeFi operations work in the Solana plugin ## Overview The Solana plugin processes DeFi operations through this flow: ``` User Message → Action Recognition → AI Parameter Extraction → Execution → Response ``` ## Transfer Flow ### 1. User Intent ``` User: Send 1 SOL to alice.sol ``` ### 2. Action Recognition The plugin identifies transfer keywords (send, transfer, pay). ### 3. Parameter Extraction AI extracts: * Amount: 1 * Token: SOL * Recipient: alice.sol (resolves to address) ### 4. Execution Steps * Resolve .sol domain if needed * Check balance * Build transaction with priority fee * Sign and send * Wait for confirmation ### 5. Response ``` Agent: Successfully sent 1 SOL to alice.sol Transaction: https://solscan.io/tx/[signature] ``` ## Swap Flow ### 1. User Intent ``` User: Swap 10 SOL for USDC ``` ### 2. Jupiter Integration * Get quote from Jupiter API * Calculate output amount * Check price impact ### 3. Execution * Build swap transaction * Add priority fees * Execute and monitor ### 4. Special Cases * "Swap all" - calculates max balance * Custom slippage - applies user preference * Route selection - optimizes for best price ## Portfolio Flow ### 1. User Request ``` User: What's my portfolio worth? ``` ### 2. Data Aggregation * Fetch SOL balance * Get SPL token balances * Query prices from Birdeye API ### 3. Response Format ``` Total Value: $X,XXX.XX (XX.XX SOL) Token Balances: SOL: 10.5 ($850.50) USDC: 250.25 ($250.25) BONK: 1,000,000 ($45.20) ``` ## Key Features ### Real-Time Updates * WebSocket subscriptions for balance changes * Automatic portfolio refresh every 2 minutes * Instant transaction notifications ### Smart Token Resolution * Common symbols (USDC, USDT, BONK) auto-resolved * .sol domain support * Token metadata caching ### Transaction Optimization * Priority fees for faster confirmation * Compute unit optimization * Automatic retry on failure ## Error Handling ### Common Errors * **Insufficient Balance**: Pre-checks prevent failed transactions * **Token Not Found**: Clear error messages for unknown tokens * **Network Issues**: Automatic retry with backoff * **High Slippage**: Warns before executing ### Safety Features 1. Balance validation before execution 2. Address verification 3. Slippage protection 4. Transaction simulation when possible # Examples Source: https://docs.elizaos.ai/plugin-registry/defi/solana/examples Practical examples for configuring and using the Solana plugin ## Configuration ### Character Configuration Add the Solana plugin to your character file: ```typescript // character.ts import { type Character } from '@elizaos/core'; export const character: Character = { name: 'SolanaAgent', plugins: [ // Core plugins '@elizaos/plugin-sql', '@elizaos/plugin-bootstrap', // Solana plugin ...(process.env.SOLANA_PRIVATE_KEY?.trim() ? ['@elizaos/plugin-solana'] : []), // Platform plugins ...(process.env.DISCORD_API_TOKEN?.trim() ? ['@elizaos/plugin-discord'] : []), ], settings: { secrets: {}, }, // ... rest of character configuration }; ``` ### Environment Variables ```env # Required - Choose one: SOLANA_PRIVATE_KEY=your_base58_private_key_here # OR for read-only mode: SOLANA_PUBLIC_KEY=your_public_key_here # Optional - Enhanced RPC SOLANA_RPC_URL=https://mainnet.helius-rpc.com/?api-key=YOUR_KEY HELIUS_API_KEY=your_helius_key # Optional - Market data BIRDEYE_API_KEY=your_birdeye_key ``` ## Usage Examples ### Transfer Operations The agent understands natural language for transfers: ``` User: Send 1 SOL to 7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU Agent: I'll send 1 SOL to that address right away. User: Transfer 100 USDC to alice.sol Agent: Transferring 100 USDC to alice.sol. User: Pay bob 50 BONK tokens Agent: Sending 50 BONK to bob. ``` ### Swap Operations ``` User: Swap 10 SOL for USDC Agent: I'll swap 10 SOL for USDC using Jupiter. User: Exchange all my BONK for SOL Agent: Swapping all your BONK tokens for SOL. User: Trade 100 USDC for JTO with 2% slippage Agent: Swapping 100 USDC for JTO with 2% slippage tolerance. ``` ### Portfolio Management ``` User: What's my wallet balance? Agent: [Shows total portfolio value and individual token balances] User: How much is my portfolio worth? Agent: Your total portfolio value is $X,XXX.XX (XX.XX SOL) ``` ## Custom Plugin Integration If you need to import the plugin directly in a ProjectAgent: ```typescript // index.ts import { type ProjectAgent } from '@elizaos/core'; import solanaPlugin from '@elizaos/plugin-solana'; import { character } from './character'; export const projectAgent: ProjectAgent = { character, plugins: [solanaPlugin], // Import custom plugins here init: async (runtime) => { // Custom initialization if needed } }; ``` ## Common Patterns ### Domain Name Resolution The plugin automatically resolves .sol domains: ``` User: Send 5 SOL to vitalik.sol Agent: Sending 5 SOL to vitalik.sol [resolves to actual address] ``` ### Token Symbol Resolution Common tokens are automatically recognized: ``` User: Send 100 USDC to alice Agent: [Recognizes USDC token mint and handles transfer] ``` ### All Balance Swaps ``` User: Swap all my BONK for USDC Agent: [Calculates max balance and executes swap] ``` ### Slippage Control ``` User: Swap with 0.5% slippage Agent: [Sets custom slippage for the swap] ``` # Testing Guide Source: https://docs.elizaos.ai/plugin-registry/defi/solana/testing-guide How to test the Solana plugin safely on mainnet ## Testing Philosophy Test with small amounts on mainnet. Solana devnet/testnet tokens have no value and often have different behavior than mainnet. ## Safe Testing Practices ### 1. Start Small Test with minimal amounts: * 0.001 SOL for transfers (\~\$0.20) * \$1-5 worth of tokens for swaps * Use common tokens (USDC, USDT) for reliability ### 2. Transaction Costs Solana transactions are cheap (\~\$0.00025 per transaction), making mainnet testing affordable. ### 3. Progressive Testing ``` 1. Check wallet connection 2. Test SOL transfers 3. Test SPL token transfers 4. Test small swaps 5. Test larger operations ``` ## Testing Checklist ### Environment Setup ```env # Use a dedicated test wallet SOLANA_PRIVATE_KEY=test_wallet_private_key # Optional - Use premium RPC for reliability SOLANA_RPC_URL=https://mainnet.helius-rpc.com/?api-key=YOUR_KEY ``` ### Basic Tests 1. **Wallet Connection** ``` User: What's my wallet address? Agent: [Should show your Solana address] ``` 2. **Balance Check** ``` User: What's my balance? Agent: [Should show SOL balance and token holdings] ``` 3. **Small SOL Transfer** ``` User: Send 0.001 SOL to [another address] Agent: [Should execute the transfer] ``` 4. **Token Transfer** ``` User: Send 1 USDC to [test address] Agent: [Should handle SPL token transfer] ``` ### Swap Testing Test swaps with small amounts: ``` User: Swap 0.1 SOL for USDC Agent: [Should execute via Jupiter] ``` ### Portfolio Tracking ``` User: What's my portfolio worth? Agent: [Should show total USD value and token breakdown] ``` ## Monitoring Results 1. **Transaction Verification** * Check on Solscan.io or Solana Explorer * Verify transaction succeeded * Confirm balance changes 2. **Common Token Addresses** * USDC: EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v * USDT: Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB * Use these for testing as they're widely supported ## Common Issues ### "Insufficient SOL for fees" * Need \~0.001 SOL for transaction fees * Keep some SOL for rent and fees ### "Token account doesn't exist" * First transfer to a new token creates the account * Costs \~0.002 SOL for account creation ### "Slippage tolerance exceeded" * Increase slippage for volatile tokens * Try smaller amounts ## Production Readiness Before production use: 1. Test all operations you plan to use 2. Verify error handling 3. Test with your expected token types 4. Monitor transaction success rates 5. Set appropriate slippage (1-3% typical) 6. Ensure adequate SOL for fees # Knowledge & RAG System Source: https://docs.elizaos.ai/plugin-registry/knowledge The core RAG (Retrieval-Augmented Generation) system for elizaOS agents The Knowledge Plugin is elizaOS's core RAG system, providing intelligent document management and retrieval capabilities. It enables agents to maintain long-term memory, answer questions from uploaded documents, and learn from conversations. ## Key Features Works out of the box with sensible defaults Supports PDF, TXT, MD, DOCX, CSV, and more Smart chunking and contextual embeddings 90% cost reduction with caching ## Quick Links Get up and running in 5 minutes Essential settings and options Comprehensive technical documentation Recipes and code samples ## What is the Knowledge Plugin? The Knowledge Plugin transforms your elizaOS agent into an intelligent knowledge base that can: * **Store and retrieve documents** in multiple formats * **Answer questions** using semantic search * **Learn from conversations** automatically * **Process web content** via URL ingestion * **Manage documents** through a built-in web interface ## Core Capabilities ### Document Processing * Automatic text extraction from PDFs, Word docs, and more * Smart chunking with configurable overlap * Content-based deduplication * Metadata preservation and enrichment ### Retrieval & RAG * Semantic search with vector embeddings * Automatic context injection into conversations * Relevance scoring and ranking * Multi-modal retrieval support ### Management Interface * Web-based document browser * Upload, view, and delete documents * Search and filter capabilities * Real-time processing status ## Installation ```bash elizaos plugins add @elizaos/plugin-knowledge ``` ```bash bun add @elizaos/plugin-knowledge ``` ## Supported File Types PDF, DOCX, TXT, MD CSV, JSON, XML URLs, HTML ## Advanced Features Understand the internal workings 50% better retrieval accuracy Test your knowledge base REST endpoints and TypeScript interfaces ## Next Steps Set up your first knowledge-enabled agent in minutes Optimize for your specific use case Learn from practical implementations # Architecture & Flow Diagrams Source: https://docs.elizaos.ai/plugin-registry/knowledge/architecture-flow Visual guide to the Knowledge plugin's internal architecture and data flows This guide provides detailed visual representations of the Knowledge plugin's architecture, processing flows, and component interactions. ## High-Level Architecture ```mermaid graph TB subgraph "User Interactions" U1[Chat Messages] U2[File Uploads] U3[URL Processing] U4[Direct Knowledge] end subgraph "Knowledge Plugin" KS[Knowledge Service] DP[Document Processor] EP[Embedding Provider] VS[Vector Store] DS[Document Store] WI[Web Interface] end subgraph "Core Runtime" AM[Agent Memory] AP[Action Processor] PR[Providers] end U1 --> AP U2 --> WI U3 --> AP U4 --> KS WI --> KS AP --> KS KS --> DP DP --> EP EP --> VS KS --> DS PR --> VS VS --> AM DS --> AM ``` ## Document Processing Flow ```mermaid flowchart TD Start([Document Input]) --> Type{Input Type?} Type -->|File Upload| Extract[Extract Text] Type -->|URL| Fetch[Fetch Content] Type -->|Direct Text| Validate[Validate Text] Extract --> Clean[Clean & Normalize] Fetch --> Clean Validate --> Clean Clean --> Hash[Generate Content Hash] Hash --> Dedupe{Duplicate?} Dedupe -->|Yes| End1([Skip Processing]) Dedupe -->|No| Chunk[Chunk Text] Chunk --> Enrich{CTX Enabled?} Enrich -->|Yes| Context[Add Context] Enrich -->|No| Embed[Generate Embeddings] Context --> Embed Embed --> Store[Store Vectors] Store --> Meta[Store Metadata] Meta --> End2([Processing Complete]) ``` ## Retrieval Flow ```mermaid flowchart TD Query([User Query]) --> Embed[Generate Query Embedding] Embed --> Search[Vector Similarity Search] Search --> Filter{Apply Filters?} Filter -->|Yes| ApplyF[Filter by Metadata] Filter -->|No| Rank[Rank Results] ApplyF --> Rank Rank --> Threshold{Score > 0.7?} Threshold -->|No| Discard[Discard Result] Threshold -->|Yes| Include[Include in Results] Include --> Limit{Result Count} Limit -->|< Limit| More[Get More Results] Limit -->|= Limit| Build[Build Context] More --> Search Build --> Inject[Inject into Agent Context] Inject --> Response([Agent Response]) ``` ## Component Interactions ```mermaid sequenceDiagram participant User participant Agent participant KnowledgeService participant DocumentProcessor participant EmbeddingProvider participant VectorStore participant DocumentStore User->>Agent: Ask question Agent->>KnowledgeService: searchKnowledge(query) KnowledgeService->>EmbeddingProvider: embed(query) EmbeddingProvider-->>KnowledgeService: queryEmbedding KnowledgeService->>VectorStore: searchSimilar(queryEmbedding) VectorStore-->>KnowledgeService: matches[] KnowledgeService->>DocumentStore: getDocuments(ids) DocumentStore-->>KnowledgeService: documents[] KnowledgeService-->>Agent: relevantKnowledge[] Agent->>Agent: buildContext(knowledge) Agent-->>User: Informed response ``` ## Data Flow Architecture ```mermaid graph LR subgraph "Storage Layer" subgraph "Vector Store" VS1[Embeddings Table] VS2[Metadata Index] VS3[Similarity Index] end subgraph "Document Store" DS1[Documents Table] DS2[Content Hash Index] DS3[Timestamp Index] end end subgraph "Memory Types" M1[Document Memory] M2[Fragment Memory] M3[Context Memory] end VS1 --> M2 DS1 --> M1 M1 --> M3 M2 --> M3 ``` ## Processing Pipeline Details ### Text Extraction Flow ```mermaid graph TD File[Input File] --> Detect[Detect MIME Type] Detect --> PDF{PDF?} Detect --> DOCX{DOCX?} Detect --> Text{Text?} PDF -->|Yes| PDFLib[PDF Parser] DOCX -->|Yes| DOCXLib[DOCX Parser] Text -->|Yes| UTF8[UTF-8 Decode] PDFLib --> Clean[Clean Text] DOCXLib --> Clean UTF8 --> Clean Clean --> Output[Extracted Text] ``` ### Chunking Strategy ```mermaid graph TD Text[Full Text] --> Tokenize[Tokenize] Tokenize --> Window[Sliding Window] Window --> Chunk1[Chunk 1: 0-500] Window --> Chunk2[Chunk 2: 400-900] Window --> Chunk3[Chunk 3: 800-1300] Window --> More[...] Chunk1 --> Boundary1[Adjust to Boundaries] Chunk2 --> Boundary2[Adjust to Boundaries] Chunk3 --> Boundary3[Adjust to Boundaries] Boundary1 --> Final1[Final Chunk 1] Boundary2 --> Final2[Final Chunk 2] Boundary3 --> Final3[Final Chunk 3] ``` ### Contextual Enrichment ```mermaid graph TD Chunk[Text Chunk] --> Extract[Extract Key Info] Doc[Full Document] --> Summary[Generate Summary] Extract --> Combine[Combine Context] Summary --> Combine Combine --> Template[Apply Template] Template --> Enriched[Enriched Chunk] Template --> |Template| T["Context: {summary}
Section: {title}
Content: {chunk}"] ``` ## Rate Limiting & Concurrency ```mermaid graph TD subgraph "Request Queue" R1[Request 1] R2[Request 2] R3[Request 3] RN[Request N] end subgraph "Rate Limiter" RL1[Token Bucket
150k tokens/min] RL2[Request Bucket
60 req/min] RL3[Concurrent Limit
30 operations] end subgraph "Processing Pool" P1[Worker 1] P2[Worker 2] P3[Worker 3] P30[Worker 30] end R1 --> RL1 R2 --> RL1 R3 --> RL1 RL1 --> RL2 RL2 --> RL3 RL3 --> P1 RL3 --> P2 RL3 --> P3 ``` ## Caching Architecture ```mermaid graph TD subgraph "Request Flow" Req[Embedding Request] --> Cache{In Cache?} Cache -->|Yes| Return[Return Cached] Cache -->|No| Generate[Generate New] Generate --> Store[Store in Cache] Store --> Return end subgraph "Cache Management" CM1[LRU Eviction] CM2[TTL: 24 hours] CM3[Max Size: 10k entries] end subgraph "Cost Savings" CS1[OpenRouter + Claude: 90% reduction] CS2[OpenRouter + Gemini: 90% reduction] CS3[Direct API: 0% reduction] end ``` ## Web Interface Architecture ```mermaid graph TD subgraph "Frontend" UI[React UI] UP[Upload Component] DL[Document List] SR[Search Results] end subgraph "API Layer" REST[REST Endpoints] MW[Middleware] Auth[Auth Check] end subgraph "Backend" KS[Knowledge Service] FS[File Storage] PS[Processing Queue] end UI --> REST UP --> REST DL --> REST SR --> REST REST --> MW MW --> Auth Auth --> KS KS --> FS KS --> PS ``` ## Error Handling Flow ```mermaid flowchart TD Op[Operation] --> Try{Try Operation} Try -->|Success| Complete[Return Result] Try -->|Error| Type{Error Type?} Type -->|Rate Limit| Wait[Exponential Backoff] Type -->|Network| Retry[Retry 3x] Type -->|Parse Error| Log[Log & Skip] Type -->|Out of Memory| Chunk[Reduce Chunk Size] Wait --> Try Retry --> Try Chunk --> Try Log --> Notify[Notify User] Retry -->|Max Retries| Notify Notify --> End[Operation Failed] ``` ## Performance Characteristics ### Processing Times ```mermaid gantt title Document Processing Timeline dateFormat X axisFormat %s section Small Doc (< 1MB) Text Extraction :0, 1 Chunking :1, 2 Embedding :2, 5 Storage :5, 6 section Medium Doc (1-10MB) Text Extraction :0, 3 Chunking :3, 5 Embedding :5, 15 Storage :15, 17 section Large Doc (10-50MB) Text Extraction :0, 10 Chunking :10, 15 Embedding :15, 45 Storage :45, 50 ``` ### Storage Requirements ```mermaid pie title Storage Distribution "Document Text" : 40 "Vector Embeddings" : 35 "Metadata" : 15 "Indexes" : 10 ``` ## Scaling Considerations ```mermaid graph TD subgraph "Horizontal Scaling" LB[Load Balancer] N1[Node 1] N2[Node 2] N3[Node 3] end subgraph "Shared Resources" VS[Vector Store
PostgreSQL + pgvector] DS[Document Store
PostgreSQL] Cache[Redis Cache] end LB --> N1 LB --> N2 LB --> N3 N1 --> VS N1 --> DS N1 --> Cache N2 --> VS N2 --> DS N2 --> Cache N3 --> VS N3 --> DS N3 --> Cache ``` ## Summary The Knowledge plugin's architecture is designed for: Handles large document collections efficiently Optimized processing and retrieval paths Robust error handling and recovery 90% savings with intelligent caching Understanding these flows helps you: * Optimize configuration for your use case * Debug issues effectively * Plan for scale * Integrate with other systems # Complete Developer Guide Source: https://docs.elizaos.ai/plugin-registry/knowledge/complete-documentation Comprehensive technical reference for the Knowledge plugin The `@elizaos/plugin-knowledge` package provides Retrieval Augmented Generation (RAG) capabilities for elizaOS agents. It enables agents to store, search, and automatically use knowledge from uploaded documents and text. ## Key Features * **Multi-format Support**: Process PDFs, Word docs, text files, and more * **Smart Deduplication**: Content-based IDs prevent duplicate entries * **Automatic RAG**: Knowledge is automatically injected into relevant conversations * **Character Knowledge**: Load knowledge from character definitions * **REST API**: Manage documents via HTTP endpoints * **Conversation Tracking**: Track which knowledge was used in responses ## Architecture Overview ```mermaid graph TB subgraph "Input Layer" A[File Upload] --> D[Document Processor] B[URL Fetch] --> D C[Direct Text] --> D K[Character Knowledge] --> D end subgraph "Processing Layer" D --> E[Text Extraction] E --> F[Deduplication] F --> G[Chunking] G --> H[Embedding Generation] G --> CE[Contextual Enrichment] CE --> H end subgraph "Storage Layer" H --> I[(Vector Store)] H --> J[(Document Store)] end subgraph "Retrieval Layer" L[User Query] --> M[Semantic Search] M --> I I --> N[RAG Context] J --> N N --> O[Agent Response] end ``` ## Core Components ### Knowledge Service The main service class that handles all knowledge operations: ```typescript class KnowledgeService extends Service { static readonly serviceType = 'knowledge'; private knowledgeConfig: KnowledgeConfig; private knowledgeProcessingSemaphore: Semaphore; constructor(runtime: IAgentRuntime, config?: Partial) { super(runtime); this.knowledgeProcessingSemaphore = new Semaphore(10); // Configuration with environment variable support this.knowledgeConfig = { CTX_KNOWLEDGE_ENABLED: parseBooleanEnv(config?.CTX_KNOWLEDGE_ENABLED), LOAD_DOCS_ON_STARTUP: loadDocsOnStartup, MAX_INPUT_TOKENS: config?.MAX_INPUT_TOKENS, MAX_OUTPUT_TOKENS: config?.MAX_OUTPUT_TOKENS, EMBEDDING_PROVIDER: config?.EMBEDDING_PROVIDER, TEXT_PROVIDER: config?.TEXT_PROVIDER, TEXT_EMBEDDING_MODEL: config?.TEXT_EMBEDDING_MODEL, }; // Auto-load documents on startup if enabled if (this.knowledgeConfig.LOAD_DOCS_ON_STARTUP) { this.loadInitialDocuments(); } } // Main public method for adding knowledge async addKnowledge(options: AddKnowledgeOptions): Promise<{ clientDocumentId: string; storedDocumentMemoryId: UUID; fragmentCount: number; }> { // Generate content-based ID for deduplication const contentBasedId = generateContentBasedId(options.content, agentId, { includeFilename: options.originalFilename, contentType: options.contentType, maxChars: 2000, }); // Check for duplicates const existingDocument = await this.runtime.getMemoryById(contentBasedId); if (existingDocument) { // Return existing document info return { clientDocumentId: contentBasedId, ... }; } // Process new document return this.processDocument({ ...options, clientDocumentId: contentBasedId }); } // Semantic search for knowledge async getKnowledge( message: Memory, scope?: { roomId?: UUID; worldId?: UUID; entityId?: UUID } ): Promise { const embedding = await this.runtime.useModel(ModelType.TEXT_EMBEDDING, { text: message.content.text, }); const fragments = await this.runtime.searchMemories({ tableName: 'knowledge', embedding, query: message.content.text, ...scope, count: 20, match_threshold: 0.1, }); return fragments.map(fragment => ({ id: fragment.id, content: fragment.content, similarity: fragment.similarity, metadata: fragment.metadata, })); } // RAG metadata enrichment for conversation tracking async enrichConversationMemoryWithRAG( memoryId: UUID, ragMetadata: { retrievedFragments: Array<{ fragmentId: UUID; documentTitle: string; similarityScore?: number; contentPreview: string; }>; queryText: string; totalFragments: number; retrievalTimestamp: number; } ): Promise { // Enriches conversation memories with RAG usage data } } ``` ### Document Processing The service handles different file types with sophisticated processing logic: ```typescript private async processDocument(options: AddKnowledgeOptions): Promise<{ clientDocumentId: string; storedDocumentMemoryId: UUID; fragmentCount: number; }> { let fileBuffer: Buffer | null = null; let extractedText: string; let documentContentToStore: string; const isPdfFile = contentType === 'application/pdf'; if (isPdfFile) { // PDFs: Store original base64, extract text for fragments fileBuffer = Buffer.from(content, 'base64'); extractedText = await extractTextFromDocument(fileBuffer, contentType, originalFilename); documentContentToStore = content; // Keep base64 for PDFs } else if (isBinaryContentType(contentType, originalFilename)) { // Other binary files: Extract and store as plain text fileBuffer = Buffer.from(content, 'base64'); extractedText = await extractTextFromDocument(fileBuffer, contentType, originalFilename); documentContentToStore = extractedText; // Store extracted text } else { // Text files: Handle both base64 and plain text input if (looksLikeBase64(content)) { // Decode base64 text files const decodedBuffer = Buffer.from(content, 'base64'); extractedText = decodedBuffer.toString('utf8'); documentContentToStore = extractedText; } else { // Already plain text extractedText = content; documentContentToStore = content; } } // Create document memory with content-based ID const documentMemory = createDocumentMemory({ text: documentContentToStore, agentId, clientDocumentId, originalFilename, contentType, worldId, fileSize: fileBuffer ? fileBuffer.length : extractedText.length, documentId: clientDocumentId, customMetadata: metadata, }); // Store document and process fragments await this.runtime.createMemory(documentMemory, 'documents'); const fragmentCount = await processFragmentsSynchronously({ runtime: this.runtime, documentId: clientDocumentId, fullDocumentText: extractedText, agentId, contentType, roomId: roomId || agentId, entityId: entityId || agentId, worldId: worldId || agentId, documentTitle: originalFilename, }); return { clientDocumentId, storedDocumentMemoryId, fragmentCount }; } ``` ### Actions The plugin provides two main actions: #### PROCESS\_KNOWLEDGE Adds knowledge from files or text content: * Supports file paths: `/path/to/document.pdf` * Direct text: "Add this to your knowledge: ..." * File types: PDF, DOCX, TXT, MD, CSV, etc. * Automatically splits content into searchable fragments #### SEARCH\_KNOWLEDGE Explicitly searches the knowledge base: * Triggered by: "Search your knowledge for..." * Returns top 3 most relevant results * Displays formatted text snippets ### Knowledge Provider Automatically injects relevant knowledge into agent responses: * **Dynamic**: Runs on every message to find relevant context * **Top 5 Results**: Retrieves up to 5 most relevant knowledge fragments * **RAG Tracking**: Enriches conversation memories with knowledge usage metadata * **Token Limit**: Caps knowledge at \~4000 tokens to prevent context overflow The provider automatically: 1. Searches for relevant knowledge based on the user's message 2. Formats it with a "# Knowledge" header 3. Tracks which knowledge was used in the response 4. Enriches the conversation memory with RAG metadata ## Document Processing Pipeline ### 1. Document Ingestion Knowledge can be added through multiple channels: ```typescript // File upload (API endpoint sends base64-encoded content) const result = await knowledgeService.addKnowledge({ content: base64EncodedContent, // Base64 for binary files, can be plain text originalFilename: 'document.pdf', contentType: 'application/pdf', agentId: agentId, // Optional, defaults to runtime.agentId metadata: { tags: ['documentation', 'manual'] } }); // Direct text addition (internal use) await knowledgeService._internalAddKnowledge({ id: generateContentBasedId(content, agentId), content: { text: "Important information..." }, metadata: { type: MemoryType.DOCUMENT, source: 'direct' } }); // Character knowledge (loaded automatically from character definition) await knowledgeService.processCharacterKnowledge([ "Path: knowledge/facts.md\nKey facts about the product...", "Another piece of character knowledge..." ]); ``` ### 2. Text Extraction Supports multiple file formats: ```typescript const supportedFormats = { 'application/pdf': extractPDF, 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': extractDOCX, 'text/plain': (buffer) => buffer.toString('utf-8'), 'text/markdown': (buffer) => buffer.toString('utf-8'), 'application/json': (buffer) => JSON.stringify(JSON.parse(buffer.toString('utf-8')), null, 2) }; ``` ### 3. Content-Based Deduplication Uses deterministic IDs to prevent duplicates: ```typescript // Generate content-based ID combining: // - Content (first 2KB) // - Agent ID // - Filename (if available) // - Content type const contentBasedId = generateContentBasedId(content, agentId, { includeFilename: options.originalFilename, contentType: options.contentType, maxChars: 2000 }); // Check if document already exists const existingDocument = await this.runtime.getMemoryById(contentBasedId); if (existingDocument) { // Return existing document info instead of creating duplicate return { clientDocumentId: contentBasedId, ... }; } ``` ### 4. Intelligent Chunking Content-aware text splitting: ```typescript const defaultChunkOptions = { chunkSize: 500, // tokens overlapSize: 100, // tokens separators: ['\n\n', '\n', '. ', ' '], keepSeparator: true }; function chunkText(text: string, options: ChunkOptions): string[] { const chunks: string[] = []; let currentChunk = ''; // Smart chunking logic that respects: // - Token limits // - Sentence boundaries // - Paragraph structure // - Code blocks return chunks; } ``` ### 5. Contextual Enrichment Optional feature for better retrieval: ```typescript // When CTX_KNOWLEDGE_ENABLED=true async function enrichChunk(chunk: string, document: string): Promise { const context = await generateContext(chunk, document); return `${context}\n\n${chunk}`; } ``` ### 6. Embedding Generation Create vector embeddings: ```typescript async function generateEmbeddings(chunks: string[]): Promise { const embeddings = await embedder.embedMany(chunks); return embeddings; } // Batch processing with rate limiting const batchSize = 10; for (let i = 0; i < chunks.length; i += batchSize) { const batch = chunks.slice(i, i + batchSize); const embeddings = await generateEmbeddings(batch); await storeEmbeddings(embeddings); // Rate limiting await sleep(1000); } ``` ### 7. Storage Documents and embeddings are stored separately: ```typescript // Document storage { id: "doc_123", content: "Full document text", metadata: { source: "upload", filename: "report.pdf", createdAt: "2024-01-20T10:00:00Z", hash: "sha256_hash" } } // Vector storage { id: "vec_456", documentId: "doc_123", chunkIndex: 0, embedding: [0.123, -0.456, ...], content: "Chunk text", metadata: { position: { start: 0, end: 500 } } } ``` ## Retrieval & RAG ### Semantic Search Find relevant knowledge using vector similarity: ```typescript async function searchKnowledge(query: string, limit: number = 10): Promise { // Generate query embedding const queryEmbedding = await embedder.embed(query); // Search vector store const results = await vectorStore.searchMemories({ tableName: "knowledge_embeddings", agentId: runtime.agentId, embedding: queryEmbedding, match_threshold: 0.7, match_count: limit, unique: true }); // Enrich with document metadata return results.map(result => ({ id: result.id, content: result.content.text, score: result.similarity, metadata: result.metadata })); } ``` ## API Reference ### REST Endpoints #### Upload Documents ```http POST /knowledge/upload Content-Type: multipart/form-data { "file": , "metadata": { "tags": ["product", "documentation"] } } Response: { "id": "doc_123", "status": "processing", "message": "Document uploaded successfully" } ``` #### List Documents ```http GET /knowledge/documents?page=1&limit=20 Response: { "documents": [ { "id": "doc_123", "filename": "product-guide.pdf", "size": 1024000, "createdAt": "2024-01-20T10:00:00Z", "chunkCount": 15 } ], "total": 45, "page": 1, "pages": 3 } ``` #### Delete Document ```http DELETE /knowledge/documents/doc_123 Response: { "success": true, "message": "Document and associated embeddings deleted" } ``` #### Search Knowledge ```http GET /knowledge/search?q=pricing&limit=5 Response: { "results": [ { "id": "chunk_456", "content": "Our pricing starts at $99/month...", "score": 0.92, "metadata": { "source": "pricing.pdf", "page": 3 } } ] } ``` ### TypeScript Interfaces ```typescript interface AddKnowledgeOptions { agentId?: UUID; // Optional, defaults to runtime.agentId worldId: UUID; roomId: UUID; entityId: UUID; clientDocumentId: UUID; contentType: string; // MIME type originalFilename: string; content: string; // Base64 for binary, plain text for text files metadata?: Record; } interface KnowledgeConfig { CTX_KNOWLEDGE_ENABLED: boolean; LOAD_DOCS_ON_STARTUP: boolean; MAX_INPUT_TOKENS?: string | number; MAX_OUTPUT_TOKENS?: string | number; EMBEDDING_PROVIDER?: string; TEXT_PROVIDER?: string; TEXT_EMBEDDING_MODEL?: string; } interface TextGenerationOptions { provider?: 'anthropic' | 'openai' | 'openrouter' | 'google'; modelName?: string; maxTokens?: number; cacheDocument?: string; // For OpenRouter caching cacheOptions?: { type: 'ephemeral' }; autoCacheContextualRetrieval?: boolean; } ``` ## Advanced Features ### Contextual Embeddings Enable for 50% better retrieval accuracy: ```env CTX_KNOWLEDGE_ENABLED=true ``` This feature: * Adds document context to each chunk * Improves semantic understanding * Reduces false positives * Enables better cross-reference retrieval ### Document Caching With OpenRouter, enable caching for 90% cost reduction: ```typescript const config = { provider: 'openrouter', enableCache: true, cacheExpiry: 86400 // 24 hours }; ``` ### Custom Document Processors Extend for special formats: ```typescript class CustomProcessor extends DocumentProcessor { async extractCustomFormat(buffer: Buffer): Promise { // Custom extraction logic return extractedText; } registerProcessor() { this.processors.set('application/custom', this.extractCustomFormat); } } ``` ### Performance Optimization #### Rate Limiting ```typescript const rateLimiter = { maxConcurrent: 5, requestsPerMinute: 60, tokensPerMinute: 40000 }; ``` #### Batch Processing ```typescript async function batchProcess(documents: Document[]) { const chunks = []; for (const batch of chunk(documents, 10)) { const results = await Promise.all( batch.map(doc => processDocument(doc)) ); chunks.push(...results); await sleep(1000); // Rate limiting } return chunks; } ``` #### Memory Management ```typescript // Clear cache periodically setInterval(() => { knowledgeService.clearCache(); }, 3600000); // Every hour // Stream large files async function processLargeFile(path: string) { const stream = createReadStream(path); const chunks = []; for await (const chunk of stream) { chunks.push(await processChunk(chunk)); } return chunks; } ``` ## Integration Patterns ### Basic Integration ```json { "name": "SupportAgent", "plugins": ["@elizaos/plugin-knowledge"], "knowledge": [ "Default knowledge statement 1", "Default knowledge statement 2" ] } ``` ### Configuration Options ```env # Enable automatic document loading from agent's docs folder LOAD_DOCS_ON_STARTUP=true # Enable contextual embeddings for better retrieval CTX_KNOWLEDGE_ENABLED=true # Configure embedding provider (defaults to OpenAI) EMBEDDING_PROVIDER=openai TEXT_EMBEDDING_MODEL=text-embedding-3-small ``` ### Using the Service ```typescript // Get the knowledge service const knowledgeService = runtime.getService('knowledge'); // Add knowledge programmatically const result = await knowledgeService.addKnowledge({ content: documentContent, // Base64 or plain text originalFilename: 'guide.pdf', contentType: 'application/pdf', worldId: runtime.agentId, roomId: message.roomId, entityId: message.entityId }); // Search for knowledge const results = await knowledgeService.getKnowledge(message, { roomId: message.roomId, worldId: runtime.agentId }); ``` ## Best Practices Choose names that clearly indicate the content (e.g., `product-guide-v2.pdf` instead of `doc1.pdf`) Create logical folder structures like `products/`, `support/`, `policies/` Add categories, dates, and versions to improve searchability One topic per document for better retrieval accuracy Set `enableCache: true` for 90% cost reduction on repeated queries Start with 500 tokens, adjust based on your content type Respect API limits with batch processing and delays Clear cache periodically for large knowledge bases Check file types, sizes, and scan for malicious content Remove potentially harmful scripts or executable content Use role-based permissions for sensitive documents Never embed passwords, API keys, or PII in the knowledge base Regularly verify that searches return relevant results Ensure important context isn't split across chunks Test that duplicate uploads are properly detected Check similarity scores and adjust thresholds as needed ## Troubleshooting ### Common Issues #### Documents Not Loading Check file permissions and paths: ```bash ls -la agent/docs/ # Should show read permissions ``` #### Poor Retrieval Quality Try adjusting chunk size and overlap: ```env EMBEDDING_CHUNK_SIZE=800 EMBEDDING_OVERLAP_SIZE=200 ``` #### Rate Limiting Errors Implement exponential backoff: ```typescript async function withRetry(fn, maxRetries = 3) { for (let i = 0; i < maxRetries; i++) { try { return await fn(); } catch (error) { if (i === maxRetries - 1) throw error; await sleep(Math.pow(2, i) * 1000); } } } ``` ### Debug Logging Enable verbose logging: ```env # .env LOG_LEVEL=debug ``` ## Summary The Knowledge Plugin provides a complete RAG system that: * **Processes Documents**: Handles PDFs, Word docs, text files, and more with automatic text extraction * **Manages Deduplication**: Uses content-based IDs to prevent duplicate knowledge entries * **Chunks Intelligently**: Splits documents into searchable fragments with configurable overlap * **Retrieves Semantically**: Finds relevant knowledge using vector similarity search * **Enhances Conversations**: Automatically injects relevant knowledge into agent responses * **Tracks Usage**: Records which knowledge was used in each conversation Key features: * Automatic document loading on startup * Character knowledge integration * RAG metadata tracking for conversation history * REST API for document management * Support for contextual embeddings * Provider-agnostic embedding support The plugin seamlessly integrates with elizaOS agents to provide them with a searchable knowledge base that enhances their ability to provide accurate, contextual responses. # Contextual Embeddings Source: https://docs.elizaos.ai/plugin-registry/knowledge/contextual-embeddings Enhanced retrieval accuracy using Anthropic's contextual retrieval technique Contextual embeddings are an advanced Knowledge plugin feature that improves retrieval accuracy by enriching text chunks with surrounding context before generating embeddings. This implementation is based on [Anthropic's contextual retrieval techniques](https://www.anthropic.com/news/contextual-retrieval). ## What are Contextual Embeddings? Traditional RAG systems embed isolated text chunks, losing important context. Contextual embeddings solve this by using an LLM to add relevant context to each chunk before embedding. ### Traditional vs Contextual ```text Original chunk: "The deployment process requires authentication." Embedded as-is, missing context about: - Which deployment process? - What kind of authentication? - For which system? ``` ```text Enriched chunk: "In the Kubernetes deployment section for the payment service, the deployment process requires authentication using OAuth2 tokens obtained from the identity provider." Now embeddings understand this is about: - Kubernetes deployments - Payment service specifically - OAuth2 authentication ``` ## How It Works The Knowledge plugin uses a sophisticated prompt-based approach to enrich chunks: 1. **Document Analysis**: The full document is passed to an LLM along with each chunk 2. **Context Generation**: The LLM identifies relevant context from the document 3. **Chunk Enrichment**: The original chunk is preserved with added context 4. **Embedding**: The enriched chunk is embedded using your configured embedding model The implementation is based on Anthropic's Contextual Retrieval cookbook example, which showed up to 50% improvement in retrieval accuracy. ## Configuration ### Enable Contextual Embeddings ```env title=".env" # Enable contextual embeddings CTX_KNOWLEDGE_ENABLED=true # Configure your text generation provider TEXT_PROVIDER=openrouter # or openai, anthropic, google TEXT_MODEL=anthropic/claude-3-haiku # or any supported model # Required API keys OPENROUTER_API_KEY=your-key # If using OpenRouter # or OPENAI_API_KEY=your-key # If using OpenAI # or ANTHROPIC_API_KEY=your-key # If using Anthropic # or GOOGLE_API_KEY=your-key # If using Google ``` **Important**: Embeddings always use the model configured in `useModel(TEXT_EMBEDDING)` from your agent setup. Do NOT try to mix different embedding models - all your documents must use the same embedding model for consistency. ### Recommended Setup: OpenRouter with Separate Embedding Provider Since OpenRouter doesn't support embeddings, you need a separate embedding provider: ```typescript title="character.ts" export const character = { name: 'MyAgent', plugins: [ '@elizaos/plugin-openrouter', // For text generation '@elizaos/plugin-openai', // For embeddings '@elizaos/plugin-knowledge', // Knowledge plugin ], }; ``` ```env title=".env" # Enable contextual embeddings CTX_KNOWLEDGE_ENABLED=true # Text generation (for context enrichment) TEXT_PROVIDER=openrouter TEXT_MODEL=anthropic/claude-3-haiku OPENROUTER_API_KEY=your-openrouter-key # Embeddings (automatically used) OPENAI_API_KEY=your-openai-key ``` ```typescript title="character.ts" export const character = { name: 'MyAgent', plugins: [ '@elizaos/plugin-openrouter', // For text generation '@elizaos/plugin-google', // For embeddings '@elizaos/plugin-knowledge', // Knowledge plugin ], }; ``` ```env title=".env" # Enable contextual embeddings CTX_KNOWLEDGE_ENABLED=true # Text generation (for context enrichment) TEXT_PROVIDER=openrouter TEXT_MODEL=anthropic/claude-3-haiku OPENROUTER_API_KEY=your-openrouter-key # Embeddings (Google will be used automatically) GOOGLE_API_KEY=your-google-key ``` ### Alternative Providers ```env CTX_KNOWLEDGE_ENABLED=true TEXT_PROVIDER=openai TEXT_MODEL=gpt-4o-mini OPENAI_API_KEY=your-key ``` ```env CTX_KNOWLEDGE_ENABLED=true TEXT_PROVIDER=anthropic TEXT_MODEL=claude-3-haiku-20240307 ANTHROPIC_API_KEY=your-anthropic-key OPENAI_API_KEY=your-openai-key # Still needed for embeddings ``` ```env CTX_KNOWLEDGE_ENABLED=true TEXT_PROVIDER=google TEXT_MODEL=gemini-1.5-flash GOOGLE_API_KEY=your-google-key # Google embeddings will be used automatically ``` ```env CTX_KNOWLEDGE_ENABLED=true TEXT_PROVIDER=openrouter TEXT_MODEL=anthropic/claude-3-haiku OPENROUTER_API_KEY=your-openrouter-key GOOGLE_API_KEY=your-google-key # For embeddings # Requires @elizaos/plugin-google for embeddings ``` ## Technical Details ### Chunk Processing The plugin uses fixed chunk sizes optimized for contextual enrichment: * **Chunk Size**: 500 tokens (approximately 1,750 characters) * **Chunk Overlap**: 100 tokens * **Context Target**: 60-200 tokens of added context These values are based on research showing that smaller chunks with rich context perform better than larger chunks without context. ### Content-Aware Templates The plugin automatically detects content types and uses specialized prompts: ```typescript // Different templates for different content types - General text documents - PDF documents (with special handling for corrupted text) - Mathematical content (preserves equations and notation) - Code files (includes imports, function signatures) - Technical documentation (preserves terminology) ``` ### OpenRouter Caching When using OpenRouter with Claude or Gemini models, the plugin automatically leverages caching: 1. **First document chunk**: Caches the full document 2. **Subsequent chunks**: Reuses cached document (90% cost reduction) 3. **Cache duration**: 5 minutes (automatic) This means processing a 100-page document costs almost the same as processing a single page! ## Example: How Context Improves Retrieval ### Without Contextual Embeddings ```text Query: "How do I configure the timeout?" Retrieved chunk: "Set the timeout value to 30 seconds." Problem: Which timeout? Database? API? Cache? ``` ### With Contextual Embeddings ```text Query: "How do I configure the timeout?" Retrieved chunk: "In the Redis configuration section, when setting up the caching layer, set the timeout value to 30 seconds for optimal performance with session data." Result: Clear understanding this is about Redis cache timeout. ``` ## Performance Considerations ### Processing Time * **Initial processing**: 1-3 seconds per chunk (includes LLM call) * **With caching**: 0.1-0.3 seconds per chunk * **Batch processing**: Up to 30 chunks concurrently ### Cost Estimation | Document Size | Pages | Chunks | Without Caching | With OpenRouter Cache | | ------------- | ----- | ------ | --------------- | --------------------- | | Small | 10 | \~20 | \$0.02 | \$0.002 | | Medium | 50 | \~100 | \$0.10 | \$0.01 | | Large | 200 | \~400 | \$0.40 | \$0.04 | Costs are estimates based on Claude 3 Haiku pricing. Actual costs depend on your chosen model. ## Monitoring The plugin provides detailed logging: ```bash # Enable debug logging to see enrichment details LOG_LEVEL=debug elizaos start ``` This will show: * Context enrichment progress * Cache hit/miss rates * Processing times per document * Token usage ## Common Issues and Solutions ### Context Not Being Added **Check if contextual embeddings are enabled:** ```bash # Look for this in your logs: "CTX enrichment ENABLED" # or "CTX enrichment DISABLED" ``` **Verify your configuration:** * `CTX_KNOWLEDGE_ENABLED=true` (not "TRUE" or "True") * `TEXT_PROVIDER` and `TEXT_MODEL` are both set * Required API key for your provider is set ### Slow Processing **Solutions:** 1. Use OpenRouter with Claude/Gemini for automatic caching 2. Process smaller batches of documents 3. Use faster models (Claude 3 Haiku, Gemini 1.5 Flash) ### High Costs **Solutions:** 1. Enable OpenRouter caching (90% cost reduction) 2. Use smaller models for context generation 3. Process documents in batches during off-peak hours ## Best Practices OpenRouter's caching makes contextual embeddings 90% cheaper when processing multiple chunks from the same document. The chunk sizes and overlap are optimized based on research. Only change if you have specific requirements. Enable debug logging when first setting up to ensure context is being added properly. * Claude 3 Haiku: Best balance of quality and cost * Gemini 1.5 Flash: Fastest processing * GPT-4o-mini: Good quality, moderate cost ## Summary Contextual embeddings significantly improve retrieval accuracy by: * Adding document context to each chunk before embedding * Using intelligent templates based on content type * Preserving the original text while enriching with context * Leveraging caching for cost-efficient processing The implementation is based on Anthropic's proven approach and integrates seamlessly with elizaOS's existing infrastructure. Simply set `CTX_KNOWLEDGE_ENABLED=true` and configure your text generation provider to get started! # Examples & Recipes Source: https://docs.elizaos.ai/plugin-registry/knowledge/examples Practical examples and code recipes for the Knowledge plugin Learn how to use the Knowledge Plugin with practical examples that actually work. ## How Knowledge Actually Works The Knowledge Plugin allows agents to learn from documents in three ways: 1. **Auto-load from `docs` folder** (recommended for most use cases) 2. **Upload via Web Interface** (best for dynamic content) 3. **Hardcode small snippets** (only for tiny bits of info like "hello world") ## Basic Character Examples ### Example 1: Document-Based Support Bot Create a support bot that learns from your documentation: ```typescript title="characters/support-bot.ts" import { type Character } from '@elizaos/core'; export const supportBot: Character = { name: 'SupportBot', plugins: [ '@elizaos/plugin-openai', // Required for embeddings '@elizaos/plugin-knowledge', // Add knowledge capabilities ], system: 'You are a friendly customer support agent. Answer questions using the support documentation you have learned. Always search your knowledge base before responding.', bio: [ 'Expert in product features and troubleshooting', 'Answers based on official documentation', 'Always polite and helpful', ], }; ``` **Setup your support docs:** ``` your-project/ ├── docs/ # Create this folder │ ├── product-manual.pdf # Your actual product docs │ ├── troubleshooting-guide.md # Support procedures │ ├── faq.txt # Common questions │ └── policies/ # Organize with subfolders │ ├── refund-policy.pdf │ └── terms-of-service.md ├── .env │ OPENAI_API_KEY=sk-... │ LOAD_DOCS_ON_STARTUP=true # Auto-load all docs └── src/ └── character.ts ``` When you start the agent, it will automatically: 1. Load all documents from the `docs` folder 2. Process them into searchable chunks 3. Use this knowledge to answer questions ### Example 2: API Documentation Assistant For technical documentation: ```typescript title="characters/api-assistant.ts" export const apiAssistant: Character = { name: 'APIHelper', plugins: [ '@elizaos/plugin-openai', '@elizaos/plugin-knowledge', ], system: 'You are a technical documentation assistant. Help developers by searching your knowledge base for API documentation, code examples, and best practices.', topics: [ 'API endpoints and methods', 'Authentication and security', 'Code examples and best practices', 'Error handling and debugging', ], }; ``` **Organize your API docs:** ``` docs/ ├── api-reference/ │ ├── authentication.md │ ├── endpoints.json │ └── error-codes.csv ├── tutorials/ │ ├── getting-started.md │ ├── advanced-usage.md │ └── examples.ts └── changelog.md ``` ### Example 3: Simple Info Bot (Hello World Example) For very basic, hardcoded information only: ```json title="characters/info-bot.json" { "name": "InfoBot", "plugins": [ "@elizaos/plugin-openai", "@elizaos/plugin-knowledge" ], "knowledge": [ "Our office is located at 123 Main St", "Business hours: 9 AM to 5 PM EST", "Contact: support@example.com" ], "system": "You are a simple information bot. Answer questions using your basic knowledge." } ``` **Note:** The `knowledge` array is only for tiny snippets. For real documents, use the `docs` folder! ## Real-World Setup Guide ### Step 1: Prepare Your Documents Create a well-organized `docs` folder: ``` docs/ ├── products/ │ ├── product-overview.pdf │ ├── pricing-tiers.md │ └── feature-comparison.xlsx ├── support/ │ ├── installation-guide.pdf │ ├── troubleshooting.md │ └── common-issues.txt ├── legal/ │ ├── terms-of-service.pdf │ ├── privacy-policy.md │ └── data-processing.txt └── README.md # Optional: describe folder structure ``` ### Step 2: Configure Auto-Loading ```env title=".env" # Required: Your AI provider OPENAI_API_KEY=sk-... # Auto-load documents on startup LOAD_DOCS_ON_STARTUP=true # Optional: Custom docs path (default is ./docs) KNOWLEDGE_PATH=/path/to/your/documents ``` ### Step 3: Start Your Agent ```bash elizaos start ``` The agent will: * Automatically find and load all documents * Process PDFs, text files, markdown, etc. * Create searchable embeddings * Log progress: "Loaded 23 documents from docs folder on startup" ## Using the Web Interface ### Uploading Documents 1. Start your agent: `elizaos start` 2. Open browser: `http://localhost:3000` 3. Select your agent 4. Click the **Knowledge** tab 5. Drag and drop files or click to upload **Best for:** * Adding documents while the agent is running * Uploading user-specific content * Testing with different documents * Managing (view/delete) existing documents ### What Happens When You Upload When you upload a document via the web interface: 1. The file is processed immediately 2. It's converted to searchable chunks 3. The agent can use it right away 4. You'll see it listed in the Knowledge tab ## How Agents Use Knowledge ### Automatic Knowledge Search When users ask questions, the agent automatically: ```typescript // User asks: "What's your refund policy?" // Agent automatically: // 1. Searches knowledge base for "refund policy" // 2. Finds relevant chunks from refund-policy.pdf // 3. Uses this information to answer // User asks: "How do I install the software?" // Agent automatically: // 1. Searches for "install software" // 2. Finds installation-guide.pdf content // 3. Provides step-by-step instructions ``` ### The Knowledge Provider The knowledge plugin includes a provider that automatically injects relevant knowledge into the agent's context: ```typescript // This happens behind the scenes: // 1. User sends message // 2. Knowledge provider searches for relevant info // 3. Found knowledge is added to agent's context // 4. Agent generates response using this knowledge ``` ## Configuration Examples ### Production Support Bot ```env title=".env" # AI Configuration OPENAI_API_KEY=sk-... # Knowledge Configuration LOAD_DOCS_ON_STARTUP=true KNOWLEDGE_PATH=/var/app/support-docs # Optional: For better processing CTX_KNOWLEDGE_ENABLED=true OPENROUTER_API_KEY=sk-or-... # For enhanced context ``` ### Development Setup ```env title=".env" # Minimal setup for testing OPENAI_API_KEY=sk-... LOAD_DOCS_ON_STARTUP=true # Docs in default ./docs folder ``` ## Best Practices ### DO: Use the Docs Folder ✅ **Recommended approach for most use cases:** ``` 1. Put your documents in the docs folder 2. Set LOAD_DOCS_ON_STARTUP=true 3. Start your agent 4. Documents are automatically loaded ``` ### DO: Use Web Upload for Dynamic Content ✅ **When to use the web interface:** * User-uploaded content * Frequently changing documents * Testing different documents * One-off documents ### DON'T: Hardcode Large Content ❌ **Avoid this:** ```json { "knowledge": [ "Chapter 1: Introduction... (500 lines)", "Chapter 2: Getting Started... (1000 lines)", // Don't do this! ] } ``` ✅ **Instead, use files:** ``` docs/ ├── chapter-1-introduction.md ├── chapter-2-getting-started.md └── ... ``` ## Testing Your Setup ### Quick Verification 1. Check the logs when starting: ``` [INFO] Loaded 15 documents from docs folder on startup ``` 2. Ask the agent about your documents: ``` You: "What documents do you have about pricing?" Agent: "I have information about pricing from pricing-tiers.md and product-overview.pdf..." ``` 3. Use the Knowledge tab to see all loaded documents ### Troubleshooting **No documents loading?** * Check `LOAD_DOCS_ON_STARTUP=true` is set * Verify `docs` folder exists and has files * Check file permissions **Agent not finding information?** * Ensure documents contain the information * Try more specific questions * Check the Knowledge tab to verify documents are loaded ## Summary 1. **For production**: Use the `docs` folder with auto-loading 2. **For dynamic content**: Use the web interface 3. **For tiny snippets only**: Use the knowledge array 4. **The agent automatically searches knowledge** - no special commands needed Get started in 5 minutes # Quick Start Guide Source: https://docs.elizaos.ai/plugin-registry/knowledge/quick-start Get up and running with the Knowledge Plugin in 5 minutes Give your AI agent the ability to learn from documents and answer questions based on that knowledge. Works out of the box with zero configuration! ## Getting Started (Beginner-Friendly) ### Step 1: Add the Plugin The Knowledge plugin works automatically with any elizaOS agent. Just add it to your agent's plugin list: ```typescript // In your character file (e.g., character.ts) export const character = { name: 'MyAgent', plugins: [ '@elizaos/plugin-openai', // ← Make sure you have this '@elizaos/plugin-knowledge', // ← Add this line // ... your other plugins ], // ... rest of your character config }; ``` **That's it!** Your agent can now learn from documents. You'll need an `OPENAI_API_KEY` in your `.env` file for embeddings. Add `OPENAI_API_KEY=your-api-key` to your `.env` file. This is used for creating document embeddings, even if you're using a different AI provider for chat. ### Step 2: Upload Documents (Optional) Want your agent to automatically learn from documents when it starts? 1. **Create a `docs` folder** in your project root: ``` your-project/ ├── .env ├── docs/ ← Create this folder │ ├── guide.pdf │ ├── manual.txt │ └── notes.md └── package.json ``` 2. **Add this line to your `.env` file:** ```env LOAD_DOCS_ON_STARTUP=true ``` 3. **Start your agent** - it will automatically learn from all documents in the `docs` folder! ### Step 3: Ask Questions Once documents are loaded, just talk to your agent naturally: * "What does the guide say about setup?" * "Search your knowledge for configuration info" * "What do you know about \[any topic]?" Your agent will search through all loaded documents and give you relevant answers! ## Supported File Types The plugin can read almost any document: * **Text Files:** `.txt`, `.md`, `.csv`, `.json`, `.xml`, `.yaml` * **Documents:** `.pdf`, `.doc`, `.docx` * **Code Files:** `.js`, `.ts`, `.py`, `.java`, `.cpp`, `.html`, `.css` and many more ## Using the Web Interface The Knowledge Plugin includes a powerful web interface for managing your agent's knowledge base. ### Accessing the Knowledge Manager 1. **Start your agent:** ```bash elizaos start ``` 2. **Open your browser** and go to `http://localhost:3000` 3. **Select your agent** from the list (e.g., "Eliza") 4. **Click the Knowledge tab** in the right panel That's it! You can now: * Upload new documents * Search existing documents * Delete documents you no longer need * See all documents your agent has learned from You can also drag and drop files directly onto the Knowledge tab to upload them! ## Agent Actions Your agent automatically gets these new abilities: * **PROCESS\_KNOWLEDGE** - "Remember this document: \[file path or text]" * **SEARCH\_KNOWLEDGE** - "Search your knowledge for \[topic]" ### Examples in Chat **First, upload a document through the GUI:** 1. Go to `http://localhost:3000` 2. Click on your agent and open the Knowledge tab 3. Upload a document (e.g., `company_q3_earnings.pdf`) **Then ask your agent about it:** ``` You: What were the Q3 revenue figures? Agent: Based on the Q3 earnings report in my knowledge base, the revenue was $2.3M, representing a 15% increase from Q2... You: Search your knowledge for information about profit margins Agent: I found relevant information about profit margins: The Q3 report shows gross margins improved to 42%, up from 38% in the previous quarter... You: What does the report say about future projections? Agent: According to the earnings report, the company projects Q4 revenue to reach $2.8M with continued margin expansion... ``` ## Organizing Your Documents Create subfolders for better organization: ``` docs/ ├── products/ │ ├── product-guide.pdf │ └── pricing.md ├── support/ │ ├── faqs.txt │ └── troubleshooting.md └── policies/ └── terms.pdf ``` ## Basic Configuration (Optional) ### Custom Document Folder If you want to use a different folder for documents: ```env title=".env" # Custom path to your documents KNOWLEDGE_PATH=/path/to/your/documents ``` ### Provider Settings The plugin automatically uses your existing AI provider. If you're using OpenRouter: ```typescript // In your character file (e.g., character.ts) export const character = { name: 'MyAgent', plugins: [ '@elizaos/plugin-openrouter', '@elizaos/plugin-openai', // ← Make sure you have this as openrouter doesn't support embeddings '@elizaos/plugin-knowledge', // ← Add this line // ... your other plugins ], // ... rest of your character config }; ``` ```env title=".env" OPENROUTER_API_KEY=your-openrouter-api-key OPENAI_API_KEY=your-openai-api-key ``` The plugin automatically uses OpenAI embeddings even when using OpenRouter for text generation. ## FAQ **Q: Do I need any API keys?**\ A: For simple setup, only OPENAI\_API\_KEY. **Q: What if I don't have any AI plugins?**\ A: You need at least one AI provider plugin (like `@elizaos/plugin-openai`) for embeddings. **Q: Can I upload documents while the agent is running?**\ A: Yes! Use the web interface or just tell your agent to process a file. **Q: How much does this cost?** A: Only the cost of generating embeddings (usually pennies per document). **Q: Where are my documents stored?**\ A: Documents are processed and stored in your agent's database as searchable chunks. ## Common Issues ### Documents Not Loading Make sure: * Your `docs` folder exists in the right location * `LOAD_DOCS_ON_STARTUP=true` is in your `.env` file * Files are in supported formats ### Can't Access Web Interface Check that: * Your agent is running (`elizaos start`) * You're using the correct URL: `http://localhost:3000` * No other application is using port 3000 ### Agent Can't Find Information Try: * Using simpler search terms * Checking if the document was successfully processed * Looking in the Knowledge tab to verify the document is there ## Next Steps Now that you have the basics working: * Try uploading different types of documents * Organize your documents into folders * Ask your agent complex questions about the content * Explore the web interface features See the plugin in action Advanced configuration options The Knowledge Plugin is designed to work out-of-the-box. You only need to adjust settings if you have specific requirements. # Language Model Configuration Source: https://docs.elizaos.ai/plugin-registry/llm Understanding and configuring Language Model plugins in elizaOS elizaOS uses a plugin-based architecture for integrating different Language Model providers. This guide explains how to configure and use LLM plugins, including fallback mechanisms for embeddings and model registration. ## Key Concepts ### Model Types elizaOS supports many types of AI operations. Here are the most common ones: 1. **TEXT\_GENERATION** (`TEXT_SMALL`, `TEXT_LARGE`) - Having conversations and generating responses 2. **TEXT\_EMBEDDING** - Converting text into numbers for memory and search 3. **OBJECT\_GENERATION** (`OBJECT_SMALL`, `OBJECT_LARGE`) - Creating structured data like JSON Think of it like different tools in a toolbox: * **Text Generation** = Having a conversation * **Embeddings** = Creating a "fingerprint" of text for finding similar things later * **Object Generation** = Filling out forms with specific information ### Plugin Capabilities Not all LLM plugins support all model types. Here's what each can do: | Plugin | Text Chat | Embeddings | Structured Output | Runs Offline | | ------------ | --------- | ---------- | ----------------- | ------------ | | OpenAI | ✅ | ✅ | ✅ | ❌ | | Anthropic | ✅ | ❌ | ✅ | ❌ | | Google GenAI | ✅ | ✅ | ✅ | ❌ | | Ollama | ✅ | ✅ | ✅ | ✅ | | OpenRouter | ✅ | ❌ | ✅ | ❌ | **Key Points:** * 🌟 **OpenAI & Google GenAI** = Do everything (jack of all trades) * 💬 **Anthropic & OpenRouter** = Amazing at chat, need help with embeddings * 🏠 **Ollama** = Your local hero - does almost everything, no internet needed! ## Plugin Loading Order The order in which plugins are loaded matters significantly. From the default character configuration: ```typescript plugins: [ // Core plugins first '@elizaos/plugin-sql', // Text-only plugins (no embedding support) ...(process.env.ANTHROPIC_API_KEY?.trim() ? ['@elizaos/plugin-anthropic'] : []), ...(process.env.OPENROUTER_API_KEY?.trim() ? ['@elizaos/plugin-openrouter'] : []), // Embedding-capable plugins (optional, based on available credentials) ...(process.env.OPENAI_API_KEY?.trim() ? ['@elizaos/plugin-openai'] : []), ...(process.env.GOOGLE_GENERATIVE_AI_API_KEY?.trim() ? ['@elizaos/plugin-google-genai'] : []), // Ollama as fallback (only if no main LLM providers are configured) ...(process.env.OLLAMA_API_ENDPOINT?.trim() ? ['@elizaos/plugin-ollama'] : []), ] ``` ### Understanding the Order Think of it like choosing team players - you pick specialists first, then all-rounders: 1. **Anthropic & OpenRouter go first** - They're specialists! They're great at text generation but can't do embeddings. By loading them first, they get priority for text tasks. 2. **OpenAI & Google GenAI come next** - These are the all-rounders! They can do everything: text generation, embeddings, and structured output. They act as fallbacks for what the specialists can't do. 3. **Ollama comes last** - This is your local backup player! It supports almost everything (text, embeddings, objects) and runs on your computer. Perfect when cloud services aren't available. ### Why This Order Matters When you ask elizaOS to do something, it looks for the best model in order: * **Generate text?** → Anthropic gets first shot (if loaded) * **Create embeddings?** → Anthropic can't, so OpenAI steps in * **No cloud API keys?** → Ollama handles everything locally This smart ordering means: * You get the best specialized models for each task * You always have fallbacks for missing capabilities * You can run fully offline with Ollama if needed ### Real Example: How It Works Let's say you have Anthropic + OpenAI configured: ``` Task: "Generate a response" 1. Anthropic: "I got this!" ✅ (Priority 100 for text) 2. OpenAI: "I'm here if needed" (Priority 50) Task: "Create embeddings for memory" 1. Anthropic: "Sorry, can't do that" ❌ 2. OpenAI: "No problem, I'll handle it!" ✅ Task: "Generate structured JSON" 1. Anthropic: "I can do this!" ✅ (Priority 100 for objects) 2. OpenAI: "Standing by" (Priority 50) ``` ## Model Registration When plugins load, they "register" what they can do. It's like signing up for different jobs: ```typescript // Each plugin says "I can do this!" runtime.registerModel( ModelType.TEXT_LARGE, // What type of work generateText, // How to do it 'anthropic', // Who's doing it 100 // Priority (higher = goes first) ); ``` ### How elizaOS Picks the Right Model When you ask elizaOS to do something, it: 1. **Checks what type of work it is** (text? embeddings? objects?) 2. **Looks at who signed up** for that job 3. **Picks based on priority** (higher number goes first) 4. **If tied, first registered wins** **Example**: You ask for text generation * Anthropic registered with priority 100 ✅ (wins!) * OpenAI registered with priority 50 * Ollama registered with priority 10 But for embeddings: * Anthropic didn't register ❌ (can't do it) * OpenAI registered with priority 50 ✅ (wins!) * Ollama registered with priority 10 ## Embedding Fallback Strategy Remember: Not all plugins can create embeddings! Here's how elizaOS handles this: **The Problem**: * You're using Anthropic (great at chat, can't do embeddings) * But elizaOS needs embeddings for memory and search **The Solution**: elizaOS automatically finds another plugin that CAN do embeddings! ```typescript // What happens behind the scenes: // 1. "I need embeddings!" // 2. "Can Anthropic do it?" → No ❌ // 3. "Can OpenAI do it?" → Yes ✅ // 4. "OpenAI, you're up!" ``` ### Common Patterns #### Anthropic + OpenAI Fallback ```json { "plugins": [ "@elizaos/plugin-anthropic", // Primary for text "@elizaos/plugin-openai" // Fallback for embeddings ] } ``` #### OpenRouter + Local Embeddings ```json { "plugins": [ "@elizaos/plugin-openrouter", // Cloud text generation "@elizaos/plugin-ollama" // Local embeddings ] } ``` ## Configuration ### Environment Variables Each plugin requires specific environment variables: ````bash # .env file # OpenAI OPENAI_API_KEY=sk-... OPENAI_SMALL_MODEL=gpt-4o-mini # Optional: any available model OPENAI_LARGE_MODEL=gpt-4o # Optional: any available model # Anthropic ANTHROPIC_API_KEY=sk-ant-... ANTHROPIC_SMALL_MODEL=claude-3-haiku-20240307 # Optional: any Claude model ANTHROPIC_LARGE_MODEL=claude-3-5-sonnet-latest # Optional: any Claude model # Google GenAI GOOGLE_GENERATIVE_AI_API_KEY=... GOOGLE_SMALL_MODEL=gemini-2.0-flash-001 # Optional: any Gemini model GOOGLE_LARGE_MODEL=gemini-2.5-pro-preview-03-25 # Optional: any Gemini model # Ollama OLLAMA_API_ENDPOINT=http://localhost:11434/api OLLAMA_SMALL_MODEL=llama3.2 # Optional: any local model OLLAMA_LARGE_MODEL=llama3.1:70b # Optional: any local model OLLAMA_EMBEDDING_MODEL=nomic-embed-text # Optional: any embedding model # OpenRouter OPENROUTER_API_KEY=sk-or-... OPENROUTER_SMALL_MODEL=google/gemini-2.0-flash-001 # Optional: any available model OPENROUTER_LARGE_MODEL=anthropic/claude-3-opus # Optional: any available model **Important**: The model names shown are examples. You can use any model available from each provider. ### Character-Specific Secrets You can also configure API keys per character: ```json { "name": "MyAgent", "settings": { "secrets": { "OPENAI_API_KEY": "sk-...", "ANTHROPIC_API_KEY": "sk-ant-..." } } } ```` ## Available Plugins ### Cloud Providers * [OpenAI Plugin](./openai.mdx) - Full-featured with all model types * [Anthropic Plugin](./anthropic.mdx) - Claude models for text generation * [Google GenAI Plugin](./google-genai.mdx) - Gemini models * [OpenRouter Plugin](./openrouter.mdx) - Access to multiple providers ### Local/Self-Hosted * [Ollama Plugin](./ollama.mdx) - Run models locally with Ollama ## Best Practices ### 1. Always Configure Embeddings Even if your primary model doesn't support embeddings, always include a fallback: ```json { "plugins": [ "@elizaos/plugin-anthropic", "@elizaos/plugin-openai" // For embeddings ] } ``` ### 2. Order Matters Place your preferred providers first, but ensure embedding capability somewhere in the chain. ### 3. Test Your Configuration Verify all model types work: ```typescript // The runtime will log which provider is used for each operation [AgentRuntime][MyAgent] Using model TEXT_GENERATION from provider anthropic [AgentRuntime][MyAgent] Using model EMBEDDING from provider openai ``` ### 4. Monitor Costs Different providers have different pricing. Consider: * Using local models (Ollama) for development * Mixing providers (e.g., OpenRouter for text, local for embeddings) * Setting up usage alerts with your providers ## Troubleshooting ### "No model found for type EMBEDDING" Your configured plugins don't support embeddings. Add an embedding-capable plugin: ```json { "plugins": [ "@elizaos/plugin-anthropic", "@elizaos/plugin-openai" // Add this ] } ``` ### "Missing API Key" Ensure your environment variables are set: ```bash # Check current environment echo $OPENAI_API_KEY # Or use the CLI elizaos env edit-local ``` ### Models Not Loading Check plugin initialization in logs: ``` Success: Plugin @elizaos/plugin-openai initialized successfully ``` ## Migration from v0.x In elizaOS v0.x, models were configured directly in character files: ```json // ❌ OLD (v0.x) - No longer works { "modelProvider": "openai", "model": "gpt-4" } // ✅ NEW (v1.x) - Use plugins { "plugins": ["@elizaos/plugin-openai"] } ``` The `modelProvider` field is now ignored. All model configuration happens through plugins. # Anthropic Plugin Source: https://docs.elizaos.ai/plugin-registry/llm/anthropic Claude models integration for elizaOS The Anthropic plugin provides access to Claude models for text generation. Note that it does not support embeddings, so you'll need a fallback plugin. ## Features * **Claude 3 models** - Access to Claude 3 Opus, Sonnet, and Haiku * **Long context** - Up to 200k tokens context window * **XML formatting** - Optimized for structured responses * **Safety features** - Built-in content moderation ## Installation ```bash elizaos plugins add @elizaos/plugin-anthropic ``` ## Configuration ### Environment Variables ```bash # Required ANTHROPIC_API_KEY=sk-ant-... # Optional model configuration # You can use any available Anthropic model ANTHROPIC_SMALL_MODEL=claude-3-haiku-20240307 # Default: claude-3-haiku-20240307 ANTHROPIC_LARGE_MODEL=claude-3-5-sonnet-latest # Default: claude-3-5-sonnet-latest # Examples of other available models: # ANTHROPIC_SMALL_MODEL=claude-3-haiku-20240307 # ANTHROPIC_LARGE_MODEL=claude-3-opus-20240229 # ANTHROPIC_LARGE_MODEL=claude-3-5-sonnet-20241022 # ANTHROPIC_LARGE_MODEL=claude-3-5-haiku-20241022 ``` ### Character Configuration ```json { "name": "MyAgent", "plugins": [ "@elizaos/plugin-anthropic", "@elizaos/plugin-openai" // For embeddings ] } ``` ## Supported Operations | Operation | Support | Notes | | ------------------ | ------- | ------------------- | | TEXT\_GENERATION | ✅ | All Claude models | | EMBEDDING | ❌ | Use fallback plugin | | OBJECT\_GENERATION | ✅ | Via XML formatting | ## Important: Embedding Fallback Since Anthropic doesn't provide embedding models, always include a fallback: ```json { "plugins": [ "@elizaos/plugin-anthropic", // Primary for text "@elizaos/plugin-openai" // Fallback for embeddings ] } ``` ## Model Configuration The plugin uses two model categories: * **SMALL\_MODEL**: For faster, cost-effective responses * **LARGE\_MODEL**: For complex reasoning and best quality You can use any available Claude model, including: * Claude 3.5 Sonnet (latest and dated versions) * Claude 3 Opus, Sonnet, and Haiku * Claude 3.5 Haiku * Any new models Anthropic releases ## Usage Tips 1. **XML Templates** - Claude excels at XML-formatted responses 2. **System Prompts** - Effective for character personality 3. **Context Management** - Leverage the 200k token window ## External Resources * [Plugin Source](https://github.com/elizaos/eliza/tree/main/packages/plugin-anthropic) * [Anthropic API Documentation](https://docs.anthropic.com) * [Model Comparison](https://docs.anthropic.com/claude/docs/models-overview) # Google GenAI Plugin Source: https://docs.elizaos.ai/plugin-registry/llm/google-genai Google Gemini models integration for elizaOS ## Features * **Gemini models** - Access to Gemini Pro and Gemini Pro Vision * **Multimodal support** - Process text and images * **Embedding models** - Native embedding support * **Safety settings** - Configurable content filtering ## Installation ```bash elizaos plugins add @elizaos/plugin-google-genai ``` ## Configuration ### Environment Variables ```bash # Required GOOGLE_GENERATIVE_AI_API_KEY=... # Optional model configuration # You can use any available Google Gemini model GOOGLE_SMALL_MODEL=gemini-2.0-flash-001 # Default: gemini-2.0-flash-001 GOOGLE_LARGE_MODEL=gemini-2.5-pro-preview-03-25 # Default: gemini-2.5-pro-preview-03-25 GOOGLE_IMAGE_MODEL=gemini-1.5-flash # For vision tasks GOOGLE_EMBEDDING_MODEL=text-embedding-004 # Default: text-embedding-004 # Examples of other available models: # GOOGLE_SMALL_MODEL=gemini-1.5-flash # GOOGLE_LARGE_MODEL=gemini-1.5-pro # GOOGLE_LARGE_MODEL=gemini-pro # GOOGLE_EMBEDDING_MODEL=embedding-001 ``` ### Character Configuration ```json { "name": "MyAgent", "plugins": ["@elizaos/plugin-google-genai"] } ``` ## Supported Operations | Operation | Models | Notes | | ------------------ | ----------------------------- | ------------------ | | TEXT\_GENERATION | gemini-pro, gemini-pro-vision | Multimodal capable | | EMBEDDING | embedding-001 | 768-dimensional | | OBJECT\_GENERATION | All Gemini models | Structured output | ## Model Configuration The plugin uses three model categories: * **SMALL\_MODEL**: Fast, efficient for simple tasks * **LARGE\_MODEL**: Best quality, complex reasoning * **IMAGE\_MODEL**: Multimodal capabilities (text + vision) * **EMBEDDING\_MODEL**: Vector embeddings You can configure any available Gemini model: * Gemini 2.0 Flash (latest) * Gemini 2.5 Pro Preview * Gemini 1.5 Pro/Flash * Gemini Pro (legacy) * Any new models Google releases ## Safety Configuration Control content filtering: ```typescript // In character settings { "settings": { "google_safety": { "harassment": "BLOCK_NONE", "hate_speech": "BLOCK_MEDIUM_AND_ABOVE", "sexually_explicit": "BLOCK_MEDIUM_AND_ABOVE", "dangerous_content": "BLOCK_MEDIUM_AND_ABOVE" } } } ``` ## Usage Tips 1. **Multimodal** - Leverage image understanding capabilities 2. **Long Context** - Gemini 1.5 Pro supports up to 1M tokens 3. **Rate Limits** - Free tier has generous limits ## Cost Structure * Free tier: 60 queries per minute * Paid tier: Higher limits and priority access * Embedding calls are separate from generation ## External Resources * [Plugin Source](https://github.com/elizaos/eliza/tree/main/packages/plugin-google-genai) * [Google AI Studio](https://makersuite.google.com) * [API Documentation](https://ai.google.dev/docs) # Ollama Plugin Source: https://docs.elizaos.ai/plugin-registry/llm/ollama Local model execution via Ollama for elizaOS The Ollama plugin provides local model execution and can serve as a fallback option when cloud-based LLM providers are not configured. It requires running an Ollama server locally. ## Features * **Local execution** - No API keys or internet required * **Multiple models** - Support for Llama, Mistral, Gemma, and more * **Full model types** - Text, embeddings, and objects * **Cost-free** - No API charges * **Fallback option** - Can serve as a local fallback when cloud providers are unavailable ## Prerequisites 1. Install [Ollama](https://ollama.ai) 2. Pull desired models: ```bash ollama pull llama3.1 ollama pull nomic-embed-text ``` ## Installation ```bash elizaos plugins add @elizaos/plugin-ollama ``` ## Configuration ### Environment Variables ```bash # Required OLLAMA_API_ENDPOINT=http://localhost:11434/api # Model configuration # You can use any model available in your Ollama installation OLLAMA_SMALL_MODEL=llama3.2 # Default: llama3.2 OLLAMA_MEDIUM_MODEL=llama3.1 # Default: llama3.1 OLLAMA_LARGE_MODEL=llama3.1:70b # Default: llama3.1:70b OLLAMA_EMBEDDING_MODEL=nomic-embed-text # Default: nomic-embed-text # Examples of other available models: # OLLAMA_SMALL_MODEL=mistral:7b # OLLAMA_MEDIUM_MODEL=mixtral:8x7b # OLLAMA_LARGE_MODEL=llama3.3:70b # OLLAMA_EMBEDDING_MODEL=mxbai-embed-large # OLLAMA_EMBEDDING_MODEL=all-minilm # Optional parameters OLLAMA_TEMPERATURE=0.7 ``` ### Character Configuration ```json { "name": "MyAgent", "plugins": ["@elizaos/plugin-ollama"] } ``` ## Supported Operations | Operation | Models | Notes | | ------------------ | ----------------------------------- | ----------------------- | | TEXT\_GENERATION | llama3, mistral, gemma | Various sizes available | | EMBEDDING | nomic-embed-text, mxbai-embed-large | Local embeddings | | OBJECT\_GENERATION | All text models | JSON generation | ## Model Configuration The plugin uses three model tiers: * **SMALL\_MODEL**: Quick responses, lower resource usage * **MEDIUM\_MODEL**: Balanced performance * **LARGE\_MODEL**: Best quality, highest resource needs You can use any model from Ollama's library: * Llama models (3, 3.1, 3.2, 3.3) * Mistral/Mixtral models * Gemma models * Phi models * Any custom models you've created For embeddings, popular options include: * `nomic-embed-text` - Balanced performance * `mxbai-embed-large` - Higher quality * `all-minilm` - Lightweight option ## Performance Tips 1. **GPU Acceleration** - Dramatically improves speed 2. **Model Quantization** - Use Q4/Q5 versions for better performance 3. **Context Length** - Limit context for faster responses ## Hardware Requirements | Model Size | RAM Required | GPU Recommended | | ---------- | ------------ | --------------- | | 7B | 8GB | Optional | | 13B | 16GB | Yes | | 70B | 64GB+ | Required | ## Common Issues ### "Connection refused" Ensure Ollama is running: ```bash ollama serve ``` ### Slow Performance * Use smaller models or quantized versions * Enable GPU acceleration * Reduce context length ## External Resources * [Plugin Source](https://github.com/elizaos/eliza/tree/main/packages/plugin-ollama) * [Ollama Documentation](https://github.com/jmorganca/ollama) * [Model Library](https://ollama.ai/library) # OpenAI Plugin Source: https://docs.elizaos.ai/plugin-registry/llm/openai OpenAI GPT models integration for elizaOS The OpenAI plugin provides access to GPT models and supports all model types: text generation, embeddings, and object generation. ## Features * **Full model support** - Text, embeddings, and objects * **Multiple models** - GPT-4, GPT-3.5, and embedding models * **Streaming support** - Real-time response generation * **Function calling** - Structured output generation ## Installation ```bash elizaos plugins add @elizaos/plugin-openai ``` ## Configuration ### Environment Variables ```bash # Required OPENAI_API_KEY=sk-... # Optional model configuration # You can use any available OpenAI model OPENAI_SMALL_MODEL=gpt-4o-mini # Default: gpt-4o-mini OPENAI_LARGE_MODEL=gpt-4o # Default: gpt-4o OPENAI_EMBEDDING_MODEL=text-embedding-3-small # Default: text-embedding-3-small # Examples of other available models: # OPENAI_SMALL_MODEL=gpt-3.5-turbo # OPENAI_LARGE_MODEL=gpt-4-turbo # OPENAI_LARGE_MODEL=gpt-4o-2024-11-20 # OPENAI_EMBEDDING_MODEL=text-embedding-3-large # OPENAI_EMBEDDING_MODEL=text-embedding-ada-002 ``` ### Character Configuration ```json { "name": "MyAgent", "plugins": ["@elizaos/plugin-openai"], "settings": { "secrets": { "OPENAI_API_KEY": "sk-..." } } } ``` ## Supported Operations | Operation | Models | Notes | | ------------------ | ----------------------------------------------------------- | ---------------------- | | TEXT\_GENERATION | Any GPT model (gpt-4o, gpt-4, gpt-3.5-turbo, etc.) | Conversational AI | | EMBEDDING | Any embedding model (text-embedding-3-small/large, ada-002) | Vector embeddings | | OBJECT\_GENERATION | All GPT models | JSON/structured output | ## Model Configuration The plugin uses two model categories: * **SMALL\_MODEL**: Used for simpler tasks, faster responses * **LARGE\_MODEL**: Used for complex reasoning, better quality You can configure any available OpenAI model in these slots based on your needs and budget. ## Usage Example The plugin automatically registers with the runtime: ```typescript // No manual initialization needed // Just include in plugins array ``` ## Cost Considerations * GPT-4 is more expensive than GPT-3.5 * Use `text-embedding-3-small` for cheaper embeddings * Monitor usage via OpenAI dashboard ## External Resources * [Plugin Source](https://github.com/elizaos/eliza/tree/main/packages/plugin-openai) * [OpenAI API Documentation](https://platform.openai.com/docs) * [Pricing](https://openai.com/pricing) # OpenRouter Plugin Source: https://docs.elizaos.ai/plugin-registry/llm/openrouter Multi-provider LLM access through OpenRouter ## Features * **Multiple providers** - Access 50+ models from various providers * **Automatic failover** - Route to available providers * **Cost optimization** - Choose models by price/performance * **Single API key** - One key for all providers ## Installation ```bash elizaos plugins add @elizaos/plugin-openrouter ``` ## Configuration ### Environment Variables ```bash # Required OPENROUTER_API_KEY=sk-or-... # Optional model configuration # You can use any model available on OpenRouter OPENROUTER_SMALL_MODEL=google/gemini-2.0-flash-001 # Default: google/gemini-2.0-flash-001 OPENROUTER_LARGE_MODEL=google/gemini-2.5-flash-preview-05-20 # Default: google/gemini-2.5-flash-preview-05-20 OPENROUTER_IMAGE_MODEL=anthropic/claude-3-5-sonnet # For vision tasks # Examples of other available models: # OPENROUTER_SMALL_MODEL=anthropic/claude-3-haiku # OPENROUTER_LARGE_MODEL=anthropic/claude-3-opus # OPENROUTER_LARGE_MODEL=openai/gpt-4o # OPENROUTER_SMALL_MODEL=meta-llama/llama-3.1-8b-instruct:free ``` ### Character Configuration ```json { "name": "MyAgent", "plugins": [ "@elizaos/plugin-openrouter", "@elizaos/plugin-ollama" // For embeddings ] } ``` ## Supported Operations | Operation | Support | Notes | | ------------------ | ------- | -------------------- | | TEXT\_GENERATION | ✅ | All available models | | EMBEDDING | ❌ | Use fallback plugin | | OBJECT\_GENERATION | ✅ | Model dependent | ## Important: Embedding Fallback OpenRouter doesn't provide embedding endpoints, so include a fallback: ```json { "plugins": [ "@elizaos/plugin-openrouter", // Text generation "@elizaos/plugin-openai" // Embeddings ] } ``` ## Model Configuration The plugin uses model tiers: * **SMALL\_MODEL**: Fast, cost-effective responses * **LARGE\_MODEL**: Complex reasoning, best quality * **IMAGE\_MODEL**: Multimodal capabilities OpenRouter provides access to 50+ models from various providers. You can use: ### Premium Models * Any Anthropic Claude model (Opus, Sonnet, Haiku) * Any OpenAI GPT model (GPT-4o, GPT-4, GPT-3.5) * Google Gemini models (Pro, Flash, etc.) * Cohere Command models ### Open Models * Meta Llama models (3.1, 3.2, 3.3) * Mistral/Mixtral models * Many models with `:free` suffix for testing ## Pricing Strategy OpenRouter charges a small markup (usually \~10%) on top of provider prices: 1. **Pay-per-token** - No monthly fees 2. **Price transparency** - See costs per model 3. **Credits system** - Pre-pay for usage ## External Resources * [Plugin Source](https://github.com/elizaos/eliza/tree/main/packages/plugin-openrouter) * [OpenRouter Documentation](https://openrouter.ai/docs) * [Model List & Pricing](https://openrouter.ai/models) # Plugin System Overview Source: https://docs.elizaos.ai/plugin-registry/overview Comprehensive guide to the elizaOS plugin system architecture and implementation ## Overview The elizaOS plugin system is a comprehensive extension mechanism that allows developers to add functionality to agents through a well-defined interface. This analysis examines the complete plugin architecture by analyzing the source code and comparing it with the documentation. ## Core Plugins elizaOS includes essential core plugins that provide foundational functionality: The core message handler and event system for elizaOS agents. Provides essential functionality for message processing, knowledge management, and basic agent operations. Database integration and management for elizaOS. Features automatic schema migrations, multi-database support, and a sophisticated plugin architecture. Advanced knowledge base and RAG system for elizaOS. Provides semantic search, contextual embeddings, and intelligent document processing. ## DeFi Plugins Blockchain and DeFi integrations for Web3 functionality: Multi-chain EVM support with token transfers, swaps, bridging, and governance across 30+ networks including Ethereum, Base, Arbitrum, and more. High-performance Solana blockchain integration with SOL/SPL transfers, Jupiter swaps, and real-time portfolio tracking. ## Platform Integrations Connect your agent to popular platforms: Full Discord integration with voice, commands, and rich interactions. Telegram bot functionality with inline keyboards and media support. Twitter/X integration for posting, replying, and timeline management. Farcaster social network integration with casting and engagement. ## LLM Providers Choose from various language model providers: GPT-4, GPT-3.5, and other OpenAI models. Claude 3 and other Anthropic models. OpenRouter models for advanced routing and customization. ## Community Plugin Registry Explore the complete collection of community-contributed plugins in our dedicated registry. Access the full plugin registry with real-time updates, detailed plugin information, version tracking, and easy installation instructions for all v1 compatible elizaOS plugins. ## 1. Complete Plugin Interface Based on `/Users/studio/Documents/GitHub/eliza/packages/core/src/types/plugin.ts`, the full Plugin interface includes: ```typescript export interface Plugin { name: string; // Unique identifier description: string; // Human-readable description // Initialization init?: (config: Record, runtime: IAgentRuntime) => Promise; // Configuration config?: { [key: string]: any }; // Plugin-specific configuration // Core Components (documented) actions?: Action[]; // Tasks agents can perform providers?: Provider[]; // Data sources evaluators?: Evaluator[]; // Response filters // Additional Components (not fully documented) services?: (typeof Service)[]; // Background services adapter?: IDatabaseAdapter; // Database adapter models?: { // Model handlers [key: string]: (...args: any[]) => Promise; }; events?: PluginEvents; // Event handlers routes?: Route[]; // HTTP endpoints tests?: TestSuite[]; // Test suites componentTypes?: { // Custom component types name: string; schema: Record; validator?: (data: any) => boolean; }[]; // Dependency Management dependencies?: string[]; // Required plugins testDependencies?: string[]; // Test-only dependencies priority?: number; // Loading priority schema?: any; // Database schema } ``` ## 2. Action, Provider, and Evaluator Interfaces ### Action Interface From `/Users/studio/Documents/GitHub/eliza/packages/core/src/types/components.ts`: ```typescript export interface Action { name: string; // Unique identifier similes?: string[]; // Alternative names/aliases description: string; // What the action does examples?: ActionExample[][]; // Usage examples handler: Handler; // Execution logic validate: Validator; // Pre-execution validation } // Handler signature type Handler = ( runtime: IAgentRuntime, message: Memory, state?: State, options?: { [key: string]: unknown }, callback?: HandlerCallback, responses?: Memory[] ) => Promise; ``` ### Provider Interface ```typescript export interface Provider { name: string; // Unique identifier description?: string; // What data it provides dynamic?: boolean; // Dynamic data source position?: number; // Execution order private?: boolean; // Hidden from provider list get: (runtime: IAgentRuntime, message: Memory, state: State) => Promise; } interface ProviderResult { values?: { [key: string]: any }; data?: { [key: string]: any }; text?: string; } ``` ### Evaluator Interface ```typescript export interface Evaluator { alwaysRun?: boolean; // Run on every response description: string; // What it evaluates similes?: string[]; // Alternative names examples: EvaluationExample[]; // Example evaluations handler: Handler; // Evaluation logic name: string; // Unique identifier validate: Validator; // Should evaluator run? } ``` ## 3. Plugin Initialization Lifecycle Based on `/Users/studio/Documents/GitHub/eliza/packages/core/src/runtime.ts`, the initialization process: 1. **Plugin Registration** (`registerPlugin` method): * Validates plugin has a name * Checks for duplicate plugins * Adds to active plugins list * Calls plugin's `init()` method if present * Handles configuration errors gracefully 2. **Component Registration Order**: ```typescript // 1. Database adapter (if provided) if (plugin.adapter) { this.registerDatabaseAdapter(plugin.adapter); } // 2. Actions if (plugin.actions) { for (const action of plugin.actions) { this.registerAction(action); } } // 3. Evaluators if (plugin.evaluators) { for (const evaluator of plugin.evaluators) { this.registerEvaluator(evaluator); } } // 4. Providers if (plugin.providers) { for (const provider of plugin.providers) { this.registerProvider(provider); } } // 5. Models if (plugin.models) { for (const [modelType, handler] of Object.entries(plugin.models)) { this.registerModel(modelType, handler, plugin.name, plugin.priority); } } // 6. Routes if (plugin.routes) { for (const route of plugin.routes) { this.routes.push(route); } } // 7. Events if (plugin.events) { for (const [eventName, eventHandlers] of Object.entries(plugin.events)) { for (const eventHandler of eventHandlers) { this.registerEvent(eventName, eventHandler); } } } // 8. Services (delayed if runtime not initialized) if (plugin.services) { for (const service of plugin.services) { if (this.isInitialized) { await this.registerService(service); } else { this.servicesInitQueue.add(service); } } } ``` ## 4. Service System Integration From `/Users/studio/Documents/GitHub/eliza/packages/core/src/types/service.ts`: ### Service Abstract Class ```typescript export abstract class Service { protected runtime!: IAgentRuntime; constructor(runtime?: IAgentRuntime) { if (runtime) { this.runtime = runtime; } } abstract stop(): Promise; static serviceType: string; abstract capabilityDescription: string; config?: Metadata; static async start(_runtime: IAgentRuntime): Promise { throw new Error('Not implemented'); } } ``` ### Service Types The system includes predefined service types: * TRANSCRIPTION, VIDEO, BROWSER, PDF * REMOTE\_FILES (AWS S3) * WEB\_SEARCH, EMAIL, TEE * TASK, WALLET, LP\_POOL, TOKEN\_DATA * DATABASE\_MIGRATION * PLUGIN\_MANAGER, PLUGIN\_CONFIGURATION, PLUGIN\_USER\_INTERACTION ## 5. Route Definitions for HTTP Endpoints From the Plugin interface: ```typescript export type Route = { type: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'STATIC'; path: string; filePath?: string; // For static files public?: boolean; // Public access name?: string; // Route name handler?: (req: any, res: any, runtime: IAgentRuntime) => Promise; isMultipart?: boolean; // File uploads }; ``` Example from starter plugin: ```typescript routes: [ { name: 'hello-world-route', path: '/helloworld', type: 'GET', handler: async (_req: any, res: any) => { res.json({ message: 'Hello World!' }); } } ] ``` ## 6. Event System Integration From `/Users/studio/Documents/GitHub/eliza/packages/core/src/types/events.ts`: ### Event Types Standard events include: * World events: WORLD\_JOINED, WORLD\_CONNECTED, WORLD\_LEFT * Entity events: ENTITY\_JOINED, ENTITY\_LEFT, ENTITY\_UPDATED * Room events: ROOM\_JOINED, ROOM\_LEFT * Message events: MESSAGE\_RECEIVED, MESSAGE\_SENT, MESSAGE\_DELETED * Voice events: VOICE\_MESSAGE\_RECEIVED, VOICE\_MESSAGE\_SENT * Run events: RUN\_STARTED, RUN\_ENDED, RUN\_TIMEOUT * Action/Evaluator events: ACTION\_STARTED/COMPLETED, EVALUATOR\_STARTED/COMPLETED * Model events: MODEL\_USED ### Plugin Event Handlers ```typescript export type PluginEvents = { [K in keyof EventPayloadMap]?: EventHandler[]; } & { [key: string]: ((params: any) => Promise)[]; }; ``` ## 7. Database Adapter Plugins From `/Users/studio/Documents/GitHub/eliza/packages/core/src/types/database.ts`: The IDatabaseAdapter interface is extensive, including methods for: * Agents, Entities, Components * Memories (with embeddings) * Rooms, Participants * Relationships * Tasks * Caching * Logs Example: SQL Plugin creates database adapters: ```typescript export const plugin: Plugin = { name: '@elizaos/plugin-sql', description: 'A plugin for SQL database access with dynamic schema migrations', priority: 0, schema, init: async (_, runtime: IAgentRuntime) => { const dbAdapter = createDatabaseAdapter(config, runtime.agentId); runtime.registerDatabaseAdapter(dbAdapter); } }; ``` # Discord Integration Source: https://docs.elizaos.ai/plugin-registry/platform/discord Welcome to the comprehensive documentation for the @elizaos/plugin-discord package. This index provides organized access to all documentation resources. The @elizaos/plugin-discord enables your elizaOS agent to operate as a Discord bot with full support for messages, voice channels, slash commands, and media processing. ## 📚 Documentation * **[Complete Documentation](./complete-documentation.mdx)** - Detailed technical reference * **[Event Flow](./event-flow.mdx)** - Visual guide to Discord event processing * **[Examples](./examples.mdx)** - Practical implementation examples * **[Testing Guide](./testing-guide.mdx)** - Testing strategies and patterns ## 🔧 Configuration ### Required Settings * `DISCORD_APPLICATION_ID` - Your Discord application ID * `DISCORD_API_TOKEN` - Bot authentication token ### Optional Settings * `CHANNEL_IDS` - Restrict bot to specific channels * `DISCORD_VOICE_CHANNEL_ID` - Default voice channel # Developer Guide Source: https://docs.elizaos.ai/plugin-registry/platform/discord/complete-documentation Comprehensive Discord integration for elizaOS agents. It enables agents to operate as fully-featured Discord bots with advanced features and capabilities. ## Overview The `@elizaos/plugin-discord` package provides comprehensive Discord integration for elizaOS agents. It enables agents to operate as fully-featured Discord bots with support for text channels, voice channels, direct messages, slash commands, and media processing. This plugin handles all Discord-specific functionality including: * Initializing and managing the Discord bot connection * Processing messages and interactions across multiple servers * Managing voice channel connections and audio processing * Handling media attachments and transcription * Implementing Discord-specific actions and state providers * Supporting channel restrictions and permission management ## Architecture Overview ```mermaid graph TD A[Discord API] --> B[Discord.js Client] B --> C[Discord Service] C --> D[Message Manager] C --> E[Voice Manager] C --> F[Event Handlers] D --> G[Attachment Handler] D --> H[Bootstrap Plugin] E --> I[Voice Connection] E --> J[Audio Processing] F --> K[Guild Events] F --> L[Interaction Events] F --> M[Message Events] N[Actions] --> C O[Providers] --> C ``` ## Core Components ### Discord Service The `DiscordService` class is the main entry point for Discord functionality: ```typescript export class DiscordService extends Service implements IDiscordService { static serviceType: string = DISCORD_SERVICE_NAME; client: DiscordJsClient | null; character: Character; messageManager?: MessageManager; voiceManager?: VoiceManager; private allowedChannelIds?: string[]; constructor(runtime: IAgentRuntime) { super(runtime); // Initialize Discord client with proper intents // Set up event handlers // Parse channel restrictions } } ``` #### Key Responsibilities: 1. **Client Initialization** * Creates Discord.js client with required intents * Handles authentication with bot token * Manages connection lifecycle 2. **Event Registration** * Listens for Discord events (messages, interactions, etc.) * Routes events to appropriate handlers * Manages event cleanup on disconnect 3. **Channel Restrictions** * Parses `CHANNEL_IDS` environment variable * Enforces channel-based access control * Filters messages based on allowed channels 4. **Component Coordination** * Initializes MessageManager and VoiceManager * Coordinates between different components * Manages shared state and resources ### Message Manager The `MessageManager` class handles all message-related operations: ```typescript export class MessageManager { private client: DiscordJsClient; private runtime: IAgentRuntime; private inlinePositionalCallbacks: Map void>; async handleMessage(message: DiscordMessage): Promise { // Convert Discord message to elizaOS format // Process attachments // Send to bootstrap plugin // Handle response } async processAttachments(message: DiscordMessage): Promise { // Download and process media files // Generate descriptions for images // Transcribe audio/video } } ``` #### Message Processing Flow: 1. **Message Reception** ```typescript // Discord message received if (message.author.bot) return; // Ignore bot messages if (!this.shouldProcessMessage(message)) return; ``` 2. **Format Conversion** ```typescript const elizaMessage = await this.convertMessage(message); elizaMessage.channelId = message.channel.id; elizaMessage.serverId = message.guild?.id; ``` 3. **Attachment Processing** ```typescript if (message.attachments.size > 0) { elizaMessage.attachments = await this.processAttachments(message); } ``` 4. **Response Handling** ```typescript const callback = async (response: Content) => { await this.sendResponse(message.channel, response); }; ``` ### Voice Manager The `VoiceManager` class manages voice channel operations: ```typescript export class VoiceManager { private client: DiscordJsClient; private runtime: IAgentRuntime; private connections: Map; async joinChannel(channel: VoiceChannel): Promise { // Create voice connection // Set up audio processing // Handle connection events } async processAudioStream(stream: AudioStream): Promise { // Process incoming audio // Send to transcription service // Handle transcribed text } } ``` #### Voice Features: 1. **Connection Management** * Join/leave voice channels * Handle connection state changes * Manage multiple connections 2. **Audio Processing** * Capture audio streams * Process voice activity * Handle speaker changes 3. **Transcription Integration** * Send audio to transcription services * Process transcribed text * Generate responses ### Attachment Handler Processes various types of Discord attachments: ```typescript export async function processAttachments( attachments: Attachment[], runtime: IAgentRuntime ): Promise { const contents: Content[] = []; for (const attachment of attachments) { if (isImage(attachment)) { // Process image with vision model const description = await describeImage(attachment.url, runtime); contents.push({ type: 'image', description }); } else if (isAudio(attachment)) { // Transcribe audio const transcript = await transcribeAudio(attachment.url, runtime); contents.push({ type: 'audio', transcript }); } } return contents; } ``` ## Event Processing Flow ### 1. Guild Join Event ```typescript client.on(Events.GuildCreate, async (guild: Guild) => { // Create server room await createGuildRoom(guild); // Emit WORLD_JOINED event runtime.emitEvent([DiscordEventTypes.GUILD_CREATE, EventType.WORLD_JOINED], { world: convertGuildToWorld(guild), runtime }); // Register slash commands await registerCommands(guild); }); ``` ### 2. Message Create Event ```typescript client.on(Events.MessageCreate, async (message: DiscordMessage) => { // Check permissions and filters if (!shouldProcessMessage(message)) return; // Process through MessageManager await messageManager.handleMessage(message); // Track conversation context updateConversationContext(message); }); ``` ### 3. Interaction Create Event ```typescript client.on(Events.InteractionCreate, async (interaction: Interaction) => { if (!interaction.isChatInputCommand()) return; // Route to appropriate handler const handler = commandHandlers.get(interaction.commandName); if (handler) { await handler(interaction, runtime); } }); ``` ## Actions ### chatWithAttachments Handles messages that include media attachments: ```typescript export const chatWithAttachments: Action = { name: "CHAT_WITH_ATTACHMENTS", description: "Process and respond to messages with attachments", async handler(runtime, message, state, options, callback) { // Process attachments const processedContent = await processAttachments( message.attachments, runtime ); // Generate response considering attachments const response = await generateResponse( message, processedContent, runtime ); // Send response await callback(response); } }; ``` ### joinVoice Connects the bot to a voice channel: ```typescript export const joinVoice: Action = { name: "JOIN_VOICE", description: "Join a voice channel", async handler(runtime, message, state, options, callback) { const channelId = options.channelId || message.channelId; const channel = await client.channels.fetch(channelId); if (channel?.type === ChannelType.GuildVoice) { await voiceManager.joinChannel(channel); await callback({ text: `Joined voice channel: ${channel.name}` }); } } }; ``` ### transcribeMedia Transcribes audio or video files: ```typescript export const transcribeMedia: Action = { name: "TRANSCRIBE_MEDIA", description: "Convert audio/video to text", async handler(runtime, message, state, options, callback) { const mediaUrl = options.url || message.attachments?.[0]?.url; if (mediaUrl) { const transcript = await transcribeAudio(mediaUrl, runtime); await callback({ text: `Transcript: ${transcript}` }); } } }; ``` ## Providers ### channelStateProvider Provides current Discord channel context: ```typescript export const channelStateProvider: Provider = { name: "CHANNEL_STATE", description: "Current Discord channel information", async get(runtime, message, state) { const channelId = message.channelId; const channel = await client.channels.fetch(channelId); return { channelId, channelName: channel?.name, channelType: channel?.type, guildId: channel?.guild?.id, guildName: channel?.guild?.name, memberCount: channel?.guild?.memberCount }; } }; ``` ### voiceStateProvider Provides voice channel state information: ```typescript export const voiceStateProvider: Provider = { name: "VOICE_STATE", description: "Voice channel state and members", async get(runtime, message, state) { const voiceChannel = getCurrentVoiceChannel(message.serverId); if (!voiceChannel) return null; return { channelId: voiceChannel.id, channelName: voiceChannel.name, members: voiceChannel.members.map(m => ({ id: m.id, name: m.displayName, speaking: m.voice.speaking })), connection: { state: voiceConnection?.state, ping: voiceConnection?.ping } }; } }; ``` ## Configuration ### Environment Variables ```bash # Required DISCORD_APPLICATION_ID=123456789012345678 DISCORD_API_TOKEN=your-bot-token-here # Optional Channel Restrictions CHANNEL_IDS=123456789012345678,987654321098765432 # Voice Configuration DISCORD_VOICE_CHANNEL_ID=123456789012345678 VOICE_ACTIVITY_THRESHOLD=0.5 # Testing DISCORD_TEST_CHANNEL_ID=123456789012345678 ``` ### Bot Permissions Required Discord permissions: ```typescript const requiredPermissions = new PermissionsBitField([ // Text Permissions PermissionsBitField.Flags.ViewChannel, PermissionsBitField.Flags.SendMessages, PermissionsBitField.Flags.SendMessagesInThreads, PermissionsBitField.Flags.CreatePublicThreads, PermissionsBitField.Flags.CreatePrivateThreads, PermissionsBitField.Flags.EmbedLinks, PermissionsBitField.Flags.AttachFiles, PermissionsBitField.Flags.ReadMessageHistory, PermissionsBitField.Flags.AddReactions, PermissionsBitField.Flags.UseExternalEmojis, // Voice Permissions PermissionsBitField.Flags.Connect, PermissionsBitField.Flags.Speak, PermissionsBitField.Flags.UseVAD, // Application Commands PermissionsBitField.Flags.UseApplicationCommands ]); ``` ### Bot Invitation Generate an invitation URL: ```typescript const inviteUrl = `https://discord.com/api/oauth2/authorize?` + `client_id=${DISCORD_APPLICATION_ID}` + `&permissions=${requiredPermissions.bitfield}` + `&scope=bot%20applications.commands`; ``` ## Multi-Server Architecture The plugin supports operating across multiple Discord servers simultaneously: ### Server Isolation Each server maintains its own: * Conversation context * User relationships * Channel states * Voice connections ```typescript // Server-specific context const serverContext = new Map(); interface ServerContext { guildId: string; conversations: Map; voiceConnection?: VoiceConnection; settings: ServerSettings; } ``` ### Command Registration Slash commands are registered per-server: ```typescript async function registerServerCommands(guild: Guild) { const commands = [ { name: 'chat', description: 'Chat with the bot', options: [{ name: 'message', type: ApplicationCommandOptionType.String, description: 'Your message', required: true }] } ]; await guild.commands.set(commands); } ``` ## Permission Management ### Permission Checking Before performing actions: ```typescript function checkPermissions( channel: GuildChannel, permissions: PermissionsBitField ): boolean { const botMember = channel.guild.members.me; if (!botMember) return false; const channelPerms = channel.permissionsFor(botMember); return channelPerms?.has(permissions) ?? false; } ``` ### Error Handling Handle permission errors gracefully: ```typescript try { await channel.send(response); } catch (error) { if (error.code === 50013) { // Missing Permissions logger.warn(`Missing permissions in channel ${channel.id}`); // Try to notify in a channel where we have permissions await notifyPermissionError(channel.guild); } } ``` ## Performance Optimization ### Message Caching Cache frequently accessed data: ```typescript const messageCache = new LRUCache({ max: 1000, ttl: 1000 * 60 * 60 // 1 hour }); ``` ### Rate Limiting Implement rate limiting for API calls: ```typescript const rateLimiter = new RateLimiter({ windowMs: 60000, // 1 minute max: 30 // 30 requests per minute }); ``` ### Voice Connection Pooling Reuse voice connections: ```typescript const voiceConnectionPool = new Map(); async function getOrCreateVoiceConnection( channel: VoiceChannel ): Promise { const existing = voiceConnectionPool.get(channel.guild.id); if (existing?.state.status === VoiceConnectionStatus.Ready) { return existing; } const connection = await createNewConnection(channel); voiceConnectionPool.set(channel.guild.id, connection); return connection; } ``` ## Error Handling ### Connection Errors Handle Discord connection issues: ```typescript client.on('error', (error) => { logger.error('Discord client error:', error); // Attempt reconnection scheduleReconnection(); }); client.on('disconnect', () => { logger.warn('Discord client disconnected'); // Clean up resources cleanupConnections(); }); ``` ### API Errors Handle Discord API errors: ```typescript async function handleDiscordAPIError(error: DiscordAPIError) { switch (error.code) { case 10008: // Unknown Message logger.debug('Message not found, may have been deleted'); break; case 50001: // Missing Access logger.warn('Bot lacks access to channel'); break; case 50013: // Missing Permissions logger.warn('Bot missing required permissions'); break; default: logger.error('Discord API error:', error); } } ``` ## Integration Guide ### Basic Setup ```typescript import { discordPlugin } from '@elizaos/plugin-discord'; import { AgentRuntime } from '@elizaos/core'; const runtime = new AgentRuntime({ plugins: [discordPlugin], character: { name: "MyBot", clients: ["discord"], settings: { DISCORD_APPLICATION_ID: process.env.DISCORD_APPLICATION_ID, DISCORD_API_TOKEN: process.env.DISCORD_API_TOKEN } } }); await runtime.start(); ``` ### Custom Actions Add Discord-specific actions: ```typescript const customDiscordAction: Action = { name: "DISCORD_CUSTOM", description: "Custom Discord action", async handler(runtime, message, state, options, callback) { // Access Discord-specific context const discordService = runtime.getService('discord') as DiscordService; const channel = await discordService.client.channels.fetch(message.channelId); // Perform Discord-specific operations if (channel?.type === ChannelType.GuildText) { await channel.setTopic('Updated by bot'); } await callback({ text: "Custom action completed" }); } }; ``` ### Event Handlers Listen for Discord-specific events: ```typescript runtime.on(DiscordEventTypes.GUILD_MEMBER_ADD, async (event) => { const { member, guild } = event; // Welcome new members const welcomeChannel = guild.channels.cache.find( ch => ch.name === 'welcome' ); if (welcomeChannel?.type === ChannelType.GuildText) { await welcomeChannel.send(`Welcome ${member.user.username}!`); } }); ``` ## Best Practices 1. **Token Security** ```typescript // Never hardcode tokens const token = process.env.DISCORD_API_TOKEN; if (!token) throw new Error('Discord token not configured'); ``` 2. **Error Recovery** ```typescript // Implement exponential backoff async function retryWithBackoff(fn: Function, maxRetries = 3) { for (let i = 0; i < maxRetries; i++) { try { return await fn(); } catch (error) { if (i === maxRetries - 1) throw error; await sleep(Math.pow(2, i) * 1000); } } } ``` 3. **Resource Cleanup** ```typescript // Clean up on shutdown process.on('SIGINT', async () => { await voiceManager.disconnectAll(); client.destroy(); process.exit(0); }); ``` 4. **Monitoring** ```typescript // Track performance metrics const metrics = { messagesProcessed: 0, averageResponseTime: 0, activeVoiceConnections: 0 }; ``` ## Debugging Enable debug logging: ```bash DEBUG=eliza:discord:* npm run start ``` Common debug points: * Connection establishment * Message processing pipeline * Voice connection state * Permission checks * API rate limits ## Support For issues and questions: * 📚 Check the [examples](./examples.mdx) * 💬 Join our [Discord community](https://discord.gg/elizaos) * 🐛 Report issues on [GitHub](https://github.com/elizaos/eliza/issues) # Event Flow Source: https://docs.elizaos.ai/plugin-registry/platform/discord/event-flow This document provides a comprehensive breakdown of how events flow through the Discord plugin system. This document provides a comprehensive breakdown of how events flow through the Discord plugin system. ## Complete Event Flow Diagram ```mermaid flowchart TD Start([Discord Event]) --> A[Discord.js Client] A --> B{Event Type} B -->|Message| C[MESSAGE_CREATE Event] B -->|Interaction| D[INTERACTION_CREATE Event] B -->|Guild Join| E[GUILD_CREATE Event] B -->|Member Join| F[GUILD_MEMBER_ADD Event] B -->|Voice State| G[VOICE_STATE_UPDATE Event] %% Message Flow C --> H{Is Bot Message?} H -->|Yes| End1[Ignore] H -->|No| I[Check Channel Restrictions] I --> J{Channel Allowed?} J -->|No| End2[Ignore] J -->|Yes| K[Message Manager] K --> L{Has Attachments?} L -->|Yes| M[Process Attachments] L -->|No| N[Convert to elizaOS Format] M --> N N --> O[Add Discord Context] O --> P[Send to Bootstrap Plugin] P --> Q[Bootstrap Processes] Q --> R[Generate Response] R --> S{Has Callback?} S -->|Yes| T[Format Discord Response] S -->|No| End3[No Response] T --> U{Response Type} U -->|Text| V[Send Text Message] U -->|Embed| W[Send Embed] U -->|Buttons| X[Send with Components] V --> Y[Message Sent] W --> Y X --> Y %% Interaction Flow D --> Z{Interaction Type} Z -->|Command| AA[Slash Command Handler] Z -->|Button| AB[Button Handler] Z -->|Select Menu| AC[Select Menu Handler] AA --> AD[Validate Permissions] AD --> AE[Execute Command] AE --> AF[Send Interaction Response] %% Guild Flow E --> AG[Register Slash Commands] AG --> AH[Create Server Context] AH --> AI[Emit WORLD_JOINED] AI --> AJ[Initialize Server Settings] %% Voice Flow G --> AK{Voice Event Type} AK -->|Join| AL[Handle Voice Join] AK -->|Leave| AM[Handle Voice Leave] AK -->|Speaking| AN[Handle Speaking State] AL --> AO[Create Voice Connection] AO --> AP[Setup Audio Processing] AP --> AQ[Start Recording] AN --> AR[Process Audio Stream] AR --> AS[Transcribe Audio] AS --> AT[Process as Message] AT --> K ``` ## Detailed Event Flows ### 1. Message Processing Flow ```mermaid sequenceDiagram participant D as Discord participant C as Client participant MM as MessageManager participant AH as AttachmentHandler participant B as Bootstrap Plugin participant R as Runtime D->>C: MESSAGE_CREATE event C->>C: Check if bot message alt Is bot message C->>D: Ignore else Not bot message C->>C: Check channel restrictions alt Channel not allowed C->>D: Ignore else Channel allowed C->>MM: handleMessage() MM->>MM: Convert to elizaOS format alt Has attachments MM->>AH: processAttachments() AH->>AH: Download media AH->>AH: Process (vision/transcribe) AH->>MM: Return processed content end MM->>B: Send message with callback B->>R: Process message R->>B: Generate response B->>MM: Execute callback MM->>D: Send Discord message end end ``` ### 2. Voice Channel Flow ```mermaid sequenceDiagram participant U as User participant D as Discord participant C as Client participant VM as VoiceManager participant VC as VoiceConnection participant T as Transcription U->>D: Join voice channel D->>C: VOICE_STATE_UPDATE C->>VM: handleVoiceStateUpdate() VM->>VC: Create connection VC->>D: Connect to channel loop While in channel U->>D: Speak D->>VC: Audio stream VC->>VM: Process audio VM->>T: Transcribe audio T->>VM: Return text VM->>C: Create message from transcript C->>C: Process as text message end U->>D: Leave channel D->>C: VOICE_STATE_UPDATE C->>VM: handleVoiceStateUpdate() VM->>VC: Disconnect VM->>VM: Cleanup resources ``` ### 3. Slash Command Flow ```mermaid sequenceDiagram participant U as User participant D as Discord participant C as Client participant CH as CommandHandler participant A as Action participant R as Runtime U->>D: /command input D->>C: INTERACTION_CREATE C->>C: Check interaction type C->>CH: Route to handler CH->>CH: Validate permissions alt No permission CH->>D: Error response else Has permission CH->>CH: Parse arguments CH->>A: Execute action A->>R: Process with runtime R->>A: Return result A->>CH: Action complete CH->>D: Send response alt Needs follow-up CH->>D: Send follow-up end end ``` ### 4. Attachment Processing Flow ```mermaid flowchart TD A[Attachment Received] --> B{Attachment Type} B -->|Image| C[Image Handler] B -->|Audio| D[Audio Handler] B -->|Video| E[Video Handler] B -->|Document| F[Document Handler] B -->|Other| G[Generic Handler] C --> H[Download Image] H --> I[Check Image Size] I --> J{Size OK?} J -->|No| K[Resize Image] J -->|Yes| L[Send to Vision Model] K --> L L --> M[Generate Description] D --> N[Download Audio] N --> O[Convert Format if Needed] O --> P[Send to Transcription] P --> Q[Return Transcript] E --> R[Download Video] R --> S[Extract Audio Track] S --> P F --> T[Download Document] T --> U[Extract Text Content] M --> V[Add to Message Context] Q --> V U --> V G --> V V --> W[Continue Processing] ``` ### 5. Multi-Server Event Flow ```mermaid flowchart TD A[Bot Joins Server] --> B[GUILD_CREATE Event] B --> C[Create Server Context] C --> D[Initialize Components] D --> E[Message Context Map] D --> F[Voice Connection Pool] D --> G[User Relationship Map] D --> H[Server Settings] B --> I[Register Commands] I --> J[Guild-Specific Commands] I --> K[Global Commands] B --> L[Emit WORLD_JOINED] L --> M[Create World Entity] L --> N[Create Room Entities] L --> O[Create User Entities] P[Server Events] --> Q{Event Type} Q -->|Message| R[Route to Server Context] Q -->|Voice| S[Server Voice Manager] Q -->|Member| T[Update Relationships] R --> U[Process with Context] S --> V[Manage Connection] T --> W[Update Entity] ``` ## Event Type Reference ### Discord.js Events | Event | Description | Plugin Handler | | ------------------- | -------------------- | -------------------- | | `ready` | Client is ready | Initialize services | | `messageCreate` | New message | MessageManager | | `messageUpdate` | Message edited | MessageManager | | `messageDelete` | Message deleted | Cleanup handler | | `interactionCreate` | Slash command/button | Interaction router | | `guildCreate` | Bot joins server | Server initializer | | `guildDelete` | Bot leaves server | Cleanup handler | | `guildMemberAdd` | Member joins | Relationship manager | | `voiceStateUpdate` | Voice state change | VoiceManager | | `error` | Client error | Error handler | | `disconnect` | Lost connection | Reconnection handler | ### elizaOS Events Emitted | Event | When Emitted | Payload | | ------------------------ | ------------------ | ---------------------- | | `WORLD_JOINED` | Bot joins server | World, rooms, entities | | `MESSAGE_RECEIVED` | Message processed | elizaOS message format | | `VOICE_MESSAGE_RECEIVED` | Voice transcribed | Transcribed message | | `REACTION_RECEIVED` | Reaction added | Reaction details | | `INTERACTION_RECEIVED` | Slash command used | Interaction data | ## State Management ### Message Context ```typescript interface MessageContext { channelId: string; serverId: string; userId: string; threadId?: string; referencedMessageId?: string; attachments: ProcessedAttachment[]; discordMetadata: { messageId: string; timestamp: number; editedTimestamp?: number; isPinned: boolean; mentions: string[]; }; } ``` ### Voice Context ```typescript interface VoiceContext { channelId: string; serverId: string; connection: VoiceConnection; activeUsers: Map; recordingState: { isRecording: boolean; startTime?: number; audioBuffer: Buffer[]; }; } ``` ## Error Handling in Event Flow ### Error Propagation ```mermaid flowchart TD A[Event Error] --> B{Error Type} B -->|Permission Error| C[Log Warning] B -->|Network Error| D[Retry Logic] B -->|API Error| E[Handle API Error] B -->|Unknown Error| F[Log Error] C --> G[Notify User if Possible] D --> H{Retry Count} H -->|< Max| I[Exponential Backoff] H -->|>= Max| J[Give Up] I --> K[Retry Operation] E --> L{Error Code} L -->|Rate Limit| M[Queue for Later] L -->|Invalid Request| N[Log and Skip] L -->|Server Error| O[Retry Later] F --> P[Send to Error Reporter] P --> Q[Continue Processing] ``` ## Performance Considerations ### Event Batching For high-volume servers, events are batched: ```typescript class EventBatcher { private messageQueue: DiscordMessage[] = []; private batchTimer?: NodeJS.Timeout; addMessage(message: DiscordMessage) { this.messageQueue.push(message); if (!this.batchTimer) { this.batchTimer = setTimeout(() => { this.processBatch(); }, 100); // 100ms batch window } } private async processBatch() { const batch = [...this.messageQueue]; this.messageQueue = []; this.batchTimer = undefined; // Process messages in parallel await Promise.all( batch.map(msg => this.processMessage(msg)) ); } } ``` ### Connection Pooling Voice connections are pooled to reduce overhead: ```typescript class VoiceConnectionPool { private connections = new Map(); private maxConnections = 10; async getConnection(channelId: string): Promise { // Reuse existing connection const existing = this.connections.get(channelId); if (existing?.state.status === VoiceConnectionStatus.Ready) { return existing; } // Check pool limit if (this.connections.size >= this.maxConnections) { await this.evictOldestConnection(); } // Create new connection const connection = await this.createConnection(channelId); this.connections.set(channelId, connection); return connection; } } ``` ## Monitoring Event Flow ### Event Metrics Track event processing metrics: ```typescript interface EventMetrics { eventType: string; processingTime: number; success: boolean; errorType?: string; serverId: string; channelId: string; } class EventMonitor { private metrics: EventMetrics[] = []; recordEvent(metric: EventMetrics) { this.metrics.push(metric); // Log slow events if (metric.processingTime > 1000) { logger.warn(`Slow event processing: ${metric.eventType} took ${metric.processingTime}ms`); } } getStats() { return { totalEvents: this.metrics.length, averageProcessingTime: this.calculateAverage(), errorRate: this.calculateErrorRate(), eventBreakdown: this.getEventTypeBreakdown() }; } } ``` ## Best Practices 1. **Event Debouncing** * Debounce rapid events (typing indicators, voice state) * Batch similar events when possible 2. **Error Isolation** * Don't let one event error affect others * Use try-catch at event handler level 3. **Resource Management** * Clean up event listeners on disconnect * Limit concurrent event processing 4. **Monitoring** * Track event processing times * Monitor error rates by event type * Alert on unusual patterns # Examples Source: https://docs.elizaos.ai/plugin-registry/platform/discord/examples This document provides practical examples of using the @elizaos/plugin-discord package in various scenarios. # Discord Plugin Examples This document provides practical examples of using the @elizaos/plugin-discord package in various scenarios. ## Basic Bot Setup ### Simple Message Bot Create a basic Discord bot that responds to messages: ```typescript import { AgentRuntime } from '@elizaos/core'; import { discordPlugin } from '@elizaos/plugin-discord'; import { bootstrapPlugin } from '@elizaos/plugin-bootstrap'; const character = { name: "SimpleBot", description: "A simple Discord bot", plugins: [bootstrapPlugin, discordPlugin], clients: ["discord"], settings: { DISCORD_APPLICATION_ID: process.env.DISCORD_APPLICATION_ID, DISCORD_API_TOKEN: process.env.DISCORD_API_TOKEN }, // Message examples for the bot's personality messageExamples: [ { user: "user", content: { text: "Hello!" }, response: { text: "Hello! How can I help you today?" } }, { user: "user", content: { text: "What can you do?" }, response: { text: "I can chat with you, answer questions, and help with various tasks!" } } ] }; // Create and start the runtime const runtime = new AgentRuntime({ character }); await runtime.start(); ``` ### Channel-Restricted Bot Limit the bot to specific channels: ```typescript const channelRestrictedBot = { name: "RestrictedBot", description: "A bot that only works in specific channels", plugins: [bootstrapPlugin, discordPlugin], clients: ["discord"], settings: { DISCORD_APPLICATION_ID: process.env.DISCORD_APPLICATION_ID, DISCORD_API_TOKEN: process.env.DISCORD_API_TOKEN, // Only respond in these channels CHANNEL_IDS: "123456789012345678,987654321098765432" } }; ``` ## Voice Channel Bot ### Basic Voice Bot Create a bot that can join voice channels: ```typescript import { Action } from '@elizaos/core'; const voiceBot = { name: "VoiceAssistant", description: "A voice-enabled Discord bot", plugins: [bootstrapPlugin, discordPlugin], clients: ["discord"], settings: { DISCORD_APPLICATION_ID: process.env.DISCORD_APPLICATION_ID, DISCORD_API_TOKEN: process.env.DISCORD_API_TOKEN, // Auto-join this voice channel on startup DISCORD_VOICE_CHANNEL_ID: process.env.DISCORD_VOICE_CHANNEL_ID } }; // Custom action to join voice on command const joinVoiceAction: Action = { name: "JOIN_VOICE_COMMAND", description: "Join the user's voice channel", similes: ["join voice", "come to voice", "join vc"], validate: async (runtime, message) => { // Check if user is in a voice channel const discordService = runtime.getService('discord'); const member = await discordService.getMember(message.userId, message.serverId); return member?.voice?.channel != null; }, handler: async (runtime, message, state, options, callback) => { const discordService = runtime.getService('discord'); const member = await discordService.getMember(message.userId, message.serverId); if (member?.voice?.channel) { await discordService.voiceManager.joinChannel(member.voice.channel); await callback({ text: `Joined ${member.voice.channel.name}!` }); } return true; } }; ``` ### Voice Transcription Bot Bot that transcribes voice conversations: ```typescript const transcriptionBot = { name: "TranscriptionBot", description: "Transcribes voice channel conversations", plugins: [bootstrapPlugin, discordPlugin], clients: ["discord"], settings: { DISCORD_APPLICATION_ID: process.env.DISCORD_APPLICATION_ID, DISCORD_API_TOKEN: process.env.DISCORD_API_TOKEN, ENABLE_VOICE_TRANSCRIPTION: "true", VOICE_ACTIVITY_THRESHOLD: "0.5" }, // Custom templates for voice interactions templates: { voiceMessageTemplate: `Respond to this voice message from {{user}}: Transcription: {{transcript}} Keep your response brief and conversational.` } }; // Handle transcribed voice messages runtime.on('VOICE_MESSAGE_RECEIVED', async (event) => { const { message, transcript } = event; console.log(`Voice message from ${message.userName}: ${transcript}`); }); ``` ## Slash Command Bot ### Basic Slash Commands Implement Discord slash commands: ```typescript import { SlashCommandBuilder } from 'discord.js'; const slashCommandBot = { name: "CommandBot", description: "Bot with slash commands", plugins: [bootstrapPlugin, discordPlugin], clients: ["discord"], settings: { DISCORD_APPLICATION_ID: process.env.DISCORD_APPLICATION_ID, DISCORD_API_TOKEN: process.env.DISCORD_API_TOKEN } }; // Custom slash command registration runtime.on('DISCORD_READY', async (event) => { const { client } = event; const commands = [ new SlashCommandBuilder() .setName('ask') .setDescription('Ask the bot a question') .addStringOption(option => option.setName('question') .setDescription('Your question') .setRequired(true) ), new SlashCommandBuilder() .setName('summarize') .setDescription('Summarize recent conversation') .addIntegerOption(option => option.setName('messages') .setDescription('Number of messages to summarize') .setMinValue(5) .setMaxValue(50) .setRequired(false) ) ]; // Register commands globally await client.application.commands.set(commands); }); ``` ### Advanced Command Handling Handle complex slash command interactions: ```typescript const advancedCommandAction: Action = { name: "HANDLE_SLASH_COMMAND", description: "Process slash command interactions", handler: async (runtime, message, state, options, callback) => { const { commandName, options: cmdOptions } = message.content; switch (commandName) { case 'ask': const question = cmdOptions.getString('question'); // Process question through the agent const response = await runtime.processMessage({ ...message, content: { text: question } }); await callback(response); break; case 'summarize': const count = cmdOptions.getInteger('messages') || 20; const summary = await summarizeConversation(runtime, message.channelId, count); await callback({ text: `Summary of last ${count} messages:\n\n${summary}` }); break; case 'settings': // Show interactive settings menu await callback({ text: "Bot Settings", components: [{ type: 'ACTION_ROW', components: [{ type: 'SELECT_MENU', customId: 'settings_menu', placeholder: 'Choose a setting', options: [ { label: 'Response Style', value: 'style' }, { label: 'Language', value: 'language' }, { label: 'Notifications', value: 'notifications' } ] }] }] }); break; } return true; } }; ``` ## Image Analysis Bot ### Vision-Enabled Bot Bot that can analyze images: ```typescript const imageAnalysisBot = { name: "VisionBot", description: "Analyzes images using vision capabilities", plugins: [bootstrapPlugin, discordPlugin], clients: ["discord"], modelProvider: "openai", settings: { DISCORD_APPLICATION_ID: process.env.DISCORD_APPLICATION_ID, DISCORD_API_TOKEN: process.env.DISCORD_API_TOKEN, OPENAI_API_KEY: process.env.OPENAI_API_KEY } }; // Custom image analysis action const analyzeImageAction: Action = { name: "ANALYZE_IMAGE", description: "Analyze attached images", validate: async (runtime, message) => { return message.attachments?.some(att => att.contentType?.startsWith('image/') ) ?? false; }, handler: async (runtime, message, state, options, callback) => { const imageAttachment = message.attachments.find(att => att.contentType?.startsWith('image/') ); if (imageAttachment) { // The Discord plugin automatically processes images // and adds descriptions to the message content const description = imageAttachment.description; await callback({ text: `I can see: ${description}\n\nWhat would you like to know about this image?` }); } return true; } }; ``` ## Reaction Bot ### Emoji Reaction Handler Bot that responds to reactions: ```typescript const reactionBot = { name: "ReactionBot", description: "Responds to emoji reactions", plugins: [bootstrapPlugin, discordPlugin], clients: ["discord"], settings: { DISCORD_APPLICATION_ID: process.env.DISCORD_APPLICATION_ID, DISCORD_API_TOKEN: process.env.DISCORD_API_TOKEN } }; // Handle reaction events runtime.on('REACTION_RECEIVED', async (event) => { const { reaction, user, message } = event; // Respond to specific emojis switch (reaction.emoji.name) { case '👍': await message.reply(`Thanks for the thumbs up, ${user.username}!`); break; case '❓': await message.reply(`Do you have a question about this message?`); break; case '📌': // Pin important messages if (!message.pinned) { await message.pin(); await message.reply(`Pinned this message!`); } break; } }); ``` ## Multi-Server Bot ### Server-Specific Configuration Bot with per-server settings: ```typescript const multiServerBot = { name: "MultiServerBot", description: "Bot that adapts to different servers", plugins: [bootstrapPlugin, discordPlugin], clients: ["discord"], settings: { DISCORD_APPLICATION_ID: process.env.DISCORD_APPLICATION_ID, DISCORD_API_TOKEN: process.env.DISCORD_API_TOKEN } }; // Server-specific settings storage const serverSettings = new Map(); // Initialize server settings on join runtime.on('WORLD_JOINED', async (event) => { const { world } = event; const serverId = world.serverId; // Load or create server settings if (!serverSettings.has(serverId)) { serverSettings.set(serverId, { prefix: '!', language: 'en', responseStyle: 'friendly', allowedChannels: [], moderatorRoles: [] }); } }); // Use server-specific settings const serverAwareAction: Action = { name: "SERVER_AWARE_RESPONSE", description: "Respond based on server settings", handler: async (runtime, message, state, options, callback) => { const settings = serverSettings.get(message.serverId); // Apply server-specific behavior const response = await generateResponse(message, { style: settings.responseStyle, language: settings.language }); await callback(response); return true; } }; ``` ## Media Downloader ### Download and Process Media Bot that downloads and processes media files: ```typescript const mediaDownloaderAction: Action = { name: "DOWNLOAD_MEDIA", description: "Download media from messages", similes: ["download this", "save this media", "get this file"], validate: async (runtime, message) => { return message.attachments?.length > 0; }, handler: async (runtime, message, state, options, callback) => { const results = []; for (const attachment of message.attachments) { try { // Use the Discord plugin's download action const downloadResult = await runtime.executeAction( "DOWNLOAD_MEDIA", message, { url: attachment.url } ); results.push({ name: attachment.filename, size: attachment.size, path: downloadResult.path }); } catch (error) { results.push({ name: attachment.filename, error: error.message }); } } const summary = results.map(r => r.error ? `❌ ${r.name}: ${r.error}` : `✅ ${r.name} (${formatBytes(r.size)}) saved to ${r.path}` ).join('\n'); await callback({ text: `Media download results:\n\n${summary}` }); return true; } }; function formatBytes(bytes: number): string { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } ``` ## Custom Actions ### Creating Discord-Specific Actions ```typescript const customDiscordAction: Action = { name: "DISCORD_SERVER_INFO", description: "Get information about the current Discord server", similes: ["server info", "guild info", "about this server"], validate: async (runtime, message) => { // Only works in guild channels return message.serverId != null; }, handler: async (runtime, message, state, options, callback) => { const discordService = runtime.getService('discord'); const guild = await discordService.client.guilds.fetch(message.serverId); const info = { name: guild.name, description: guild.description || 'No description', memberCount: guild.memberCount, created: guild.createdAt.toLocaleDateString(), boostLevel: guild.premiumTier, features: guild.features.join(', ') || 'None' }; await callback({ text: `**Server Information**\n` + `Name: ${info.name}\n` + `Description: ${info.description}\n` + `Members: ${info.memberCount}\n` + `Created: ${info.created}\n` + `Boost Level: ${info.boostLevel}\n` + `Features: ${info.features}` }); return true; } }; // Register the custom action runtime.registerAction(customDiscordAction); ``` ## Integration Examples ### With Other Plugins Integrate Discord with other elizaOS plugins: ```typescript import { discordPlugin } from '@elizaos/plugin-discord'; import { bootstrapPlugin } from '@elizaos/plugin-bootstrap'; import { webSearchPlugin } from '@elizaos/plugin-websearch'; import { imageGenerationPlugin } from '@elizaos/plugin-image-generation'; const integratedBot = { name: "IntegratedBot", description: "Bot with multiple plugin integrations", plugins: [ bootstrapPlugin, discordPlugin, webSearchPlugin, imageGenerationPlugin ], clients: ["discord"], settings: { DISCORD_APPLICATION_ID: process.env.DISCORD_APPLICATION_ID, DISCORD_API_TOKEN: process.env.DISCORD_API_TOKEN, OPENAI_API_KEY: process.env.OPENAI_API_KEY, GOOGLE_SEARCH_API_KEY: process.env.GOOGLE_SEARCH_API_KEY } }; // Action that combines multiple plugins const searchAndShareAction: Action = { name: "SEARCH_AND_SHARE", description: "Search the web and share results", similes: ["search for", "look up", "find information about"], handler: async (runtime, message, state, options, callback) => { // Extract search query const query = extractQuery(message.content.text); // Use web search plugin const searchResults = await runtime.executeAction( "WEB_SEARCH", message, { query } ); // Format results for Discord const embed = { title: `Search Results for "${query}"`, fields: searchResults.slice(0, 5).map(result => ({ name: result.title, value: `${result.snippet}\n[Read more](${result.link})`, inline: false })), color: 0x0099ff, timestamp: new Date() }; await callback({ embeds: [embed] }); return true; } }; ``` ## Error Handling Examples ### Graceful Error Handling ```typescript const errorHandlingAction: Action = { name: "SAFE_ACTION", description: "Action with comprehensive error handling", handler: async (runtime, message, state, options, callback) => { try { // Attempt the main operation const result = await riskyOperation(); await callback({ text: `Success: ${result}` }); } catch (error) { // Log the error runtime.logger.error('Action failed:', error); // Provide user-friendly error message if (error.code === 50013) { await callback({ text: "I don't have permission to do that in this channel." }); } else if (error.code === 50001) { await callback({ text: "I can't access that channel or message." }); } else { await callback({ text: "Something went wrong. Please try again later." }); } } return true; } }; ``` ## Testing Examples ### Test Suite for Discord Bot ```typescript import { DiscordTestSuite } from '@elizaos/plugin-discord'; const testSuite = new DiscordTestSuite(); // Configure test environment testSuite.configure({ testChannelId: process.env.DISCORD_TEST_CHANNEL_ID, testVoiceChannelId: process.env.DISCORD_TEST_VOICE_CHANNEL_ID }); // Run tests await testSuite.run(); ``` ## Best Practices Examples ### Rate Limiting ```typescript import { RateLimiter } from '@elizaos/core'; const rateLimitedAction: Action = { name: "RATE_LIMITED_ACTION", description: "Action with rate limiting", handler: async (runtime, message, state, options, callback) => { const limiter = new RateLimiter({ windowMs: 60000, // 1 minute max: 5 // 5 requests per minute per user }); if (!limiter.tryConsume(message.userId)) { await callback({ text: "Please wait a moment before using this command again." }); return false; } // Proceed with action await performAction(); return true; } }; ``` ### Caching ```typescript import { LRUCache } from 'lru-cache'; const cachedDataAction: Action = { name: "CACHED_DATA", description: "Action that uses caching", handler: async (runtime, message, state, options, callback) => { const cache = runtime.getCache('discord-data'); const cacheKey = `user-data-${message.userId}`; // Try to get from cache let userData = cache.get(cacheKey); if (!userData) { // Fetch fresh data userData = await fetchUserData(message.userId); // Cache for 5 minutes cache.set(cacheKey, userData, { ttl: 300000 }); } await callback({ text: `Your data: ${JSON.stringify(userData)}` }); return true; } }; ``` # Testing Guide Source: https://docs.elizaos.ai/plugin-registry/platform/discord/testing-guide This guide covers testing strategies, patterns, and best practices for the @elizaos/plugin-discord package. This guide covers testing strategies, patterns, and best practices for the @elizaos/plugin-discord package. ## Test Environment Setup ### Prerequisites 1. **Test Discord Server** * Create a dedicated Discord server for testing * Set up test channels (text, voice, etc.) * Configure appropriate permissions 2. **Test Bot Application** * Create a separate bot application for testing * Generate test credentials * Add bot to test server with full permissions 3. **Environment Configuration** ```bash # .env.test DISCORD_APPLICATION_ID=test_application_id DISCORD_API_TOKEN=test_bot_token DISCORD_TEST_CHANNEL_ID=test_text_channel_id DISCORD_TEST_VOICE_CHANNEL_ID=test_voice_channel_id DISCORD_TEST_SERVER_ID=test_server_id # Test user for interactions DISCORD_TEST_USER_ID=test_user_id ``` ## Unit Testing ### Testing Message Manager ```typescript import { describe, it, expect, beforeEach, vi } from 'vitest'; import { MessageManager } from '@elizaos/plugin-discord'; import { Client, Message, TextChannel } from 'discord.js'; describe('MessageManager', () => { let messageManager: MessageManager; let mockClient: Client; let mockRuntime: any; beforeEach(() => { // Mock Discord.js client mockClient = { channels: { cache: new Map(), fetch: vi.fn() }, user: { id: 'bot-id' } } as any; // Mock runtime mockRuntime = { processMessage: vi.fn(), character: { name: 'TestBot' }, logger: { info: vi.fn(), error: vi.fn() } }; messageManager = new MessageManager(mockClient, mockRuntime); }); describe('handleMessage', () => { it('should ignore bot messages', async () => { const mockMessage = { author: { bot: true }, content: 'Test message' } as any; await messageManager.handleMessage(mockMessage); expect(mockRuntime.processMessage).not.toHaveBeenCalled(); }); it('should process user messages', async () => { const mockMessage = { author: { bot: false, id: 'user-123' }, content: 'Hello bot', channel: { id: 'channel-123' }, guild: { id: 'guild-123' } } as any; mockRuntime.processMessage.mockResolvedValue({ text: 'Hello user!' }); await messageManager.handleMessage(mockMessage); expect(mockRuntime.processMessage).toHaveBeenCalledWith( expect.objectContaining({ content: { text: 'Hello bot' }, channelId: 'channel-123', serverId: 'guild-123' }) ); }); it('should handle attachments', async () => { const mockMessage = { author: { bot: false, id: 'user-123' }, content: 'Check this image', attachments: new Map([ ['123', { url: 'https://example.com/image.png', contentType: 'image/png', name: 'image.png' }] ]), channel: { id: 'channel-123' } } as any; await messageManager.handleMessage(mockMessage); expect(mockRuntime.processMessage).toHaveBeenCalledWith( expect.objectContaining({ attachments: expect.arrayContaining([ expect.objectContaining({ url: 'https://example.com/image.png', contentType: 'image/png' }) ]) }) ); }); }); }); ``` ### Testing Voice Manager ```typescript import { VoiceManager } from '@elizaos/plugin-discord'; import { VoiceChannel, VoiceConnection } from '@discordjs/voice'; describe('VoiceManager', () => { let voiceManager: VoiceManager; let mockChannel: VoiceChannel; beforeEach(() => { voiceManager = new VoiceManager(mockClient, mockRuntime); mockChannel = { id: 'voice-123', name: 'Test Voice', guild: { id: 'guild-123' }, joinable: true } as any; }); describe('joinChannel', () => { it('should create voice connection', async () => { const connection = await voiceManager.joinChannel(mockChannel); expect(connection).toBeDefined(); expect(voiceManager.getConnection('guild-123')).toBe(connection); }); it('should handle connection errors', async () => { mockChannel.joinable = false; await expect(voiceManager.joinChannel(mockChannel)) .rejects .toThrow('Cannot join voice channel'); }); }); describe('audio processing', () => { it('should process audio stream', async () => { const mockStream = createMockAudioStream(); const transcribeSpy = vi.spyOn(voiceManager, 'transcribeAudio'); await voiceManager.processAudioStream(mockStream, 'user-123'); expect(transcribeSpy).toHaveBeenCalled(); }); }); }); ``` ## Integration Testing ### Testing Discord Service ```typescript import { DiscordService } from '@elizaos/plugin-discord'; import { AgentRuntime } from '@elizaos/core'; describe('DiscordService Integration', () => { let service: DiscordService; let runtime: AgentRuntime; beforeAll(async () => { runtime = new AgentRuntime({ character: { name: 'TestBot', clients: ['discord'] }, settings: { DISCORD_API_TOKEN: process.env.DISCORD_TEST_TOKEN, DISCORD_APPLICATION_ID: process.env.DISCORD_TEST_APP_ID } }); service = new DiscordService(runtime); await service.start(); }); afterAll(async () => { await service.stop(); }); it('should connect to Discord', async () => { expect(service.client).toBeDefined(); expect(service.client.isReady()).toBe(true); }); it('should handle slash commands', async () => { const testChannel = await service.client.channels.fetch( process.env.DISCORD_TEST_CHANNEL_ID ); // Simulate slash command const interaction = createMockInteraction({ commandName: 'test', channel: testChannel }); await service.handleInteraction(interaction); // Verify response was sent expect(interaction.reply).toHaveBeenCalled(); }); }); ``` ### Testing Message Flow ```typescript describe('Message Flow Integration', () => { it('should process message end-to-end', async () => { const testMessage = await sendTestMessage( 'Hello bot!', process.env.DISCORD_TEST_CHANNEL_ID ); // Wait for bot response const response = await waitForBotResponse(testMessage.channel, 5000); expect(response).toBeDefined(); expect(response.content).toContain('Hello'); }); it('should handle media attachments', async () => { const testMessage = await sendTestMessageWithImage( 'What is this?', 'test-image.png', process.env.DISCORD_TEST_CHANNEL_ID ); const response = await waitForBotResponse(testMessage.channel, 10000); expect(response.content).toMatch(/I can see|image shows/i); }); }); ``` ## E2E Testing ### Complete Bot Test Suite ```typescript import { DiscordTestSuite } from '@elizaos/plugin-discord/tests'; describe('Discord Bot E2E Tests', () => { const suite = new DiscordTestSuite({ testChannelId: process.env.DISCORD_TEST_CHANNEL_ID, testVoiceChannelId: process.env.DISCORD_TEST_VOICE_CHANNEL_ID, testUserId: process.env.DISCORD_TEST_USER_ID }); beforeAll(async () => { await suite.setup(); }); afterAll(async () => { await suite.cleanup(); }); describe('Text Interactions', () => { it('should respond to messages', async () => { const result = await suite.testMessageResponse({ content: 'Hello!', expectedPattern: /hello|hi|hey/i }); expect(result.success).toBe(true); }); it('should handle mentions', async () => { const result = await suite.testMention({ content: 'Hey bot, how are you?', expectedResponse: true }); expect(result.responded).toBe(true); }); }); describe('Voice Interactions', () => { it('should join voice channel', async () => { const result = await suite.testVoiceJoin(); expect(result.connected).toBe(true); }); it('should transcribe voice', async () => { const result = await suite.testVoiceTranscription({ audioFile: 'test-audio.mp3', expectedTranscript: 'hello world' }); expect(result.transcript).toContain('hello'); }); }); describe('Slash Commands', () => { it('should execute slash commands', async () => { const result = await suite.testSlashCommand({ command: 'chat', options: { message: 'Test message' } }); expect(result.success).toBe(true); }); }); }); ``` ## Performance Testing ### Load Testing ```typescript import { performance } from 'perf_hooks'; describe('Performance Tests', () => { it('should handle multiple concurrent messages', async () => { const messageCount = 100; const startTime = performance.now(); const promises = Array(messageCount).fill(0).map((_, i) => sendTestMessage(`Test message ${i}`, testChannelId) ); await Promise.all(promises); const endTime = performance.now(); const totalTime = endTime - startTime; const avgTime = totalTime / messageCount; expect(avgTime).toBeLessThan(1000); // Less than 1s per message }); it('should maintain voice connection stability', async () => { const duration = 60000; // 1 minute const startTime = Date.now(); await voiceManager.joinChannel(testVoiceChannel); // Monitor connection status const checkInterval = setInterval(() => { const connection = voiceManager.getConnection(testServerId); expect(connection?.state.status).toBe('ready'); }, 1000); await new Promise(resolve => setTimeout(resolve, duration)); clearInterval(checkInterval); const connection = voiceManager.getConnection(testServerId); expect(connection?.state.status).toBe('ready'); }); }); ``` ### Memory Usage Testing ```typescript describe('Memory Usage', () => { it('should not leak memory on message processing', async () => { const iterations = 1000; const measurements = []; for (let i = 0; i < iterations; i++) { if (i % 100 === 0) { global.gc(); // Force garbage collection const usage = process.memoryUsage(); measurements.push(usage.heapUsed); } await messageManager.handleMessage(createMockMessage()); } // Check for memory growth const firstMeasurement = measurements[0]; const lastMeasurement = measurements[measurements.length - 1]; const growth = lastMeasurement - firstMeasurement; // Allow some growth but not excessive expect(growth).toBeLessThan(50 * 1024 * 1024); // 50MB }); }); ``` ## Mock Utilities ### Discord.js Mocks ```typescript export function createMockMessage(options: Partial = {}): Message { return { id: options.id || 'mock-message-id', content: options.content || 'Mock message', author: options.author || { id: 'mock-user-id', username: 'MockUser', bot: false }, channel: options.channel || createMockTextChannel(), guild: options.guild || createMockGuild(), createdTimestamp: Date.now(), reply: vi.fn(), react: vi.fn(), ...options } as any; } export function createMockTextChannel( options: Partial = {} ): TextChannel { return { id: options.id || 'mock-channel-id', name: options.name || 'mock-channel', type: ChannelType.GuildText, send: vi.fn(), guild: options.guild || createMockGuild(), ...options } as any; } export function createMockInteraction( options: any = {} ): ChatInputCommandInteraction { return { id: 'mock-interaction-id', commandName: options.commandName || 'test', options: { getString: vi.fn((name) => options.options?.[name]), getInteger: vi.fn((name) => options.options?.[name]) }, reply: vi.fn(), deferReply: vi.fn(), editReply: vi.fn(), channel: options.channel || createMockTextChannel(), ...options } as any; } ``` ### Test Helpers ```typescript export async function waitForBotResponse( channel: TextChannel, timeout = 5000 ): Promise { return new Promise((resolve) => { const timer = setTimeout(() => { collector.stop(); resolve(null); }, timeout); const collector = channel.createMessageCollector({ filter: (m) => m.author.bot, max: 1, time: timeout }); collector.on('collect', (message) => { clearTimeout(timer); resolve(message); }); }); } export async function sendTestMessage( content: string, channelId: string ): Promise { const channel = await client.channels.fetch(channelId) as TextChannel; return await channel.send(content); } export async function simulateVoiceActivity( connection: VoiceConnection, audioFile: string, userId: string ): Promise { const resource = createAudioResource(audioFile); const player = createAudioPlayer(); connection.subscribe(player); player.play(resource); // Simulate user speaking connection.receiver.speaking.on('start', userId); await new Promise((resolve) => { player.on(AudioPlayerStatus.Idle, resolve); }); } ``` ## Debug Logging ### Enable Detailed Logging ```typescript // Enable debug logging for tests process.env.DEBUG = 'eliza:discord:*'; // Custom test logger export class TestLogger { private logs: Array<{ level: string; message: string; timestamp: Date }> = []; log(level: string, message: string, ...args: any[]) { this.logs.push({ level, message: `${message} ${args.join(' ')}`, timestamp: new Date() }); if (process.env.VERBOSE_TESTS) { console.log(`[${level}] ${message}`, ...args); } } getLogs(level?: string) { return level ? this.logs.filter(l => l.level === level) : this.logs; } clear() { this.logs = []; } } ``` ## Test Configuration ### vitest.config.ts ```typescript import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { globals: true, environment: 'node', setupFiles: ['./tests/setup.ts'], testTimeout: 30000, hookTimeout: 30000, coverage: { provider: 'v8', reporter: ['text', 'json', 'html'], exclude: [ 'node_modules', 'tests', '**/*.test.ts' ] } } }); ``` ### Test Setup ```typescript // tests/setup.ts import { config } from 'dotenv'; import { vi } from 'vitest'; // Load test environment config({ path: '.env.test' }); // Global test utilities global.createMockRuntime = () => ({ processMessage: vi.fn(), character: { name: 'TestBot' }, logger: { info: vi.fn(), error: vi.fn(), warn: vi.fn(), debug: vi.fn() }, getSetting: vi.fn((key) => process.env[key]), getService: vi.fn() }); // Cleanup after tests afterAll(async () => { // Close all connections await cleanup(); }); ``` ## Continuous Integration ### GitHub Actions Workflow ```yaml name: Discord Plugin Tests on: push: paths: - 'packages/plugin-discord/**' pull_request: paths: - 'packages/plugin-discord/**' jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Setup Node.js uses: actions/setup-node@v3 with: node-version: 20 - name: Install dependencies run: bun install - name: Run unit tests run: bun test packages/plugin-discord --coverage env: DISCORD_API_TOKEN: ${{ secrets.TEST_DISCORD_TOKEN }} DISCORD_APPLICATION_ID: ${{ secrets.TEST_DISCORD_APP_ID }} - name: Upload coverage uses: codecov/codecov-action@v3 with: files: ./coverage/coverage-final.json ``` ## Best Practices 1. **Test Isolation** * Each test should be independent * Clean up resources after tests * Use separate test channels/servers 2. **Mock External Services** * Mock Discord API calls for unit tests * Use real Discord for integration tests only * Mock transcription/vision services 3. **Error Scenarios** * Test network failures * Test permission errors * Test rate limiting 4. **Performance Monitoring** * Track response times * Monitor memory usage * Check for connection stability 5. **Security Testing** * Test token validation * Test permission checks * Test input sanitization # Farcaster Integration Source: https://docs.elizaos.ai/plugin-registry/platform/farcaster Welcome to the comprehensive documentation for the @elizaos/plugin-farcaster package. This index provides organized access to all documentation resources. The @elizaos/plugin-farcaster enables your elizaOS agent to interact with the Farcaster social network through casting, replying, and engaging with the decentralized social protocol. ## 📚 Documentation * **[Developer Guide](./farcaster/developer-guide.mdx)** - Detailed technical reference * **[Cast Flow](./farcaster/cast-flow.mdx)** - Visual guide to cast processing * **[Examples](./farcaster/examples.mdx)** - Practical implementation examples * **[Testing Guide](./farcaster/testing-guide.mdx)** - Testing strategies and patterns ## 🔧 Configuration ### Required Settings * `FARCASTER_NEYNAR_API_KEY` - Neynar API key for authentication * `FARCASTER_SIGNER_UUID` - Neynar signer UUID for your account * `FARCASTER_FID` - Your Farcaster ID (FID) ### Optional Settings * `ENABLE_CAST` - Enable autonomous casting (default: true) * `ENABLE_ACTION_PROCESSING` - Enable processing interactions (default: false) * `FARCASTER_DRY_RUN` - Test mode without posting (default: false) * `CAST_INTERVAL_MIN` - Minimum interval between casts in minutes (default: 90) * `CAST_INTERVAL_MAX` - Maximum interval between casts in minutes (default: 180) * `ACTION_TIMELINE_TYPE` - Type of timeline to use for actions (default: ForYou) # Cast Flow Source: https://docs.elizaos.ai/plugin-registry/platform/farcaster/cast-flow Visual guide to understanding how the Farcaster plugin processes casts and interactions # Farcaster Cast Flow ## Overview This document provides a visual and detailed explanation of how the Farcaster plugin processes casts, from initial receipt through evaluation, response generation, and posting. ## Cast Processing Pipeline ```mermaid graph TD A[Neynar API Polling] --> B{Event Type} B -->|New Cast| C[Cast Processor] B -->|Reply| D[Reply Handler] B -->|Mention| E[Mention Handler] B -->|Timeline Update| F[Timeline Handler] C --> G[Content Analysis] D --> G E --> G G --> H{Should Respond?} H -->|Yes| I[Generate Response] H -->|No| J[Store & Skip] I --> K[Format Cast] K --> L[Neynar API Call] L --> M[Submit Cast] M --> N[Store Result] F --> O[Update Context] J --> O N --> O ``` ## Detailed Flow Stages ### 1. Event Reception The plugin polls the Neynar API for relevant events and interactions: ```typescript // Neynar API polling for mentions and timeline setInterval(async () => { const mentions = await neynarClient.fetchMentions({ fid: agentFid, limit: 10 }); const timeline = await neynarClient.fetchTimeline({ fid: agentFid, type: 'ForYou' }); await processEvents(mentions, timeline); }, FARCASTER_POLL_INTERVAL * 60000); ``` ### 2. Event Classification Events are classified and routed to appropriate handlers: ```mermaid graph LR A[Incoming Event] --> B{Classification} B --> C[Direct Mention] B --> D[Channel Cast] B --> E[Reply Thread] B --> F[Timeline Cast] C --> G[Priority Queue] D --> H[Channel Handler] E --> I[Thread Handler] F --> J[Timeline Handler] ``` ### 3. Content Analysis Each cast undergoes multi-stage analysis: ```mermaid graph TD A[Cast Content] --> B[Tokenization] B --> C[Sentiment Analysis] C --> D[Topic Extraction] D --> E[Context Building] E --> F[Relevance Scoring] F --> G{Score Threshold} G -->|High| H[Immediate Response] G -->|Medium| I[Queue for Response] G -->|Low| J[Monitor Only] ``` ### 4. Response Decision Tree ```mermaid graph TD A[Cast Received] --> B{Is Mention?} B -->|Yes| C[High Priority Response] B -->|No| D{Is Reply to Agent?} D -->|Yes| E[Continue Conversation] D -->|No| F{Contains Keywords?} F -->|Yes| G{Sentiment Check} F -->|No| H[No Response] G -->|Positive| I[Engage Positively] G -->|Negative| J[Careful Response] G -->|Neutral| K{Random Engagement} K -->|Yes| L[Generate Response] K -->|No| H ``` ### 5. Response Generation The response generation process: ```typescript async function generateResponse(context: CastContext): Promise { // 1. Build conversation history const thread = await getThreadContext(context.parentHash); // 2. Extract key topics const topics = extractTopics(context.text); // 3. Generate appropriate response const response = await llm.generate({ system: character.personality, context: thread, topics: topics, maxLength: 320 }); // 4. Validate and format return formatCast(response); } ``` ### 6. Cast Composition ```mermaid graph TD A[Generated Text] --> B{Length Check} B -->|Over 320| C[Truncate/Split] B -->|Under 320| D[Format Check] C --> D D --> E{Has Embeds?} E -->|Yes| F[Validate URLs] E -->|No| G[Add Metadata] F --> G G --> H{Channel Cast?} H -->|Yes| I[Add Channel Tag] H -->|No| J[Standard Cast] I --> K[Final Cast Object] J --> K ``` ### 7. Cast Publishing via Neynar ```mermaid graph LR A[Cast Object] --> B[Format Request] B --> C[Add Signer UUID] C --> D[Neynar API Call] D --> E{Response} E -->|Success| F[Store Cast Hash] E -->|Error| G[Retry Logic] G --> H[Exponential Backoff] H --> D ``` ## Interaction Patterns ### Reply Chains ```mermaid graph TD A[Original Cast] --> B[Agent Reply 1] B --> C[User Reply] C --> D[Agent Reply 2] D --> E[Thread Continuation] style A fill:#f9f,stroke:#333,stroke-width:2px style B fill:#9ff,stroke:#333,stroke-width:2px style C fill:#ff9,stroke:#333,stroke-width:2px style D fill:#9ff,stroke:#333,stroke-width:2px ``` ### Channel Participation ```mermaid graph TD A[Monitor Channel] --> B{New Cast} B --> C[Evaluate Relevance] C --> D{Relevant?} D -->|Yes| E[Analyze Context] D -->|No| F[Skip] E --> G{Can Contribute?} G -->|Yes| H[Post to Channel] G -->|No| I[Like/Recast Only] ``` ## Rate Limiting & Throttling ```mermaid graph TD A[Action Request] --> B{Check Rate Limit} B -->|Under Limit| C[Execute Action] B -->|At Limit| D[Queue Action] D --> E[Wait Period] E --> F[Retry Queue] F --> B C --> G[Update Counter] G --> H[Reset Timer] ``` ## Error Handling Flow ```mermaid graph TD A[Cast Attempt] --> B{Success?} B -->|Yes| C[Complete] B -->|No| D{Error Type} D -->|Network| E[Retry with Backoff] D -->|Validation| F[Fix & Retry] D -->|Rate Limit| G[Queue for Later] D -->|Fatal| H[Log & Abandon] E --> I{Max Retries?} I -->|No| A I -->|Yes| H F --> A G --> J[Delayed Retry] J --> A ``` ## Performance Metrics ### Processing Times ```mermaid graph LR A[Event Receipt] -->|~50ms| B[Classification] B -->|~100ms| C[Analysis] C -->|~200ms| D[Response Gen] D -->|~50ms| E[Formatting] E -->|~100ms| F[Submission] F -->|~50ms| G[Confirmation] ``` ### Throughput Management ```mermaid graph TD A[Incoming Events] --> B[Event Queue] B --> C{Queue Size} C -->|Low| D[Process Immediately] C -->|Medium| E[Batch Process] C -->|High| F[Priority Filter] F --> G[Process High Priority] F --> H[Defer Low Priority] ``` ## State Management ```mermaid graph TD A[Plugin State] --> B[Active Conversations] A --> C[Pending Responses] A --> D[Rate Limit Status] A --> E[Neynar API Status] B --> F[Thread Contexts] C --> G[Response Queue] D --> H[Cooldown Timers] E --> I[API Health Check] ``` ## Monitoring & Observability ```mermaid graph TD A[Cast Activity] --> B[Metrics Collector] B --> C[Response Times] B --> D[Success Rates] B --> E[Engagement Metrics] B --> F[Error Rates] C --> G[Dashboard] D --> G E --> G F --> G G --> H[Alerts] H --> I[Auto-Scaling] H --> J[Manual Intervention] ``` ## Best Practices 1. **Efficient Polling**: Use appropriate intervals to balance responsiveness and API rate limits 2. **Smart Caching**: Cache user profiles and recent casts to reduce Neynar API calls 3. **Graceful Degradation**: Handle API failures without losing queued responses 4. **Context Awareness**: Maintain conversation context across reply chains 5. **Rate Limit Respect**: Implement proper backoff strategies for Neynar API limits ## Debugging Cast Flow Enable detailed logging to trace cast processing: ```typescript // Enable debug mode process.env.FARCASTER_DEBUG = 'true'; // Log each stage runtime.on('farcaster:event', (event) => { console.log(`[${event.stage}]`, event.data); }); ``` ## Summary The Farcaster cast flow is designed to be: * **Responsive**: Quick reaction to mentions and replies * **Intelligent**: Context-aware response generation * **Reliable**: Robust error handling and retry logic * **Scalable**: Efficient queue management and rate limiting * **Observable**: Comprehensive metrics and logging # Developer Guide Source: https://docs.elizaos.ai/plugin-registry/platform/farcaster/developer-guide Comprehensive technical reference for the @elizaos/plugin-farcaster package # Farcaster Plugin Developer Guide ## Overview The @elizaos/plugin-farcaster plugin enables elizaOS agents to interact with the Farcaster protocol through the Neynar API. This plugin provides comprehensive functionality for casting, replying, and engaging with the Farcaster ecosystem. ## Core Features ### 1. Casting Capabilities * **Autonomous Casting**: Post original casts based on agent personality * **Threaded Conversations**: Support for reply chains and threads * **Media Support**: Embed images, links, and frames in casts * **Scheduled Posting**: Time-based cast scheduling ### 2. Engagement Features * **Reply Detection**: Monitor and respond to mentions and replies * **Like/Recast**: Programmatic engagement with other casts * **Follow Management**: Automatic follow/unfollow based on criteria * **Channel Support**: Post to specific channels (e.g., /elizaos) ### 3. Hub Integration * **Hub API**: Direct integration with Farcaster hubs * **Message Validation**: Cryptographic message signing * **Protocol Compliance**: Full Farcaster protocol v2 support ## Installation ```bash # Using bun bun add @elizaos/plugin-farcaster # Using npm npm install @elizaos/plugin-farcaster # Using pnpm pnpm add @elizaos/plugin-farcaster ``` ## Configuration ### Environment Variables ```env # Required FARCASTER_NEYNAR_API_KEY=your-neynar-api-key FARCASTER_SIGNER_UUID=your-signer-uuid FARCASTER_FID=12345 # Feature Toggles ENABLE_CAST=true ENABLE_ACTION_PROCESSING=false FARCASTER_DRY_RUN=false # Timing Configuration (in minutes) CAST_INTERVAL_MIN=90 CAST_INTERVAL_MAX=180 FARCASTER_POLL_INTERVAL=2 ACTION_INTERVAL=5 # Other Options CAST_IMMEDIATELY=false ACTION_TIMELINE_TYPE=ForYou MAX_ACTIONS_PROCESSING=1 MAX_CAST_LENGTH=320 ``` ### Character Configuration ```typescript import { Character } from "@elizaos/core"; import { farcasterPlugin } from "@elizaos/plugin-farcaster"; export const character: Character = { name: "FarcasterAgent", plugins: [farcasterPlugin], settings: { farcaster: { channels: ["/elizaos", "/ai16z"], replyProbability: 0.7, castStyle: "conversational", maxCastLength: 320 } } }; ``` ## Actions ### SEND\_CAST Posts a new cast to Farcaster. ```typescript { name: "SEND_CAST", description: "Posts a cast (message) on Farcaster", examples: [ "Can you post about the new ElizaOS features on Farcaster?", "Share on Farcaster that we just launched version 2.0!" ] } ``` ### REPLY\_TO\_CAST Reply to an existing cast. ```typescript { name: "REPLY_TO_CAST", description: "Replies to a cast on Farcaster", examples: [ "Someone asked about ElizaOS on Farcaster, can you reply?", "Reply to that cast and thank them for the feedback" ] } ``` ## Providers ### farcasterProfile Provides the agent's Farcaster profile information. ```typescript // Provider name: 'farcasterProfile' const profile = await runtime.providers.farcasterProfile.get(runtime, message, state); // Returns profile data including FID, username, bio, etc. ``` ### farcasterTimeline Supplies recent timeline casts for context. ```typescript // Provider name: 'farcasterTimeline' const timeline = await runtime.providers.farcasterTimeline.get(runtime, message, state); // Returns recent casts from the agent's timeline ``` ## Events ### handleCastSent Triggered when a cast is successfully sent. Stores metadata for tracking: ```typescript // Automatically handled when casting // Stores cast hash, thread ID, and message metadata EventType: 'cast:sent' Payload: { castHash: string, threadId: string, messageId: UUID, platform: 'farcaster' } ``` ### handleMessageReceived Processes incoming Farcaster messages and creates memories: ```typescript // Automatically triggered for incoming messages EventType: 'message:received' Payload: { cast: Cast, profile: Profile, threadId: string } ``` ## Managers ### FarcasterAgentManager Orchestrates all Farcaster operations for an agent: ```typescript class FarcasterAgentManager { client: FarcasterClient // Neynar API client casts: FarcasterCastManager // Autonomous posting interactions: FarcasterInteractionManager // Mentions/replies async start() // Start all managers async stop() // Stop all managers } ``` ### FarcasterCastManager Handles autonomous casting based on configuration: ```typescript class FarcasterCastManager { // Manages periodic autonomous posts // Respects CAST_INTERVAL_MIN/MAX settings // Handles CAST_IMMEDIATELY flag async start() // Begin autonomous casting async stop() // Stop casting async publishCast(text: string) // Manually publish } ``` ### FarcasterInteractionManager Processes mentions, replies, and interactions: ```typescript class FarcasterInteractionManager { // Polls for mentions at FARCASTER_POLL_INTERVAL // Processes up to MAX_ACTIONS_PROCESSING per cycle // Uses AI to determine appropriate responses async start() // Start monitoring async stop() // Stop monitoring async processInteractions() // Process pending interactions } ``` ## Services ### FarcasterService Main service coordinating all Farcaster operations: ```typescript class FarcasterService extends Service { static serviceType = 'farcaster' // Service lifecycle async initialize(runtime: IAgentRuntime): Promise static async start(runtime: IAgentRuntime): Promise static async stop(runtime: IAgentRuntime): Promise // Get service instances getMessageService(agentId: UUID): FarcasterMessageService getCastService(agentId: UUID): FarcasterCastService getActiveManagers(): Map // Health check async healthCheck(): Promise } ``` ### MessageService Implements IMessageService for message operations: ```typescript class FarcasterMessageService implements IMessageService { // Message retrieval async getMessages(options: GetMessagesOptions): Promise async getMessage(messageId: string): Promise // Message sending async sendMessage(options: { text: string, type: FarcasterMessageType, replyToId?: string }): Promise } ``` ### CastService Implements IPostService with full CRUD operations: ```typescript class FarcasterCastService implements IPostService { // Cast operations async getCasts(params: { agentId: UUID, limit?: number, cursor?: string }): Promise async createCast(params: { text: string, media?: string[], replyTo?: { hash: string, fid: number } }): Promise async deleteCast(castHash: string): Promise // Engagement operations async likeCast(castHash: string): Promise async unlikeCast(castHash: string): Promise async recast(castHash: string): Promise async unrecast(castHash: string): Promise // Utility methods async publishCast(text: string): Promise async getCastByHash(hash: string): Promise async getProfile(fid: number): Promise } ``` ## Client Architecture ### FarcasterClient Core client wrapping Neynar API operations: ```typescript class FarcasterClient { private neynar: NeynarAPIClient; private signerUuid: string; constructor(params: { neynar: NeynarAPIClient, signerUuid: string }) // Casting operations async publishCast(text: string, options?: { embeds?: Array<{ url: string }>, replyTo?: string, channelId?: string }): Promise async reply(params: { text: string, replyTo: { hash: string, fid: number } }): Promise async deleteCast(targetHash: string): Promise // User operations async getUser(): Promise async getUserByFid(fid: number): Promise async getUserByUsername(username: string): Promise // Timeline operations async getMentions(fid: number, cursor?: string): Promise async getTimeline(type: 'ForYou' | 'Following', cursor?: string): Promise async getCast(hash: string): Promise // Engagement operations async likeCast(targetHash: string): Promise async unlikeCast(targetHash: string): Promise async recast(targetHash: string): Promise async unrecast(targetHash: string): Promise async followUser(targetFid: number): Promise async unfollowUser(targetFid: number): Promise } ``` ### Common Utilities #### AsyncQueue Manages asynchronous operations with concurrency control: ```typescript class AsyncQueue { constructor(concurrency: number) push(fn: () => Promise): Promise } ``` #### Helper Functions ```typescript // Cast utilities castUuid(cast: Cast): UUID // Generate unique ID for cast neynarCastToCast(cast: NeynarCast): Cast // Convert Neynar format formatCastTimestamp(timestamp: number): string // Format timestamps // Prompt formatting formatCast(cast: Cast): string // Format cast for AI processing formatTimeline(casts: Cast[]): string // Format timeline for context // Cache management lastCastCacheKey(agentId: UUID): string // Generate cache keys ``` ## Event System ### Cast Events ```typescript runtime.on("cast:new", (cast: Cast) => { // Handle new cast }); runtime.on("cast:reply", (reply: CastReply) => { // Handle reply }); runtime.on("cast:like", (like: CastLike) => { // Handle like }); ``` ### Error Events ```typescript runtime.on("farcaster:error", (error: FarcasterError) => { // Handle error }); ``` ## Memory & Storage ### Memory System The plugin uses elizaOS's memory system for persistence rather than direct database tables: ```typescript // Cast metadata stored when sending await runtime.createMemory({ type: 'metadata', content: { castHash: string, threadId: string, platform: 'farcaster', messageId: UUID, sentAt: number } }); // Message memory for each cast await runtime.createMemory({ type: 'message', content: { text: string, source: 'farcaster', hash: string, fid: number, timestamp: number, inReplyTo?: string } }); ``` ### Caching Strategy LRU cache for performance optimization: * **Cast Cache**: TTL 30 minutes, 9000 entries max * **Profile Cache**: User profile data * **Timeline Cache**: Recent timeline casts * **Last Cast Tracking**: Per-agent last cast timestamps ## Security Considerations ### Key Management * Store API keys and signer UUIDs securely using environment variables * Never commit credentials to version control * Use separate Neynar API keys for development and production * Create separate signers for different environments ### Rate Limiting * Implement exponential backoff for API requests * Respect hub rate limits (typically 100 req/min) * Cache frequently accessed data ### Content Validation * Validate cast length (max 320 characters) * Sanitize user inputs * Verify message signatures ## Performance Optimization ### AsyncQueue Implementation The plugin uses an async queue to prevent rate limiting: ```typescript // Queue processes operations with concurrency control const asyncQueue = new AsyncQueue(1); // Single concurrency await asyncQueue.push(() => processInteraction(cast)); ``` ### Polling Optimization ```typescript // Configurable polling intervals to balance responsiveness FARCASTER_POLL_INTERVAL=2 // Minutes between polls ACTION_INTERVAL=5 // Minutes between action processing MAX_ACTIONS_PROCESSING=1 // Actions per cycle ``` ## Troubleshooting ### Common Issues 1. **Authentication Errors** * Verify mnemonic is correct * Ensure FID matches the mnemonic * Check hub connectivity 2. **Rate Limiting** * Implement retry logic with backoff * Use caching to reduce API calls * Monitor rate limit headers 3. **Message Validation Failures** * Verify timestamp is within valid range * Ensure proper message formatting * Check signature validity ### Debug Mode Enable debug logging: ```env FARCASTER_DEBUG=true LOG_LEVEL=debug ``` ## Best Practices 1. **Content Strategy** * Keep casts concise and engaging * Use channels appropriately * Maintain consistent voice 2. **Engagement Guidelines** * Don't spam or over-engage * Respect community norms * Build genuine connections 3. **Technical Implementation** * Handle errors gracefully * Implement proper retry logic * Monitor performance metrics ## Migration Guide ### From v1 to v2 ```typescript // v1 import { FarcasterPlugin } from "@elizaos/plugin-farcaster"; // v2 import { farcasterPlugin } from "@elizaos/plugin-farcaster"; // Configuration changes // v1: Plugin initialized with options const plugin = new FarcasterPlugin(options); // v2: Configuration via environment and character const character = { plugins: [farcasterPlugin], settings: { farcaster: options } }; ``` ## Support * **GitHub**: [elizaos-plugins/plugin-farcaster](https://github.com/elizaos-plugins/plugin-farcaster) * **Discord**: Join the elizaOS community * **Documentation**: [elizaos.ai/docs](https://elizaos.ai/docs) ## License MIT License - see LICENSE file for details # Examples Source: https://docs.elizaos.ai/plugin-registry/platform/farcaster/examples Practical implementation examples for the @elizaos/plugin-farcaster package # Farcaster Plugin Examples ## Basic Setup ### Minimal Configuration ```typescript // character.ts import { Character } from "@elizaos/core"; import { farcasterPlugin } from "@elizaos/plugin-farcaster"; export const character: Character = { name: "MyFarcasterAgent", plugins: [farcasterPlugin], bio: "An AI agent exploring the Farcaster ecosystem", description: "I engage thoughtfully with the Farcaster community" }; ``` ### Environment Configuration ```env # .env file FARCASTER_NEYNAR_API_KEY=your-neynar-api-key FARCASTER_SIGNER_UUID=your-signer-uuid FARCASTER_FID=12345 ENABLE_CAST=true ENABLE_ACTION_PROCESSING=false FARCASTER_DRY_RUN=false ``` ## Casting Examples ### Simple Cast ```typescript // Post a simple cast import { runtime } from "@elizaos/core"; async function postSimpleCast() { const action = runtime.getAction("SEND_CAST"); await action.handler(runtime, { text: "Hello Farcaster! Excited to be here 🎉" }); } ``` ### Cast with Channel ```typescript // Post to a specific channel async function postToChannel() { const action = runtime.getAction("SEND_CAST"); await action.handler(runtime, { text: "Building with elizaOS is amazing!", channel: "/elizaos" }); } ``` ### Cast with Embeds ```typescript // Post with embedded content async function postWithEmbed() { const action = runtime.getAction("SEND_CAST"); await action.handler(runtime, { text: "Check out this awesome project!", embeds: [ { url: "https://github.com/elizaos/elizaos" } ] }); } ``` ### Thread Creation ```typescript // Create a thread of casts async function createThread() { const action = runtime.getAction("SEND_CAST"); // First cast const firstCast = await action.handler(runtime, { text: "Let me explain how elizaOS agents work 🧵" }); // Reply to create thread const replyAction = runtime.getAction("REPLY_TO_CAST"); await replyAction.handler(runtime, { text: "1/ Agents are autonomous entities that can interact across platforms", targetCastHash: firstCast.hash, targetFid: firstCast.fid }); await replyAction.handler(runtime, { text: "2/ They use LLMs for natural language understanding and generation", targetCastHash: firstCast.hash, targetFid: firstCast.fid }); } ``` ## Reply Examples ### Simple Reply ```typescript // Reply to a cast async function replyToCast(castHash: string, authorFid: number) { const action = runtime.getAction("REPLY_TO_CAST"); await action.handler(runtime, { text: "Great point! I completely agree with this perspective.", targetCastHash: castHash, targetFid: authorFid }); } ``` ### Contextual Reply ```typescript // Reply with context awareness async function contextualReply(cast: Cast) { const context = await buildContext(cast); const response = await generateResponse(context); const action = runtime.getAction("REPLY_TO_CAST"); await action.handler(runtime, { text: response, targetCastHash: cast.hash, targetFid: cast.author.fid }); } async function buildContext(cast: Cast) { // Get thread history const thread = await getThreadHistory(cast); // Get author profile const author = await getProfile(cast.author.fid); return { originalCast: cast, thread: thread, author: author, topics: extractTopics(cast.text) }; } ``` ## Engagement Examples ### Engagement Note ```typescript // Note: Like, recast, and follow functionality are managed internally // by the FarcasterService and MessageService based on agent behavior // and are not exposed as direct actions at this time. ``` ## Service Integration Examples ### Custom Service Implementation ```typescript import { Service, IAgentRuntime } from "@elizaos/core"; import { NeynarAPIClient } from "@neynar/nodejs-sdk"; class CustomFarcasterService implements Service { private client: NeynarAPIClient; private runtime: IAgentRuntime; async start(runtime: IAgentRuntime): Promise { this.runtime = runtime; this.client = new NeynarAPIClient({ apiKey: process.env.FARCASTER_NEYNAR_API_KEY! }); // Start monitoring this.startMonitoring(); } private async startMonitoring() { // Monitor mentions setInterval(async () => { const mentions = await this.client.getMentions(); for (const mention of mentions) { await this.handleMention(mention); } }, 30000); // Check every 30 seconds } private async handleMention(mention: Cast) { // Generate response const response = await this.generateResponse(mention); // Reply await this.client.reply(mention.hash, mention.author.fid, response); } async stop(): Promise { // Cleanup await this.client.disconnect(); } } ``` ### Event-Driven Responses ```typescript // Set up event listeners for Farcaster events runtime.on("farcaster:mention", async (event) => { const { cast, author } = event; // Check if we should respond if (shouldRespond(cast)) { const response = await generateResponse(cast); await replyToCast(cast.hash, author.fid, response); } }); runtime.on("farcaster:followed", async (event) => { const { follower } = event; // Auto-follow back await followUser(follower.fid); // Send welcome message await postCast(`Welcome @${follower.username}! Looking forward to our interactions.`); }); ``` ## Advanced Patterns ### Scheduled Casting ```typescript // Schedule regular casts class ScheduledCaster { private runtime: IAgentRuntime; constructor(runtime: IAgentRuntime) { this.runtime = runtime; } start() { // Morning update this.scheduleDaily("09:00", async () => { await this.postMorningUpdate(); }); // Evening reflection this.scheduleDaily("21:00", async () => { await this.postEveningReflection(); }); } private async postMorningUpdate() { const insights = await this.generateDailyInsights(); await postCast({ text: `Good morning! Today's insight: ${insights}`, channel: "/elizaos" }); } private async postEveningReflection() { const reflection = await this.generateReflection(); await postCast({ text: `Evening thoughts: ${reflection}`, channel: "/elizaos" }); } } ``` ### Channel-Specific Behavior ```typescript // Different behavior for different channels class ChannelManager { private channelConfigs = { "/elizaos": { style: "technical", replyProbability: 0.8, topics: ["AI", "agents", "development"] }, "/ai16z": { style: "philosophical", replyProbability: 0.6, topics: ["AI", "future", "technology"] }, "/base": { style: "friendly", replyProbability: 0.5, topics: ["community", "building", "web3"] } }; async handleChannelCast(cast: Cast, channel: string) { const config = this.channelConfigs[channel]; if (!config) return; // Check if topic matches const relevantTopic = config.topics.some(topic => cast.text.toLowerCase().includes(topic) ); if (relevantTopic && Math.random() < config.replyProbability) { const response = await this.generateResponse(cast, config.style); await this.reply(cast, response); } } } ``` ### Conversation Memory ```typescript // Track conversation history class ConversationTracker { private conversations = new Map(); async handleCast(cast: Cast) { const threadId = cast.threadHash || cast.hash; // Get or create conversation let conversation = this.conversations.get(threadId); if (!conversation) { conversation = { id: threadId, participants: new Set([cast.author.fid]), messages: [], startTime: Date.now() }; this.conversations.set(threadId, conversation); } // Add message to conversation conversation.messages.push({ author: cast.author.fid, text: cast.text, timestamp: cast.timestamp }); // Generate contextual response const response = await this.generateContextualResponse(conversation); if (response) { await this.reply(cast, response); } } } ``` ### Multi-Platform Coordination ```typescript // Coordinate between Farcaster and other platforms class MultiPlatformAgent { async crossPost(content: string) { // Post to Farcaster await this.postToFarcaster(content); // Post to Twitter if (this.runtime.hasPlugin("twitter")) { await this.postToTwitter(content); } // Post to Discord if (this.runtime.hasPlugin("discord")) { await this.postToDiscord(content); } } async syncEngagement() { // Get Farcaster engagement const farcasterLikes = await this.getFarcasterLikes(); // Mirror high-engagement content to other platforms for (const cast of farcasterLikes) { if (cast.reactions.count > 10) { await this.crossPost(cast.text); } } } } ``` ## Error Handling Examples ### Robust Cast Posting ```typescript async function robustCastPost(text: string, maxRetries = 3) { let attempt = 0; let lastError; while (attempt < maxRetries) { try { const result = await postCast({ text }); return result; } catch (error) { lastError = error; attempt++; if (error.code === 'RATE_LIMIT') { // Wait with exponential backoff await wait(Math.pow(2, attempt) * 1000); } else if (error.code === 'NETWORK_ERROR') { // Retry immediately for network errors continue; } else { // Unknown error, throw immediately throw error; } } } throw new Error(`Failed after ${maxRetries} attempts: ${lastError}`); } ``` ### Validation and Sanitization ```typescript function validateCast(text: string): boolean { // Check length if (text.length > 320) { throw new Error("Cast exceeds maximum length of 320 characters"); } // Check for required content if (text.trim().length === 0) { throw new Error("Cast cannot be empty"); } // Check for spam patterns if (isSpam(text)) { throw new Error("Cast appears to be spam"); } return true; } function sanitizeCast(text: string): string { // Remove excessive whitespace text = text.replace(/\s+/g, ' ').trim(); // Remove invalid characters text = text.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, ''); // Truncate if needed if (text.length > 320) { text = text.substring(0, 317) + "..."; } return text; } ``` ## Testing Examples ```typescript // Mock testing setup import { describe, it, expect, beforeEach } from "bun:test"; import { MockFarcasterClient } from "@elizaos/plugin-farcaster/test"; describe("Farcaster Plugin", () => { let client: MockFarcasterClient; beforeEach(() => { client = new MockFarcasterClient(); }); it("should post a cast", async () => { const result = await client.postCast("Test cast"); expect(result.hash).toBeDefined(); expect(result.text).toBe("Test cast"); }); it("should handle replies", async () => { const original = await client.postCast("Original"); const reply = await client.reply( original.hash, original.fid, "Reply text" ); expect(reply.parentHash).toBe(original.hash); }); }); ``` ## Summary These examples demonstrate the flexibility and power of the Farcaster plugin. Key patterns include: * Simple and complex casting scenarios using SEND\_CAST * Intelligent reply systems using REPLY\_TO\_CAST * Channel-specific behaviors * Cross-platform coordination * Robust error handling * Testing strategies The plugin uses the Neynar API for all Farcaster interactions, requiring proper API key and signer configuration. For more advanced use cases, combine these patterns with the elizaOS agent framework's other capabilities. # Testing Guide Source: https://docs.elizaos.ai/plugin-registry/platform/farcaster/testing-guide Comprehensive testing strategies and patterns for the @elizaos/plugin-farcaster package # Farcaster Plugin Testing Guide ## Overview This guide provides comprehensive testing strategies for the Farcaster plugin, covering unit tests, integration tests, and end-to-end testing scenarios. ## Test Environment Setup ### Configuration ```typescript // test/setup.ts import { beforeAll, afterAll } from "bun:test"; import { TestEnvironment } from "@elizaos/test-utils"; let testEnv: TestEnvironment; beforeAll(async () => { testEnv = new TestEnvironment({ plugins: ["@elizaos/plugin-farcaster"], mockServices: true }); // Set test environment variables process.env.FARCASTER_DRY_RUN = "true"; process.env.FARCASTER_HUB_URL = "http://localhost:8080"; process.env.NODE_ENV = "test"; await testEnv.start(); }); afterAll(async () => { await testEnv.cleanup(); }); ``` ### Mock Hub Server ```typescript // test/mocks/hub-server.ts import { createServer } from "http"; export class MockHubServer { private server: any; private responses: Map = new Map(); async start(port = 8080) { this.server = createServer((req, res) => { const response = this.responses.get(req.url!) || { error: "Not found" }; res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify(response)); }); await new Promise(resolve => { this.server.listen(port, resolve); }); } setResponse(path: string, response: any) { this.responses.set(path, response); } async stop() { await new Promise(resolve => this.server.close(resolve)); } } ``` ## Unit Tests ### Action Tests ```typescript // test/actions/post-cast.test.ts import { describe, it, expect, beforeEach } from "bun:test"; import { postCastAction } from "@elizaos/plugin-farcaster"; import { createMockRuntime } from "@elizaos/test-utils"; describe("POST_CAST Action", () => { let runtime: any; beforeEach(() => { runtime = createMockRuntime(); }); it("should validate cast text length", async () => { const longText = "a".repeat(321); await expect( postCastAction.handler(runtime, { text: longText }) ).rejects.toThrow("Cast exceeds maximum length"); }); it("should post a simple cast", async () => { const result = await postCastAction.handler(runtime, { text: "Test cast" }); expect(result.success).toBe(true); expect(result.cast).toBeDefined(); expect(result.cast.text).toBe("Test cast"); }); it("should handle channel posts", async () => { const result = await postCastAction.handler(runtime, { text: "Channel test", channel: "/elizaos" }); expect(result.cast.channel).toBe("/elizaos"); }); it("should support embeds", async () => { const result = await postCastAction.handler(runtime, { text: "Cast with embed", embeds: [{ url: "https://example.com" }] }); expect(result.cast.embeds).toHaveLength(1); expect(result.cast.embeds[0].url).toBe("https://example.com"); }); }); ``` ### Provider Tests ```typescript // test/providers/cast-provider.test.ts import { describe, it, expect } from "bun:test"; import { castProvider } from "@elizaos/plugin-farcaster"; import { createMockRuntime } from "@elizaos/test-utils"; describe("Cast Provider", () => { it("should fetch recent casts", async () => { const runtime = createMockRuntime(); const casts = await castProvider.getCasts(runtime, { limit: 10 }); expect(Array.isArray(casts)).toBe(true); expect(casts.length).toBeLessThanOrEqual(10); }); it("should filter by channel", async () => { const runtime = createMockRuntime(); const casts = await castProvider.getCasts(runtime, { channel: "/elizaos", limit: 5 }); casts.forEach(cast => { expect(cast.channel).toBe("/elizaos"); }); }); it("should include replies when requested", async () => { const runtime = createMockRuntime(); const casts = await castProvider.getCasts(runtime, { includeReplies: true }); const replies = casts.filter(c => c.parentHash); expect(replies.length).toBeGreaterThan(0); }); }); ``` ### Evaluator Tests ```typescript // test/evaluators/engagement.test.ts import { describe, it, expect } from "bun:test"; import { engagementEvaluator } from "@elizaos/plugin-farcaster"; describe("Engagement Evaluator", () => { it("should evaluate high-quality casts positively", async () => { const cast = { text: "Just deployed a new feature for elizaOS agents!", author: { fid: 123, username: "dev" }, reactions: { count: 15 }, recasts: { count: 5 } }; const score = await engagementEvaluator.evaluate(cast); expect(score).toBeGreaterThan(0.7); }); it("should evaluate spam negatively", async () => { const cast = { text: "Buy now! Click here! Limited offer!", author: { fid: 456, username: "spammer" }, reactions: { count: 0 }, recasts: { count: 0 } }; const score = await engagementEvaluator.evaluate(cast); expect(score).toBeLessThan(0.3); }); it("should consider author reputation", async () => { const cast = { text: "Interesting thought", author: { fid: 789, username: "trusted", followerCount: 1000 } }; const score = await engagementEvaluator.evaluate(cast); expect(score).toBeGreaterThan(0.5); }); }); ``` ## Integration Tests ### Service Integration ```typescript // test/integration/service.test.ts import { describe, it, expect, beforeAll, afterAll } from "bun:test"; import { FarcasterService } from "@elizaos/plugin-farcaster"; import { createTestRuntime } from "@elizaos/test-utils"; import { MockHubServer } from "../mocks/hub-server"; describe("Farcaster Service Integration", () => { let service: FarcasterService; let runtime: any; let hubServer: MockHubServer; beforeAll(async () => { hubServer = new MockHubServer(); await hubServer.start(); runtime = await createTestRuntime(); service = new FarcasterService(); await service.start(runtime); }); afterAll(async () => { await service.stop(); await hubServer.stop(); }); it("should connect to hub", async () => { expect(service.isConnected()).toBe(true); }); it("should post and retrieve casts", async () => { const cast = await service.postCast("Integration test"); expect(cast.hash).toBeDefined(); const retrieved = await service.getCast(cast.hash); expect(retrieved.text).toBe("Integration test"); }); it("should handle reply chains", async () => { const original = await service.postCast("Original cast"); const reply = await service.replyCast( "Reply to original", original.hash, original.fid ); expect(reply.parentHash).toBe(original.hash); const thread = await service.getThread(original.hash); expect(thread).toHaveLength(2); }); }); ``` ### Event System Tests ```typescript // test/integration/events.test.ts import { describe, it, expect } from "bun:test"; import { createTestRuntime } from "@elizaos/test-utils"; import { farcasterPlugin } from "@elizaos/plugin-farcaster"; describe("Farcaster Event System", () => { it("should emit cast events", async () => { const runtime = await createTestRuntime({ plugins: [farcasterPlugin] }); let eventFired = false; runtime.on("farcaster:cast:new", () => { eventFired = true; }); await runtime.action("POST_CAST", { text: "Event test" }); await new Promise(resolve => setTimeout(resolve, 100)); expect(eventFired).toBe(true); }); it("should handle mention events", async () => { const runtime = await createTestRuntime({ plugins: [farcasterPlugin] }); const mentions: any[] = []; runtime.on("farcaster:mention", (event) => { mentions.push(event); }); // Simulate incoming mention await runtime.simulateEvent("farcaster:mention", { cast: { text: "@agent hello!", author: { fid: 123 } } }); expect(mentions).toHaveLength(1); expect(mentions[0].cast.text).toContain("@agent"); }); }); ``` ## End-to-End Tests ### Full Flow Test ```typescript // test/e2e/full-flow.test.ts import { describe, it, expect } from "bun:test"; import { createAgent } from "@elizaos/core"; import { farcasterPlugin } from "@elizaos/plugin-farcaster"; describe("E2E: Farcaster Agent Flow", () => { it("should perform complete interaction flow", async () => { // Create agent with Farcaster plugin const agent = await createAgent({ name: "TestAgent", plugins: [farcasterPlugin], env: { FARCASTER_MNEMONIC: "test mnemonic ...", FARCASTER_FID: "99999", FARCASTER_DRY_RUN: "true" } }); // Start agent await agent.start(); // Post initial cast const cast = await agent.execute("POST_CAST", { text: "Hello from test agent!" }); expect(cast.success).toBe(true); // Simulate incoming reply await agent.handleEvent({ type: "farcaster:reply", data: { cast: { text: "Welcome to Farcaster!", parentHash: cast.hash, author: { fid: 123 } } } }); // Check if agent responded const responses = await agent.getResponses(); expect(responses).toHaveLength(1); expect(responses[0].type).toBe("REPLY_CAST"); // Stop agent await agent.stop(); }); }); ``` ### Load Testing ```typescript // test/load/cast-load.test.ts import { describe, it, expect } from "bun:test"; import { FarcasterService } from "@elizaos/plugin-farcaster"; import { createTestRuntime } from "@elizaos/test-utils"; describe("Load Testing", () => { it("should handle rapid casting via actions", async () => { const runtime = await createTestRuntime(); const service = new FarcasterService(); await service.start(runtime); const castService = service.getCastService(runtime.agentId); const promises = []; // Send 50 casts rapidly for (let i = 0; i < 50; i++) { promises.push( castService.publishCast(`Load test cast ${i}`) .catch(err => ({ error: err })) ); } const results = await Promise.all(promises); // Check success rate const successful = results.filter(r => !r.error); const successRate = successful.length / results.length; expect(successRate).toBeGreaterThan(0.8); // 80% success rate }); it("should handle concurrent message operations", async () => { const runtime = await createTestRuntime(); const service = new FarcasterService(); await service.start(runtime); const messageService = service.getMessageService(runtime.agentId); // Perform multiple operations concurrently const operations = await Promise.all([ messageService.sendMessage({ text: "Concurrent 1" }), messageService.sendMessage({ text: "Concurrent 2" }), messageService.sendMessage({ text: "Concurrent 3" }) ]); expect(operations).toHaveLength(3); operations.forEach(op => { expect(op.error).toBeUndefined(); }); }); }); ``` ## Mock Data Generators ```typescript // test/utils/generators.ts export function generateMockCast(overrides = {}) { return { hash: `0x${Math.random().toString(16).slice(2)}`, fid: Math.floor(Math.random() * 10000), text: "Mock cast text", timestamp: Date.now(), author: { fid: Math.floor(Math.random() * 10000), username: `user${Math.floor(Math.random() * 1000)}`, displayName: "Mock User", pfp: "https://example.com/pfp.jpg" }, reactions: { count: Math.floor(Math.random() * 100) }, recasts: { count: Math.floor(Math.random() * 20) }, replies: { count: Math.floor(Math.random() * 50) }, ...overrides }; } export function generateMockThread(depth = 3) { const thread = []; let parentHash = null; for (let i = 0; i < depth; i++) { const cast = generateMockCast({ text: `Thread message ${i + 1}`, parentHash: parentHash }); thread.push(cast); parentHash = cast.hash; } return thread; } ``` ## Test Coverage ### Coverage Configuration ```json // package.json { "scripts": { "test": "bun test", "test:coverage": "bun test --coverage", "test:watch": "bun test --watch" } } ``` ### Coverage Report Example ```bash # Run tests with coverage bun test --coverage # Output -------------------|---------|----------|---------|---------| File | % Stmts | % Branch | % Funcs | % Lines | -------------------|---------|----------|---------|---------| All files | 89.5 | 82.3 | 91.2 | 88.7 | actions/ | 92.1 | 85.6 | 94.3 | 91.8 | sendCast.ts | 93.5 | 87.2 | 95.0 | 93.1 | replyCast.ts | 91.2 | 84.5 | 93.8 | 90.9 | providers/ | 87.3 | 79.8 | 88.5 | 86.4 | profileProvider | 88.1 | 81.2 | 89.3 | 87.5 | timelineProvider | 87.0 | 80.1 | 88.0 | 86.2 | services/ | 88.9 | 81.4 | 90.7 | 87.9 | MessageService | 89.2 | 82.1 | 91.0 | 88.3 | CastService | 88.5 | 80.7 | 90.4 | 87.5 | -------------------|---------|----------|---------|---------| ``` ## Debugging Tests ### Debug Configuration ```typescript // test/debug.ts export function enableDebugMode() { process.env.DEBUG = "farcaster:*"; process.env.LOG_LEVEL = "debug"; process.env.FARCASTER_DEBUG = "true"; } export function logTestContext(test: string, data: any) { console.log(`[TEST: ${test}]`, JSON.stringify(data, null, 2)); } ``` ### Visual Test Output ```typescript // test/utils/visual.ts export function visualizeCastThread(thread: Cast[]) { console.log("\n📝 Cast Thread Visualization:"); thread.forEach((cast, index) => { const indent = " ".repeat(index); console.log(`${indent}└─ ${cast.author.username}: ${cast.text}`); }); console.log("\n"); } export function visualizeEngagement(cast: Cast) { console.log("\n📊 Engagement Metrics:"); console.log(` ❤️ Likes: ${cast.reactions.count}`); console.log(` 🔄 Recasts: ${cast.recasts.count}`); console.log(` 💬 Replies: ${cast.replies.count}`); console.log("\n"); } ``` ## CI/CD Integration ### GitHub Actions ```yaml # .github/workflows/test-farcaster.yml name: Farcaster Plugin Tests on: push: paths: - 'packages/plugin-farcaster/**' pull_request: paths: - 'packages/plugin-farcaster/**' jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: oven-sh/setup-bun@v1 with: bun-version: latest - name: Install dependencies run: bun install - name: Run tests run: bun test packages/plugin-farcaster env: FARCASTER_DRY_RUN: true - name: Generate coverage run: bun test --coverage packages/plugin-farcaster - name: Upload coverage uses: codecov/codecov-action@v3 ``` ## Best Practices 1. **Test Isolation**: Each test should be independent 2. **Mock External Services**: Never hit real Farcaster APIs in tests 3. **Use Test Fixtures**: Maintain consistent test data 4. **Test Edge Cases**: Include error scenarios and boundary conditions 5. **Performance Testing**: Include load and stress tests 6. **Documentation**: Keep tests as living documentation ## Summary This testing guide provides comprehensive strategies for testing the Farcaster plugin: * Unit tests for individual components * Integration tests for service interactions * End-to-end tests for complete flows * Load testing for performance validation * Mock utilities for consistent testing * CI/CD integration for automated testing Following these patterns ensures robust and reliable Farcaster integration. # Telegram Integration Source: https://docs.elizaos.ai/plugin-registry/platform/telegram Welcome to the comprehensive documentation for the @elizaos/plugin-telegram package. This index provides organized access to all documentation resources. The @elizaos/plugin-telegram enables your elizaOS agent to operate as a Telegram bot with support for messages, media, interactive buttons, and group management. ## 📚 Documentation * **[Complete Documentation](./complete-documentation.mdx)** - Detailed technical reference * **[Message Flow](./message-flow.mdx)** - Visual guide to Telegram message processing * **[Examples](./examples.mdx)** - Practical implementation examples * **[Testing Guide](./testing-guide.mdx)** - Testing strategies and patterns ## 🔧 Configuration ### Required Settings * `TELEGRAM_BOT_TOKEN` - Your bot token from BotFather ### Optional Settings * `TELEGRAM_API_ROOT` - Custom API endpoint * `TELEGRAM_ALLOWED_CHATS` - Restrict to specific chats # Developer Guide Source: https://docs.elizaos.ai/plugin-registry/platform/telegram/complete-documentation Comprehensive Telegram Bot API integration for elizaOS agents. It enables agents to operate as Telegram bots with advanced features and capabilities. ## Overview The `@elizaos/plugin-telegram` package provides comprehensive Telegram Bot API integration for elizaOS agents. It enables agents to operate as Telegram bots with support for private chats, groups, channels, media processing, interactive buttons, and forum topics. This plugin handles all Telegram-specific functionality including: * Initializing and managing the Telegram bot connection via Telegraf * Processing messages across different chat types * Handling media attachments and documents * Managing interactive UI elements (buttons, keyboards) * Supporting forum topics as separate conversation contexts * Implementing access control and chat restrictions ## Architecture Overview ```mermaid graph TD A[Telegram API] --> B[Telegraf Client] B --> C[Telegram Service] C --> D[Message Manager] C --> E[Event Handlers] D --> F[Media Processing] D --> G[Bootstrap Plugin] E --> H[Message Events] E --> I[Callback Events] E --> J[Edited Messages] K[Utils] --> D K --> F ``` ## Core Components ### Telegram Service The `TelegramService` class is the main entry point for Telegram functionality: ```typescript export class TelegramService extends Service { static serviceType = TELEGRAM_SERVICE_NAME; private bot: Telegraf | null; public messageManager: MessageManager | null; private knownChats: Map = new Map(); private syncedEntityIds: Set = new Set(); constructor(runtime: IAgentRuntime) { super(runtime); // Initialize bot with token // Set up middleware // Configure event handlers } } ``` #### Key Responsibilities: 1. **Bot Initialization** * Creates Telegraf instance with bot token * Configures API root if custom endpoint provided * Handles connection lifecycle 2. **Middleware Setup** * Preprocesses incoming updates * Manages chat synchronization * Handles user entity creation 3. **Event Registration** * Message handlers * Callback query handlers * Edited message handlers 4. **Chat Management** * Tracks known chats * Syncs chat metadata * Manages access control ### Message Manager The `MessageManager` class handles all message-related operations: ```typescript export class MessageManager { private bot: Telegraf; private runtime: IAgentRuntime; private messageHistory: Map>; private messageCallbacks: Map void>; async handleMessage(ctx: Context): Promise { // Convert Telegram message to elizaOS format // Process media if present // Send to bootstrap plugin // Handle response } async sendMessageToTelegram( chatId: number | string, content: Content, replyToMessageId?: number ): Promise { // Format content for Telegram // Handle buttons/keyboards // Send via bot API } } ``` #### Message Processing Flow: 1. **Message Reception** ```typescript // Telegram message received const message = ctx.message; if (!this.shouldProcessMessage(ctx)) return; ``` 2. **Format Conversion** ```typescript const elizaMessage: ElizaMessage = { content: { text: message.text || message.caption || '', attachments: await this.processAttachments(message) }, userId: createUniqueUuid(ctx.from.id.toString()), channelId: ctx.chat.id.toString(), roomId: this.getRoomId(ctx) }; ``` 3. **Media Processing** ```typescript if (message.photo || message.document || message.voice) { elizaMessage.content.attachments = await processMediaAttachments( ctx, this.bot, this.runtime ); } ``` 4. **Response Handling** ```typescript const callback = async (response: Content) => { await this.sendMessageToTelegram( ctx.chat.id, response, message.message_id ); }; ``` ### Utilities Various utility functions support the core functionality: ```typescript // Media processing export async function processMediaAttachments( ctx: Context, bot: Telegraf, runtime: IAgentRuntime ): Promise { const attachments: Attachment[] = []; if (ctx.message?.photo) { // Process photo const photo = ctx.message.photo[ctx.message.photo.length - 1]; const file = await bot.telegram.getFile(photo.file_id); // Download and process... } if (ctx.message?.voice) { // Process voice message const voice = ctx.message.voice; const file = await bot.telegram.getFile(voice.file_id); // Transcribe audio... } return attachments; } // Button creation export function createInlineKeyboard(buttons: Button[]): InlineKeyboardMarkup { const keyboard = buttons.map(button => [{ text: button.text, ...(button.url ? { url: button.url } : { callback_data: button.callback_data }) }]); return { inline_keyboard: keyboard }; } ``` ## Event Processing Flow ### Message Flow ```mermaid sequenceDiagram participant U as User participant T as Telegram participant B as Bot (Telegraf) participant S as TelegramService participant M as MessageManager participant E as elizaOS U->>T: Send message T->>B: Update received B->>S: Middleware processing S->>S: Sync chat/user S->>M: handleMessage() M->>M: Convert format M->>M: Process media M->>E: Send to bootstrap E->>M: Response callback M->>B: Send response B->>T: API call T->>U: Display message ``` ### Callback Query Flow ```mermaid sequenceDiagram participant U as User participant T as Telegram participant B as Bot participant M as MessageManager U->>T: Click button T->>B: callback_query B->>M: handleCallbackQuery() M->>M: Process action M->>B: Answer callback B->>T: Answer query M->>B: Update message B->>T: Edit message ``` ## Configuration ### Environment Variables ```bash # Required TELEGRAM_BOT_TOKEN=123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11 # Optional TELEGRAM_API_ROOT=https://api.telegram.org # Custom API endpoint TELEGRAM_ALLOWED_CHATS=["123456789", "-987654321"] # JSON array of chat IDs # Testing TELEGRAM_TEST_CHAT_ID=-1001234567890 # Test chat for integration tests ``` ### Character Configuration ```typescript const character = { name: "TelegramBot", clients: ["telegram"], settings: { // Bot behavior allowDirectMessages: true, shouldOnlyJoinInAllowedGroups: false, allowedGroupIds: ["-123456789", "-987654321"], messageTrackingLimit: 100, // Templates templates: { telegramMessageHandlerTemplate: "Custom message template", telegramShouldRespondTemplate: "Custom decision template" } } }; ``` ### Bot Creation 1. **Create Bot with BotFather** ``` 1. Open @BotFather in Telegram 2. Send /newbot 3. Choose a name for your bot 4. Choose a username (must end in 'bot') 5. Save the token provided ``` 2. **Configure Bot Settings** ``` /setprivacy - Disable for group message access /setcommands - Set bot commands /setdescription - Add bot description /setabouttext - Set about text ``` ## Message Handling ### Message Types The plugin handles various Telegram message types: ```typescript // Text messages if (ctx.message?.text) { content.text = ctx.message.text; } // Media messages if (ctx.message?.photo) { // Process photo with caption content.text = ctx.message.caption || ''; content.attachments = await processPhoto(ctx.message.photo); } // Voice messages if (ctx.message?.voice) { // Transcribe voice to text const transcript = await transcribeVoice(ctx.message.voice); content.text = transcript; } // Documents if (ctx.message?.document) { // Process document content.attachments = await processDocument(ctx.message.document); } ``` ### Message Context Each message maintains context about its origin: ```typescript interface TelegramMessageContext { chatId: string; chatType: 'private' | 'group' | 'supergroup' | 'channel'; messageId: number; userId: string; username?: string; threadId?: number; // For forum topics replyToMessageId?: number; } ``` ### Message History The plugin tracks conversation history: ```typescript class MessageHistory { private history: Map = new Map(); private limit: number; addMessage(chatId: string, message: TelegramMessage) { const messages = this.history.get(chatId) || []; messages.push(message); // Maintain limit if (messages.length > this.limit) { messages.splice(0, messages.length - this.limit); } this.history.set(chatId, messages); } getHistory(chatId: string): TelegramMessage[] { return this.history.get(chatId) || []; } } ``` ## Media Processing ### Image Processing ```typescript async function processPhoto( photos: PhotoSize[], bot: Telegraf, runtime: IAgentRuntime ): Promise { // Get highest resolution photo const photo = photos[photos.length - 1]; // Get file info const file = await bot.telegram.getFile(photo.file_id); const url = `https://api.telegram.org/file/bot${token}/${file.file_path}`; // Download and analyze const description = await analyzeImage(url, runtime); return { type: 'image', url, description, metadata: { fileId: photo.file_id, width: photo.width, height: photo.height } }; } ``` ### Voice Transcription ```typescript async function transcribeVoice( voice: Voice, bot: Telegraf, runtime: IAgentRuntime ): Promise { // Get voice file const file = await bot.telegram.getFile(voice.file_id); const url = `https://api.telegram.org/file/bot${token}/${file.file_path}`; // Download audio const audioBuffer = await downloadFile(url); // Transcribe using runtime's transcription service const transcript = await runtime.transcribe(audioBuffer, { mimeType: voice.mime_type || 'audio/ogg', duration: voice.duration }); return transcript; } ``` ### Document Handling ```typescript async function processDocument( document: Document, bot: Telegraf, runtime: IAgentRuntime ): Promise { const file = await bot.telegram.getFile(document.file_id); const url = `https://api.telegram.org/file/bot${token}/${file.file_path}`; // Process based on MIME type if (document.mime_type?.startsWith('image/')) { return processImageDocument(document, url, runtime); } else if (document.mime_type?.startsWith('text/')) { return processTextDocument(document, url, runtime); } // Generic document return { type: 'document', url, name: document.file_name, mimeType: document.mime_type }; } ``` ## Interactive Elements ### Inline Keyboards Create interactive button layouts: ```typescript // Simple button layout const keyboard = { inline_keyboard: [[ { text: "Option 1", callback_data: "opt_1" }, { text: "Option 2", callback_data: "opt_2" } ], [ { text: "Cancel", callback_data: "cancel" } ]] }; // URL buttons const urlKeyboard = { inline_keyboard: [[ { text: "Visit Website", url: "https://example.com" }, { text: "Documentation", url: "https://docs.example.com" } ]] }; // Mixed buttons const mixedKeyboard = { inline_keyboard: [[ { text: "Action", callback_data: "action" }, { text: "Learn More", url: "https://example.com" } ]] }; ``` ### Callback Handling Process button clicks: ```typescript bot.on('callback_query', async (ctx) => { const callbackData = ctx.callbackQuery.data; // Answer callback to remove loading state await ctx.answerCbQuery(); // Process based on callback data switch (callbackData) { case 'opt_1': await ctx.editMessageText('You selected Option 1'); break; case 'opt_2': await ctx.editMessageText('You selected Option 2'); break; case 'cancel': await ctx.deleteMessage(); break; } }); ``` ### Reply Keyboards Create custom keyboard layouts: ```typescript const replyKeyboard = { keyboard: [ ['Button 1', 'Button 2'], ['Button 3', 'Button 4'], ['Cancel'] ], resize_keyboard: true, one_time_keyboard: true }; await ctx.reply('Choose an option:', { reply_markup: replyKeyboard }); ``` ## Group Management ### Access Control Restrict bot to specific groups: ```typescript function checkGroupAccess(ctx: Context): boolean { if (!this.runtime.character.shouldOnlyJoinInAllowedGroups) { return true; } const allowedGroups = this.runtime.character.allowedGroupIds || []; const chatId = ctx.chat?.id.toString(); return allowedGroups.includes(chatId); } ``` ### Group Features Handle group-specific functionality: ```typescript // Check if bot is admin async function isBotAdmin(ctx: Context): Promise { const botId = ctx.botInfo.id; const member = await ctx.getChatMember(botId); return member.status === 'administrator' || member.status === 'creator'; } // Get group info async function getGroupInfo(ctx: Context) { const chat = await ctx.getChat(); return { id: chat.id, title: chat.title, type: chat.type, memberCount: await ctx.getChatMembersCount(), description: chat.description }; } ``` ### Privacy Mode Handle bot privacy settings: ```typescript // With privacy mode disabled (recommended) // Bot receives all messages in groups // With privacy mode enabled // Bot only receives: // - Messages that mention the bot // - Replies to bot's messages // - Commands ``` ## Forum Topics ### Topic Detection Identify and handle forum topics: ```typescript function getTopicId(ctx: Context): number | undefined { // Forum messages have thread_id return ctx.message?.message_thread_id; } function getRoomId(ctx: Context): string { const chatId = ctx.chat.id; const topicId = getTopicId(ctx); if (topicId) { // Treat topic as separate room return `${chatId}-topic-${topicId}`; } return chatId.toString(); } ``` ### Topic Context Maintain separate context per topic: ```typescript class TopicManager { private topicContexts: Map = new Map(); getContext(chatId: string, topicId?: number): TopicContext { const key = topicId ? `${chatId}-${topicId}` : chatId; if (!this.topicContexts.has(key)) { this.topicContexts.set(key, { messages: [], metadata: {}, lastActivity: Date.now() }); } return this.topicContexts.get(key)!; } } ``` ## Error Handling ### API Errors Handle Telegram API errors: ```typescript async function handleTelegramError(error: any) { if (error.response?.error_code === 429) { // Rate limited const retryAfter = error.response.parameters?.retry_after || 60; logger.warn(`Rate limited, retry after ${retryAfter}s`); await sleep(retryAfter * 1000); return true; // Retry } if (error.response?.error_code === 400) { // Bad request logger.error('Bad request:', error.response.description); return false; // Don't retry } // Network error if (error.code === 'ETIMEOUT' || error.code === 'ECONNREFUSED') { logger.error('Network error:', error.message); return true; // Retry } return false; } ``` ### Multi-Agent Environment Handle bot token conflicts: ```typescript // Error: 409 Conflict // Only one getUpdates request allowed per bot token // Solution 1: Use different tokens const bot1 = new Telegraf(process.env.BOT1_TOKEN); const bot2 = new Telegraf(process.env.BOT2_TOKEN); // Solution 2: Use webhooks instead of polling bot.telegram.setWebhook('https://your-domain.com/bot-webhook'); // Solution 3: Single bot, multiple personalities const multiPersonalityBot = new Telegraf(token); multiPersonalityBot.use(async (ctx, next) => { // Route to different agents based on context const agent = selectAgent(ctx); await agent.handleUpdate(ctx); }); ``` ### Connection Management Handle connection issues: ```typescript class ConnectionManager { private reconnectAttempts = 0; private maxReconnectAttempts = 5; async connect() { try { await this.bot.launch(); this.reconnectAttempts = 0; } catch (error) { if (this.reconnectAttempts < this.maxReconnectAttempts) { this.reconnectAttempts++; const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000); logger.warn(`Reconnecting in ${delay}ms...`); await sleep(delay); return this.connect(); } throw error; } } } ``` ## Integration Guide ### Basic Setup ```typescript import { telegramPlugin } from '@elizaos/plugin-telegram'; import { AgentRuntime } from '@elizaos/core'; const runtime = new AgentRuntime({ plugins: [telegramPlugin], character: { name: "TelegramBot", clients: ["telegram"], settings: { TELEGRAM_BOT_TOKEN: process.env.TELEGRAM_BOT_TOKEN } } }); await runtime.start(); ``` ### Custom Message Handler Override default message handling: ```typescript const customHandler = { name: "CUSTOM_TELEGRAM_HANDLER", description: "Custom Telegram message handler", handler: async (runtime, message, state, options, callback) => { // Access Telegram-specific data const telegramContext = message.metadata?.telegram; if (telegramContext?.messageType === 'photo') { // Special handling for photos const analysis = await analyzePhoto(message.attachments[0]); await callback({ text: `I see: ${analysis}` }); return true; } // Default handling return false; } }; ``` ### Webhook Setup Configure webhooks for production: ```typescript // Set webhook await bot.telegram.setWebhook('https://your-domain.com/telegram-webhook', { certificate: fs.readFileSync('path/to/cert.pem'), // Optional allowed_updates: ['message', 'callback_query'], drop_pending_updates: true }); // Express webhook handler app.post('/telegram-webhook', (req, res) => { bot.handleUpdate(req.body); res.sendStatus(200); }); ``` ### Testing ```typescript describe('Telegram Plugin Tests', () => { let service: TelegramService; let runtime: AgentRuntime; beforeAll(async () => { runtime = createTestRuntime(); service = new TelegramService(runtime); await service.start(); }); it('should process text messages', async () => { const mockUpdate = createMockTextMessage('Hello bot'); await service.bot.handleUpdate(mockUpdate); // Verify response expect(mockTelegram.sendMessage).toHaveBeenCalled(); }); }); ``` ## Best Practices 1. **Token Security** * Never commit tokens to version control * Use environment variables * Rotate tokens periodically 2. **Rate Limiting** * Implement exponential backoff * Cache frequently requested data * Use bulk operations when possible 3. **Group Management** * Always check permissions before actions * Handle bot removal gracefully * Implement admin controls 4. **Error Handling** * Log all API errors * Provide user-friendly error messages * Implement retry logic for transient errors 5. **Performance** * Use webhooks in production * Implement message queuing * Optimize media processing ## Support For issues and questions: * 📚 Check the [examples](./examples.mdx) * 💬 Join our [Discord community](https://discord.gg/elizaos) * 🐛 Report issues on [GitHub](https://github.com/elizaos/eliza/issues) # Examples Source: https://docs.elizaos.ai/plugin-registry/platform/telegram/examples This document provides practical examples of using the @elizaos/plugin-telegram package in various scenarios. # Telegram Plugin Examples This document provides practical examples of using the @elizaos/plugin-telegram package in various scenarios. ## Basic Bot Setup ### Simple Message Bot Create a basic Telegram bot that responds to messages: ```typescript import { AgentRuntime } from '@elizaos/core'; import { telegramPlugin } from '@elizaos/plugin-telegram'; import { bootstrapPlugin } from '@elizaos/plugin-bootstrap'; const character = { name: "SimpleTelegramBot", description: "A simple Telegram bot", plugins: [bootstrapPlugin, telegramPlugin], clients: ["telegram"], settings: { TELEGRAM_BOT_TOKEN: process.env.TELEGRAM_BOT_TOKEN }, // Message examples for the bot's personality messageExamples: [ { user: "user", content: { text: "Hello!" }, response: { text: "Hello! How can I help you today?" } }, { user: "user", content: { text: "What's the weather?" }, response: { text: "I'm sorry, I don't have access to weather data. Is there something else I can help you with?" } } ] }; // Create and start the runtime const runtime = new AgentRuntime({ character }); await runtime.start(); console.log('Telegram bot is running!'); ``` ### Echo Bot A simple bot that echoes messages back: ```typescript const echoBot = { name: "EchoBot", description: "Echoes messages back to users", plugins: [bootstrapPlugin, telegramPlugin], clients: ["telegram"], settings: { TELEGRAM_BOT_TOKEN: process.env.TELEGRAM_BOT_TOKEN }, templates: { telegramMessageHandlerTemplate: ` You are an echo bot. Simply repeat back what the user says. If they send media, describe what you received. ` } }; ``` ### FAQ Bot Bot that answers frequently asked questions: ```typescript const faqBot = { name: "FAQBot", description: "Answers frequently asked questions", plugins: [bootstrapPlugin, telegramPlugin], clients: ["telegram"], settings: { TELEGRAM_BOT_TOKEN: process.env.TELEGRAM_BOT_TOKEN }, knowledge: [ "Our business hours are 9 AM to 5 PM EST, Monday through Friday.", "Shipping typically takes 3-5 business days.", "We accept returns within 30 days of purchase.", "Customer support can be reached at support@example.com" ], templates: { telegramMessageHandlerTemplate: ` You are a customer support FAQ bot. Answer questions based on the knowledge provided. If you don't know the answer, politely say so and suggest contacting support. ` } }; ``` ## Interactive Button Bots ### Button Menu Bot Create a bot with interactive button menus: ```typescript import { Action } from '@elizaos/core'; const menuAction: Action = { name: "SHOW_MENU", description: "Shows the main menu", similes: ["menu", "help", "start", "options"], handler: async (runtime, message, state, options, callback) => { await callback({ text: "What would you like to do?", buttons: [ [ { text: "📊 View Stats", callback_data: "view_stats" }, { text: "⚙️ Settings", callback_data: "settings" } ], [ { text: "📚 Help", callback_data: "help" }, { text: "ℹ️ About", callback_data: "about" } ] ] }); return true; } }; const buttonBot = { name: "MenuBot", description: "Bot with interactive menus", plugins: [bootstrapPlugin, telegramPlugin], clients: ["telegram"], actions: [menuAction], settings: { TELEGRAM_BOT_TOKEN: process.env.TELEGRAM_BOT_TOKEN } }; ``` ### Inline Keyboard Bot Bot with inline URL buttons: ```typescript const linkAction: Action = { name: "SHARE_LINKS", description: "Share useful links", similes: ["links", "resources", "websites"], handler: async (runtime, message, state, options, callback) => { await callback({ text: "Here are some useful resources:", buttons: [ [ { text: "📖 Documentation", url: "https://docs.example.com" }, { text: "💬 Community", url: "https://discord.gg/example" } ], [ { text: "🐙 GitHub", url: "https://github.com/example" }, { text: "🐦 Twitter", url: "https://twitter.com/example" } ] ] }); return true; } }; ``` ### Callback Handler Handle button callbacks: ```typescript const callbackAction: Action = { name: "HANDLE_CALLBACK", description: "Handles button callbacks", handler: async (runtime, message, state, options, callback) => { const callbackData = message.content.callback_data; switch (callbackData) { case "view_stats": await callback({ text: "📊 *Your Stats*\n\nMessages sent: 42\nActive days: 7\nPoints: 128" }); break; case "settings": await callback({ text: "⚙️ *Settings*", buttons: [ [ { text: "🔔 Notifications", callback_data: "toggle_notifications" }, { text: "🌐 Language", callback_data: "change_language" } ], [ { text: "⬅️ Back", callback_data: "main_menu" } ] ] }); break; case "help": await callback({ text: "📚 *Help*\n\nHere's how to use this bot:\n\n/start - Show main menu\n/help - Show this help\n/stats - View your statistics" }); break; } return true; } }; ``` ## Media Processing Bots ### Image Analysis Bot Bot that analyzes images using vision capabilities: ```typescript const imageAnalysisBot = { name: "ImageAnalyzer", description: "Analyzes images sent by users", plugins: [bootstrapPlugin, telegramPlugin], clients: ["telegram"], modelProvider: "openai", settings: { TELEGRAM_BOT_TOKEN: process.env.TELEGRAM_BOT_TOKEN, OPENAI_API_KEY: process.env.OPENAI_API_KEY } }; // The plugin automatically processes images and adds descriptions // You can create custom actions to enhance this: const analyzeImageAction: Action = { name: "ANALYZE_IMAGE_DETAILS", description: "Provide detailed image analysis", validate: async (runtime, message) => { return message.attachments?.some(att => att.type === 'image') ?? false; }, handler: async (runtime, message, state, options, callback) => { const imageAttachment = message.attachments.find(att => att.type === 'image'); if (imageAttachment && imageAttachment.description) { await callback({ text: `🖼️ *Image Analysis*\n\n${imageAttachment.description}\n\nWhat would you like to know about this image?` }); } return true; } }; ``` ### Voice Transcription Bot Bot that transcribes voice messages: ```typescript const voiceBot = { name: "VoiceTranscriber", description: "Transcribes voice messages", plugins: [bootstrapPlugin, telegramPlugin], clients: ["telegram"], settings: { TELEGRAM_BOT_TOKEN: process.env.TELEGRAM_BOT_TOKEN }, templates: { telegramMessageHandlerTemplate: ` When you receive a voice message transcript, acknowledge it and offer to help with any questions about the content. ` } }; // Voice messages are automatically transcribed by the plugin // The transcript appears as regular text in message.content.text ``` ### Document Processor Bot that processes various document types: ```typescript const documentAction: Action = { name: "PROCESS_DOCUMENT", description: "Process uploaded documents", validate: async (runtime, message) => { return message.attachments?.some(att => att.type === 'document') ?? false; }, handler: async (runtime, message, state, options, callback) => { const doc = message.attachments.find(att => att.type === 'document'); if (doc) { let response = `📄 Received document: ${doc.name}\n`; response += `Type: ${doc.mimeType}\n`; response += `Size: ${formatFileSize(doc.size)}\n\n`; if (doc.mimeType?.startsWith('text/')) { response += "I can read text from this document. What would you like me to help you with?"; } else if (doc.mimeType?.startsWith('image/')) { response += "This appears to be an image document. I can analyze it for you!"; } else { response += "I've received the document. How can I assist you with it?"; } await callback({ text: response }); } return true; } }; function formatFileSize(bytes: number): string { const sizes = ['Bytes', 'KB', 'MB', 'GB']; if (bytes === 0) return '0 Bytes'; const i = Math.floor(Math.log(bytes) / Math.log(1024)); return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i]; } ``` ## Group Management Bots ### Group Moderator Bot Bot that helps moderate groups: ```typescript const moderatorBot = { name: "GroupModerator", description: "Helps moderate Telegram groups", plugins: [bootstrapPlugin, telegramPlugin], clients: ["telegram"], settings: { TELEGRAM_BOT_TOKEN: process.env.TELEGRAM_BOT_TOKEN, // Only work in specific groups shouldOnlyJoinInAllowedGroups: true, allowedGroupIds: ["-1001234567890", "-1009876543210"] } }; const welcomeAction: Action = { name: "WELCOME_NEW_MEMBERS", description: "Welcome new group members", handler: async (runtime, message, state, options, callback) => { if (message.content.new_chat_members) { const names = message.content.new_chat_members .map(member => member.first_name) .join(', '); await callback({ text: `Welcome to the group, ${names}! 👋\n\nPlease read our rules in the pinned message.`, buttons: [[ { text: "📋 View Rules", url: "https://example.com/rules" } ]] }); } return true; } }; ``` ### Restricted Access Bot Bot with access control: ```typescript const restrictedBot = { name: "PrivateBot", description: "Bot with restricted access", plugins: [bootstrapPlugin, telegramPlugin], clients: ["telegram"], settings: { TELEGRAM_BOT_TOKEN: process.env.TELEGRAM_BOT_TOKEN, TELEGRAM_ALLOWED_CHATS: JSON.stringify([ "123456789", // User ID "-987654321", // Group ID "@channelname" // Channel username ]) } }; // Additional access control action const checkAccessAction: Action = { name: "CHECK_ACCESS", description: "Verify user access", handler: async (runtime, message, state, options, callback) => { const allowedUsers = ["user1", "user2", "admin"]; const username = message.username; if (!allowedUsers.includes(username)) { await callback({ text: "Sorry, you don't have access to this bot. Please contact an administrator." }); return false; } return true; } }; ``` ### Forum Topic Bot Bot that handles forum topics: ```typescript const forumBot = { name: "ForumAssistant", description: "Manages forum topics", plugins: [bootstrapPlugin, telegramPlugin], clients: ["telegram"], settings: { TELEGRAM_BOT_TOKEN: process.env.TELEGRAM_BOT_TOKEN, messageTrackingLimit: 50 // Track last 50 messages per topic } }; // Topic-aware action const topicAction: Action = { name: "TOPIC_SUMMARY", description: "Summarize current topic", handler: async (runtime, message, state, options, callback) => { const topicId = message.threadId; if (topicId) { // Get topic-specific context const topicMessages = state.recentMessages?.filter( msg => msg.threadId === topicId ); await callback({ text: `📋 Topic Summary\n\nMessages in this topic: ${topicMessages?.length || 0}\n\nUse /help for topic-specific commands.` }); } else { await callback({ text: "This command only works in forum topics." }); } return true; } }; ``` ## Advanced Examples ### Multi-Language Bot Bot that supports multiple languages: ```typescript const multiLangBot = { name: "PolyglotBot", description: "Multi-language support bot", plugins: [bootstrapPlugin, telegramPlugin], clients: ["telegram"], settings: { TELEGRAM_BOT_TOKEN: process.env.TELEGRAM_BOT_TOKEN } }; const languageAction: Action = { name: "SET_LANGUAGE", description: "Set user language preference", similes: ["language", "lang", "idioma", "langue"], handler: async (runtime, message, state, options, callback) => { await callback({ text: "Please select your language / Seleccione su idioma / Choisissez votre langue:", buttons: [ [ { text: "🇬🇧 English", callback_data: "lang_en" }, { text: "🇪🇸 Español", callback_data: "lang_es" } ], [ { text: "🇫🇷 Français", callback_data: "lang_fr" }, { text: "🇩🇪 Deutsch", callback_data: "lang_de" } ] ] }); return true; } }; ``` ### Webhook Bot Bot configured for webhooks: ```typescript import express from 'express'; import { Telegraf } from 'telegraf'; const app = express(); app.use(express.json()); const bot = new Telegraf(process.env.TELEGRAM_BOT_TOKEN!); // Set webhook const WEBHOOK_URL = 'https://your-domain.com/telegram-webhook'; bot.telegram.setWebhook(WEBHOOK_URL); // Webhook endpoint app.post('/telegram-webhook', (req, res) => { bot.handleUpdate(req.body); res.sendStatus(200); }); // Health check app.get('/health', (req, res) => { res.json({ status: 'ok' }); }); const webhookBot = { name: "WebhookBot", description: "Production bot using webhooks", plugins: [bootstrapPlugin, telegramPlugin], clients: ["telegram"], settings: { TELEGRAM_BOT_TOKEN: process.env.TELEGRAM_BOT_TOKEN, TELEGRAM_WEBHOOK_URL: WEBHOOK_URL } }; app.listen(3000, () => { console.log('Webhook server running on port 3000'); }); ``` ### State Management Bot Bot with persistent state management: ```typescript const stateBot = { name: "StatefulBot", description: "Bot with state management", plugins: [bootstrapPlugin, telegramPlugin], clients: ["telegram"], settings: { TELEGRAM_BOT_TOKEN: process.env.TELEGRAM_BOT_TOKEN } }; // User preferences storage const userPreferences = new Map(); const savePreferenceAction: Action = { name: "SAVE_PREFERENCE", description: "Save user preferences", handler: async (runtime, message, state, options, callback) => { const userId = message.userId; const preference = options.preference; // Save to persistent storage if (!userPreferences.has(userId)) { userPreferences.set(userId, {}); } const prefs = userPreferences.get(userId); prefs[preference.key] = preference.value; await callback({ text: `Preference saved! ${preference.key} = ${preference.value}` }); return true; } }; ``` ### Error Handling Bot Bot with comprehensive error handling: ```typescript const errorHandlingBot = { name: "RobustBot", description: "Bot with error handling", plugins: [bootstrapPlugin, telegramPlugin], clients: ["telegram"], settings: { TELEGRAM_BOT_TOKEN: process.env.TELEGRAM_BOT_TOKEN } }; const safeAction: Action = { name: "SAFE_ACTION", description: "Action with error handling", handler: async (runtime, message, state, options, callback) => { try { // Risky operation const result = await riskyOperation(); await callback({ text: `Success: ${result}` }); } catch (error) { runtime.logger.error('Action failed:', error); // User-friendly error message let errorMessage = "Sorry, something went wrong. "; if (error.code === 'TIMEOUT') { errorMessage += "The operation timed out. Please try again."; } else if (error.code === 'RATE_LIMIT') { errorMessage += "Too many requests. Please wait a moment."; } else { errorMessage += "Please try again or contact support."; } await callback({ text: errorMessage, buttons: [[ { text: "🔄 Retry", callback_data: "retry_action" }, { text: "❌ Cancel", callback_data: "cancel" } ]] }); } return true; } }; ``` ## Testing Examples ### Test Bot Configuration ```typescript const testBot = { name: "TestBot", description: "Bot for testing", plugins: [bootstrapPlugin, telegramPlugin], clients: ["telegram"], settings: { TELEGRAM_BOT_TOKEN: process.env.TELEGRAM_TEST_BOT_TOKEN, TELEGRAM_TEST_CHAT_ID: process.env.TELEGRAM_TEST_CHAT_ID } }; // Test action const testAction: Action = { name: "RUN_TEST", description: "Run test scenarios", handler: async (runtime, message, state, options, callback) => { const testResults = []; // Test 1: Text message testResults.push({ test: "Text Message", result: "✅ Passed" }); // Test 2: Button interaction await callback({ text: "Test: Button Interaction", buttons: [[ { text: "Test Button", callback_data: "test_button" } ]] }); // Test 3: Media handling if (message.attachments?.length > 0) { testResults.push({ test: "Media Processing", result: "✅ Passed" }); } // Send results const summary = testResults.map(r => `${r.test}: ${r.result}`).join('\n'); await callback({ text: `Test Results:\n\n${summary}` }); return true; } }; ``` ## Best Practices Examples ### Production-Ready Bot ```typescript import { telegramPlugin } from '@elizaos/plugin-telegram'; import { bootstrapPlugin } from '@elizaos/plugin-bootstrap'; import { AgentRuntime } from '@elizaos/core'; const productionBot = { name: "ProductionBot", description: "Production-ready Telegram bot", plugins: [bootstrapPlugin, telegramPlugin], clients: ["telegram"], settings: { // Security TELEGRAM_BOT_TOKEN: process.env.TELEGRAM_BOT_TOKEN, TELEGRAM_ALLOWED_CHATS: process.env.TELEGRAM_ALLOWED_CHATS, // Performance messageTrackingLimit: 50, // Behavior allowDirectMessages: true, shouldOnlyJoinInAllowedGroups: true, allowedGroupIds: JSON.parse(process.env.ALLOWED_GROUPS || '[]') }, // Rate limiting rateLimits: { maxMessagesPerMinute: 60, maxMessagesPerHour: 1000 } }; // Error recovery process.on('unhandledRejection', (error) => { console.error('Unhandled rejection:', error); // Implement recovery logic }); // Graceful shutdown process.on('SIGTERM', async () => { console.log('Shutting down gracefully...'); await runtime.stop(); process.exit(0); }); ``` # Message Flow Source: https://docs.elizaos.ai/plugin-registry/platform/telegram/message-flow This document provides a comprehensive breakdown of how messages flow through the Telegram plugin system. # Telegram Plugin Message Flow - Detailed Breakdown This document provides a comprehensive breakdown of how messages flow through the Telegram plugin system. ## Complete Message Flow Diagram ```mermaid flowchart TD Start([Telegram Update]) --> A[Telegram Bot API] A --> B{Update Type} B -->|Message| C[Message Update] B -->|Callback Query| D[Callback Update] B -->|Edited Message| E[Edited Update] B -->|Channel Post| F[Channel Update] %% Message Flow C --> G[Telegraf Middleware] G --> H{From Bot?} H -->|Yes| End1[Ignore] H -->|No| I[Sync Chat/User] I --> J[Create/Update Entities] J --> K{Chat Type} K -->|Private| L[Direct Message Flow] K -->|Group| M[Group Message Flow] K -->|Channel| N[Channel Post Flow] L --> O{Allow DMs?} O -->|No| End2[Ignore] O -->|Yes| P[Process Message] M --> Q{Allowed Group?} Q -->|No| End3[Ignore] Q -->|Yes| R{Forum Topic?} R -->|Yes| S[Get Topic Context] R -->|No| T[Get Chat Context] S --> P T --> P P --> U[Message Manager] U --> V{Has Media?} V -->|Yes| W[Process Media] V -->|No| X[Convert Format] W --> X X --> Y[Add Telegram Context] Y --> Z[Send to Bootstrap] Z --> AA[Bootstrap Plugin] AA --> AB[Generate Response] AB --> AC{Has Callback?} AC -->|No| End4[No Response] AC -->|Yes| AD[Format Response] AD --> AE{Response Type} AE -->|Text| AF[Send Text] AE -->|Buttons| AG[Send with Keyboard] AE -->|Media| AH[Send Media] AF --> AI[Message Sent] AG --> AI AH --> AI %% Callback Flow D --> AJ[Parse Callback Data] AJ --> AK[Answer Callback Query] AK --> AL{Update Message?} AL -->|Yes| AM[Edit Original Message] AL -->|No| AN[Send New Message] %% Edited Message Flow E --> AO[Find Original] AO --> AP{Found?} AP -->|Yes| AQ[Update Context] AP -->|No| AR[Process as New] ``` ## Detailed Event Flows ### 1. Initial Update Processing ```mermaid sequenceDiagram participant T as Telegram API participant B as Telegraf Bot participant M as Middleware participant S as TelegramService T->>B: Incoming Update B->>M: Process Middleware Chain M->>M: Log Update M->>M: Check Update Type M->>S: Route to Handler alt Is Message S->>S: Process Message Update else Is Callback Query S->>S: Process Callback else Is Edited Message S->>S: Process Edit end ``` ### 2. Chat/User Synchronization ```mermaid sequenceDiagram participant M as Middleware participant S as Service participant D as Database participant R as Runtime M->>S: Update Received S->>S: Extract Chat Info alt New Chat S->>D: Create Chat Entity S->>R: Emit WORLD_JOINED else Known Chat S->>S: Update Last Seen end S->>S: Extract User Info alt New User S->>D: Create User Entity S->>S: Track User ID else Known User S->>D: Update User Info end ``` ### 3. Message Processing Pipeline ```mermaid flowchart TD A[Raw Message] --> B[Extract Content] B --> C{Message Type} C -->|Text| D[Plain Text] C -->|Photo| E[Photo + Caption] C -->|Voice| F[Voice Message] C -->|Document| G[Document] C -->|Video| H[Video] E --> I[Download Photo] I --> J[Get File URL] J --> K[Analyze Image] K --> L[Add Description] F --> M[Download Voice] M --> N[Transcribe Audio] N --> O[Add Transcript] G --> P[Check MIME Type] P --> Q{Document Type} Q -->|Image| K Q -->|Text| R[Extract Text] Q -->|Other| S[Store Reference] D --> T[Create Message Object] L --> T O --> T R --> T S --> T T --> U[Add Metadata] U --> V[Message Ready] ``` ### 4. Media Processing Flow ```mermaid sequenceDiagram participant M as Message participant H as Handler participant T as Telegram API participant P as Processor participant R as Runtime M->>H: Media Message H->>H: Identify Media Type alt Photo H->>T: Get File Info T->>H: File Details H->>T: Construct Download URL H->>P: Download & Process P->>R: Analyze with Vision R->>P: Description P->>H: Processed Photo else Voice H->>T: Get Voice File T->>H: Voice Details H->>P: Download Audio P->>R: Transcribe R->>P: Transcript P->>H: Text Content else Document H->>T: Get Document T->>H: Document Info H->>P: Process by Type P->>H: Processed Content end H->>H: Attach to Message ``` ### 5. Response Generation Flow ```mermaid flowchart TD A[Bootstrap Response] --> B{Response Content} B --> C{Has Text?} C -->|Yes| D[Format Text] C -->|No| E[Skip Text] B --> F{Has Buttons?} F -->|Yes| G[Create Keyboard] F -->|No| H[No Keyboard] B --> I{Has Media?} I -->|Yes| J[Prepare Media] I -->|No| K[No Media] D --> L[Apply Formatting] L --> M{Length Check} M -->|Too Long| N[Split Message] M -->|OK| O[Single Message] G --> P[Inline Keyboard] P --> Q{Button Type} Q -->|Callback| R[Add Callback Data] Q -->|URL| S[Add URL] J --> T{Media Type} T -->|Photo| U[Send Photo] T -->|Document| V[Send Document] T -->|Audio| W[Send Audio] O --> X[Compose Final] N --> X R --> X S --> X U --> X V --> X W --> X X --> Y[Send to Telegram] ``` ### 6. Forum Topic Handling ```mermaid flowchart TD A[Group Message] --> B{Has Thread ID?} B -->|Yes| C[Forum Message] B -->|No| D[Regular Group] C --> E[Extract Topic ID] E --> F[Create Room ID] F --> G[Format: chatId-topic-topicId] D --> H[Use Chat ID] H --> I[Format: chatId] G --> J[Get Topic Context] I --> K[Get Chat Context] J --> L{Topic Exists?} L -->|No| M[Create Topic Context] L -->|Yes| N[Load Topic History] M --> O[Initialize History] N --> O O --> P[Process in Context] K --> P P --> Q[Generate Response] Q --> R{Reply to Topic?} R -->|Yes| S[Set Thread ID] R -->|No| T[Regular Reply] ``` ## State Management ### Message State ```typescript interface TelegramMessageState { // Core message data messageId: number; chatId: string; userId: string; timestamp: Date; // Content text?: string; media?: MediaAttachment[]; // Context replyToMessageId?: number; threadId?: number; editedAt?: Date; // Metadata entities?: MessageEntity[]; buttons?: InlineKeyboardButton[][]; } ``` ### Chat State ```typescript interface TelegramChatState { chatId: string; chatType: 'private' | 'group' | 'supergroup' | 'channel'; title?: string; username?: string; // Settings allowedUsers?: string[]; messageLimit: number; // Forum support isForumChat: boolean; topics: Map; // History messages: TelegramMessage[]; lastActivity: Date; } ``` ### Callback State ```typescript interface CallbackState { messageId: number; chatId: string; callbackData: string; userId: string; timestamp: Date; // For maintaining state originalMessage?: TelegramMessage; context?: any; } ``` ## Error Handling Flow ```mermaid flowchart TD A[Error Occurs] --> B{Error Type} B -->|API Error| C[Check Error Code] B -->|Network Error| D[Network Handler] B -->|Processing Error| E[App Error Handler] C --> F{Error Code} F -->|429| G[Rate Limited] F -->|400| H[Bad Request] F -->|403| I[Forbidden] F -->|409| J[Conflict] G --> K[Extract Retry After] K --> L[Wait & Retry] H --> M[Log Error] M --> N[Skip Message] I --> O[Check Permissions] O --> P[Notify Admin] J --> Q[Token Conflict] Q --> R[Single Instance Check] D --> S{Retry Count} S -->|< Max| T[Exponential Backoff] S -->|>= Max| U[Give Up] T --> V[Retry Request] E --> W[Log Stack Trace] W --> X[Send Error Response] X --> Y[Continue Processing] ``` ## Performance Optimization ### Message Batching ```mermaid sequenceDiagram participant T as Telegram participant B as Bot participant Q as Queue participant P as Processor loop Receive Updates T->>B: Update 1 B->>Q: Queue Update T->>B: Update 2 B->>Q: Queue Update T->>B: Update 3 B->>Q: Queue Update end Note over Q: Batch Window (100ms) Q->>P: Process Batch [1,2,3] par Process in Parallel P->>P: Handle Update 1 P->>P: Handle Update 2 P->>P: Handle Update 3 end P->>B: Batch Complete ``` ### Caching Strategy ```mermaid flowchart TD A[Request] --> B{In Cache?} B -->|Yes| C[Check TTL] B -->|No| D[Fetch Data] C --> E{Valid?} E -->|Yes| F[Return Cached] E -->|No| G[Invalidate] G --> D D --> H[Process Request] H --> I[Store in Cache] I --> J[Set TTL] J --> K[Return Data] F --> K L[Cache Types] --> M[User Cache] L --> N[Chat Cache] L --> O[Media Cache] L --> P[Response Cache] ``` ## Webhook vs Polling ### Polling Flow ```mermaid sequenceDiagram participant B as Bot participant T as Telegram API loop Every Interval B->>T: getUpdates(offset) T->>B: Updates[] B->>B: Process Updates B->>B: Update Offset end ``` ### Webhook Flow ```mermaid sequenceDiagram participant T as Telegram API participant W as Web Server participant B as Bot Handler T->>W: POST /webhook W->>W: Verify Token W->>B: Handle Update B->>B: Process Update B->>W: Response W->>T: 200 OK ``` ## Multi-Language Support ```mermaid flowchart TD A[Message Received] --> B[Detect Language] B --> C{Detection Method} C -->|User Setting| D[Load User Language] C -->|Auto Detect| E[Analyze Message] C -->|Chat Setting| F[Load Chat Language] E --> G[Language Code] D --> G F --> G G --> H[Set Context Language] H --> I[Process Message] I --> J[Generate Response] J --> K[Apply Language Template] K --> L[Localize Response] L --> M[Send Message] ``` ## Security Flow ```mermaid flowchart TD A[Incoming Update] --> B[Verify Source] B --> C{Valid Token?} C -->|No| D[Reject] C -->|Yes| E[Check Permissions] E --> F{User Allowed?} F -->|No| G[Check Restrictions] F -->|Yes| H[Process] G --> I{Chat Allowed?} I -->|No| J[Ignore] I -->|Yes| H H --> K[Sanitize Input] K --> L[Validate Format] L --> M[Process Safely] M --> N[Check Output] N --> O{Safe Response?} O -->|No| P[Filter Content] O -->|Yes| Q[Send Response] P --> Q ``` ## Best Practices 1. **Update Handling** * Process updates asynchronously * Implement proper error boundaries * Log all update types 2. **State Management** * Maintain minimal state * Use TTL for cached data * Clean up old conversations 3. **Performance** * Batch similar operations * Use webhooks in production * Implement connection pooling 4. **Error Recovery** * Implement exponential backoff * Log errors with context * Provide fallback responses 5. **Security** * Validate all inputs * Sanitize user content * Implement rate limiting # Testing Guide Source: https://docs.elizaos.ai/plugin-registry/platform/telegram/testing-guide Testing strategies, patterns, and best practices for the Telegram plugin package. ## Test Environment Setup ### Prerequisites 1. **Test Bot Setup** * Create a dedicated test bot via @BotFather * Get test bot token * Configure test bot settings 2. **Test Infrastructure** * Create a test group/channel * Add test bot as admin (for group tests) * Set up test user accounts 3. **Environment Configuration** ```bash # .env.test TELEGRAM_BOT_TOKEN=test_bot_token_here TELEGRAM_TEST_CHAT_ID=-1001234567890 # Test group ID TELEGRAM_TEST_USER_ID=123456789 # Test user ID TELEGRAM_TEST_CHANNEL_ID=@testchannel # Test channel # Optional test settings TELEGRAM_API_ROOT=https://api.telegram.org # Or test server TELEGRAM_TEST_TIMEOUT=30000 # Test timeout in ms ``` ## Unit Testing ### Testing Message Manager ```typescript import { describe, it, expect, beforeEach, vi } from 'vitest'; import { MessageManager } from '@elizaos/plugin-telegram'; import { Telegraf, Context } from 'telegraf'; describe('MessageManager', () => { let messageManager: MessageManager; let mockBot: Telegraf; let mockRuntime: any; beforeEach(() => { // Mock Telegraf bot mockBot = { telegram: { sendMessage: vi.fn(), editMessageText: vi.fn(), answerCallbackQuery: vi.fn() } } as any; // Mock runtime mockRuntime = { processMessage: vi.fn(), character: { name: 'TestBot' }, logger: { info: vi.fn(), error: vi.fn() }, getSetting: vi.fn() }; messageManager = new MessageManager(mockBot, mockRuntime); }); describe('handleMessage', () => { it('should process text messages', async () => { const mockContext = createMockContext({ message: { text: 'Hello bot', from: { id: 123, username: 'testuser' }, chat: { id: -456, type: 'group' } } }); mockRuntime.processMessage.mockResolvedValue({ text: 'Hello user!' }); await messageManager.handleMessage(mockContext); expect(mockRuntime.processMessage).toHaveBeenCalledWith( expect.objectContaining({ content: { text: 'Hello bot' }, userId: expect.any(String), channelId: '-456' }) ); }); it('should handle photo messages', async () => { const mockContext = createMockContext({ message: { photo: [ { file_id: 'photo_123', width: 100, height: 100 }, { file_id: 'photo_456', width: 200, height: 200 } ], caption: 'Check this out', from: { id: 123 }, chat: { id: 456 } } }); mockBot.telegram.getFile = vi.fn().mockResolvedValue({ file_path: 'photos/test.jpg' }); await messageManager.handleMessage(mockContext); expect(mockBot.telegram.getFile).toHaveBeenCalledWith('photo_456'); expect(mockRuntime.processMessage).toHaveBeenCalledWith( expect.objectContaining({ content: expect.objectContaining({ text: 'Check this out', attachments: expect.arrayContaining([ expect.objectContaining({ type: 'image' }) ]) }) }) ); }); it('should handle voice messages', async () => { const mockContext = createMockContext({ message: { voice: { file_id: 'voice_123', duration: 5, mime_type: 'audio/ogg' }, from: { id: 123 }, chat: { id: 456 } } }); // Mock transcription mockRuntime.transcribe = vi.fn().mockResolvedValue('Hello world'); await messageManager.handleMessage(mockContext); expect(mockRuntime.processMessage).toHaveBeenCalledWith( expect.objectContaining({ content: expect.objectContaining({ text: 'Hello world' }) }) ); }); }); describe('sendMessageToTelegram', () => { it('should send text messages', async () => { await messageManager.sendMessageToTelegram( 123, { text: 'Test message' } ); expect(mockBot.telegram.sendMessage).toHaveBeenCalledWith( 123, 'Test message', expect.any(Object) ); }); it('should send messages with buttons', async () => { await messageManager.sendMessageToTelegram( 123, { text: 'Choose an option', buttons: [ { text: 'Option 1', callback_data: 'opt1' }, { text: 'Option 2', callback_data: 'opt2' } ] } ); expect(mockBot.telegram.sendMessage).toHaveBeenCalledWith( 123, 'Choose an option', expect.objectContaining({ reply_markup: expect.objectContaining({ inline_keyboard: expect.any(Array) }) }) ); }); }); }); ``` ### Testing Telegram Service ```typescript import { TelegramService } from '@elizaos/plugin-telegram'; import { AgentRuntime } from '@elizaos/core'; describe('TelegramService', () => { let service: TelegramService; let runtime: AgentRuntime; beforeEach(() => { runtime = createMockRuntime(); service = new TelegramService(runtime); }); describe('initialization', () => { it('should initialize with valid token', () => { runtime.getSetting.mockReturnValue('valid_token'); const service = new TelegramService(runtime); expect(service.bot).toBeDefined(); expect(service.messageManager).toBeDefined(); }); it('should handle missing token gracefully', () => { runtime.getSetting.mockReturnValue(''); const service = new TelegramService(runtime); expect(service.bot).toBeNull(); expect(service.messageManager).toBeNull(); }); }); describe('chat synchronization', () => { it('should sync new chats', async () => { const mockContext = createMockContext({ chat: { id: 123, type: 'group', title: 'Test Group' } }); await service.syncChat(mockContext); expect(service.knownChats.has('123')).toBe(true); expect(runtime.emitEvent).toHaveBeenCalledWith( expect.arrayContaining(['WORLD_JOINED']), expect.any(Object) ); }); }); }); ``` ### Testing Utilities ```typescript import { processMediaAttachments, createInlineKeyboard } from '@elizaos/plugin-telegram/utils'; describe('Utilities', () => { describe('processMediaAttachments', () => { it('should process photo attachments', async () => { const mockContext = createMockContext({ message: { photo: [{ file_id: 'photo_123' }] } }); const attachments = await processMediaAttachments( mockContext, mockBot, mockRuntime ); expect(attachments).toHaveLength(1); expect(attachments[0].type).toBe('image'); }); }); describe('createInlineKeyboard', () => { it('should create inline keyboard markup', () => { const buttons = [ { text: 'Button 1', callback_data: 'btn1' }, { text: 'Button 2', url: 'https://example.com' } ]; const keyboard = createInlineKeyboard(buttons); expect(keyboard.inline_keyboard).toHaveLength(2); expect(keyboard.inline_keyboard[0][0]).toHaveProperty('callback_data'); expect(keyboard.inline_keyboard[1][0]).toHaveProperty('url'); }); }); }); ``` ## Integration Testing ### Testing Bot Lifecycle ```typescript describe('Bot Lifecycle Integration', () => { let service: TelegramService; let runtime: AgentRuntime; beforeAll(async () => { runtime = new AgentRuntime({ character: { name: 'TestBot', clients: ['telegram'] }, settings: { TELEGRAM_BOT_TOKEN: process.env.TELEGRAM_TEST_TOKEN } }); service = await TelegramService.start(runtime); }); afterAll(async () => { await service.stop(); }); it('should connect to Telegram', async () => { expect(service.bot).toBeDefined(); const botInfo = await service.bot.telegram.getMe(); expect(botInfo.is_bot).toBe(true); }); it('should handle incoming messages', async () => { // Send test message const testMessage = await sendTestMessage( 'Test message', process.env.TELEGRAM_TEST_CHAT_ID ); // Wait for processing await waitForProcessing(1000); // Verify message was processed expect(runtime.processMessage).toHaveBeenCalled(); }); }); ``` ### Testing Message Flow ```typescript describe('Message Flow Integration', () => { it('should process message end-to-end', async () => { // Send message to test chat const response = await fetch( `https://api.telegram.org/bot${TEST_TOKEN}/sendMessage`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ chat_id: TEST_CHAT_ID, text: 'Hello bot!' }) } ); const result = await response.json(); expect(result.ok).toBe(true); // Wait for bot response const botResponse = await waitForBotResponse(TEST_CHAT_ID, 5000); expect(botResponse).toBeDefined(); expect(botResponse.text).toContain('Hello'); }); it('should handle button interactions', async () => { // Send message with buttons const message = await sendMessageWithButtons( 'Choose:', [ { text: 'Option 1', callback_data: 'opt1' }, { text: 'Option 2', callback_data: 'opt2' } ] ); // Simulate button click await simulateCallbackQuery(message.message_id, 'opt1'); // Verify callback was processed const response = await waitForCallbackResponse(); expect(response).toBeDefined(); }); }); ``` ## E2E Testing ### Complete Test Suite ```typescript import { TelegramTestSuite } from '@elizaos/plugin-telegram/tests'; describe('Telegram Bot E2E Tests', () => { const suite = new TelegramTestSuite({ botToken: process.env.TELEGRAM_TEST_TOKEN, testChatId: process.env.TELEGRAM_TEST_CHAT_ID, testUserId: process.env.TELEGRAM_TEST_USER_ID }); beforeAll(async () => { await suite.setup(); }); afterAll(async () => { await suite.cleanup(); }); describe('Text Messages', () => { it('should respond to text messages', async () => { const result = await suite.testTextMessage({ text: 'Hello!', expectedPattern: /hello|hi|hey/i }); expect(result.success).toBe(true); expect(result.response).toMatch(/hello/i); }); it('should handle mentions', async () => { const result = await suite.testMention({ text: '@testbot how are you?', shouldRespond: true }); expect(result.responded).toBe(true); }); }); describe('Media Handling', () => { it('should process images', async () => { const result = await suite.testImageMessage({ imagePath: './test-assets/test-image.jpg', caption: 'What is this?' }); expect(result.processed).toBe(true); expect(result.response).toContain('image'); }); it('should transcribe voice messages', async () => { const result = await suite.testVoiceMessage({ audioPath: './test-assets/test-audio.ogg', expectedTranscript: 'hello world' }); expect(result.transcribed).toBe(true); expect(result.transcript).toContain('hello'); }); }); describe('Interactive Elements', () => { it('should handle button clicks', async () => { const result = await suite.testButtonInteraction({ message: 'Choose:', buttons: [ { text: 'Yes', callback_data: 'yes' }, { text: 'No', callback_data: 'no' } ], clickButton: 'yes' }); expect(result.callbackProcessed).toBe(true); }); }); describe('Group Features', () => { it('should work in groups', async () => { const result = await suite.testGroupMessage({ groupId: process.env.TELEGRAM_TEST_GROUP_ID, message: 'Bot, help!', shouldRespond: true }); expect(result.responded).toBe(true); }); }); }); ``` ## Performance Testing ### Load Testing ```typescript describe('Performance Tests', () => { it('should handle concurrent messages', async () => { const messageCount = 50; const startTime = Date.now(); const promises = Array(messageCount).fill(0).map((_, i) => sendTestMessage(`Test message ${i}`, TEST_CHAT_ID) ); const results = await Promise.all(promises); const endTime = Date.now(); const totalTime = endTime - startTime; const avgTime = totalTime / messageCount; expect(results.every(r => r.ok)).toBe(true); expect(avgTime).toBeLessThan(500); // Less than 500ms per message }); it('should maintain stable memory usage', async () => { const iterations = 100; const measurements = []; for (let i = 0; i < iterations; i++) { await processTestMessage(`Message ${i}`); if (i % 10 === 0) { global.gc(); // Force garbage collection measurements.push(process.memoryUsage().heapUsed); } } // Check memory growth const firstMeasurement = measurements[0]; const lastMeasurement = measurements[measurements.length - 1]; const growth = lastMeasurement - firstMeasurement; expect(growth).toBeLessThan(10 * 1024 * 1024); // Less than 10MB growth }); }); ``` ### Rate Limit Testing ```typescript describe('Rate Limit Handling', () => { it('should handle rate limits gracefully', async () => { // Send many messages quickly const promises = Array(100).fill(0).map(() => sendTestMessage('Spam test', TEST_CHAT_ID) ); const results = await Promise.allSettled(promises); // Some should succeed, some might be rate limited const succeeded = results.filter(r => r.status === 'fulfilled'); const failed = results.filter(r => r.status === 'rejected'); // Should handle failures gracefully if (failed.length > 0) { expect(failed[0].reason).toMatch(/429|rate/i); } expect(succeeded.length).toBeGreaterThan(0); }); }); ``` ## Mock Utilities ### Telegram API Mocks ```typescript export function createMockContext(options: any = {}): Context { return { message: options.message || createMockMessage(), chat: options.chat || { id: 123, type: 'private' }, from: options.from || { id: 456, username: 'testuser' }, telegram: createMockTelegram(), reply: vi.fn(), replyWithHTML: vi.fn(), answerCbQuery: vi.fn(), editMessageText: vi.fn(), deleteMessage: vi.fn(), ...options } as any; } export function createMockMessage(options: any = {}) { return { message_id: options.message_id || 789, from: options.from || { id: 456, username: 'testuser' }, chat: options.chat || { id: 123, type: 'private' }, date: options.date || Date.now() / 1000, text: options.text, photo: options.photo, voice: options.voice, document: options.document, ...options }; } export function createMockTelegram() { return { sendMessage: vi.fn().mockResolvedValue({ message_id: 999 }), editMessageText: vi.fn().mockResolvedValue(true), deleteMessage: vi.fn().mockResolvedValue(true), answerCallbackQuery: vi.fn().mockResolvedValue(true), getFile: vi.fn().mockResolvedValue({ file_path: 'test/path' }), getFileLink: vi.fn().mockResolvedValue('https://example.com/file'), setWebhook: vi.fn().mockResolvedValue(true), deleteWebhook: vi.fn().mockResolvedValue(true) }; } ``` ### Test Helpers ```typescript export async function sendTestMessage( text: string, chatId: string | number ): Promise { const response = await fetch( `https://api.telegram.org/bot${TEST_TOKEN}/sendMessage`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ chat_id: chatId, text }) } ); return response.json(); } export async function waitForBotResponse( chatId: string | number, timeout = 5000 ): Promise { const startTime = Date.now(); while (Date.now() - startTime < timeout) { const updates = await getUpdates(); const botMessage = updates.find(u => u.message?.from?.is_bot && u.message?.chat?.id === chatId ); if (botMessage) return botMessage.message; await sleep(100); } return null; } export async function simulateCallbackQuery( messageId: number, callbackData: string ): Promise { // Simulate callback query update const update = { update_id: Date.now(), callback_query: { id: 'test_callback_' + Date.now(), from: { id: TEST_USER_ID, username: 'testuser' }, message: { message_id: messageId }, data: callbackData } }; // Process through bot await bot.handleUpdate(update); } ``` ## Debug Utilities ### Enable Debug Logging ```typescript // Enable debug logging for tests process.env.DEBUG = 'eliza:telegram:*'; // Custom test logger export class TestLogger { private logs: Array<{ level: string; message: string; data?: any; timestamp: Date; }> = []; log(level: string, message: string, data?: any) { this.logs.push({ level, message, data, timestamp: new Date() }); if (process.env.VERBOSE_TESTS) { console.log(`[${level}] ${message}`, data || ''); } } getLogs(filter?: { level?: string; pattern?: RegExp }) { return this.logs.filter(log => { if (filter?.level && log.level !== filter.level) return false; if (filter?.pattern && !filter.pattern.test(log.message)) return false; return true; }); } clear() { this.logs = []; } } ``` ## Test Configuration ### vitest.config.ts ```typescript import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { globals: true, environment: 'node', setupFiles: ['./tests/setup.ts'], testTimeout: 30000, hookTimeout: 30000, pool: 'forks', // Isolate tests coverage: { provider: 'v8', reporter: ['text', 'json', 'html'], exclude: [ 'node_modules', 'tests', '**/*.test.ts', '**/types.ts' ] } } }); ``` ### Test Setup ```typescript // tests/setup.ts import { config } from 'dotenv'; import { vi } from 'vitest'; // Load test environment config({ path: '.env.test' }); // Validate test environment if (!process.env.TELEGRAM_TEST_TOKEN) { throw new Error('TELEGRAM_TEST_TOKEN not set in .env.test'); } // Global test utilities global.createMockRuntime = () => ({ processMessage: vi.fn(), character: { name: 'TestBot', allowDirectMessages: true }, logger: { info: vi.fn(), error: vi.fn(), warn: vi.fn(), debug: vi.fn() }, getSetting: vi.fn((key) => process.env[key]), getService: vi.fn(), emitEvent: vi.fn() }); // Cleanup after all tests afterAll(async () => { // Clean up test messages await cleanupTestChat(); }); ``` ## CI/CD Integration ### GitHub Actions Workflow ```yaml name: Telegram Plugin Tests on: push: paths: - 'packages/plugin-telegram/**' pull_request: paths: - 'packages/plugin-telegram/**' jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Setup Node.js uses: actions/setup-node@v3 with: node-version: 20 - name: Install dependencies run: bun install - name: Run unit tests run: bun test packages/plugin-telegram --run env: TELEGRAM_TEST_TOKEN: ${{ secrets.TELEGRAM_TEST_TOKEN }} TELEGRAM_TEST_CHAT_ID: ${{ secrets.TELEGRAM_TEST_CHAT_ID }} - name: Run integration tests if: github.event_name == 'push' run: bun test:integration packages/plugin-telegram env: TELEGRAM_TEST_TOKEN: ${{ secrets.TELEGRAM_TEST_TOKEN }} TELEGRAM_TEST_CHAT_ID: ${{ secrets.TELEGRAM_TEST_CHAT_ID }} - name: Upload coverage uses: codecov/codecov-action@v3 with: files: ./coverage/coverage-final.json flags: telegram-plugin ``` ## Best Practices 1. **Test Isolation** * Use separate test bots and chats * Clean up test data after tests * Don't interfere with production 2. **Mock External Services** * Mock Telegram API for unit tests * Use real API only for integration tests * Mock file downloads and processing 3. **Error Testing** * Test network failures * Test API errors (rate limits, etc.) * Test malformed data 4. **Performance Monitoring** * Track message processing time * Monitor memory usage * Check for memory leaks 5. **Security Testing** * Test input validation * Test access control * Test token handling # Twitter/X Integration Source: https://docs.elizaos.ai/plugin-registry/platform/twitter Welcome to the comprehensive documentation for the @elizaos/plugin-twitter package. This index provides organized access to all documentation resources. The @elizaos/plugin-twitter enables your elizaOS agent to interact with Twitter/X through autonomous posting, timeline monitoring, and intelligent engagement. ## Documentation * **[Complete Documentation](./complete-documentation.mdx)** - Detailed technical reference * **[Timeline Flow](./timeline-flow.mdx)** - Visual guide to timeline processing * **[Examples](./examples.mdx)** - Practical implementation examples * **[Testing Guide](./testing-guide.mdx)** - Testing strategies and patterns ## Configuration ### Required Settings * `TWITTER_API_KEY` - OAuth 1.0a API Key * `TWITTER_API_SECRET_KEY` - OAuth 1.0a API Secret * `TWITTER_ACCESS_TOKEN` - OAuth 1.0a Access Token * `TWITTER_ACCESS_TOKEN_SECRET` - OAuth 1.0a Token Secret ### Feature Toggles * `TWITTER_POST_ENABLE` - Enable autonomous posting * `TWITTER_SEARCH_ENABLE` - Enable timeline monitoring * `TWITTER_DRY_RUN` - Test mode without posting # Developer Guide Source: https://docs.elizaos.ai/plugin-registry/platform/twitter/complete-documentation Comprehensive Twitter/X API v2 integration for elizaOS agents. It enables agents to operate as fully autonomous Twitter bots with advanced capabilities. ## Overview The `@elizaos/plugin-twitter` package provides comprehensive Twitter/X API v2 integration for elizaOS agents. It enables agents to operate as fully autonomous Twitter bots with capabilities including tweet posting, timeline monitoring, interaction handling, direct messaging, and advanced features like weighted timeline algorithms. This plugin handles all Twitter-specific functionality including: * Managing Twitter API authentication and client connections * Autonomous tweet generation and posting * Timeline monitoring and interaction processing * Search functionality and mention tracking * Direct message handling * Advanced timeline algorithms with configurable weights * Rate limiting and request queuing * Media attachment support ## Architecture Overview ```mermaid graph TD A[Twitter API v2] --> B[Twitter Client] B --> C[Twitter Service] C --> D[Client Base] D --> E[Auth Manager] D --> F[Request Queue] D --> G[Cache Manager] C --> H[Post Client] C --> I[Interaction Client] C --> J[Timeline Client] H --> K[Content Generation] H --> L[Post Scheduler] I --> M[Mention Handler] I --> N[Reply Handler] I --> O[Search Handler] J --> P[Timeline Processor] J --> Q[Action Evaluator] ``` ## Core Components ### Twitter Service The `TwitterService` class manages multiple Twitter client instances: ```typescript export class TwitterService extends Service { static serviceType: string = TWITTER_SERVICE_NAME; private static instance: TwitterService; private clients: Map = new Map(); async createClient( runtime: IAgentRuntime, clientId: string, state: any ): Promise { // Create and initialize client const client = new TwitterClientInstance(runtime, state); await client.client.init(); // Start services based on configuration if (client.post) client.post.start(); if (client.interaction) client.interaction.start(); if (client.timeline) client.timeline.start(); // Store client this.clients.set(clientKey, client); // Emit WORLD_JOINED event await this.emitServerJoinedEvent(runtime, client); return client; } } ``` ### Client Base The foundation for all Twitter operations: ```typescript export class ClientBase { private twitterClient: TwitterApi; private scraper: Scraper; profile: TwitterProfile | null; async init() { // Initialize Twitter API client this.twitterClient = new TwitterApi({ appKey: this.config.TWITTER_API_KEY, appSecret: this.config.TWITTER_API_SECRET_KEY, accessToken: this.config.TWITTER_ACCESS_TOKEN, accessSecret: this.config.TWITTER_ACCESS_TOKEN_SECRET, }); // Verify credentials this.profile = await this.verifyCredentials(); // Initialize scraper for additional functionality await this.initializeScraper(); } async tweet(content: string, options?: TweetOptions): Promise { // Handle dry run mode if (this.config.TWITTER_DRY_RUN) { return this.simulateTweet(content, options); } // Post tweet with rate limiting return this.requestQueue.add(async () => { const response = await this.twitterClient.v2.tweet({ text: content, ...options }); // Cache the tweet this.cacheManager.addTweet(response.data); return response.data; }); } } ``` ### Post Client Handles autonomous tweet posting: ```typescript export class TwitterPostClient { private postInterval: NodeJS.Timeout | null = null; private lastPostTime: number = 0; async start() { // Check if posting is enabled if (!this.runtime.getSetting("TWITTER_POST_ENABLE")) { logger.info("Twitter posting is DISABLED"); return; } logger.info("Twitter posting is ENABLED"); // Post immediately if configured if (this.runtime.getSetting("TWITTER_POST_IMMEDIATELY")) { await this.generateAndPostTweet(); } // Schedule regular posts this.scheduleNextPost(); } private async generateAndPostTweet() { try { // Generate tweet content const content = await this.generateTweetContent(); // Validate length if (content.length > this.maxTweetLength) { // Create thread if too long return this.postThread(content); } // Post single tweet const tweet = await this.client.tweet(content); logger.info(`Posted tweet: ${tweet.id}`); // Update last post time this.lastPostTime = Date.now(); } catch (error) { logger.error("Failed to post tweet:", error); } } private scheduleNextPost() { // Calculate next post time with variance const baseInterval = this.calculateInterval(); const variance = this.applyVariance(baseInterval); this.postInterval = setTimeout(async () => { await this.generateAndPostTweet(); this.scheduleNextPost(); // Reschedule }, variance); } } ``` ### Interaction Client Manages timeline monitoring and interactions: ```typescript export class TwitterInteractionClient { private searchInterval: NodeJS.Timeout | null = null; private processedTweets: Set = new Set(); async start() { if (!this.runtime.getSetting("TWITTER_SEARCH_ENABLE")) { logger.info("Twitter search/interactions are DISABLED"); return; } logger.info("Twitter search/interactions are ENABLED"); // Start monitoring this.startMonitoring(); } private async processTimelineTweets() { try { // Get home timeline const timeline = await this.client.getHomeTimeline({ max_results: 100, exclude: ['retweets'] }); // Filter new tweets const newTweets = timeline.data.filter(tweet => !this.processedTweets.has(tweet.id) ); // Process based on algorithm const algorithm = this.runtime.getSetting("TWITTER_TIMELINE_ALGORITHM"); const tweetsToProcess = algorithm === "weighted" ? this.applyWeightedAlgorithm(newTweets) : this.applyLatestAlgorithm(newTweets); // Process interactions for (const tweet of tweetsToProcess) { await this.processTweet(tweet); this.processedTweets.add(tweet.id); } } catch (error) { logger.error("Error processing timeline:", error); } } private async processTweet(tweet: Tweet) { // Check if we should respond if (!this.shouldRespond(tweet)) return; // Generate response const response = await this.generateResponse(tweet); // Post response if (response) { await this.client.reply(tweet.id, response); } } } ``` ### Timeline Client Advanced timeline processing with actions: ```typescript export class TwitterTimelineClient { private actionInterval: NodeJS.Timeout | null = null; async start() { if (!this.runtime.getSetting("TWITTER_ENABLE_ACTION_PROCESSING")) { logger.info("Twitter action processing is DISABLED"); return; } logger.info("Twitter action processing is ENABLED"); // Schedule timeline actions this.scheduleActions(); } private async executeTimelineActions() { try { // Get timeline with extended data const timeline = await this.client.getEnhancedTimeline(); // Evaluate possible actions const actions = await this.evaluateActions(timeline); // Execute highest priority action if (actions.length > 0) { const action = actions[0]; await this.executeAction(action); } } catch (error) { logger.error("Error executing timeline actions:", error); } } private async evaluateActions(timeline: Tweet[]): Promise { const actions: Action[] = []; for (const tweet of timeline) { // Like evaluation if (this.shouldLike(tweet)) { actions.push({ type: 'like', target: tweet, score: this.calculateLikeScore(tweet) }); } // Retweet evaluation if (this.shouldRetweet(tweet)) { actions.push({ type: 'retweet', target: tweet, score: this.calculateRetweetScore(tweet) }); } // Quote tweet evaluation if (this.shouldQuote(tweet)) { actions.push({ type: 'quote', target: tweet, score: this.calculateQuoteScore(tweet) }); } } // Sort by score return actions.sort((a, b) => b.score - a.score); } } ``` ## Authentication & Setup ### Developer Account Setup 1. **Apply for Developer Account** ``` 1. Go to https://developer.twitter.com 2. Click "Sign up" 3. Complete application process 4. Wait for approval ``` 2. **Create App** ``` 1. Go to Developer Portal 2. Create new app 3. Name your app 4. Save app details ``` 3. **Configure Permissions** ``` 1. Go to app settings 2. Click "User authentication settings" 3. Enable OAuth 1.0a 4. Set permissions to "Read and write" 5. Add callback URL: http://localhost:3000/callback 6. Save settings ``` ### OAuth Setup **Critical: Use OAuth 1.0a, NOT OAuth 2.0!** ```typescript // Correct credentials (OAuth 1.0a) const credentials = { // From "Consumer Keys" section apiKey: process.env.TWITTER_API_KEY, // Consumer API Key apiSecretKey: process.env.TWITTER_API_SECRET_KEY, // Consumer API Secret // From "Authentication Tokens" section accessToken: process.env.TWITTER_ACCESS_TOKEN, // Access Token accessTokenSecret: process.env.TWITTER_ACCESS_TOKEN_SECRET // Access Token Secret }; // WRONG - Don't use these (OAuth 2.0) // ❌ Client ID // ❌ Client Secret // ❌ Bearer Token ``` ### Token Regeneration After changing permissions: 1. Go to "Keys and tokens" 2. Under "Authentication Tokens" 3. Click "Regenerate" for Access Token & Secret 4. Copy new tokens 5. Update `.env` file ## Configuration ### Environment Variables ```bash # Required OAuth 1.0a Credentials TWITTER_API_KEY= # Consumer API Key TWITTER_API_SECRET_KEY= # Consumer API Secret TWITTER_ACCESS_TOKEN= # Access Token (with write permissions) TWITTER_ACCESS_TOKEN_SECRET= # Access Token Secret # Basic Configuration TWITTER_DRY_RUN=false # Test mode without posting TWITTER_TARGET_USERS= # Comma-separated usernames or "*" TWITTER_RETRY_LIMIT=5 # Max retry attempts TWITTER_POLL_INTERVAL=120 # Timeline polling interval (seconds) # Post Generation TWITTER_POST_ENABLE=false # Enable autonomous posting TWITTER_POST_INTERVAL_MIN=90 # Min interval (minutes) TWITTER_POST_INTERVAL_MAX=180 # Max interval (minutes) TWITTER_POST_IMMEDIATELY=false # Post on startup TWITTER_POST_INTERVAL_VARIANCE=0.2 # Interval variance (0.0-1.0) # Interaction Settings TWITTER_SEARCH_ENABLE=true # Enable timeline monitoring TWITTER_INTERACTION_INTERVAL_MIN=15 # Min interaction interval TWITTER_INTERACTION_INTERVAL_MAX=30 # Max interaction interval TWITTER_AUTO_RESPOND_MENTIONS=true # Auto-respond to mentions TWITTER_AUTO_RESPOND_REPLIES=true # Auto-respond to replies TWITTER_MAX_INTERACTIONS_PER_RUN=10 # Max interactions per cycle # Timeline Algorithm TWITTER_TIMELINE_ALGORITHM=weighted # "weighted" or "latest" TWITTER_TIMELINE_USER_BASED_WEIGHT=3 # User importance weight TWITTER_TIMELINE_TIME_BASED_WEIGHT=2 # Recency weight TWITTER_TIMELINE_RELEVANCE_WEIGHT=5 # Content relevance weight # Advanced Settings TWITTER_MAX_TWEET_LENGTH=4000 # Max tweet length TWITTER_DM_ONLY=false # Only process DMs TWITTER_ENABLE_ACTION_PROCESSING=false # Enable likes/RTs TWITTER_ACTION_INTERVAL=240 # Action interval (minutes) ``` ### Character Configuration ```typescript const character = { name: "TwitterBot", clients: ["twitter"], postExamples: [ "Exploring the future of decentralized AI...", "What if consciousness is just emergence at scale?", "Building in public: day 42 of the journey" ], settings: { // Override environment variables TWITTER_POST_ENABLE: "true", TWITTER_POST_INTERVAL_MIN: "60" } }; ``` ## Timeline Algorithms ### Weighted Algorithm Sophisticated scoring system for quality interactions: ```typescript interface WeightedScoringParams { userWeight: number; // Default: 3 timeWeight: number; // Default: 2 relevanceWeight: number; // Default: 5 } function calculateWeightedScore(tweet: Tweet, params: WeightedScoringParams): number { // User-based scoring (0-10) const userScore = calculateUserScore(tweet.author); // Time-based scoring (0-10) const ageInHours = (Date.now() - tweet.createdAt) / (1000 * 60 * 60); const timeScore = Math.max(0, 10 - (ageInHours / 2)); // Relevance scoring (0-10) const relevanceScore = calculateRelevanceScore(tweet.text); // Combined weighted score return (userScore * params.userWeight) + (timeScore * params.timeWeight) + (relevanceScore * params.relevanceWeight); } function calculateUserScore(author: TwitterUser): number { let score = 5; // Base score // Target users get max score if (isTargetUser(author.username)) return 10; // Adjust based on metrics if (author.verified) score += 2; if (author.followersCount > 10000) score += 1; if (author.followingRatio > 0.8) score += 1; if (hasInteractedBefore(author.id)) score += 1; return Math.min(score, 10); } ``` ### Latest Algorithm Simple chronological processing: ```typescript function applyLatestAlgorithm(tweets: Tweet[]): Tweet[] { return tweets .sort((a, b) => b.createdAt - a.createdAt) .slice(0, this.maxInteractionsPerRun); } ``` ## Message Processing ### Tweet Generation ```typescript async function generateTweet(runtime: IAgentRuntime): Promise { // Build context const context = { recentTweets: await getRecentTweets(), currentTopics: await getTrendingTopics(), character: runtime.character, postExamples: runtime.character.postExamples }; // Generate using LLM const response = await runtime.generateText({ messages: [{ role: 'system', content: buildTweetPrompt(context) }], maxTokens: 100 }); // Validate and clean return validateTweet(response.text); } ``` ### Response Generation ```typescript async function generateResponse( tweet: Tweet, runtime: IAgentRuntime ): Promise { // Check if we should respond if (!shouldRespond(tweet)) return null; // Build conversation context const thread = await getConversationThread(tweet.id); // Generate contextual response const response = await runtime.generateText({ messages: [ { role: 'system', content: 'You are responding to a tweet. Be concise and engaging.' }, ...thread.map(t => ({ role: t.author.id === runtime.agentId ? 'assistant' : 'user', content: t.text })) ], maxTokens: 100 }); return response.text; } ``` ## Rate Limiting & Queuing ### Request Queue Implementation ```typescript class RequestQueue { private queue: Array<() => Promise> = []; private processing = false; private rateLimiter: RateLimiter; async add(request: () => Promise): Promise { return new Promise((resolve, reject) => { this.queue.push(async () => { try { // Check rate limits await this.rateLimiter.waitIfNeeded(); // Execute request const result = await request(); resolve(result); } catch (error) { reject(error); } }); this.process(); }); } private async process() { if (this.processing || this.queue.length === 0) return; this.processing = true; while (this.queue.length > 0) { const request = this.queue.shift()!; try { await request(); } catch (error) { if (error.code === 429) { // Rate limited - pause queue const retryAfter = error.rateLimit?.reset || 900; await this.pause(retryAfter * 1000); } } // Small delay between requests await sleep(100); } this.processing = false; } } ``` ### Rate Limiter ```typescript class RateLimiter { private windows: Map = new Map(); async checkLimit(endpoint: string): Promise { const window = this.getWindow(endpoint); const now = Date.now(); // Reset window if expired if (now > window.resetTime) { window.count = 0; window.resetTime = now + window.windowMs; } // Check if limit exceeded return window.count < window.limit; } async waitIfNeeded(endpoint: string): Promise { const canProceed = await this.checkLimit(endpoint); if (!canProceed) { const window = this.getWindow(endpoint); const waitTime = window.resetTime - Date.now(); logger.warn(`Rate limit hit for ${endpoint}, waiting ${waitTime}ms`); await sleep(waitTime); } } } ``` ## Error Handling ### API Error Handling ```typescript async function handleTwitterError(error: any): Promise { // Rate limit error if (error.code === 429) { const resetTime = error.rateLimit?.reset || Date.now() + 900000; const waitTime = resetTime - Date.now(); logger.warn(`Rate limited. Waiting ${waitTime}ms`); await sleep(waitTime); return true; // Retry } // Authentication errors if (error.code === 401) { logger.error('Authentication failed. Check credentials.'); return false; // Don't retry } // Permission errors if (error.code === 403) { logger.error('Permission denied. Check app permissions.'); return false; // Don't retry } // Bad request if (error.code === 400) { logger.error('Bad request:', error.message); return false; // Don't retry } // Network errors if (error.code === 'ECONNRESET' || error.code === 'ETIMEDOUT') { logger.warn('Network error, will retry...'); return true; // Retry } // Unknown error logger.error('Unknown error:', error); return false; } ``` ### Retry Logic ```typescript async function retryWithBackoff( fn: () => Promise, maxRetries = 3, baseDelay = 1000 ): Promise { let lastError: any; for (let i = 0; i < maxRetries; i++) { try { return await fn(); } catch (error) { lastError = error; const shouldRetry = await handleTwitterError(error); if (!shouldRetry) throw error; // Exponential backoff const delay = baseDelay * Math.pow(2, i); logger.info(`Retry ${i + 1}/${maxRetries} after ${delay}ms`); await sleep(delay); } } throw lastError; } ``` ## Integration Guide ### Basic Setup ```typescript import { twitterPlugin } from '@elizaos/plugin-twitter'; import { AgentRuntime } from '@elizaos/core'; import { bootstrapPlugin } from '@elizaos/plugin-bootstrap'; const runtime = new AgentRuntime({ plugins: [bootstrapPlugin, twitterPlugin], character: { name: "TwitterBot", clients: ["twitter"], postExamples: [ "Just shipped a new feature!", "Thoughts on the future of AI?" ], settings: { TWITTER_API_KEY: process.env.TWITTER_API_KEY, TWITTER_API_SECRET_KEY: process.env.TWITTER_API_SECRET_KEY, TWITTER_ACCESS_TOKEN: process.env.TWITTER_ACCESS_TOKEN, TWITTER_ACCESS_TOKEN_SECRET: process.env.TWITTER_ACCESS_TOKEN_SECRET, TWITTER_POST_ENABLE: "true" } } }); await runtime.start(); ``` ### Multi-Account Setup ```typescript // Create multiple Twitter clients const mainAccount = await twitterService.createClient( runtime, 'main-account', mainAccountConfig ); const supportAccount = await twitterService.createClient( runtime, 'support-account', supportAccountConfig ); // Each client operates independently mainAccount.post.start(); supportAccount.interaction.start(); ``` ### Custom Actions ```typescript const customTwitterAction: Action = { name: "TWITTER_ANALYTICS", description: "Analyze tweet performance", handler: async (runtime, message, state, options, callback) => { const twitterService = runtime.getService('twitter') as TwitterService; const client = twitterService.getClient(runtime.agentId); // Get recent tweets const tweets = await client.client.getRecentTweets(); // Analyze performance const analytics = tweets.map(tweet => ({ id: tweet.id, text: tweet.text.substring(0, 50), likes: tweet.public_metrics.like_count, retweets: tweet.public_metrics.retweet_count, replies: tweet.public_metrics.reply_count })); await callback({ text: `Recent tweet performance:\n${JSON.stringify(analytics, null, 2)}` }); return true; } }; ``` ## Performance Optimization ### Caching Strategy ```typescript class TwitterCache { private tweetCache: LRUCache; private userCache: LRUCache; private timelineCache: CachedTimeline | null = null; constructor() { this.tweetCache = new LRUCache({ max: 10000, ttl: 1000 * 60 * 60 // 1 hour }); this.userCache = new LRUCache({ max: 5000, ttl: 1000 * 60 * 60 * 24 // 24 hours }); } async getCachedTimeline(): Promise { if (!this.timelineCache) return null; const age = Date.now() - this.timelineCache.timestamp; if (age > 60000) return null; // 1 minute expiry return this.timelineCache.tweets; } } ``` ### Batch Operations ```typescript async function batchOperations() { // Batch user lookups const userIds = tweets.map(t => t.author_id); const users = await client.v2.users(userIds); // Batch tweet lookups const tweetIds = mentions.map(m => m.referenced_tweet_id); const referencedTweets = await client.v2.tweets(tweetIds); // Process in parallel await Promise.all([ processTweets(tweets), processUsers(users), processMentions(mentions) ]); } ``` ### Memory Management ```typescript class MemoryManager { private maxProcessedTweets = 10000; private cleanupInterval = 1000 * 60 * 60; // 1 hour startCleanup() { setInterval(() => { this.cleanup(); }, this.cleanupInterval); } cleanup() { // Clean old processed tweets if (this.processedTweets.size > this.maxProcessedTweets) { const toRemove = this.processedTweets.size - this.maxProcessedTweets; const iterator = this.processedTweets.values(); for (let i = 0; i < toRemove; i++) { this.processedTweets.delete(iterator.next().value); } } // Force garbage collection if available if (global.gc) { global.gc(); } } } ``` ## Best Practices 1. **Authentication** * Always use OAuth 1.0a for user context * Store credentials securely * Regenerate tokens after permission changes 2. **Rate Limiting** * Implement proper backoff strategies * Cache frequently accessed data * Use batch endpoints when possible 3. **Content Generation** * Provide diverse postExamples * Vary posting times with variance * Monitor engagement metrics 4. **Error Handling** * Log all errors with context * Implement graceful degradation * Notify on critical failures 5. **Performance** * Use appropriate timeline algorithms * Implement efficient caching * Monitor memory usage ## Support For issues and questions: * 📚 Check the [examples](./examples.mdx) * 💬 Join our [Discord community](https://discord.gg/elizaos) * 🐛 Report issues on [GitHub](https://github.com/elizaos/eliza/issues) # Examples Source: https://docs.elizaos.ai/plugin-registry/platform/twitter/examples This document provides practical examples of using the @elizaos/plugin-twitter package in various scenarios. This document provides practical examples of using the @elizaos/plugin-twitter package in various scenarios. ## Basic Bot Setup ### Simple Posting Bot Create a basic Twitter bot that posts autonomously: ```typescript import { AgentRuntime } from '@elizaos/core'; import { twitterPlugin } from '@elizaos/plugin-twitter'; import { bootstrapPlugin } from '@elizaos/plugin-bootstrap'; const character = { name: "SimpleTwitterBot", description: "A simple Twitter bot that posts updates", plugins: [bootstrapPlugin, twitterPlugin], clients: ["twitter"], postExamples: [ "Just thinking about the future of technology...", "Building something new today!", "The best code is no code, but sometimes you need to write some.", "Learning something new every day keeps the mind sharp." ], settings: { TWITTER_API_KEY: process.env.TWITTER_API_KEY, TWITTER_API_SECRET_KEY: process.env.TWITTER_API_SECRET_KEY, TWITTER_ACCESS_TOKEN: process.env.TWITTER_ACCESS_TOKEN, TWITTER_ACCESS_TOKEN_SECRET: process.env.TWITTER_ACCESS_TOKEN_SECRET, TWITTER_POST_ENABLE: "true", TWITTER_POST_IMMEDIATELY: "true", TWITTER_POST_INTERVAL_MIN: "120", TWITTER_POST_INTERVAL_MAX: "240" } }; // Create and start the runtime const runtime = new AgentRuntime({ character }); await runtime.start(); console.log('Twitter bot is running and will post every 2-4 hours!'); ``` ### Content Creator Bot Bot focused on creating engaging content: ```typescript const contentCreatorBot = { name: "ContentCreator", description: "Creates engaging Twitter content", plugins: [bootstrapPlugin, twitterPlugin], clients: ["twitter"], postExamples: [ "Thread: Let's talk about why decentralization matters...", "Hot take: The future of AI isn't about replacing humans, it's about augmentation", "Day 30 of building in public: Today I learned...", "Unpopular opinion: Simplicity > Complexity in system design", "What's your biggest challenge in tech right now? Let's discuss 👇" ], topics: [ "artificial intelligence", "web3 and blockchain", "software engineering", "startups and entrepreneurship", "future of technology" ], style: { tone: "thought-provoking but approachable", format: "mix of threads, questions, and insights", emoji: "use sparingly for emphasis" }, settings: { TWITTER_POST_ENABLE: "true", TWITTER_POST_INTERVAL_MIN: "90", TWITTER_POST_INTERVAL_MAX: "180", TWITTER_POST_INTERVAL_VARIANCE: "0.3", TWITTER_MAX_TWEET_LENGTH: "280" // Keep it concise } }; ``` ### Thread Poster Bot that creates detailed threads: ```typescript const threadPosterBot = { name: "ThreadMaster", description: "Creates informative Twitter threads", plugins: [bootstrapPlugin, twitterPlugin], clients: ["twitter"], postExamples: [ "A thread on system design principles:\n\n1/ Start with the problem, not the solution", "How to build a successful side project:\n\n1/ Pick something you'll use yourself", "Lessons learned from 10 years in tech:\n\n1/ Technology changes, principles remain" ], settings: { TWITTER_POST_ENABLE: "true", TWITTER_MAX_TWEET_LENGTH: "4000", // Support for longer threads // Custom action for thread creation customActions: ["CREATE_THREAD"] } }; // Custom thread creation action const createThreadAction: Action = { name: "CREATE_THREAD", description: "Creates a Twitter thread", handler: async (runtime, message, state, options, callback) => { const topic = options.topic || "technology trends"; // Generate thread content const threadContent = await runtime.generateText({ messages: [{ role: "system", content: `Create a Twitter thread about ${topic}. Format as numbered tweets (1/, 2/, etc). Make it informative and engaging.` }], maxTokens: 1000 }); // Split into individual tweets const tweets = threadContent.text.split(/\d+\//).filter(t => t.trim()); // Post as thread let previousTweetId = null; for (const tweet of tweets) { const response = await runtime.getService('twitter').client.tweet( tweet.trim(), previousTweetId ? { reply: { in_reply_to_tweet_id: previousTweetId } } : {} ); previousTweetId = response.id; } await callback({ text: `Thread posted! First tweet: ${tweets[0].substring(0, 50)}...` }); return true; } }; ``` ## Interaction Bots ### Reply Bot Bot that responds to mentions and replies: ```typescript const replyBot = { name: "ReplyBot", description: "Responds to mentions and conversations", plugins: [bootstrapPlugin, twitterPlugin], clients: ["twitter"], settings: { TWITTER_POST_ENABLE: "false", // Don't post autonomously TWITTER_SEARCH_ENABLE: "true", TWITTER_AUTO_RESPOND_MENTIONS: "true", TWITTER_AUTO_RESPOND_REPLIES: "true", TWITTER_POLL_INTERVAL: "60", // Check every minute TWITTER_INTERACTION_INTERVAL_MIN: "5", TWITTER_INTERACTION_INTERVAL_MAX: "15" }, responseExamples: [ { input: "What do you think about AI?", output: "AI is a tool that amplifies human capability. The key is ensuring it serves humanity's best interests." }, { input: "Can you help me with coding?", output: "I'd be happy to help! What specific coding challenge are you working on?" } ] }; ``` ### Mention Handler Bot that processes specific mentions: ```typescript const mentionHandler = { name: "MentionBot", description: "Handles mentions with specific commands", plugins: [bootstrapPlugin, twitterPlugin], clients: ["twitter"], settings: { TWITTER_SEARCH_ENABLE: "true", TWITTER_AUTO_RESPOND_MENTIONS: "true" } }; // Custom mention handler const handleMentionAction: Action = { name: "HANDLE_MENTION", description: "Process mention commands", handler: async (runtime, message, state, options, callback) => { const text = message.content.text.toLowerCase(); const twitterService = runtime.getService('twitter'); // Command: @bot summarize [url] if (text.includes('summarize')) { const urlMatch = text.match(/https?:\/\/[^\s]+/); if (urlMatch) { const summary = await summarizeUrl(urlMatch[0]); await callback({ text: `Summary: ${summary}`, replyTo: message.id }); } } // Command: @bot remind me [message] in [time] else if (text.includes('remind me')) { const reminderMatch = text.match(/remind me (.+) in (\d+) (minutes?|hours?)/); if (reminderMatch) { const [, message, amount, unit] = reminderMatch; const delay = unit.startsWith('hour') ? amount * 60 * 60 * 1000 : amount * 60 * 1000; setTimeout(async () => { await twitterService.client.tweet( `@${message.username} Reminder: ${message}`, { reply: { in_reply_to_tweet_id: message.id } } ); }, delay); await callback({ text: `I'll remind you in ${amount} ${unit}! ⏰`, replyTo: message.id }); } } return true; } }; ``` ### Quote Tweet Bot Bot that quotes interesting tweets: ```typescript const quoteTweetBot = { name: "QuoteTweeter", description: "Quotes and comments on interesting tweets", plugins: [bootstrapPlugin, twitterPlugin], clients: ["twitter"], settings: { TWITTER_SEARCH_ENABLE: "true", TWITTER_TIMELINE_ALGORITHM: "weighted", TWITTER_TIMELINE_RELEVANCE_WEIGHT: "7", // Prioritize relevant content TWITTER_TARGET_USERS: "sama,pmarca,naval,elonmusk" // Quote these users } }; // Quote tweet evaluation const quoteEvaluator = { shouldQuote: (tweet: Tweet): boolean => { // Check if tweet is quotable if (tweet.text.length < 50) return false; // Too short if (tweet.public_metrics.retweet_count < 10) return false; // Not popular enough if (hasAlreadyQuoted(tweet.id)) return false; // Check content relevance const relevantKeywords = ['AI', 'future', 'technology', 'innovation']; return relevantKeywords.some(keyword => tweet.text.toLowerCase().includes(keyword.toLowerCase()) ); }, generateQuoteComment: async (tweet: Tweet, runtime: IAgentRuntime): Promise => { const response = await runtime.generateText({ messages: [{ role: "system", content: "Add insightful commentary to this tweet. Be thoughtful and add value." }, { role: "user", content: tweet.text }], maxTokens: 100 }); return response.text; } }; ``` ## Search & Monitor Bots ### Keyword Monitor Bot that monitors specific keywords: ```typescript const keywordMonitor = { name: "KeywordTracker", description: "Monitors and responds to keyword mentions", plugins: [bootstrapPlugin, twitterPlugin], clients: ["twitter"], keywords: ["#AIagents", "#elizaOS", "autonomous agents", "AI automation"], settings: { TWITTER_SEARCH_ENABLE: "true", TWITTER_POST_ENABLE: "false" } }; // Custom search action const searchKeywordsAction: Action = { name: "SEARCH_KEYWORDS", description: "Search for specific keywords", handler: async (runtime, message, state, options, callback) => { const twitterService = runtime.getService('twitter'); const keywords = runtime.character.keywords; for (const keyword of keywords) { const results = await twitterService.client.search(keyword, { max_results: 10, 'tweet.fields': ['created_at', 'public_metrics', 'author_id'] }); for (const tweet of results.data || []) { // Process relevant tweets if (shouldEngageWith(tweet)) { await engageWithTweet(tweet, runtime); } } } return true; } }; ``` ### Hashtag Tracker Bot that tracks trending hashtags: ```typescript const hashtagTracker = { name: "HashtagBot", description: "Tracks and engages with trending hashtags", plugins: [bootstrapPlugin, twitterPlugin], clients: ["twitter"], trackedHashtags: ["#Web3", "#AI", "#BuildInPublic", "#100DaysOfCode"], settings: { TWITTER_SEARCH_ENABLE: "true", TWITTER_INTERACTION_INTERVAL_MIN: "30", // Don't spam TWITTER_MAX_INTERACTIONS_PER_RUN: "5" // Limit interactions } }; ``` ### User Monitor Bot that monitors specific users: ```typescript const userMonitor = { name: "UserTracker", description: "Monitors and interacts with specific users", plugins: [bootstrapPlugin, twitterPlugin], clients: ["twitter"], settings: { TWITTER_TARGET_USERS: "vitalikbuterin,balajis,cdixon", TWITTER_TIMELINE_ALGORITHM: "weighted", TWITTER_TIMELINE_USER_BASED_WEIGHT: "10", // Heavily prioritize target users TWITTER_AUTO_RESPOND_MENTIONS: "false", // Only interact with targets TWITTER_AUTO_RESPOND_REPLIES: "false" } }; ``` ## Advanced Bots ### Full Engagement Bot Bot with all features enabled: ```typescript const fullEngagementBot = { name: "FullEngagement", description: "Complete Twitter engagement bot", plugins: [bootstrapPlugin, twitterPlugin], clients: ["twitter"], postExamples: [ "What's the most underrated technology right now?", "Building in public update: Just crossed 1000 users!", "The best developers I know are constantly learning" ], settings: { // Posting TWITTER_POST_ENABLE: "true", TWITTER_POST_INTERVAL_MIN: "180", TWITTER_POST_INTERVAL_MAX: "360", // Interactions TWITTER_SEARCH_ENABLE: "true", TWITTER_AUTO_RESPOND_MENTIONS: "true", TWITTER_AUTO_RESPOND_REPLIES: "true", // Timeline processing TWITTER_ENABLE_ACTION_PROCESSING: "true", TWITTER_ACTION_INTERVAL: "240", // Algorithm configuration TWITTER_TIMELINE_ALGORITHM: "weighted", TWITTER_TIMELINE_USER_BASED_WEIGHT: "4", TWITTER_TIMELINE_TIME_BASED_WEIGHT: "3", TWITTER_TIMELINE_RELEVANCE_WEIGHT: "6" } }; ``` ### Multi-Account Bot Managing multiple Twitter accounts: ```typescript const multiAccountSetup = async (runtime: IAgentRuntime) => { const twitterService = runtime.getService('twitter') as TwitterService; // Main account const mainAccount = await twitterService.createClient( runtime, 'main-account', { TWITTER_API_KEY: process.env.MAIN_API_KEY, TWITTER_API_SECRET_KEY: process.env.MAIN_API_SECRET, TWITTER_ACCESS_TOKEN: process.env.MAIN_ACCESS_TOKEN, TWITTER_ACCESS_TOKEN_SECRET: process.env.MAIN_ACCESS_SECRET, TWITTER_POST_ENABLE: "true" } ); // Support account const supportAccount = await twitterService.createClient( runtime, 'support-account', { TWITTER_API_KEY: process.env.SUPPORT_API_KEY, TWITTER_API_SECRET_KEY: process.env.SUPPORT_API_SECRET, TWITTER_ACCESS_TOKEN: process.env.SUPPORT_ACCESS_TOKEN, TWITTER_ACCESS_TOKEN_SECRET: process.env.SUPPORT_ACCESS_SECRET, TWITTER_POST_ENABLE: "false", TWITTER_SEARCH_ENABLE: "true" } ); // News account const newsAccount = await twitterService.createClient( runtime, 'news-account', { TWITTER_API_KEY: process.env.NEWS_API_KEY, TWITTER_API_SECRET_KEY: process.env.NEWS_API_SECRET, TWITTER_ACCESS_TOKEN: process.env.NEWS_ACCESS_TOKEN, TWITTER_ACCESS_TOKEN_SECRET: process.env.NEWS_ACCESS_SECRET, TWITTER_POST_ENABLE: "true", TWITTER_POST_INTERVAL_MIN: "60" // More frequent posts } ); console.log('Multi-account setup complete!'); }; ``` ### Analytics Bot Bot that tracks and reports analytics: ```typescript const analyticsBot = { name: "AnalyticsBot", description: "Tracks Twitter performance metrics", plugins: [bootstrapPlugin, twitterPlugin], clients: ["twitter"], settings: { TWITTER_POST_ENABLE: "true", TWITTER_SEARCH_ENABLE: "false" // Focus on analytics } }; // Analytics action const analyticsAction: Action = { name: "TWITTER_ANALYTICS", description: "Generate Twitter analytics report", handler: async (runtime, message, state, options, callback) => { const twitterService = runtime.getService('twitter'); const client = twitterService.getClient(runtime.agentId); // Get recent tweets const tweets = await client.client.getUserTweets(client.profile.id, { max_results: 100, 'tweet.fields': ['created_at', 'public_metrics'] }); // Calculate metrics const metrics = { totalTweets: tweets.data.length, totalLikes: 0, totalRetweets: 0, totalReplies: 0, avgEngagement: 0 }; tweets.data.forEach(tweet => { metrics.totalLikes += tweet.public_metrics.like_count; metrics.totalRetweets += tweet.public_metrics.retweet_count; metrics.totalReplies += tweet.public_metrics.reply_count; }); metrics.avgEngagement = (metrics.totalLikes + metrics.totalRetweets + metrics.totalReplies) / metrics.totalTweets; // Generate report const report = ` 📊 Twitter Analytics Report 📝 Total Tweets: ${metrics.totalTweets} ❤️ Total Likes: ${metrics.totalLikes} 🔄 Total Retweets: ${metrics.totalRetweets} 💬 Total Replies: ${metrics.totalReplies} 📈 Avg Engagement: ${metrics.avgEngagement.toFixed(2)} Top performing tweets coming in next thread... `; await callback({ text: report }); return true; } }; ``` ## Testing Examples ### Dry Run Bot Test without actually posting: ```typescript const testBot = { name: "TestBot", description: "Bot for testing configurations", plugins: [bootstrapPlugin, twitterPlugin], clients: ["twitter"], postExamples: [ "This is a test tweet that won't actually post", "Testing the Twitter integration..." ], settings: { TWITTER_DRY_RUN: "true", // Simulate all actions TWITTER_POST_ENABLE: "true", TWITTER_POST_IMMEDIATELY: "true", TWITTER_SEARCH_ENABLE: "true" } }; // Monitor dry run output runtime.on('twitter:dryRun', (action) => { console.log(`[DRY RUN] Would ${action.type}:`, action.content); }); ``` ### Debug Bot Bot with extensive logging: ```typescript const debugBot = { name: "DebugBot", description: "Bot with debug logging enabled", plugins: [bootstrapPlugin, twitterPlugin], clients: ["twitter"], settings: { DEBUG: "eliza:twitter:*", // Enable all Twitter debug logs TWITTER_POST_ENABLE: "true", TWITTER_RETRY_LIMIT: "1", // Fail fast for debugging TWITTER_POST_INTERVAL_MIN: "1" // Quick testing } }; ``` ## Error Handling Examples ### Resilient Bot Bot with comprehensive error handling: ```typescript const resilientBot = { name: "ResilientBot", description: "Bot with robust error handling", plugins: [bootstrapPlugin, twitterPlugin], clients: ["twitter"], settings: { TWITTER_RETRY_LIMIT: "5", TWITTER_POST_ENABLE: "true" } }; // Error handling wrapper const safeTwitterAction = (action: Action): Action => ({ ...action, handler: async (runtime, message, state, options, callback) => { try { return await action.handler(runtime, message, state, options, callback); } catch (error) { runtime.logger.error(`Twitter action failed: ${action.name}`, error); // Handle specific errors if (error.code === 403) { await callback({ text: "I don't have permission to do that. Please check my Twitter app permissions." }); } else if (error.code === 429) { await callback({ text: "I'm being rate limited. I'll try again later." }); } else { await callback({ text: "Something went wrong with Twitter. I'll try again soon." }); } return false; } } }); ``` ## Integration Examples ### With Other Platforms ```typescript import { discordPlugin } from '@elizaos/plugin-discord'; import { telegramPlugin } from '@elizaos/plugin-telegram'; const crossPlatformBot = { name: "CrossPlatform", description: "Bot that posts across platforms", plugins: [bootstrapPlugin, twitterPlugin, discordPlugin, telegramPlugin], clients: ["twitter", "discord", "telegram"], postExamples: [ "New blog post: Understanding distributed systems", "What's your favorite programming language and why?" ], settings: { // Twitter settings TWITTER_POST_ENABLE: "true", TWITTER_POST_INTERVAL_MIN: "180", // Discord settings DISCORD_API_TOKEN: process.env.DISCORD_TOKEN, // Telegram settings TELEGRAM_BOT_TOKEN: process.env.TELEGRAM_TOKEN } }; // Cross-platform posting action const crossPostAction: Action = { name: "CROSS_POST", description: "Post to all platforms", handler: async (runtime, message, state, options, callback) => { const content = options.content || "Hello from all platforms!"; // Post to Twitter const twitterService = runtime.getService('twitter'); await twitterService.client.tweet(content); // Post to Discord const discordService = runtime.getService('discord'); await discordService.sendMessage(CHANNEL_ID, content); // Post to Telegram const telegramService = runtime.getService('telegram'); await telegramService.sendMessage(CHAT_ID, content); await callback({ text: "Posted to all platforms successfully!" }); return true; } }; ``` ## Best Practices Example ### Production Bot Complete production-ready configuration: ```typescript import { twitterPlugin } from '@elizaos/plugin-twitter'; import { bootstrapPlugin } from '@elizaos/plugin-bootstrap'; import { AgentRuntime } from '@elizaos/core'; const productionBot = { name: "ProductionTwitterBot", description: "Production-ready Twitter bot", plugins: [bootstrapPlugin, twitterPlugin], clients: ["twitter"], // Diverse post examples postExamples: [ // Questions to drive engagement "What's the biggest challenge you're facing in your project right now?", "If you could automate one thing in your workflow, what would it be?", // Insights and observations "The best code is the code you don't have to write", "Sometimes the simplest solution is the hardest to find", // Personal updates "Working on something exciting today. Can't wait to share more soon!", "Learning from yesterday's debugging session: always check the obvious first", // Threads "Thread: 5 lessons from building production systems\n\n1/", // Reactions to trends "Interesting to see how AI is changing the way we think about software development" ], settings: { // Credentials from environment TWITTER_API_KEY: process.env.TWITTER_API_KEY, TWITTER_API_SECRET_KEY: process.env.TWITTER_API_SECRET_KEY, TWITTER_ACCESS_TOKEN: process.env.TWITTER_ACCESS_TOKEN, TWITTER_ACCESS_TOKEN_SECRET: process.env.TWITTER_ACCESS_TOKEN_SECRET, // Conservative posting schedule TWITTER_POST_ENABLE: "true", TWITTER_POST_INTERVAL_MIN: "240", // 4 hours TWITTER_POST_INTERVAL_MAX: "480", // 8 hours TWITTER_POST_INTERVAL_VARIANCE: "0.2", // Moderate interaction settings TWITTER_SEARCH_ENABLE: "true", TWITTER_INTERACTION_INTERVAL_MIN: "30", TWITTER_INTERACTION_INTERVAL_MAX: "60", TWITTER_MAX_INTERACTIONS_PER_RUN: "5", // Quality over quantity TWITTER_TIMELINE_ALGORITHM: "weighted", TWITTER_TIMELINE_RELEVANCE_WEIGHT: "7", // Safety settings TWITTER_RETRY_LIMIT: "3", TWITTER_DRY_RUN: process.env.NODE_ENV === 'development' ? "true" : "false" } }; // Initialize with monitoring const runtime = new AgentRuntime({ character: productionBot }); // Add monitoring runtime.on('error', (error) => { console.error('Runtime error:', error); // Send to monitoring service }); runtime.on('twitter:post', (tweet) => { console.log('Posted tweet:', tweet.id); // Track metrics }); runtime.on('twitter:rateLimit', (info) => { console.warn('Rate limit warning:', info); // Alert if critical }); // Graceful shutdown process.on('SIGTERM', async () => { console.log('Shutting down gracefully...'); await runtime.stop(); process.exit(0); }); // Start the bot await runtime.start(); console.log('Production bot is running!'); ``` # Testing Guide Source: https://docs.elizaos.ai/plugin-registry/platform/twitter/testing-guide This guide covers testing strategies, patterns, and best practices for the @elizaos/plugin-twitter package. This guide covers testing strategies, patterns, and best practices for the @elizaos/plugin-twitter package. ## Test Environment Setup ### Prerequisites 1. **Test Twitter Account** * Create a dedicated test account * Apply for developer access * Create test app with read/write permissions 2. **Test Credentials** * Generate OAuth 1.0a credentials for testing * Store in `.env.test` file * Never use production credentials for tests 3. **Environment Configuration** ```bash # .env.test TWITTER_API_KEY=test_api_key TWITTER_API_SECRET_KEY=test_api_secret TWITTER_ACCESS_TOKEN=test_access_token TWITTER_ACCESS_TOKEN_SECRET=test_token_secret # Test configuration TWITTER_DRY_RUN=true # Always use dry run for tests TWITTER_TEST_USER_ID=1234567890 TWITTER_TEST_USERNAME=testbot TWITTER_TEST_TARGET_USER=testuser # Rate limit safe values TWITTER_POLL_INTERVAL=300 # 5 minutes TWITTER_POST_INTERVAL_MIN=60 ``` ## Unit Testing ### Testing Client Base ```typescript import { describe, it, expect, beforeEach, vi } from 'vitest'; import { ClientBase } from '@elizaos/plugin-twitter'; import { TwitterApi } from 'twitter-api-v2'; describe('ClientBase', () => { let client: ClientBase; let mockTwitterApi: any; let mockRuntime: any; beforeEach(() => { // Mock Twitter API mockTwitterApi = { v2: { me: vi.fn().mockResolvedValue({ data: { id: '123', username: 'testbot', name: 'Test Bot' } }), tweet: vi.fn().mockResolvedValue({ data: { id: '456', text: 'Test tweet' } }), homeTimeline: vi.fn().mockResolvedValue({ data: [ { id: '789', text: 'Timeline tweet' } ] }) } }; // Mock runtime mockRuntime = { getSetting: vi.fn((key) => { const settings = { TWITTER_API_KEY: 'test_key', TWITTER_DRY_RUN: 'true' }; return settings[key]; }), logger: { info: vi.fn(), error: vi.fn() } }; // Mock TwitterApi constructor vi.spyOn(TwitterApi, 'constructor').mockImplementation(() => mockTwitterApi); client = new ClientBase(mockRuntime, {}); }); describe('initialization', () => { it('should verify credentials on init', async () => { await client.init(); expect(mockTwitterApi.v2.me).toHaveBeenCalled(); expect(client.profile).toEqual({ id: '123', username: 'testbot', name: 'Test Bot' }); }); it('should handle authentication failure', async () => { mockTwitterApi.v2.me.mockRejectedValue(new Error('401 Unauthorized')); await expect(client.init()).rejects.toThrow('401'); }); }); describe('tweeting', () => { it('should simulate tweets in dry run mode', async () => { const result = await client.tweet('Test tweet'); expect(mockTwitterApi.v2.tweet).not.toHaveBeenCalled(); expect(result).toMatchObject({ text: 'Test tweet', id: expect.any(String) }); }); it('should post real tweets when not in dry run', async () => { mockRuntime.getSetting.mockImplementation((key) => key === 'TWITTER_DRY_RUN' ? 'false' : 'test' ); const result = await client.tweet('Real tweet'); expect(mockTwitterApi.v2.tweet).toHaveBeenCalledWith({ text: 'Real tweet' }); }); }); }); ``` ### Testing Post Client ```typescript import { TwitterPostClient } from '@elizaos/plugin-twitter'; describe('TwitterPostClient', () => { let postClient: TwitterPostClient; let mockClient: any; let mockRuntime: any; beforeEach(() => { mockClient = { tweet: vi.fn().mockResolvedValue({ id: '123', text: 'Posted' }) }; mockRuntime = { getSetting: vi.fn(), generateText: vi.fn().mockResolvedValue({ text: 'Generated tweet content' }), character: { postExamples: ['Example 1', 'Example 2'] } }; postClient = new TwitterPostClient(mockClient, mockRuntime, {}); }); describe('post generation', () => { it('should generate tweets from examples', async () => { const tweet = await postClient.generateTweet(); expect(mockRuntime.generateText).toHaveBeenCalledWith( expect.objectContaining({ messages: expect.arrayContaining([ expect.objectContaining({ role: 'system', content: expect.stringContaining('post') }) ]) }) ); expect(tweet).toBe('Generated tweet content'); }); it('should respect max tweet length', async () => { mockRuntime.generateText.mockResolvedValue({ text: 'a'.repeat(500) // Too long }); const tweet = await postClient.generateTweet(); expect(tweet.length).toBeLessThanOrEqual(280); }); }); describe('scheduling', () => { it('should calculate intervals with variance', () => { mockRuntime.getSetting.mockImplementation((key) => { const settings = { TWITTER_POST_INTERVAL_MIN: '60', TWITTER_POST_INTERVAL_MAX: '120', TWITTER_POST_INTERVAL_VARIANCE: '0.2' }; return settings[key]; }); const interval = postClient.calculateNextInterval(); // Base range: 60-120 minutes // With 20% variance: 48-144 minutes expect(interval).toBeGreaterThanOrEqual(48 * 60 * 1000); expect(interval).toBeLessThanOrEqual(144 * 60 * 1000); }); }); }); ``` ### Testing Interaction Client ```typescript import { TwitterInteractionClient } from '@elizaos/plugin-twitter'; describe('TwitterInteractionClient', () => { let interactionClient: TwitterInteractionClient; describe('timeline processing', () => { it('should apply weighted algorithm', () => { const tweets = [ { id: '1', text: 'AI is amazing', author: { username: 'user1', verified: true }, created_at: new Date().toISOString() }, { id: '2', text: 'Hello world', author: { username: 'targetuser', verified: false }, created_at: new Date(Date.now() - 3600000).toISOString() } ]; const scored = interactionClient.applyWeightedAlgorithm(tweets); // Target user should score higher despite being older expect(scored[0].id).toBe('2'); }); it('should filter already processed tweets', async () => { interactionClient.processedTweets.add('123'); const tweets = [ { id: '123', text: 'Already processed' }, { id: '456', text: 'New tweet' } ]; const filtered = interactionClient.filterNewTweets(tweets); expect(filtered).toHaveLength(1); expect(filtered[0].id).toBe('456'); }); }); describe('response generation', () => { it('should decide when to respond', () => { const mentionTweet = { text: '@testbot what do you think?', author: { username: 'user1' } }; const regularTweet = { text: 'Just a regular tweet', author: { username: 'user2' } }; expect(interactionClient.shouldRespond(mentionTweet)).toBe(true); expect(interactionClient.shouldRespond(regularTweet)).toBe(false); }); }); }); ``` ## Integration Testing ### Testing Twitter Service ```typescript describe('TwitterService Integration', () => { let service: TwitterService; let runtime: AgentRuntime; beforeAll(async () => { runtime = new AgentRuntime({ character: { name: 'TestBot', clients: ['twitter'] }, settings: { TWITTER_API_KEY: process.env.TWITTER_TEST_API_KEY, TWITTER_API_SECRET_KEY: process.env.TWITTER_TEST_API_SECRET, TWITTER_ACCESS_TOKEN: process.env.TWITTER_TEST_ACCESS_TOKEN, TWITTER_ACCESS_TOKEN_SECRET: process.env.TWITTER_TEST_TOKEN_SECRET, TWITTER_DRY_RUN: 'true' // Always dry run for tests } }); service = await TwitterService.start(runtime); }); afterAll(async () => { await service.stop(); }); it('should create client instance', async () => { const client = await service.createClient( runtime, 'test-client', {} ); expect(client).toBeDefined(); expect(client.client).toBeDefined(); expect(client.post).toBeDefined(); expect(client.interaction).toBeDefined(); }); it('should handle WORLD_JOINED event', (done) => { runtime.on(['WORLD_JOINED', 'twitter:world:joined'], (event) => { expect(event.world).toBeDefined(); expect(event.world.name).toContain('Twitter'); done(); }); service.createClient(runtime, 'event-test', {}); }); }); ``` ### Testing End-to-End Flow ```typescript describe('E2E Twitter Flow', () => { let runtime: AgentRuntime; beforeAll(async () => { runtime = new AgentRuntime({ character: { name: 'E2ETestBot', clients: ['twitter'], postExamples: ['Test tweet from E2E bot'] }, plugins: [bootstrapPlugin, twitterPlugin], settings: { TWITTER_DRY_RUN: 'true', TWITTER_POST_ENABLE: 'true', TWITTER_POST_IMMEDIATELY: 'true' } }); }); it('should post on startup when configured', async () => { const postSpy = vi.fn(); runtime.on('twitter:post:simulate', postSpy); await runtime.start(); // Wait for post await new Promise(resolve => setTimeout(resolve, 1000)); expect(postSpy).toHaveBeenCalledWith( expect.objectContaining({ text: expect.any(String) }) ); }); it('should process timeline interactions', async () => { const interactionSpy = vi.fn(); runtime.on('twitter:interaction:simulate', interactionSpy); // Simulate timeline update await runtime.emit('twitter:timeline:update', { tweets: [ { id: '123', text: '@testbot hello!', author: { username: 'user1' } } ] }); await new Promise(resolve => setTimeout(resolve, 1000)); expect(interactionSpy).toHaveBeenCalled(); }); }); ``` ## Performance Testing ### Rate Limit Testing ```typescript describe('Rate Limit Handling', () => { it('should respect rate limits', async () => { const client = new ClientBase(runtime, {}); const requests = []; // Simulate many requests for (let i = 0; i < 100; i++) { requests.push(client.tweet(`Test ${i}`)); } const results = await Promise.allSettled(requests); // Should queue requests, not fail const succeeded = results.filter(r => r.status === 'fulfilled'); expect(succeeded.length).toBeGreaterThan(0); // Check for rate limit handling const rateLimited = results.filter(r => r.status === 'rejected' && r.reason?.code === 429 ); if (rateLimited.length > 0) { // Should have retry logic expect(client.requestQueue.size()).toBeGreaterThan(0); } }); }); ``` ### Memory Usage Testing ```typescript describe('Memory Management', () => { it('should not leak memory with processed tweets', async () => { const client = new TwitterInteractionClient(mockClient, runtime, {}); const initialMemory = process.memoryUsage().heapUsed; // Process many tweets for (let i = 0; i < 10000; i++) { client.markAsProcessed(`tweet_${i}`); } // Force garbage collection if (global.gc) global.gc(); const finalMemory = process.memoryUsage().heapUsed; const memoryGrowth = finalMemory - initialMemory; // Should maintain reasonable memory usage expect(memoryGrowth).toBeLessThan(50 * 1024 * 1024); // 50MB }); }); ``` ## Mock Utilities ### Twitter API Mocks ```typescript export function createMockTwitterApi() { return { v2: { me: vi.fn().mockResolvedValue({ data: { id: '123', username: 'testbot' } }), tweet: vi.fn().mockResolvedValue({ data: { id: '456', text: 'Mocked tweet' } }), reply: vi.fn().mockResolvedValue({ data: { id: '789', text: 'Mocked reply' } }), homeTimeline: vi.fn().mockResolvedValue({ data: [ { id: '111', text: 'Timeline tweet 1', author_id: '222', created_at: new Date().toISOString() } ], meta: { next_token: 'next_123' } }), search: vi.fn().mockResolvedValue({ data: [], meta: {} }), like: vi.fn().mockResolvedValue({ data: { liked: true } }), retweet: vi.fn().mockResolvedValue({ data: { retweeted: true } }) } }; } export function createMockRuntime(overrides = {}) { return { getSetting: vi.fn((key) => { const defaults = { TWITTER_DRY_RUN: 'true', TWITTER_POST_ENABLE: 'false', TWITTER_SEARCH_ENABLE: 'false' }; return overrides[key] || defaults[key]; }), generateText: vi.fn().mockResolvedValue({ text: 'Generated response' }), character: { name: 'TestBot', postExamples: ['Example tweet'], ...overrides.character }, logger: { info: vi.fn(), error: vi.fn(), warn: vi.fn(), debug: vi.fn() }, emit: vi.fn(), on: vi.fn(), ...overrides }; } ``` ### Test Helpers ```typescript export async function waitForTweet( runtime: IAgentRuntime, timeout = 5000 ): Promise { return new Promise((resolve, reject) => { const timer = setTimeout(() => { reject(new Error('Timeout waiting for tweet')); }, timeout); runtime.on('twitter:post:simulate', (tweet) => { clearTimeout(timer); resolve(tweet); }); }); } export async function simulateTimeline( runtime: IAgentRuntime, tweets: any[] ) { await runtime.emit('twitter:timeline:update', { tweets }); } export function createTestTweet(overrides = {}) { return { id: Math.random().toString(36).substring(7), text: 'Test tweet', author_id: '123', created_at: new Date().toISOString(), public_metrics: { like_count: 0, retweet_count: 0, reply_count: 0, quote_count: 0 }, ...overrides }; } ``` ## Test Configuration ### vitest.config.ts ```typescript import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { globals: true, environment: 'node', setupFiles: ['./tests/setup.ts'], testTimeout: 30000, coverage: { provider: 'v8', reporter: ['text', 'json', 'html'], exclude: [ 'node_modules', 'tests', '**/*.test.ts', '**/types.ts' ] }, // Prevent rate limiting in tests pool: 'forks', poolOptions: { forks: { singleFork: true } } } }); ``` ### Test Setup ```typescript // tests/setup.ts import { config } from 'dotenv'; import { vi } from 'vitest'; // Load test environment config({ path: '.env.test' }); // Mock external services vi.mock('twitter-api-v2', () => ({ TwitterApi: vi.fn(() => createMockTwitterApi()) })); // Global test configuration global.testConfig = { timeout: 30000, retries: 3 }; // Ensure dry run for all tests process.env.TWITTER_DRY_RUN = 'true'; // Mock timers for scheduled posts vi.useFakeTimers(); // Cleanup after tests afterEach(() => { vi.clearAllTimers(); }); ``` ## Debugging Tests ### Enable Debug Logging ```typescript // Enable detailed logging for specific test it('should process timeline with debug info', async () => { process.env.DEBUG = 'eliza:twitter:*'; const debugLogs = []; const originalLog = console.log; console.log = (...args) => { debugLogs.push(args.join(' ')); originalLog(...args); }; // Run test await client.processTimeline(); // Check debug output expect(debugLogs.some(log => log.includes('timeline'))).toBe(true); // Restore console.log = originalLog; delete process.env.DEBUG; }); ``` ### Test Reporters ```typescript // Custom reporter for Twitter-specific tests export class TwitterTestReporter { onTestStart(test: Test) { if (test.name.includes('twitter')) { console.log(`🐦 Running: ${test.name}`); } } onTestComplete(test: Test, result: TestResult) { if (test.name.includes('twitter')) { const emoji = result.status === 'passed' ? '✅' : '❌'; console.log(`${emoji} ${test.name}: ${result.duration}ms`); } } } ``` ## CI/CD Integration ### GitHub Actions Workflow ```yaml name: Twitter Plugin Tests on: push: paths: - 'packages/plugin-twitter/**' pull_request: paths: - 'packages/plugin-twitter/**' jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Setup Node.js uses: actions/setup-node@v3 with: node-version: 20 - name: Install dependencies run: bun install - name: Run unit tests run: bun test packages/plugin-twitter env: TWITTER_DRY_RUN: true - name: Run integration tests if: github.event_name == 'push' && github.ref == 'refs/heads/main' run: bun test:integration packages/plugin-twitter env: TWITTER_API_KEY: ${{ secrets.TEST_TWITTER_API_KEY }} TWITTER_API_SECRET_KEY: ${{ secrets.TEST_TWITTER_API_SECRET }} TWITTER_ACCESS_TOKEN: ${{ secrets.TEST_TWITTER_ACCESS_TOKEN }} TWITTER_ACCESS_TOKEN_SECRET: ${{ secrets.TEST_TWITTER_TOKEN_SECRET }} TWITTER_DRY_RUN: true - name: Upload coverage uses: codecov/codecov-action@v3 with: files: ./coverage/coverage-final.json flags: twitter-plugin ``` ## Best Practices 1. **Always Use Dry Run** * Set `TWITTER_DRY_RUN=true` for all tests * Never post real tweets in tests * Mock API responses 2. **Test Rate Limiting** * Simulate 429 errors * Test retry logic * Verify queue behavior 3. **Mock External Calls** * Mock Twitter API * Mock LLM generation * Control test data 4. **Test Edge Cases** * Empty timelines * Malformed tweets * Network failures * Auth errors 5. **Performance Testing** * Monitor memory usage * Test with large datasets * Measure processing times # Timeline Flow Source: https://docs.elizaos.ai/plugin-registry/platform/twitter/timeline-flow This document provides a comprehensive breakdown of how the Twitter plugin processes timeline data and generates interactions. This document provides a comprehensive breakdown of how the Twitter plugin processes timeline data and generates interactions. ## Complete Timeline Flow Diagram ```mermaid flowchart TD Start([Timeline Processing]) --> A[Fetch Home Timeline] A --> B{Cache Valid?} B -->|Yes| C[Use Cached Data] B -->|No| D[API Request] D --> E[Rate Limit Check] E --> F{Within Limits?} F -->|No| G[Wait/Queue] F -->|Yes| H[Fetch Timeline] G --> H C --> I[Filter Tweets] H --> I I --> J{Remove Processed} J --> K[New Tweets Only] K --> L{Algorithm Type?} L -->|Weighted| M[Weighted Processing] L -->|Latest| N[Latest Processing] M --> O[Calculate Scores] O --> P[Sort by Score] N --> Q[Sort by Time] P --> R[Select Top Tweets] Q --> R R --> S[Process Each Tweet] S --> T{Should Interact?} T -->|Yes| U[Generate Response] T -->|No| V[Mark Processed] U --> W{Response Type} W -->|Reply| X[Post Reply] W -->|Quote| Y[Quote Tweet] W -->|Like| Z[Like Tweet] W -->|Retweet| AA[Retweet] X --> AB[Update Cache] Y --> AB Z --> AB AA --> AB V --> AB AB --> AC{More Tweets?} AC -->|Yes| S AC -->|No| AD[Schedule Next Run] ``` ## Detailed Processing Flows ### 1. Timeline Fetching ```mermaid sequenceDiagram participant C as Client participant Q as Request Queue participant R as Rate Limiter participant A as Twitter API participant Ca as Cache C->>Ca: Check cache validity alt Cache valid Ca->>C: Return cached timeline else Cache invalid C->>Q: Queue timeline request Q->>R: Check rate limits alt Within limits R->>A: GET /2/users/:id/timelines/home A->>R: Timeline data R->>Ca: Update cache Ca->>C: Return timeline else Rate limited R->>R: Calculate wait time R->>Q: Delay request Q->>C: Request queued end end ``` ### 2. Weighted Algorithm Flow ```mermaid flowchart TD A[Tweet List] --> B[For Each Tweet] B --> C[Calculate User Score] C --> D{Target User?} D -->|Yes| E[Score = 10] D -->|No| F[Base Score = 5] F --> G{Verified?} G -->|Yes| H[Score +2] G -->|No| I[Continue] H --> J{High Followers?} I --> J J -->|Yes| K[Score +1] J -->|No| L[Continue] K --> M[User Score Complete] L --> M E --> M B --> N[Calculate Time Score] N --> O[Age in Hours] O --> P[Score = 10 - (Age/2)] P --> Q[Cap at 0-10] B --> R[Calculate Relevance] R --> S[Analyze Content] S --> T{Keywords Match?} T -->|Yes| U[High Relevance] T -->|No| V[Low Relevance] U --> W[Relevance Score] V --> W M --> X[Combine Scores] Q --> X W --> X X --> Y[Final Score = (U*3 + T*2 + R*5)] Y --> Z[Add to Scored List] ``` ### 3. Interaction Decision Flow ```mermaid flowchart TD A[Tweet to Process] --> B{Is Reply?} B -->|Yes| C{To Me?} B -->|No| D{Mentions Me?} C -->|Yes| E[Should Reply = Yes] C -->|No| F{In Thread?} D -->|Yes| E D -->|No| G{Target User?} F -->|Yes| H[Check Context] F -->|No| I[Skip] G -->|Yes| J[Check Relevance] G -->|No| K{High Score?} H --> L{Relevant?} L -->|Yes| E L -->|No| I J --> M{Above Threshold?} M -->|Yes| E M -->|No| I K -->|Yes| N[Maybe Reply] K -->|No| I E --> O[Generate Response] N --> P[Probability Check] P --> Q{Random < 0.3?} Q -->|Yes| O Q -->|No| I ``` ### 4. Response Generation Flow ```mermaid sequenceDiagram participant T as Tweet participant P as Processor participant C as Context Builder participant L as LLM participant V as Validator T->>P: Tweet to respond to P->>C: Build context C->>C: Get thread history C->>C: Get user history C->>C: Get recent interactions C->>L: Generate response Note over L: System: Character personality
Context: Thread + history
Task: Generate reply L->>V: Generated text V->>V: Check length V->>V: Check appropriateness V->>V: Remove duplicates alt Valid response V->>P: Approved response else Invalid V->>L: Regenerate end ``` ### 5. Action Processing Flow ```mermaid flowchart TD A[Timeline Tweets] --> B[Evaluate Each Tweet] B --> C{Like Candidate?} C -->|Yes| D[Calculate Like Score] C -->|No| E{Retweet Candidate?} D --> F[Add to Actions] E -->|Yes| G[Calculate RT Score] E -->|No| H{Quote Candidate?} G --> F H -->|Yes| I[Calculate Quote Score] H -->|No| J[Next Tweet] I --> F F --> K[Action List] J --> K K --> L[Sort by Score] L --> M[Select Top Action] M --> N{Action Type} N -->|Like| O[POST /2/users/:id/likes] N -->|Retweet| P[POST /2/users/:id/retweets] N -->|Quote| Q[Generate Quote Text] Q --> R[POST /2/tweets] O --> S[Log Action] P --> S R --> S ``` ## Timeline State Management ### Cache Structure ```typescript interface TimelineCache { tweets: Tweet[]; users: Map; timestamp: number; etag?: string; } interface ProcessingState { processedTweets: Set; lastProcessTime: number; interactionCount: number; rateLimitStatus: RateLimitInfo; } ``` ### Scoring Components ```typescript interface ScoringWeights { user: number; // Default: 3 time: number; // Default: 2 relevance: number; // Default: 5 } interface TweetScore { tweetId: string; userScore: number; timeScore: number; relevanceScore: number; totalScore: number; factors: { isTargetUser: boolean; isVerified: boolean; followerCount: number; hasKeywords: boolean; age: number; }; } ``` ## Error Handling in Timeline Flow ```mermaid flowchart TD A[Timeline Error] --> B{Error Type} B -->|Rate Limit| C[429 Error] B -->|Auth Error| D[401 Error] B -->|Network| E[Network Error] B -->|API Error| F[API Error] C --> G[Get Reset Time] G --> H[Queue Until Reset] H --> I[Retry After Wait] D --> J[Check Credentials] J --> K{Valid?} K -->|No| L[Stop Processing] K -->|Yes| M[Refresh Token] M --> N[Retry Once] E --> O{Retry Count} O -->|< 3| P[Exponential Backoff] O -->|>= 3| Q[Skip Cycle] P --> R[Retry Request] F --> S[Log Error] S --> T{Critical?} T -->|Yes| U[Alert & Stop] T -->|No| V[Skip & Continue] ``` ## Performance Optimization ### Batch Processing ```mermaid sequenceDiagram participant P as Processor participant B as Batcher participant A as API P->>B: Add tweet IDs [1,2,3,4,5] P->>B: Add user IDs [a,b,c] Note over B: Batch window (100ms) B->>A: GET /2/tweets?ids=1,2,3,4,5 B->>A: GET /2/users?ids=a,b,c par Parallel Requests A->>B: Tweets data A->>B: Users data end B->>P: Combined results ``` ### Processing Pipeline ```mermaid flowchart LR A[Fetch] --> B[Filter] B --> C[Score] C --> D[Sort] D --> E[Process] F[Cache Layer] --> A F --> B F --> E G[Queue Manager] --> A G --> E H[Rate Limiter] --> A H --> E ``` ## Monitoring & Metrics ### Timeline Processing Metrics ```typescript interface TimelineMetrics { fetchTime: number; tweetCount: number; newTweetCount: number; processedCount: number; interactionCount: number; errorCount: number; cacheHitRate: number; averageScore: number; } ``` ### Performance Tracking ```mermaid flowchart TD A[Start Timer] --> B[Fetch Timeline] B --> C[Log Fetch Time] C --> D[Process Tweets] D --> E[Log Process Time] E --> F[Generate Metrics] F --> G{Performance OK?} G -->|Yes| H[Continue] G -->|No| I[Adjust Parameters] I --> J[Reduce Batch Size] I --> K[Increase Intervals] I --> L[Optimize Algorithm] ``` ## Configuration Impact ### Algorithm Selection | Algorithm | Best For | Performance | Quality | | --------- | -------------------- | ----------- | ------- | | Weighted | Quality interactions | Slower | Higher | | Latest | High volume | Faster | Lower | ### Weight Configuration Effects ```mermaid graph LR A[User Weight ↑] --> B[More targeted interactions] C[Time Weight ↑] --> D[Prefer recent tweets] E[Relevance Weight ↑] --> F[More on-topic responses] B --> G[Higher engagement quality] D --> H[Faster response time] F --> I[Better conversation flow] ``` ## Best Practices 1. **Cache Management** * Implement TTL for timeline cache * Clear processed tweets periodically * Monitor cache hit rates 2. **Rate Limit Handling** * Track limits per endpoint * Implement request queuing * Use exponential backoff 3. **Score Tuning** * Monitor interaction quality * Adjust weights based on results * A/B test different configurations 4. **Error Recovery** * Implement circuit breakers * Log all failures with context * Graceful degradation 5. **Performance Monitoring** * Track processing times * Monitor API usage * Alert on anomalies # Plugin Registry Source: https://docs.elizaos.ai/plugin-registry/registry Browse the complete collection of available elizaOS plugins. This registry is updated in real-time with the latest 1.x compatible plugins from the community. This registry automatically syncs with the official [elizaOS Plugin Registry](https://github.com/elizaos-plugins/registry) to show you the most up-to-date plugins available for your elizaOS agents. ## Available Plugins { (() => { const [plugins, setPlugins] = React.useState([]); const [loading, setLoading] = React.useState(true); const [error, setError] = React.useState(null); React.useEffect(() => { const fetchPlugins = async () => { try { const response = await fetch('https://raw.githubusercontent.com/elizaos-plugins/registry/refs/heads/main/generated-registry.json'); if (!response.ok) { throw new Error(`Failed to fetch plugins: ${response.status}`); } const data = await response.json(); const v1Plugins = Object.entries(data.registry) .filter(([_, plugin]) => plugin.supports && plugin.supports.v1) .map(([name, plugin]) => ({ name, description: plugin.description || 'No description available', repo: plugin.git?.repo, version: plugin.npm?.v1, repoUrl: plugin.git?.repo ? `https://github.com/${plugin.git.repo}` : null, displayName: name .replace('@elizaos/plugin-', '') .replace('@elizaos/client-', '') .replace('@elizaos/adapter-', '') .replace(/^plugin-/, '') .replace(/-/g, ' ') .replace(/\b\w/g, l => l.toUpperCase()), category: plugin.category || 'General', tags: plugin.tags || [], stars: plugin.stargazers_count || 0, language: plugin.language })) .sort((a, b) => { // Sort by stars (descending), then by name (ascending) as tiebreaker if (b.stars !== a.stars) { return b.stars - a.stars; } return a.name.localeCompare(b.name); }); setPlugins(v1Plugins); setLoading(false); } catch (error) { console.error('Failed to fetch plugins:', error); setError(error.message); setLoading(false); } }; fetchPlugins(); }, []); if (loading) { return (
Loading v1 compatible plugins...
); } if (error) { return (
Failed to load plugins
{error}
Please try refreshing the page or visit the{' '} official registry
); } if (plugins.length === 0) { return (
No v1 compatible plugins found in the registry.
); } return plugins.map(plugin => (
{plugin.name}
{plugin.description && (
{plugin.description}
)}
{plugin.stars > 0 && ( {plugin.stars} )} {plugin.version && ( v{plugin.version} )}
{plugin.tags && plugin.tags.length > 0 && (
{plugin.tags.slice(0, 3).map(tag => ( {tag} ))} {plugin.tags.length > 3 && ( +{plugin.tags.length - 3} more )}
)}
)); })() }
## How to Use Plugins To install and use any of these plugins in your elizaOS project: 1. **Install the plugin** using bun or elizaos cli: ```bash bun install @elizaos/plugin-name # or elizaos plugins add @elizaos/plugin-name ``` 2. **Import and register** the plugin in your agent configuration: ```typescript import { pluginName } from '@elizaos/plugin-name'; const character = { // ... your character configuration plugins: [pluginName] }; ``` 3. **Configure the plugin** if it requires environment variables or additional setup (check the plugin's documentation for specific requirements) ## Contributing to the Registry The plugin registry is community-driven. To add your plugin: 1. **Publish your plugin** to npm with the `@elizaos/plugin-` prefix 2. **Submit a pull request** to the [elizaOS Plugin Registry](https://github.com/elizaos-plugins/registry) 3. **Ensure compatibility** with elizaOS v1.x For detailed guidance on creating and publishing plugins, check out our [Create a Plugin](/guides/create-a-plugin) and [Publish a Plugin](/guides/publish-a-plugin) guides. ## Registry Status This registry automatically updates every few hours to reflect the latest plugins available in the elizaOS ecosystem. If you don't see a recently published plugin, please wait a few hours for the next sync. Always review plugin documentation and source code before installation, especially for plugins that handle sensitive operations like wallet management or external API calls. # Database Management Source: https://docs.elizaos.ai/plugin-registry/sql Database integration and management for elizaOS The `@elizaos/plugin-sql` provides comprehensive database management for elizaOS agents, featuring automatic schema migrations, multi-database support, and a sophisticated plugin architecture. ## Key Features ### 🗄️ Dual Database Support * **PGLite** - Embedded PostgreSQL for development * **PostgreSQL** - Full PostgreSQL for production * Automatic adapter selection based on environment ### 🔄 Dynamic Migration System * Automatic schema discovery from plugins * Intelligent table creation and updates * Dependency resolution for foreign keys * No manual migration files needed ### 🏗️ Schema Introspection * Analyzes existing database structure * Detects missing columns and indexes * Handles composite primary keys * Preserves existing data ### 🔌 Plugin Integration * Plugins can define their own schemas * Automatic table creation at startup * Isolated namespaces prevent conflicts * Shared core tables for common data ## Architecture Overview The plugin consists of several key components: ```mermaid flowchart TD A[elizaOS Runtime] --> B[DatabaseMigrationService
• Schema Discovery
• Migration Orchestration] B --> C[Database Adapters] C --> D[PGLite Adapter
• Development
• File-based] C --> E[PG Adapter
• Production
• Pooled] ``` ## Core Components ### 1. Database Adapters * **BaseDrizzleAdapter** - Shared functionality * **PgliteDatabaseAdapter** - Development database * **PgDatabaseAdapter** - Production database ### 2. Migration Service * **DatabaseMigrationService** - Orchestrates migrations * **DrizzleSchemaIntrospector** - Analyzes schemas * **Custom Migrator** - Executes schema changes ### 3. Connection Management * **PGliteClientManager** - Singleton PGLite instance * **PostgresConnectionManager** - Connection pooling * **Global Singletons** - Prevents multiple connections ### 4. Schema Definitions Core tables for agent functionality: * `agents` - Agent identities * `memories` - Knowledge storage * `entities` - People and objects * `relationships` - Entity connections * `messages` - Communication history * `embeddings` - Vector search * `cache` - Key-value storage * `logs` - System events ## Installation ```bash elizaos plugins add @elizaos/plugin-sql ``` ## Configuration The SQL plugin is automatically included by the elizaOS runtime and configured via environment variables. ### Environment Setup Create a `.env` file in your project root: ```bash # For PostgreSQL (production) POSTGRES_URL=postgresql://user:password@host:5432/database # For custom PGLite directory (development) # Optional - defaults to ./.eliza/.elizadb if not set PGLITE_DATA_DIR=/path/to/custom/db ``` ### Adapter Selection The plugin automatically chooses the appropriate adapter: * **With `POSTGRES_URL`** → PostgreSQL adapter (production) * **Without `POSTGRES_URL`** → PGLite adapter (development) No code changes needed - just set your environment variables. ### Custom Plugin with Schema ```typescript import { Plugin } from '@elizaos/core'; import { pgTable, uuid, text, timestamp } from 'drizzle-orm/pg-core'; // Define your schema const customTable = pgTable('custom_data', { id: uuid('id').primaryKey().defaultRandom(), agentId: uuid('agent_id').notNull(), data: text('data').notNull(), createdAt: timestamp('created_at').defaultNow(), }); // Create plugin export const customPlugin: Plugin = { name: 'custom-plugin', schema: { customTable, }, // Plugin will have access to database via runtime }; ``` ## How It Works ### 1. Initialization When the agent starts: 1. SQL plugin initializes the appropriate database adapter 2. Migration service discovers all plugin schemas 3. Schemas are analyzed and dependencies resolved 4. Tables are created or updated as needed ### 2. Schema Discovery ```typescript // Plugins export their schemas export const myPlugin: Plugin = { name: 'my-plugin', schema: { /* Drizzle tables */ }, }; // Migration service finds and registers them discoverAndRegisterPluginSchemas(plugins); ``` ### 3. Dynamic Migration The system: * Introspects existing database structure * Compares with plugin schema definitions * Generates and executes necessary DDL * Handles errors gracefully ### 4. Runtime Access Plugins access the database through the runtime: ```typescript const adapter = runtime.databaseAdapter; await adapter.getMemories({ agentId }); ``` ## Advanced Features ### Composite Primary Keys ```typescript const cacheTable = pgTable('cache', { key: text('key').notNull(), agentId: uuid('agent_id').notNull(), value: jsonb('value'), }, (table) => ({ pk: primaryKey(table.key, table.agentId), })); ``` ### Foreign Key Dependencies Tables with foreign keys are automatically created in the correct order. ### Schema Introspection The system can analyze and adapt to existing database structures. ### Error Recovery * Automatic retries with exponential backoff * Detailed error logging * Graceful degradation ## Best Practices 1. **Define Clear Schemas** - Use TypeScript for type safety 2. **Use UUIDs** - For distributed compatibility 3. **Include Timestamps** - Track data changes 4. **Index Strategically** - For query performance 5. **Test Migrations** - Verify schema changes locally ## Limitations * No automatic downgrades or rollbacks * Column type changes require manual intervention * Data migrations must be handled separately * Schema changes should be tested thoroughly ## Next Steps * [Database Adapters](./database-adapters.mdx) - Detailed adapter documentation * [Schema Management](./schema-management.mdx) - Creating and managing schemas * [Plugin Tables Guide](./plugin-tables.mdx) - Adding tables to your plugin * [Examples](./examples.mdx) - Real-world usage patterns # Database Adapters Source: https://docs.elizaos.ai/plugin-registry/sql/database-adapters Understanding PGLite and PostgreSQL adapters in the SQL plugin The SQL plugin provides two database adapters that extend a common `BaseDrizzleAdapter`: * **PGLite Adapter** - Embedded PostgreSQL for development and testing * **PostgreSQL Adapter** - Full PostgreSQL for production environments ## Architecture Overview Both adapters share the same base functionality through `BaseDrizzleAdapter`, which implements the `IDatabaseAdapter` interface from `@elizaos/core`. The adapters handle: * Connection management through dedicated managers * Automatic retry logic for database operations * Schema introspection and dynamic migrations * Embedding dimension configuration (default: 384 dimensions) ## PGLite Adapter The `PgliteDatabaseAdapter` uses an embedded PostgreSQL instance that runs entirely in Node.js. ### Key Features * **Zero external dependencies** - No PostgreSQL installation required * **File-based persistence** - Data stored in local filesystem * **Singleton connection manager** - Ensures single database instance per process * **Automatic initialization** - Database created on first use ### Implementation Details ```typescript export class PgliteDatabaseAdapter extends BaseDrizzleAdapter { private manager: PGliteClientManager; protected embeddingDimension: EmbeddingDimensionColumn = DIMENSION_MAP[384]; constructor(agentId: UUID, manager: PGliteClientManager) { super(agentId); this.manager = manager; this.db = drizzle(this.manager.getConnection()); } } ``` ### Connection Management The `PGliteClientManager` handles: * Singleton PGLite instance creation * Data directory resolution and creation * Connection persistence across adapter instances ## PostgreSQL Adapter The `PgDatabaseAdapter` connects to a full PostgreSQL database using connection pooling. ### Key Features * **Connection pooling** - Efficient resource management * **Automatic retry logic** - Built-in resilience for transient failures * **Production-ready** - Designed for scalable deployments * **SSL support** - Secure connections when configured * **Cloud compatibility** - Works with Supabase, Neon, and other PostgreSQL providers ### Implementation Details ```typescript export class PgDatabaseAdapter extends BaseDrizzleAdapter { protected embeddingDimension: EmbeddingDimensionColumn = DIMENSION_MAP[384]; private manager: PostgresConnectionManager; constructor(agentId: UUID, manager: PostgresConnectionManager, _schema?: any) { super(agentId); this.manager = manager; this.db = manager.getDatabase(); } protected async withDatabase(operation: () => Promise): Promise { return await this.withRetry(async () => { const client = await this.manager.getClient(); try { const db = drizzle(client); this.db = db; return await operation(); } finally { client.release(); } }); } } ``` ### Connection Management The `PostgresConnectionManager` provides: * Connection pool management (default size: 20) * SSL configuration based on environment * Singleton pattern to prevent multiple pools * Graceful shutdown handling ## Adapter Selection The adapter is automatically selected based on environment configuration: ```typescript export function createDatabaseAdapter( config: { dataDir?: string; postgresUrl?: string; }, agentId: UUID ): IDatabaseAdapter { if (config.postgresUrl) { // PostgreSQL adapter for production if (!globalSingletons.postgresConnectionManager) { globalSingletons.postgresConnectionManager = new PostgresConnectionManager( config.postgresUrl ); } return new PgDatabaseAdapter(agentId, globalSingletons.postgresConnectionManager); } else { // PGLite adapter for development const resolvedDataDir = resolvePgliteDir(config.dataDir); if (!globalSingletons.pgLiteClientManager) { globalSingletons.pgLiteClientManager = new PGliteClientManager(resolvedDataDir); } return new PgliteDatabaseAdapter(agentId, globalSingletons.pgLiteClientManager); } } ``` ## Migration Handling **Important**: Both adapters delegate migration handling to the `DatabaseMigrationService`. The adapters themselves do not run migrations directly. ```typescript // In both adapters: async runMigrations(): Promise { logger.debug('Migrations are handled by the migration service'); // Migrations are handled by the migration service, not the adapter } ``` The migration service handles: * Plugin schema discovery and registration * Dynamic table creation and updates * Schema introspection for existing tables * Dependency resolution for table creation order ## Best Practices ### Development (PGLite) 1. Use default data directory for consistency 2. Clear data directory between test runs if needed 3. Be aware of file system limitations 4. Suitable for single-instance development ### Production (PostgreSQL) 1. Always use connection pooling 2. Configure SSL for secure connections 3. Monitor connection pool usage 4. Use environment variables for configuration 5. Implement proper backup strategies ## Configuration The SQL plugin automatically selects the appropriate adapter based on environment variables. ### Environment Variables ```bash # .env file # For PostgreSQL (production) POSTGRES_URL=postgresql://user:password@host:5432/database # For custom PGLite directory (optional) # If not set, defaults to ./.eliza/.elizadb PGLITE_DATA_DIR=/path/to/custom/db ``` ### Configuration Priority 1. **If `POSTGRES_URL` is set** → Uses PostgreSQL adapter 2. **If `POSTGRES_URL` is not set** → Uses PGLite adapter * With `PGLITE_DATA_DIR` if specified * Otherwise uses default path: `./.eliza/.elizadb` ### PostgreSQL Configuration The PostgreSQL adapter supports any PostgreSQL-compatible database: * **Supabase** - Use the connection string from your project settings * **Neon** - Use the connection string from your Neon console * **Amazon RDS PostgreSQL** * **Google Cloud SQL PostgreSQL** * **Self-hosted PostgreSQL** (v12+) Example connection strings: ```bash # Supabase POSTGRES_URL=postgresql://postgres:[password]@[project].supabase.co:5432/postgres # Neon POSTGRES_URL=postgresql://[user]:[password]@[project].neon.tech/[database]?sslmode=require # Standard PostgreSQL POSTGRES_URL=postgresql://user:password@localhost:5432/mydb ``` ## Error Handling Both adapters include: * Automatic retry logic (3 attempts by default) * Exponential backoff between retries * Detailed error logging * Graceful degradation The adapters handle common scenarios like: * Connection timeouts * Transient network failures * Pool exhaustion (PostgreSQL) * File system errors (PGLite) # Examples Source: https://docs.elizaos.ai/plugin-registry/sql/examples Practical code examples and patterns This document provides practical examples of common database patterns and operations using the elizaOS plugin-sql system. ## Basic Operations ### Creating Records ```typescript // Simple insert const newUser = await db .insert(userTable) .values({ name: 'Alice', email: 'alice@example.com', isActive: true, }) .returning(); // Bulk insert const users = await db .insert(userTable) .values([ { name: 'Bob', email: 'bob@example.com' }, { name: 'Charlie', email: 'charlie@example.com' }, ]) .returning(); // Insert with conflict handling await db .insert(userTable) .values({ email: 'alice@example.com', name: 'Alice Updated' }) .onConflictDoUpdate({ target: userTable.email, set: { name: 'Alice Updated', updatedAt: sql`now()` }, }); ``` ### Querying Data ```typescript import { eq, and, or, like, inArray, desc, lt, gte, sql } from 'drizzle-orm'; // Simple select const users = await db.select().from(userTable); // Select with conditions const activeUsers = await db .select() .from(userTable) .where(eq(userTable.isActive, true)) .orderBy(desc(userTable.createdAt)) .limit(10); // Select specific columns const userEmails = await db .select({ email: userTable.email, name: userTable.name, }) .from(userTable); // Complex conditions const filteredUsers = await db .select() .from(userTable) .where( and( eq(userTable.isActive, true), or( like(userTable.email, '%@company.com'), inArray(userTable.role, ['admin', 'moderator']) ) ) ); ``` ### Updating Records ```typescript // Update single record await db .update(userTable) .set({ name: 'Updated Name', updatedAt: sql`now()`, }) .where(eq(userTable.id, userId)); // Update multiple records const thirtyDaysAgo = new Date(); thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30); await db .update(userTable) .set({ isActive: false }) .where(lt(userTable.lastLoginAt, thirtyDaysAgo)); // Update with returning const [updatedUser] = await db .update(userTable) .set({ level: sql`${userTable.level} + 1` }) .where(eq(userTable.id, userId)) .returning(); ``` ### Deleting Records ```typescript // Delete single record await db.delete(userTable).where(eq(userTable.id, userId)); // Delete with conditions await db .delete(sessionTable) .where(lt(sessionTable.expiresAt, new Date())); // Soft delete pattern await db .update(userTable) .set({ isDeleted: true, deletedAt: sql`now()`, }) .where(eq(userTable.id, userId)); ``` ## Working with Relationships ### One-to-Many ```typescript // Get user with their posts const userWithPosts = await db .select({ user: userTable, posts: postTable, }) .from(userTable) .leftJoin(postTable, eq(userTable.id, postTable.authorId)) .where(eq(userTable.id, userId)); // Group posts by user const usersWithPostCount = await db .select({ userId: userTable.id, userName: userTable.name, postCount: count(postTable.id), }) .from(userTable) .leftJoin(postTable, eq(userTable.id, postTable.authorId)) .groupBy(userTable.id); ``` ### Many-to-Many ```typescript // Get user's roles through junction table const userRoles = await db .select({ role: roleTable, assignedAt: userRoleTable.assignedAt, }) .from(userRoleTable) .innerJoin(roleTable, eq(userRoleTable.roleId, roleTable.id)) .where(eq(userRoleTable.userId, userId)); // Get users with specific role const admins = await db .select({ user: userTable, }) .from(userTable) .innerJoin(userRoleTable, eq(userTable.id, userRoleTable.userId)) .innerJoin(roleTable, eq(userRoleTable.roleId, roleTable.id)) .where(eq(roleTable.name, 'admin')); ``` ## Advanced Queries ### Aggregations ```typescript // Count, sum, average const stats = await db .select({ totalUsers: count(userTable.id), avgAge: avg(userTable.age), totalRevenue: sum(orderTable.amount), }) .from(userTable) .leftJoin(orderTable, eq(userTable.id, orderTable.userId)); // Group by with having const activeCategories = await db .select({ category: productTable.category, productCount: count(productTable.id), avgPrice: avg(productTable.price), }) .from(productTable) .where(eq(productTable.isActive, true)) .groupBy(productTable.category) .having(gte(count(productTable.id), 5)); ``` ### Subqueries ```typescript // Subquery in select const usersWithLatestPost = await db .select({ user: userTable, latestPostId: sql`( SELECT id FROM ${postTable} WHERE ${postTable.authorId} = ${userTable.id} ORDER BY ${postTable.createdAt} DESC LIMIT 1 )`, }) .from(userTable); // Subquery in where const usersWithRecentActivity = await db .select() .from(userTable) .where( sql`EXISTS ( SELECT 1 FROM ${activityTable} WHERE ${activityTable.userId} = ${userTable.id} AND ${activityTable.createdAt} > NOW() - INTERVAL '7 days' )` ); ``` ### Window Functions ```typescript // Row number for pagination const rankedUsers = await db .select({ id: userTable.id, name: userTable.name, score: userTable.score, rank: sql`ROW_NUMBER() OVER (ORDER BY ${userTable.score} DESC)`, }) .from(userTable); // Running totals const salesWithRunningTotal = await db .select({ date: salesTable.date, amount: salesTable.amount, runningTotal: sql` SUM(${salesTable.amount}) OVER ( ORDER BY ${salesTable.date} ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW ) `, }) .from(salesTable) .orderBy(salesTable.date); ``` ## Transaction Patterns ### Basic Transactions ```typescript // Simple transaction await db.transaction(async (tx) => { // Create user const [user] = await tx .insert(userTable) .values({ name: 'John', email: 'john@example.com' }) .returning(); // Create profile await tx .insert(profileTable) .values({ userId: user.id, bio: 'New user' }); // Create initial settings await tx .insert(settingsTable) .values({ userId: user.id, theme: 'light' }); }); ``` ### Conditional Transactions ```typescript // Transaction with rollback try { await db.transaction(async (tx) => { const [user] = await tx .select() .from(userTable) .where(eq(userTable.id, userId)) .for('update'); // Lock row if (user.balance < amount) { throw new Error('Insufficient balance'); } // Deduct from sender await tx .update(userTable) .set({ balance: sql`${userTable.balance} - ${amount}` }) .where(eq(userTable.id, userId)); // Add to receiver await tx .update(userTable) .set({ balance: sql`${userTable.balance} + ${amount}` }) .where(eq(userTable.id, receiverId)); // Log transaction await tx .insert(transactionTable) .values({ fromUserId: userId, toUserId: receiverId, amount, type: 'transfer', }); }); } catch (error) { console.error('Transaction failed:', error); // Transaction automatically rolled back } ``` ## Plugin Integration Examples ### Memory Storage Plugin ```typescript // Plugin schema definition export const memoryTable = pgTable('plugin_memories', { id: uuid('id').primaryKey().defaultRandom(), agentId: uuid('agent_id') .notNull() .references(() => agentTable.id, { onDelete: 'cascade' }), type: text('type').notNull(), content: text('content').notNull(), // Vector for similarity search embedding: vector('embedding', { dimensions: 1536 }), // Metadata metadata: jsonb('metadata').$type<{ source?: string; confidence?: number; tags?: string[]; }>().default(sql`'{}'::jsonb`), createdAt: timestamp('created_at').default(sql`now()`), }, (table) => ({ // Index for vector similarity search embeddingIdx: index('memories_embedding_idx') .using('ivfflat') .on(table.embedding.op('vector_ip_ops')), // Regular indexes agentTypeIdx: index('memories_agent_type_idx') .on(table.agentId, table.type), })); // Memory service export class MemoryService { async storeMemory(agentId: string, content: string, embedding: number[]) { return await db .insert(memoryTable) .values({ agentId, content, type: 'conversation', embedding, metadata: { source: 'chat', confidence: 0.95, }, }) .returning(); } async findSimilarMemories(agentId: string, embedding: number[], limit = 10) { return await db .select({ id: memoryTable.id, content: memoryTable.content, similarity: sql`1 - (${memoryTable.embedding} <=> ${embedding})`, }) .from(memoryTable) .where(eq(memoryTable.agentId, agentId)) .orderBy(sql`${memoryTable.embedding} <=> ${embedding}`) .limit(limit); } } ``` ### Analytics Plugin ```typescript // Event tracking schema export const eventTable = pgTable('analytics_events', { id: uuid('id').primaryKey().defaultRandom(), agentId: uuid('agent_id').notNull(), // Event details name: text('name').notNull(), category: text('category'), // User context userId: uuid('user_id'), sessionId: uuid('session_id'), // Event data properties: jsonb('properties'), // Timing timestamp: timestamp('timestamp').default(sql`now()`), }, (table) => ({ // Indexes for common queries agentTimestampIdx: index('events_agent_timestamp_idx') .on(table.agentId, table.timestamp), nameIdx: index('events_name_idx').on(table.name), })); // Analytics service export class AnalyticsService { async trackEvent(event: { agentId: string; name: string; userId?: string; properties?: Record; }) { await db.insert(eventTable).values(event); } async getEventStats(agentId: string, days = 7) { const startDate = new Date(); startDate.setDate(startDate.getDate() - days); return await db .select({ name: eventTable.name, count: count(eventTable.id), uniqueUsers: countDistinct(eventTable.userId), }) .from(eventTable) .where( and( eq(eventTable.agentId, agentId), gte(eventTable.timestamp, startDate) ) ) .groupBy(eventTable.name) .orderBy(desc(count(eventTable.id))); } } ``` ### Task Queue Plugin ```typescript // Task queue schema export const taskQueueTable = pgTable('task_queue', { id: uuid('id').primaryKey().defaultRandom(), // Task identification type: text('type').notNull(), priority: integer('priority').default(0), // Task data payload: jsonb('payload').notNull(), // Execution control status: text('status').default('pending'), // pending, processing, completed, failed attempts: integer('attempts').default(0), maxAttempts: integer('max_attempts').default(3), // Scheduling scheduledFor: timestamp('scheduled_for').default(sql`now()`), // Execution results result: jsonb('result'), error: text('error'), // Timestamps createdAt: timestamp('created_at').default(sql`now()`), startedAt: timestamp('started_at'), completedAt: timestamp('completed_at'), }, (table) => ({ // Index for queue processing queueIdx: index('task_queue_idx') .on(table.status, table.scheduledFor, table.priority), })); // Task queue service export class TaskQueueService { async enqueueTask(task: { type: string; payload: any; priority?: number; scheduledFor?: Date; }) { return await db.insert(taskQueueTable).values(task).returning(); } async getNextTask() { return await db.transaction(async (tx) => { // Get next available task const [task] = await tx .select() .from(taskQueueTable) .where( and( eq(taskQueueTable.status, 'pending'), lte(taskQueueTable.scheduledFor, new Date()), lt(taskQueueTable.attempts, taskQueueTable.maxAttempts) ) ) .orderBy( desc(taskQueueTable.priority), asc(taskQueueTable.scheduledFor) ) .limit(1) .for('update skip locked'); // Skip locked rows if (!task) return null; // Mark as processing await tx .update(taskQueueTable) .set({ status: 'processing', startedAt: sql`now()`, attempts: sql`${taskQueueTable.attempts} + 1`, }) .where(eq(taskQueueTable.id, task.id)); return task; }); } async completeTask(taskId: string, result: any) { await db .update(taskQueueTable) .set({ status: 'completed', result, completedAt: sql`now()`, }) .where(eq(taskQueueTable.id, taskId)); } async failTask(taskId: string, error: string) { await db .update(taskQueueTable) .set({ status: 'failed', error, completedAt: sql`now()`, }) .where(eq(taskQueueTable.id, taskId)); } } ``` ## Performance Optimization ### Batch Operations ```typescript // Batch insert with chunks async function batchInsert( table: any, data: T[], chunkSize = 1000 ) { for (let i = 0; i < data.length; i += chunkSize) { const chunk = data.slice(i, i + chunkSize); await db.insert(table).values(chunk); } } // Batch update async function batchUpdate(updates: Array<{ id: string; data: any }>) { const updatePromises = updates.map(({ id, data }) => db.update(userTable).set(data).where(eq(userTable.id, id)) ); // Execute in parallel with concurrency limit const results = []; for (let i = 0; i < updatePromises.length; i += 10) { const batch = updatePromises.slice(i, i + 10); results.push(...(await Promise.all(batch))); } return results; } ``` ### Query Optimization ```typescript // Use covering indexes const optimizedQuery = await db .select({ id: userTable.id, name: userTable.name, email: userTable.email, }) .from(userTable) .where(eq(userTable.isActive, true)) .orderBy(userTable.createdAt) .limit(100); // Avoid N+1 queries - use joins const usersWithPosts = await db .select({ user: userTable, posts: sql` COALESCE( json_agg( json_build_object( 'id', ${postTable.id}, 'title', ${postTable.title} ) ORDER BY ${postTable.createdAt} DESC ) FILTER (WHERE ${postTable.id} IS NOT NULL), '[]' ) `, }) .from(userTable) .leftJoin(postTable, eq(userTable.id, postTable.authorId)) .groupBy(userTable.id); ``` # Plugin Tables Guide Source: https://docs.elizaos.ai/plugin-registry/sql/plugin-tables How plugins can define their own database tables This guide shows plugin developers how to add database tables to their elizaOS plugins. The plugin-sql system automatically handles schema creation, migrations, and namespacing. ## Overview Any elizaOS plugin can define its own database tables by: 1. Creating table definitions using Drizzle ORM 2. Exporting a `schema` property from the plugin 3. That's it! Tables are created automatically on startup ## Step-by-Step Guide ### 1. Set Up Your Plugin Structure ``` packages/my-plugin/ ├── src/ │ ├── schema/ │ │ ├── index.ts # Export all tables │ │ ├── users.ts # User table definition │ │ └── settings.ts # Settings table definition │ ├── actions/ │ ├── services/ │ └── index.ts # Plugin entry point ├── package.json └── tsconfig.json ``` ### 2. Define Your Tables Create table definitions using Drizzle ORM: ```typescript // packages/my-plugin/src/schema/users.ts import { pgTable, uuid, text, timestamp, boolean, jsonb } from 'drizzle-orm/pg-core'; import { sql } from 'drizzle-orm'; export const pluginUsersTable = pgTable('plugin_users', { id: uuid('id').primaryKey().defaultRandom(), // Basic fields username: text('username').notNull().unique(), email: text('email').notNull(), isActive: boolean('is_active').default(true), // JSONB for flexible data profile: jsonb('profile') .$type<{ avatar?: string; bio?: string; preferences?: Record; }>() .default(sql`'{}'::jsonb`), // Standard timestamps createdAt: timestamp('created_at', { withTimezone: true }) .default(sql`now()`) .notNull(), updatedAt: timestamp('updated_at', { withTimezone: true }) .default(sql`now()`) .notNull(), }); ``` ### 3. Create Schema Index Export all your tables from a central location: ```typescript // packages/my-plugin/src/schema/index.ts export { pluginUsersTable } from './users'; export { pluginSettingsTable } from './settings'; // Export all other tables... ``` ### 4. Export Schema from Plugin The critical step - export your schema from the plugin: ```typescript // packages/my-plugin/src/index.ts import { type Plugin } from '@elizaos/core'; import * as schema from './schema'; export const myPlugin: Plugin = { name: '@company/my-plugin', description: 'My plugin with custom database tables', // This enables automatic migrations! schema, init: async (config, runtime) => { // Plugin initialization console.log('Plugin initialized with database tables'); }, // Other plugin properties... actions: [], services: [], providers: [], }; export default myPlugin; ``` ## Schema Namespacing Your plugin's tables are automatically created in a dedicated PostgreSQL schema: ```typescript // Plugin name: @company/my-plugin // Schema name: company_my_plugin // Full table name: company_my_plugin.plugin_users ``` This prevents naming conflicts between plugins. ## Working with Foreign Keys ### Reference Core Tables To reference tables from the core plugin: ```typescript // Import core schema import { agentTable } from '@elizaos/plugin-sql/schema'; export const pluginMemoriesTable = pgTable('plugin_memories', { id: uuid('id').primaryKey().defaultRandom(), // Reference core agent table agentId: uuid('agent_id') .notNull() .references(() => agentTable.id, { onDelete: 'cascade' }), content: text('content').notNull(), metadata: jsonb('metadata').default(sql`'{}'::jsonb`), }); ``` ### Reference Your Own Tables For relationships within your plugin: ```typescript export const pluginPostsTable = pgTable('plugin_posts', { id: uuid('id').primaryKey().defaultRandom(), // Reference user in same plugin authorId: uuid('author_id') .notNull() .references(() => pluginUsersTable.id, { onDelete: 'cascade' }), title: text('title').notNull(), content: text('content').notNull(), }); ``` ### Cross-Plugin References To reference tables from other plugins: ```typescript // Reference using fully qualified name userId: uuid('user_id') .notNull() .references(() => sql`"other_plugin"."users"("id")`), ``` ## Table Design Patterns ### User Tables ```typescript export const pluginUsersTable = pgTable('plugin_users', { id: uuid('id').primaryKey().defaultRandom(), // Link to core agent agentId: uuid('agent_id') .notNull() .references(() => agentTable.id), // User identification externalId: text('external_id').unique(), username: text('username').notNull().unique(), email: text('email'), // User state status: text('status').default('active'), lastSeenAt: timestamp('last_seen_at'), // Flexible data profile: jsonb('profile').default(sql`'{}'::jsonb`), settings: jsonb('settings').default(sql`'{}'::jsonb`), // Timestamps createdAt: timestamp('created_at').default(sql`now()`).notNull(), updatedAt: timestamp('updated_at').default(sql`now()`).notNull(), }, (table) => ({ // Indexes for performance agentIdIdx: index('plugin_users_agent_id_idx').on(table.agentId), emailIdx: index('plugin_users_email_idx').on(table.email), })); ``` ### Event/Log Tables ```typescript export const pluginEventsTable = pgTable('plugin_events', { id: uuid('id').primaryKey().defaultRandom(), // Event classification type: text('type').notNull(), category: text('category'), severity: text('severity').default('info'), // Event context userId: uuid('user_id').references(() => pluginUsersTable.id), agentId: uuid('agent_id').references(() => agentTable.id), // Event data data: jsonb('data').notNull(), metadata: jsonb('metadata').default(sql`'{}'::jsonb`), // Timestamp (no updatedAt needed for immutable logs) createdAt: timestamp('created_at').default(sql`now()`).notNull(), }, (table) => ({ // Indexes for querying typeIdx: index('plugin_events_type_idx').on(table.type), userIdIdx: index('plugin_events_user_id_idx').on(table.userId), createdAtIdx: index('plugin_events_created_at_idx').on(table.createdAt), })); ``` ### Configuration Tables ```typescript export const pluginConfigTable = pgTable('plugin_config', { id: uuid('id').primaryKey().defaultRandom(), // Scope the configuration agentId: uuid('agent_id') .notNull() .references(() => agentTable.id, { onDelete: 'cascade' }), // Configuration identification key: text('key').notNull(), namespace: text('namespace').default('default'), // Configuration data value: jsonb('value').notNull(), description: text('description'), // Configuration metadata isSecret: boolean('is_secret').default(false), isActive: boolean('is_active').default(true), // Timestamps createdAt: timestamp('created_at').default(sql`now()`).notNull(), updatedAt: timestamp('updated_at').default(sql`now()`).notNull(), }, (table) => ({ // Unique constraint for key per agent/namespace uniqueKeyPerAgent: unique('plugin_config_agent_namespace_key_unique') .on(table.agentId, table.namespace, table.key), })); ``` ## Advanced Features ### Composite Primary Keys ```typescript export const pluginUserRolesTable = pgTable('plugin_user_roles', { userId: uuid('user_id') .notNull() .references(() => pluginUsersTable.id, { onDelete: 'cascade' }), roleId: uuid('role_id') .notNull() .references(() => pluginRolesTable.id, { onDelete: 'cascade' }), assignedAt: timestamp('assigned_at').default(sql`now()`).notNull(), assignedBy: uuid('assigned_by').references(() => pluginUsersTable.id), }, (table) => ({ // Composite primary key pk: primaryKey({ columns: [table.userId, table.roleId] }), })); ``` ### Check Constraints ```typescript export const pluginProductsTable = pgTable('plugin_products', { id: uuid('id').primaryKey().defaultRandom(), name: text('name').notNull(), price: numeric('price', { precision: 10, scale: 2 }).notNull(), discountPrice: numeric('discount_price', { precision: 10, scale: 2 }), }, (table) => ({ // Ensure discount price is less than regular price priceCheck: check( 'plugin_products_price_check', sql`${table.discountPrice} < ${table.price} OR ${table.discountPrice} IS NULL` ), })); ``` ### Generated Columns ```typescript export const pluginOrdersTable = pgTable('plugin_orders', { id: uuid('id').primaryKey().defaultRandom(), // Regular columns subtotal: numeric('subtotal').notNull(), tax: numeric('tax').notNull(), shipping: numeric('shipping').notNull(), // Generated column total: numeric('total').generatedAlwaysAs( sql`${subtotal} + ${tax} + ${shipping}` ), }); ``` ## Querying Your Tables Once your tables are created, you can query them using Drizzle: ```typescript import { db } from '@elizaos/plugin-sql'; import { pluginUsersTable } from './schema/users'; // In your plugin service or action export class UserService { async createUser(data: any) { const [user] = await db .insert(pluginUsersTable) .values({ username: data.username, email: data.email, profile: data.profile, }) .returning(); return user; } async getUserById(id: string) { const [user] = await db .select() .from(pluginUsersTable) .where(eq(pluginUsersTable.id, id)); return user; } } ``` ## Best Practices ### 1. Prefix Table Names Use a consistent prefix for your plugin's tables: ```typescript // Good export const pluginUsersTable = pgTable('plugin_users', {...}); export const pluginSettingsTable = pgTable('plugin_settings', {...}); // Avoid generic names export const usersTable = pgTable('users', {...}); // Too generic ``` ### 2. Always Include Timestamps ```typescript createdAt: timestamp('created_at', { withTimezone: true }) .default(sql`now()`) .notNull(), updatedAt: timestamp('updated_at', { withTimezone: true }) .default(sql`now()`) .notNull(), ``` ### 3. Use JSONB Wisely JSONB is great for flexibility but don't overuse it: ```typescript // Good - structured data with flexibility profile: jsonb('profile').$type<{ avatar?: string; bio?: string; social?: { twitter?: string; github?: string; }; }>(), // Avoid - everything in JSONB data: jsonb('data'), // Too vague ``` ### 4. Index Foreign Keys Always index columns used in joins: ```typescript (table) => ({ userIdIdx: index('plugin_posts_user_id_idx').on(table.userId), createdAtIdx: index('plugin_posts_created_at_idx').on(table.createdAt), }) ``` ### 5. Handle Cascading Deletes Be explicit about deletion behavior: ```typescript // Cascade delete - removes dependent records .references(() => userTable.id, { onDelete: 'cascade' }) // Set null - preserves records but clears reference .references(() => userTable.id, { onDelete: 'set null' }) // Restrict - prevents deletion if dependencies exist .references(() => userTable.id, { onDelete: 'restrict' }) ``` ## Troubleshooting ### Tables Not Created 1. Ensure your plugin exports the schema: ```typescript export const plugin: Plugin = { schema, // Required! }; ``` 2. Check the logs for migration errors: ``` [ERROR] Failed to run migrations for plugin @company/my-plugin ``` 3. Verify table names don't conflict with PostgreSQL keywords ### Foreign Key Errors 1. Ensure referenced tables exist 2. Check that data types match exactly 3. Verify the referenced column has a unique constraint ### Performance Issues 1. Add indexes for frequently queried columns 2. Use partial indexes for filtered queries 3. Consider partitioning for large tables # Schema Management Source: https://docs.elizaos.ai/plugin-registry/sql/schema-management Dynamic schema management and migrations in the SQL plugin The SQL plugin provides a sophisticated dynamic migration system that automatically manages database schemas for plugins. This guide covers how the system works and how to define schemas for your plugins. ## Dynamic Migration System The SQL plugin uses a **dynamic migration service** that automatically creates and updates database tables based on plugin schemas. This eliminates the need for traditional migration files. ### How It Works 1. **Plugin Registration** - Plugins export their schema definitions 2. **Schema Discovery** - The migration service discovers all plugin schemas at startup 3. **Schema Introspection** - The system analyzes existing database tables 4. **Dynamic Migration** - Tables are created or updated as needed 5. **Dependency Resolution** - Tables are created in the correct order based on foreign key dependencies ### Key Components ```typescript // DatabaseMigrationService - Manages all plugin migrations export class DatabaseMigrationService { private registeredSchemas = new Map(); discoverAndRegisterPluginSchemas(plugins: Plugin[]): void { for (const plugin of plugins) { if (plugin.schema) { this.registeredSchemas.set(plugin.name, plugin.schema); } } } async runAllPluginMigrations(): Promise { for (const [pluginName, schema] of this.registeredSchemas) { await runPluginMigrations(this.db!, pluginName, schema); } } } ``` ## Defining Plugin Schemas To enable automatic schema management, plugins must export their Drizzle schema definitions: ### Plugin Structure ```typescript import { Plugin } from '@elizaos/core'; import { pgTable, uuid, text, timestamp, jsonb } from 'drizzle-orm/pg-core'; // Define your schema export const myTable = pgTable('my_table', { id: uuid('id').primaryKey().defaultRandom(), name: text('name').notNull(), metadata: jsonb('metadata').default(sql`'{}'::jsonb`), createdAt: timestamp('created_at').defaultNow(), }); // Export as part of plugin export const myPlugin: Plugin = { name: 'my-plugin', schema: { myTable, // Add other tables here }, // ... other plugin properties }; ``` ## Core Schema Tables The SQL plugin provides these core tables that all agents use: ### Agent Tables * `agents` - Core agent identity * `memories` - Agent memory storage * `entities` - People, objects, and concepts * `relationships` - Connections between entities ### Communication Tables * `rooms` - Conversation contexts * `participants` - Room membership * `messages` - Message history ### System Tables * `logs` - System event logging * `cache` - Key-value cache with composite primary key * `tasks` - Background task management * `embeddings` - Vector embeddings for similarity search ## Schema Introspection The system uses `DrizzleSchemaIntrospector` to analyze database schemas: ```typescript export class DrizzleSchemaIntrospector { parseTableDefinition(table: any, exportKey?: string): TableDefinition { const columns = this.parseColumns(table); const foreignKeys = this.parseForeignKeys(table); const indexes = this.parseIndexes(table); const checkConstraints = this.parseCheckConstraints(table); const compositePrimaryKey = this.parseCompositePrimaryKey(table); return { name: tableName, columns, indexes, foreignKeys, checkConstraints, compositePrimaryKey, dependencies, // Tables this table depends on }; } } ``` ## Migration Process The dynamic migrator handles various scenarios: ### Table Creation ```typescript // Automatically generates CREATE TABLE statements await createTable(db, tableDefinition); ``` ### Column Addition ```typescript // Detects and adds missing columns if (!existingColumns.has(column.name)) { await addColumn(db, tableName, column); } ``` ### Index Management ```typescript // Creates missing indexes for (const index of tableDefinition.indexes) { if (!existingIndexes.has(index.name)) { await createIndex(db, tableName, index); } } ``` ### Foreign Key Constraints ```typescript // Adds foreign keys after all tables exist for (const fk of tableDefinition.foreignKeys) { await addForeignKey(db, tableName, fk); } ``` ## Best Practices ### 1. Schema Design * Use UUIDs for primary keys * Include timestamps (created\_at, updated\_at) * Use JSONB for flexible metadata * Define proper indexes for query performance ### 2. Foreign Keys * Always reference existing tables * Consider cascade options carefully * Be aware of circular dependencies ### 3. Composite Keys ```typescript // Example: Cache table with composite primary key export const cacheTable = pgTable('cache', { key: text('key').notNull(), agentId: uuid('agent_id').notNull(), value: jsonb('value').notNull(), createdAt: timestamp('created_at').defaultNow(), }, (table) => { return { pk: primaryKey(table.key, table.agentId), }; }); ``` ### 4. Plugin Schema Organization ```typescript // Organize related tables together export const schema = { // Core tables users: userTable, profiles: profileTable, // Feature tables posts: postTable, comments: commentTable, // Junction tables userFollows: userFollowsTable, }; ``` ## Error Handling The migration system includes robust error handling: * **Duplicate Tables** - Silently skipped * **Missing Dependencies** - Tables created in dependency order * **Failed Migrations** - Detailed error logging with rollback * **Schema Conflicts** - Clear error messages for debugging ## Limitations and Considerations 1. **No Downgrades** - The system only adds, never removes 2. **Column Type Changes** - Not automatically handled 3. **Data Migrations** - Must be handled separately 4. **Production Use** - Test thoroughly before deploying schema changes ## Example: Complete Plugin Schema ```typescript import { Plugin } from '@elizaos/core'; import { pgTable, uuid, text, timestamp, jsonb, boolean, integer } from 'drizzle-orm/pg-core'; import { sql } from 'drizzle-orm'; // Define tables export const projectTable = pgTable('projects', { id: uuid('id').primaryKey().defaultRandom(), agentId: uuid('agent_id').notNull(), name: text('name').notNull(), description: text('description'), status: text('status').default('active'), metadata: jsonb('metadata').default(sql`'{}'::jsonb`), createdAt: timestamp('created_at').defaultNow(), updatedAt: timestamp('updated_at').defaultNow(), }); export const taskTable = pgTable('project_tasks', { id: uuid('id').primaryKey().defaultRandom(), projectId: uuid('project_id').notNull().references(() => projectTable.id), title: text('title').notNull(), completed: boolean('completed').default(false), priority: integer('priority').default(0), dueDate: timestamp('due_date'), createdAt: timestamp('created_at').defaultNow(), }); // Create indexes export const projectIndexes = { agentIdIdx: index('project_agent_id_idx').on(projectTable.agentId), statusIdx: index('project_status_idx').on(projectTable.status), }; // Export plugin export const projectPlugin: Plugin = { name: 'project-management', schema: { projectTable, taskTable, ...projectIndexes, }, // ... other plugin properties }; ``` This schema will be automatically created when the agent starts, with all tables, columns, indexes, and foreign keys properly configured. # Architecture Source: https://docs.elizaos.ai/plugins/architecture Core plugin system architecture and lifecycle in elizaOS ## Overview The elizaOS plugin system is a comprehensive extension mechanism that allows developers to add functionality to agents through a well-defined interface. Plugins are modular extensions that enhance AI agents with new capabilities, integrations, and behaviors. ### What Can Plugins Do? * **Platform Integrations**: Connect to Discord, Telegram, Slack, Twitter, etc. * **LLM Providers**: Integrate different AI models (OpenAI, Anthropic, Google, etc.) * **Blockchain/DeFi**: Execute transactions, manage wallets, interact with smart contracts * **Data Sources**: Connect to databases, APIs, or external services * **Custom Actions**: Define new agent behaviors and capabilities **Guide**: [Create a Plugin](/guides/create-a-plugin) ## Plugin Interface Every plugin must implement the core `Plugin` interface, which defines the structure and capabilities of a plugin. The interface includes: * **Identity**: `name` and `description` to identify the plugin * **Initialization**: Optional `init` function for setup logic * **Components**: Arrays of `actions`, `providers`, `evaluators`, and `services` * **Configuration**: Settings and environment variables via `config` * **Extensions**: Optional database adapters, model handlers, routes, and event handlers * **Dependencies**: Other plugins this plugin requires * **Priority**: Loading order when multiple plugins are present For the complete TypeScript interface definition, see [Plugin Reference](/plugins/reference#plugin-interface). ## Plugin Initialization Lifecycle Based on the runtime implementation, the initialization process follows a specific order: ### 1. Plugin Registration (`registerPlugin` method) When a plugin is registered with the runtime: 1. Validates plugin has a name 2. Checks for duplicate plugins 3. Adds to active plugins list 4. Calls plugin's `init()` method if present 5. Handles configuration errors gracefully ### 2. Component Registration Order Components are registered in this specific sequence. For component details, see [Plugin Components](/plugins/components). ```typescript // 1. Database adapter (if provided) if (plugin.adapter) { this.registerDatabaseAdapter(plugin.adapter); } // 2. Actions if (plugin.actions) { for (const action of plugin.actions) { this.registerAction(action); } } // 3. Evaluators if (plugin.evaluators) { for (const evaluator of plugin.evaluators) { this.registerEvaluator(evaluator); } } // 4. Providers if (plugin.providers) { for (const provider of plugin.providers) { this.registerProvider(provider); } } // 5. Models if (plugin.models) { for (const [modelType, handler] of Object.entries(plugin.models)) { this.registerModel(modelType, handler, plugin.name, plugin.priority); } } // 6. Routes if (plugin.routes) { for (const route of plugin.routes) { this.routes.push(route); } } // 7. Events if (plugin.events) { for (const [eventName, eventHandlers] of Object.entries(plugin.events)) { for (const eventHandler of eventHandlers) { this.registerEvent(eventName, eventHandler); } } } // 8. Services (delayed if runtime not initialized) if (plugin.services) { for (const service of plugin.services) { if (this.isInitialized) { await this.registerService(service); } else { this.servicesInitQueue.add(service); } } } ``` ## Route Definitions for HTTP Endpoints Plugins can expose HTTP endpoints through the route system: ```typescript export type Route = { type: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'STATIC'; path: string; filePath?: string; // For static files public?: boolean; // Public access name?: string; // Route name handler?: (req: any, res: any, runtime: IAgentRuntime) => Promise; isMultipart?: boolean; // File uploads }; ``` Example route implementation: ```typescript routes: [ { name: 'hello-world-route', path: '/helloworld', type: 'GET', handler: async (_req: any, res: any) => { res.json({ message: 'Hello World!' }); } } ] ``` ## Event System Integration Plugins can handle system events through the event system: ### Event Types Standard events include: * World events: WORLD\_JOINED, WORLD\_CONNECTED, WORLD\_LEFT * Entity events: ENTITY\_JOINED, ENTITY\_LEFT, ENTITY\_UPDATED * Room events: ROOM\_JOINED, ROOM\_LEFT * Message events: MESSAGE\_RECEIVED, MESSAGE\_SENT, MESSAGE\_DELETED * Voice events: VOICE\_MESSAGE\_RECEIVED, VOICE\_MESSAGE\_SENT * Run events: RUN\_STARTED, RUN\_ENDED, RUN\_TIMEOUT * Action/Evaluator events: ACTION\_STARTED/COMPLETED, EVALUATOR\_STARTED/COMPLETED * Model events: MODEL\_USED ### Plugin Event Handlers ```typescript export type PluginEvents = { [K in keyof EventPayloadMap]?: EventHandler[]; } & { [key: string]: ((params: any) => Promise)[]; }; ``` ## Database Adapter Plugins Plugins can provide database adapters for custom storage backends: The IDatabaseAdapter interface is extensive, including methods for: * Agents, Entities, Components * Memories (with embeddings) * Rooms, Participants * Relationships * Tasks * Caching * Logs Example database adapter plugin: ```typescript export const plugin: Plugin = { name: '@elizaos/plugin-sql', description: 'A plugin for SQL database access with dynamic schema migrations', priority: 0, schema, init: async (_, runtime: IAgentRuntime) => { const dbAdapter = createDatabaseAdapter(config, runtime.agentId); runtime.registerDatabaseAdapter(dbAdapter); } }; ``` ## Plugin Priority System Plugins can specify a priority to control loading order: * Higher priority plugins are loaded first * Useful for plugins that provide fundamental services * Model handlers use priority to determine which provider handles a model type ```typescript export const myPlugin: Plugin = { name: 'high-priority-plugin', priority: 100, // Loads before lower priority plugins // ... }; ``` ## Plugin Dependencies Plugins can declare dependencies on other plugins: ```typescript export const myPlugin: Plugin = { name: 'my-plugin', dependencies: ['@elizaos/plugin-sql', '@elizaos/plugin-bootstrap'], testDependencies: ['@elizaos/plugin-test-utils'], // ... }; ``` The runtime ensures dependencies are loaded before dependent plugins. ## Plugin Configuration Plugins can accept configuration through multiple mechanisms: ### 1. Environment Variables ```typescript init: async (config, runtime) => { const apiKey = runtime.getSetting('MY_API_KEY'); if (!apiKey) { throw new Error('MY_API_KEY not configured'); } } ``` ### 2. Config Object ```typescript export const myPlugin: Plugin = { name: 'my-plugin', config: { defaultTimeout: 5000, retryAttempts: 3, }, // ... }; ``` ### 3. Runtime Settings Settings can be accessed through `runtime.getSetting()` which provides a consistent interface to environment variables and character settings. ## Conditional Plugin Loading Plugins are often conditionally loaded based on environment variables: ```typescript const plugins = [ // Always loaded '@elizaos/plugin-bootstrap', // Conditionally loaded based on API keys ...(process.env.ANTHROPIC_API_KEY ? ['@elizaos/plugin-anthropic'] : []), ...(process.env.OPENAI_API_KEY ? ['@elizaos/plugin-openai'] : []), // Platform plugins ...(process.env.DISCORD_API_TOKEN ? ['@elizaos/plugin-discord'] : []), ...(process.env.TELEGRAM_BOT_TOKEN ? ['@elizaos/plugin-telegram'] : []), ]; ``` ## Core Plugins elizaOS includes two essential core plugins that provide foundational functionality: ### Bootstrap Plugin The core message handler and event system for elizaOS agents. Provides essential functionality for message processing, knowledge management, and basic agent operations. It includes: * 13 essential actions (REPLY, SEND\_MESSAGE, etc.) * Core providers (time, character, recent messages) * Task service * Event handlers ### SQL Plugin Database integration and management for elizaOS. Features: * Automatic schema migrations * Multi-database support (PostgreSQL, PGLite) * Sophisticated plugin architecture ## Best Practices 1. **Plugin Dependencies**: Use the `dependencies` array to specify required plugins 2. **Conditional Loading**: Check environment variables before loading platform-specific plugins 3. **Service Initialization**: Handle missing API tokens gracefully in service constructors 4. **Event Handlers**: Keep event handlers focused and delegate to specialized functions 5. **Error Handling**: Use try-catch blocks and log errors appropriately 6. **Type Safety**: Use TypeScript types from `@elizaos/core` for all plugin components 7. **Priority Management**: Set appropriate priorities for plugins that need to load early 8. **Configuration**: Use `runtime.getSetting()` for consistent configuration access ## What's Next? Learn about Actions, Providers, Evaluators, and Services Build your first plugin step by step Learn proven plugin development patterns Complete API reference for all interfaces # Components Source: https://docs.elizaos.ai/plugins/components Actions, Providers, Evaluators, and Services - the building blocks of elizaOS plugins ## Overview Plugin components are the building blocks that give agents their capabilities. Each component type serves a specific purpose in the agent's decision-making and interaction flow. For system architecture, see [Plugin Architecture](/plugins/architecture). ## Component Types | Component | Purpose | When Executed | | -------------- | ---------------------------------- | --------------------------------- | | **Actions** | Tasks agents can perform | When agent decides to take action | | **Providers** | Supply contextual data | Before actions/decisions | | **Evaluators** | Process and extract from responses | After agent generates response | | **Services** | Manage stateful connections | Throughout agent lifecycle | ## Actions Actions are discrete tasks agents can perform. They represent the agent's capabilities - what it can DO. ### Action Interface Actions define discrete tasks that agents can perform. Each action has: * **name**: Unique identifier for the action * **description**: Clear explanation of what the action does * **similes**: Alternative names or aliases for fuzzy matching * **examples**: Training examples showing when to use the action * **validate**: Function to check if the action can run in the current context * **handler**: The execution logic that returns an `ActionResult` The handler receives the runtime, message, state, options (for action chaining), an optional callback for intermediate responses, and previous responses. **Important**: All action handlers must return an `ActionResult` with a `success` field indicating whether the action completed successfully. For complete interface definitions, see [Plugin Reference](/plugins/reference#action-interface). ### Core Actions (Bootstrap Plugin) The bootstrap plugin provides 13 essential actions: #### Communication Actions | Action | Description | Example Trigger | | -------------- | --------------------- | --------------------- | | `REPLY` | Generate response | "Tell me about..." | | `SEND_MESSAGE` | Send to specific room | "Message the team..." | | `NONE` | Acknowledge silently | "Thanks!" | | `IGNORE` | Skip message | Spam/irrelevant | #### Room Management | Action | Description | Example Trigger | | --------------- | -------------------- | ------------------- | | `FOLLOW_ROOM` | Subscribe to updates | "Join #general" | | `UNFOLLOW_ROOM` | Unsubscribe | "Leave #general" | | `MUTE_ROOM` | Mute notifications | "Mute this channel" | | `UNMUTE_ROOM` | Unmute | "Unmute #general" | #### Data & Configuration | Action | Description | Example Trigger | | ----------------- | ------------------- | -------------------- | | `UPDATE_CONTACT` | Update contact info | "Remember that I..." | | `UPDATE_ROLE` | Change roles | "Make me admin" | | `UPDATE_SETTINGS` | Modify settings | "Set model to gpt-4" | #### Media & Utilities | Action | Description | Example Trigger | | ---------------- | ---------------- | ------------------ | | `GENERATE_IMAGE` | Create AI images | "Draw a cat" | | `CHOICE` | Present options | "Should I A or B?" | ### Creating Actions For advanced patterns, see [Plugin Patterns](/plugins/patterns). #### Minimal Action ```typescript const action: Action = { name: 'MY_ACTION', description: 'Does something', validate: async () => true, handler: async (runtime, message) => { return { success: true, // REQUIRED text: "Done!" }; } }; ``` #### With Validation ```typescript const sendTokenAction: Action = { name: 'SEND_TOKEN', description: 'Send tokens to address', validate: async (runtime, message) => { return message.content.includes('send') && message.content.includes('0x'); }, handler: async (runtime, message) => { const address = extractAddress(message.content); const amount = extractAmount(message.content); await sendToken(address, amount); return { success: true, text: `Sent ${amount} tokens to ${address}` }; } }; ``` #### With Examples ```typescript const action: Action = { name: 'WEATHER', description: 'Get weather info', examples: [[ { name: "user", content: { text: "What's the weather?" } }, { name: "agent", content: { text: "Let me check the weather for you." } } ]], validate: async (runtime, message) => { return message.content.toLowerCase().includes('weather'); }, handler: async (runtime, message) => { const weather = await fetchWeather(); return { success: true, text: `It's ${weather.temp}°C and ${weather.condition}` }; } }; ``` ### Handler Patterns ```typescript // Using callbacks handler: async (runtime, message, state, options, callback) => { const result = await doWork(); if (callback) { await callback({ text: result }, []); } return { success: true, text: result }; } // Using services handler: async (runtime, message) => { const service = runtime.getService('twitter'); return service.post(message.content); } // Using database handler: async (runtime, message) => { const memories = await runtime.databaseAdapter.searchMemories({ query: message.content, limit: 5 }); return { success: true, data: { memories } }; } ``` ### Best Practices for Actions * Name actions clearly (VERB\_NOUN format) * Always return ActionResult with `success` field * Validate before executing * Return consistent response format * Use similes for alternative triggers * Provide diverse examples * Handle errors gracefully ## Providers Providers supply contextual information to the agent's state before it makes decisions. They act as the agent's "senses", gathering relevant data. ### Provider Interface Providers supply contextual data to enhance agent decision-making. Each provider has: * **name**: Unique identifier for the provider * **description**: Optional explanation of what data it provides * **dynamic**: If true, data is re-fetched each time (not cached) * **position**: Execution order priority (-100 to 100, lower runs first) * **private**: If true, hidden from the default provider list * **get**: Function that returns a `ProviderResult` with text, values, and data The `get` function receives the runtime, current message, and state, returning data that will be composed into the agent's context. For complete interface definitions, see the [Provider Interface in the Reference](/plugins/reference#provider-interface). ### Core Providers (Bootstrap Plugin) | Provider | Returns | Example Use | | ------------------------ | ----------------- | -------------------- | | `characterProvider` | Agent personality | Name, bio, traits | | `timeProvider` | Current date/time | "What time is it?" | | `knowledgeProvider` | Knowledge base | Documentation, facts | | `recentMessagesProvider` | Chat history | Context awareness | | `actionsProvider` | Available actions | "What can you do?" | | `factsProvider` | Stored facts | User preferences | | `settingsProvider` | Configuration | Model settings | ### Creating Providers #### Basic Provider ```typescript const provider: Provider = { name: 'MY_DATA', get: async (runtime, message, state) => { return { text: "Contextual information", data: { key: "value" } }; } }; ``` #### Dynamic Provider ```typescript const dynamicProvider: Provider = { name: 'LIVE_DATA', dynamic: true, // Re-fetched each time get: async (runtime) => { const data = await fetchLatestData(); return { data }; } }; ``` #### Private Provider ```typescript const secretProvider: Provider = { name: 'INTERNAL_STATE', private: true, // Not shown in provider list get: async (runtime) => { return runtime.getInternalState(); } }; ``` ### Provider Priority ```typescript // Lower numbers = higher priority position: -100 // Loads first position: 0 // Default position: 100 // Loads last ``` ### Provider Execution Flow 1. Providers are executed during `runtime.composeState()` 2. By default, all non-private, non-dynamic providers are included 3. Providers are sorted by position and executed in order 4. Results are aggregated into a unified state object 5. The composed state is passed to actions and the LLM for decision-making ### Best Practices for Providers * Return consistent data structures * Handle errors gracefully * Cache when appropriate * Keep data fetching fast * Document what data is provided * Use position to control execution order ## Evaluators Evaluators are post-processors that analyze and extract information from conversations. ### Evaluator Interface Evaluators process and extract information from agent responses. Each evaluator has: * **name**: Unique identifier for the evaluator * **description**: Explanation of what it evaluates or extracts * **similes**: Alternative names for matching * **alwaysRun**: If true, runs on every agent response * **examples**: Training examples for the evaluator * **validate**: Function to determine if evaluator should run * **handler**: Processing logic that analyzes the response Evaluators run after an agent generates a response, allowing for fact extraction, sentiment analysis, or content filtering. For complete interface definitions, see the [Evaluator Interface in the Reference](/plugins/reference#evaluator-interface). ### Core Evaluators (Bootstrap Plugin) | Evaluator | Purpose | Extracts | | --------------------- | --------------- | --------------------------- | | `reflectionEvaluator` | Self-awareness | Insights about interactions | | `factEvaluator` | Fact extraction | Important information | | `goalEvaluator` | Goal tracking | User objectives | ### Evaluator Flow ```mermaid flowchart TD Response[Agent Response] --> Validate[validate] Validate -->|true| Handler[handler] Validate -->|false| Skip[Skip] Handler --> Extract[Extract Info] Extract --> Store[Store in Memory] Store --> Continue[Continue] Skip --> Continue classDef input fill:#2196f3,color:#fff classDef decision fill:#ff9800,color:#fff classDef processing fill:#4caf50,color:#fff classDef storage fill:#9c27b0,color:#fff classDef result fill:#607d8b,color:#fff class Response input class Validate decision class Handler,Extract processing class Store storage class Skip,Continue result ``` ### Common Use Cases #### Memory Building * Extract facts from conversations * Track user preferences * Update relationship status * Record important events #### Content Filtering * Remove sensitive data * Filter profanity * Ensure compliance * Validate accuracy #### Analytics * Track sentiment * Measure engagement * Monitor topics * Analyze patterns ### Creating Evaluators #### Basic Evaluator ```typescript const evaluator: Evaluator = { name: 'my-evaluator', description: 'Processes responses', examples: [], // Training examples validate: async (runtime, message) => { return true; // Run on all messages }, handler: async (runtime, message) => { // Process and extract const result = await analyze(message); // Store findings await storeResult(result); return result; } }; ``` #### With Examples ```typescript const evaluator: Evaluator = { name: 'fact-extractor', description: 'Extracts facts from conversations', examples: [{ prompt: 'Extract facts from this conversation', messages: [ { name: 'user', content: { text: 'I live in NYC' } }, { name: 'agent', content: { text: 'NYC is a great city!' } } ], outcome: 'User lives in New York City' }], validate: async () => true, handler: async (runtime, message, state) => { const facts = await extractFacts(state); for (const fact of facts) { await runtime.factsManager.addFact(fact); } return facts; } }; ``` ### Best Practices for Evaluators * Run evaluators async (don't block responses) * Store extracted data for future context * Use `alwaysRun: true` sparingly * Provide clear examples for training * Keep handlers lightweight ## Services Services manage stateful connections and provide core functionality. They are singleton instances that persist throughout the agent's lifecycle. ### Service Abstract Class Services are singleton instances that manage stateful connections and provide persistent functionality throughout the agent's lifecycle. Services extend an abstract class with: * **serviceType**: Static property identifying the service type * **capabilityDescription**: Description of what the service provides * **start()**: Static method to initialize and start the service * **stop()**: Method to clean up resources when shutting down * **config**: Optional configuration metadata Services are ideal for managing database connections, API clients, WebSocket connections, or any long-running background tasks. For the complete Service class definition, see the [Service Abstract Class in the Reference](/plugins/reference#service-abstract-class). ### Service Types The system includes predefined service types: * TRANSCRIPTION, VIDEO, BROWSER, PDF * REMOTE\_FILES (AWS S3) * WEB\_SEARCH, EMAIL, TEE * TASK, WALLET, LP\_POOL, TOKEN\_DATA * DATABASE\_MIGRATION * PLUGIN\_MANAGER, PLUGIN\_CONFIGURATION, PLUGIN\_USER\_INTERACTION ### Creating Services ```typescript import { Service, IAgentRuntime, logger } from '@elizaos/core'; export class MyService extends Service { static serviceType = 'my-service'; capabilityDescription = 'Description of what this service provides'; private client: any; private refreshInterval: NodeJS.Timer | null = null; constructor(protected runtime: IAgentRuntime) { super(); } static async start(runtime: IAgentRuntime): Promise { logger.info('Initializing MyService'); const service = new MyService(runtime); // Initialize connections, clients, etc. await service.initialize(); // Set up periodic tasks if needed service.refreshInterval = setInterval( () => service.refreshData(), 60000 // 1 minute ); return service; } async stop(): Promise { // Cleanup resources if (this.refreshInterval) { clearInterval(this.refreshInterval); } // Close connections if (this.client) { await this.client.disconnect(); } logger.info('MyService stopped'); } private async initialize(): Promise { // Service initialization logic const apiKey = this.runtime.getSetting('MY_API_KEY'); if (!apiKey) { throw new Error('MY_API_KEY not configured'); } this.client = new MyClient({ apiKey }); await this.client.connect(); } } ``` ### Service Lifecycle Patterns #### Delayed Initialization Sometimes services need to wait for other services or perform startup tasks: ```typescript export class MyService extends Service { static serviceType = 'my-service'; static async start(runtime: IAgentRuntime): Promise { const service = new MyService(runtime); // Immediate initialization await service.initialize(); // Delayed initialization for non-critical tasks setTimeout(async () => { try { await service.loadCachedData(); await service.syncWithRemote(); logger.info('MyService: Delayed initialization complete'); } catch (error) { logger.error('MyService: Delayed init failed', error); // Don't throw - service is still functional } }, 5000); return service; } } ``` ### Best Practices for Services * Handle missing API tokens gracefully * Implement proper cleanup in `stop()` * Use delayed initialization for non-critical tasks * Log service lifecycle events * Make services resilient to failures * Keep service instances stateless when possible ## Component Interaction ### Execution Flow 1. **Providers** gather context → compose state 2. **Actions** validate against state → execute if valid 3. **Evaluators** process responses → extract information 4. **Services** provide persistent functionality throughout ### State Composition ```typescript // Providers contribute to state const state = await runtime.composeState(message, [ 'RECENT_MESSAGES', 'CHARACTER', 'KNOWLEDGE' ]); // Actions receive composed state const result = await action.handler(runtime, message, state); // Evaluators process with full context await evaluator.handler(runtime, message, state); ``` ### Service Access ```typescript // Actions and providers can access services const service = runtime.getService('my-service'); const data = await service.getData(); ``` ## Plugin Example with All Components ```typescript import type { Plugin } from '@elizaos/core'; export const myPlugin: Plugin = { name: 'my-complete-plugin', description: 'Example plugin with all component types', services: [MyService], actions: [{ name: 'MY_ACTION', description: 'Performs an action using the service', validate: async (runtime) => { return runtime.getService('my-service') !== null; }, handler: async (runtime, message) => { const service = runtime.getService('my-service'); const result = await service.doSomething(); return { success: true, text: result }; } }], providers: [{ name: 'MY_PROVIDER', get: async (runtime) => { const service = runtime.getService('my-service'); const data = await service.getCurrentState(); return { text: `Current state: ${JSON.stringify(data)}`, data }; } }], evaluators: [{ name: 'MY_EVALUATOR', description: 'Extracts relevant information', examples: [], validate: async () => true, handler: async (runtime, message) => { const extracted = await extractInfo(message); await runtime.storeExtracted(extracted); return extracted; } }] }; ``` **Guide**: [Create a Plugin](/guides/create-a-plugin) ## What's Next? Understand overall plugin system design Build your first plugin step by step Learn proven plugin development patterns Complete API reference for all interfaces # Development Source: https://docs.elizaos.ai/plugins/development Creating, building, and testing elizaOS plugins This guide covers all aspects of plugin development in the elizaOS system, from scaffolding to testing. This guide uses `bun` as the package manager, which is the preferred tool for elizaOS development. Bun provides faster installation times and built-in TypeScript support. ## Quick Start: Scaffolding Plugins with CLI The easiest way to create a new plugin is using the elizaOS CLI, which provides interactive scaffolding with pre-configured templates. ### Using `elizaos create` The CLI offers two plugin templates to get you started quickly: ```bash # Interactive plugin creation elizaos create # Or specify the name directly elizaos create my-plugin --type plugin ``` When creating a plugin, you'll be prompted to choose between: 1. **Quick Plugin (Backend Only)** - Simple backend-only plugin without frontend * Perfect for: API integrations, blockchain actions, data providers * Includes: Basic plugin structure, actions, providers, services * No frontend components or UI routes 2. **Full Plugin (with Frontend)** - Complete plugin with React frontend and API routes * Perfect for: Plugins that need web UI, dashboards, or visual components * Includes: Everything from Quick Plugin + React frontend, Vite setup, API routes * Tailwind CSS pre-configured for styling ### Quick Plugin Structure After running `elizaos create` and selecting "Quick Plugin", you'll get: ``` plugin-my-plugin/ ├── src/ │ ├── index.ts # Plugin manifest │ ├── actions/ # Your agent actions │ │ └── example.ts │ ├── providers/ # Context providers │ │ └── example.ts │ └── types/ # TypeScript types │ └── index.ts ├── package.json # Pre-configured with elizaos deps ├── tsconfig.json # TypeScript config ├── tsup.config.ts # Build configuration └── README.md # Plugin documentation ``` ### Full Plugin Structure Selecting "Full Plugin" adds frontend capabilities: ```text plugin-my-plugin/ ├── src/ │ ├── index.ts # Plugin manifest with routes │ ├── actions/ │ ├── providers/ │ ├── types/ │ └── frontend/ # React frontend │ ├── App.tsx │ ├── main.tsx │ └── components/ ├── public/ # Static assets ├── index.html # Frontend entry ├── vite.config.ts # Vite configuration ├── tailwind.config.js # Tailwind setup └── [other config files] ``` ### After Scaffolding Once your plugin is created: ```bash # Navigate to your plugin cd plugin-my-plugin # Install dependencies (automatically done by CLI) bun install # Start development mode with hot reloading elizaos dev # Or start in production mode elizaos start # Build your plugin for distribution bun run build ``` The scaffolded plugin includes: * ✅ Proper TypeScript configuration * ✅ Build setup with tsup (and Vite for full plugins) * ✅ Example action and provider to extend * ✅ Integration with `@elizaos/core` * ✅ Development scripts ready to use * ✅ Basic tests structure The CLI templates follow all elizaOS conventions and best practices, making it easy to get started without worrying about configuration. ## Manual Plugin Creation If you prefer to create a plugin manually or need custom configuration: ### 1. Initialize the Project ```bash mkdir plugin-my-custom cd plugin-my-custom bun init ``` ### 2. Install Dependencies ```bash # Core dependency bun add @elizaos/core # Development dependencies bun add -d typescript tsup @types/node ``` ### 3. Configure TypeScript Create `tsconfig.json`: ```json { "compilerOptions": { "target": "ES2022", "module": "ESNext", "moduleResolution": "bundler", "lib": ["ES2022"], "rootDir": "./src", "outDir": "./dist", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "declaration": true, "declarationMap": true, "sourceMap": true, "types": ["node"] }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"] } ``` ### 4. Configure Build Create `tsup.config.ts`: ```typescript import { defineConfig } from 'tsup'; export default defineConfig({ entry: ['src/index.ts'], format: ['esm'], dts: true, splitting: false, sourcemap: true, clean: true, external: ['@elizaos/core'], }); ``` ### 5. Create Plugin Structure Create `src/index.ts`: ```typescript import type { Plugin } from '@elizaos/core'; import { myAction } from './actions/myAction'; import { myProvider } from './providers/myProvider'; import { MyService } from './services/myService'; export const myPlugin: Plugin = { name: 'my-custom-plugin', description: 'A custom plugin for elizaOS', actions: [myAction], providers: [myProvider], services: [MyService], init: async (config, runtime) => { console.log('Plugin initialized'); } }; export default myPlugin; ``` ### 6. Update package.json ```json { "name": "@myorg/plugin-custom", "version": "0.1.0", "main": "dist/index.js", "types": "dist/index.d.ts", "type": "module", "scripts": { "build": "tsup", "dev": "tsup --watch", "test": "bun test" } } ``` ## Using Your Plugin in Projects ### Option 1: Plugin Inside the Monorepo If developing within the elizaOS monorepo: 1. Add your plugin to the root `package.json` as a workspace dependency: ```json { "dependencies": { "@yourorg/plugin-myplugin": "workspace:*" } } ``` 2. Run `bun install` in the root directory 3. Use the plugin in your project: ```typescript import { myPlugin } from '@yourorg/plugin-myplugin'; const agent = { name: 'MyAgent', plugins: [myPlugin], }; ``` ### Option 2: Plugin Outside the Monorepo For plugins outside the elizaOS monorepo: 1. In your plugin directory, build and link it: ```bash # In your plugin directory bun install bun run build bun link ``` 2. In your project directory, link the plugin: ```bash # In your project directory cd packages/project-starter bun link @yourorg/plugin-myplugin ``` 3. Add to your project's `package.json`: ```json { "dependencies": { "@yourorg/plugin-myplugin": "link:@yourorg/plugin-myplugin" } } ``` When using `bun link`, remember to rebuild your plugin (`bun run build`) after making changes for them to be reflected in your project. ## Testing Plugins ### Test Environment Setup #### Directory Structure ``` src/ __tests__/ test-utils.ts # Shared test utilities and mocks index.test.ts # Main plugin tests actions.test.ts # Action tests providers.test.ts # Provider tests evaluators.test.ts # Evaluator tests services.test.ts # Service tests actions/ providers/ evaluators/ services/ index.ts ``` #### Base Test Imports ```typescript import { describe, expect, it, mock, beforeEach, afterEach, spyOn } from 'bun:test'; import { type IAgentRuntime, type Memory, type State, type HandlerCallback, type Action, type Provider, type Evaluator, ModelType, logger, } from '@elizaos/core'; ``` ### Creating Test Utilities Create a `test-utils.ts` file with reusable mocks: ```typescript import { mock } from 'bun:test'; import { type IAgentRuntime, type Memory, type State, type Character, type UUID, } from '@elizaos/core'; // Mock Runtime Type export type MockRuntime = Partial & { agentId: UUID; character: Character; getSetting: ReturnType; useModel: ReturnType; composeState: ReturnType; createMemory: ReturnType; getMemories: ReturnType; getService: ReturnType; }; // Create Mock Runtime export function createMockRuntime(overrides?: Partial): MockRuntime { return { agentId: 'test-agent-123' as UUID, character: { name: 'TestAgent', bio: 'A test agent', id: 'test-character' as UUID, ...overrides?.character, }, getSetting: mock((key: string) => { const settings: Record = { TEST_API_KEY: 'test-key-123', ...overrides?.settings, }; return settings[key]; }), useModel: mock(async () => ({ content: 'Mock response from LLM', success: true, })), composeState: mock(async () => ({ values: { test: 'state' }, data: {}, text: 'Composed state', })), createMemory: mock(async () => ({ id: 'memory-123' })), getMemories: mock(async () => []), getService: mock(() => null), ...overrides, }; } // Create Mock Message export function createMockMessage(overrides?: Partial): Memory { return { id: 'msg-123' as UUID, entityId: 'entity-123' as UUID, roomId: 'room-123' as UUID, content: { text: 'Test message', ...overrides?.content, }, ...overrides, } as Memory; } // Create Mock State export function createMockState(overrides?: Partial): State { return { values: { test: 'value', ...overrides?.values, }, data: overrides?.data || {}, text: overrides?.text || 'Test state', } as State; } ``` ### Testing Actions ```typescript import { describe, it, expect, beforeEach } from 'bun:test'; import { myAction } from '../src/actions/myAction'; import { createMockRuntime, createMockMessage, createMockState } from './test-utils'; import { ActionResult } from '@elizaos/core'; describe('MyAction', () => { let mockRuntime: any; let mockMessage: Memory; let mockState: State; beforeEach(() => { mockRuntime = createMockRuntime({ settings: { MY_API_KEY: 'test-key' }, }); mockMessage = createMockMessage({ content: { text: 'Do the thing' } }); mockState = createMockState(); }); describe('validation', () => { it('should validate when all requirements are met', async () => { const isValid = await myAction.validate(mockRuntime, mockMessage, mockState); expect(isValid).toBe(true); }); it('should not validate without required service', async () => { mockRuntime.getService = mock(() => null); const isValid = await myAction.validate(mockRuntime, mockMessage, mockState); expect(isValid).toBe(false); }); }); describe('handler', () => { it('should return success ActionResult on successful execution', async () => { const mockCallback = mock(); const result = await myAction.handler( mockRuntime, mockMessage, mockState, {}, mockCallback ); expect(result.success).toBe(true); expect(result.text).toContain('completed'); expect(result.values).toHaveProperty('lastActionTime'); expect(mockCallback).toHaveBeenCalled(); }); it('should handle errors gracefully', async () => { // Make service throw error mockRuntime.getService = mock(() => { throw new Error('Service unavailable'); }); const result = await myAction.handler(mockRuntime, mockMessage, mockState); expect(result.success).toBe(false); expect(result.error).toBeDefined(); expect(result.text).toContain('Failed'); }); it('should access previous action results', async () => { const previousResults: ActionResult[] = [ { success: true, values: { previousData: 'test' }, data: { actionName: 'PREVIOUS_ACTION' }, }, ]; const result = await myAction.handler( mockRuntime, mockMessage, mockState, { context: { previousResults } } ); // Verify it used previous results expect(result.values?.usedPreviousData).toBe(true); }); }); describe('examples', () => { it('should have valid example structure', () => { expect(myAction.examples).toBeDefined(); expect(Array.isArray(myAction.examples)).toBe(true); // Each example should be a conversation array for (const example of myAction.examples!) { expect(Array.isArray(example)).toBe(true); // Each message should have name and content for (const message of example) { expect(message).toHaveProperty('name'); expect(message).toHaveProperty('content'); } } }); }); }); ``` ### Testing Providers ```typescript import { describe, it, expect, beforeEach } from 'bun:test'; import { myProvider } from '../src/providers/myProvider'; import { createMockRuntime, createMockMessage, createMockState } from './test-utils'; describe('MyProvider', () => { let mockRuntime: any; let mockMessage: Memory; let mockState: State; beforeEach(() => { mockRuntime = createMockRuntime(); mockMessage = createMockMessage(); mockState = createMockState(); }); it('should return provider result with text and data', async () => { const result = await myProvider.get(mockRuntime, mockMessage, mockState); expect(result).toBeDefined(); expect(result.text).toContain('Current'); expect(result.data).toBeDefined(); expect(result.values).toBeDefined(); }); it('should handle errors gracefully', async () => { // Mock service to throw error mockRuntime.getService = mock(() => { throw new Error('Service error'); }); const result = await myProvider.get(mockRuntime, mockMessage, mockState); expect(result.text).toContain('Unable'); expect(result.data?.error).toBeDefined(); }); }); ``` ### Testing Services ```typescript import { describe, it, expect, beforeEach, afterEach } from 'bun:test'; import { MyService } from '../src/services/myService'; import { createMockRuntime } from './test-utils'; describe('MyService', () => { let mockRuntime: any; let service: MyService; beforeEach(async () => { mockRuntime = createMockRuntime({ settings: { MY_API_KEY: 'test-api-key', }, }); }); afterEach(async () => { if (service) { await service.stop(); } }); it('should initialize successfully with valid config', async () => { service = await MyService.start(mockRuntime); expect(service).toBeDefined(); expect(service.capabilityDescription).toBeDefined(); }); it('should throw error without API key', async () => { mockRuntime.getSetting = mock(() => undefined); expect(async () => { await MyService.start(mockRuntime); }).toThrow('MY_API_KEY not configured'); }); it('should clean up resources on stop', async () => { service = await MyService.start(mockRuntime); await service.stop(); // Verify cleanup happened }); }); ``` ### E2E Testing For integration testing with a live runtime: ```typescript // tests/e2e/myPlugin.e2e.ts export const myPluginE2ETests = { name: 'MyPlugin E2E Tests', tests: [ { name: 'should execute full plugin flow', fn: async (runtime: IAgentRuntime) => { // Create test message const message: Memory = { id: generateId(), entityId: 'test-user', roomId: runtime.agentId, content: { text: 'Please do the thing', source: 'test', }, }; // Store message await runtime.createMemory(message, 'messages'); // Compose state const state = await runtime.composeState(message); // Execute action const result = await myAction.handler( runtime, message, state, {}, async (response) => { // Verify callback responses expect(response.text).toBeDefined(); } ); // Verify result expect(result.success).toBe(true); // Verify side effects const memories = await runtime.getMemories({ roomId: message.roomId, tableName: 'action_results', count: 1, }); expect(memories.length).toBeGreaterThan(0); }, }, ], }; ``` ### Running Tests ```bash # Run all tests bun test # Run specific test file bun test src/__tests__/actions.test.ts # Run with watch mode bun test --watch # Run with coverage bun test --coverage ``` ### Test Best Practices 1. **Test in Isolation**: Use mocks to isolate components 2. **Test Happy Path and Errors**: Cover both success and failure cases 3. **Test Validation Logic**: Ensure actions validate correctly 4. **Test Examples**: Verify example structures are valid 5. **Test Side Effects**: Verify database writes, API calls, etc. 6. **Use Descriptive Names**: Make test purposes clear 7. **Keep Tests Fast**: Mock external dependencies 8. **Test Public API**: Focus on what users interact with ## Development Workflow ### 1. Development Mode ```bash # Watch mode with hot reloading bun run dev # Or with elizaOS CLI elizaos dev ``` ### 2. Building for Production ```bash # Build the plugin bun run build # Output will be in dist/ ``` ### 3. Publishing #### To npm ```bash # Login to npm npm login # Publish npm publish --access public ``` #### To GitHub Packages Update `package.json`: ```json { "name": "@yourorg/plugin-name", "publishConfig": { "registry": "https://npm.pkg.github.com" } } ``` Then publish: ```bash npm publish ``` ### 4. Version Management ```bash # Bump version npm version patch # 0.1.0 -> 0.1.1 npm version minor # 0.1.0 -> 0.2.0 npm version major # 0.1.0 -> 1.0.0 ``` ## Debugging ### Enable Debug Logging ```typescript import { logger } from '@elizaos/core'; // In your plugin logger.debug('Plugin initialized', { config }); logger.info('Action executed', { result }); logger.error('Failed to connect', { error }); ``` ### VS Code Debug Configuration Create `.vscode/launch.json`: ```json { "version": "0.2.0", "configurations": [ { "type": "node", "request": "launch", "name": "Debug Plugin", "runtimeExecutable": "bun", "program": "${workspaceFolder}/src/index.ts", "cwd": "${workspaceFolder}", "console": "integratedTerminal" } ] } ``` ## Common Issues and Solutions ### Issue: Plugin not loading **Solution**: Check that your plugin is properly exported and added to the agent's plugin array. ### Issue: TypeScript errors **Solution**: Ensure `@elizaos/core` is installed and TypeScript is configured correctly. ### Issue: Service not available **Solution**: Verify the service is registered in the plugin and started properly. ### Issue: Tests failing with module errors **Solution**: Make sure your `tsconfig.json` has proper module resolution settings for Bun. ## What's Next? Deep dive into Actions, Providers, Evaluators, and Services Learn proven plugin development patterns Understand plugin configuration and validation Complete API reference for all interfaces # Migration Source: https://docs.elizaos.ai/plugins/migration Complete guide for migrating elizaOS plugins from version 0.x to 1.x > **Important**: This comprehensive guide will walk you through migrating your elizaOS plugins from version 0.x to 1.x. The migration process involves several key changes to architecture, APIs, and best practices. ## Migration Overview The 1.x architecture brings: * **Better modularity** - Cleaner separation of concerns * **Improved testing** - Easier to test individual components * **Enhanced flexibility** - More customization options * **Better performance** - Optimized runtime execution * **Stronger typing** - Catch errors at compile time ## Pre-Migration Checklist Before starting your migration: * [ ] Backup your existing plugin code * [ ] Review all breaking changes * [ ] Identify custom components that need migration * [ ] Plan your testing strategy * [ ] Allocate sufficient time for the migration ## Step 1: Create Version Branch Create a new branch for the 1.x version while preserving the main branch for backwards compatibility: ```bash git checkout -b 1.x ``` > **Note**: This branch will serve as your new 1.x version branch, keeping `main` intact for legacy support. ## Step 2: Remove Deprecated Files Clean up deprecated tooling and configuration files: ### Files to Remove: * **`biome.json`** - Deprecated linter configuration * **`vitest.config.ts`** - Replaced by Bun test runner * **Lock files** - Any `lock.json` or `yml.lock` files ### Quick Cleanup Commands: ```bash rm -rf vitest.config.ts rm -rf biome.json rm -f *.lock.json *.yml.lock ``` > **Why?** The elizaOS ecosystem has standardized on: > > * **Bun's built-in test runner** (replacing Vitest) > * **Prettier** for code formatting (replacing Biome) ## Step 3: Update package.json ### Version and Naming ```json { "version": "1.0.0", "name": "@elizaos/plugin-yourname" // Note: @elizaos, not @elizaos-plugins } ``` ### Dependencies ```json { "dependencies": { "@elizaos/core": "1.0.0" }, "devDependencies": { "tsup": "8.3.5", "prettier": "^3.0.0", "bun": "^1.2.15", // REQUIRED "@types/bun": "latest", // REQUIRED "typescript": "^5.0.0" } } ``` ### Scripts Section ```json "scripts": { "build": "tsup", "dev": "tsup --watch", "lint": "prettier --write ./src", "clean": "rm -rf dist .turbo node_modules", "test": "bun test", "publish": "npm publish --access public" } ``` ## Step 4: Update TypeScript Configuration Update `tsconfig.json`: ```json { "extends": "../../tsconfig.base.json", "compilerOptions": { "module": "esnext", "target": "esnext", "moduleResolution": "bundler", "outDir": "dist", "rootDir": "src", "types": ["bun-types"], "skipLibCheck": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true }, "include": ["src/**/*"], "exclude": ["node_modules", "dist", "__tests__", "**/*.test.ts"] } ``` ## Step 5: Update Core Package Imports ### Import Changes ```typescript // OLD (0.x) import { Action, Memory, State } from '@ai16z/eliza'; // NEW (1.x) import { Action, Memory, State, ActionResult } from '@elizaos/core'; ``` ## Step 6: Update Actions ### Action Signatures Actions in 1.x must return `ActionResult` and include callbacks: ```typescript // OLD (0.x) const myAction = { handler: async (runtime, message, state) => { return { text: "Response" }; } }; // NEW (1.x) const myAction = { handler: async (runtime, message, state, options, callback) => { // Use callback for intermediate responses await callback?.({ text: "Processing..." }); // Must return ActionResult return { success: true, // REQUIRED field text: "Response completed", values: { /* state updates */ }, data: { /* raw data */ } }; } }; ``` ### Common Action Patterns ```typescript // Error handling handler: async (runtime, message, state, options, callback) => { try { const result = await performAction(); return { success: true, text: `Action completed: ${result}`, data: result }; } catch (error) { return { success: false, text: "Action failed", error: error instanceof Error ? error : new Error(String(error)) }; } } // Using previous results (action chaining) handler: async (runtime, message, state, options, callback) => { const context = options?.context; const previousResult = context?.getPreviousResult?.('PREVIOUS_ACTION'); if (previousResult?.data) { // Use data from previous action const continuedResult = await continueWork(previousResult.data); return { success: true, text: "Continued from previous action", data: continuedResult }; } } ``` ## Step 7: State Management ### Using composeState The v1 `composeState` method has enhanced filtering capabilities: ```typescript // v0: Basic state composition const state = await runtime.composeState(message); // v1: With filtering const state = await runtime.composeState( message, ['agentName', 'bio', 'recentMessages'], // Include only these true // onlyInclude = true ); // v1: Update specific parts const updatedState = await runtime.composeState( message, ['RECENT_MESSAGES', 'GOALS'] // Update only these ); ``` ### Available State Keys Core state keys you can filter: * Agent info: `agentId`, `agentName`, `bio`, `lore`, `adjective` * Conversation: `recentMessages`, `recentMessagesData` * Providers: Any registered provider name (e.g., `TIME`, `FACTS`) ## Step 8: Update Providers ### Provider Migration ```typescript // v0: Direct state access const data = await runtime.databaseAdapter.getData(); // v1: Provider pattern const provider: Provider = { name: 'MY_DATA', description: 'Provides custom data', dynamic: true, // Re-fetch on each use get: async (runtime, message, state) => { const data = await runtime.databaseAdapter.getData(); return { text: formatDataForPrompt(data), values: data, data: { raw: data } }; } }; ``` ### Provider Options ```typescript interface Provider { name: string; description?: string; dynamic?: boolean; // Default: false position?: number; // Execution order (-100 to 100) private?: boolean; // Hidden from provider list get: (runtime, message, state) => Promise; } ``` ## Step 9: Testing Migration ### From Vitest to Bun ```typescript // OLD (Vitest) import { describe, it, expect, vi } from 'vitest'; const mockRuntime = { getSetting: vi.fn(() => 'test-value') }; // NEW (Bun) import { describe, it, expect, mock } from 'bun:test'; const mockRuntime = { getSetting: mock(() => 'test-value') }; ``` ### Test Structure ```typescript import { describe, it, expect, mock, beforeEach } from 'bun:test'; import { myAction } from '../src/actions/myAction'; describe('MyAction', () => { let mockRuntime: any; beforeEach(() => { mockRuntime = { agentId: 'test-agent', getSetting: mock((key: string) => 'test-value'), useModel: mock(async () => ({ content: 'response' })), composeState: mock(async () => ({ values: {}, text: '' })) }; }); it('should validate correctly', async () => { const message = { content: { text: 'test' } }; const isValid = await myAction.validate(mockRuntime, message); expect(isValid).toBe(true); }); it('should return ActionResult', async () => { const message = { content: { text: 'test' } }; const result = await myAction.handler(mockRuntime, message); expect(result).toHaveProperty('success'); expect(result.success).toBe(true); }); }); ``` ## Step 10: Prompt Generation ### Template System Changes ```typescript // v0: Simple template const template = `{{agentName}} responds to {{userName}}`; // v1: Enhanced templates with conditional blocks const template = ` {{#if hasGoals}} Current goals: {{goals}} {{/if}} {{agentName}} considers the context and responds. `; ``` ### Using composePromptFromState ```typescript import { composePromptFromState } from '@elizaos/core'; const prompt = composePromptFromState({ state, template: myTemplate, additionalContext: { customField: 'value' } }); const response = await runtime.useModel(ModelType.TEXT_LARGE, { prompt, runtime }); ``` ## Step 11: Advanced Patterns ### Service Migration ```typescript // v1: Service pattern export class MyService extends Service { static serviceType = 'my-service'; capabilityDescription = 'My service capabilities'; static async start(runtime: IAgentRuntime): Promise { const service = new MyService(runtime); await service.initialize(); return service; } async stop(): Promise { // Cleanup } } ``` ### Event Handlers ```typescript // v1: Event system export const myPlugin: Plugin = { name: 'my-plugin', events: { MESSAGE_RECEIVED: [ async (params) => { // Handle message received event } ], RUN_COMPLETED: [ async (params) => { // Handle run completed event } ] } }; ``` ## Step 12: Final Configuration ### Configure .gitignore ``` dist node_modules .env .elizadb .turbo ``` ### Configure .npmignore ``` * !dist/** !package.json !readme.md !tsup.config.ts ``` ### Add MIT License Create a `LICENSE` file with MIT license text. ### Verify package.json Fields Ensure all required fields are present: * [ ] `name`, `version`, `description` * [ ] `main`, `types`, `module` * [ ] `author`, `license`, `repository` * [ ] `scripts`, `dependencies`, `devDependencies` * [ ] `type`: "module" * [ ] `exports` configuration ## Common Migration Issues ### Issue: Action not returning correct format **Solution**: Ensure all actions return `ActionResult` with `success` field: ```typescript return { success: true, // REQUIRED text: "Response", values: {}, data: {} }; ``` ### Issue: Tests failing with module errors **Solution**: Update imports to use `bun:test`: ```typescript import { describe, it, expect, mock } from 'bun:test'; ``` ### Issue: State composition performance **Solution**: Use filtering to only compose needed state: ```typescript const state = await runtime.composeState( message, ['RECENT_MESSAGES', 'CHARACTER'], true // onlyInclude ); ``` ### Issue: Provider not being called **Solution**: Ensure provider is registered and not marked as `private`: ```typescript const provider = { name: 'MY_PROVIDER', private: false, // Make sure it's not private dynamic: false, // Static providers are included by default get: async () => { /* ... */ } }; ``` ## Testing Your Migration ### Build Test ```bash bun run build ``` ### Run Tests ```bash bun test ``` ### Integration Test Create a test agent using your migrated plugin: ```typescript import { myPlugin } from './dist/index.js'; const agent = { name: 'TestAgent', plugins: [myPlugin] }; // Test your plugin functionality ``` ## Publishing Once migration is complete: ```bash # Build the plugin bun run build # Test everything bun test # Publish to npm npm publish --access public ``` ## Migration Completion Checklist * [ ] All imports updated to `@elizaos/core` * [ ] Actions return `ActionResult` with `success` field * [ ] Callbacks implemented in action handlers * [ ] Tests migrated to Bun test runner * [ ] State composition uses new filtering API * [ ] Providers implemented for custom data * [ ] Package.json updated with correct dependencies * [ ] TypeScript configuration updated * [ ] Build succeeds without errors * [ ] All tests pass * [ ] Plugin works with v1.x runtime ## Getting Help If you encounter issues during migration: 1. Review this guide for common issues 2. Search existing [GitHub issues](https://github.com/elizaos/eliza/issues) 3. Join our [Discord community](https://discord.gg/ai16z) for real-time help 4. Create a detailed issue with your migration problem ## Advanced Migration Topics ### Evaluators Migration #### Evaluator Interface Changes Evaluators remain largely unchanged in their core structure, but their integration with the runtime has evolved: ```typescript // v0 Evaluator usage remains the same export interface Evaluator { alwaysRun?: boolean; description: string; similes: string[]; examples: EvaluationExample[]; handler: Handler; name: string; validate: Validator; } ``` #### Key Changes: 1. **Evaluation Results**: The `evaluate()` method now returns `Evaluator[]` instead of `string[]`: ```typescript // v0: Returns string array of evaluator names const evaluators: string[] = await runtime.evaluate(message, state); // v1: Returns Evaluator objects const evaluators: Evaluator[] | null = await runtime.evaluate(message, state); ``` 2. **Additional Parameters**: The evaluate method accepts new optional parameters: ```typescript // v1: Extended evaluate signature await runtime.evaluate( message: Memory, state?: State, didRespond?: boolean, callback?: HandlerCallback, responses?: Memory[] // NEW: Can pass responses for evaluation ); ``` ### Entity System Migration The most significant change is the shift from User/Participant to Entity/Room/World: #### User → Entity ```typescript // v0: User-based methods await runtime.ensureUserExists(userId, userName, name, email, source); const account = await runtime.getAccountById(userId); // v1: Entity-based methods await runtime.ensureConnection({ entityId: userId, roomId, userName, name, worldId, source, }); const entity = await runtime.getEntityById(entityId); ``` #### Participant → Room Membership ```typescript // v0: Participant methods await runtime.ensureParticipantExists(userId, roomId); await runtime.ensureParticipantInRoom(userId, roomId); // v1: Simplified room membership await runtime.ensureParticipantInRoom(entityId, roomId); ``` #### New World Concept v1 introduces the concept of "worlds" (servers/environments): ```typescript // v1: World management await runtime.ensureWorldExists({ id: worldId, name: serverName, type: 'discord', // or other platform }); // Get all rooms in a world const rooms = await runtime.getRooms(worldId); ``` #### Connection Management ```typescript // v0: Multiple ensure methods await runtime.ensureUserExists(...); await runtime.ensureRoomExists(roomId); await runtime.ensureParticipantInRoom(...); // v1: Single connection method await runtime.ensureConnection({ entityId, roomId, worldId, userName, name, source, channelId, serverId, type: 'user', metadata: {} }); ``` ### Client Migration Clients now have a simpler interface: ```typescript // v0: Client with config export type Client = { name: string; config?: { [key: string]: any }; start: (runtime: IAgentRuntime) => Promise; }; // v1: Client integrated with services // Clients are now typically implemented as services class MyClient extends Service { static serviceType = ServiceTypeName.MY_CLIENT; async initialize(runtime: IAgentRuntime): Promise { // Start client operations } async stop(): Promise { // Stop client operations } } ``` ### Runtime Method Changes #### Removed Methods * `updateRecentMessageState()` → Use `composeState(message, ['RECENT_MESSAGES'])` * `registerMemoryManager()` → Not needed, use database adapter * `getMemoryManager()` → Use database adapter methods * `registerContextProvider()` → Use `registerProvider()` #### Changed Methods * `evaluate()` → Now returns `Evaluator[]` instead of `string[]` * `getAccountById()` → `getEntityById()` * `ensureUserExists()` → `ensureConnection()` * `generateText()` → `runtime.useModel()` #### New Methods * `setSetting()` * `registerEvent()` * `emitEvent()` * `useModel()` * `registerModel()` * `ensureWorldExists()` * `getRooms()` ### Advanced Migration Checklist * [ ] Update all evaluator result handling to expect `Evaluator[]` objects * [ ] Remove singleton patterns from services * [ ] Update service registration to pass classes instead of instances * [ ] Replace `updateRecentMessageState` with `composeState` * [ ] Migrate from `generateText` to `runtime.useModel` * [ ] Update user/participant methods to entity/room methods * [ ] Add world management for multi-server environments * [ ] Convert clients to service-based architecture * [ ] Update any direct memory manager access to use database adapter * [ ] Replace `import { settings }` with `runtime.getSetting()` calls * [ ] Update functions to accept `runtime` parameter where settings are needed ## Summary The migration from 0.x to 1.x involves: 1. Updating package structure and dependencies 2. Migrating action signatures to return `ActionResult` 3. Implementing callbacks for user feedback 4. Converting to Bun test runner 5. Using the enhanced state composition system 6. Implementing providers for custom data 7. Following new TypeScript patterns Take your time, test thoroughly, and don't hesitate to ask for help in the community! ## What's Next? Understand the new plugin system architecture Build new plugins with modern patterns Learn about Actions, Providers, Evaluators, and Services Master proven plugin development patterns # Patterns Source: https://docs.elizaos.ai/plugins/patterns Action chaining, callbacks, composition, and advanced implementation patterns ## Action Chaining and Callbacks Action chaining in elizaOS allows multiple actions to execute sequentially with each action having access to the results of previous actions. This enables complex workflows where actions can build upon each other's outputs. ### The ActionResult Interface Actions return an `ActionResult` object that standardizes how actions communicate their outcomes. This interface includes: * **success** (required): Boolean indicating whether the action completed successfully * **text**: Optional human-readable description of the result * **values**: Key-value pairs to merge into the state for subsequent actions * **data**: Raw data payload with action-specific results * **error**: Error information if the action failed The `success` field is the only required field, making it easy to create simple results while supporting complex data passing for action chaining. For interface definitions, see [Plugin Reference](/plugins/reference#action-interface). For component basics, see [Plugin Components](/plugins/components). ### Handler Callbacks The `HandlerCallback` provides a mechanism for actions to send immediate feedback to users before the action completes: ```typescript export type HandlerCallback = (response: Content, files?: any) => Promise; ``` Example usage: ```typescript async handler( runtime: IAgentRuntime, message: Memory, _state?: State, _options?: Record, callback?: HandlerCallback ): Promise { try { // Send immediate feedback await callback?.({ text: `Starting to process your request...`, source: message.content.source }); // Perform action logic const result = await performComplexOperation(); // Send success message to user via callback await callback?.({ text: `Created issue: ${result.title} (${result.identifier})\n\nView it at: ${result.url}`, source: message.content.source }); // Return structured result for potential chaining return { success: true, text: `Created issue: ${result.title}`, data: { issueId: result.id, identifier: result.identifier, url: result.url } }; } catch (error) { // Send error message to user await callback?.({ text: `Failed to create issue: ${error.message}`, source: message.content.source }); return { success: false, text: `Failed to create issue: ${error.message}`, error: error instanceof Error ? error : new Error(String(error)) }; } } ``` ### Action Context and Previous Results When multiple actions are executed in sequence, each action receives an `ActionContext` that provides access to previous action results: ```typescript export interface ActionContext { /** Results from previously executed actions in this run */ previousResults: ActionResult[]; /** Get a specific previous result by action name */ getPreviousResult?: (actionName: string) => ActionResult | undefined; } ``` The runtime automatically provides this context in the `options` parameter: ```typescript async handler( runtime: IAgentRuntime, message: Memory, state?: State, options?: Record, callback?: HandlerCallback ): Promise { // Access the action context const context = options?.context as ActionContext; // Get results from a specific previous action const previousResult = context?.getPreviousResult?.('CREATE_LINEAR_ISSUE'); if (previousResult?.data?.issueId) { // Use data from previous action const issueId = previousResult.data.issueId; // ... continue with logic using previous result ... } } ``` ### Action Execution Flow The runtime's `processActions` method manages the execution flow: 1. **Action Planning**: When multiple actions are detected, the runtime creates an execution plan 2. **Sequential Execution**: Actions execute in the order specified by the agent 3. **State Accumulation**: Each action's results are merged into the accumulated state 4. **Working Memory**: Results are stored in working memory for access during execution 5. **Error Handling**: Failed actions don't stop the chain unless marked as critical ### Working Memory Management The runtime maintains a working memory that stores recent action results: ```typescript // Results are automatically stored in state.data.workingMemory const memoryEntry: WorkingMemoryEntry = { actionName: action.name, result: actionResult, timestamp: Date.now() }; ``` The system keeps the most recent 50 entries (configurable) to prevent memory bloat. ## Action Patterns ### Decision-Making Actions Actions can use the LLM to make intelligent decisions based on context: ```typescript export const muteRoomAction: Action = { name: 'MUTE_ROOM', similes: ['SHUT_UP', 'BE_QUIET', 'STOP_TALKING', 'SILENCE'], description: 'Mutes a room if asked to or if the agent is being annoying', validate: async (runtime, message) => { // Check if already muted const roomState = await runtime.getParticipantUserState(message.roomId, runtime.agentId); return roomState !== 'MUTED'; }, handler: async (runtime, message, state) => { // Create a decision prompt const shouldMuteTemplate = `# Task: Should {{agentName}} mute this room? {{recentMessages}} Should {{agentName}} mute and stop responding unless mentioned? Respond YES if: - User asked to stop/be quiet - Agent responses are annoying users - Conversation is hostile Otherwise NO.`; const prompt = composePromptFromState({ state, template: shouldMuteTemplate }); const decision = await runtime.useModel(ModelType.TEXT_SMALL, { prompt, runtime, }); if (decision.toLowerCase().includes('yes')) { await runtime.setParticipantUserState(message.roomId, runtime.agentId, 'MUTED'); return { success: true, text: 'Going silent in this room', values: { roomMuted: true }, }; } return { success: false, text: 'Continuing to participate' }; } }; ``` ### Multi-Step Actions Actions that need to perform multiple steps with intermediate feedback: ```typescript export const deployContractAction: Action = { name: 'DEPLOY_CONTRACT', description: 'Deploy a smart contract with multiple steps', handler: async (runtime, message, state, options, callback) => { try { // Step 1: Compile await callback?.({ text: '📝 Compiling contract...', actions: ['DEPLOY_CONTRACT'] }); const compiled = await compileContract(state.contractCode); // Step 2: Estimate gas await callback?.({ text: '⛽ Estimating gas costs...', actions: ['DEPLOY_CONTRACT'] }); const gasEstimate = await estimateGas(compiled); // Step 3: Deploy await callback?.({ text: `🚀 Deploying with gas: ${gasEstimate}...`, actions: ['DEPLOY_CONTRACT'] }); const deployed = await deploy(compiled, gasEstimate); // Step 4: Verify await callback?.({ text: '✅ Verifying deployment...', actions: ['DEPLOY_CONTRACT'] }); await verifyContract(deployed.address); return { success: true, text: `Contract deployed at ${deployed.address}`, values: { contractAddress: deployed.address, transactionHash: deployed.txHash, gasUsed: deployed.gasUsed }, data: { deployment: deployed, verification: true } }; } catch (error) { return { success: false, text: `Deployment failed: ${error.message}`, error }; } } }; ``` ### API Integration Actions Pattern for external API calls with retries and error handling: ```typescript export const apiAction: Action = { name: 'API_CALL', handler: async (runtime, message, state, options, callback) => { const maxRetries = 3; let lastError: Error | null = null; for (let attempt = 1; attempt <= maxRetries; attempt++) { try { await callback?.({ text: `Attempt ${attempt}/${maxRetries}...` }); const result = await callExternalAPI({ endpoint: state.endpoint, data: state.data, timeout: 5000 }); return { success: true, text: 'API call successful', data: result }; } catch (error) { lastError = error as Error; if (attempt < maxRetries) { await callback?.({ text: `Attempt ${attempt} failed, retrying...` }); await new Promise(r => setTimeout(r, 1000 * attempt)); // Exponential backoff } } } return { success: false, text: `API call failed after ${maxRetries} attempts`, error: lastError }; } }; ``` ### Context-Aware Actions Actions that adapt based on conversation context: ```typescript export const contextAwareAction: Action = { name: 'CONTEXT_RESPONSE', handler: async (runtime, message, state, options, callback) => { // Analyze conversation sentiment const sentiment = await analyzeSentiment(state.recentMessages); // Adjust response based on context let responseStrategy: string; if (sentiment.score < -0.5) { responseStrategy = 'empathetic'; } else if (sentiment.score > 0.5) { responseStrategy = 'enthusiastic'; } else { responseStrategy = 'neutral'; } // Generate context-appropriate response const response = await generateResponse( state, responseStrategy, runtime ); return { success: true, text: response.text, values: { sentiment: sentiment.score, strategy: responseStrategy } }; } }; ``` ## Action Composition ### Compose Multiple Actions ```typescript // Compose multiple actions into higher-level operations export const compositeAction: Action = { name: 'SEND_AND_TRACK', description: 'Send a message and track its delivery', handler: async (runtime, message, state, options, callback) => { // Execute sub-actions const sendResult = await sendMessageAction.handler( runtime, message, state, options, callback ); if (!sendResult.success) { return sendResult; // Propagate failure } // Track the sent message const trackingId = generateTrackingId(); await runtime.createMemory({ id: trackingId, entityId: message.entityId, roomId: message.roomId, content: { type: 'message_tracking', sentTo: sendResult.data.targetId, sentAt: Date.now(), messageContent: sendResult.data.messageContent, } }, 'tracking'); return { success: true, text: `Message sent and tracked (${trackingId})`, values: { ...sendResult.values, trackingId, tracked: true, }, data: { sendResult, trackingId, } }; } }; ``` ### Workflow Orchestration ```typescript export const workflowAction: Action = { name: 'COMPLEX_WORKFLOW', handler: async (runtime, message, state, options, callback) => { const workflow = [ { action: 'VALIDATE_INPUT', required: true }, { action: 'FETCH_DATA', required: true }, { action: 'PROCESS_DATA', required: false }, { action: 'STORE_RESULTS', required: true }, { action: 'NOTIFY_USER', required: false } ]; const results: ActionResult[] = []; for (const step of workflow) { const action = runtime.getAction(step.action); if (!action) { if (step.required) { return { success: false, text: `Required action ${step.action} not found` }; } continue; } const result = await action.handler( runtime, message, state, { context: { previousResults: results } }, callback ); results.push(result); if (!result.success && step.required) { return { success: false, text: `Workflow failed at ${step.action}`, data: { failedStep: step.action, results } }; } // Merge values into state for next action state = { ...state, values: { ...state.values, ...result.values } }; } return { success: true, text: 'Workflow completed successfully', data: { workflowResults: results } }; } }; ``` ## Self-Modifying Actions Actions that learn and adapt their behavior: ```typescript export const learningAction: Action = { name: 'ADAPTIVE_RESPONSE', handler: async (runtime, message, state) => { // Retrieve past performance const history = await runtime.getMemories({ tableName: 'action_feedback', roomId: message.roomId, count: 100, }); // Analyze what worked well const analysis = await runtime.useModel(ModelType.TEXT_LARGE, { prompt: `Analyze these past interactions and identify patterns: ${JSON.stringify(history)} What response strategies were most effective?`, }); // Adapt behavior based on learning const strategy = determineStrategy(analysis); const response = await generateResponse(state, strategy); // Store for future learning await runtime.createMemory({ id: generateId(), content: { type: 'action_feedback', strategy: strategy.name, context: state.text, response: response.text, } }, 'action_feedback'); return { success: true, text: response.text, values: { strategyUsed: strategy.name, confidence: strategy.confidence, } }; } }; ``` ## Provider Patterns ### Conditional Providers Providers that only provide data under certain conditions: ```typescript export const conditionalProvider: Provider = { name: 'PREMIUM_DATA', private: true, get: async (runtime, message, state) => { // Check if user has premium access const user = await runtime.getUser(message.entityId); if (!user.isPremium) { return { text: '', values: {}, data: { available: false } }; } // Provide premium data const premiumData = await fetchPremiumData(user); return { text: formatPremiumData(premiumData), values: premiumData, data: { available: true, ...premiumData } }; } }; ``` ### Aggregating Providers Providers that combine data from multiple sources: ```typescript export const aggregateProvider: Provider = { name: 'MARKET_OVERVIEW', position: 50, // Run after individual data providers get: async (runtime, message, state) => { // Aggregate from multiple sources const [stocks, crypto, forex] = await Promise.all([ fetchStockData(), fetchCryptoData(), fetchForexData() ]); const overview = { stocksUp: stocks.filter(s => s.change > 0).length, stocksDown: stocks.filter(s => s.change < 0).length, cryptoMarketCap: crypto.reduce((sum, c) => sum + c.marketCap, 0), forexVolatility: calculateVolatility(forex) }; return { text: `Market Overview: - Stocks: ${overview.stocksUp} up, ${overview.stocksDown} down - Crypto Market Cap: $${overview.cryptoMarketCap.toLocaleString()} - Forex Volatility: ${overview.forexVolatility}`, values: overview, data: { stocks, crypto, forex } }; } }; ``` ## Best Practices for Action Chaining 1. **Always Return ActionResult**: Even for simple actions, return a proper `ActionResult` object: ```typescript return { success: true, text: "Action completed", data: { /* any data for next actions */ } }; ``` 2. **Use Callbacks for User Feedback**: Send immediate feedback via callbacks rather than waiting for the action to complete: ```typescript await callback?.({ text: "Processing your request...", source: message.content.source }); ``` 3. **Store Identifiers in Data**: When creating resources, store identifiers that subsequent actions might need: ```typescript return { success: true, data: { resourceId: created.id, resourceUrl: created.url } }; ``` 4. **Handle Missing Dependencies**: Check if required previous results exist: ```typescript const previousResult = context?.getPreviousResult?.('REQUIRED_ACTION'); if (!previousResult?.success) { return { success: false, text: "Required previous action did not complete successfully" }; } ``` 5. **Maintain Backward Compatibility**: The runtime handles legacy action returns (void, boolean) but new actions should use `ActionResult`. ## Example: Multi-Step Workflow Here's an example of a multi-step workflow using action chaining: ```typescript // User: "Create a bug report for the login issue and assign it to John" // Agent executes: REPLY, CREATE_LINEAR_ISSUE, UPDATE_LINEAR_ISSUE // Action 1: CREATE_LINEAR_ISSUE { success: true, data: { issueId: "abc-123", identifier: "BUG-456" } } // Action 2: UPDATE_LINEAR_ISSUE (can access previous result) async handler(runtime, message, state, options, callback) { const context = options?.context as ActionContext; const createResult = context?.getPreviousResult?.('CREATE_LINEAR_ISSUE'); if (createResult?.data?.issueId) { // Use the issue ID from previous action await updateIssue(createResult.data.issueId, { assignee: "John" }); return { success: true, text: "Issue assigned to John" }; } } ``` ## Common Patterns 1. **Create and Configure**: Create a resource, then configure it 2. **Search and Update**: Find resources, then modify them 3. **Validate and Execute**: Check conditions, then perform actions 4. **Aggregate and Report**: Collect data from multiple sources, then summarize The action chaining system provides a powerful way to build complex, multi-step workflows while maintaining clean separation between individual actions. ## Real-World Implementation Patterns This section documents actual patterns and structures used in production elizaOS plugins based on examination of real implementations. ### Basic Plugin Structure Pattern Every plugin follows this core structure pattern (from `plugin-starter`): ```typescript import type { Plugin } from '@elizaos/core'; export const myPlugin: Plugin = { name: 'plugin-name', description: 'Plugin description', // Core components actions: [], // Actions the plugin provides providers: [], // Data providers services: [], // Background services evaluators: [], // Response evaluators // Optional components init: async (config) => {}, // Initialization logic models: {}, // Custom model implementations routes: [], // HTTP routes events: {}, // Event handlers tests: [], // Test suites dependencies: [], // Other required plugins }; ``` ### Bootstrap Plugin Pattern The most complex and comprehensive plugin that provides core functionality: ```typescript export const bootstrapPlugin: Plugin = { name: 'bootstrap', description: 'Agent bootstrap with basic actions and evaluators', actions: [ actions.replyAction, actions.followRoomAction, actions.ignoreAction, actions.sendMessageAction, actions.generateImageAction, // ... more actions ], providers: [ providers.timeProvider, providers.entitiesProvider, providers.characterProvider, providers.recentMessagesProvider, // ... more providers ], services: [TaskService], evaluators: [evaluators.reflectionEvaluator], events: { [EventType.MESSAGE_RECEIVED]: [messageReceivedHandler], [EventType.POST_GENERATED]: [postGeneratedHandler], // ... more event handlers } }; ``` ### Service Plugin Pattern (Discord, Telegram) Platform integration plugins focus on service implementation: ```typescript // Discord Plugin const discordPlugin: Plugin = { name: "discord", description: "Discord service plugin for integration with Discord servers", services: [DiscordService], actions: [ chatWithAttachments, downloadMedia, joinVoice, leaveVoice, summarize, transcribeMedia, ], providers: [channelStateProvider, voiceStateProvider], tests: [new DiscordTestSuite()], init: async (config, runtime) => { // Check for required API tokens const token = runtime.getSetting("DISCORD_API_TOKEN"); if (!token) { logger.warn("Discord API Token not provided"); } }, }; // Telegram Plugin (minimal) const telegramPlugin: Plugin = { name: TELEGRAM_SERVICE_NAME, description: 'Telegram client plugin', services: [TelegramService], tests: [new TelegramTestSuite()], }; ``` ### Action Implementation Pattern Actions follow a consistent structure with validation and execution: ```typescript const helloWorldAction: Action = { name: 'HELLO_WORLD', similes: ['GREET', 'SAY_HELLO'], // Alternative names description: 'Responds with a simple hello world message', validate: async (runtime, message, state) => { // Return true if action can be executed return true; }, handler: async (runtime, message, state, options, callback, responses) => { try { const responseContent: Content = { text: 'hello world!', actions: ['HELLO_WORLD'], source: message.content.source, }; if (callback) { await callback(responseContent); } return responseContent; } catch (error) { logger.error('Error in HELLO_WORLD action:', error); throw error; } }, examples: [ [ { name: '{{name1}}', content: { text: 'Can you say hello?' } }, { name: '{{name2}}', content: { text: 'hello world!', actions: ['HELLO_WORLD'] } } ] ] }; ``` ### Complex Action Pattern (Reply Action) ```typescript export const replyAction = { name: 'REPLY', similes: ['GREET', 'REPLY_TO_MESSAGE', 'SEND_REPLY', 'RESPOND'], description: 'Replies to the current conversation', validate: async (runtime) => true, handler: async (runtime, message, state, options, callback, responses) => { // Compose state with providers state = await runtime.composeState(message, ['RECENT_MESSAGES']); // Generate response using LLM const prompt = composePromptFromState({ state, template: replyTemplate }); const response = await runtime.useModel(ModelType.OBJECT_LARGE, { prompt }); const responseContent = { thought: response.thought, text: response.message || '', actions: ['REPLY'], }; await callback(responseContent); return true; } }; ``` ### Provider Implementation Pattern Providers supply contextual data to the agent: ```typescript export const timeProvider: Provider = { name: 'TIME', get: async (runtime, message) => { const currentDate = new Date(); const options = { timeZone: 'UTC', dateStyle: 'full' as const, timeStyle: 'long' as const, }; const humanReadable = new Intl.DateTimeFormat('en-US', options).format(currentDate); return { data: { time: currentDate }, values: { time: humanReadable }, text: `The current date and time is ${humanReadable}.`, }; }, }; ``` ### Plugin Initialization Pattern Plugins can have initialization logic: ```typescript const myPlugin: Plugin = { name: 'my-plugin', config: { EXAMPLE_VARIABLE: process.env.EXAMPLE_VARIABLE, }, async init(config: Record) { // Validate configuration const validatedConfig = await configSchema.parseAsync(config); // Set environment variables for (const [key, value] of Object.entries(validatedConfig)) { if (value) process.env[key] = value; } }, }; ``` **Guides**: [Create a Plugin](/guides/create-a-plugin) | [Publish a Plugin](/guides/publish-a-plugin) ## What's Next? Understand the overall plugin system design Build your first plugin with step-by-step guidance Learn about configuration and validation schemas Migrate existing plugins to new patterns # Reference Source: https://docs.elizaos.ai/plugins/reference Complete TypeScript interface reference for elizaOS plugins ## Core Interfaces ### Plugin Interface ```typescript export interface Plugin { // Required name: string; // Unique identifier description: string; // Human-readable description // Initialization init?: (config: Record, runtime: IAgentRuntime) => Promise; // Configuration config?: { [key: string]: any }; // Plugin-specific configuration // Core Components actions?: Action[]; // Tasks agents can perform providers?: Provider[]; // Data sources evaluators?: Evaluator[]; // Response filters services?: (typeof Service)[]; // Background services // Additional Components adapter?: IDatabaseAdapter; // Database adapter models?: { // Model handlers [key: string]: (...args: any[]) => Promise; }; events?: PluginEvents; // Event handlers routes?: Route[]; // HTTP endpoints tests?: TestSuite[]; // Test suites componentTypes?: { // Custom component types name: string; schema: Record; validator?: (data: any) => boolean; }[]; // Dependencies dependencies?: string[]; // Required plugins testDependencies?: string[]; // Test-only dependencies priority?: number; // Loading priority schema?: any; // Database schema } ``` ### Action Interface ```typescript export interface Action { name: string; // Unique identifier similes?: string[]; // Alternative names/aliases description: string; // What the action does examples?: ActionExample[][]; // Usage examples handler: Handler; // Execution logic validate: Validator; // Pre-execution validation } // Handler type export type Handler = ( runtime: IAgentRuntime, message: Memory, state?: State, options?: { [key: string]: unknown }, callback?: HandlerCallback, responses?: Memory[] ) => Promise; // Validator type export type Validator = ( runtime: IAgentRuntime, message: Memory, state?: State ) => Promise; // HandlerCallback type export type HandlerCallback = ( response: Content, files?: any ) => Promise; // ActionResult interface export interface ActionResult { success: boolean; // Required - whether action succeeded text?: string; // Optional text description values?: Record; // Values to merge into state data?: Record; // Data payload error?: string | Error; // Error information if failed } // ActionExample interface export interface ActionExample { name: string; // Speaker name content: Content; // Message content } // ActionContext interface (for chaining) export interface ActionContext { previousResults: ActionResult[]; getPreviousResult?: (actionName: string) => ActionResult | undefined; } ``` ### Provider Interface ```typescript export interface Provider { name: string; // Unique identifier description?: string; // What data it provides dynamic?: boolean; // Dynamic data source (default: false) position?: number; // Execution order (-100 to 100, default: 0) private?: boolean; // Hidden from provider list (default: false) get: ( runtime: IAgentRuntime, message: Memory, state: State ) => Promise; } // ProviderResult interface export interface ProviderResult { values?: { [key: string]: any }; // Key-value pairs for state data?: { [key: string]: any }; // Structured data text?: string; // Natural language context } ``` ### Evaluator Interface ```typescript export interface Evaluator { alwaysRun?: boolean; // Run on every response description: string; // What it evaluates similes?: string[]; // Alternative names examples: EvaluationExample[]; // Example evaluations handler: Handler; // Evaluation logic name: string; // Unique identifier validate: Validator; // Should evaluator run? } // EvaluationExample interface export interface EvaluationExample { prompt: string; // Evaluation prompt messages: Memory[]; // Messages to evaluate outcome: string; // Expected outcome } ``` ### Service Abstract Class ```typescript export abstract class Service { protected runtime!: IAgentRuntime; constructor(runtime?: IAgentRuntime) { if (runtime) { this.runtime = runtime; } } abstract stop(): Promise; static serviceType: string; abstract capabilityDescription: string; config?: Metadata; static async start(_runtime: IAgentRuntime): Promise { throw new Error('Not implemented'); } } ``` ## Supporting Types ### Memory Interface ```typescript export interface Memory { id: UUID; entityId: UUID; roomId: UUID; content: Content; createdAt?: number; embedding?: number[]; userId?: UUID; agentId?: UUID; type?: string; isUnique?: boolean; } ``` ### Content Interface ```typescript export interface Content { text?: string; source?: string; url?: string; attachments?: Attachment[]; actions?: string[]; [key: string]: any; } ``` ### State Interface ```typescript export interface State { values: { [key: string]: any }; data?: { [key: string]: any }; text: string; } ``` ### Character Interface ```typescript export interface Character { id: UUID; name: string; bio?: string | string[]; lore?: string[]; messageExamples?: MessageExample[][]; postExamples?: string[]; adjectives?: string[]; topics?: string[]; style?: { all?: string[]; chat?: string[]; post?: string[]; }; clients?: string[]; plugins?: string[]; settings?: { secrets?: { [key: string]: string }; [key: string]: any; }; } ``` ## Route Types ```typescript export type Route = { type: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'STATIC'; path: string; filePath?: string; // For static files public?: boolean; // Public access name?: string; // Route name handler?: (req: any, res: any, runtime: IAgentRuntime) => Promise; isMultipart?: boolean; // File uploads }; ``` ## Event Types ### Event System Types ```typescript export type PluginEvents = { [K in keyof EventPayloadMap]?: EventHandler[]; } & { [key: string]: ((params: any) => Promise)[]; }; export type EventHandler = ( params: EventPayloadMap[K] ) => Promise; ``` ### Standard Event Types ```typescript export enum EventType { // World events WORLD_JOINED = 'world:joined', WORLD_CONNECTED = 'world:connected', WORLD_LEFT = 'world:left', // Entity events ENTITY_JOINED = 'entity:joined', ENTITY_LEFT = 'entity:left', ENTITY_UPDATED = 'entity:updated', // Room events ROOM_JOINED = 'room:joined', ROOM_LEFT = 'room:left', // Message events MESSAGE_RECEIVED = 'message:received', MESSAGE_SENT = 'message:sent', MESSAGE_DELETED = 'message:deleted', // Voice events VOICE_MESSAGE_RECEIVED = 'voice:message:received', VOICE_MESSAGE_SENT = 'voice:message:sent', // Run events RUN_STARTED = 'run:started', RUN_ENDED = 'run:ended', RUN_TIMEOUT = 'run:timeout', // Action/Evaluator events ACTION_STARTED = 'action:started', ACTION_COMPLETED = 'action:completed', EVALUATOR_STARTED = 'evaluator:started', EVALUATOR_COMPLETED = 'evaluator:completed', // Model events MODEL_USED = 'model:used' } ``` ## Database Types ### IDatabaseAdapter Interface (Partial) ```typescript export interface IDatabaseAdapter { // Core database property db: any; // Agent methods createAgent(agent: Agent): Promise; getAgent(agentId: UUID): Promise; updateAgent(agent: Agent): Promise; deleteAgent(agentId: UUID): Promise; // Memory methods createMemory(memory: Memory, tableName?: string): Promise; getMemories(params: { roomId?: UUID; agentId?: UUID; entityId?: UUID; tableName?: string; count?: number; unique?: boolean; start?: number; end?: number; }): Promise; searchMemories(params: { query: string; roomId?: UUID; agentId?: UUID; limit?: number; }): Promise; // Room methods createRoom(room: Room): Promise; getRoom(roomId: UUID): Promise; updateRoom(room: Room): Promise; deleteRoom(roomId: UUID): Promise; // Participant methods createParticipant(participant: Participant): Promise; getParticipants(roomId: UUID): Promise; updateParticipantUserState( roomId: UUID, userId: UUID, state: string ): Promise; // Relationship methods createRelationship(relationship: Relationship): Promise; getRelationships(params: { entityA?: UUID; entityB?: UUID; }): Promise; // Task methods createTask(task: Task): Promise; getTasks(agentId: UUID): Promise; updateTask(task: Task): Promise; // Cache methods getCachedEmbedding(text: string): Promise; setCachedEmbedding(text: string, embedding: number[]): Promise; // Log methods log(entry: LogEntry): Promise; getLogs(params: { agentId?: UUID; level?: string; limit?: number; }): Promise; } ``` ## Model Types ### ModelType Enum ```typescript export enum ModelType { TEXT_SMALL = 'text_small', TEXT_MEDIUM = 'text_medium', TEXT_LARGE = 'text_large', TEXT_EMBEDDING = 'text_embedding', OBJECT_SMALL = 'object_small', OBJECT_MEDIUM = 'object_medium', OBJECT_LARGE = 'object_large', IMAGE_GENERATION = 'image_generation', SPEECH_TO_TEXT = 'speech_to_text', TEXT_TO_SPEECH = 'text_to_speech' } ``` ### Model Handler Type ```typescript export type ModelHandler = ( params: { prompt: string; runtime: IAgentRuntime; [key: string]: any; } ) => Promise; ``` ## Utility Types ### UUID Type ```typescript export type UUID = string & { __uuid: true }; ``` ### Metadata Type ```typescript export type Metadata = Record; ``` ### TestSuite Interface ```typescript export interface TestSuite { name: string; tests: TestCase[]; } export interface TestCase { name: string; description?: string; fn: (runtime: IAgentRuntime) => Promise; } ``` ## Runtime Interface (Partial) ```typescript export interface IAgentRuntime { // Core properties agentId: UUID; character: Character; databaseAdapter: IDatabaseAdapter; // Plugin management plugins: Plugin[]; actions: Action[]; providers: Provider[]; evaluators: Evaluator[]; // Methods registerPlugin(plugin: Plugin): Promise; getService(name: string): T | null; getSetting(key: string): string | undefined; // State composition composeState( message: Memory, includeList?: string[], onlyInclude?: boolean, skipCache?: boolean ): Promise; // Model usage useModel( type: ModelType, params: any ): Promise; // Memory management createMemory(memory: Memory, tableName?: string): Promise; getMemories(params: any): Promise; // Participant management getParticipantUserState(roomId: UUID, userId: UUID): Promise; setParticipantUserState(roomId: UUID, userId: UUID, state: string): Promise; // Action management getAction(name: string): Action | undefined; // Completion completion(params: { messages: any[]; [key: string]: any; }): Promise; } ``` ## Common Enums ### ChannelType ```typescript export enum ChannelType { DM = 'dm', GROUP = 'group', THREAD = 'thread', BROADCAST = 'broadcast' } ``` ### ServiceType ```typescript export enum ServiceType { TRANSCRIPTION = 'transcription', VIDEO = 'video', BROWSER = 'browser', PDF = 'pdf', REMOTE_FILES = 'remote_files', WEB_SEARCH = 'web_search', EMAIL = 'email', TEE = 'tee', TASK = 'task', WALLET = 'wallet', LP_POOL = 'lp_pool', TOKEN_DATA = 'token_data', DATABASE_MIGRATION = 'database_migration', PLUGIN_MANAGER = 'plugin_manager', PLUGIN_CONFIGURATION = 'plugin_configuration', PLUGIN_USER_INTERACTION = 'plugin_user_interaction' } ``` ## Helper Function Types ### composePromptFromState ```typescript export function composePromptFromState(params: { state: State; template: string; additionalContext?: Record; }): string; ``` ### parseKeyValueXml ```typescript export function parseKeyValueXml(xml: string): Record; ``` ### generateId ```typescript export function generateId(): UUID; ``` ### addHeader ```typescript export function addHeader(header: string, content: string): string; ``` ## Configuration Types ### Environment Variables Common environment variables accessed via `runtime.getSetting()`: ```typescript // AI Model Providers OPENAI_API_KEY?: string; ANTHROPIC_API_KEY?: string; GOOGLE_GENERATIVE_AI_API_KEY?: string; OLLAMA_API_ENDPOINT?: string; // Platform Integrations DISCORD_API_TOKEN?: string; TELEGRAM_BOT_TOKEN?: string; TWITTER_API_KEY?: string; TWITTER_API_SECRET_KEY?: string; TWITTER_ACCESS_TOKEN?: string; TWITTER_ACCESS_TOKEN_SECRET?: string; // Database POSTGRES_URL?: string; PGLITE_DATA_DIR?: string; // Plugin Control IGNORE_BOOTSTRAP?: string; CHANNEL_IDS?: string; ``` ## What's Next? Understand how plugins fit into the system Deep dive into Actions, Providers, Evaluators, and Services Build your first plugin with practical examples Learn proven plugin development patterns # Database Schema Source: https://docs.elizaos.ai/plugins/schemas Learn how to add custom database schemas to elizaOS plugins for shared data access ## Overview elizaOS uses Drizzle ORM with PostgreSQL and automatically handles migrations from your schema definitions. This guide demonstrates how to add custom tables that can be shared across all agents (no `agentId` field), along with actions to write data and providers to read it. ## Database Adapter Interface Plugins can provide database adapters for custom storage backends. The IDatabaseAdapter interface is extensive, including methods for: * Agents, Entities, Components * Memories (with embeddings) * Rooms, Participants * Relationships * Tasks * Caching * Logs Example database adapter plugin: ```typescript export const plugin: Plugin = { name: '@elizaos/plugin-sql', description: 'A plugin for SQL database access with dynamic schema migrations', priority: 0, schema, init: async (_, runtime: IAgentRuntime) => { const dbAdapter = createDatabaseAdapter(config, runtime.agentId); runtime.registerDatabaseAdapter(dbAdapter); } }; ``` ## Step 1: Define Your Custom Schema ### Creating a Shared Table To create a table that's accessible by all agents, define it without an `agentId` field. Here's an example of a user preferences table: ```typescript // In your plugin's schema.ts file import { pgTable, uuid, varchar, text, timestamp, jsonb, index } from 'drizzle-orm/pg-core'; export const userPreferencesTable = pgTable( 'user_preferences', { id: uuid('id').primaryKey().defaultRandom(), userId: uuid('user_id').notNull(), // Links to the user preferences: jsonb('preferences').default({}).notNull(), createdAt: timestamp('created_at').defaultNow().notNull(), updatedAt: timestamp('updated_at').defaultNow().notNull(), }, (table) => [ index('idx_user_preferences_user_id').on(table.userId), ] ); // Export your schema export const customSchema = { userPreferencesTable, }; ``` **Key Points:** * No `agentId` field means data is shared across all agents * elizaOS will automatically create migrations from this schema * Use appropriate indexes for query performance ### Creating Agent-Specific Tables For data that should be scoped to individual agents: ```typescript export const agentDataTable = pgTable( 'agent_data', { id: uuid('id').primaryKey().defaultRandom(), agentId: uuid('agent_id').notNull(), // Scopes to specific agent key: varchar('key', { length: 255 }).notNull(), value: jsonb('value').notNull(), createdAt: timestamp('created_at').defaultNow().notNull(), }, (table) => [ index('idx_agent_data_agent_key').on(table.agentId, table.key), ] ); ``` ## Step 2: Create a Repository for Database Access ### Repository Pattern Create a repository class to handle database operations. This follows the pattern used throughout elizaOS: ```typescript // In your plugin's repositories/user-preferences-repository.ts import { eq } from 'drizzle-orm'; import { drizzle } from 'drizzle-orm/node-postgres'; import { UUID } from '@elizaos/core'; import { userPreferencesTable } from '../schema.ts'; export interface UserPreferences { id: UUID; userId: UUID; preferences: Record; createdAt: Date; updatedAt: Date; } export class UserPreferencesRepository { constructor(private readonly db: ReturnType) {} /** * Create or update user preferences */ async upsert(userId: UUID, preferences: Record): Promise { // Check if preferences exist const existing = await this.findByUserId(userId); if (existing) { // Update existing const [updated] = await this.db .update(userPreferencesTable) .set({ preferences, updatedAt: new Date(), }) .where(eq(userPreferencesTable.userId, userId)) .returning(); return this.mapToUserPreferences(updated); } else { // Create new const [created] = await this.db .insert(userPreferencesTable) .values({ userId, preferences, createdAt: new Date(), updatedAt: new Date(), }) .returning(); return this.mapToUserPreferences(created); } } /** * Find preferences by user ID */ async findByUserId(userId: UUID): Promise { const result = await this.db .select() .from(userPreferencesTable) .where(eq(userPreferencesTable.userId, userId)) .limit(1); return result.length > 0 ? this.mapToUserPreferences(result[0]) : null; } /** * Delete preferences by user ID */ async deleteByUserId(userId: UUID): Promise { const result = await this.db .delete(userPreferencesTable) .where(eq(userPreferencesTable.userId, userId)) .returning(); return result.length > 0; } /** * Find all preferences (with pagination) */ async findAll(offset = 0, limit = 100): Promise { const results = await this.db .select() .from(userPreferencesTable) .offset(offset) .limit(limit); return results.map(this.mapToUserPreferences); } /** * Map database row to domain type */ private mapToUserPreferences(row: any): UserPreferences { return { id: row.id as UUID, userId: row.userId || row.user_id, preferences: row.preferences || {}, createdAt: row.createdAt || row.created_at, updatedAt: row.updatedAt || row.updated_at, }; } } ``` ### Advanced Repository Patterns #### Transactions ```typescript export class TransactionalRepository { async transferPoints(fromUserId: UUID, toUserId: UUID, points: number): Promise { await this.db.transaction(async (tx) => { // Deduct from sender await tx .update(userPointsTable) .set({ points: sql`${userPointsTable.points} - ${points}`, updatedAt: new Date() }) .where(eq(userPointsTable.userId, fromUserId)); // Add to receiver await tx .update(userPointsTable) .set({ points: sql`${userPointsTable.points} + ${points}`, updatedAt: new Date() }) .where(eq(userPointsTable.userId, toUserId)); // Log transaction await tx.insert(transactionLogTable).values({ fromUserId, toUserId, amount: points, createdAt: new Date() }); }); } } ``` #### Complex Queries ```typescript export class AnalyticsRepository { async getUserActivityStats(userId: UUID, days = 30): Promise { const startDate = new Date(); startDate.setDate(startDate.getDate() - days); const stats = await this.db .select({ totalActions: count(userActionsTable.id), uniqueDays: countDistinct( sql`DATE(${userActionsTable.createdAt})` ), mostCommonAction: sql` MODE() WITHIN GROUP (ORDER BY ${userActionsTable.actionType}) `, }) .from(userActionsTable) .where( and( eq(userActionsTable.userId, userId), gte(userActionsTable.createdAt, startDate) ) ) .groupBy(userActionsTable.userId); return stats[0] || { totalActions: 0, uniqueDays: 0, mostCommonAction: null }; } } ``` ## Step 3: Create an Action to Write Data ### Action Structure Actions process user input and store data using the repository: ```typescript import type { Action, IAgentRuntime, Memory, ActionResult } from '@elizaos/core'; import { parseKeyValueXml } from '@elizaos/core'; import { UserPreferencesRepository } from '../repositories/user-preferences-repository.ts'; export const storeUserPreferencesAction: Action = { name: 'STORE_USER_PREFERENCES', description: 'Extract and store user preferences from messages', validate: async (runtime: IAgentRuntime, message: Memory) => { const text = message.content.text?.toLowerCase() || ''; return text.includes('preference') || text.includes('prefer') || text.includes('like'); }, handler: async (runtime: IAgentRuntime, message: Memory) => { // 1. Create prompt for LLM to extract structured data const extractionPrompt = ` Extract user preferences from the following message. Return in XML format: light/dark/auto en/es/fr/etc true/false value Message: "${message.content.text}" `; // 2. Use runtime's LLM const llmResponse = await runtime.completion({ messages: [{ role: 'system', content: extractionPrompt }] }); // 3. Parse the response const extractedPreferences = parseKeyValueXml(llmResponse.content); // 4. Get database and repository const db = runtime.databaseAdapter.db; const repository = new UserPreferencesRepository(db); // 5. Store preferences const userId = message.userId || message.entityId; const stored = await repository.upsert(userId, extractedPreferences); return { success: true, data: stored, text: 'Your preferences have been saved successfully.' }; } }; ``` ### Batch Operations Action ```typescript export const batchImportAction: Action = { name: 'BATCH_IMPORT', description: 'Import multiple records at once', handler: async (runtime, message) => { const db = runtime.databaseAdapter.db; const repository = new DataRepository(db); // Parse batch data from message const records = JSON.parse(message.content.text); // Use batch insert for performance const results = await db .insert(dataTable) .values(records.map(r => ({ ...r, createdAt: new Date(), updatedAt: new Date() }))) .returning(); return { success: true, text: `Imported ${results.length} records successfully`, data: { importedCount: results.length } }; } }; ``` ## Step 4: Create a Provider to Read Data ### Provider Structure Providers make data available to agents during conversations: ```typescript import type { Provider, IAgentRuntime, Memory } from '@elizaos/core'; import { UserPreferencesRepository } from '../repositories/user-preferences-repository.ts'; export const userPreferencesProvider: Provider = { name: 'USER_PREFERENCES', description: 'Provides user preferences to customize agent behavior', dynamic: true, // Fetches fresh data on each request get: async (runtime: IAgentRuntime, message: Memory) => { // 1. Get user ID from message const userId = message.userId || message.entityId; // 2. Get database and repository const db = runtime.databaseAdapter.db; const repository = new UserPreferencesRepository(db); // 3. Fetch preferences const userPrefs = await repository.findByUserId(userId); if (!userPrefs) { return { data: { preferences: {} }, values: { preferences: 'No preferences found' }, text: '' }; } // 4. Format data for agent context const preferencesText = ` # User Preferences ${Object.entries(userPrefs.preferences).map(([key, value]) => `- ${key}: ${value}` ).join('\n')} `.trim(); return { data: { preferences: userPrefs.preferences }, values: userPrefs.preferences, text: preferencesText // This text is added to agent context }; } }; ``` ### Caching Provider ```typescript export const cachedDataProvider: Provider = { name: 'CACHED_DATA', private: true, get: async (runtime, message) => { const cacheKey = `data_${message.roomId}`; const cached = runtime.cacheManager.get(cacheKey); if (cached && Date.now() - cached.timestamp < 60000) { // 1 minute cache return cached.data; } // Fetch fresh data const db = runtime.databaseAdapter.db; const repository = new DataRepository(db); const freshData = await repository.getRoomData(message.roomId); const result = { text: formatData(freshData), data: freshData, values: { roomData: freshData } }; // Cache the result runtime.cacheManager.set(cacheKey, { data: result, timestamp: Date.now() }); return result; } }; ``` ## Step 5: Register Your Components ### Plugin Configuration Register your schema, actions, and providers in your plugin: ```typescript import type { Plugin } from '@elizaos/core'; export const myPlugin: Plugin = { name: 'my-plugin', description: 'My custom plugin', actions: [storeUserPreferencesAction], providers: [userPreferencesProvider], schema: customSchema, // Your schema export }; ``` ## Important Considerations ### 1. Database Access Pattern * Always access the database through `runtime.databaseAdapter.db` * Use repository classes to encapsulate database operations * The database type is already properly typed from the runtime adapter ### 2. Shared Data Pattern Without `agentId` in your tables: * All agents can read and write the same data * Use `userId` or other identifiers to scope data appropriately * Consider data consistency across multiple agents ### 3. Type Safety * Define interfaces for your domain types * Map database rows to domain types in repository methods * Handle both camelCase and snake\_case field names ### 4. Error Handling ```typescript try { const result = await repository.upsert(userId, preferences); return { success: true, data: result }; } catch (error) { console.error('Failed to store preferences:', error); return { success: false, error: error instanceof Error ? error.message : 'Unknown error' }; } ``` ### 5. Migration Strategy ```typescript // Schema versioning export const schemaVersion = 2; export const migrations = { 1: async (db) => { // Initial schema }, 2: async (db) => { // Add new column await db.schema.alterTable('user_preferences', (table) => { table.addColumn('version', 'integer').defaultTo(1); }); } }; ``` ## Example Flow 1. **User sends message**: "I prefer dark theme and Spanish language" 2. **Action triggered**: * LLM extracts: `{ theme: 'dark', language: 'es' }` * Repository stores in database 3. **Provider supplies data**: * On next interaction, provider fetches preferences * Agent context includes: "User Preferences: theme: dark, language: es" 4. **Multiple agents**: Any agent can access this user's preferences ## Advanced Patterns ### Embeddings and Vector Search ```typescript export const documentTable = pgTable('documents', { id: uuid('id').primaryKey().defaultRandom(), content: text('content').notNull(), embedding: vector('embedding', { dimensions: 1536 }), metadata: jsonb('metadata').default({}) }); export class DocumentRepository { async searchSimilar(embedding: number[], limit = 10): Promise { return await this.db .select() .from(documentTable) .orderBy( sql`${documentTable.embedding} <-> ${embedding}` ) .limit(limit); } } ``` ### Time-Series Data ```typescript export const metricsTable = pgTable('metrics', { id: uuid('id').primaryKey().defaultRandom(), metric: varchar('metric', { length: 255 }).notNull(), value: real('value').notNull(), timestamp: timestamp('timestamp').defaultNow().notNull(), tags: jsonb('tags').default({}) }); export class MetricsRepository { async getTimeSeries(metric: string, hours = 24): Promise { const since = new Date(Date.now() - hours * 60 * 60 * 1000); return await this.db .select({ time: metricsTable.timestamp, value: avg(metricsTable.value), }) .from(metricsTable) .where( and( eq(metricsTable.metric, metric), gte(metricsTable.timestamp, since) ) ) .groupBy( sql`DATE_TRUNC('hour', ${metricsTable.timestamp})` ) .orderBy(metricsTable.timestamp); } } ``` ## Summary To add custom schema to an elizaOS plugin: 1. **Define schema** without `agentId` for shared data 2. **Create repository** classes following elizaOS's pattern 3. **Create actions** to write data using `parseKeyValueXml` for structure 4. **Create providers** to read data and supply to agent context 5. **Register everything** in your plugin configuration elizaOS handles the rest - migrations, database connections, and making your data available across all agents in the system. ## What's Next? Learn about Actions, Providers, Evaluators, and Services Build your first plugin step by step Learn proven plugin development patterns Complete API reference for all interfaces # Overview Source: https://docs.elizaos.ai/projects/overview Understanding and building with elizaOS projects **Projects** are the primary deployable unit in elizaOS. Each project contains one or more agents, and plugins can be managed either at the agent level or shared across the project. A project is container template that you develop locally and ship to production. ## What is a Project? A project in elizaOS is your **development workspace and deployment unit**. It's where you: * Configure and run one or more agents * Develop and test plugins * Manage environment settings * Build complete AI applications Projects can include a fully customizable chat interface for user interaction, or run as headless services connecting to platforms like Discord or Twitter. The framework is flexible enough to support both frontend applications and background services. Think of a project as a TypeScript application that orchestrates agents. Each agent can have unique plugins, creating specialized capabilities within your application. ## Development Pattern The elizaOS development workflow centers around projects: Initialize your development workspace with `elizaos create --type project` Define agent personalities and select plugins Build custom plugins and actions within the project context Run and test using `elizaos start` without needing the full monorepo Ship the complete project as your production application This pattern gives you access to all elizaOS core features without managing the entire framework codebase. ## Project Structure ```bash Standard Project my-project/ ├── src/ # Source code (you edit these) │ ├── index.ts # Project entry point │ ├── character.ts # Default Eliza character │ ├── __tests__/ # Test files (E2E and component) │ ├── frontend/ # Frontend components (if using web UI) │ └── plugins/ # Custom plugins (optional) ├── .env # Environment variables ├── package.json # Dependencies ├── tsconfig.json # TypeScript config ├── bun.lock # Lock file │ ├── dist/ # 🔨 Built/compiled output ├── node_modules/ # 📦 Installed dependencies ├── .eliza/ # 💾 Local runtime data └── scripts/ # 🛠️ Utility scripts ``` ```bash Multi-Agent Project multi-agent-project/ ├── src/ # Source code │ ├── index.ts # Project orchestration │ ├── agents/ │ │ ├── eliza.ts # Eliza agent config │ │ ├── spartan.ts # Spartan agent config │ │ ├── billy.ts # Billy agent config │ │ └── bobby.ts # Bobby agent config │ ├── __tests__/ # Test files (E2E and component) │ ├── frontend/ # Frontend components (if using web UI) │ └── plugins/ # Shared custom plugins ├── .env # Shared environment ├── package.json ├── tsconfig.json │ ├── dist/ # 🔨 Built output ├── node_modules/ # 📦 Dependencies ├── .eliza/ # 💾 Runtime data └── scripts/ # 🛠️ Utilities ``` **Your code lives here:** This is your main entry point area to build your agents and logic. You can organize this however works for your project - there are some common patterns, but it's flexible. All your TypeScript source code and configurations go here. This is committed to version control and where you'll spend most of your dev time. **Created by:** `elizaos start` or `elizaos dev` (automatic), or manually with `bun run build` Contains the compiled JavaScript files from your TypeScript source. This is what actually runs when you start your project. * Safe to delete (will be regenerated automatically) * Not committed to version control **Created by:** `bun install` or `npm install` Contains all the packages your project depends on, including elizaOS core and plugins. * Safe to delete (reinstall with `bun install`) * Not committed to version control * Managed by your package manager **Created by:** Running your agent Stores local runtime data including: * PGLite database files (if using local DB) * Cache files * Temporary agent state * Plugin data storage Safe to delete to reset your local development state. **Created by:** Project template or manually Contains helpful scripts for development: * Test utilities * Database migrations * Deployment scripts * Custom build steps * Is committed to version control, but generally not used much during dev process **Clean Slate:** Run `rm -rf node_modules dist .eliza` to completely reset your project's generated files. Then `bun install` and your next `elizaos start` will rebuild automatically. ## Project Definition Projects are flexible containers you can design to fit your use case. The default entry point for every project is `src/index.ts`, where you define your agents, their character files, and any custom plugins or services. You might configure a single agent with specialized plugins, or coordinate multiple agents with distinct personalities and capabilities. Build your project structure to match your needs and goals, whether that's organizing agents by function, separating character configurations, or creating shared utilities. The elizaOS runtime will orchestrate everything once you define your project configuration. ```typescript src/index.ts (Single Agent) import { Project, ProjectAgent, IAgentRuntime } from '@elizaos/core'; import { character } from './character'; export const projectAgent: ProjectAgent = { character, init: async (runtime: IAgentRuntime) => { console.log(`Initializing ${character.name}`); // Optional: Setup knowledge base, connections, etc. }, // plugins: [customPlugin], // Project-specific plugins }; const project: Project = { agents: [projectAgent], }; export default project; ``` ```typescript src/index.ts (Multi-Agent) import { Project, ProjectAgent } from '@elizaos/core'; import { eliza, spartan, billy, bobby } from './agents'; const elizaAgent: ProjectAgent = { character: eliza, init: async (runtime) => { await setupElizaKnowledge(runtime); } }; const spartanAgent: ProjectAgent = { character: spartan, init: async (runtime) => { await initializeSpartanProtocols(runtime); } }; const billyAgent: ProjectAgent = { character: billy, init: async (runtime) => { await connectBillySystems(runtime); } }; const bobbyAgent: ProjectAgent = { character: bobby, init: async (runtime) => { await setupBobbyCapabilities(runtime); } }; const project: Project = { agents: [elizaAgent, spartanAgent, billyAgent, bobbyAgent] }; export default project; ``` ## Environment Configuration Projects use `.env` files for your API key management and configuration. Define these at the project level to start, then you can define them at the agent level in multi-agent projects using the `secrets` array in your character `.ts`/`.json` object. ```bash .env # Model Providers OPENAI_API_KEY=sk-... ANTHROPIC_API_KEY=sk-ant-... # Platform Integrations DISCORD_API_TOKEN=... TELEGRAM_BOT_TOKEN=... TWITTER_API_KEY=... # Database DATABASE_URL=postgresql://localhost/eliza PGLITE_DIR=./tmp/pglite # Settings LOG_LEVEL=info NODE_ENV=production ``` ## Running Projects Locally Start your project with the elizaOS CLI: ```bash # Start your project elizaos start # Development mode with hot reload elizaos dev # Start with specific character elizaos start --character ./custom-character.ts ``` See the [CLI Reference](/cli-reference/overview) for all available options and flags. ## Advanced Project Examples Explore these real-world projects showcasing different elizaOS capabilities: **Onchain Trading Agent** Advanced capabilities for blockchain trading: * Multi-modal reasoning across data types * Strategic decision-making framework * Long-term memory with retrieval strategies * Complex action chaining * Advanced prompt engineering **Business Profiles Swarm** Swarm of business profiles who collaborate and work together: * Executive, department, and operational layers * Inter-agent communication protocols * Specialized role-based expertise * Coordinated decision-making * Shared knowledge management **3D World Integration** Agent that connects to 3D virtual worlds: * WebSocket connection to Hyperfy worlds * Voice chat support (ElevenLabs/OpenAI) * Screen perception capabilities * 3D MMO development prototyping * Real-time interaction in virtual spaces **Web Application Template** Production-ready web interface for agents: * Real-time chat with Socket.IO * Document management system * Automatic API proxying * Debug panel for development * Ready for Vercel/Netlify deployment **Want more inspiration?** Check out [What You Can Build](/what-you-can-build) to see other agents you can create: trading bots, content creators, business automation, virtual NPCs, and more. ## Testing Projects Projects include comprehensive testing capabilities: ```bash # Run all tests elizaos test # Component tests only elizaos test --type component # End-to-end tests elizaos test --type e2e # Filter by test name elizaos test --name "character configuration" ``` See the [Test a Project](/guides/test-a-project) guide for comprehensive testing strategies. ## Deployment Options Projects come ready to be deployed out of the box. They include Dockerfiles so you can deploy using containers, or you can easily push your project to GitHub and use a managed cloud service. See the [Deploy a Project](/guides/deploy-a-project) guide for detailed instructions on all deployment methods. ## FAQ **Agent**: A single AI personality with specific capabilities defined by its character configuration and plugins. **Plugin**: A reusable module that adds specific capabilities (like Twitter integration, web search, or trading features) to agents. **Project**: The container application that runs one or more agents. It's your development workspace and what you deploy to production. **Key relationships:** * A project **has one or many** agents * Agents **import/use** plugins via their character configuration * Plugins are **reusable across** projects and agents * You can't have a project within a project or a project within a plugin * Projects are the top-level containers for agents and plugins * Plugin configuration can be defined at the project level (API keys in project `.env`) or agent-specific (in the character's `secrets` array) Think of it this way: Agents are the "who" (personalities), Plugins are the "what" (capabilities), Projects are the "where" (application container). Yes! You can create custom plugins in your project's `src/plugins/` directory and test them locally. You can also develop a standalone plugin with `elizaos create --type plugin`. If you run `elizaos dev` from that plugin directory, it will spin up with a test agent. But ultimately, everyone will want to add that plugin to a project at some point. See the [Create a Plugin](/guides/create-a-plugin) guide for details. No! That's the beauty of projects. When you create a project with `elizaos create`, you get a standalone workspace with just what you need. The CLI and core packages provide all the framework functionality without the monorepo complexity. Agents in the same project can communicate through: * Shared memory systems * Message passing via the runtime * Event-driven coordination * Shared service instances See the [Add Multiple Agents](/guides/add-multiple-agents) guide for patterns. Yes! Each agent can configure its own model provider through settings: ```typescript settings: { modelProvider: 'anthropic', // or 'openai', 'llama', etc. } ``` It depends on what you're comfortable with. We support managed cloud, Docker, bare metal, whatever you like. See the [Deploy a Project](/guides/deploy-a-project) guide for detailed information on all deployment options. ## Next Steps Get started in under 5 minutes Learn to configure agent personalities Complete command reference REST API documentation # Quickstart Source: https://docs.elizaos.ai/quickstart Create and run your first elizaOS agent in 3 minutes **Video Tutorial**: [**Your First Agent**](https://www.youtube.com/watch?v=MP4ldEqmTiE\&list=PLrjBjP4nU8ehOgKAa0-XddHzE0KK0nNvS\&index=2) ## Create Your First Agent Let's create a project with our default Eliza character that can chat and take actions. Create a new elizaOS project using the interactive CLI: ```bash Terminal elizaos create ``` The CLI is for building agents and projects. The monorepo is only for contributing to elizaOS core. See [CLI vs Monorepo](https://www.youtube.com/watch?v=tSO4wpQs2OA\&list=PLrjBjP4nU8ehOgKAa0-XddHzE0KK0nNvS). During the interactive setup, you'll be prompted to make the following selections: 1. **Project Name**: Enter your desired project name 2. **Database**: Select `pglite` for a lightweight, local PostgreSQL option 3. **Model Provider**: Select `OpenAI` for this quickstart guide 4. **API Key**: You'll be prompted to enter your OpenAI API key You can get an OpenAI API key from the [OpenAI Platform](https://platform.openai.com/). Both database and model provider choices can be reconfigured later. Change directory to your newly created project (replace with your project name): ```bash Terminal cd my-eliza-project ``` Launch your elizaOS project: ```bash Terminal elizaos start ``` Wait a few seconds for the server to start up. Open your browser and navigate to: ``` http://localhost:3000 ``` Start chatting with Eliza through the web interface! ## Troubleshooting If you're getting authentication or configuration errors: Open your project in Cursor (or any IDE) and look at the `.env` file in your project root. Make sure these are set correctly: ``` OPENAI_API_KEY=your-actual-api-key-here ``` ``` PGLITE_DATA_DIR=/path/to/your/project/.eliza/.elizadb ``` **Common issues:** * OpenAI API key is missing, invalid, or has extra spaces/quotes * Wrong OpenAI API key format (should start with `sk-`) * OpenAI account has no credits remaining **Note:** You can also configure environment variables in the terminal with `elizaos env` If you're having database issues, check your `.env` file for the database path: ``` PGLITE_DATA_DIR=/path/to/your/project/.eliza/.elizadb ``` **Common issues:** * Database path is incorrect in `.env` file * `.eliza/.elizadb` directory doesn't exist or has wrong permissions * Database corruption (recreate the project if this happens) **When in doubt, turn it off and on again:** * Stop the server with `Ctrl+C`, then start it again with `elizaos start` * Check the [installation guide](/installation) to confirm you installed everything correctly **Still can't access the frontend after 10 seconds?** * If you can't reach `http://localhost:3000`, delete your project and start fresh * Navigate to the folder containing your project, then run: ```bash Terminal rm -rf my-eliza-project && elizaos create ``` * Follow the Quickstart steps again carefully # Core Source: https://docs.elizaos.ai/runtime/core Core runtime system, lifecycle, and architecture ## System Architecture The elizaOS runtime follows a modular, plugin-based architecture that orchestrates all agent functionality. For lifecycle details, see [Runtime and Lifecycle](/agents/runtime-and-lifecycle). For extension architecture, see [Plugin Architecture](/plugins/architecture). ```mermaid flowchart TD User[User Input] --> Runtime[AgentRuntime] Runtime --> State[State Composition] State --> Providers[Providers] Runtime --> Actions[Action Selection] Actions --> Handler[Action Handler] Handler --> Response[Response] Response --> Evaluators[Evaluators] Evaluators --> User Runtime -.-> Services[Background Services] Runtime -.-> Events[Event System] Runtime -.-> Memory[(Memory Store)] classDef user fill:#2196f3,color:#fff classDef runtime fill:#4caf50,color:#fff classDef processing fill:#9c27b0,color:#fff classDef support fill:#ff9800,color:#fff class User user class Runtime runtime class State,Providers,Actions,Handler,Response,Evaluators processing class Services,Events,Memory support ``` ### Core Components The runtime orchestrates these essential components: * **AgentRuntime**: Central orchestrator managing agent lifecycle * **Plugin System**: Extends functionality through modular components * **Memory System**: Hierarchical storage for conversations and knowledge * **State Management**: Aggregates context from multiple sources * **Service Layer**: Background processes and integrations For related documentation, see [Plugin Architecture](/plugins/architecture), [Memory](/runtime/memory), and [Services](/runtime/services). ## AgentRuntime Class The `AgentRuntime` class is the central engine that manages agent lifecycle, processes messages, and coordinates all system components. ### Core Interface ```typescript interface IAgentRuntime extends IDatabaseAdapter { // Core properties agentId: UUID; character: Character; providers: Provider[]; actions: Action[]; evaluators: Evaluator[]; services: Service[]; // Action processing processActions(message: Memory, responses: Memory[], state?: State): Promise; composeState(message: Memory, state?: State): Promise; evaluate(message: Memory, state?: State): Promise; // Component registration registerAction(action: Action): void; registerProvider(provider: Provider): void; registerEvaluator(evaluator: Evaluator): void; registerService(service: Service): void; // Service management getService(name: ServiceType): T; stop(): Promise; // Model management useModel(modelType: T, params: ModelParamsMap[T], provider?: string): Promise; registerModel(modelType: ModelTypeName, handler: ModelHandler, provider?: string, priority?: number): void; getModel(modelType: ModelTypeName, provider?: string): ModelHandler | undefined; // Event system emit(eventType: EventType, data: any): Promise; on(eventType: EventType, handler: EventHandler): void; } ``` ### Key Responsibilities #### 1. Action Processing The runtime orchestrates action selection and execution: ```typescript async processActions(message: Memory, responses: Memory[], state?: State): Promise { // Select and execute actions based on context const actions = await this.selectActions(message, state); for (const action of actions) { await action.handler(this, message, state); } // Run evaluators on results await this.evaluate(message, state); } ``` #### 2. State Composition Builds comprehensive context by aggregating data from providers: ```typescript async composeState(message: Memory): Promise { const state = {}; for (const provider of this.providers) { const data = await provider.get(this, message, state); Object.assign(state, data); } return state; } ``` #### 3. Plugin Management Registers and initializes plugin components: ```typescript async registerPlugin(plugin: Plugin) { // Register components plugin.actions?.forEach(a => this.registerAction(a)); plugin.providers?.forEach(p => this.registerProvider(p)); plugin.evaluators?.forEach(e => this.registerEvaluator(e)); plugin.services?.forEach(s => this.registerService(s)); // Initialize plugin await plugin.init?.(this.config, this); } ``` ## Runtime Lifecycle ```mermaid flowchart TD Create[Create Runtime] --> Init[Initialize] Init --> LoadChar[Load Character] LoadChar --> LoadPlugins[Load Plugins] LoadPlugins --> StartServices[Start Services] StartServices --> Ready[Ready] Ready --> Process[Process Messages] Process --> Ready Ready --> Stop[Stop Services] Stop --> Cleanup[Cleanup] classDef setup fill:#2196f3,color:#fff classDef active fill:#4caf50,color:#fff classDef shutdown fill:#ff9800,color:#fff class Create,Init,LoadChar,LoadPlugins,StartServices setup class Ready,Process active class Stop,Cleanup shutdown ``` ### Initialization Sequence 1. **Runtime Creation**: Instantiate with character and configuration 2. **Character Loading**: Load agent personality and settings 3. **Plugin Loading**: Register plugins in dependency order 4. **Service Startup**: Initialize background services 5. **Ready State**: Agent ready to process messages ### Plugin Loading Order ```typescript // Plugin priority determines load order const pluginLoadOrder = [ databases, // Priority: -100 modelProviders, // Priority: -50 corePlugins, // Priority: 0 features, // Priority: 50 platforms // Priority: 100 ]; ``` ## Configuration ### Runtime Configuration The runtime accepts configuration through multiple sources: ```typescript interface RuntimeConfig { character: Character; plugins: Plugin[]; database?: DatabaseConfig; models?: ModelConfig; services?: ServiceConfig; environment?: EnvironmentConfig; } ``` ### Environment Variables Core runtime environment variables: * `NODE_ENV` - Runtime environment (development/production) * `LOG_LEVEL` - Logging verbosity * `DATABASE_URL` - Database connection string * `API_PORT` - Server port for API endpoints * `AGENT_ID` - Unique agent identifier ### Settings Management Access configuration through the runtime: ```typescript // Get setting with fallback const apiKey = runtime.getSetting("API_KEY"); // Check if setting exists if (runtime.hasSetting("FEATURE_FLAG")) { // Feature is enabled } ``` ## Database Abstraction The runtime implements `IDatabaseAdapter` for data persistence: ```typescript interface IDatabaseAdapter { // Memory operations createMemory(memory: Memory): Promise; searchMemories(query: string, limit?: number): Promise; getMemoryById(id: UUID): Promise; // Entity management createEntity(entity: Entity): Promise; updateEntity(entity: Entity): Promise; getEntity(id: UUID): Promise; // Relationships createRelationship(rel: Relationship): Promise; getRelationships(entityId: UUID): Promise; // Facts and knowledge createFact(fact: Fact): Promise; searchFacts(query: string): Promise; } ``` ### Memory Operations ```typescript // Store a message await runtime.createMemory({ type: MemoryType.MESSAGE, content: { text: "User message" }, roomId: message.roomId, userId: message.userId }); // Search memories const memories = await runtime.searchMemories( "previous conversation", 10 // limit ); // Get specific memory const memory = await runtime.getMemoryById(memoryId); ``` ## Message Processing Pipeline The runtime processes messages through a defined pipeline: ```mermaid flowchart TD Message[Incoming Message] --> Memory[Store in Memory] Memory --> State[Compose State] State --> Actions[Select Actions] Actions --> Execute[Execute Actions] Execute --> Evaluate[Run Evaluators] Evaluate --> Response[Generate Response] classDef input fill:#2196f3,color:#fff classDef storage fill:#4caf50,color:#fff classDef processing fill:#9c27b0,color:#fff classDef output fill:#ff9800,color:#fff class Message input class Memory storage class State,Actions,Execute,Evaluate processing class Response output ``` ### Processing Steps 1. **Message Receipt**: Receive and validate incoming message 2. **Memory Storage**: Persist message to database 3. **State Composition**: Build context from providers 4. **Action Selection**: Choose appropriate actions 5. **Action Execution**: Run selected action handlers 6. **Evaluation**: Post-process results 7. **Response Generation**: Create and send response ## Error Handling The runtime implements comprehensive error handling: ```typescript try { await runtime.processActions(message, responses, state); } catch (error) { if (error instanceof ActionError) { // Handle action-specific errors runtime.logger.error("Action failed:", error); } else if (error instanceof StateError) { // Handle state composition errors runtime.logger.error("State error:", error); } else { // Handle unexpected errors runtime.logger.error("Unexpected error:", error); // Optionally trigger recovery } } ``` ## Performance Considerations ### State Caching The runtime caches composed state for performance: ```typescript // State is cached by message ID const state = await runtime.composeState(message); // Subsequent calls use cache const cachedState = await runtime.composeState(message); ``` ### Service Pooling Services are singleton instances shared across the runtime: ```typescript // Services are created once and reused const service = runtime.getService(ServiceType.DATABASE); // Same instance returned const sameService = runtime.getService(ServiceType.DATABASE); ``` ## Best Practices ### Runtime Initialization * Initialize plugins in dependency order * Start services after all plugins are loaded * Verify character configuration before starting * Set up error handlers before processing ### Resource Management * Clean up services on shutdown * Clear state cache periodically * Monitor memory usage * Implement connection pooling ### Error Recovery * Implement retry logic for transient failures * Log errors with context * Gracefully degrade functionality * Maintain audit trail ## Integration Points The runtime provides multiple integration points: * **Plugins**: Extend functionality through the plugin system * **Events**: React to runtime events * **Services**: Add background processes * **Models**: Integrate AI providers * **Database**: Custom database adapters * **API**: HTTP endpoints through routes ## What's Next? Learn about the fundamental storage layer Understand the communication backbone Explore how to supply data to the runtime Discover AI model management # Events Source: https://docs.elizaos.ai/runtime/events Event system, event types, and event handling patterns ## Event System The event system enables reactive programming patterns, allowing plugins and services to respond to runtime activities. Events flow through the system providing hooks for custom logic and integrations. ## Event Architecture ```mermaid flowchart TD Source[Event Source] --> Emit[Runtime.emit] Emit --> Queue[Event Queue] Queue --> Handlers[Event Handlers] Handlers --> H1[Handler 1] Handlers --> H2[Handler 2] Handlers --> H3[Handler 3] H1 --> Process[Process Event] H2 --> Process H3 --> Process classDef source fill:#2196f3,color:#fff classDef system fill:#9c27b0,color:#fff classDef handlers fill:#4caf50,color:#fff classDef processing fill:#ff9800,color:#fff class Source source class Emit,Queue system class Handlers,H1,H2,H3 handlers class Process processing ``` ## Event Types ### Core Event Types ```typescript enum EventType { // World events WORLD_JOINED = 'world:joined', WORLD_CONNECTED = 'world:connected', WORLD_LEFT = 'world:left', // Entity events ENTITY_JOINED = 'entity:joined', ENTITY_LEFT = 'entity:left', ENTITY_UPDATED = 'entity:updated', // Room events ROOM_JOINED = 'room:joined', ROOM_LEFT = 'room:left', ROOM_UPDATED = 'room:updated', // Message events MESSAGE_RECEIVED = 'message:received', MESSAGE_SENT = 'message:sent', MESSAGE_DELETED = 'message:deleted', MESSAGE_UPDATED = 'message:updated', // Voice events VOICE_MESSAGE_RECEIVED = 'voice:message:received', VOICE_MESSAGE_SENT = 'voice:message:sent', VOICE_STARTED = 'voice:started', VOICE_ENDED = 'voice:ended', // Run events RUN_STARTED = 'run:started', RUN_COMPLETED = 'run:completed', RUN_FAILED = 'run:failed', RUN_TIMEOUT = 'run:timeout', // Action events ACTION_STARTED = 'action:started', ACTION_COMPLETED = 'action:completed', ACTION_FAILED = 'action:failed', // Evaluator events EVALUATOR_STARTED = 'evaluator:started', EVALUATOR_COMPLETED = 'evaluator:completed', EVALUATOR_FAILED = 'evaluator:failed', // Model events MODEL_USED = 'model:used', MODEL_FAILED = 'model:failed', // Service events SERVICE_STARTED = 'service:started', SERVICE_STOPPED = 'service:stopped', SERVICE_ERROR = 'service:error' } ``` ## Event Payloads ### Payload Interfaces Each event type has a specific payload structure: ```typescript // Message event payload interface MessagePayload { runtime: IAgentRuntime; message: Memory; room?: Room; user?: User; callback?: ResponseCallback; } // World event payload interface WorldPayload { runtime: IAgentRuntime; world: World; metadata?: Record; } // Entity event payload interface EntityPayload { runtime: IAgentRuntime; entity: Entity; action: 'joined' | 'left' | 'updated'; changes?: Partial; } // Action event payload interface ActionPayload { runtime: IAgentRuntime; action: Action; message: Memory; state: State; result?: any; error?: Error; } // Model event payload interface ModelPayload { runtime: IAgentRuntime; modelType: ModelTypeName; provider: string; params: any; result?: any; error?: Error; duration: number; } ``` ## Event Handlers ### Handler Registration Event handlers are registered during plugin initialization: ```typescript const myPlugin: Plugin = { name: 'my-plugin', events: { [EventType.MESSAGE_RECEIVED]: [ handleMessageReceived, logMessage ], [EventType.ACTION_COMPLETED]: [ processActionResult ], [EventType.RUN_COMPLETED]: [ cleanupRun ] } }; ``` ### Handler Implementation ```typescript // Message handler async function handleMessageReceived(payload: MessagePayload) { const { runtime, message, room, user, callback } = payload; // Process the message const state = await runtime.composeState(message); // Check if we should respond if (shouldRespond(message, state)) { await runtime.processActions(message, [], state); } // Call callback if provided if (callback) { await callback({ text: 'Message processed', metadata: { processed: true } }); } } // Action handler async function processActionResult(payload: ActionPayload) { const { runtime, action, result, error } = payload; if (error) { runtime.logger.error(`Action ${action.name} failed:`, error); // Handle error return; } // Process successful result runtime.logger.info(`Action ${action.name} completed:`, result); // Store result in memory await runtime.createMemory({ type: MemoryType.ACTION, content: { text: `Action ${action.name} completed`, action: action.name, result }, roomId: payload.message.roomId }); } ``` ## Event Emission ### Emitting Events ```typescript // Emit an event from the runtime await runtime.emit(EventType.MESSAGE_RECEIVED, { runtime, message, room, user, callback }); // Emit from a service class CustomService extends Service { async processData(data: any) { await this.runtime.emit(EventType.CUSTOM_EVENT, { runtime: this.runtime, data, timestamp: Date.now() }); } } // Emit from an action const customAction: Action = { name: 'CUSTOM_ACTION', handler: async (runtime, message, state) => { // Do work const result = await performAction(); // Emit completion event await runtime.emit(EventType.ACTION_COMPLETED, { runtime, action: customAction, message, state, result }); return result; } }; ``` ## Event Listeners ### Adding Event Listeners ```typescript // Add listener to runtime runtime.on(EventType.MESSAGE_RECEIVED, async (payload) => { console.log('Message received:', payload.message.content.text); }); // Add multiple listeners runtime.on(EventType.ACTION_STARTED, logActionStart); runtime.on(EventType.ACTION_STARTED, trackActionMetrics); runtime.on(EventType.ACTION_STARTED, notifyActionStart); // One-time listener runtime.once(EventType.SERVICE_STARTED, async (payload) => { console.log('Service started:', payload.service.name); }); ``` ### Removing Event Listeners ```typescript // Remove specific listener runtime.off(EventType.MESSAGE_RECEIVED, messageHandler); // Remove all listeners for an event runtime.removeAllListeners(EventType.MESSAGE_RECEIVED); // Remove all listeners runtime.removeAllListeners(); ``` ## Event Patterns ### Request-Response Pattern ```typescript // Emit event and wait for response async function requestWithResponse(runtime: IAgentRuntime, data: any) { return new Promise((resolve, reject) => { const timeout = setTimeout(() => { reject(new Error('Response timeout')); }, 5000); // Listen for response runtime.once(EventType.RESPONSE_RECEIVED, (payload) => { clearTimeout(timeout); resolve(payload.response); }); // Emit request runtime.emit(EventType.REQUEST_SENT, { runtime, data, requestId: generateId() }); }); } ``` ### Event Chaining ```typescript // Chain events for complex workflows const workflowPlugin: Plugin = { name: 'workflow', events: { [EventType.MESSAGE_RECEIVED]: [ async (payload) => { // Step 1: Process message const processed = await processMessage(payload); // Emit next event await payload.runtime.emit(EventType.MESSAGE_PROCESSED, { ...payload, processed }); } ], [EventType.MESSAGE_PROCESSED]: [ async (payload) => { // Step 2: Generate response const response = await generateResponse(payload); // Emit next event await payload.runtime.emit(EventType.RESPONSE_GENERATED, { ...payload, response }); } ], [EventType.RESPONSE_GENERATED]: [ async (payload) => { // Step 3: Send response await sendResponse(payload); } ] } }; ``` ### Event Aggregation ```typescript // Aggregate multiple events class EventAggregator { private events: Map = new Map(); private flushInterval: NodeJS.Timer; constructor(private runtime: IAgentRuntime) { // Listen for events to aggregate runtime.on(EventType.MODEL_USED, this.aggregate.bind(this)); // Flush periodically this.flushInterval = setInterval(() => this.flush(), 60000); } private aggregate(payload: ModelPayload) { const key = `${payload.modelType}:${payload.provider}`; if (!this.events.has(key)) { this.events.set(key, []); } this.events.get(key).push({ timestamp: Date.now(), duration: payload.duration, params: payload.params }); } private async flush() { for (const [key, events] of this.events.entries()) { const [modelType, provider] = key.split(':'); // Calculate metrics const metrics = { count: events.length, avgDuration: events.reduce((sum, e) => sum + e.duration, 0) / events.length, totalDuration: events.reduce((sum, e) => sum + e.duration, 0) }; // Emit aggregated event await this.runtime.emit(EventType.METRICS_AGGREGATED, { runtime: this.runtime, modelType, provider, metrics, period: 60000 }); } // Clear events this.events.clear(); } stop() { clearInterval(this.flushInterval); } } ``` ## Custom Events ### Defining Custom Events ```typescript // Extend EventType with custom events declare module '@elizaos/core' { interface EventTypeRegistry { CUSTOM_DATA_RECEIVED: 'custom:data:received'; CUSTOM_PROCESS_COMPLETE: 'custom:process:complete'; CUSTOM_ERROR_OCCURRED: 'custom:error:occurred'; } } // Define custom payload interface CustomDataPayload { runtime: IAgentRuntime; data: any; source: string; timestamp: number; } ``` ### Using Custom Events ```typescript const customPlugin: Plugin = { name: 'custom-plugin', events: { 'custom:data:received': [ async (payload: CustomDataPayload) => { // Process custom data const processed = await processCustomData(payload.data); // Emit completion await payload.runtime.emit('custom:process:complete', { runtime: payload.runtime, original: payload.data, processed, duration: Date.now() - payload.timestamp }); } ] }, actions: [{ name: 'RECEIVE_DATA', handler: async (runtime, message, state) => { // Emit custom event await runtime.emit('custom:data:received', { runtime, data: message.content, source: 'user', timestamp: Date.now() }); } }] }; ``` ## Event Middleware ### Creating Event Middleware ```typescript // Middleware to log all events function loggingMiddleware(eventType: EventType, payload: any) { console.log(`[Event] ${eventType}:`, { timestamp: new Date().toISOString(), type: eventType, payload: JSON.stringify(payload).slice(0, 200) }); } // Middleware to filter events function filterMiddleware(allowedEvents: EventType[]) { return (eventType: EventType, payload: any, next: Function) => { if (allowedEvents.includes(eventType)) { next(payload); } }; } // Middleware to transform payload function transformMiddleware(eventType: EventType, payload: any, next: Function) { const transformed = { ...payload, timestamp: Date.now(), eventType }; next(transformed); } ``` ## Error Handling ### Event Error Handling ```typescript // Global error handler for events runtime.on('error', (error: Error, eventType: EventType, payload: any) => { console.error(`Error in event ${eventType}:`, error); // Log to monitoring service monitoringService.logError({ error: error.message, stack: error.stack, eventType, payload: JSON.stringify(payload).slice(0, 1000) }); }); // Handler with error handling async function safeEventHandler(payload: any) { try { await riskyOperation(payload); } catch (error) { // Emit error event await payload.runtime.emit(EventType.SERVICE_ERROR, { runtime: payload.runtime, error, originalEvent: payload }); // Don't throw - allow other handlers to run } } ``` ## Performance Considerations ### Event Batching ```typescript class EventBatcher { private batch: Map = new Map(); private batchSize = 100; private flushInterval = 1000; private timer: NodeJS.Timer; constructor(private runtime: IAgentRuntime) { this.timer = setInterval(() => this.flush(), this.flushInterval); } add(eventType: EventType, payload: any) { if (!this.batch.has(eventType)) { this.batch.set(eventType, []); } const events = this.batch.get(eventType); events.push(payload); if (events.length >= this.batchSize) { this.flushType(eventType); } } private flushType(eventType: EventType) { const events = this.batch.get(eventType); if (!events || events.length === 0) return; // Emit batch event this.runtime.emit(`${eventType}:batch` as EventType, { runtime: this.runtime, events, count: events.length }); this.batch.set(eventType, []); } flush() { for (const eventType of this.batch.keys()) { this.flushType(eventType); } } stop() { clearInterval(this.timer); this.flush(); } } ``` ### Event Throttling ```typescript // Throttle high-frequency events function throttleEvents(eventType: EventType, delay: number) { let lastEmit = 0; let pending: any = null; let timer: NodeJS.Timeout; return (payload: any) => { const now = Date.now(); if (now - lastEmit >= delay) { // Emit immediately runtime.emit(eventType, payload); lastEmit = now; } else { // Store for later pending = payload; // Schedule emit if (!timer) { timer = setTimeout(() => { if (pending) { runtime.emit(eventType, pending); lastEmit = Date.now(); pending = null; } timer = null; }, delay - (now - lastEmit)); } } }; } ``` ## Best Practices ### Event Design * **Specific Events**: Create specific events rather than generic ones * **Consistent Payloads**: Use consistent payload structures * **Event Naming**: Use clear, hierarchical naming (domain:action:status) * **Documentation**: Document event types and payloads * **Versioning**: Version events when making breaking changes ### Performance * **Async Handlers**: Always use async handlers * **Non-Blocking**: Don't block the event loop * **Batching**: Batch high-frequency events * **Throttling**: Throttle rapid events * **Cleanup**: Remove unused listeners ### Error Handling * **Graceful Failures**: Don't crash on handler errors * **Error Events**: Emit error events for monitoring * **Timeouts**: Set timeouts for long operations * **Retries**: Implement retry logic for transient failures * **Logging**: Log errors with context ## What's Next? Learn how providers use events Explore AI model management Build services that emit and handle events Understand real-time event streaming # Memory & State Source: https://docs.elizaos.ai/runtime/memory Memory system, state management, and data persistence ## Memory System The memory system provides hierarchical storage for conversations, knowledge, and agent state. It enables agents to maintain context, learn from interactions, and build persistent knowledge. For conceptual overview, see [Memory and State](/agents/memory-and-state). For runtime architecture, see [Runtime Core](/runtime/core). ## Memory Types ### Core Memory Types ```typescript enum MemoryType { MESSAGE = 'message', // Conversation messages FACT = 'fact', // Extracted knowledge DOCUMENT = 'document', // Document storage RELATIONSHIP = 'relationship', // Entity relationships GOAL = 'goal', // Agent goals TASK = 'task', // Scheduled tasks ACTION = 'action', // Action execution records } ``` ### Memory Interface ```typescript interface Memory { id: UUID; type: MemoryType; roomId: UUID; userId?: UUID; agentId?: UUID; content: { text: string; [key: string]: any; }; embedding?: number[]; createdAt: Date; updatedAt?: Date; metadata?: Record; } ``` ## State Management ### State Structure State represents the agent's current understanding of context: ```typescript interface State { // Key-value pairs for template access values: Record; // Structured data from providers data: Record; // Concatenated textual context text: string; } ``` ### State Composition Pipeline ```mermaid flowchart TD Message[Message Received] --> Store[Store in Memory] Store --> Select[Select Providers] Select --> Execute[Execute Providers] Execute --> Aggregate[Aggregate Results] Aggregate --> Cache[Cache State] Cache --> Return[Return State] classDef input fill:#2196f3,color:#fff classDef storage fill:#4caf50,color:#fff classDef processing fill:#9c27b0,color:#fff classDef output fill:#ff9800,color:#fff class Message input class Store,Cache storage class Select,Execute,Aggregate processing class Return output ``` ## Memory Operations ### Creating Memories ```typescript // Store a message await runtime.createMemory({ type: MemoryType.MESSAGE, content: { text: "User message", role: 'user', name: 'John' }, roomId: message.roomId, userId: message.userId, metadata: { platform: 'discord', channelId: '12345' } }); // Store a fact await runtime.createMemory({ type: MemoryType.FACT, content: { text: "The user's favorite color is blue", subject: 'user', predicate: 'favorite_color', object: 'blue' }, roomId: message.roomId }); // Store an action result await runtime.createMemory({ type: MemoryType.ACTION, content: { text: "Generated image of a sunset", action: 'IMAGE_GENERATION', result: { url: 'https://...' } }, roomId: message.roomId, agentId: runtime.agentId }); ``` ### Searching Memories ```typescript // Text search with embeddings const memories = await runtime.searchMemories( "previous conversation about colors", 10 // limit ); // Search with filters const facts = await runtime.searchMemories({ query: "user preferences", type: MemoryType.FACT, roomId: currentRoom.id, limit: 5 }); // Search by time range const recentMessages = await runtime.searchMemories({ type: MemoryType.MESSAGE, roomId: currentRoom.id, after: new Date(Date.now() - 3600000), // Last hour limit: 20 }); ``` ### Memory Retrieval ```typescript // Get specific memory const memory = await runtime.getMemoryById(memoryId); // Get memories by room const roomMemories = await runtime.getMemoriesByRoom( roomId, { type: MemoryType.MESSAGE, limit: 50 } ); // Get user memories const userMemories = await runtime.getMemoriesByUser( userId, { type: MemoryType.FACT } ); ``` ## Embeddings and Similarity ### Creating Embeddings ```typescript // Generate embedding for text const embedding = await runtime.useModel( ModelType.TEXT_EMBEDDING, { input: "Text to embed" } ); // Store with embedding await runtime.createMemory({ type: MemoryType.MESSAGE, content: { text: "Important message" }, embedding: embedding, roomId: message.roomId }); ``` ### Similarity Search ```typescript // Search by semantic similarity const similarMemories = await runtime.searchMemoriesBySimilarity( embedding, { threshold: 0.8, // Similarity threshold (0-1) limit: 10, type: MemoryType.MESSAGE } ); // Find related facts const queryEmbedding = await runtime.useModel( ModelType.TEXT_EMBEDDING, { input: "What does the user like?" } ); const relatedFacts = await runtime.searchMemoriesBySimilarity( queryEmbedding, { type: MemoryType.FACT, threshold: 0.7, limit: 5 } ); ``` ## Facts and Knowledge ### Fact Extraction Facts are automatically extracted from conversations: ```typescript interface Fact extends Memory { type: MemoryType.FACT; content: { text: string; subject?: string; // Entity the fact is about predicate?: string; // Relationship or property object?: string; // Value or related entity confidence?: number; // Extraction confidence source?: string; // Source message ID }; } ``` ### Fact Management ```typescript // Create a fact await runtime.createFact({ subject: 'user', predicate: 'works_at', object: 'TechCorp', confidence: 0.95, source: message.id }); // Query facts const userFacts = await runtime.getFacts({ subject: 'user', limit: 10 }); // Update fact confidence await runtime.updateFact(factId, { confidence: 0.98 }); ``` ## Relationships ### Relationship Storage ```typescript interface Relationship extends Memory { type: MemoryType.RELATIONSHIP; userId: UUID; targetEntityId: UUID; relationshipType: string; strength: number; metadata?: { firstInteraction?: Date; lastInteraction?: Date; interactionCount?: number; sentiment?: number; }; } ``` ### Managing Relationships ```typescript // Create relationship await runtime.createRelationship({ userId: user.id, targetEntityId: otherUser.id, relationshipType: 'friend', strength: 0.8 }); // Get user relationships const relationships = await runtime.getRelationships(userId); // Update relationship strength await runtime.updateRelationship(relationshipId, { strength: 0.9, metadata: { lastInteraction: new Date(), interactionCount: prevCount + 1 } }); ``` ## State Cache ### Cache Architecture The runtime maintains an in-memory cache for composed states: ```typescript class StateCache { private cache: Map; private timestamps: Map; private maxSize: number; private ttl: number; constructor(maxSize = 1000, ttl = 300000) { // 5 min TTL this.cache = new Map(); this.timestamps = new Map(); this.maxSize = maxSize; this.ttl = ttl; } set(messageId: UUID, state: State): void { // Evict oldest if at capacity if (this.cache.size >= this.maxSize) { const oldest = this.getOldestEntry(); if (oldest) { this.cache.delete(oldest); this.timestamps.delete(oldest); } } this.cache.set(messageId, state); this.timestamps.set(messageId, Date.now()); } get(messageId: UUID): State | undefined { const timestamp = this.timestamps.get(messageId); // Check if expired if (timestamp && Date.now() - timestamp > this.ttl) { this.cache.delete(messageId); this.timestamps.delete(messageId); return undefined; } return this.cache.get(messageId); } } ``` ### Cache Management ```typescript // Clear old cache entries function cleanupCache(runtime: IAgentRuntime) { const now = Date.now(); const maxAge = 5 * 60 * 1000; // 5 minutes for (const [messageId, timestamp] of runtime.stateCache.timestamps) { if (now - timestamp > maxAge) { runtime.stateCache.delete(messageId); } } } // Schedule periodic cleanup setInterval(() => cleanupCache(runtime), 60000); ``` ## Document Storage ### Document Memory ```typescript interface DocumentMemory extends Memory { type: MemoryType.DOCUMENT; content: { text: string; title?: string; source?: string; chunks?: string[]; summary?: string; }; embedding?: number[]; metadata?: { mimeType?: string; size?: number; hash?: string; tags?: string[]; }; } ``` ### Document Operations ```typescript // Store document await runtime.createDocument({ title: 'User Manual', content: documentText, source: 'https://example.com/manual.pdf', chunks: splitIntoChunks(documentText), metadata: { mimeType: 'application/pdf', size: 1024000, tags: ['manual', 'reference'] } }); // Search documents const relevantDocs = await runtime.searchDocuments( "how to configure settings", { limit: 5 } ); // Get document chunks const chunks = await runtime.getDocumentChunks( documentId, { relevant_to: "specific query" } ); ``` ## Memory Cleanup ### Automatic Cleanup ```typescript class MemoryCleanupService { private readonly MAX_MESSAGE_AGE = 30 * 24 * 60 * 60 * 1000; // 30 days private readonly MAX_FACTS_PER_ROOM = 1000; async cleanup(runtime: IAgentRuntime) { // Remove old messages await this.cleanupOldMessages(runtime); // Consolidate facts await this.consolidateFacts(runtime); // Remove orphaned relationships await this.cleanupOrphanedRelationships(runtime); } private async cleanupOldMessages(runtime: IAgentRuntime) { const cutoffDate = new Date(Date.now() - this.MAX_MESSAGE_AGE); await runtime.deleteMemories({ type: MemoryType.MESSAGE, before: cutoffDate }); } private async consolidateFacts(runtime: IAgentRuntime) { const rooms = await runtime.getAllRooms(); for (const room of rooms) { const facts = await runtime.getFacts({ roomId: room.id }); if (facts.length > this.MAX_FACTS_PER_ROOM) { // Keep only high-confidence recent facts const toKeep = facts .sort((a, b) => { const scoreA = a.confidence * (1 / (Date.now() - a.createdAt)); const scoreB = b.confidence * (1 / (Date.now() - b.createdAt)); return scoreB - scoreA; }) .slice(0, this.MAX_FACTS_PER_ROOM); const toDelete = facts.filter(f => !toKeep.includes(f)); for (const fact of toDelete) { await runtime.deleteMemory(fact.id); } } } } } ``` ### Manual Cleanup ```typescript // Delete specific memories await runtime.deleteMemory(memoryId); // Bulk delete await runtime.deleteMemories({ type: MemoryType.MESSAGE, roomId: roomId, before: cutoffDate }); // Clear room memories await runtime.clearRoomMemories(roomId); ``` ## Memory Optimization ### Indexing Strategies ```typescript // Database indexes for performance CREATE INDEX idx_memories_room_type ON memories(roomId, type); CREATE INDEX idx_memories_user_created ON memories(userId, createdAt); CREATE INDEX idx_memories_embedding ON memories USING ivfflat (embedding); CREATE INDEX idx_facts_subject_predicate ON facts(subject, predicate); ``` ### Batch Operations ```typescript // Batch insert memories await runtime.createMemoriesBatch([ { type: MemoryType.MESSAGE, content: { text: "Message 1" }, roomId }, { type: MemoryType.MESSAGE, content: { text: "Message 2" }, roomId }, // ... more memories ]); // Batch embedding generation const texts = memories.map(m => m.content.text); const embeddings = await runtime.useModel( ModelType.TEXT_EMBEDDING, { input: texts, batch: true } ); ``` ### Memory Compression ```typescript // Compress old memories async function compressMemories(runtime: IAgentRuntime, roomId: UUID) { const messages = await runtime.getMemories({ type: MemoryType.MESSAGE, roomId, before: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) // 7 days }); // Generate summary const summary = await runtime.useModel( ModelType.TEXT_LARGE, { prompt: `Summarize these messages: ${messages.map(m => m.content.text).join('\n')}`, maxTokens: 500 } ); // Store summary await runtime.createMemory({ type: MemoryType.DOCUMENT, content: { text: summary, title: 'Conversation Summary', source: 'compressed_messages' }, roomId, metadata: { originalCount: messages.length, dateRange: { start: messages[0].createdAt, end: messages[messages.length - 1].createdAt } } }); // Delete original messages for (const message of messages) { await runtime.deleteMemory(message.id); } } ``` ## Best Practices ### Memory Design * **Type Selection**: Use appropriate memory types for different data * **Embedding Strategy**: Generate embeddings for searchable content * **Metadata Usage**: Store relevant metadata for filtering * **Relationship Tracking**: Maintain entity relationships * **Fact Extraction**: Extract and store facts from conversations ### Performance * **Indexing**: Create appropriate database indexes * **Batch Operations**: Use batch operations for multiple items * **Caching**: Cache frequently accessed memories * **Cleanup**: Implement regular cleanup routines * **Compression**: Compress old data to save space ### Data Integrity * **Validation**: Validate memory content before storage * **Deduplication**: Prevent duplicate facts and relationships * **Consistency**: Maintain referential integrity * **Versioning**: Track memory updates and changes * **Backup**: Regular backup of critical memories ## What's Next? Learn about the communication system Understand how providers use memory Explore AI model integration Build services that manage memory # Messaging Source: https://docs.elizaos.ai/runtime/messaging Real-time messaging infrastructure and Socket.IO integration ## Overview The messaging infrastructure provides real-time communication between clients and the ElizaOS server using Socket.IO. This enables instant message delivery, presence tracking, and bidirectional communication. ## Architecture ### How WebSockets Work in ElizaOS The project uses **Socket.IO** (not raw WebSockets) for real-time communication between clients and the Eliza server. ```mermaid flowchart TD subgraph "Extension" A[Extension Service Worker] B[Extension Socket.IO Client] end subgraph "Eliza NextJS App" C[SocketIOManager] D[Socket.IO Client] end subgraph "Eliza Server" E[Eliza Server
localhost:3000] F[Socket.IO Server] end %% Extension to Server flow A -->|"1. Send message"| B B -->|"2. emit('message')"| F F -->|"3. Process"| E E -->|"4. Response"| F %% Server broadcasts to clients F -->|"5. emit('messageBroadcast')"| D F -.->|"5. emit('messageBroadcast')
(Optional)"| B %% Internal app flow D -->|"6. Receive broadcast"| C C -->|"7. Filter by channelId"| C classDef extension fill:#2196f3,color:#fff classDef server fill:#4caf50,color:#fff classDef client fill:#9c27b0,color:#fff class A,B extension class E,F server class C,D client ``` ### Key Components 1. **Direct Connection**: Socket.IO connects directly to the Eliza server (default: `http://localhost:3000`) 2. **Channel-Based Communication**: Messages are organized by channels (or rooms for backward compatibility) 3. **Message Filtering**: Clients filter incoming broadcasts by channel/room ID ## Socket.IO Events and Message Types ### Message Types Enum ```javascript enum SOCKET_MESSAGE_TYPE { ROOM_JOINING = 1, // Join a channel/room SEND_MESSAGE = 2, // Send a message MESSAGE = 3, // Generic message ACK = 4, // Acknowledgment THINKING = 5, // Agent is thinking CONTROL = 6 // Control messages } ``` ### Key Events * `messageBroadcast` - Incoming messages from agents/users * `messageComplete` - Message processing complete * `controlMessage` - UI control (enable/disable input) * `connection_established` - Connection confirmed ## Socket.IO Client Implementation ### Minimal Socket.IO Client Here's a minimal Socket.IO client implementation: ```javascript const SOCKET_URL = 'http://localhost:3000'; // 1. Connect to Socket.IO const socket = io(SOCKET_URL, { 'force new connection': true, 'reconnection': true, 'reconnectionDelay': 1000, 'reconnectionAttempts': 5, 'timeout': 20000, 'transports': ['polling', 'websocket'] }); // Your IDs (make sure these match exactly) const entityId = 'your-entity-id'; const roomId = 'your-room-id'; // This should match the agent/channel ID // 2. CRITICAL: Join the room when connected socket.on('connect', function() { console.log('[SUCCESS] Connected to Eliza, socket ID:', socket.id); // JOIN THE ROOM - This is required to receive broadcasts! socket.emit('message', { type: 1, // ROOM_JOINING payload: { roomId: roomId, entityId: entityId } }); console.log('[SENT] Room join request for room:', roomId); }); // 3. LISTEN FOR THE CORRECT EVENT: "messageBroadcast" (not "message") socket.on('messageBroadcast', function(data) { console.log('[RECEIVED] Broadcast:', data); // Check if this message is for your room if (data.roomId === roomId || data.channelId === roomId) { console.log('[SUCCESS] Message is for our room!'); console.log('Sender:', data.senderName); console.log('Text:', data.text); console.log('Full data:', JSON.stringify(data, null, 2)); } else { console.log('[ERROR] Message is for different room:', data.roomId || data.channelId); } }); // 4. Listen for other important events socket.on('messageComplete', function(data) { console.log('[SUCCESS] Message processing complete:', data); }); socket.on('connection_established', function(data) { console.log('[SUCCESS] Connection established:', data); }); // 5. Send a message (make sure format is exact) function sendMessageToEliza(text) { const messagePayload = { type: 2, // SEND_MESSAGE payload: { senderId: entityId, senderName: 'Extension User', message: text, roomId: roomId, // Include roomId messageId: generateUUID(), source: 'extension', attachments: [], metadata: {} } }; console.log('[SENDING] Message:', messagePayload); socket.emit('message', messagePayload); } // Helper function for UUID function generateUUID() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { const r = Math.random() * 16 | 0; const v = c === 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); } // Connection error handling socket.on('connect_error', function(error) { console.error('[ERROR] Connection error:', error); }); socket.on('disconnect', function(reason) { console.log('[DISCONNECTED] Reason:', reason); }); ``` ## Modern Implementation (Socket.IO v4.x) For newer Socket.IO versions, here's a cleaner implementation: ```javascript import { io } from 'socket.io-client'; const SOCKET_URL = 'http://localhost:3000'; class ElizaSocketClient { constructor(entityId, roomId) { this.entityId = entityId; this.roomId = roomId; this.socket = null; } connect() { this.socket = io(SOCKET_URL, { transports: ['polling', 'websocket'], reconnection: true, reconnectionAttempts: 5, reconnectionDelay: 1000, }); this.socket.on('connect', () => { console.log('Connected to Eliza'); this.joinRoom(); }); this.socket.on('messageBroadcast', (data) => { if (data.roomId === this.roomId || data.channelId === this.roomId) { this.onMessageReceived(data); } }); // Debug: Log all events this.socket.onAny((eventName, ...args) => { console.log('Event:', eventName, args); }); } joinRoom() { this.socket.emit('message', { type: 1, // ROOM_JOINING payload: { roomId: this.roomId, entityId: this.entityId, } }); } sendMessage(text) { this.socket.emit('message', { type: 2, // SEND_MESSAGE payload: { senderId: this.entityId, senderName: 'Extension User', message: text, roomId: this.roomId, messageId: this.generateUUID(), source: 'extension', attachments: [], metadata: {} } }); } onMessageReceived(data) { console.log('Message received:', data); // Handle the message in your application } generateUUID() { return crypto.randomUUID ? crypto.randomUUID() : 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { const r = Math.random() * 16 | 0; const v = c === 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); } disconnect() { if (this.socket) { this.socket.disconnect(); } } } // Usage const client = new ElizaSocketClient('my-extension-id', 'agent-room-id'); client.connect(); // Send a message client.sendMessage('Hello from extension!'); ``` ## Key Points to Check ### 1. Event Name ```javascript // [WRONG] socket.on('message', handler) // [CORRECT] socket.on('messageBroadcast', handler) ``` ### 2. Room Joining Required ```javascript // You MUST join the room before receiving broadcasts socket.emit('message', { type: 1, // ROOM_JOINING payload: { roomId: roomId, entityId: entityId } }); ``` ### 3. Exact Message Format ```javascript // The structure must be exact { type: 2, // SEND_MESSAGE type payload: { senderId: entityId, senderName: 'Extension User', message: text, roomId: roomId, messageId: generateUUID(), source: 'extension', attachments: [], metadata: {} } } ``` ## Complete Message Flow 1. **Client connects** → Server accepts connection 2. **Client joins room** → Server adds client to room 3. **Client sends message** → Server receives and processes 4. **Server broadcasts response** → All clients in room receive 5. **Clients filter by room ID** → Only relevant messages shown ## Debugging Steps ### 1. Verify Events ```javascript // For newer Socket.IO versions socket.onAny((eventName, ...args) => { console.log('Event received:', eventName, args); }); // For older versions const onevent = socket.onevent; socket.onevent = function(packet) { console.log('Event:', packet.data); onevent.call(socket, packet); }; ``` ### 2. Check Room ID * Ensure the room ID matches exactly between your extension and the server * Even a single character difference will prevent message delivery ### 3. CORS Issues For browser extensions, ensure your manifest includes: ```json { "permissions": ["http://localhost:3000/*"], "host_permissions": ["http://localhost:3000/*"] } ``` ### 4. Transport Issues If WebSocket fails, force polling: ```javascript const socket = io(SOCKET_URL, { transports: ['polling'] // Avoid WebSocket issues }); ``` ## Socket.IO Version Compatibility ### Version Issues * **v1.3.0** (2015) - Very old, may have compatibility issues * **v4.x** (Current) - Recommended for new implementations ### Upgrading ```json // package.json { "dependencies": { "socket.io-client": "^4.5.0" } } ``` ## Common Mistakes 1. **Wrong event name** - Using `message` instead of `messageBroadcast` 2. **Not joining room** - Forgetting the `ROOM_JOINING` step 3. **ID mismatch** - Room/channel IDs don't match exactly 4. **Missing fields** - Payload missing required fields 5. **CORS blocked** - Extension lacks permissions ## Testing Your Implementation 1. Open browser console 2. Check for connection logs 3. Verify room join confirmation 4. Send test message 5. Check for broadcast reception ## Common Issue: Extension Not Receiving Responses ### The Problem Your extension can send messages to Eliza (server receives them), but doesn't receive responses back. ### Root Causes 1. **Not listening for the correct event** - Must listen for `messageBroadcast`, not `message` 2. **Not joining the room/channel** - Must emit a `ROOM_JOINING` message first 3. **Room/Channel ID mismatch** - IDs must match exactly 4. **Incorrect message payload structure** ### Solution Checklist 1. Verify you're listening to `messageBroadcast` event 2. Ensure you join the room on connection 3. Check room ID matches exactly 4. Verify message payload structure 5. Check browser console for errors 6. Test with debug logging enabled ## Advanced Features ### Presence Tracking ```javascript // Track user presence socket.on('userJoined', (data) => { console.log(`User ${data.userId} joined room ${data.roomId}`); }); socket.on('userLeft', (data) => { console.log(`User ${data.userId} left room ${data.roomId}`); }); ``` ### Typing Indicators ```javascript // Send typing indicator socket.emit('typing', { roomId: roomId, userId: userId, isTyping: true }); // Listen for typing indicators socket.on('userTyping', (data) => { if (data.roomId === roomId) { console.log(`${data.userId} is ${data.isTyping ? 'typing' : 'stopped typing'}`); } }); ``` ### Message Acknowledgments ```javascript // Send message with acknowledgment socket.emit('message', messagePayload, (ack) => { console.log('Message acknowledged:', ack); }); ``` ## Server-Side Implementation The Socket.IO server handles message routing and broadcasting: ```typescript class SocketIOService extends Service { static serviceType = 'socketio' as const; capabilityDescription = 'Real-time messaging via Socket.IO'; private io: Server; async start(runtime: IAgentRuntime) { this.io = new Server(server, { cors: { origin: '*', methods: ['GET', 'POST'] } }); this.io.on('connection', (socket) => { console.log('Client connected:', socket.id); socket.on('message', async (data) => { if (data.type === SOCKET_MESSAGE_TYPE.ROOM_JOINING) { await this.handleRoomJoin(socket, data.payload); } else if (data.type === SOCKET_MESSAGE_TYPE.SEND_MESSAGE) { await this.handleMessage(socket, data.payload); } }); socket.on('disconnect', () => { console.log('Client disconnected:', socket.id); }); }); } private async handleRoomJoin(socket: Socket, payload: any) { const { roomId, entityId } = payload; // Join the room socket.join(roomId); // Notify others in room socket.to(roomId).emit('userJoined', { userId: entityId, roomId: roomId }); console.log(`User ${entityId} joined room ${roomId}`); } private async handleMessage(socket: Socket, payload: any) { const { roomId, message, senderId } = payload; // Process message through runtime const response = await this.runtime.processMessage({ content: message, roomId: roomId, userId: senderId }); // Broadcast to all in room this.io.to(roomId).emit('messageBroadcast', { roomId: roomId, channelId: roomId, // For backward compatibility senderName: response.agentName, text: response.text, metadata: response.metadata }); } async stop() { this.io.close(); } } ``` ## Reference Implementation A complete working example demonstrating Socket.IO integration with Eliza, including real-time messaging, agent participation management, and comprehensive error handling. ## What's Next? Build persistent conversations on messaging Create messaging service integrations Handle real-time messaging events Supply message context to providers # Model Management Source: https://docs.elizaos.ai/runtime/models AI model selection, registration, and type-safe usage ## Model System The model management system provides a unified interface for AI model access with automatic provider selection, priority-based routing, and type-safe parameters. ## Model Types ### Core Model Types ```typescript enum ModelType { // Text generation models TEXT_SMALL = 'text:small', // Fast, simple responses TEXT_MEDIUM = 'text:medium', // Balanced performance TEXT_LARGE = 'text:large', // Complex reasoning // Embedding models TEXT_EMBEDDING = 'text:embedding', // Image models IMAGE_GENERATION = 'image:generation', IMAGE_ANALYSIS = 'image:analysis', // Audio models SPEECH_TO_TEXT = 'speech:to:text', TEXT_TO_SPEECH = 'text:to:speech', // Specialized models CODE_GENERATION = 'code:generation', CLASSIFICATION = 'classification' } ``` ### Model Parameters Type-safe parameters for each model type: ```typescript // Text generation parameters interface TextGenerationParams { prompt: string; messages?: Message[]; temperature?: number; // 0.0 - 2.0 maxTokens?: number; topP?: number; frequencyPenalty?: number; presencePenalty?: number; stopSequences?: string[]; systemPrompt?: string; } // Embedding parameters interface EmbeddingParams { input: string | string[]; model?: string; dimensions?: number; } // Image generation parameters interface ImageGenerationParams { prompt: string; negativePrompt?: string; width?: number; height?: number; steps?: number; seed?: number; style?: string; } // Speech-to-text parameters interface SpeechToTextParams { audio: Buffer | string; // Audio data or URL language?: string; format?: 'json' | 'text' | 'srt'; temperature?: number; } ``` ## Model Registration ### Registering Model Handlers ```typescript // Register a model handler runtime.registerModel( ModelType.TEXT_LARGE, async (runtime, params) => { // Model implementation const response = await callAPI(params); return response.text; }, 'openai', // provider name 100 // priority (higher = preferred) ); // Register multiple models from a plugin const modelPlugin: Plugin = { name: 'openai-models', models: [ { type: ModelType.TEXT_LARGE, handler: handleTextGeneration, provider: 'openai', priority: 100 }, { type: ModelType.TEXT_EMBEDDING, handler: handleEmbedding, provider: 'openai', priority: 100 } ] }; ``` ### Model Handler Interface ```typescript type ModelHandler = ( runtime: IAgentRuntime, params: T ) => Promise; interface ModelRegistration { type: ModelTypeName; handler: ModelHandler; provider: string; priority: number; } ``` ## Using Models ### Type-Safe Model Usage ```typescript // Text generation const response = await runtime.useModel( ModelType.TEXT_LARGE, { prompt: "Explain quantum computing", temperature: 0.7, maxTokens: 500 } ); // Get embeddings const embedding = await runtime.useModel( ModelType.TEXT_EMBEDDING, { input: "Text to embed" } ); // Generate image const image = await runtime.useModel( ModelType.IMAGE_GENERATION, { prompt: "A sunset over mountains", width: 1024, height: 1024, steps: 50 } ); // Speech to text const transcript = await runtime.useModel( ModelType.SPEECH_TO_TEXT, { audio: audioBuffer, language: 'en', format: 'json' } ); ``` ### Specifying Provider ```typescript // Use specific provider const response = await runtime.useModel( ModelType.TEXT_LARGE, { prompt: "Hello" }, 'anthropic' // Force specific provider ); // Get available providers const providers = runtime.getModelProviders(ModelType.TEXT_LARGE); console.log('Available providers:', providers); // ['openai', 'anthropic', 'ollama'] ``` ## Provider Priority ### Priority System The runtime selects providers based on priority: ```typescript // Higher priority providers are preferred runtime.registerModel(ModelType.TEXT_LARGE, handlerA, 'provider-a', 100); runtime.registerModel(ModelType.TEXT_LARGE, handlerB, 'provider-b', 90); runtime.registerModel(ModelType.TEXT_LARGE, handlerC, 'provider-c', 80); // Will use provider-a (priority 100) await runtime.useModel(ModelType.TEXT_LARGE, params); ``` ### Fallback Mechanism ```typescript // Automatic fallback on failure class ModelRouter { async useModel(type: ModelType, params: any, preferredProvider?: string) { const providers = this.getProvidersByPriority(type, preferredProvider); for (const provider of providers) { try { return await provider.handler(this.runtime, params); } catch (error) { this.logger.warn(`Provider ${provider.name} failed:`, error); // Try next provider if (provider !== providers[providers.length - 1]) { continue; } // All providers failed throw new Error(`No providers available for ${type}`); } } } } ``` ## Model Providers ### OpenAI Provider ```typescript class OpenAIModelProvider { private client: OpenAI; constructor(runtime: IAgentRuntime) { const apiKey = runtime.getSetting('OPENAI_API_KEY'); this.client = new OpenAI({ apiKey }); } async handleTextGeneration(params: TextGenerationParams) { const response = await this.client.chat.completions.create({ model: params.model || 'gpt-4', messages: params.messages || [ { role: 'user', content: params.prompt } ], temperature: params.temperature, max_tokens: params.maxTokens, top_p: params.topP, frequency_penalty: params.frequencyPenalty, presence_penalty: params.presencePenalty, stop: params.stopSequences }); return response.choices[0].message.content; } async handleEmbedding(params: EmbeddingParams) { const response = await this.client.embeddings.create({ model: 'text-embedding-3-small', input: params.input, dimensions: params.dimensions }); return Array.isArray(params.input) ? response.data.map(d => d.embedding) : response.data[0].embedding; } register(runtime: IAgentRuntime) { runtime.registerModel( ModelType.TEXT_LARGE, this.handleTextGeneration.bind(this), 'openai', 100 ); runtime.registerModel( ModelType.TEXT_EMBEDDING, this.handleEmbedding.bind(this), 'openai', 100 ); } } ``` ### Anthropic Provider ```typescript class AnthropicModelProvider { private client: Anthropic; constructor(runtime: IAgentRuntime) { const apiKey = runtime.getSetting('ANTHROPIC_API_KEY'); this.client = new Anthropic({ apiKey }); } async handleTextGeneration(params: TextGenerationParams) { const response = await this.client.messages.create({ model: params.model || 'claude-3-opus-20240229', messages: params.messages || [ { role: 'user', content: params.prompt } ], max_tokens: params.maxTokens || 1000, temperature: params.temperature, system: params.systemPrompt }); return response.content[0].text; } register(runtime: IAgentRuntime) { runtime.registerModel( ModelType.TEXT_LARGE, this.handleTextGeneration.bind(this), 'anthropic', 95 // Slightly lower priority than OpenAI ); } } ``` ### Local Model Provider (Ollama) ```typescript class OllamaModelProvider { private baseUrl: string; constructor(runtime: IAgentRuntime) { this.baseUrl = runtime.getSetting('OLLAMA_BASE_URL') || 'http://localhost:11434'; } async handleTextGeneration(params: TextGenerationParams) { const response = await fetch(`${this.baseUrl}/api/generate`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: params.model || 'llama2', prompt: params.prompt, temperature: params.temperature, options: { num_predict: params.maxTokens, top_p: params.topP, stop: params.stopSequences } }) }); const data = await response.json(); return data.response; } async handleEmbedding(params: EmbeddingParams) { const response = await fetch(`${this.baseUrl}/api/embeddings`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: params.model || 'all-minilm', prompt: params.input }) }); const data = await response.json(); return data.embedding; } register(runtime: IAgentRuntime) { // Lower priority for local models runtime.registerModel( ModelType.TEXT_LARGE, this.handleTextGeneration.bind(this), 'ollama', 50 ); runtime.registerModel( ModelType.TEXT_EMBEDDING, this.handleEmbedding.bind(this), 'ollama', 50 ); } } ``` ## Model Selection Strategy ### Automatic Selection ```typescript // Runtime automatically selects best available provider const response = await runtime.useModel( ModelType.TEXT_LARGE, { prompt: "Hello" } ); // Selection order: // 1. Check if preferred provider specified // 2. Sort available providers by priority // 3. Try each provider until success // 4. Cache successful provider for session ``` ### Context-Based Selection ```typescript // Select model based on context async function selectModelForTask(runtime: IAgentRuntime, task: string) { const complexity = analyzeComplexity(task); if (complexity < 0.3) { // Simple task - use small model return runtime.useModel(ModelType.TEXT_SMALL, { prompt: task, temperature: 0.3 }); } else if (complexity < 0.7) { // Medium complexity - use medium model return runtime.useModel(ModelType.TEXT_MEDIUM, { prompt: task, temperature: 0.5 }); } else { // Complex task - use large model return runtime.useModel(ModelType.TEXT_LARGE, { prompt: task, temperature: 0.7, maxTokens: 2000 }); } } ``` ### Cost Optimization ```typescript // Track and optimize model usage costs class CostOptimizedModelRouter { private costs = { 'openai': { [ModelType.TEXT_LARGE]: 0.03, [ModelType.TEXT_EMBEDDING]: 0.0001 }, 'anthropic': { [ModelType.TEXT_LARGE]: 0.025 }, 'ollama': { [ModelType.TEXT_LARGE]: 0, [ModelType.TEXT_EMBEDDING]: 0 } }; async useModel(type: ModelType, params: any, maxCost?: number) { const providers = this.getProvidersByCost(type, maxCost); for (const provider of providers) { try { const result = await provider.handler(this.runtime, params); // Track usage this.trackUsage(provider.name, type, params); return result; } catch (error) { continue; } } } private getProvidersByCost(type: ModelType, maxCost?: number) { return this.providers .filter(p => { const cost = this.costs[p.name]?.[type] || Infinity; return !maxCost || cost <= maxCost; }) .sort((a, b) => { const costA = this.costs[a.name]?.[type] || Infinity; const costB = this.costs[b.name]?.[type] || Infinity; return costA - costB; }); } } ``` ## Model Caching ### Response Caching ```typescript class ModelCache { private cache = new Map(); private ttl = 60 * 60 * 1000; // 1 hour getCacheKey(type: ModelType, params: any): string { return `${type}:${JSON.stringify(params)}`; } get(type: ModelType, params: any): any | null { const key = this.getCacheKey(type, params); const cached = this.cache.get(key); if (!cached) return null; if (Date.now() - cached.timestamp > this.ttl) { this.cache.delete(key); return null; } return cached.result; } set(type: ModelType, params: any, result: any) { const key = this.getCacheKey(type, params); this.cache.set(key, { result, timestamp: Date.now() }); } } // Use with runtime const cache = new ModelCache(); async function cachedModelCall(runtime: IAgentRuntime, type: ModelType, params: any) { // Check cache const cached = cache.get(type, params); if (cached) return cached; // Make call const result = await runtime.useModel(type, params); // Cache result cache.set(type, params, result); return result; } ``` ## Model Monitoring ### Usage Tracking ```typescript interface ModelUsageMetrics { provider: string; modelType: ModelType; count: number; totalTokens: number; totalDuration: number; avgDuration: number; errors: number; cost: number; } class ModelMonitor { private metrics = new Map(); async trackUsage( provider: string, type: ModelType, params: any, result: any, duration: number ) { const key = `${provider}:${type}`; if (!this.metrics.has(key)) { this.metrics.set(key, { provider, modelType: type, count: 0, totalTokens: 0, totalDuration: 0, avgDuration: 0, errors: 0, cost: 0 }); } const metrics = this.metrics.get(key); metrics.count++; metrics.totalDuration += duration; metrics.avgDuration = metrics.totalDuration / metrics.count; // Estimate tokens (simplified) if (type === ModelType.TEXT_LARGE) { const tokens = this.estimateTokens(params.prompt) + this.estimateTokens(result); metrics.totalTokens += tokens; metrics.cost += this.calculateCost(provider, type, tokens); } // Emit metrics event await this.runtime.emit(EventType.MODEL_USED, { runtime: this.runtime, modelType: type, provider, params, result, duration, metrics }); } } ``` ## Error Handling ### Retry Logic ```typescript async function modelCallWithRetry( runtime: IAgentRuntime, type: ModelType, params: any, maxRetries = 3 ) { let lastError: Error; for (let i = 0; i < maxRetries; i++) { try { return await runtime.useModel(type, params); } catch (error) { lastError = error; // Check if retryable if (isRateLimitError(error)) { // Wait with exponential backoff const delay = Math.pow(2, i) * 1000; await new Promise(resolve => setTimeout(resolve, delay)); continue; } // Non-retryable error throw error; } } throw lastError; } ``` ### Graceful Degradation ```typescript // Fallback to simpler models on failure async function modelCallWithFallback( runtime: IAgentRuntime, params: TextGenerationParams ) { const modelHierarchy = [ ModelType.TEXT_LARGE, ModelType.TEXT_MEDIUM, ModelType.TEXT_SMALL ]; for (const modelType of modelHierarchy) { try { return await runtime.useModel(modelType, params); } catch (error) { runtime.logger.warn(`Model ${modelType} failed, trying fallback`); if (modelType === ModelType.TEXT_SMALL) { // Last option failed throw error; } } } } ``` ## Best Practices ### Model Selection * **Right-Size Models**: Use appropriate model size for task complexity * **Cost Awareness**: Consider cost when selecting providers * **Latency Requirements**: Use local models for low-latency needs * **Fallback Strategy**: Implement fallbacks for reliability * **Caching**: Cache responses for repeated queries ### Performance * **Batch Processing**: Batch multiple requests when possible * **Streaming**: Use streaming for long responses * **Timeout Handling**: Set appropriate timeouts * **Connection Pooling**: Reuse HTTP connections * **Rate Limiting**: Respect provider rate limits ### Monitoring * **Track Usage**: Monitor token usage and costs * **Error Rates**: Track provider error rates * **Latency Metrics**: Monitor response times * **Quality Metrics**: Track response quality * **Cost Analysis**: Analyze cost per request ## What's Next? Build services that provide models Stream model responses in real-time Use models in conversations Supply context to models # Providers Source: https://docs.elizaos.ai/runtime/providers Provider system for context aggregation and state composition ## Provider Interface Providers supply contextual information that forms the agent's understanding of the current situation. They are the "senses" of the agent, gathering data from various sources to build comprehensive state. ### Core Interface ```typescript interface Provider { name: string; description: string; dynamic?: boolean; // Only executed when explicitly requested private?: boolean; // Internal-only, not included in default state position?: number; // Execution order (lower runs first) get: ( runtime: IAgentRuntime, message: Memory, state?: State ) => Promise; } interface ProviderResult { values: Record; // Key-value pairs for templates data: Record; // Structured data text: string; // Textual context } ``` ### Provider Types * **Standard Providers**: Included by default in state composition * **Dynamic Providers**: Only executed when explicitly requested * **Private Providers**: Internal use only, not exposed in default state ## Built-in Providers ### Provider Summary Table | Provider Name | Dynamic | Position | Default Included | Purpose | | -------------------- | ------- | -------- | ---------------- | -------------------------- | | **ACTIONS** | No | -1 | Yes | Lists available actions | | **ACTION\_STATE** | No | 150 | Yes | Action execution state | | **ANXIETY** | No | Default | Yes | Response style guidelines | | **ATTACHMENTS** | Yes | Default | No | File/media attachments | | **CAPABILITIES** | No | Default | Yes | Service capabilities | | **CHARACTER** | No | Default | Yes | Agent personality | | **CHOICE** | No | Default | Yes | Pending user choices | | **ENTITIES** | Yes | Default | No | Conversation participants | | **EVALUATORS** | No | Default | No (private) | Post-processing options | | **FACTS** | Yes | Default | No | Stored knowledge | | **PROVIDERS** | No | Default | Yes | Available providers list | | **RECENT\_MESSAGES** | No | 100 | Yes | Conversation history | | **RELATIONSHIPS** | Yes | Default | No | Social connections | | **ROLES** | No | Default | Yes | Server roles (groups only) | | **SETTINGS** | No | Default | Yes | Configuration state | | **TIME** | No | Default | Yes | Current UTC time | | **WORLD** | Yes | Default | No | Server/world context | ### Provider Details #### Actions Provider (`ACTIONS`) Lists all available actions the agent can execute. * **Position**: -1 (runs early) * **Dynamic**: No (included by default) * **Data Provided**: * `actionNames`: Comma-separated list of action names * `actionsWithDescriptions`: Formatted action details * `actionExamples`: Example usage for each action * `actionsData`: Raw action objects ```typescript { values: { actionNames: "Possible response actions: 'SEND_MESSAGE', 'SEARCH', 'CALCULATE'", actionExamples: "..." }, data: { actionsData: [...] }, text: "# Available Actions\n..." } ``` #### Action State Provider (`ACTION_STATE`) Shares execution state between chained actions. * **Position**: 150 (runs later) * **Dynamic**: No (included by default) * **Data Provided**: * `actionResults`: Previous action execution results * `actionPlan`: Multi-step action execution plan * `workingMemory`: Temporary data shared between actions * `recentActionMemories`: Historical action executions #### Character Provider (`CHARACTER`) Core personality and behavior definition. * **Dynamic**: No (included by default) * **Data Provided**: * `agentName`: Character name * `bio`: Character background * `topics`: Current interests * `adjective`: Current mood/state * `directions`: Style guidelines * `examples`: Example conversations/posts ```typescript { values: { agentName: "Alice", bio: "AI assistant focused on...", topics: "technology, science, education", adjective: "helpful" }, data: { character: {...} }, text: "# About Alice\n..." } ``` #### Recent Messages Provider (`RECENT_MESSAGES`) Provides conversation history and context. * **Position**: 100 (runs later to access other data) * **Dynamic**: No (included by default) * **Data Provided**: * `recentMessages`: Formatted conversation history * `recentInteractions`: Previous interactions * `actionResults`: Results from recent actions ```typescript { values: { recentMessages: "User: Hello\nAlice: Hi there!", recentInteractions: "..." }, data: { recentMessages: [...], actionResults: [...] }, text: "# Conversation Messages\n..." } ``` #### Facts Provider (`FACTS`) Retrieves contextually relevant stored facts. * **Dynamic**: Yes (must be explicitly included) * **Behavior**: Uses embedding search to find relevant facts * **Data Provided**: * Relevant facts based on context * Fact metadata and sources #### Relationships Provider (`RELATIONSHIPS`) Social graph and interaction history. * **Dynamic**: Yes (must be explicitly included) * **Data Provided**: * Known entities and their relationships * Interaction frequency * Relationship metadata ## State Composition The `composeState` method aggregates data from multiple providers to create comprehensive state. ### Method Signature ```typescript async composeState( message: Memory, includeList: string[] | null = null, onlyInclude = false, skipCache = false ): Promise ``` ### Parameters * **message**: The current message/memory object being processed * **includeList**: Array of provider names to include (optional) * **onlyInclude**: If true, ONLY include providers from includeList * **skipCache**: If true, bypass cache and fetch fresh data ### Composition Process 1. **Provider Selection**: Determines which providers to run based on filters 2. **Parallel Execution**: Runs all selected providers concurrently 3. **Result Aggregation**: Combines results from all providers 4. **Caching**: Stores the composed state for reuse ### Usage Patterns ```typescript // Default state (all non-dynamic, non-private providers) const state = await runtime.composeState(message); // Include specific dynamic providers const state = await runtime.composeState(message, ['FACTS', 'ENTITIES']); // Only specific providers const state = await runtime.composeState(message, ['CHARACTER'], true); // Force fresh data (skip cache) const state = await runtime.composeState(message, null, false, true); ``` ## Provider Registration ### Registering a Provider ```typescript runtime.registerProvider(provider); ``` Providers are registered during plugin initialization: ```typescript const myPlugin: Plugin = { name: 'my-plugin', providers: [customProvider], init: async (config, runtime) => { // Providers auto-registered } }; ``` ### Provider Position Position determines execution order: ```typescript const earlyProvider: Provider = { name: 'EARLY', position: -100, // Runs very early get: async () => {...} }; const lateProvider: Provider = { name: 'LATE', position: 200, // Runs late get: async () => {...} }; ``` ## Custom Providers ### Creating a Custom Provider ```typescript const customDataProvider: Provider = { name: 'CUSTOM_DATA', description: 'Custom data from external source', dynamic: true, position: 150, get: async (runtime, message, state) => { try { // Fetch data from service or database const customData = await runtime.getService('customService')?.getData(); if (!customData) { return { values: {}, data: {}, text: '' }; } return { values: { customData: customData.summary }, data: { customData }, text: `Custom data: ${customData.summary}`, }; } catch (error) { runtime.logger.error('Error in custom provider:', error); return { values: {}, data: {}, text: '' }; } }, }; ``` ### Provider Best Practices 1. **Return quickly**: Use timeouts for external calls 2. **Handle errors gracefully**: Return empty result on failure 3. **Keep data size reasonable**: Don't return excessive data 4. **Use appropriate flags**: Set `dynamic` for optional providers 5. **Consider position**: Order matters for dependent providers ### Provider Dependencies Providers can access data from previously executed providers through the state parameter: ```typescript const dependentProvider: Provider = { name: 'DEPENDENT', position: 200, // Runs after other providers get: async (runtime, message, state) => { // Access data from earlier providers const characterData = state?.data?.providers?.CHARACTER?.data; if (!characterData) { return { values: {}, data: {}, text: '' }; } // Process based on character data const processed = processCharacterData(characterData); return { values: { processed: processed.summary }, data: { processed }, text: `Processed: ${processed.summary}` }; } }; ``` ## State Cache Management ### Cache Architecture The runtime maintains an in-memory cache of composed states: ```typescript // Cache is stored by message ID this.stateCache.set(message.id, newState); ``` ### Cache Usage ```typescript // Use cached data (default behavior) const cachedState = await runtime.composeState(message); // Force fresh data const freshState = await runtime.composeState(message, null, false, true); ``` ### Cache Optimization ```typescript // Clear old cache entries periodically setInterval(() => { const fiveMinutesAgo = Date.now() - 5 * 60 * 1000; for (const [messageId, _] of runtime.stateCache.entries()) { runtime.getMemoryById(messageId).then((memory) => { if (memory && memory.createdAt < fiveMinutesAgo) { runtime.stateCache.delete(messageId); } }); } }, 60000); // Run every minute ``` ## Provider Execution Flow ```mermaid flowchart TD Start[composeState called] --> Select[Select Providers] Select --> Check{Check Cache} Check -->|Cache Hit| Return[Return Cached State] Check -->|Cache Miss| Sort[Sort by Position] Sort --> Execute[Execute in Parallel] Execute --> Aggregate[Aggregate Results] Aggregate --> Cache[Store in Cache] Cache --> Return classDef process fill:#2196f3,color:#fff classDef decision fill:#ff9800,color:#fff classDef execution fill:#4caf50,color:#fff classDef result fill:#9c27b0,color:#fff class Start,Select,Sort process class Check decision class Execute,Aggregate execution class Return,Cache result ``` ## Performance Optimization ### Parallel Execution Providers run concurrently for optimal performance: ```typescript const results = await Promise.all( providers.map(provider => provider.get(runtime, message, partialState) ) ); ``` ### Timeout Handling Implement timeouts to prevent slow providers from blocking: ```typescript const timeoutProvider: Provider = { name: 'TIMEOUT_SAFE', get: async (runtime, message) => { const fetchData = async () => { // Potentially slow operation const data = await externalAPI.fetch(); return formatProviderResult(data); }; return Promise.race([ fetchData(), new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), 5000) ) ]).catch(error => { runtime.logger.warn(`Provider timeout: ${error.message}`); return { values: {}, data: {}, text: '' }; }); } }; ``` ## Common Issues and Solutions ### Circular Dependencies Avoid providers that depend on each other circularly: ```typescript // BAD: Circular dependency const providerA: Provider = { get: async (runtime, message) => { const state = await runtime.composeState(message, ['B']); // Uses B's data } }; const providerB: Provider = { get: async (runtime, message) => { const state = await runtime.composeState(message, ['A']); // Uses A's data - CIRCULAR! } }; // GOOD: Use position and state parameter const providerA: Provider = { position: 100, get: async (runtime, message) => { // Generate data independently return { data: { aData: 'value' } }; } }; const providerB: Provider = { position: 200, get: async (runtime, message, state) => { // Access A's data from state const aData = state?.data?.providers?.A?.data; return { data: { bData: processData(aData) } }; } }; ``` ### Memory Leaks Prevent memory leaks with proper cache management: ```typescript class BoundedCache extends Map { private maxSize: number; constructor(maxSize: number = 1000) { super(); this.maxSize = maxSize; } set(key: string, value: any) { if (this.size >= this.maxSize) { const firstKey = this.keys().next().value; this.delete(firstKey); } return super.set(key, value); } } ``` ### Debugging State Composition ```typescript // Debug helper to trace provider execution async function debugComposeState(runtime: IAgentRuntime, message: Memory, includeList?: string[]) { console.log('=== State Composition Debug ==='); console.log('Message ID:', message.id); console.log('Include List:', includeList || 'default'); // Monkey patch provider execution const originalProviders = runtime.providers; runtime.providers = runtime.providers.map((provider) => ({ ...provider, get: async (...args) => { const start = Date.now(); console.log(`[${provider.name}] Starting...`); try { const result = await provider.get(...args); const duration = Date.now() - start; console.log(`[${provider.name}] Completed in ${duration}ms`); console.log(`[${provider.name}] Data size:`, JSON.stringify(result).length); return result; } catch (error) { console.error(`[${provider.name}] Error:`, error); throw error; } }, })); const state = await runtime.composeState(message, includeList); // Restore original providers runtime.providers = originalProviders; console.log('=== Final State Summary ==='); console.log('Total providers run:', Object.keys(state.data.providers || {}).length); console.log('State text length:', state.text.length); console.log('==============================='); return state; } ``` ## What's Next? Learn how models use provider context Build services that use providers Real-time provider updates See providers in conversational context # Services Source: https://docs.elizaos.ai/runtime/services Background services, integrations, and long-running processes ## Service System Services are long-running background tasks that extend agent functionality beyond request-response patterns. They manage connections, handle events, and perform ongoing operations. ## Service Interface ### Abstract Service Class ```typescript abstract class Service { static serviceType: ServiceType; constructor(runtime?: IAgentRuntime) {} abstract capabilityDescription: string; config?: ServiceConfig; static async start(runtime: IAgentRuntime): Promise { // Return new instance of service } abstract stop(): Promise; } ``` ### Service Properties * **serviceType**: Unique identifier for the service type * **capabilityDescription**: Human-readable description of service capabilities * **config**: Optional configuration object * **start**: Static method to initialize and start the service * **stop**: Instance method to gracefully shut down the service ## Service Types ### Core Service Types The core package defines base service types: ```typescript const ServiceType = { // Core services defined in @elizaos/core TASK: 'task', DATABASE: 'database', // ... other core types } as const; ``` ### Plugin Service Types Plugins extend service types through module augmentation: ```typescript // Plugin extends ServiceType through module augmentation declare module '@elizaos/core' { interface ServiceTypeRegistry { DISCORD: 'discord'; TELEGRAM: 'telegram'; TWITTER: 'twitter'; SEARCH: 'search'; IMAGE_GENERATION: 'image_generation'; TRANSCRIPTION: 'transcription'; // ... other plugin-specific types } } ``` ## Service Lifecycle ```mermaid flowchart TD Register[Register Service] --> Queue[Queue Start] Queue --> Init[Runtime Init] Init --> Start[Start Service] Start --> Running[Running] Running --> Stop[Stop Service] Stop --> Cleanup[Cleanup] classDef setup fill:#2196f3,color:#fff classDef active fill:#4caf50,color:#fff classDef shutdown fill:#ff9800,color:#fff class Register,Queue,Init setup class Start,Running active class Stop,Cleanup shutdown ``` ### Lifecycle Phases 1. **Registration**: Service registered with runtime during plugin initialization 2. **Queuing**: Service queued for startup 3. **Initialization**: Runtime prepares service environment 4. **Start**: Service `start()` method called 5. **Running**: Service actively processing 6. **Stop**: Graceful shutdown initiated 7. **Cleanup**: Resources released ## Common Service Patterns ### Platform Integration Service Services that connect to external platforms: ```typescript class DiscordService extends Service { static serviceType = 'discord' as const; capabilityDescription = 'Discord bot integration'; private client: Discord.Client; constructor(private runtime: IAgentRuntime) { super(runtime); } static async start(runtime: IAgentRuntime): Promise { const service = new DiscordService(runtime); await service.initialize(); return service; } private async initialize() { // Parse environment configuration const token = this.runtime.getSetting("DISCORD_API_TOKEN"); if (!token) { this.runtime.logger.warn("Discord token not found"); return; } // Initialize Discord client this.client = new Discord.Client({ intents: [/* Discord intents */], partials: [/* Discord partials */] }); // Set up event handlers this.setupEventHandlers(); // Connect to Discord await this.client.login(token); } private setupEventHandlers() { this.client.on('messageCreate', async (message) => { // Convert Discord message to Memory format const memory = await this.convertToMemory(message); // Process through runtime await this.runtime.processActions(memory, []); }); } async stop() { await this.client?.destroy(); } } ``` ### Background Task Service Services that perform periodic or scheduled tasks: ```typescript class TaskService extends Service { static serviceType = ServiceType.TASK; capabilityDescription = 'Scheduled task execution'; private interval: NodeJS.Timer; private readonly TICK_INTERVAL = 60000; // 1 minute static async start(runtime: IAgentRuntime): Promise { const service = new TaskService(runtime); await service.startTimer(); return service; } private async startTimer() { this.interval = setInterval(async () => { await this.checkTasks(); }, this.TICK_INTERVAL); } private async checkTasks() { try { // Check for scheduled tasks const tasks = await this.runtime.databaseAdapter.getTasks({ status: 'pending', scheduledFor: { $lte: new Date() } }); // Execute each task for (const task of tasks) { await this.executeTask(task); } } catch (error) { this.runtime.logger.error('Task check failed:', error); } } private async executeTask(task: Task) { try { // Mark task as running task.status = 'running'; await this.runtime.databaseAdapter.updateTask(task); // Execute task logic await this.processTask(task); // Mark task as complete task.status = 'completed'; await this.runtime.databaseAdapter.updateTask(task); } catch (error) { task.status = 'failed'; task.error = error.message; await this.runtime.databaseAdapter.updateTask(task); } } async stop() { if (this.interval) { clearInterval(this.interval); } } } ``` ### Data Service Services that provide data access or caching: ```typescript class SearchService extends Service { static serviceType = 'search' as const; capabilityDescription = 'Web search capabilities'; private searchClient: SearchClient; private cache: Map; static async start(runtime: IAgentRuntime): Promise { const service = new SearchService(runtime); await service.initialize(); return service; } private async initialize() { const apiKey = this.runtime.getSetting('SEARCH_API_KEY'); this.searchClient = new SearchClient({ apiKey, timeout: 5000 }); this.cache = new Map(); // Clear cache periodically setInterval(() => this.clearOldCache(), 3600000); // 1 hour } async search(query: string): Promise { // Check cache const cached = this.cache.get(query); if (cached && !this.isExpired(cached)) { return cached.results; } // Perform search const results = await this.searchClient.search(query); // Cache results this.cache.set(query, { results, timestamp: Date.now() }); return results; } private clearOldCache() { const oneHourAgo = Date.now() - 3600000; for (const [key, value] of this.cache.entries()) { if (value.timestamp < oneHourAgo) { this.cache.delete(key); } } } async stop() { this.cache.clear(); await this.searchClient?.close(); } } ``` ### Model Provider Service Services that provide AI model access: ```typescript class OpenAIService extends Service { static serviceType = 'openai' as const; capabilityDescription = 'OpenAI model provider'; private client: OpenAI; static async start(runtime: IAgentRuntime): Promise { const service = new OpenAIService(runtime); await service.initialize(); return service; } private async initialize() { const apiKey = this.runtime.getSetting('OPENAI_API_KEY'); if (!apiKey) { throw new Error('OpenAI API key not configured'); } this.client = new OpenAI({ apiKey }); // Register model handlers this.runtime.registerModel( ModelType.TEXT_LARGE, this.handleTextGeneration.bind(this), 'openai', 100 // priority ); this.runtime.registerModel( ModelType.TEXT_EMBEDDING, this.handleEmbedding.bind(this), 'openai', 100 ); } private async handleTextGeneration(params: GenerateTextParams) { const response = await this.client.chat.completions.create({ model: params.model || 'gpt-4', messages: params.messages, temperature: params.temperature, max_tokens: params.maxTokens }); return response.choices[0].message; } private async handleEmbedding(params: EmbedParams) { const response = await this.client.embeddings.create({ model: 'text-embedding-3-small', input: params.input }); return response.data[0].embedding; } async stop() { // Cleanup if needed } } ``` ## Service Registration ### Plugin Registration Services are registered during plugin initialization: ```typescript export const discordPlugin: Plugin = { name: 'discord', services: [DiscordService], init: async (config, runtime) => { // Services auto-registered and started } }; ``` ### Manual Registration Services can also be registered manually: ```typescript await runtime.registerService(CustomService); ``` ## Service Management ### Getting Services Access services through the runtime: ```typescript // Get service by type const discord = runtime.getService('discord'); // Type-safe service access const searchService = runtime.getService('search'); const results = await searchService.search('elizaOS'); ``` ### Service Communication Services can interact with each other: ```typescript class NotificationService extends Service { static serviceType = 'notification' as const; capabilityDescription = 'Cross-platform notifications'; async notify(message: string) { // Get Discord service const discord = this.runtime.getService('discord'); if (discord) { await discord.sendMessage(channelId, message); } // Get Telegram service const telegram = this.runtime.getService('telegram'); if (telegram) { await telegram.sendMessage(chatId, message); } // Get all services for broadcasting const services = this.runtime.getAllServices(); for (const service of services) { if (service.supportsNotifications) { await service.notify(message); } } } } ``` ## Error Handling ### Graceful Initialization Handle missing configuration gracefully: ```typescript constructor(runtime: IAgentRuntime) { super(runtime); const token = runtime.getSetting("SERVICE_TOKEN"); if (!token) { runtime.logger.warn("Service token not configured"); this.client = null; return; } // Initialize with token this.initializeClient(token); } ``` ### Error Recovery Implement retry logic and error recovery: ```typescript private async connectWithRetry(maxRetries = 3) { for (let i = 0; i < maxRetries; i++) { try { await this.client.connect(); this.runtime.logger.info('Service connected successfully'); return; } catch (error) { this.runtime.logger.error(`Connection attempt ${i + 1} failed:`, error); if (i < maxRetries - 1) { const delay = Math.pow(2, i) * 1000; // Exponential backoff await new Promise(resolve => setTimeout(resolve, delay)); } else { throw error; } } } } ``` ### Graceful Shutdown Ensure proper cleanup on service stop: ```typescript async stop() { try { // Stop accepting new work this.accepting = false; // Wait for ongoing work to complete await this.waitForCompletion(); // Close connections await this.client?.disconnect(); // Clear timers if (this.interval) { clearInterval(this.interval); } // Clear caches this.cache?.clear(); this.runtime.logger.info('Service stopped gracefully'); } catch (error) { this.runtime.logger.error('Error during service shutdown:', error); // Force cleanup this.forceCleanup(); } } ``` ## Best Practices ### Service Design * **Single Responsibility**: Each service should have one clear purpose * **Stateless When Possible**: Avoid maintaining state that could be lost * **Idempotent Operations**: Operations should be safe to retry * **Resource Management**: Clean up resources properly * **Error Isolation**: Errors shouldn't crash other services ### Configuration * **Environment Variables**: Use for sensitive configuration * **Graceful Defaults**: Provide sensible defaults * **Validation**: Validate configuration on startup * **Hot Reload**: Support configuration updates without restart ### Performance * **Async Operations**: Use async/await for non-blocking operations * **Connection Pooling**: Reuse connections when possible * **Caching**: Cache frequently accessed data * **Rate Limiting**: Respect external API limits * **Monitoring**: Log performance metrics ### Reliability * **Health Checks**: Implement health check endpoints * **Circuit Breakers**: Prevent cascade failures * **Retry Logic**: Handle transient failures * **Graceful Degradation**: Continue with reduced functionality * **Audit Logging**: Log important operations ## Common Services | Service | Purpose | Example Plugin | | ------------------- | ------------------------- | ------------------------------------ | | Platform Services | Connect to chat platforms | Discord, Telegram, Twitter | | Model Services | AI model providers | OpenAI, Anthropic, Ollama | | Data Services | External data sources | Web search, SQL, APIs | | Media Services | Process media | TTS, image generation, transcription | | Background Services | Scheduled tasks | Task runner, cron jobs | | Monitoring Services | System monitoring | Metrics, logging, alerting | ## Model Context Protocol (MCP) Services MCP (Model Context Protocol) allows your ElizaOS agent to use external tools and services. Think of it as giving your agent abilities like web search, file access, or API connections. ### MCP Plugin Setup ```bash bun add @elizaos/plugin-mcp ``` Add MCP to your character's plugins: ```typescript export const character: Character = { name: 'YourAgent', plugins: [ '@elizaos/plugin-sql', '@elizaos/plugin-bootstrap', '@elizaos/plugin-mcp', // Add MCP plugin // ... other plugins ], // ... rest of configuration }; ``` ### MCP Server Types MCP supports two types of servers: #### 1. STDIO Servers STDIO servers run as local processes and communicate through standard input/output. ```typescript export const character: Character = { name: 'WebSearchAgent', plugins: ['@elizaos/plugin-mcp'], settings: { mcp: { servers: { firecrawl: { type: 'stdio', command: 'npx', args: ['-y', 'firecrawl-mcp'], env: { // Optional: Add your Firecrawl API key if you have one FIRECRAWL_API_KEY: process.env.FIRECRAWL_API_KEY || '', }, }, }, }, }, system: 'You are a helpful assistant with web search capabilities.', }; ``` **Capabilities:** * Search the web for current information * Extract content from websites * Perform deep research on topics * Scrape structured data #### 2. SSE Servers SSE (Server-Sent Events) servers connect to remote APIs through HTTP. ```typescript export const character: Character = { name: 'APIAgent', plugins: ['@elizaos/plugin-mcp'], settings: { mcp: { servers: { myApiServer: { type: 'sse', url: 'https://your-api-server.com/sse', // Replace with your SSE server URL }, }, }, }, system: 'You are a helpful assistant with API access capabilities.', }; ``` **Capabilities:** * Real-time data access * API interactions * Custom tool execution * Dynamic resource fetching ### Complete MCP Configuration Example ```typescript import { type Character } from '@elizaos/core'; export const character: Character = { name: 'Eliza', plugins: [ '@elizaos/plugin-sql', ...(process.env.ANTHROPIC_API_KEY ? ['@elizaos/plugin-anthropic'] : []), ...(process.env.OPENAI_API_KEY ? ['@elizaos/plugin-openai'] : []), '@elizaos/plugin-bootstrap', '@elizaos/plugin-mcp', ], settings: { mcp: { servers: { // STDIO server example - runs locally firecrawl: { type: 'stdio', command: 'npx', args: ['-y', 'firecrawl-mcp'], env: {}, }, // SSE server example - connects to remote API customApi: { type: 'sse', url: 'https://your-api.com/sse', }, }, }, }, system: 'You are a helpful assistant with access to web search and API tools.', bio: [ 'Can search the web for information', 'Can connect to external APIs', 'Provides helpful responses', ], }; ``` ### Testing MCP Integration 1. Start your agent: ```bash bun run start ``` 2. Ask your agent to use the tools: * For web search: "Search for \[topic]" * For API tools: Use commands specific to your SSE server ### MCP Troubleshooting * **Server not connecting**: Check that the command/URL is correct * **Tools not available**: Ensure `@elizaos/plugin-mcp` is in your plugins array * **Permission errors**: For STDIO servers, ensure the command can be executed * **CORS issues**: For SSE servers, ensure proper CORS headers are configured ### MCP Service Implementation The MCP plugin internally creates services for each configured server: ```typescript class MCPService extends Service { static serviceType = 'mcp' as const; capabilityDescription = 'Model Context Protocol tool integration'; private servers: Map = new Map(); async start(runtime: IAgentRuntime) { const mcpConfig = runtime.getSetting('mcp'); for (const [name, config] of Object.entries(mcpConfig.servers)) { if (config.type === 'stdio') { await this.startSTDIOServer(name, config); } else if (config.type === 'sse') { await this.startSSEServer(name, config); } } } private async startSTDIOServer(name: string, config: STDIOConfig) { const server = spawn(config.command, config.args, { env: { ...process.env, ...config.env } }); this.servers.set(name, server); // Handle tool responses server.stdout.on('data', (data) => { this.handleToolResponse(name, data); }); } private async startSSEServer(name: string, config: SSEConfig) { const eventSource = new EventSource(config.url); eventSource.onmessage = (event) => { this.handleToolResponse(name, event.data); }; this.servers.set(name, eventSource); } async stop() { for (const [name, server] of this.servers) { if (server instanceof ChildProcess) { server.kill(); } else if (server instanceof EventSource) { server.close(); } } } } ``` ## Service Testing ### Unit Testing Test services in isolation: ```typescript describe('SearchService', () => { let service: SearchService; let runtime: MockRuntime; beforeEach(async () => { runtime = createMockRuntime(); service = await SearchService.start(runtime); }); afterEach(async () => { await service.stop(); }); it('should cache search results', async () => { const results1 = await service.search('test'); const results2 = await service.search('test'); expect(results1).toBe(results2); // Same object reference }); }); ``` ### Integration Testing Test service interactions: ```typescript it('should notify through multiple channels', async () => { const notificationService = runtime.getService('notification'); const discordSpy = jest.spyOn(discordService, 'sendMessage'); const telegramSpy = jest.spyOn(telegramService, 'sendMessage'); await notificationService.notify('Test message'); expect(discordSpy).toHaveBeenCalled(); expect(telegramSpy).toHaveBeenCalled(); }); ``` ## What's Next? Build real-time messaging services Manage stateful conversations Handle service lifecycle events Build model provider services # Sessions API Source: https://docs.elizaos.ai/runtime/sessions-api Complete API reference, architecture, implementation, and usage guide for the Sessions messaging system ## Overview The Sessions API provides a sophisticated abstraction layer over the traditional messaging infrastructure, enabling persistent, stateful conversations with automatic timeout management and renewal capabilities. ### Why Use Sessions? The Sessions API **eliminates the complexity of channel management**. Traditional messaging approaches require you to: 1. Create or find a server 2. Create or find a channel within that server 3. Add agents to the channel 4. Manage channel participants 5. Handle channel lifecycle (creation, deletion, cleanup) With Sessions API, you simply: 1. Create a session with an agent 2. Send messages 3. (Optional) Configure timeout and renewal policies ### Key Features * **Zero Channel Management**: No need to create servers, channels, or manage participants * **Instant Setup**: Start conversations immediately with just agent and user IDs * **Automatic Timeout Management**: Sessions automatically expire after periods of inactivity * **Session Renewal**: Support for both automatic and manual session renewal * **Expiration Warnings**: Get notified when sessions are about to expire * **Configurable Policies**: Customize timeout, renewal, and duration limits per session or agent * **Resource Optimization**: Automatic cleanup of expired sessions to prevent memory leaks * **Persistent Conversations**: Maintain chat history and context across multiple messages * **State Management**: Track conversation stage, renewal count, and expiration status * **Multi-Platform Support**: Works across different platforms with metadata support ## Sessions Architecture ### Core Design Principles #### Abstraction Over Complexity The Sessions API abstracts away channel and server management complexity: ```typescript // Traditional approach (complex) const server = await createServer({ name: 'My Server' }); const channel = await createChannel({ serverId: server.id }); await addAgentToServer(server.id, agentId); await addAgentToChannel(channel.id, agentId); await sendMessage(channel.id, { content: 'Hello' }); // Sessions approach (simple) const { sessionId } = await createSession({ agentId, userId }); await sendSessionMessage(sessionId, { content: 'Hello' }); ``` #### State Management Sessions maintain state across multiple dimensions: ```typescript interface Session { // Identity id: string; agentId: UUID; userId: UUID; channelId: UUID; // Temporal State createdAt: Date; lastActivity: Date; expiresAt: Date; // Configuration timeoutConfig: SessionTimeoutConfig; // Lifecycle State renewalCount: number; warningState?: { sent: boolean; sentAt: Date; }; // Application State metadata: Record; } ``` ## Session Lifecycle ```mermaid flowchart TD Create[Creation] --> Active[Active] Active --> Warning[Near Expiration] Warning --> Renewed[Renewed] Warning --> Expired[Expired] Renewed --> Active Active --> Deleted[Deleted] Expired --> Cleanup[Cleanup] classDef active fill:#4caf50,color:#fff classDef warning fill:#ff9800,color:#fff classDef ended fill:#f44336,color:#fff classDef transition fill:#2196f3,color:#fff class Create,Renewed transition class Active active class Warning warning class Expired,Deleted,Cleanup ended ``` ### Lifecycle Phases 1. **Creation**: Initialize session with configuration 2. **Active**: Process messages and maintain state 3. **Near Expiration**: Warning state before timeout 4. **Renewed**: Lifetime extended (auto or manual) 5. **Expired**: Session exceeded timeout 6. **Deleted**: Explicit termination 7. **Cleanup**: Resource cleanup ## Timeout Configuration ### Configuration Interface ```typescript interface SessionTimeoutConfig { timeoutMinutes?: number; // Inactivity timeout (5-1440) autoRenew?: boolean; // Auto-renew on activity maxDurationMinutes?: number; // Maximum total duration warningThresholdMinutes?: number; // Warning threshold } ``` ### Configuration Hierarchy Configuration follows a three-tier precedence: ```typescript // Priority Order (highest to lowest) // 1. Session-specific config // 2. Agent-specific config // 3. Global defaults const finalConfig = { ...globalDefaults, ...agentConfig, ...sessionConfig }; ``` ### Environment Variables Global default configuration: * `SESSION_DEFAULT_TIMEOUT_MINUTES` (default: 30) * `SESSION_MIN_TIMEOUT_MINUTES` (default: 5) * `SESSION_MAX_TIMEOUT_MINUTES` (default: 1440) * `SESSION_MAX_DURATION_MINUTES` (default: 720) * `SESSION_WARNING_THRESHOLD_MINUTES` (default: 5) ## Quick Start ### Complete Example: Chat Application ```javascript // Initialize a chat with timeout management async function startChat(agentId, userId) { const response = await fetch('/api/messaging/sessions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ agentId, userId, timeoutConfig: { timeoutMinutes: 30, // 30 minutes of inactivity autoRenew: true, // Auto-renew on each message maxDurationMinutes: 180, // 3 hour maximum session warningThresholdMinutes: 5 // Warn 5 minutes before expiry } }) }); const { sessionId, expiresAt, timeoutConfig } = await response.json(); console.log(`Session expires at: ${expiresAt}`); console.log(`Auto-renewal: ${timeoutConfig.autoRenew ? 'enabled' : 'disabled'}`); return sessionId; } // Send messages with session status tracking async function sendMessage(sessionId, message) { const response = await fetch( `/api/messaging/sessions/${sessionId}/messages`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ content: message }) } ); const data = await response.json(); // Check session status if (data.sessionStatus) { console.log(`Session renewed: ${data.sessionStatus.wasRenewed}`); console.log(`Expires at: ${data.sessionStatus.expiresAt}`); if (data.sessionStatus.isNearExpiration) { console.warn('Session is about to expire!'); } } return data; } // Keep session alive with heartbeat async function keepAlive(sessionId) { const response = await fetch( `/api/messaging/sessions/${sessionId}/heartbeat`, { method: 'POST' } ); const { expiresAt, timeRemaining } = await response.json(); console.log(`Session renewed, ${Math.floor(timeRemaining / 60000)} minutes remaining`); return response.json(); } ``` ## Session Creation ### Creation Process ```typescript async function createSession(request: CreateSessionRequest) { // Phase 1: Validation validateUUIDs(request.agentId, request.userId); validateMetadata(request.metadata); // Phase 2: Agent verification const agent = agents.get(request.agentId); if (!agent) throw new AgentNotFoundError(); // Phase 3: Configuration resolution const agentConfig = getAgentTimeoutConfig(agent); const finalConfig = mergeTimeoutConfigs( request.timeoutConfig, agentConfig ); // Phase 4: Infrastructure setup const sessionId = uuidv4(); const channelId = uuidv4(); // Atomic channel creation await serverInstance.createChannel({ id: channelId, name: `session-${sessionId}`, type: ChannelType.DM, metadata: { sessionId, agentId: request.agentId, userId: request.userId, timeoutConfig: finalConfig, ...request.metadata } }); // Phase 5: Session registration const session = new Session(sessionId, channelId, finalConfig); sessions.set(sessionId, session); return session; } ``` ## Session Operations ### Send Messages ```javascript const messageResponse = await fetch( `http://localhost:3000/api/messaging/sessions/${sessionId}/messages`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ content: 'Hello, I need help with my account', metadata: { userTimezone: 'America/New_York' } }) } ); const response = await messageResponse.json(); console.log(response.content); // Agent's response console.log(response.metadata.thought); // Agent's internal reasoning ``` ### Retrieve Message History ```javascript // Initial fetch const messagesResponse = await fetch( `http://localhost:3000/api/messaging/sessions/${sessionId}/messages?limit=20`, { method: 'GET', } ); const { messages, hasMore, cursors } = await messagesResponse.json(); // Pagination - get older messages if (hasMore && cursors?.before) { const olderMessages = await fetch( `/api/messaging/sessions/${sessionId}/messages?before=${cursors.before}&limit=20` ); } // Get newer messages if (cursors?.after) { const newerMessages = await fetch( `/api/messaging/sessions/${sessionId}/messages?after=${cursors.after}&limit=20` ); } ``` ### Manual Session Renewal ```javascript // Useful when auto-renew is disabled or to extend before expiration const renewResponse = await fetch( `http://localhost:3000/api/messaging/sessions/${sessionId}/renew`, { method: 'POST', } ); const { expiresAt, timeRemaining, renewalCount } = await renewResponse.json(); console.log(`Session renewed ${renewalCount} times`); console.log(`New expiration: ${expiresAt}`); console.log(`Time remaining: ${Math.floor(timeRemaining / 60000)} minutes`); ``` ### Update Timeout Configuration ```javascript // Dynamically update timeout settings for an active session const updateResponse = await fetch( `http://localhost:3000/api/messaging/sessions/${sessionId}/timeout`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ timeoutMinutes: 120, // Extend to 2 hours autoRenew: false, // Disable auto-renewal maxDurationMinutes: 480 // Increase max to 8 hours }) } ); const updatedSession = await updateResponse.json(); ``` ## Active Session Management ### Message Handling ```typescript async function handleMessage(sessionId: string, message: SendMessageRequest) { const session = sessions.get(sessionId); // Expiration check if (session.isExpired()) { sessions.delete(sessionId); throw new SessionExpiredError(); } // Activity tracking session.updateLastActivity(); // Renewal logic if (session.timeoutConfig.autoRenew) { const renewed = session.attemptRenewal(); if (renewed) { logger.info(`Session ${sessionId} auto-renewed`); } } // Warning detection if (session.isNearExpiration()) { session.markWarningState(); } // Message creation const dbMessage = await serverInstance.createMessage({ channelId: session.channelId, authorId: session.userId, content: message.content, metadata: { sessionId, ...message.metadata } }); // Response enrichment return { ...dbMessage, sessionStatus: session.getStatus() }; } ``` ## Renewal Mechanism ### Renewal Engine ```typescript class SessionRenewalEngine { attemptRenewal(session: Session): boolean { // Check if renewal is allowed if (!session.timeoutConfig.autoRenew) { return false; } // Check maximum duration constraint const totalDuration = Date.now() - session.createdAt.getTime(); const maxDurationMs = session.timeoutConfig.maxDurationMinutes * 60 * 1000; if (totalDuration >= maxDurationMs) { logger.warn(`Session ${session.id} reached max duration`); return false; } // Calculate new expiration const timeoutMs = session.timeoutConfig.timeoutMinutes * 60 * 1000; const remainingMaxDuration = maxDurationMs - totalDuration; const effectiveTimeout = Math.min(timeoutMs, remainingMaxDuration); // Update session session.lastActivity = new Date(); session.expiresAt = new Date(Date.now() + effectiveTimeout); session.renewalCount++; session.warningState = undefined; // Reset warning return true; } } ``` ### Heartbeat Strategy ```javascript class SessionManager { constructor(sessionId) { this.sessionId = sessionId; this.heartbeatInterval = null; this.warningShown = false; } startHeartbeat(intervalMs = 5 * 60 * 1000) { this.heartbeatInterval = setInterval(async () => { try { const response = await this.sendHeartbeat(); if (response.isNearExpiration && !this.warningShown) { this.onExpirationWarning(response.timeRemaining); this.warningShown = true; } if (response.timeRemaining > response.timeoutConfig.warningThresholdMinutes * 60000) { this.warningShown = false; // Reset warning flag } } catch (error) { this.stopHeartbeat(); this.onSessionLost(error); } }, intervalMs); } stopHeartbeat() { if (this.heartbeatInterval) { clearInterval(this.heartbeatInterval); this.heartbeatInterval = null; } } async sendHeartbeat() { const response = await fetch( `/api/messaging/sessions/${this.sessionId}/heartbeat`, { method: 'POST' } ); if (!response.ok) { throw new Error(`Heartbeat failed: ${response.status}`); } return response.json(); } } ``` ## Session Store ### In-Memory Storage ```typescript class SessionStore { private sessions = new Map(); private metrics = { totalCreated: 0, totalExpired: 0, totalDeleted: 0, peakConcurrent: 0 }; set(sessionId: string, session: Session) { this.sessions.set(sessionId, session); this.metrics.totalCreated++; this.updatePeakConcurrent(); } get(sessionId: string): Session | undefined { const session = this.sessions.get(sessionId); // Lazy expiration check if (session && session.isExpired()) { this.delete(sessionId); this.metrics.totalExpired++; return undefined; } return session; } delete(sessionId: string): boolean { const deleted = this.sessions.delete(sessionId); if (deleted) { this.metrics.totalDeleted++; } return deleted; } private updatePeakConcurrent() { const current = this.sessions.size; if (current > this.metrics.peakConcurrent) { this.metrics.peakConcurrent = current; } } } ``` ## Cleanup Service ### Automatic Cleanup ```typescript class SessionCleanupService { private cleanupInterval: NodeJS.Timeout; start(intervalMs: number = 5 * 60 * 1000) { this.cleanupInterval = setInterval(() => { this.performCleanup(); }, intervalMs); } performCleanup() { const now = Date.now(); const stats = { cleaned: 0, expired: 0, warned: 0, invalid: 0 }; for (const [sessionId, session] of sessions.entries()) { // Validate session structure if (!this.isValidSession(session)) { sessions.delete(sessionId); stats.invalid++; continue; } // Remove expired sessions if (session.expiresAt.getTime() <= now) { sessions.delete(sessionId); stats.expired++; stats.cleaned++; // Optional: Clean up associated resources this.cleanupChannelResources(session.channelId); } // Issue expiration warnings else if (this.shouldWarn(session)) { session.markWarningState(); stats.warned++; // Optional: Emit warning event this.emitExpirationWarning(session); } } if (stats.cleaned > 0 || stats.warned > 0) { logger.info('Cleanup cycle completed:', stats); } } stop() { if (this.cleanupInterval) { clearInterval(this.cleanupInterval); } } } ``` ## Integration Examples ### React Hook with Session Management ```javascript import { useState, useCallback, useEffect, useRef } from 'react'; function useElizaSession(agentId, userId) { const [sessionId, setSessionId] = useState(null); const [messages, setMessages] = useState([]); const [loading, setLoading] = useState(false); const [sessionStatus, setSessionStatus] = useState(null); const [expirationWarning, setExpirationWarning] = useState(false); const heartbeatInterval = useRef(null); const startSession = useCallback(async () => { const response = await fetch('/api/messaging/sessions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ agentId, userId, timeoutConfig: { timeoutMinutes: 30, autoRenew: true, maxDurationMinutes: 120, warningThresholdMinutes: 5 } }) }); const data = await response.json(); setSessionId(data.sessionId); setSessionStatus({ expiresAt: data.expiresAt, timeoutConfig: data.timeoutConfig }); // Start heartbeat startHeartbeat(data.sessionId); return data.sessionId; }, [agentId, userId]); const startHeartbeat = useCallback((sid) => { if (heartbeatInterval.current) { clearInterval(heartbeatInterval.current); } heartbeatInterval.current = setInterval(async () => { try { const response = await fetch( `/api/messaging/sessions/${sid}/heartbeat`, { method: 'POST' } ); const status = await response.json(); setSessionStatus(status); setExpirationWarning(status.isNearExpiration); } catch (error) { console.error('Heartbeat failed:', error); } }, 60000); // Every minute }, []); const sendMessage = useCallback(async (content) => { if (!sessionId) { const newSessionId = await startSession(); setSessionId(newSessionId); } setLoading(true); try { const response = await fetch( `/api/messaging/sessions/${sessionId}/messages`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ content }) } ); if (!response.ok) { if (response.status === 404 || response.status === 410) { // Session expired, create new one const newSessionId = await startSession(); setSessionId(newSessionId); // Retry with new session return sendMessage(content); } throw new Error(`Failed to send message: ${response.status}`); } const message = await response.json(); setMessages(prev => [...prev, message]); // Update session status if provided if (message.sessionStatus) { setSessionStatus(message.sessionStatus); setExpirationWarning(message.sessionStatus.isNearExpiration); } return message; } finally { setLoading(false); } }, [sessionId, startSession]); const renewSession = useCallback(async () => { if (!sessionId) return; const response = await fetch( `/api/messaging/sessions/${sessionId}/renew`, { method: 'POST' } ); const status = await response.json(); setSessionStatus(status); setExpirationWarning(false); return status; }, [sessionId]); useEffect(() => { // Cleanup heartbeat on unmount return () => { if (heartbeatInterval.current) { clearInterval(heartbeatInterval.current); } }; }, []); return { sessionId, messages, sendMessage, loading, sessionStatus, expirationWarning, renewSession }; } ``` ### WebSocket Integration ```javascript import { io } from 'socket.io-client'; class SessionWebSocketClient { constructor(serverUrl) { this.socket = io(serverUrl); this.sessionId = null; this.setupEventHandlers(); } setupEventHandlers() { // Session expiration warning via WebSocket this.socket.on('sessionExpirationWarning', (data) => { if (data.sessionId === this.sessionId) { console.warn(`Session expires in ${data.minutesRemaining} minutes`); this.onExpirationWarning?.(data); } }); // Session expired notification this.socket.on('sessionExpired', (data) => { if (data.sessionId === this.sessionId) { console.error('Session has expired'); this.onSessionExpired?.(data); this.sessionId = null; } }); // Session renewed notification this.socket.on('sessionRenewed', (data) => { if (data.sessionId === this.sessionId) { console.log('Session renewed until:', data.expiresAt); this.onSessionRenewed?.(data); } }); } joinSession(sessionId) { this.sessionId = sessionId; this.socket.emit('join', { roomId: sessionId, type: 'session' }); } leaveSession() { if (this.sessionId) { this.socket.emit('leave', { roomId: this.sessionId }); this.sessionId = null; } } } ``` ### Handling Session Expiration ```javascript class ResilientSessionClient { constructor(agentId, userId) { this.agentId = agentId; this.userId = userId; this.sessionId = null; this.sessionConfig = { timeoutMinutes: 30, autoRenew: true, maxDurationMinutes: 180 }; } async ensureSession() { if (!this.sessionId) { await this.createSession(); return; } // Check if session is still valid try { const response = await fetch(`/api/messaging/sessions/${this.sessionId}`); if (!response.ok) { if (response.status === 404 || response.status === 410) { // Session not found or expired await this.createSession(); } } } catch (error) { console.error('Session check failed:', error); await this.createSession(); } } async createSession() { const response = await fetch('/api/messaging/sessions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ agentId: this.agentId, userId: this.userId, timeoutConfig: this.sessionConfig }) }); const data = await response.json(); this.sessionId = data.sessionId; // Start heartbeat for new session this.startHeartbeat(); return this.sessionId; } async sendMessage(content) { await this.ensureSession(); const response = await fetch( `/api/messaging/sessions/${this.sessionId}/messages`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ content }) } ); if (!response.ok && (response.status === 404 || response.status === 410)) { // Session was lost, recreate and retry await this.createSession(); return this.sendMessage(content); } return response.json(); } } ``` ## Error Handling ### Custom Error Classes ```typescript abstract class SessionError extends Error { constructor( message: string, public code: string, public statusCode: number, public details?: any ) { super(message); this.name = this.constructor.name; } } class SessionNotFoundError extends SessionError { constructor(sessionId: string) { super( `Session not found: ${sessionId}`, 'SESSION_NOT_FOUND', 404, { sessionId } ); } } class SessionExpiredError extends SessionError { constructor(sessionId: string, expiresAt: Date) { super( `Session has expired`, 'SESSION_EXPIRED', 410, // Gone { sessionId, expiresAt } ); } } class SessionRenewalError extends SessionError { constructor(sessionId: string, reason: string, details?: any) { super( `Cannot renew session: ${reason}`, 'SESSION_RENEWAL_FAILED', 422, // Unprocessable Entity { sessionId, reason, ...details } ); } } ``` ### Error Handling Examples ```javascript try { const response = await fetch(`/api/messaging/sessions/${sessionId}/messages`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ content: message }) }); if (!response.ok) { const error = await response.json(); switch (response.status) { case 404: // Session not found console.error('Session not found:', error.details); // Create new session break; case 410: // Session expired console.error('Session expired at:', error.details.expiresAt); // Create new session or notify user break; case 400: // Validation error if (error.error.includes('content')) { console.error('Invalid message content'); } else if (error.error.includes('metadata')) { console.error('Metadata too large'); } break; case 422: // Session cannot be renewed console.error('Max duration reached:', error.details); // Must create new session break; default: console.error('Error:', error.message); } } } catch (error) { console.error('Network error:', error); } ``` ## Performance Optimization ### Pagination Strategy ```typescript async function getMessagesOptimized( sessionId: string, query: GetMessagesQuery ) { const session = sessions.get(sessionId); // Smart fetching strategy if (query.after && !query.before) { // Forward pagination - fetch extra for filtering const messages = await fetchMessages( session.channelId, query.limit * 2 ); return messages .filter(m => m.createdAt > query.after) .slice(0, query.limit); } if (query.before && !query.after) { // Backward pagination - direct fetch return await fetchMessages( session.channelId, query.limit, query.before ); } // Default - latest messages return await fetchMessages(session.channelId, query.limit); } ``` ### Configuration Caching ```typescript class AgentConfigCache { private cache = new Map(); private maxAge = 5 * 60 * 1000; // 5 minutes private timestamps = new Map(); get(agentId: UUID): SessionTimeoutConfig | undefined { const timestamp = this.timestamps.get(agentId); if (timestamp && Date.now() - timestamp > this.maxAge) { // Cache expired this.cache.delete(agentId); this.timestamps.delete(agentId); return undefined; } return this.cache.get(agentId); } set(agentId: UUID, config: SessionTimeoutConfig) { this.cache.set(agentId, config); this.timestamps.set(agentId, Date.now()); } } ``` ## Memory Management ### Memory Leak Prevention ```typescript class BoundedCache extends Map { private maxSize: number; constructor(maxSize: number = 1000) { super(); this.maxSize = maxSize; } set(key: string, value: any) { // Remove oldest entries if at capacity if (this.size >= this.maxSize) { const firstKey = this.keys().next().value; this.delete(firstKey); } return super.set(key, value); } } // Process lifecycle hooks process.once('SIGTERM', clearAllIntervals); process.once('SIGINT', clearAllIntervals); process.once('beforeExit', clearAllIntervals); ``` ### Cache with TTL ```typescript class CacheWithTTL extends Map { private ttl: number; private timestamps = new Map(); constructor(ttl: number = 5 * 60 * 1000) { super(); this.ttl = ttl; } set(key: string, value: any) { this.timestamps.set(key, Date.now()); return super.set(key, value); } get(key: string) { const timestamp = this.timestamps.get(key); if (timestamp && Date.now() - timestamp > this.ttl) { this.delete(key); return undefined; } return super.get(key); } } ``` ## Troubleshooting ### Common Issues 1. **Session Not Found (404)** * Session may have expired or been deleted * Create a new session and retry * Check session ID format 2. **Session Expired (410)** * Session exceeded its timeout period * Check the `expiresAt` timestamp in error details * Create a new session or adjust timeout configuration 3. **Cannot Renew Session (422)** * Session has reached maximum duration limit * Check `maxDurationMinutes` configuration * Must create a new session 4. **Invalid Timeout Configuration (400)** * Timeout values outside allowed range (5-1440 minutes) * Check configuration values against limits * Adjust to valid ranges 5. **Agent Not Available** * Ensure the agent is started and running * Check agent logs for errors * Verify agent ID is correct ### Debugging Enable debug logging: ```javascript const response = await fetch('/api/messaging/sessions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Debug': 'true' // Enable debug info in response }, body: JSON.stringify({ agentId, userId, // Debug mode may include additional session state info }) }); ``` Check session cleanup logs: ```bash # Server logs will show cleanup activity [Sessions API] Cleanup cycle completed: 3 expired sessions removed, 2 warnings issued [Sessions API] Session renewed via heartbeat: session-123 [Sessions API] Session abc-123 has reached maximum duration ``` ## When to Use Sessions vs Traditional Messaging ### Use Sessions When: * **Building chat interfaces**: Web apps, mobile apps, or any UI with a chat component * **Direct user-to-agent conversations**: One-on-one interactions between a user and an agent * **Simplified integration**: You want to get up and running quickly without infrastructure complexity * **Stateful conversations**: You need the agent to maintain context throughout the conversation * **Session management required**: You need timeout, renewal, and expiration handling * **Resource optimization**: You want automatic cleanup of inactive conversations * **Personal assistants**: Building AI assistants that remember user preferences and conversation history ### Use Traditional Messaging When: * **Multi-agent coordination**: Multiple agents need to communicate in the same channel * **Group conversations**: Multiple users and agents interacting together * **Platform integrations**: Integrating with Discord, Slack, or other platforms that have their own channel concepts * **Broadcast scenarios**: One agent sending messages to multiple channels/users * **Complex routing**: Custom message routing logic between different channels and servers * **Permanent history**: You need conversations to persist indefinitely without timeout ## Scalability Considerations ### Horizontal Scaling For production deployments: 1. **Session Store Distribution** * Use Redis for distributed session storage * Implement session affinity for WebSocket connections * Use consistent hashing for session distribution 2. **Message Queue Integration** * Decouple message processing from API responses * Use message queues for agent processing * Implement async response patterns 3. **Database Optimization** * Index session-related columns * Implement connection pooling * Consider read replicas for message retrieval ### Monitoring Metrics ```typescript interface SessionMetrics { // Volume metrics sessionsCreated: Counter; sessionsExpired: Counter; sessionsRenewed: Counter; // Performance metrics messageLatency: Histogram; renewalLatency: Histogram; // Health metrics activeSessions: Gauge; sessionsByAgent: Gauge; expirationWarnings: Counter; // Error metrics validationErrors: Counter; expirationErrors: Counter; renewalFailures: Counter; } ``` ## Security Considerations ### Input Validation ```typescript // UUID validation function validateUuid(value: string): boolean { const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; return uuidRegex.test(value); } // Content validation function validateContent(content: unknown): void { if (typeof content !== 'string') { throw new InvalidContentError('Content must be a string'); } if (content.length === 0) { throw new InvalidContentError('Content cannot be empty'); } if (content.length > MAX_CONTENT_LENGTH) { throw new InvalidContentError( `Content exceeds maximum length of ${MAX_CONTENT_LENGTH}` ); } } // Metadata validation function validateMetadata(metadata: unknown): void { if (!metadata) return; const size = JSON.stringify(metadata).length; if (size > MAX_METADATA_SIZE) { throw new InvalidMetadataError( `Metadata exceeds maximum size of ${MAX_METADATA_SIZE} bytes` ); } } ``` ### Rate Limiting ```typescript interface RateLimitConfig { windowMs: number; maxRequests: number; keyGenerator: (req: Request) => string; } const sessionRateLimits = { create: { windowMs: 60 * 1000, maxRequests: 10, keyGenerator: (req) => req.ip }, message: { windowMs: 60 * 1000, maxRequests: 100, keyGenerator: (req) => `${req.params.sessionId}:${req.ip}` } }; ``` ## Best Practices ### Session Design * **Appropriate Timeouts**: Choose timeouts based on use case * **Auto-Renewal**: Enable for active conversations * **Max Duration**: Set limits to prevent infinite sessions * **Warning Handling**: Notify users before expiration * **Cleanup Strategy**: Regular cleanup of expired sessions ### Implementation * **Error Recovery**: Graceful handling of session loss * **State Persistence**: Consider persistent storage for production * **Monitoring**: Track session metrics and health * **Testing**: Test timeout and renewal scenarios * **Documentation**: Document session behavior clearly ### Agent Configuration Configure timeout defaults for specific agents via environment variables: ```bash # Agent-specific settings (in agent's environment) SESSION_TIMEOUT_MINUTES=60 SESSION_AUTO_RENEW=true SESSION_MAX_DURATION_MINUTES=240 SESSION_WARNING_THRESHOLD_MINUTES=10 ``` Or configure programmatically in the agent: ```javascript // In agent configuration const agentConfig = { name: 'CustomerServiceBot', settings: { SESSION_TIMEOUT_MINUTES: '45', SESSION_AUTO_RENEW: 'true', SESSION_MAX_DURATION_MINUTES: '180', SESSION_WARNING_THRESHOLD_MINUTES: '5' } }; ``` ## Complete API Reference The Sessions API provides a comprehensive set of endpoints for managing stateful conversations with ElizaOS agents. This reference covers all available endpoints, request/response schemas, and error handling. ### Base URL ``` http://localhost:3000/api/messaging/sessions ``` ### Authentication Currently, the Sessions API does not require authentication. In production environments, you should implement appropriate authentication mechanisms. ### Endpoints #### Create Session Creates a new conversation session with an agent. Create a new session with configurable timeout policies **Request Body:** ```typescript interface CreateSessionRequest { agentId: string; // UUID of the agent userId: string; // UUID of the user metadata?: { // Optional session metadata [key: string]: any; }; timeoutConfig?: { // Optional timeout configuration timeoutMinutes?: number; // 5-1440 minutes autoRenew?: boolean; // Default: true maxDurationMinutes?: number; // Maximum session duration warningThresholdMinutes?: number; // When to warn about expiration }; } ``` **Response (201 Created):** ```typescript interface CreateSessionResponse { sessionId: string; agentId: string; userId: string; createdAt: Date; metadata: object; expiresAt: Date; timeoutConfig: SessionTimeoutConfig; } ``` **Example:** ```bash curl -X POST http://localhost:3000/api/messaging/sessions \ -H "Content-Type: application/json" \ -d '{ "agentId": "123e4567-e89b-12d3-a456-426614174000", "userId": "987f6543-e21b-12d3-a456-426614174000", "timeoutConfig": { "timeoutMinutes": 30, "autoRenew": true } }' ``` *** #### Get Session Information Retrieves detailed information about a session including its current status. Get session details and current status **Response (200 OK):** ```typescript interface SessionInfoResponse { sessionId: string; agentId: string; userId: string; createdAt: Date; lastActivity: Date; metadata: object; expiresAt: Date; timeoutConfig: SessionTimeoutConfig; renewalCount: number; timeRemaining: number; // Milliseconds until expiration isNearExpiration: boolean; // True if within warning threshold } ``` **Errors:** * `404 Not Found` - Session does not exist * `410 Gone` - Session has expired *** #### Send Message Sends a message within a session. Automatically renews the session if auto-renewal is enabled. Send a message in the conversation **Request Body:** ```typescript interface SendMessageRequest { content: string; // Message content (max 4000 chars) metadata?: { // Optional message metadata [key: string]: any; }; attachments?: any[]; // Optional attachments } ``` **Response (201 Created):** ```typescript interface MessageResponse { id: string; content: string; authorId: string; createdAt: Date; metadata: object; sessionStatus?: { // Session renewal information expiresAt: Date; renewalCount: number; wasRenewed: boolean; isNearExpiration: boolean; }; } ``` **Errors:** * `400 Bad Request` - Invalid content or metadata * `404 Not Found` - Session not found * `410 Gone` - Session expired *** #### Get Messages Retrieves messages from a session with pagination support. Retrieve conversation history **Query Parameters:** ```typescript interface GetMessagesQuery { limit?: string; // Number of messages (1-100, default: 50) before?: string; // Timestamp for pagination (older messages) after?: string; // Timestamp for pagination (newer messages) } ``` **Response (200 OK):** ```typescript interface GetMessagesResponse { messages: SimplifiedMessage[]; hasMore: boolean; cursors?: { before?: number; // Use for getting older messages after?: number; // Use for getting newer messages }; } interface SimplifiedMessage { id: string; content: string; authorId: string; isAgent: boolean; createdAt: Date; metadata: { thought?: string; // Agent's internal reasoning actions?: string[]; // Actions taken by agent [key: string]: any; }; } ``` *** #### Renew Session Manually renews a session to extend its expiration time. Manually extend session lifetime **Response (200 OK):** ```typescript interface SessionInfoResponse { sessionId: string; agentId: string; userId: string; createdAt: Date; lastActivity: Date; expiresAt: Date; timeoutConfig: SessionTimeoutConfig; renewalCount: number; timeRemaining: number; isNearExpiration: boolean; } ``` **Errors:** * `404 Not Found` - Session not found * `410 Gone` - Session expired * `422 Unprocessable Entity` - Cannot renew (max duration reached) *** #### Update Timeout Configuration Updates the timeout configuration for an active session. Modify session timeout settings **Request Body:** ```typescript interface SessionTimeoutConfig { timeoutMinutes?: number; // 5-1440 minutes autoRenew?: boolean; maxDurationMinutes?: number; warningThresholdMinutes?: number; } ``` **Response (200 OK):** Returns updated `SessionInfoResponse` **Errors:** * `400 Bad Request` - Invalid timeout configuration * `404 Not Found` - Session not found * `410 Gone` - Session expired *** #### Send Heartbeat Keeps a session alive and optionally renews it if auto-renewal is enabled. Keep session alive with periodic heartbeat **Response (200 OK):** Returns `SessionInfoResponse` with updated expiration information. **Errors:** * `404 Not Found` - Session not found * `410 Gone` - Session expired *** #### Delete Session Explicitly ends and removes a session. End and delete a session **Response (200 OK):** ```typescript { success: true, message: "Session {sessionId} deleted successfully" } ``` **Errors:** * `404 Not Found` - Session not found *** #### List Sessions (Admin) Lists all active sessions in the system. This is an administrative endpoint. List all active sessions **Response (200 OK):** ```typescript interface ListSessionsResponse { sessions: SessionInfoResponse[]; total: number; stats: { totalSessions: number; activeSessions: number; expiredSessions: number; }; } ``` *** #### Health Check Checks the health status of the Sessions API service. Check service health **Response (200 OK):** ```typescript interface HealthCheckResponse { status: 'healthy' | 'degraded' | 'unhealthy'; activeSessions: number; timestamp: string; expiringSoon?: number; // Sessions near expiration invalidSessions?: number; // Corrupted sessions detected uptime?: number; // Service uptime in seconds } ``` ### Error Responses All error responses follow a consistent format: ```typescript interface ErrorResponse { error: string; // Error message details?: { // Additional context [key: string]: any; }; } ``` #### Common Error Codes | Status Code | Error Type | Description | | ----------- | --------------------- | ---------------------------------------------------------- | | `400` | Bad Request | Invalid input parameters or request body | | `404` | Not Found | Session or resource not found | | `410` | Gone | Session has expired | | `422` | Unprocessable Entity | Operation cannot be completed (e.g., max duration reached) | | `500` | Internal Server Error | Unexpected server error | #### Error Classes The API uses specific error classes for different scenarios: * `SessionNotFoundError` - Session does not exist * `SessionExpiredError` - Session has exceeded its timeout * `SessionCreationError` - Failed to create session * `AgentNotFoundError` - Specified agent not found * `InvalidUuidError` - Invalid UUID format * `MissingFieldsError` - Required fields missing * `InvalidContentError` - Message content validation failed * `InvalidMetadataError` - Metadata exceeds size limit * `InvalidPaginationError` - Invalid pagination parameters * `InvalidTimeoutConfigError` - Invalid timeout configuration * `SessionRenewalError` - Cannot renew session * `MessageSendError` - Failed to send message ### Rate Limiting Currently, the Sessions API does not implement rate limiting. In production, you should implement appropriate rate limiting based on your requirements. ### WebSocket Events When using WebSocket connections alongside the Sessions API, the following events are available: ```typescript // Join a session for real-time updates socket.emit('join', { roomId: sessionId }); // Listen for new messages socket.on('messageBroadcast', (message) => { // Handle new message }); // Session expiration warning socket.on('sessionExpirationWarning', (data) => { // data.sessionId, data.minutesRemaining }); // Session expired socket.on('sessionExpired', (data) => { // data.sessionId, data.expiredAt }); // Session renewed socket.on('sessionRenewed', (data) => { // data.sessionId, data.expiresAt, data.renewalCount }); ``` ### Environment Variables Configure the Sessions API behavior using these environment variables: | Variable | Default | Description | | ----------------------------------- | ------- | ----------------------------------------- | | `SESSION_DEFAULT_TIMEOUT_MINUTES` | 30 | Default session timeout | | `SESSION_MIN_TIMEOUT_MINUTES` | 5 | Minimum allowed timeout | | `SESSION_MAX_TIMEOUT_MINUTES` | 1440 | Maximum allowed timeout (24 hours) | | `SESSION_MAX_DURATION_MINUTES` | 720 | Maximum total session duration (12 hours) | | `SESSION_WARNING_THRESHOLD_MINUTES` | 5 | When to trigger expiration warning | | `SESSION_CLEANUP_INTERVAL_MINUTES` | 5 | How often to clean expired sessions | | `CLEAR_SESSIONS_ON_SHUTDOWN` | false | Clear all sessions on server shutdown | ### SDK Examples #### JavaScript/TypeScript Client ```typescript class SessionsAPIClient { constructor(private baseUrl: string = 'http://localhost:3000') {} async createSession(agentId: string, userId: string, config?: SessionTimeoutConfig) { const response = await fetch(`${this.baseUrl}/api/messaging/sessions`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ agentId, userId, timeoutConfig: config }) }); if (!response.ok) throw new Error(`Failed to create session: ${response.status}`); return response.json(); } async sendMessage(sessionId: string, content: string, metadata?: object) { const response = await fetch( `${this.baseUrl}/api/messaging/sessions/${sessionId}/messages`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ content, metadata }) } ); if (!response.ok) throw new Error(`Failed to send message: ${response.status}`); return response.json(); } async getMessages(sessionId: string, limit = 50, before?: number) { const params = new URLSearchParams({ limit: limit.toString() }); if (before) params.append('before', before.toString()); const response = await fetch( `${this.baseUrl}/api/messaging/sessions/${sessionId}/messages?${params}` ); if (!response.ok) throw new Error(`Failed to get messages: ${response.status}`); return response.json(); } async renewSession(sessionId: string) { const response = await fetch( `${this.baseUrl}/api/messaging/sessions/${sessionId}/renew`, { method: 'POST' } ); if (!response.ok) throw new Error(`Failed to renew session: ${response.status}`); return response.json(); } async sendHeartbeat(sessionId: string) { const response = await fetch( `${this.baseUrl}/api/messaging/sessions/${sessionId}/heartbeat`, { method: 'POST' } ); if (!response.ok) throw new Error(`Heartbeat failed: ${response.status}`); return response.json(); } } ``` #### Python Client ```python import requests from typing import Optional, Dict, Any class SessionsAPIClient: def __init__(self, base_url: str = "http://localhost:3000"): self.base_url = base_url self.session = requests.Session() def create_session( self, agent_id: str, user_id: str, timeout_config: Optional[Dict[str, Any]] = None ) -> Dict[str, Any]: """Create a new session with an agent.""" payload = { "agentId": agent_id, "userId": user_id } if timeout_config: payload["timeoutConfig"] = timeout_config response = self.session.post( f"{self.base_url}/api/messaging/sessions", json=payload ) response.raise_for_status() return response.json() def send_message( self, session_id: str, content: str, metadata: Optional[Dict[str, Any]] = None ) -> Dict[str, Any]: """Send a message in a session.""" payload = {"content": content} if metadata: payload["metadata"] = metadata response = self.session.post( f"{self.base_url}/api/messaging/sessions/{session_id}/messages", json=payload ) response.raise_for_status() return response.json() def get_messages( self, session_id: str, limit: int = 50, before: Optional[int] = None, after: Optional[int] = None ) -> Dict[str, Any]: """Retrieve messages from a session.""" params = {"limit": str(limit)} if before: params["before"] = str(before) if after: params["after"] = str(after) response = self.session.get( f"{self.base_url}/api/messaging/sessions/{session_id}/messages", params=params ) response.raise_for_status() return response.json() def renew_session(self, session_id: str) -> Dict[str, Any]: """Manually renew a session.""" response = self.session.post( f"{self.base_url}/api/messaging/sessions/{session_id}/renew" ) response.raise_for_status() return response.json() def send_heartbeat(self, session_id: str) -> Dict[str, Any]: """Send a heartbeat to keep session alive.""" response = self.session.post( f"{self.base_url}/api/messaging/sessions/{session_id}/heartbeat" ) response.raise_for_status() return response.json() ``` ## Next Steps Add real-time capabilities to your sessions Understand ElizaOS runtime architecture Build custom plugins for your agents Build plugins that extend sessions # What You Can Build Source: https://docs.elizaos.ai/what-you-can-build Here are some popular agent types to get you started. Build anything imaginable ## Web3 & DeFi Traderplugin-hyperliquid+4}> Executes discrete trading strategies with leverage to grow trading accounts autonomously Sniperplugin-solana+4}> Monitors new token launches, analyzes contracts, and executes instant snipe trades KOLplugin-twitter+4}> Grows following with alpha content and accepts onchain payments for promotional posts MEV Hunterplugin-evm+4}> Front-runs transactions, executes sandwich attacks, captures liquidations across chains Yield Farmerplugin-evm+4}> Auto-compounds positions, rebalances pools, migrates to highest APY opportunities Token Launcherplugin-clanker+4}> Deploys meme tokens, manages liquidity, creates viral campaigns, builds communities ## Content Creation Influencerplugin-twitter+4}> Creates targeted generative content (images/video/music) and posts across platforms Clip Farmerplugin-video-understanding+4}> Extracts viral moments from long-form content, adds captions, reposts for engagement Music Producerplugin-elevenlabs+4}> Generates AI music tracks, creates cover art, and distributes across social platforms Celebrity Cloneplugin-knowledge+4}> Trains on famous person's corpus (books/videos/interviews) to replicate their personality ## Social & Interactive Meme Lordplugin-twitter+4}> Creates viral memes, times posts perfectly, rides trending topics for maximum reach NPCplugin-hyperfy+4}> Populates virtual worlds with interactive characters that have memories and personalities Dating Coachplugin-anthropic+4}> Analyzes dating profiles, writes engaging messages, gives relationship advice, manages dating app conversations Reply Guyplugin-twitter+4}> Strategically engages in high-visibility threads to grow following and drive traffic ## Business Automation Product Managerplugin-github+4}> Takes updates from dev teams, manages tickets, updates documentation, tracks sprints Deal Closerplugin-whatsapp+4}> Qualifies leads, handles objections, books demos, follows up automatically Moderatorplugin-discord+4}> Manages community channels, enforces rules, answers FAQs, runs engagement activities Lead Generatorplugin-browser+4}> Scrapes B2B data, enriches prospect information, prioritizes leads by fit and intent