# Create a new agent Source: https://eliza.how/api-reference/agents/create-a-new-agent post /api/agents Creates a new agent from character configuration # Create a world for an agent Source: https://eliza.how/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://eliza.how/api-reference/agents/delete-an-agent delete /api/agents/{agentId} Permanently deletes an agent # Get agent details Source: https://eliza.how/api-reference/agents/get-agent-details get /api/agents/{agentId} Returns detailed information about a specific agent # Get agent panels Source: https://eliza.how/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://eliza.how/api-reference/agents/get-all-worlds get /api/agents/worlds Get all worlds across all agents # List all agents Source: https://eliza.how/api-reference/agents/list-all-agents get /api/agents Returns a list of all available agents # Start an agent Source: https://eliza.how/api-reference/agents/start-an-agent post /api/agents/{agentId}/start Starts an existing agent # Stop an agent Source: https://eliza.how/api-reference/agents/stop-an-agent post /api/agents/{agentId}/stop Stops a running agent # Update a world Source: https://eliza.how/api-reference/agents/update-a-world patch /api/agents/{agentId}/worlds/{worldId} Update world properties # Update agent Source: https://eliza.how/api-reference/agents/update-agent patch /api/agents/{agentId} Update an existing agent # Convert conversation to speech Source: https://eliza.how/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://eliza.how/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://eliza.how/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://eliza.how/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://eliza.how/api-reference/audio/transcribe-audio post /api/audio/{agentId}/transcriptions Transcribe audio file to text # Clear system logs Source: https://eliza.how/api-reference/logs/clear-system-logs delete /api/server/logs Clear all system logs # Delete a specific log entry Source: https://eliza.how/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://eliza.how/api-reference/logs/get-agent-logs get /api/agents/{agentId}/logs Retrieve logs for a specific agent # Get system logs Source: https://eliza.how/api-reference/logs/get-system-logs get /api/server/logs Retrieve system logs with optional filtering # Get system logs (POST) Source: https://eliza.how/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://eliza.how/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://eliza.how/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://eliza.how/api-reference/memory/create-a-room post /api/memory/{agentId}/rooms Create a new room for an agent # Delete all agent memories Source: https://eliza.how/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://eliza.how/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://eliza.how/api-reference/memory/get-agent-memories get /api/memory/{agentId}/memories Retrieve all memories for a specific agent # Get room memories Source: https://eliza.how/api-reference/memory/get-room-memories get /api/agents/{agentId}/rooms/{roomId}/memories Retrieves memories for a specific room # Update a memory Source: https://eliza.how/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://eliza.how/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://eliza.how/api-reference/messaging/add-agent-to-server post /api/messaging/servers/{serverId}/agents Add an agent to a server # Create central channel Source: https://eliza.how/api-reference/messaging/create-central-channel post /api/messaging/central-channels Create a channel in the central database # Create channel Source: https://eliza.how/api-reference/messaging/create-channel post /api/messaging/channels Create a new channel # Create group channel Source: https://eliza.how/api-reference/messaging/create-group-channel post /api/messaging/group-channels Create a group channel with multiple participants # Create server Source: https://eliza.how/api-reference/messaging/create-server post /api/messaging/servers Create a new server # Delete all channel messages Source: https://eliza.how/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://eliza.how/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://eliza.how/api-reference/messaging/delete-channel delete /api/messaging/central-channels/{channelId} Delete a channel # Delete channel message Source: https://eliza.how/api-reference/messaging/delete-channel-message delete /api/messaging/central-channels/{channelId}/messages/{messageId} Delete a specific message from a channel # Get central server channels Source: https://eliza.how/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://eliza.how/api-reference/messaging/get-central-servers get /api/messaging/central-servers Get all servers from central database # Get channel details Source: https://eliza.how/api-reference/messaging/get-channel-details get /api/messaging/central-channels/{channelId}/details Get details for a specific channel # Get channel info Source: https://eliza.how/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://eliza.how/api-reference/messaging/get-channel-messages get /api/messaging/central-channels/{channelId}/messages Get messages for a channel # Get channel participants Source: https://eliza.how/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://eliza.how/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://eliza.how/api-reference/messaging/get-server-agents get /api/messaging/servers/{serverId}/agents Get all agents for a server # Get server channels Source: https://eliza.how/api-reference/messaging/get-server-channels get /api/messaging/servers/{serverId}/channels Get all channels for a server # Ingest messages from external platforms Source: https://eliza.how/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. # Mark message processing as complete Source: https://eliza.how/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://eliza.how/api-reference/messaging/process-external-message post /api/messaging/external-messages Process a message from an external platform # Remove agent from server Source: https://eliza.how/api-reference/messaging/remove-agent-from-server delete /api/messaging/servers/{serverId}/agents/{agentId} Remove an agent from a server # Send message to channel Source: https://eliza.how/api-reference/messaging/send-message-to-channel post /api/messaging/central-channels/{channelId}/messages Send a message to a channel # Submit a message to the central messaging system Source: https://eliza.how/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://eliza.how/api-reference/messaging/update-channel patch /api/messaging/central-channels/{channelId} Update channel details # Create a room Source: https://eliza.how/api-reference/rooms/create-a-room post /api/agents/{agentId}/rooms Creates a new room for an agent # Delete a room Source: https://eliza.how/api-reference/rooms/delete-a-room delete /api/agents/{agentId}/rooms/{roomId} Deletes a specific room # Get agent rooms Source: https://eliza.how/api-reference/rooms/get-agent-rooms get /api/agents/{agentId}/rooms Retrieves all rooms for a specific agent # Get room details Source: https://eliza.how/api-reference/rooms/get-room-details get /api/agents/{agentId}/rooms/{roomId} Retrieves details about a specific room # Update a room Source: https://eliza.how/api-reference/rooms/update-a-room patch /api/agents/{agentId}/rooms/{roomId} Updates a specific room # Basic health check Source: https://eliza.how/api-reference/system/basic-health-check get /api/server/hello Simple hello world test endpoint # Get local environment variables Source: https://eliza.how/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://eliza.how/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://eliza.how/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://eliza.how/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://eliza.how/api-reference/system/health-check-endpoint get /api/server/health Detailed health check for the system # Ping health check Source: https://eliza.how/api-reference/system/ping-health-check get /api/server/ping Simple ping endpoint to check if server is responsive # Stop the server Source: https://eliza.how/api-reference/system/stop-the-server post /api/server/stop Initiates server shutdown # Update local environment variables Source: https://eliza.how/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://eliza.how/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://eliza.how/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://eliza.how/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: * `sqlite` (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**: `sqlite` ### Interactive Creation (Recommended) ```bash # Start interactive wizard elizaos create ``` This will prompt you for: * Project name * Database selection (sqlite or postgres) ### Quick Creation with Defaults ```bash # Create project with defaults (name: "myproject", database: sqlite) 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://eliza.how/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 Eliza 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://eliza.how/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 | | `SQLITE_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://eliza.how/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://eliza.how/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 ``` ## 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://eliza.how/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://eliza.how/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 * [Plugin Publishing Guide](/guides/plugin-publishing-guide): Complete walkthrough # Start Command Source: https://eliza.how/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://eliza.how/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://eliza.how/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) | | `--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 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 elizaos test component --name specific-test # Run tests matching a pattern elizaos test --name "auth|user" ``` ### 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://eliza.how/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 # Agents Source: https://eliza.how/core-concepts/agents AI personalities in elizaOS ## What are Agents? Agents = AI personalities with memory, actions, and unique behaviors. ## Character Interface The complete TypeScript interface for agents: | Property | Type | Required | Description | | ----------------- | ------------------- | -------- | ---------------------------------------------- | | `name` | string | ✅ | Agent's display name | | `bio` | string \| string\[] | ✅ | Background/personality description | | `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 | | `id` | UUID | ❌ | Unique identifier | | `username` | string | ❌ | Social media username | | `system` | string | ❌ | System prompt override | | `templates` | object | ❌ | Custom prompt templates | ### Style Object Structure ```typescript style: { all?: string[]; // General style rules chat?: string[]; // Chat-specific style post?: string[]; // Post-specific style } ``` ### Templates Object Structure ```typescript templates?: Templates; // Custom prompt templates ``` ## Working Example ```typescript export const character: Character = { name: "Eliza", bio: [ "Helpful AI assistant", "Expert in technical topics", "Friendly conversationalist" ], adjectives: ["helpful", "knowledgeable", "friendly"], topics: ["technology", "programming", "general knowledge"], // 2D array: each sub-array is a conversation messageExamples: [[ { name: "{{user}}", content: { text: "Can you help me debug this?" } }, { name: "Eliza", content: { text: "I'd be happy to help! Can you share the error message?" } } ]], style: { all: ["be concise", "use examples"], chat: ["be conversational"], post: ["use emojis sparingly"] }, // Plugins loaded based on environment plugins: [ "@elizaos/plugin-bootstrap", // Core functionality ...(process.env.DISCORD_API_TOKEN ? ["@elizaos/plugin-discord"] : []), ...(process.env.OPENAI_API_KEY ? ["@elizaos/plugin-openai"] : []) ] }; ``` ## Knowledge Configuration ```typescript // String facts knowledge: ["I am an AI assistant", "I help with coding"] ``` ## Memory & Runtime Agents remember: * Recent conversations * Important facts about users * Context from previous interactions At runtime, characters become `Agent` instances with status tracking: ```typescript interface Agent extends Character { enabled?: boolean; status?: 'active' | 'inactive'; createdAt: number; updatedAt: number; } ``` ## Character Definition Characters can be defined in TypeScript (recommended) or JSON: ```typescript import { Character } from '@elizaos/core'; export const character: Character = { name: "TechHelper", bio: [ "AI assistant specialized in technology", "Expert in web development" ], // ... rest of configuration }; ``` ```json { "name": "TechHelper", "bio": [ "An AI assistant specialized in technology and programming", "Loves helping developers solve problems", "Expert in web development and open source" ], "adjectives": [ "helpful", "technical", "precise", "friendly" ], "topics": [ "programming", "web development", "open source", "debugging" ], "messageExamples": [ [ { "name": "User", "content": {"text": "I'm having trouble with my React app"} }, { "name": "TechHelper", "content": {"text": "I'd be happy to help debug your React app! Can you describe what specific issue you're encountering?"} } ] ], "postExamples": [ "Just discovered an awesome new debugging technique for React apps! Thread below 🧵", "Open source tip: Always read the contributing guidelines before submitting a PR 📖" ], "style": { "all": [ "use technical terms accurately", "provide code examples when relevant", "be encouraging and supportive" ], "chat": [ "ask clarifying questions", "break down complex topics", "offer step-by-step guidance" ], "post": [ "share useful tips and tricks", "use relevant emojis sparingly", "create engaging technical content" ] }, "knowledge": [ "I specialize in modern web development frameworks", {"path": "./knowledge/react-guide.md"}, {"directory": "./knowledge/tutorials", "shared": true} ], "plugins": [ "@elizaos/plugin-web-search", "@elizaos/plugin-code-runner" ], "settings": { "voice": { "model": "en_US-male-medium" }, "maxResponseLength": 1000 } } ``` ## Creating an Agent Create `character.ts` or `character.json` ```typescript plugins: [ "@elizaos/plugin-bootstrap", "@elizaos/plugin-discord" ] ``` ```bash elizaos start ``` ## Best Practices * Keep personality traits consistent * Provide diverse message examples * Focus knowledge on the agent's purpose * Test conversations before deploying * Use TypeScript for better type safety * Load plugins conditionally based on environment ## Next Steps Extend your agent's capabilities Create multi-agent systems # Core Concepts Source: https://eliza.how/core-concepts/index The building blocks of elizaOS ## Building Blocks AI personalities with memory Extend capabilities Deploy applications ## Architecture Overview ```mermaid graph LR User[User Input] --> Runtime[AgentRuntime] Runtime --> Plugins Plugins --> Actions[Actions] Actions --> Response Response --> Evaluators Evaluators --> User Providers --> Runtime Services -.-> Runtime ``` ## Complete Example ```typescript import { Character } from '@elizaos/core'; export const character: Character = { name: "Assistant", bio: "A helpful AI agent", plugins: [ "@elizaos/plugin-bootstrap", // Core functionality "@elizaos/plugin-discord", // Discord integration "plugin-web-search" // Web search capability ] }; ``` ## Plugin Components | Component | Purpose | Example | | -------------- | ----------------- | ----------------------------- | | **Actions** | Tasks to perform | Send message, fetch data | | **Providers** | Supply context | Time, user info, knowledge | | **Evaluators** | Process responses | Extract facts, filter content | | **Services** | Background tasks | Scheduled posts, monitoring | ## Getting Started Create your character configuration Enable capabilities you need Run locally or in production ## Learn More * **New to elizaOS?** Start with [Agents](/core-concepts/agents) * **Building features?** Explore [Plugins](/core-concepts/plugins) * **Ready to deploy?** Check [Projects](/core-concepts/projects) * **Want details?** Dive into [Architecture](/deep-dive/architecture) # Plugins Source: https://eliza.how/core-concepts/plugins Extend agent capabilities with plugins ## What are Plugins? Plugins extend agent capabilities through: * **Actions** - Tasks agents can perform * **Providers** - Data sources for context * **Evaluators** - Response processors * **Services** - Background tasks & integrations * **Routes** - HTTP endpoints * **Events** - Event handlers ## Plugin Interface ```typescript export const myPlugin: Plugin = { name: 'my-plugin', // Core components actions: [], // Tasks to perform providers: [], // Data providers evaluators: [], // Response processors services: [], // Background services // Optional features routes: [], // HTTP endpoints events: {}, // Event handlers // Configuration init: async (config, runtime) => {} }; ``` ## Core Plugin: Bootstrap Every agent includes `@elizaos/plugin-bootstrap` which provides essential functionality for message handling, knowledge management, and basic agent operations. For detailed information, see the [Bootstrap Plugin Deep Dive](/deep-dive/bootstrap-plugin). ## Platform Plugins | Plugin | Description | Environment Variable | | -------------------------- | ----------------------- | -------------------- | | `@elizaos/plugin-discord` | Discord bot integration | `DISCORD_API_TOKEN` | | `@elizaos/plugin-telegram` | Telegram bot | `TELEGRAM_BOT_TOKEN` | | `@elizaos/plugin-twitter` | Twitter/X integration | `TWITTER_API_KEY` | ## Feature Plugins Available at [github.com/elizaos-plugins](https://github.com/elizaos-plugins): * `plugin-binance` - Crypto trading * `plugin-dexscreener` - Token prices * `plugin-web-search` - Web search * `plugin-firecrawl` - Web scraping * `plugin-0x` - DEX trading * And many more... ## Services Background tasks and long-running processes: ```typescript class MyService extends Service { async start() { // Initialize service setInterval(() => this.checkUpdates(), 60000); } async stop() { // Cleanup } } ``` ## Using Plugins ### Conditional Loading ```typescript plugins: [ "@elizaos/plugin-bootstrap", ...(process.env.DISCORD_API_TOKEN ? ["@elizaos/plugin-discord"] : []), ...(process.env.OPENAI_API_KEY ? ["@elizaos/plugin-openai"] : []) ] ``` ### Environment Variables ```bash # Platform APIs DISCORD_API_TOKEN=... TELEGRAM_BOT_TOKEN=... TWITTER_API_KEY=... # AI Models OPENAI_API_KEY=... ANTHROPIC_API_KEY=... ``` ## Quick Plugin Example ```typescript import { Plugin } from '@elizaos/core'; export const weatherPlugin: Plugin = { name: 'weather-plugin', description: 'Provides weather data', providers: [{ name: 'WEATHER', get: async (runtime, message) => { const weather = await fetchWeather(); return { temperature: weather.temp }; } }], actions: [{ name: 'CHECK_WEATHER', description: 'Check current weather', validate: async () => true, handler: async (runtime, message) => { const weather = await fetchWeather(); return { text: `Current temperature: ${weather.temp}°C` }; } }] }; ``` ## Plugin Categories * Discord, Telegram, Twitter * WhatsApp, Slack, Teams * Reddit, LinkedIn * OpenAI, Anthropic * Ollama, LocalAI * Google Gemini * Binance, Coinbase * DexScreener, 0x * Jupiter, Uniswap * Web Search, Firecrawl * SQL, Vector DB * Image Gen, TTS ## Next Steps Learn about actions Explore providers Understand evaluators # Actions Source: https://eliza.how/core-concepts/plugins/actions Things agents can do ## What are Actions? Actions = discrete tasks agents can perform. ## Action Interface ```typescript interface Action { name: string; // Unique identifier description: string; // What it does similes?: string[]; // Alternative names examples?: ActionExample[][]; // Usage examples validate: Validator; // Can this run? handler: Handler; // Execute the action } ``` ## 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?" | ## Plugin Action Examples | Action | Plugin | Example | | --------------- | ------------------ | ----------------- | | `GET_PRICE` | plugin-binance | "BTC price?" | | `EXECUTE_TRADE` | plugin-binance | "Buy 0.1 BTC" | | `TOKEN_PRICE` | plugin-dexscreener | "Price of \$PEPE" | | Action | Plugin | Example | | -------------- | --------------- | ------------------ | | `POST_TWEET` | plugin-twitter | "Tweet: GM!" | | `SEND_MESSAGE` | plugin-discord | "Tell #general..." | | `REPLY` | plugin-telegram | Auto-replies | | Action | Plugin | Example | | ------------ | ----------------- | --------------------- | | `WEB_SEARCH` | plugin-web-search | "Search for..." | | `SCRAPE_URL` | plugin-firecrawl | "Get content from..." | | `QUERY_DB` | plugin-sql | "Find users where..." | ## Creating Actions ### Minimal Action ```typescript const action: Action = { name: 'MY_ACTION', description: 'Does something', validate: async () => true, handler: async (runtime, message) => { return { 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 { 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 { 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 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 { memories }; } ## Best Practices - Name actions clearly (VERB_NOUN format) - Validate before executing - Return consistent response format - Use similes for alternative triggers - Provide diverse examples ## Next Steps Learn about data providers Explore response evaluation ``` # Evaluators Source: https://eliza.how/core-concepts/plugins/evaluators Assess and filter agent responses ## What are Evaluators? Evaluators = post-processors that analyze and extract information from conversations. ## Evaluator Interface ```typescript interface Evaluator { name: string; // Unique identifier description: string; // What it evaluates similes?: string[]; // Alternative names alwaysRun?: boolean; // Run on every message? examples: EvaluationExample[]; // Training examples validate: Validator; // Should this run? handler: Handler; // Process the response } ``` ## Core Evaluators (Bootstrap Plugin) | Evaluator | Purpose | Extracts | | --------------------- | --------------- | --------------------------- | | `reflectionEvaluator` | Self-awareness | Insights about interactions | | `factEvaluator` | Fact extraction | Important information | | `goalEvaluator` | Goal tracking | User objectives | ## Plugin Evaluator Examples | Evaluator | Plugin | Purpose | | --------------------- | ------------------ | ----------------------- | | `sentimentEvaluator` | plugin-sentiment | Track conversation mood | | `toxicityEvaluator` | plugin-moderation | Filter harmful content | | `tokenPriceEvaluator` | plugin-dexscreener | Detect price queries | | `summaryEvaluator` | plugin-knowledge | Summarize conversations | ## Evaluator Flow ```mermaid graph LR 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 ``` ## Common Use Cases * Extract facts from conversations * Track user preferences * Update relationship status * Record important events * Remove sensitive data * Filter profanity * Ensure compliance * Validate accuracy * 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 * 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 ## Next Steps See how all components work together Create multi-agent systems # Providers Source: https://eliza.how/core-concepts/plugins/providers Supply data to agents ## What are Providers? Providers = data sources that supply context for agent decision-making. ## Provider Interface ```typescript interface Provider { name: string; // Unique identifier description?: string; // What it provides dynamic?: boolean; // Changes over time? position?: number; // Load priority (-100 to 100) private?: boolean; // Hidden from provider list? get: (runtime, message, state) => Promise; } ``` ## 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 | ## Plugin Provider Examples | Provider | Plugin | Returns | | -------------------- | ------------------ | ----------------------- | | `walletProvider` | plugin-sei | Balance, portfolio | | `marketProvider` | plugin-arbitrage | Price feeds | | `tokenPriceProvider` | plugin-dexscreener | Token prices, liquidity | | Provider | Plugin | Returns | | ----------------- | --------------- | ---------------- | | `userProvider` | plugin-discord | User info, roles | | `channelProvider` | plugin-telegram | Channel data | | `serverProvider` | plugin-discord | Server settings | | Provider | Plugin | Returns | | ------------------ | ----------------- | ---------------- | | `searchProvider` | plugin-web-search | Search results | | `documentProvider` | plugin-obsidian | Note content | | `queryProvider` | plugin-sql | Database results | ## 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 Return Format ```typescript interface ProviderResult { text?: string; // Natural language context data?: { // Structured data [key: string]: any; }; values?: { // Key-value pairs [key: string]: any; }; } ``` ## Provider Priority ```typescript // Lower numbers = higher priority position: -100 // Loads first position: 0 // Default position: 100 // Loads last ``` ## Best Practices * Return consistent data structures * Handle errors gracefully * Cache when appropriate * Keep data fetching fast * Document what data is provided ## Next Steps See how actions use provider data Learn about response evaluation # Projects Source: https://eliza.how/core-concepts/projects Collections of agents and plugins ## What are Projects? Projects = TypeScript applications that configure and run one or more agents. ## Project Structure ``` my-project/ ├── src/ │ ├── index.ts # Entry point │ └── character.ts # Agent configuration ├── .env # Environment variables ├── package.json # Dependencies └── tsconfig.json # TypeScript config ``` ## Project Definition ```typescript // src/index.ts import { Project, ProjectAgent, IAgentRuntime } from '@elizaos/core'; import { character } from './character'; export const projectAgent: ProjectAgent = { character, init: async (runtime: IAgentRuntime) => { // Optional initialization logic console.log('Initializing:', character.name); }, // plugins: [customPlugin], // Optional project-specific plugins }; const project: Project = { agents: [projectAgent], }; export default project; ``` ```` ## Character Configuration ```typescript // src/character.ts import { Character } from '@elizaos/core'; export const character: Character = { name: "MyAgent", bio: "A helpful assistant", plugins: [ "@elizaos/plugin-bootstrap", ...(process.env.DISCORD_API_TOKEN ? ["@elizaos/plugin-discord"] : []) ] }; ```` ## Multi-Agent Projects ```typescript import { supportAgent, analyticsAgent } from './agents'; const supportProjectAgent: ProjectAgent = { character: supportAgent, init: async (runtime) => { console.log('Support agent ready'); } }; const analyticsProjectAgent: ProjectAgent = { character: analyticsAgent, init: async (runtime) => { console.log('Analytics agent ready'); } }; const project: Project = { agents: [supportProjectAgent, analyticsProjectAgent] }; export default project; ``` ## Environment Configuration ```bash .env # API Keys OPENAI_API_KEY=sk-... DISCORD_BOT_TOKEN=... # Database DATABASE_URL=postgresql://localhost/mydb # Project Settings LOG_LEVEL=info NODE_ENV=production ``` ## Running Projects ```bash # Start with default character elizaos start # Start with specific character file elizaos start --character character.json # Development mode elizaos dev ``` ## Project Templates ### Starter Project ```bash elizaos create my-agent cd my-agent bun install bun start ``` ### Custom Project Setup ```bash mkdir my-project && cd my-project bun init bun add @elizaos/core @elizaos/cli bun add @elizaos/plugin-bootstrap ``` ## Example Projects ### Single Agent Projects ```typescript plugins: [ "@elizaos/plugin-bootstrap", "@elizaos/plugin-discord" ] ``` ```typescript plugins: [ "@elizaos/plugin-bootstrap", "plugin-binance", "plugin-dexscreener" ] ``` ### Advanced Single Agent: Spartan [Spartan](https://github.com/elizaos/spartan) showcases a sophisticated single-agent implementation: ```typescript export const spartan: Character = { name: "Spartan", bio: "Elite AI agent with advanced capabilities", plugins: [ "@elizaos/plugin-bootstrap", "@elizaos/plugin-twitter", // Social media presence "plugin-web-search", // Research capabilities "plugin-code-analysis", // Code understanding "plugin-sentiment", // Emotion analysis "plugin-knowledge-graph" // Complex reasoning ], // Advanced features settings: { modelProvider: "anthropic", responseMode: "strategic", memoryDepth: "extended" } }; ``` **Advanced Features:** * Multi-modal reasoning * Long-term memory strategies * Complex action chains * Advanced prompt engineering * Custom evaluation pipelines ### Real-World Multi-Agent Project: The Org [The Org](https://github.com/elizaos/the-org) demonstrates a sophisticated multi-agent system: ```typescript // Multiple specialized agents working together const project: Project = { agents: [ // Executive agent - high-level decisions { character: ceo, init: initCEO }, // Department heads - specialized domains { character: cto, init: initCTO }, { character: cfo, init: initCFO }, { character: cmo, init: initCMO }, // Operational agents - specific tasks { character: devOps, init: initDevOps }, { character: researcher, init: initResearcher } ] }; ``` **Key Features:** * Hierarchical agent organization * Inter-agent communication * Specialized roles and permissions * Shared knowledge base * Coordinated decision-making ## Best Practices * Use TypeScript for better type safety * Load plugins conditionally based on environment * Keep character definitions modular * Test locally before deploying * Use `elizaos dev` for development ## Project vs Agent | Aspect | Agent | Project | | ------------- | --------------------- | -------------------------- | | Definition | Single AI personality | Application with 1+ agents | | Configuration | Character interface | Project interface | | Plugins | Per-character | Per-agent or shared | | Use case | Simple bots | Complex systems | ## Next Steps Browse sample agent configurations # Architecture Overview Source: https://eliza.how/deep-dive/architecture Deep dive into elizaOS architecture ## System Architecture elizaOS follows a modular, plugin-based architecture: ```mermaid graph TB 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)] ``` ## Core Components ### AgentRuntime The central orchestrator that: * Manages agent lifecycle * Processes messages * Coordinates plugins * Handles state composition * Manages services ### Plugin System Plugins extend functionality through: * **Actions** - Discrete tasks * **Providers** - Context data * **Evaluators** - Response processing * **Services** - Background processes * **Routes** - HTTP endpoints * **Events** - Event handlers ### Memory System Hierarchical memory storage: * **Messages** - Conversation history * **Facts** - Extracted information * **Documents** - Knowledge base * **Relationships** - Entity connections ### State Management State flows through the system: 1. Providers contribute context 2. Runtime composes state 3. Actions use state for decisions 4. Evaluators process results ## Plugin Loading ```typescript // Plugin priority determines load order const pluginLoadOrder = [ databases, // Priority: -100 modelProviders, // Priority: -50 corePlugins, // Priority: 0 features, // Priority: 50 platforms // Priority: 100 ]; ``` ## Service Lifecycle ```typescript class Service { async start(runtime: IAgentRuntime) { // Initialize service } async stop() { // Cleanup } } ``` ## Event Flow Events propagate through the system: 1. Runtime emits event 2. Plugins handle event 3. Services react to events 4. State updates ## Database Abstraction ```typescript interface IDatabaseAdapter { // Memory operations createMemory(memory: Memory): Promise searchMemories(query: string): Promise // Entity management createEntity(entity: Entity): Promise updateEntity(entity: Entity): Promise // Relationships createRelationship(rel: Relationship): Promise } ``` ## Next Steps Explore the AgentRuntime # Plugin Internals Source: https://eliza.how/deep-dive/plugin-internals Comprehensive guide to the ElizaOS plugin system architecture and implementation ## Overview The Eliza 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 two 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. ## 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); } }; ``` # Real-World Plugin and Project Patterns Source: https://eliza.how/deep-dive/real-world-patterns Practical patterns and structures used in the Eliza framework based on real implementations This guide documents the actual patterns and structures used in the Eliza framework based on examination of real plugin implementations and project structures. ## Plugin Structure Patterns ### Basic Plugin Structure Every plugin follows this core structure (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 }; ``` ### Real Examples #### Bootstrap Plugin (`plugin-bootstrap`) 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 Plugins (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 Patterns 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 Example (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 Patterns 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}.`, }; }, }; ``` ## Service Patterns Services run in the background and handle ongoing tasks: ```typescript export class TaskService extends Service { static serviceType = ServiceType.TASK; capabilityDescription = 'The agent is able to schedule and execute tasks'; static async start(runtime: IAgentRuntime): Promise { const service = new TaskService(runtime); await service.startTimer(); return service; } static async stop(runtime: IAgentRuntime) { const service = runtime.getService(ServiceType.TASK); if (service) { await service.stop(); } } private async startTimer() { this.timer = setInterval(async () => { await this.checkTasks(); }, this.TICK_INTERVAL); } } ``` ### Platform Service Example (Discord) ```typescript export class DiscordService extends Service implements IDiscordService { static serviceType: string = DISCORD_SERVICE_NAME; capabilityDescription = "The agent is able to send and receive messages on discord"; constructor(runtime: IAgentRuntime) { super(runtime); // Parse environment configuration const token = runtime.getSetting("DISCORD_API_TOKEN"); if (!token) { this.client = null; return; } // Initialize Discord client this.client = new DiscordJsClient({ intents: [/* Discord intents */], partials: [/* Discord partials */] }); // Set up event handlers this.setupEventHandlers(); } } ``` ## Project Structure Patterns ### Single Agent Project ```typescript // packages/project-starter/src/index.ts import { type IAgentRuntime, type Project, type ProjectAgent } from '@elizaos/core'; import { character } from './character.ts'; const initCharacter = ({ runtime }: { runtime: IAgentRuntime }) => { logger.info('Initializing character'); logger.info('Name: ', character.name); }; export const projectAgent: ProjectAgent = { character, init: async (runtime: IAgentRuntime) => await initCharacter({ runtime }), // plugins: [starterPlugin], // Custom plugins here }; const project: Project = { agents: [projectAgent], }; export default project; ``` ### Character Configuration Characters define personality and plugin configuration: ```typescript export const character: Character = { name: 'Eliza', plugins: [ // Core plugins first '@elizaos/plugin-sql', // Conditional plugins based on environment ...(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'] : []), // Bootstrap plugin (unless explicitly disabled) ...(!process.env.IGNORE_BOOTSTRAP ? ['@elizaos/plugin-bootstrap'] : []), ], settings: { secrets: {}, avatar: 'https://elizaos.github.io/eliza-avatars/Eliza/portrait.png', }, system: 'Respond to all messages in a helpful, conversational manner...', bio: [ 'Engages with all types of questions and conversations', 'Provides helpful, concise responses', // ... ], topics: ['general knowledge', 'problem solving', 'technology'], messageExamples: [/* conversation examples */], style: { all: ['Keep responses concise', 'Use clear language'], chat: ['Be conversational', 'Show personality'], }, }; ``` ## Plugin Registration and Initialization ### Environment-Based Plugin Loading Plugins are conditionally loaded based on environment variables: ```typescript const plugins = [ // Always loaded '@elizaos/plugin-sql', // 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'] : []), ]; ``` ### Plugin Initialization 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; } }, }; ``` ## Event Handling Patterns Plugins can handle various system events: ```typescript const events = { [EventType.MESSAGE_RECEIVED]: [ async (payload: MessagePayload) => { await messageReceivedHandler({ runtime: payload.runtime, message: payload.message, callback: payload.callback, }); }, ], [EventType.WORLD_JOINED]: [ async (payload: WorldPayload) => { await handleServerSync(payload); }, ], [EventType.ENTITY_JOINED]: [ async (payload: EntityPayload) => { await syncSingleUser(/* params */); }, ], }; ``` ## Multi-Agent Projects While the examples show single-agent projects, the structure supports multiple agents: ```typescript const project: Project = { agents: [ { character: elizaCharacter, init: async (runtime) => { /* Eliza init */ }, plugins: [/* Eliza plugins */], }, { character: assistantCharacter, init: async (runtime) => { /* Assistant init */ }, plugins: [/* Assistant plugins */], }, ], }; ``` ## Environment Variable Usage Common environment variables used by plugins: ```bash # AI Model Providers OPENAI_API_KEY= ANTHROPIC_API_KEY= GOOGLE_GENERATIVE_AI_API_KEY= OLLAMA_API_ENDPOINT= # Platform Integrations DISCORD_API_TOKEN= TELEGRAM_BOT_TOKEN= TWITTER_API_KEY= TWITTER_API_SECRET_KEY= TWITTER_ACCESS_TOKEN= TWITTER_ACCESS_TOKEN_SECRET= # Plugin Control IGNORE_BOOTSTRAP= # Skip bootstrap plugin CHANNEL_IDS= # Restrict Discord to specific channels # Database POSTGRES_URL= PGLITE_DATA_DIR= ``` ## 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. **Provider Data**: Return structured data with `data`, `values`, and `text` fields 6. **Action Validation**: Always implement validation logic before execution 7. **Error Handling**: Use try-catch blocks and log errors appropriately 8. **Type Safety**: Use TypeScript types from `@elizaos/core` for all plugin components # AgentRuntime Source: https://eliza.how/deep-dive/runtime The core engine of elizaOS ## What is AgentRuntime? The AgentRuntime is the central orchestrator that manages agent lifecycle, processes messages, and coordinates all system components. ## Key Responsibilities ### 1. Action Processing ```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 The runtime builds context by aggregating data from all 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 ```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 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; } ``` ## Lifecycle ```mermaid graph 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] ``` ## Model Management The runtime manages AI model selection through a priority system: ```typescript // Plugins register model handlers runtime.registerModel( ModelType.TEXT_LARGE, async (runtime, params) => { // Call OpenAI API, Anthropic, etc. return generatedText; }, 'openai', // provider name 100 // priority ); // Use models with type safety const result = await runtime.useModel( ModelType.TEXT_LARGE, { prompt: "Generate a response", temperature: 0.7 } ); // Get embeddings const embedding = await runtime.useModel( ModelType.TEXT_EMBEDDING, { input: "Text to embed" } ); ``` ## Memory Management ```typescript // Store memories await runtime.databaseAdapter.createMemory({ type: MemoryType.MESSAGE, content: { text: "User message" }, roomId: message.roomId }); // Search memories const memories = await runtime.databaseAdapter.searchMemories({ query: "previous conversation", limit: 10 }); ``` ## Best Practices * Initialize plugins in dependency order * Start services after all plugins loaded * Clean up resources on shutdown * Handle errors gracefully * Use appropriate model sizes ## Next Steps Learn about background services # Service System Source: https://eliza.how/deep-dive/services Background tasks and integrations ## What are Services? Services are long-running background tasks that extend agent functionality beyond request-response patterns. ## Service Interface ```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 Types ```typescript // Core service types (from @elizaos/core) const ServiceType = { // Define core service types } as const; // Plugins extend service types through module augmentation declare module '@elizaos/core' { interface ServiceTypeRegistry { DISCORD: 'discord'; TELEGRAM: 'telegram'; TWITTER: 'twitter'; // ... other plugin-specific types } } ``` ## Common Service Patterns ### Platform Integration Service ```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() { this.client = new Discord.Client(); this.client.on('messageCreate', async (message) => { // Convert to Memory format const memory = await this.convertMessage(message); // Process through runtime await this.runtime.processActions(memory, []); }); await this.client.login(process.env.DISCORD_TOKEN); } async stop() { await this.client.destroy(); } } ``` ### Background Task Service ```typescript class TaskService extends Service { name = ServiceType.TASK; private interval: NodeJS.Timer; async start(runtime: IAgentRuntime) { // Check for scheduled tasks every minute this.interval = setInterval(async () => { const tasks = await runtime.databaseAdapter.getTasks({ status: 'pending', scheduledFor: { $lte: new Date() } }); for (const task of tasks) { await this.executeTask(task, runtime); } }, 60000); } async stop() { clearInterval(this.interval); } } ``` ### Model Service Pattern ```typescript // Model services typically implement model providers instead class OpenAIModelProvider implements ModelProvider { async generateText(params: GenerateTextParams) { const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); const response = await client.chat.completions.create({ model: params.model || "gpt-4", messages: params.messages }); return response.choices[0].message; } async embed(params: EmbedParams) { // Implementation for embeddings } } ``` ## Service Lifecycle ```mermaid graph LR Register[Register Service] --> Queue[Queue Start] Queue --> Init[Runtime Init] Init --> Start[Start Service] Start --> Running[Running] Running --> Stop[Stop Service] Stop --> Cleanup[Cleanup] ``` ## Service Registration ```typescript // In a plugin export const discordPlugin: Plugin = { name: 'discord', services: [DiscordService], init: async (config, runtime) => { // Services auto-registered and started } }; ``` ## Service Communication Services can interact with the runtime and other services: ```typescript class NotificationService extends Service { static serviceType = 'notification' as const; capabilityDescription = 'Cross-platform notifications'; async notify(message: string) { // Get service by type const discord = this.runtime.getService('discord'); if (discord) { await discord.sendMessage(channelId, message); } // Check all registered services const services = this.runtime.getAllServices(); // Coordinate across services } } ``` ## Best Practices * Implement graceful shutdown in `stop()` * Handle errors without crashing * Use environment variables for config * Avoid blocking operations * Clean up resources properly ## Common Services | Service | Purpose | Example Plugin | | ------------------- | ------------------------- | ----------------- | | Platform Services | Connect to chat platforms | Discord, Telegram | | Model Services | AI model providers | OpenAI, Anthropic | | Data Services | External data sources | Web search, SQL | | Media Services | Process media | TTS, image gen | | Background Services | Scheduled tasks | Task runner | ## Next Steps Browse sample character configurations # Development Source: https://eliza.how/development Learn how to develop with elizaOS - from simple character modifications to core framework contributions **Prerequisites**: Make sure you have completed the [Quickstart](/quickstart) guide and have Node.js (version 23.0 or higher) and Bun installed. ## Development Tracks elizaOS offers two distinct development paths depending on your goals and experience level: Perfect for creating and customizing your own AI agents using the elizaOS CLI For contributors and developers building custom elizaOS versions ## Beginner Development Track The beginner track focuses on using the elizaOS CLI to create and customize your agents without diving into the core framework code. ### Getting Started with CLI Development If you haven't already, create a new agent using the elizaOS CLI: ```bash elizaos create my-custom-agent ``` ```bash cd my-custom-agent ``` Your agent directory contains: * `character.json` - Your agent's personality and configuration * `package.json` - Project dependencies and scripts * `.env` - Environment variables and API keys * `plugins/` - Directory for custom plugins ### Modifying Your Character The `character.json` file defines your agent's personality, knowledge, and behavior: ```json character.json { "name": "MyAgent", "bio": "A helpful AI assistant created with elizaOS", "adjectives": [ "friendly", "knowledgeable", "professional" ], "knowledge": [ "I am an AI assistant created with elizaOS", "I can help with various tasks and questions" ], "messageExamples": [ [ { "name": "User", "content": {"text": "Hello!"} }, { "name": "MyAgent", "content": {"text": "Hello! How can I assist you today?"} } ] ], "plugins": [] } ``` Experiment with different personality traits and knowledge entries to create unique agent behaviors. ### Working with Plugins Plugins extend your agent's capabilities with additional features: #### Installing Plugins Use the elizaOS CLI to add existing plugins: ```bash elizaos plugins add @elizaos/plugin-twitter elizaos plugins add @elizaos/plugin-discord ``` After installing plugins via CLI, you **must** add them to your character file (`.json` or `.ts`) to activate them: ```json character.json { "name": "MyAgent", "plugins": [ "@elizaos/plugin-sql", "@elizaos/plugin-twitter", "@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.TWITTER_API_KEY ? ["@elizaos/plugin-twitter"] : []), ...(process.env.DISCORD_API_TOKEN ? ["@elizaos/plugin-discord"] : []), ...(process.env.OPENAI_API_KEY ? ["@elizaos/plugin-openai"] : []) ], bio: ["Your agent's description"], style: { all: ["conversational", "friendly"] } }; ``` #### Removing Plugins To remove a plugin: ```bash elizaos plugins remove @elizaos/plugin-twitter ``` Remember to also remove it from your character file (`.json` or `.ts`). #### Available Plugins * `@elizaos/plugin-bootstrap` - Base plugin infrastructure * `@elizaos/plugin-sql` - SQL database integration * `@elizaos/plugin-forms` - Forms for structured data collection * `@elizaos/plugin-starter` - Template for creating new plugins ### Testing Your Changes After making modifications: ```bash elizaos start ``` Visit `http://localhost:3000` to interact with your customized agent. ## Advanced Development Track The advanced track is for developers who want to contribute to the elizaOS core framework or build custom versions. ### Setting Up the Monorepo Clone the official elizaOS monorepo: ```bash git clone https://github.com/elizaos/eliza.git cd eliza ``` Use Bun to install all dependencies: ```bash bun install ``` Build all packages in the monorepo: ```bash bun run build ``` ### Monorepo Structure The elizaOS monorepo is organized as follows: ``` eliza/ ├── packages/ │ ├── core/ # Core elizaOS framework │ ├── cli/ # CLI tool source code │ ├── client/ # Client libraries │ └── plugin-*/ # Official plugins └── scripts/ # Build and utility scripts ``` ### Contributing to Core Framework #### Development Workflow ```bash git checkout -b feature/my-new-feature ``` Edit files in the relevant package directory ```bash bun test ``` ```bash bun run build bun run typecheck ``` Push your changes and create a PR on GitHub #### Key Development Areas Work on the fundamental elizaOS engine: * Agent reasoning logic * Memory management * Plugin system architecture * Performance optimizations Create new plugins to extend functionality: * Integration with external services * New communication channels * Custom tools and capabilities Enhance the developer experience: * New CLI commands * Better error handling * Template management ### Creating Custom elizaOS Versions For specialized use cases, you might want to create a custom fork: ```bash # Fork the repository on GitHub first git clone https://github.com/YOUR_USERNAME/eliza.git cd eliza # Add upstream remote git remote add upstream https://github.com/elizaos/eliza.git # Create your custom branch git checkout -b custom/my-version ``` #### Custom Version Best Practices Keep your custom version compatible with the core plugin system Clearly document all modifications and custom features Regularly sync with upstream to get the latest improvements: ```bash git fetch upstream git merge upstream/develop ``` ### Local Development Tips #### Environment Setup Create a `.env.local` file for development: ```bash .env.local NODE_ENV=development LOG_LEVEL=debug ENABLE_HOT_RELOAD=true ``` #### Using Development Mode Run the framework in development mode with hot reload: ```bash bun run dev ``` #### Debugging Set `LOG_LEVEL=debug` in your environment variables The monorepo includes VS Code debug configurations in `.vscode/launch.json` Use `bun run profile` to generate performance reports ## Best Practices ### For Beginners * Start with small character modifications * Test changes frequently * Back up your `character.json` before major changes * Join the community for plugin recommendations ### For Advanced Users * Follow the project's coding standards * Write comprehensive tests for new features * Document your code thoroughly * Participate in code reviews ## Getting Help Comprehensive guides and API references Report bugs and request features Get help from the community ## Next Steps Decide whether to start with the beginner or advanced track based on your goals Follow the setup instructions for your chosen track Begin creating your agent or contributing to the framework Share your creations with the elizaOS community! # State Management Source: https://eliza.how/guides/compose-state-guide Comprehensive documentation on the composeState method in ElizaOS and how it interacts with different types of providers to build contextual state for agent decision-making This guide provides comprehensive documentation on the `composeState` method in ElizaOS and how it interacts with different types of providers to build contextual state for agent decision-making. ## Introduction The `composeState` method is a core function in ElizaOS that aggregates data from multiple providers to create a comprehensive state object. This state represents the agent's understanding of the current context and is used for decision-making, action selection, and response generation. ### What is State? State in ElizaOS is a structured object containing: * **values**: Key-value pairs for direct access (used in templates) * **data**: Structured data from providers * **text**: Concatenated textual context from all providers ## Quick Reference ### 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 | ### Common 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); ``` ## Understanding composeState ### 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 ### How It Works 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 ## Provider Types ElizaOS includes various built-in providers, each serving a specific purpose: ### Core Providers #### 1. **Character Provider** Provides character information, personality, and behavior guidelines. ```typescript // Included data: { values: { agentName: "Alice", bio: "AI assistant focused on...", topics: "technology, science, education", adjective: "helpful" }, data: { character: {...} }, text: "# About Alice\n..." } ``` #### 2. **Recent Messages Provider** Provides conversation history and context. ```typescript // Included data: { values: { recentMessages: "User: Hello\nAlice: Hi there!", recentInteractions: "..." }, data: { recentMessages: [...], actionResults: [...] }, text: "# Conversation Messages\n..." } ``` #### 3. **Actions Provider** Lists available actions the agent can take. ```typescript // Included data: { values: { actionNames: "Possible response actions: 'SEND_MESSAGE', 'SEARCH', 'CALCULATE'", actionExamples: "..." }, data: { actionsData: [...] }, text: "# Available Actions\n..." } ``` ### Dynamic Providers Dynamic providers are only executed when explicitly requested: * **Facts Provider**: Retrieves relevant facts from memory * **Relationships Provider**: Gets relationship information * **Settings Provider**: Fetches configuration settings * **Roles Provider**: Server role hierarchy ### Private Providers Private providers are internal and not included in default state composition: * **Evaluators Provider**: Post-interaction processing options ## Detailed Provider Reference ### Built-in Providers #### Actions Provider (`ACTIONS`) * **Purpose**: 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 #### Action State Provider (`ACTION_STATE`) * **Purpose**: Shares execution state between chained actions * **Position**: 150 * **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 #### Anxiety Provider (`ANXIETY`) * **Purpose**: Provides behavioral guidelines based on channel type * **Position**: Default * **Dynamic**: No (included by default) * **Behavior**: Adjusts response style for DMs, groups, and voice channels #### Attachments Provider (`ATTACHMENTS`) * **Purpose**: Lists files and media in the conversation * **Position**: Default * **Dynamic**: Yes (must be explicitly included) * **Data Provided**: * File names, URLs, descriptions * Text content from attachments * Media metadata #### Capabilities Provider (`CAPABILITIES`) * **Purpose**: Lists agent's available services and capabilities * **Position**: Default * **Dynamic**: No (included by default) * **Data Provided**: Service descriptions and available functions #### Character Provider (`CHARACTER`) * **Purpose**: Core personality and behavior definition * **Position**: Default * **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 #### Choice Provider (`CHOICE`) * **Purpose**: Lists pending decisions awaiting user input * **Position**: Default * **Dynamic**: No (included by default) * **Use Case**: Interactive workflows requiring user selection #### Entities Provider (`ENTITIES`) * **Purpose**: Information about conversation participants * **Position**: Default * **Dynamic**: Yes (must be explicitly included) * **Data Provided**: * User names and aliases * Entity metadata * Sender identification #### Facts Provider (`FACTS`) * **Purpose**: Retrieves relevant stored facts * **Position**: Default * **Dynamic**: Yes (must be explicitly included) * **Behavior**: Uses embedding search to find contextually relevant facts #### Providers Provider (`PROVIDERS`) * **Purpose**: Meta-provider listing all available providers * **Position**: Default * **Dynamic**: No (included by default) * **Use Case**: Dynamic provider discovery #### Recent Messages Provider (`RECENT_MESSAGES`) * **Purpose**: Conversation history and context * **Position**: 100 (runs later to access other data) * **Dynamic**: No (included by default) * **Data Provided**: * Recent dialogue messages * Action execution history * Formatted conversation context #### Relationships Provider (`RELATIONSHIPS`) * **Purpose**: Social graph and interaction history * **Position**: Default * **Dynamic**: Yes (must be explicitly included) * **Data Provided**: * Known entities and their relationships * Interaction frequency * Relationship metadata #### Roles Provider (`ROLES`) * **Purpose**: Server/group role hierarchy * **Position**: Default * **Dynamic**: No (included by default) * **Restrictions**: Only available in group channels #### Settings Provider (`SETTINGS`) * **Purpose**: Configuration and onboarding state * **Position**: Default * **Dynamic**: No (included by default) * **Behavior**: Different output for DMs (onboarding) vs groups #### Time Provider (`TIME`) * **Purpose**: Current time and timezone information * **Position**: Default * **Dynamic**: No (included by default) * **Data Provided**: Formatted timestamps and timezone data #### World Provider (`WORLD`) * **Purpose**: Virtual world/server context * **Position**: Default * **Dynamic**: No (included by default) * **Data Provided**: World metadata and configuration ## Cache Management ### How Caching Works The `composeState` method uses an in-memory cache (`stateCache`) to store composed states: ```typescript // Cache is stored by message ID this.stateCache.set(message.id, newState); ``` ### Getting Cached Data ```typescript // Inside a runtime context (this.stateCache is a Map) // The cache is internal to the runtime and accessed via composeState with skipCache parameter // To get fresh data, skip the cache: const freshState = await runtime.composeState(message, null, false, true); // skipCache = true // To use cached data (default behavior): const cachedState = await runtime.composeState(message); // uses cache if available ``` ### Clearing Cache ```typescript import { AgentRuntime } from '@elizaos/core'; // The stateCache is internal to the runtime instance // In a custom runtime extension or plugin initialization: class ExtendedRuntime extends AgentRuntime { clearOldStateCache() { const oneHourAgo = Date.now() - 60 * 60 * 1000; for (const [messageId, state] of this.stateCache.entries()) { // Check if we have the message in memory this.getMemoryById(messageId).then((memory) => { if (memory && memory.createdAt < oneHourAgo) { this.stateCache.delete(messageId); } }); } } // Clear entire cache clearAllStateCache() { this.stateCache.clear(); } } ``` ## Usage Scenarios ### Scenario 1: Default State Composition Most common usage - compose state with all non-private, non-dynamic providers: ```typescript // Default usage - includes all standard providers const state = await runtime.composeState(message); // Access composed data console.log(state.values.agentName); // Character name console.log(state.values.recentMessages); // Recent conversation console.log(state.values.actionNames); // Available actions ``` ### Scenario 2: Including Dynamic Providers Include specific dynamic providers for additional context: ```typescript // Include facts and relationships providers const state = await runtime.composeState( message, ['FACTS', 'RELATIONSHIPS'], // Include these dynamic providers false, // Don't exclude other providers false // Use cache if available ); // Access additional data console.log(state.values.facts); // Relevant facts console.log(state.values.relationships); // Relationship data ``` ### Scenario 3: Selective Provider Inclusion Only include specific providers for focused processing: ```typescript // Only get character and recent messages const state = await runtime.composeState( message, ['CHARACTER', 'RECENT_MESSAGES'], // Only these providers true, // onlyInclude = true false // Use cache ); // State will only contain data from specified providers ``` ### Scenario 4: Force Fresh Data Skip cache for real-time data requirements: ```typescript // Force fresh data fetch const state = await runtime.composeState( message, null, // Include default providers false, // Don't limit to includeList true // skipCache = true ); ``` ## Examples ### Example 1: Action Processing with State ```typescript import type { IAgentRuntime, Memory, State } from '@elizaos/core'; // In an action handler async function handleAction(runtime: IAgentRuntime, message: Memory) { // Compose state with action-specific providers const state = await runtime.composeState( message, ['CHARACTER', 'RECENT_MESSAGES', 'ACTION_STATE'], false, false ); // Use state for decision making if (state.values.hasActionResults) { console.log('Previous actions completed:', state.values.completedActions); } // Process action based on state // Actions are typically processed through runtime.processActions // but can be executed directly if needed const action = runtime.actions.find((a) => a.name === 'SEND_MESSAGE'); if (action && action.handler) { const result = await action.handler(runtime, message, state); return result; } } ``` ### Example 2: Settings-Based Configuration ```typescript // Check settings in DM for configuration async function checkConfiguration(runtime: IAgentRuntime, message: Memory) { // Include settings provider for DM configuration const state = await runtime.composeState( message, ['SETTINGS', 'CHARACTER'], false, true // Skip cache for fresh settings ); // Helper function to check if all required settings are configured const hasRequiredSettings = (settings: any) => { if (!settings) return false; // Check if all required settings have values return Object.entries(settings).every(([key, setting]: [string, any]) => { if (setting.required) { return setting.value !== null && setting.value !== undefined; } return true; }); }; // Check if configuration is needed const settings = state.data.providers?.SETTINGS?.data?.settings; if (settings && hasRequiredSettings(settings)) { return 'Configuration complete!'; } else { return "Let's configure your settings..."; } } ``` ### Example 3: Context-Aware Response Generation ```typescript import { ModelType } from '@elizaos/core'; // Generate response with full context async function generateContextualResponse(runtime: IAgentRuntime, message: Memory) { // Get comprehensive state const state = await runtime.composeState( message, ['CHARACTER', 'RECENT_MESSAGES', 'FACTS', 'ENTITIES', 'ATTACHMENTS'], false, false ); // Build prompt from state const prompt = ` ${state.values.bio} ${state.values.recentMessages} Known facts: ${state.values.facts || 'No relevant facts'} People in conversation: ${state.values.entities || 'Just you and me'} Respond to the last message appropriately. `; // Generate response using composed context const response = await runtime.useModel(ModelType.TEXT_LARGE, { prompt, maxTokens: 500, }); return response; } ``` ### Example 4: Custom Provider Integration ```typescript import type { Provider } from '@elizaos/core'; // Define a custom provider const customDataProvider: Provider = { name: 'CUSTOM_DATA', description: 'Custom data from external source', dynamic: true, get: async (runtime, message) => { try { // Example: fetch data from a 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: '' }; } }, }; // Register the provider runtime.registerProvider(customDataProvider); // Use in state composition const state = await runtime.composeState( message, ['CHARACTER', 'CUSTOM_DATA'], // Include custom provider false, true ); console.log(state.values.customData); // Access custom data value ``` ### Example 5: Performance Monitoring ```typescript // Monitor provider performance async function composeStateWithMetrics(runtime: IAgentRuntime, message: Memory) { const startTime = Date.now(); // Compose state const state = await runtime.composeState(message); // Check individual provider timings const providers = state.data.providers; for (const [name, result] of Object.entries(providers)) { console.log(`Provider ${name} data size:`, JSON.stringify(result).length); } const totalTime = Date.now() - startTime; console.log(`Total composition time: ${totalTime}ms`); return state; } ``` ## Advanced Examples ### Multi-Step Action Coordination ```typescript import type { IAgentRuntime, Memory, State } from '@elizaos/core'; // Example ActionPlan type interface ActionPlan { thought: string; steps: Array<{ actionName: string; params?: any; }>; totalSteps: number; } // Coordinate multiple actions with shared state async function executeMultiStepPlan(runtime: IAgentRuntime, message: Memory, plan: ActionPlan) { const workingMemory: { [key: string]: any } = {}; for (let i = 0; i < plan.steps.length; i++) { // Update state with working memory const state = await runtime.composeState( message, ['CHARACTER', 'ACTION_STATE', 'RECENT_MESSAGES'], false, true // Fresh state for each step ); // Inject working memory into state state.data.workingMemory = workingMemory; state.data.actionPlan = { ...plan, currentStep: i + 1, }; // Execute action through runtime's processActions const action = runtime.actions.find((a) => a.name === plan.steps[i].actionName); if (action && action.handler) { const result = await action.handler(runtime, message, state); // Store result in working memory workingMemory[`step_${i + 1}_result`] = result; } } return workingMemory; } ``` ### Provider Dependency Management ```typescript // Provider that depends on other providers const enhancedFactsProvider: Provider = { name: 'ENHANCED_FACTS', description: 'Facts with relationship context', dynamic: true, position: 200, // Run after other providers get: async (runtime, message, state) => { // First ensure we have entities and relationships const enhancedState = await runtime.composeState( message, ['ENTITIES', 'RELATIONSHIPS', 'FACTS'], false, false ); // Enhance facts with relationship context const facts = enhancedState.data.providers?.FACTS?.data?.facts || []; const relationships = enhancedState.data.providers?.RELATIONSHIPS?.data?.relationships || []; // Helper function to find related entities const findRelatedEntities = (fact: any, relationships: any[]) => { // Simple implementation - match entities mentioned in fact text return relationships.filter((rel) => fact.content?.text?.includes(rel.targetEntityId)); }; const formatEnhancedFacts = (facts: any[]) => { return facts .map( (fact) => `${fact.content?.text} [Related: ${fact.relatedEntities?.length || 0} entities]` ) .join('\n'); }; const enhancedFacts = facts.map((fact) => ({ ...fact, relatedEntities: findRelatedEntities(fact, relationships), })); return { values: { enhancedFacts: formatEnhancedFacts(enhancedFacts) }, data: { enhancedFacts }, text: `Enhanced facts with relationships:\n${formatEnhancedFacts(enhancedFacts)}`, }; }, }; ``` ### Dynamic Provider Loading ```typescript import { ChannelType, type IAgentRuntime, type Memory } from '@elizaos/core'; // Conditionally load providers based on context async function adaptiveStateComposition(runtime: IAgentRuntime, message: Memory) { // Determine context const room = await runtime.getRoom(message.roomId); const isDM = room?.type === ChannelType.DM; const isVoice = room?.type === ChannelType.VOICE_GROUP || room?.type === ChannelType.VOICE_DM; // Build provider list based on context const providers: string[] = ['CHARACTER', 'RECENT_MESSAGES']; if (isDM) { providers.push('SETTINGS'); // Configuration in DMs } else { providers.push('ROLES', 'ENTITIES'); // Group context } if (isVoice) { // Voice channels might need different providers // For example, you might want to limit providers for performance providers = ['CHARACTER', 'ANXIETY']; // Minimal providers for voice } // Check if user mentioned facts or history if (message.content.text.match(/remember|fact|history/i)) { providers.push('FACTS', 'RELATIONSHIPS'); } // Compose adaptive state return runtime.composeState(message, providers, false, false); } ``` ### State Transformation Pipeline ```typescript // Transform state through multiple stages async function stateTransformationPipeline(runtime: IAgentRuntime, message: Memory) { // Stage 1: Base state const baseState = await runtime.composeState( message, ['CHARACTER', 'RECENT_MESSAGES'], true, false ); // Helper function to determine if facts are needed const needsFactEnrichment = (state: State) => { // Check if the message mentions facts, history, or memory const messageText = state.values.recentMessages || ''; return /remember|fact|history|memory/i.test(messageText); }; // Stage 2: Enrich with facts if needed let enrichedState = baseState; if (needsFactEnrichment(baseState)) { const factsState = await runtime.composeState(message, ['FACTS'], false, false); enrichedState = mergeStates(baseState, factsState); } // Stage 3: Add action context const actionState = await runtime.composeState( message, ['ACTIONS', 'ACTION_STATE'], false, false ); // Final merged state return mergeStates(enrichedState, actionState); } function mergeStates(state1: State, state2: State): State { return { values: { ...state1.values, ...state2.values }, data: { ...state1.data, ...state2.data, providers: { ...state1.data.providers, ...state2.data.providers, }, }, text: `${state1.text}\n\n${state2.text}`, }; } ``` ## Troubleshooting ### Common Issues and Solutions #### 1. Stale Cache Data **Problem**: State contains outdated information ```typescript // Symptom: Old messages or incorrect data const state = await runtime.composeState(message); console.log(state.values.recentMessages); // Shows old messages ``` **Solution**: Force fresh data or implement cache TTL ```typescript // Option 1: Skip cache const freshState = await runtime.composeState(message, null, false, true); // Option 2: Implement cache TTL class CacheWithTTL extends Map { private ttl: number; private timestamps = new Map(); constructor(ttl: number = 5 * 60 * 1000) { // 5 minutes default 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); } } ``` #### 2. Provider Timeout Issues **Problem**: Slow providers blocking state composition ```typescript // Provider that might hang const slowProvider: Provider = { name: 'SLOW_API', get: async (runtime, message) => { // This could take forever without proper timeout handling try { // Simulate a slow external API call const response = await fetch('https://slow-api.example.com/data'); const data = await response.json(); return { data: { apiResponse: data }, values: { slowApiStatus: 'success' }, text: `Fetched data from slow API`, }; } catch (error) { // This might never complete if the API hangs return { data: {}, values: {}, text: '' }; } }, }; ``` **Solution**: Implement timeouts ```typescript const timeoutProvider: Provider = { name: 'TIMEOUT_SAFE', get: async (runtime, message) => { try { // Example of a provider that might take too long const fetchData = async () => { // Simulate an API call or expensive operation const service = runtime.getService('externalAPI'); if (!service) { return { values: {}, data: {}, text: '' }; } const data = await service.fetchData(); return { values: { apiData: data.summary }, data: { apiData: data }, text: `API data: ${data.summary}`, }; }; const result = await Promise.race([ fetchData(), new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), 5000)), ]); return result; } catch (error) { runtime.logger.warn(`Provider TIMEOUT_SAFE timed out`); return { values: {}, data: {}, text: '' }; } }, }; ``` #### 3. Memory Leaks from Cache **Problem**: Cache grows indefinitely ```typescript // This will grow forever for (const message of messages) { await runtime.composeState(message); } ``` **Solution**: Implement cache size limits ```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); } } ``` #### 4. Circular Provider Dependencies **Problem**: Providers depending on each other ```typescript // DON'T DO THIS const providerA: Provider = { name: 'A', get: async (runtime, message) => { const state = await runtime.composeState(message, ['B']); // Uses B's data }, }; const providerB: Provider = { name: 'B', get: async (runtime, message) => { const state = await runtime.composeState(message, ['A']); // Uses A's data - CIRCULAR! }, }; ``` **Solution**: Use provider positioning and cached state ```typescript const providerA: Provider = { name: 'A', position: 100, // Runs first get: async (runtime, message) => { // Generate data independently return { data: { aData: 'value' } }; }, }; const providerB: Provider = { name: 'B', position: 200, // Runs after A get: async (runtime, message, cachedState) => { // Access A's data from cached state const aData = cachedState.data?.providers?.A?.data?.aData; // Process the data from provider A const processedData = aData ? { enhanced: true, originalValue: aData, processedAt: Date.now(), } : null; return { data: { bData: processedData }, values: { bProcessed: processedData ? 'success' : 'no data' }, text: processedData ? `Processed data from A: ${aData}` : '', }; }, }; ``` ### 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; } ``` ## Best Practices ### 1. Provider Selection * Use default providers for general conversation * Include dynamic providers only when needed * Use `onlyInclude` for performance-critical paths ### 2. Cache Management ```typescript // Good: Use cache for repeated operations const state1 = await runtime.composeState(message); // First call const state2 = await runtime.composeState(message); // Uses cache // Good: Skip cache for real-time data const freshState = await runtime.composeState(message, null, false, true); // Bad: Always skipping cache // This defeats the purpose of caching and hurts performance ``` ### 3. Error Handling ```typescript try { const state = await runtime.composeState(message, ['CUSTOM_PROVIDER']); // Use state } catch (error) { console.error('State composition failed:', error); // Fallback to minimal state const minimalState = await runtime.composeState( message, ['CHARACTER'], // Just character data true, true ); } ``` ### 4. Memory Optimization ```typescript // Clean up 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 ``` ### 5. Custom Provider Guidelines When creating custom providers: ```typescript const customProvider: Provider = { name: 'CUSTOM_DATA', description: 'Provides custom data', dynamic: true, // Set to true if not always needed position: 150, // Higher numbers run later private: false, // Set to true for internal-only providers get: async (runtime, message, state) => { // Best practices: // 1. Return quickly - use timeouts // 2. Handle errors gracefully // 3. Return empty result on failure // 4. Keep data size reasonable try { // Example: Fetch data with timeout const fetchDataWithTimeout = async (timeout: number) => { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeout); try { const response = await fetch('https://api.example.com/data', { signal: controller.signal, }); clearTimeout(timeoutId); return await response.json(); } catch (error) { clearTimeout(timeoutId); throw error; } }; const data = await fetchDataWithTimeout(5000); return { values: { customValue: data.summary || 'No summary' }, data: { fullData: data }, text: `Custom data: ${data.summary || 'No data available'}`, }; } catch (error) { runtime.logger.error('Error in CUSTOM_DATA provider:', error); // Return empty result on error return { values: {}, data: {}, text: '', }; } }, }; ``` ## Summary The `composeState` method is central to ElizaOS's context management system. It provides a flexible way to aggregate data from multiple sources, manage caching for performance, and create rich contextual state for agent decision-making. By understanding how to effectively use providers and manage state composition, you can build more intelligent and context-aware agents. Key takeaways: * Use default composition for most scenarios * Include dynamic providers when specific data is needed * Leverage caching for performance * Clear cache when data freshness is critical * Monitor provider performance in production * Handle errors gracefully # Plugin Developer Guide Source: https://eliza.how/guides/plugin-developer-guide Comprehensive guide covering all aspects of plugin development in the ElizaOS system This comprehensive guide covers all aspects of plugin development in the ElizaOS system, consolidating patterns and best practices from platform plugins, LLM plugins, and DeFi plugins. 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. ## Table of Contents 1. [Introduction](#introduction) 2. [Quick Start: Scaffolding Plugins with CLI](#quick-start-scaffolding-plugins-with-cli) 3. [Plugin Architecture Overview](#plugin-architecture-overview) 4. [Core Plugin Components](#core-plugin-components) * [Services](#1-services) * [Actions](#2-actions) * [Providers](#3-providers) * [Evaluators](#4-evaluators) * [Model Handlers](#5-model-handlers-llm-plugins) * [HTTP Routes](#6-http-routes-and-api-endpoints) 5. [Advanced: Creating Plugins Manually](#advanced-creating-plugins-manually) 6. [Plugin Types and Patterns](#plugin-types-and-patterns) 7. [Advanced Configuration](#advanced-configuration) 8. [Testing Strategies](#testing-strategies) 9. [Security Best Practices](#security-best-practices) 10. [Publishing and Distribution](#publishing-and-distribution) 11. [Reference Examples](#reference-examples) ## Introduction ElizaOS plugins are modular extensions that enhance AI agents with new capabilities, integrations, and behaviors. The plugin system follows a consistent architecture that enables: * **Modularity**: Add or remove functionality without modifying core code * **Reusability**: Share plugins across different agents * **Type Safety**: Full TypeScript support for robust development * **Flexibility**: Support for various plugin types (platform, LLM, DeFi, etc.) ### 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 ## 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. This is the recommended approach for most developers. ### 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: ``` 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. ### Using Your Plugin in Projects Plugins don't necessarily need to be in the ElizaOS monorepo. You have two options for using your plugin in a project: #### Option 1: Plugin Inside the Monorepo If you're developing your plugin within the ElizaOS monorepo (in the `packages/` directory), you need to add it as a workspace dependency: 1. Add your plugin to the root `package.json` as a workspace dependency: ```json { "dependencies": { "@elizaos/plugin-knowledge": "workspace:*", "@yourorg/plugin-myplugin": "workspace:*" } } ``` 2. Run `bun install` in the root directory to link the workspace dependency 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 If you're creating a plugin outside of the ElizaOS monorepo (recommended for most users), use `bun link`: 1. In your plugin directory, build and link it: ```bash # In your plugin directory (e.g., plugin-myplugin/) bun install bun run build bun link ``` 2. In your project directory (e.g., using project-starter), link the plugin: ```bash # In your project directory cd packages/project-starter # or wherever your agent project is bun link @yourorg/plugin-myplugin ``` 3. Add the plugin to your project's `package.json` dependencies: ```json { "dependencies": { "@yourorg/plugin-myplugin": "link:@yourorg/plugin-myplugin" } } ``` 4. Use the plugin in your project: ```typescript import { myPlugin } from '@yourorg/plugin-myplugin'; const agent = { name: 'MyAgent', plugins: [myPlugin], }; ``` When using `bun link`, remember to rebuild your plugin (`bun run build`) after making changes for them to be reflected in your project. ## Plugin Architecture Overview ### Plugin Interface Every plugin must implement the core `Plugin` interface: ```typescript import type { Plugin } from '@elizaos/core'; export interface Plugin { name: string; description: string; // Initialize plugin with runtime services init?: (config: Record, runtime: IAgentRuntime) => Promise; // Configuration config?: { [key: string]: any }; // Services - Note: This is (typeof Service)[] not Service[] services?: (typeof Service)[]; // Entity component definitions componentTypes?: { name: string; schema: Record; validator?: (data: any) => boolean; }[]; // Optional plugin features actions?: Action[]; providers?: Provider[]; evaluators?: Evaluator[]; adapter?: IDatabaseAdapter; models?: { [key: string]: (...args: any[]) => Promise; }; events?: PluginEvents; routes?: Route[]; tests?: TestSuite[]; // Dependencies dependencies?: string[]; testDependencies?: string[]; // Plugin priority (higher priority plugins are loaded first) priority?: number; // Schema for validation schema?: any; } ``` ### Directory Structure Standard plugin structure: ``` packages/plugin-/ ├── src/ │ ├── index.ts # Plugin manifest and exports │ ├── service.ts # Main service implementation │ ├── actions/ # Agent capabilities │ │ └── *.ts │ ├── providers/ # Context providers │ │ └── *.ts │ ├── evaluators/ # Post-processing │ │ └── *.ts │ ├── handlers/ # LLM model handlers │ │ └── *.ts │ ├── types/ # TypeScript definitions │ │ └── index.ts │ ├── constants/ # Configuration constants │ │ └── index.ts │ ├── utils/ # Helper functions │ │ └── *.ts │ └── tests.ts # Test suite ├── __tests__/ # Unit tests ├── package.json ├── tsconfig.json ├── tsup.config.ts └── README.md ``` ## Core Plugin Components ### 1. Services Services manage stateful connections and provide core functionality. They are singleton instances that persist throughout the agent's lifecycle. ```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 Services have a specific lifecycle that plugins must respect: #### 1. 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; } } ``` ### 2. Actions Actions are the heart of what your agent can DO. They're intelligent, context-aware operations that can make decisions, interact with services, and chain together for complex workflows. #### Quick Reference | Component | Purpose | Required | Returns | | ------------- | ------------------------------------ | -------- | ----------------------- | | `name` | Unique identifier | ✅ | - | | `description` | Help LLM understand when to use | ✅ | - | | `similes` | Alternative names for fuzzy matching | ❌ | - | | `validate` | Check if action can run | ✅ | `Promise` | | `handler` | Execute the action logic | ✅ | `Promise` | | `examples` | Teach LLM through scenarios | ✅ | - | **Key Concepts:** * Actions MUST return `ActionResult` with `success` field * Actions receive composed state from providers * Actions can chain together, passing values through state * Actions can use callbacks for intermediate responses * Actions are selected by the LLM based on validation and examples #### Action Anatomy ```typescript import { type Action, type ActionExample, type ActionResult, type HandlerCallback, type IAgentRuntime, type Memory, type State, ModelType, composePromptFromState, logger, } from '@elizaos/core'; export const myAction: Action = { name: 'MY_ACTION', description: 'Clear, concise description for the LLM to understand when to use this', // Similes help with fuzzy matching - be creative! similes: ['SIMILAR_ACTION', 'ANOTHER_NAME', 'CASUAL_REFERENCE'], // Validation: Can this action run in the current context? validate: async (runtime: IAgentRuntime, message: Memory, state?: State): Promise => { // Check permissions, settings, current state, etc. const hasPermission = await checkUserPermissions(runtime, message); const serviceAvailable = runtime.getService('my-service') !== null; return hasPermission && serviceAvailable; }, // Handler: The brain of your action handler: async ( runtime: IAgentRuntime, message: Memory, state?: State, options?: { [key: string]: unknown }, callback?: HandlerCallback, responses?: Memory[] ): Promise => { // ALWAYS return ActionResult with success field! try { // Access previous action results from multi-step chains const context = options?.context; const previousResults = context?.previousResults || []; // Get your state (providers have already run) if (!state) { state = await runtime.composeState(message, [ 'RECENT_MESSAGES', 'CHARACTER', 'ACTION_STATE', // Includes previous action results ]); } // Your action logic here const result = await doSomethingAmazing(); // Use callback for intermediate responses if (callback) { await callback({ text: `Working on it...`, actions: ['MY_ACTION'], }); } // Return structured result return { success: true, // REQUIRED field text: `Action completed: ${result.summary}`, values: { // These merge into state for next actions lastActionTime: Date.now(), resultData: result.data, }, data: { // Raw data for logging/debugging actionName: 'MY_ACTION', fullResult: result, }, }; } catch (error) { logger.error('Action failed:', error); return { success: false, // REQUIRED field text: 'Failed to complete action', error: error instanceof Error ? error : new Error(String(error)), data: { actionName: 'MY_ACTION', errorDetails: error.message, }, }; } }, // Examples: Teach the LLM through scenarios examples: [ [ { name: '{{user1}}', content: { text: 'Can you do the thing?' }, }, { name: '{{agent}}', content: { text: "I'll do that for you right away!", actions: ['MY_ACTION'], }, }, ], ] as ActionExample[][], }; ``` #### Real-World Action Patterns ##### 1. 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: true, text: 'Continuing to participate', values: { roomMuted: false }, }; }, }; ``` ##### 2. Multi-Target Actions Actions can handle complex targeting and routing: ```typescript export const sendMessageAction: Action = { name: 'SEND_MESSAGE', similes: ['DM', 'MESSAGE', 'PING', 'TELL', 'NOTIFY'], description: 'Send a message to a user or room on any platform', handler: async (runtime, message, state, options, callback) => { // Extract target using LLM const targetPrompt = composePromptFromState({ state, template: `Extract message target from: {{recentMessages}} Return JSON: { "targetType": "user|room", "source": "platform", "identifiers": { "username": "...", "roomName": "..." } }`, }); const targetData = await runtime.useModel(ModelType.OBJECT_LARGE, { prompt: targetPrompt, schema: targetSchema, }); // Route to appropriate service if (targetData.targetType === 'user') { const user = await findEntityByName(runtime, message, state); const service = runtime.getService(targetData.source); await service.sendDirectMessage(runtime, user.id, message.content.text); return { success: true, text: `Message sent to ${user.names[0]}`, values: { messageSent: true, targetUser: user.names[0] }, }; } // Handle room messages similarly... }, }; ``` ##### 3. Stateful Reply Action The simplest yet most important action - generating contextual responses: ```typescript export const replyAction: Action = { name: 'REPLY', similes: ['RESPOND', 'ANSWER', 'SPEAK', 'SAY'], description: 'Generate and send a response to the conversation', validate: async () => true, // Always valid handler: async (runtime, message, state, options, callback) => { // Access chain context const previousActions = options?.context?.previousResults || []; // Include dynamic providers from previous actions const dynamicProviders = responses?.flatMap((r) => r.content?.providers ?? []) ?? []; // Compose state with action results state = await runtime.composeState(message, [ ...dynamicProviders, 'RECENT_MESSAGES', 'ACTION_STATE', // Includes previous action results ]); const replyTemplate = `# Generate response as {{agentName}} {{providers}} Previous actions taken: {{actionResults}} Generate thoughtful response considering the context and any actions performed. \`\`\`json { "thought": "reasoning about response", "message": "the actual response" } \`\`\``; const response = await runtime.useModel(ModelType.OBJECT_LARGE, { prompt: composePromptFromState({ state, template: replyTemplate }), }); await callback({ text: response.message, thought: response.thought, actions: ['REPLY'], }); return { success: true, text: 'Reply sent', values: { lastReply: response.message, thoughtProcess: response.thought, }, }; }, }; ``` ##### 4. Permission-Based Actions Actions that modify system state with permission checks: ```typescript export const updateRoleAction: Action = { name: 'UPDATE_ROLE', similes: ['MAKE_ADMIN', 'CHANGE_PERMISSIONS', 'PROMOTE', 'DEMOTE'], description: 'Update user roles with permission validation', validate: async (runtime, message) => { // Only in group contexts with server ID return message.content.channelType === ChannelType.GROUP && !!message.content.serverId; }, handler: async (runtime, message, state) => { // Get requester's role const world = await runtime.getWorld(worldId); const requesterRole = world.metadata?.roles[message.entityId] || Role.NONE; // Extract role changes using LLM const changes = await runtime.useModel(ModelType.OBJECT_LARGE, { prompt: 'Extract role assignments from: ' + state.text, schema: { type: 'array', items: { type: 'object', properties: { entityId: { type: 'string' }, newRole: { type: 'string', enum: ['OWNER', 'ADMIN', 'NONE'], }, }, }, }, }); // Validate each change const results = []; for (const change of changes) { if (canModifyRole(requesterRole, currentRole, change.newRole)) { world.metadata.roles[change.entityId] = change.newRole; results.push({ success: true, ...change }); } else { results.push({ success: false, reason: 'Insufficient permissions', ...change, }); } } await runtime.updateWorld(world); return { success: results.some((r) => r.success), text: `Updated ${results.filter((r) => r.success).length} roles`, data: { results }, }; }, }; ``` #### Action Best Practices 1. **Always Return ActionResult** ```typescript // ❌ Old style - DON'T DO THIS return true; // ✅ New style - ALWAYS DO THIS return { success: true, text: 'Action completed', values: { /* state updates */ }, data: { /* raw data */ }, }; ``` 2. **Use Callbacks for User Feedback** ```typescript // Acknowledge immediately await callback?.({ text: "I'm working on that...", actions: ['MY_ACTION'], }); // Do the work const result = await longRunningOperation(); // Final response await callback?.({ text: `Done! ${result.summary}`, actions: ['MY_ACTION_COMPLETE'], }); ``` 3. **Chain Actions with Context** ```typescript // Access previous results const previousResults = options?.context?.previousResults || []; const lastResult = previousResults[previousResults.length - 1]; if (lastResult?.data?.needsFollowUp) { // Continue the chain } ``` 4. **Validate Thoughtfully** ```typescript validate: async (runtime, message, state) => { // Check multiple conditions const hasPermission = await checkPermissions(runtime, message); const hasRequiredService = !!runtime.getService('required-service'); const isRightContext = message.content.channelType === ChannelType.GROUP; return hasPermission && hasRequiredService && isRightContext; }; ``` 5. **Write Teaching Examples** ```typescript examples: [ // Show the happy path [ { name: '{{user}}', content: { text: 'Please do X' } }, { name: '{{agent}}', content: { text: 'Doing X now!', actions: ['DO_X'], }, }, ], // Show edge cases [ { name: '{{user}}', content: { text: 'Do X without permission' } }, { name: '{{agent}}', content: { text: "I don't have permission for that", actions: ['REPLY'], }, }, ], // Show the action being ignored when not relevant [ { name: '{{user}}', content: { text: 'Unrelated conversation' } }, { name: '{{agent}}', content: { text: 'Responding normally', actions: ['REPLY'], }, }, ], ]; ``` #### Understanding ActionResult The `ActionResult` interface is crucial for action interoperability: ```typescript interface ActionResult { // REQUIRED: Indicates if the action succeeded success: boolean; // Optional: User-facing message about what happened text?: string; // Optional: Values to merge into state for subsequent actions values?: Record; // Optional: Raw data for logging/debugging data?: Record; // Optional: Error information if action failed error?: string | Error; } ``` **Why ActionResult Matters:** 1. **State Propagation**: Values from one action flow to the next 2. **Error Handling**: Consistent error reporting across all actions 3. **Logging**: Structured data for debugging and analytics 4. **Action Chaining**: Success/failure determines flow control #### Action Execution Lifecycle ```typescript // 1. Provider phase - gather context const state = await runtime.composeState(message, ['ACTIONS']); // 2. Action selection - LLM chooses based on available actions const validActions = await getValidActions(runtime, message, state); // 3. Action execution - may include multiple actions await runtime.processActions(message, responses, state, callback); // 4. State accumulation - each action's values merge into state // Action 1 returns: { values: { step1Complete: true } } // Action 2 receives: state.values.step1Complete === true // 5. Evaluator phase - post-processing await runtime.evaluate(message, state, true, callback, responses); ``` #### Advanced Action Techniques ##### Dynamic Action Registration ```typescript // Actions can register other actions dynamically export const pluginLoaderAction: Action = { name: 'LOAD_PLUGIN', handler: async (runtime, message, state) => { const pluginName = extractPluginName(state); const plugin = await import(pluginName); // Register new actions from the loaded plugin if (plugin.actions) { for (const action of plugin.actions) { runtime.registerAction(action); } } return { success: true, text: `Loaded ${plugin.actions.length} new actions`, values: { loadedPlugin: pluginName, newActions: plugin.actions.map((a) => a.name), }, }; }, }; ``` ##### Conditional Action Chains ```typescript export const conditionalWorkflow: Action = { name: 'SMART_WORKFLOW', handler: async (runtime, message, state, options, callback) => { // Step 1: Analyze const analysis = await analyzeRequest(state); if (analysis.requiresApproval) { // Trigger approval action await callback({ text: 'This requires approval', actions: ['REQUEST_APPROVAL'], }); return { success: true, values: { workflowPaused: true, pendingApproval: true, }, }; } // Step 2: Execute if (analysis.complexity === 'simple') { return await executeSimpleTask(runtime, analysis); } else { // Trigger complex workflow await callback({ text: 'Starting complex workflow', actions: ['COMPLEX_WORKFLOW'], }); return { success: true, values: { workflowType: 'complex', analysisData: analysis, }, }; } }, }; ``` ##### Action Composition ```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, }, }; }, }; ``` ##### Self-Modifying Actions ```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, }, }; }, }; ``` #### Testing Actions Actions should be thoroughly tested to ensure they behave correctly in various scenarios: ```typescript // __tests__/myAction.test.ts import { describe, it, expect, beforeEach } from 'bun:test'; import { myAction } from '../src/actions/myAction'; import { createMockRuntime } from '@elizaos/test-utils'; import { ActionResult, Memory, State } from '@elizaos/core'; describe('MyAction', () => { let mockRuntime: any; let mockMessage: Memory; let mockState: State; beforeEach(() => { mockRuntime = createMockRuntime({ settings: { MY_API_KEY: 'test-key' }, }); mockMessage = { id: 'test-id', entityId: 'user-123', roomId: 'room-456', content: { text: 'Do the thing' }, }; mockState = { values: { recentMessages: 'test context' }, data: { room: { name: 'Test Room' } }, text: 'State text', }; }); 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 = () => 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 = jest.fn(); 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 = () => { 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'); } } }); }); }); ``` ##### E2E Testing Actions For integration testing with a live runtime: ```typescript // tests/e2e/myAction.e2e.ts export const myActionE2ETests = { name: 'MyAction E2E Tests', tests: [ { name: 'should execute full action 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); }, }, ], }; ``` ### 3. Providers Providers supply contextual information to the agent's state before it makes decisions. They act as the agent's "senses", gathering relevant data that helps the LLM understand the current context. ```typescript import { Provider, ProviderResult, IAgentRuntime, Memory, State, addHeader } from '@elizaos/core'; export const myProvider: Provider = { name: 'myProvider', description: 'Provides contextual information about X', // Optional: Set to true if this provider should only run when explicitly requested dynamic: false, // Optional: Control execution order (lower numbers run first, can be negative) position: 100, // Optional: Set to true to exclude from default provider list private: false, get: async (runtime: IAgentRuntime, message: Memory, state: State): Promise => { try { const service = runtime.getService('my-service') as MyService; const data = await service.getCurrentData(); // Format data for LLM context const formattedText = addHeader( '# Current System Status', `Field 1: ${data.field1} Field 2: ${data.field2} Last updated: ${new Date(data.timestamp).toLocaleString()}` ); return { // Text that will be included in the LLM prompt text: formattedText, // Values that can be accessed by other providers/actions values: { currentField1: data.field1, currentField2: data.field2, lastUpdate: data.timestamp, }, // Raw data for internal use data: { raw: data, processed: true, }, }; } catch (error) { return { text: 'Unable to retrieve current status', values: {}, data: { error: error.message }, }; } }, }; ``` #### Provider Properties * **`name`** (required): Unique identifier for the provider * **`description`** (optional): Human-readable description of what the provider does * **`dynamic`** (optional, default: false): If true, the provider is not included in default state composition and must be explicitly requested * **`position`** (optional, default: 0): Controls execution order. Lower numbers execute first. Can be negative for early execution * **`private`** (optional, default: false): If true, the provider is excluded from regular provider lists and must be explicitly included #### 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 #### Common Provider Patterns **Recent Messages Provider (position: 100)** ```typescript export const recentMessagesProvider: Provider = { name: 'RECENT_MESSAGES', description: 'Recent messages, interactions and other memories', position: 100, // Runs after most other providers get: async (runtime, message) => { const messages = await runtime.getMemories({ roomId: message.roomId, count: runtime.getConversationLength(), unique: false, }); const formattedMessages = formatMessages(messages); return { text: addHeader('# Conversation Messages', formattedMessages), values: { recentMessages: formattedMessages }, data: { messages }, }; }, }; ``` **Actions Provider (position: -1)** ```typescript export const actionsProvider: Provider = { name: 'ACTIONS', description: 'Possible response actions', position: -1, // Runs early to inform other providers get: async (runtime, message, state) => { // Get all valid actions for this context const validActions = await Promise.all( runtime.actions.map(async (action) => { const isValid = await action.validate(runtime, message, state); return isValid ? action : null; }) ); const actions = validActions.filter(Boolean); const actionNames = formatActionNames(actions); return { text: `Possible response actions: ${actionNames}`, values: { actionNames }, data: { actionsData: actions }, }; }, }; ``` **Dynamic Knowledge Provider** ```typescript export const knowledgeProvider: Provider = { name: 'KNOWLEDGE', description: 'Knowledge from the knowledge base', dynamic: true, // Only runs when explicitly requested get: async (runtime, message) => { const knowledgeService = runtime.getService('knowledge'); const relevantKnowledge = await knowledgeService.search(message.content.text); if (!relevantKnowledge.length) { return { text: '', values: {}, data: {} }; } return { text: addHeader('# Relevant Knowledge', formatKnowledge(relevantKnowledge)), values: { knowledgeUsed: true }, data: { knowledge: relevantKnowledge }, }; }, }; ``` ### 4. Evaluators Evaluators run after the agent generates a response, allowing for analysis, learning, and side effects. They use the same handler pattern as actions but run post-response. ```typescript import { Evaluator, IAgentRuntime, Memory, State, HandlerCallback } from '@elizaos/core'; export const myEvaluator: Evaluator = { name: 'myEvaluator', description: 'Analyzes responses for quality and extracts insights', // Examples help the LLM understand when to use this evaluator examples: [ { prompt: 'User asks about product pricing', messages: [ { name: 'user', content: { text: 'How much does it cost?' } }, { name: 'assistant', content: { text: 'The price is $99' } }, ], outcome: 'Extract pricing information for future reference', }, ], // Similar descriptions for fuzzy matching similes: ['RESPONSE_ANALYZER', 'QUALITY_CHECK'], // Optional: Run even if the agent didn't respond alwaysRun: false, // Validation: Determines if evaluator should run validate: async (runtime: IAgentRuntime, message: Memory, state?: State): Promise => { // Example: Only run for certain types of responses return message.content?.text?.includes('transaction') || false; }, // Handler: Main evaluation logic handler: async ( runtime: IAgentRuntime, message: Memory, state?: State, options?: { [key: string]: unknown }, callback?: HandlerCallback, responses?: Memory[] ): Promise => { try { // Analyze the response const responseText = responses?.[0]?.content?.text || ''; if (responseText.includes('transaction')) { // Extract and store transaction data const txHash = extractTransactionHash(responseText); if (txHash) { // Store for future reference await runtime.createMemory( { id: generateId(), entityId: message.entityId, roomId: message.roomId, content: { text: `Transaction processed: ${txHash}`, type: 'transaction_record', data: { txHash, timestamp: Date.now() }, }, }, 'facts' ); // Log the evaluation await runtime.adapter.log({ entityId: message.entityId, roomId: message.roomId, type: 'evaluator', body: { evaluator: 'myEvaluator', result: 'transaction_extracted', txHash, }, }); } } // Can also trigger follow-up actions via callback if (callback) { callback({ text: 'Analysis complete', content: { analyzed: true }, }); } } catch (error) { runtime.logger.error('Evaluator error:', error); } }, }; ``` #### Common Evaluator Patterns **Fact Extraction Evaluator** ```typescript export const factExtractor: Evaluator = { name: 'FACT_EXTRACTOR', description: 'Extracts facts from conversations for long-term memory', alwaysRun: true, // Run after every response validate: async () => true, // Always valid handler: async (runtime, message, state, options, callback, responses) => { const facts = await extractFactsFromConversation(runtime, message, responses); for (const fact of facts) { await runtime.createMemory( { id: generateId(), entityId: message.entityId, roomId: message.roomId, content: { text: fact.statement, type: 'fact', confidence: fact.confidence, }, }, 'facts', true ); // unique = true to avoid duplicates } }, }; ``` **Response Quality Evaluator** ```typescript export const qualityEvaluator: Evaluator = { name: 'QUALITY_CHECK', description: 'Evaluates response quality and coherence', validate: async (runtime, message) => { // Only evaluate responses to direct questions return message.content?.text?.includes('?') || false; }, handler: async (runtime, message, state, options, callback, responses) => { const quality = await assessResponseQuality(responses[0]); if (quality.score < 0.7) { // Log low quality response for review await runtime.adapter.log({ entityId: message.entityId, roomId: message.roomId, type: 'quality_alert', body: { score: quality.score, issues: quality.issues, responseId: responses[0].id, }, }); } }, }; ``` ### Understanding the Agent Lifecycle: Providers, Actions, and Evaluators When an agent receives a message, components execute in this order: 1. **Providers** gather context by calling `runtime.composeState()` * Non-private, non-dynamic providers run automatically * Sorted by position (lower numbers first) * Results aggregated into state object 2. **Actions** are validated and presented to the LLM * The actions provider lists available actions * LLM decides which actions to execute * Actions execute with the composed state 3. **Evaluators** run after response generation * Process the response for insights * Can store memories, log events, or trigger follow-ups * Use `alwaysRun: true` to run even without a response ```typescript // Example flow in pseudocode async function processMessage(message: Memory) { // 1. Compose state with providers const state = await runtime.composeState(message, ['RECENT_MESSAGES', 'CHARACTER', 'ACTIONS']); // 2. Generate response using LLM with composed state const response = await generateResponse(state); // 3. Execute any actions the LLM chose if (response.actions?.length > 0) { await runtime.processActions(message, [response], state); } // 4. Run evaluators on the response await runtime.evaluate(message, state, true, callback, [response]); } ``` ### Accessing Provider Data in Actions Actions receive the composed state containing all provider data. Here's how to access it: ```typescript export const myAction: Action = { name: 'MY_ACTION', handler: async (runtime, message, state, options, callback) => { // Access provider values const recentMessages = state.values?.recentMessages; const actionNames = state.values?.actionNames; // Access raw provider data const providerData = state.data?.providers; if (providerData) { // Get specific provider's data const knowledgeData = providerData['KNOWLEDGE']?.data; const characterData = providerData['CHARACTER']?.data; } // Access action execution context const previousResults = state.data?.actionResults || []; const actionPlan = state.data?.actionPlan; // Use the data to make decisions if (state.values?.knowledgeUsed) { // Knowledge was found, incorporate it } }, }; ``` ### Custom Provider Inclusion To include specific providers when composing state: ```typescript // Include only specific providers const state = await runtime.composeState(message, ['CHARACTER', 'KNOWLEDGE'], true); // Add extra providers to the default set const state = await runtime.composeState(message, ['KNOWLEDGE', 'CUSTOM_PROVIDER']); // Skip cache and regenerate const state = await runtime.composeState(message, null, false, true); ``` ### 5. Model Handlers (LLM Plugins) For LLM plugins, implement model handlers for different model types: ```typescript import { ModelType, GenerateTextParams, EventType } from '@elizaos/core'; export const models = { [ModelType.TEXT_SMALL]: async ( runtime: IAgentRuntime, params: GenerateTextParams ): Promise => { const client = createClient(runtime); const { text, usage } = await client.generateText({ model: getSmallModel(runtime), prompt: params.prompt, temperature: params.temperature ?? 0.7, maxTokens: params.maxTokens ?? 4096, }); // Emit usage event runtime.emitEvent(EventType.MODEL_USED, { provider: 'my-llm', type: ModelType.TEXT_SMALL, tokens: usage, }); return text; }, [ModelType.TEXT_EMBEDDING]: async ( runtime: IAgentRuntime, params: TextEmbeddingParams | string | null ): Promise => { if (params === null) { // Return test embedding return Array(1536).fill(0); } const text = typeof params === 'string' ? params : params.text; const embedding = await client.createEmbedding(text); return embedding; }, }; ``` ### 6. HTTP Routes and API Endpoints Plugins can expose HTTP endpoints for webhooks, APIs, or web interfaces. Routes are defined using the `Route` type and exported as part of the plugin: ```typescript import { Plugin, Route, IAgentRuntime } from '@elizaos/core'; // Define route handlers async function statusHandler(req: any, res: any, runtime: IAgentRuntime) { try { const service = runtime.getService('my-service') as MyService; const status = await service.getStatus(); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end( JSON.stringify({ success: true, data: status, timestamp: new Date().toISOString(), }) ); } catch (error) { res.writeHead(500, { 'Content-Type': 'application/json' }); res.end( JSON.stringify({ success: false, error: error.message, }) ); } } // Export routes array export const myPluginRoutes: Route[] = [ { type: 'GET', path: '/api/status', handler: statusHandler, public: true, // Makes this route discoverable in UI name: 'API Status', // Display name for UI tab }, { type: 'POST', path: '/api/webhook', handler: webhookHandler, }, ]; // Include routes in plugin definition export const myPlugin: Plugin = { name: 'my-plugin', description: 'My plugin with HTTP routes', services: [MyService], routes: myPluginRoutes, // Add routes here }; ``` #### Response Helpers Create consistent API responses: ```typescript // Helper functions for standardized responses function sendSuccess(res: any, data: any, status = 200) { res.writeHead(status, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: true, data })); } function sendError(res: any, status: number, message: string, details?: any) { res.writeHead(status, { 'Content-Type': 'application/json' }); res.end( JSON.stringify({ success: false, error: { message, details }, }) ); } // Use in handlers async function apiHandler(req: any, res: any, runtime: IAgentRuntime) { try { const result = await processRequest(req.body); sendSuccess(res, result); } catch (error) { sendError(res, 500, 'Processing failed', error.message); } } ``` #### Authentication Patterns Implement authentication in route handlers: ```typescript async function authenticatedHandler(req: any, res: any, runtime: IAgentRuntime) { // Check API key from headers const apiKey = req.headers['x-api-key']; const validKey = runtime.getSetting('API_KEY'); if (!apiKey || apiKey !== validKey) { sendError(res, 401, 'Unauthorized'); return; } // Process authenticated request const data = await processAuthenticatedRequest(req); sendSuccess(res, data); } ``` #### File Upload Support Handle multipart form data: ```typescript // The server automatically applies multer middleware for multipart routes export const uploadRoute: Route = { type: 'POST', path: '/api/upload', handler: async (req: any, res: any, runtime: IAgentRuntime) => { // Access uploaded files via req.files const files = req.files as Express.Multer.File[]; if (!files || files.length === 0) { sendError(res, 400, 'No files uploaded'); return; } // Process uploaded files for (const file of files) { await processFile(file.buffer, file.originalname); } sendSuccess(res, { processed: files.length }); }, isMultipart: true, // Enable multipart handling }; ``` ## Advanced: Creating Plugins Manually For developers working within the ElizaOS monorepo or those who need complete control over their plugin structure, you can create plugins manually. This approach is useful when: * Contributing directly to the ElizaOS monorepo * Creating highly customized plugin structures * Integrating with existing codebases * Learning the internals of plugin architecture ### Step 1: Set Up the Project ```bash # Create plugin directory mkdir packages/plugin-myplugin cd packages/plugin-myplugin # Initialize package.json bun init -y # Install dependencies bun add @elizaos/core zod bun add -d typescript tsup @types/node @types/bun # Create directory structure mkdir -p src/{actions,providers,types,constants} ``` ### Step 2: Configure package.json ```json { "name": "@yourorg/plugin-myplugin", "version": "1.0.0", "type": "module", "main": "dist/index.js", "types": "dist/index.d.ts", "exports": { ".": { "import": { "types": "./dist/index.d.ts", "default": "./dist/index.js" } } }, "files": ["dist"], "scripts": { "build": "tsup", "dev": "tsup --watch", "test": "bun test", "test:watch": "bun test --watch", "test:coverage": "bun test --coverage", "lint": "eslint ./src --ext .ts", "format": "prettier --write ./src" }, "dependencies": { "@elizaos/core": "^1.0.0", "zod": "^3.24.2" }, "devDependencies": { "typescript": "^5.8.3", "tsup": "^8.4.0", "@types/bun": "^1.2.16", "@types/node": "^22.15.3" }, "agentConfig": { "pluginType": "elizaos:plugin:1.0.0", "pluginParameters": { "MY_API_KEY": { "type": "string", "description": "API key for MyPlugin", "required": true, "sensitive": true } } } } ``` ### Step 3: Create TypeScript Configuration ```typescript // tsconfig.json { "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "./dist", "rootDir": "./src" }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"] } ``` ```typescript // tsup.config.ts import { defineConfig } from 'tsup'; export default defineConfig({ entry: ['src/index.ts'], format: ['esm'], dts: true, splitting: false, sourcemap: true, clean: true, external: ['@elizaos/core'], }); ``` ```toml # bunfig.toml [install] # Optional: Configure registry # registry = "https://registry.npmjs.org" # Optional: Save exact versions save-exact = true # Optional: Configure trusted dependencies for postinstall scripts # trustedDependencies = ["package-name"] ``` ### Step 4: Implement the Plugin ```typescript // src/index.ts import type { Plugin } from '@elizaos/core'; import { MyService } from './service'; import { myAction } from './actions/myAction'; import { myProvider } from './providers/myProvider'; export const myPlugin: Plugin = { name: 'myplugin', description: 'My custom plugin for ElizaOS', services: [MyService], // Pass the class constructor, not an instance actions: [myAction], providers: [myProvider], routes: myPluginRoutes, // Optional: HTTP endpoints init: async (config: Record, runtime: IAgentRuntime) => { // Optional initialization logic logger.info('MyPlugin initialized'); }, }; export default myPlugin; ``` ## Plugin Types and Patterns ### Plugin Dependencies and Priority Plugins can declare dependencies on other plugins and control their loading order: ```typescript export const myPlugin: Plugin = { name: 'my-plugin', description: 'Plugin that depends on other plugins', // Required dependencies - plugin won't load without these dependencies: ['plugin-sql', 'plugin-bootstrap'], // Optional test dependencies testDependencies: ['plugin-test-utils'], // Higher priority = loads earlier (default: 0) priority: 100, async init(config, runtime) { // Dependencies are guaranteed to be loaded const sqlService = runtime.getService('sql'); if (!sqlService) { throw new Error('SQL service not found despite dependency'); } }, }; ``` #### Checking for Optional Dependencies ```typescript async init(config, runtime) { // Check if optional plugin is available const hasKnowledgePlugin = runtime.getService('knowledge') !== null; if (hasKnowledgePlugin) { logger.info('Knowledge plugin detected, enabling enhanced features'); this.enableKnowledgeIntegration = true; } } ``` ### Platform Plugins Platform plugins connect agents to communication platforms: ```typescript // Key patterns for platform plugins: // 1. Entity mapping (server → world, channel → room, user → entity) // 2. Message conversion // 3. Event handling // 4. Rate limiting export class PlatformService extends Service { private client: PlatformClient; private messageManager: MessageManager; async handleIncomingMessage(platformMessage: any) { // 1. Sync entities const { worldId, roomId, userId } = await this.syncEntities(platformMessage); // 2. Convert to Memory const memory = await this.messageManager.convertToMemory(platformMessage, roomId, userId); // 3. Process through runtime await this.runtime.processMemory(memory); // 4. Emit events await this.runtime.emit(EventType.MESSAGE_RECEIVED, memory); } } ``` ### LLM Plugins LLM plugins integrate different AI model providers: ```typescript // Key patterns for LLM plugins: // 1. Model type handlers // 2. Configuration management // 3. Usage tracking // 4. Error handling export const llmPlugin: Plugin = { name: 'my-llm', models: { [ModelType.TEXT_LARGE]: async (runtime, params) => { const client = createClient(runtime); const response = await client.generate(params); // Track usage runtime.emitEvent(EventType.MODEL_USED, { provider: 'my-llm', tokens: response.usage, }); return response.text; }, }, }; ``` ### Event Handling Plugins can register event handlers to react to system events: ```typescript import { EventType, EventHandler } from '@elizaos/core'; export const myPlugin: Plugin = { name: 'my-plugin', events: { // Handle message events [EventType.MESSAGE_CREATED]: [ async (params) => { logger.info('New message created:', params.message.id); // React to new messages }, ], // Handle custom events 'custom:data-sync': [ async (params) => { await syncDataWithExternal(params); }, ], }, async init(config, runtime) { // Emit custom events runtime.emitEvent('custom:data-sync', { timestamp: Date.now(), source: 'my-plugin', }); }, }; ``` ### DeFi Plugins DeFi plugins enable blockchain interactions: ```typescript // Key patterns for DeFi plugins: // 1. Wallet management // 2. Transaction handling // 3. Gas optimization // 4. Security validation export class DeFiService extends Service { private walletClient: WalletClient; private publicClient: PublicClient; async executeTransaction(params: TransactionParams) { // 1. Validate inputs validateAddress(params.to); validateAmount(params.amount); // 2. Estimate gas const gasLimit = await this.estimateGas(params); // 3. Execute with retry return await withRetry(() => this.walletClient.sendTransaction({ ...params, gasLimit, }) ); } } ``` ## Advanced Configuration ### Configuration Hierarchy ElizaOS follows a specific hierarchy for configuration resolution. Understanding this is critical for proper plugin behavior: ```typescript // Configuration resolution order (first found wins): // 1. Runtime settings (via runtime.getSetting()) // 2. Environment variables (process.env) // 3. Plugin config defaults // 4. Hardcoded defaults // IMPORTANT: During init(), runtime might not be fully available! export async function getConfigValue( key: string, runtime?: IAgentRuntime, defaultValue?: string ): Promise { // Try runtime first (if available) if (runtime) { const runtimeValue = runtime.getSetting(key); if (runtimeValue !== undefined) return runtimeValue; } // Fall back to environment const envValue = process.env[key]; if (envValue !== undefined) return envValue; // Use default return defaultValue; } ``` ### Init-time vs Runtime Configuration During plugin initialization, configuration access is different: ```typescript export const myPlugin: Plugin = { name: 'my-plugin', // Plugin-level config defaults config: { DEFAULT_TIMEOUT: 30000, RETRY_ATTEMPTS: 3, }, async init(config: Record, runtime?: IAgentRuntime) { // During init, runtime might not be available or fully initialized // Always check multiple sources: const apiKey = config.API_KEY || // From agent character config runtime?.getSetting('API_KEY') || // From runtime (may be undefined) process.env.API_KEY; // From environment if (!apiKey) { throw new Error('API_KEY required for my-plugin'); } // For boolean values, be careful with string parsing const isEnabled = config.FEATURE_ENABLED === 'true' || runtime?.getSetting('FEATURE_ENABLED') === 'true' || process.env.FEATURE_ENABLED === 'true'; }, }; ``` ### Configuration Validation Use Zod for runtime validation: ```typescript import { z } from 'zod'; export const configSchema = z.object({ API_KEY: z.string().min(1, 'API key is required'), ENDPOINT_URL: z.string().url().optional(), TIMEOUT: z.number().positive().default(30000), }); export async function validateConfig(runtime: IAgentRuntime) { const config = { API_KEY: runtime.getSetting('MY_API_KEY'), ENDPOINT_URL: runtime.getSetting('MY_ENDPOINT_URL'), TIMEOUT: Number(runtime.getSetting('MY_TIMEOUT') || 30000), }; return configSchema.parse(config); } ``` ### agentConfig in package.json Declare plugin parameters: ```json { "agentConfig": { "pluginType": "elizaos:plugin:1.0.0", "pluginParameters": { "MY_API_KEY": { "type": "string", "description": "API key for authentication", "required": true, "sensitive": true }, "MY_TIMEOUT": { "type": "number", "description": "Request timeout in milliseconds", "required": false, "default": 30000 } } } } ``` ## Testing Strategies Bun provides a built-in test runner that's fast and compatible with Jest-like syntax. No need for additional testing frameworks. ### Unit Tests Test individual components: ```typescript // __tests__/myAction.test.ts import { describe, it, expect, mock, beforeEach } from 'bun:test'; import { myAction } from '../src/actions/myAction'; import { createMockRuntime } from '@elizaos/test-utils'; describe('MyAction', () => { beforeEach(() => { // Reset all mocks before each test mock.restore(); }); it('should validate when configured', async () => { const mockRuntime = createMockRuntime({ settings: { MY_API_KEY: 'test-key', }, }); const isValid = await myAction.validate(mockRuntime); expect(isValid).toBe(true); }); it('should handle action execution', async () => { const mockRuntime = createMockRuntime(); const mockService = { executeAction: mock().mockResolvedValue({ success: true }), }; mockRuntime.getService = mock().mockReturnValue(mockService); const callback = mock(); const result = await myAction.handler(mockRuntime, mockMessage, mockState, {}, callback); expect(result).toBe(true); expect(callback).toHaveBeenCalledWith({ text: expect.stringContaining('Successfully'), content: expect.objectContaining({ success: true }), }); }); }); ``` ### Integration Tests Test component interactions: ```typescript describe('Plugin Integration', () => { let runtime: IAgentRuntime; let service: MyService; beforeAll(async () => { runtime = await createTestRuntime({ settings: { MY_API_KEY: process.env.TEST_API_KEY, }, }); service = await MyService.start(runtime); }); it('should handle complete flow', async () => { const message = createTestMessage('Execute my action'); const response = await runtime.processMessage(message); expect(response.success).toBe(true); }); }); ``` ### Test Suite Implementation Include tests in your plugin: ```typescript export class MyPluginTestSuite implements TestSuite { name = 'myplugin'; tests: Array<{ name: string; fn: (runtime: IAgentRuntime) => Promise }>; constructor() { this.tests = [ { name: 'Test initialization', fn: this.testInitialization.bind(this), }, { name: 'Test action execution', fn: this.testActionExecution.bind(this), }, ]; } private async testInitialization(runtime: IAgentRuntime): Promise { const service = runtime.getService('my-service') as MyService; if (!service) { throw new Error('Service not initialized'); } const isConnected = await service.isConnected(); if (!isConnected) { throw new Error('Failed to connect'); } } } ``` ## Security Best Practices ### 1. Credential Management ```typescript // Never hardcode credentials // ❌ BAD const apiKey = 'sk-1234...'; // ✅ GOOD const apiKey = runtime.getSetting('API_KEY'); if (!apiKey) { throw new Error('API_KEY not configured'); } ``` ### 2. Input Validation ```typescript function validateInput(input: any): ValidatedInput { // Validate types if (typeof input.address !== 'string') { throw new Error('Invalid address type'); } // Validate format if (!isValidAddress(input.address)) { throw new Error('Invalid address format'); } // Sanitize input const sanitized = { address: input.address.toLowerCase().trim(), amount: Math.abs(parseFloat(input.amount)), }; return sanitized; } ``` ### 3. Rate Limiting ```typescript class RateLimiter { private requests = new Map(); canExecute(userId: string, limit = 10, window = 60000): boolean { const now = Date.now(); const userRequests = this.requests.get(userId) || []; const recentRequests = userRequests.filter((time) => now - time < window); if (recentRequests.length >= limit) { return false; } recentRequests.push(now); this.requests.set(userId, recentRequests); return true; } } ``` ### 4. Error Handling ```typescript export class PluginError extends Error { constructor(message: string, public code: string, public details?: any) { super(message); this.name = 'PluginError'; } } async function handleOperation(operation: () => Promise, context: string): Promise { try { return await operation(); } catch (error) { logger.error(`Error in ${context}:`, error); if (error.code === 'NETWORK_ERROR') { throw new PluginError('Network connection failed', 'NETWORK_ERROR', { context }); } throw new PluginError('An unexpected error occurred', 'UNKNOWN_ERROR', { context, originalError: error, }); } } ``` ## Publishing and Distribution For detailed information on publishing your plugin to npm and the ElizaOS registry, see our [Plugin Publishing Guide](/guides/plugin-publishing-guide). ### Quick Reference ```bash # Test your plugin elizaos test # Dry run to verify everything elizaos publish --test # Publish to npm and registry elizaos publish --npm ``` The Plugin Publishing Guide covers the complete process including: * Pre-publication validation * npm authentication * GitHub repository setup * Registry PR submission * Post-publication updates ## Reference Examples ### Minimal Plugin ```typescript // Minimal viable plugin import { Plugin } from '@elizaos/core'; export const minimalPlugin: Plugin = { name: 'minimal', description: 'A minimal plugin example', actions: [ { name: 'HELLO', description: 'Says hello', validate: async () => true, handler: async (runtime, message, state, options, callback) => { callback?.({ text: 'Hello from minimal plugin!' }); return true; }, examples: [], }, ], }; export default minimalPlugin; ``` ### Complete Templates * **Quick Start Templates**: Use `elizaos create` for instant plugin scaffolding with pre-configured TypeScript, build tools, and example components ## Common Patterns and Utilities ### Retry Logic ```typescript async function withRetry(fn: () => Promise, maxRetries = 3, delay = 1000): Promise { let lastError: Error; for (let i = 0; i < maxRetries; i++) { try { return await fn(); } catch (error) { lastError = error; if (i < maxRetries - 1) { await new Promise((resolve) => setTimeout(resolve, delay * (i + 1))); } } } throw lastError!; } ``` ## Troubleshooting ### Common Issues 1. **Plugin not loading**: Check that the plugin is properly exported and listed in the agent's plugins array 2. **Configuration errors**: Verify all required settings are provided 3. **Type errors**: Ensure @elizaos/core version matches other plugins 4. **Runtime errors**: Check service initialization and error handling ### Debug Tips ```typescript // Enable detailed logging import { elizaLogger } from '@elizaos/core'; elizaLogger.level = 'debug'; // Add debug logging elizaLogger.debug('Plugin state:', { service: !!runtime.getService('my-service'), settings: runtime.getSetting('MY_API_KEY') ? 'set' : 'missing', }); ``` ## Bun-Specific Tips When developing plugins with Bun: 1. **Fast Installation**: Bun's package installation is significantly faster than npm 2. **Built-in TypeScript**: No need for separate TypeScript compilation during development 3. **Native Test Runner**: Use `bun test` for running tests without additional setup (no vitest needed) 4. **Workspace Support**: Bun handles monorepo workspaces efficiently 5. **Lock File**: Bun uses `bun.lockb` (binary format) for faster dependency resolution ### Bun Test Features Bun's built-in test runner provides: * Jest-compatible API (`describe`, `it`, `expect`, `mock`) * Built-in mocking with `mock()` from 'bun:test' * Fast execution with no compilation step * Coverage reports with `--coverage` flag * Watch mode with `--watch` flag * Snapshot testing support ### Test File Conventions Bun automatically discovers test files matching these patterns: * `*.test.ts` or `*.test.js` * `*.spec.ts` or `*.spec.js` * Files in `__tests__/` directories * Files in `test/` or `tests/` directories ### Useful Bun Commands for Plugin Development ```bash # Install all dependencies bun install # Add a new dependency bun add # Add a dev dependency bun add -d # Run scripts bun run # Run tests bun test # Update dependencies bun update # Clean install (remove node_modules and reinstall) bun install --force ``` ## Best Practices Summary 1. **Architecture**: Follow the standard plugin structure 2. **Type Safety**: Use TypeScript strictly 3. **Error Handling**: Always handle errors gracefully 4. **Configuration**: Use runtime.getSetting() for all config 5. **Testing**: Write comprehensive unit and integration tests 6. **Security**: Never hardcode sensitive data 7. **Documentation**: Provide clear usage examples 8. **Performance**: Implement caching and rate limiting 9. **Logging**: Use appropriate log levels 10. **Versioning**: Follow semantic versioning ## Getting Help * **Documentation**: Check the main ElizaOS docs * **Examples**: Study existing plugins in `packages/` * **Community**: Join the ElizaOS Discord * **Issues**: Report bugs on GitHub Remember: The plugin system is designed to be flexible and extensible. When in doubt, look at existing plugins for patterns and inspiration. # Advanced Topics Source: https://eliza.how/guides/plugin-migration/advanced-migration-guide Advanced breaking changes for evaluators, services, and runtime methods > **Important**: This guide covers advanced breaking changes for evaluators, services, and runtime methods. Read the main [migration guide](./migration-guide) first for actions, providers, and basic migrations. ## Table of Contents * [Evaluators Migration](#evaluators-migration) * [Services & Clients Migration](#services--clients-migration) * [Runtime Method Changes](#runtime-method-changes) * [Entity System Migration](#entity-system-migration) *** ## 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 ); ``` *** ## Services & Clients Migration ### Service Registration Changes Services have undergone significant architectural changes: ```typescript // v0: Service extends abstract Service class export abstract class Service { static get serviceType(): ServiceType { throw new Error('Service must implement static serviceType getter'); } public static getInstance(): T { // Singleton pattern } abstract initialize(runtime: IAgentRuntime): Promise; } // v1: Service is now a class with static properties export class Service { static serviceType: ServiceTypeName; async initialize(runtime: IAgentRuntime): Promise { // Implementation } } ``` ### Migration Steps: 1. **Remove Singleton Pattern**: ```typescript // v0: Singleton getInstance class MyService extends Service { private static instance: MyService | null = null; public static getInstance(): MyService { if (!this.instance) { this.instance = new MyService(); } return this.instance; } } // v1: Direct instantiation class MyService extends Service { static serviceType = ServiceTypeName.MY_SERVICE; // No getInstance needed } ``` 2. **Update Service Registration**: ```typescript // v0: Register instance await runtime.registerService(MyService.getInstance()); // v1: Register class await runtime.registerService(MyService); ``` 3. **Service Type Enum Changes**: ```typescript // v0: ServiceType enum export enum ServiceType { IMAGE_DESCRIPTION = 'image_description', TRANSCRIPTION = 'transcription', // ... } // v1: ServiceTypeName (similar but may have new values) export enum ServiceTypeName { IMAGE_DESCRIPTION = 'image_description', TRANSCRIPTION = 'transcription', // Check for any renamed or new service types } ``` *** ## Runtime Method Changes ### 1. State Management The `updateRecentMessageState` method has been removed: ```typescript // v0: Separate method for updating state currentState = await runtime.updateRecentMessageState(currentState); // v1: Use composeState with specific keys currentState = await runtime.composeState(message, ['RECENT_MESSAGES']); ``` ### 2. Memory Manager Access Memory managers are no longer directly accessible: ```typescript // v0: Direct access to memory managers runtime.messageManager.getMemories({...}); runtime.registerMemoryManager(manager); const manager = runtime.getMemoryManager("messages"); // v1: Use database adapter methods await runtime.getMemories({ roomId, count, unique: false, tableName: "messages", agentId: runtime.agentId }); ``` ### 3. Model Usage Complete overhaul of model interaction: ```typescript // v0: generateText with ModelClass import { generateText, ModelClass } from '@elizaos/core'; const result = await generateText({ runtime, context: prompt, modelClass: ModelClass.SMALL, }); // v1: useModel with ModelTypeName const result = await runtime.useModel(ModelTypeName.TEXT_SMALL, { prompt, stopSequences: [], }); ``` ### 4. Settings Management #### Global Settings Object Removed The global `settings` object is no longer exported from `@elizaos/core`: ```typescript // v0: Import and use global settings import { settings } from '@elizaos/core'; const charityAddress = settings[networkKey]; const apiKey = settings.OPENAI_API_KEY; // v1: Use runtime.getSetting() // Remove the settings import import { elizaLogger, type IAgentRuntime } from '@elizaos/core'; const charityAddress = runtime.getSetting(networkKey); const apiKey = runtime.getSetting('OPENAI_API_KEY'); ``` #### New Settings Methods ```typescript // v0: Only getSetting through runtime const value = runtime.getSetting(key); // v1: Both get and set const value = runtime.getSetting(key); runtime.setSetting(key, value, isSecret); ``` #### Migration Example ```typescript // v0: utils.ts using global settings import { settings } from '@elizaos/core'; export function getCharityAddress(network: string): string | null { const networkKey = `CHARITY_ADDRESS_${network.toUpperCase()}`; const charityAddress = settings[networkKey]; return charityAddress; } // v1: Pass runtime to access settings export function getCharityAddress(runtime: IAgentRuntime, network: string): string | null { const networkKey = `CHARITY_ADDRESS_${network.toUpperCase()}`; const charityAddress = runtime.getSetting(networkKey); return charityAddress; } ``` #### Common Settings Migration Patterns 1. **Environment Variables**: Both v0 and v1 read from environment variables, but access patterns differ 2. **Dynamic Settings**: v1 allows runtime setting updates with `setSetting()` 3. **Secret Management**: v1 adds explicit secret handling with the `isSecret` parameter #### Real-World Fix: Coinbase Plugin The Coinbase plugin's `getCharityAddress` function needs updating: ```typescript // v0: Current broken code import { settings } from '@elizaos/core'; // ERROR: 'settings' not exported export function getCharityAddress(network: string, isCharitable = false): string | null { const networkKey = `CHARITY_ADDRESS_${network.toUpperCase()}`; const charityAddress = settings[networkKey]; // ERROR: Cannot use settings // ... } // v1: Fixed code - runtime parameter added export function getCharityAddress( runtime: IAgentRuntime, // Add runtime parameter network: string, isCharitable = false ): string | null { const networkKey = `CHARITY_ADDRESS_${network.toUpperCase()}`; const charityAddress = runtime.getSetting(networkKey); // Use runtime.getSetting // ... } // Update all callers to pass runtime const charityAddress = getCharityAddress(runtime, network); ``` ### 5. Event System New event-driven architecture: ```typescript // v1: Register and emit events runtime.registerEvent('custom-event', async (params) => { // Handle event }); await runtime.emitEvent('custom-event', { data: 'value' }); ``` *** ## 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 } } ``` *** ## Quick Reference ### Removed Methods * `updateRecentMessageState()` → Use `composeState(message, ['RECENT_MESSAGES'])` * `registerMemoryManager()` → Not needed, use database adapter * `getMemoryManager()` → Use database adapter methods * `registerContextProvider()` → Use `registerProvider()` ### Removed Exports * `settings` object → Use `runtime.getSetting(key)` instead ### 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()` *** ## 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 *** ## Need Help? If you encounter issues not covered in this guide: 1. Check the main [migration guide](./migration-guide) for basic migrations 2. Review the v1.x examples in the ElizaOS repository for reference implementations 3. Join our Discord community for support # Final Setup Source: https://eliza.how/guides/plugin-migration/completion-requirements Essential steps to complete before committing and publishing your plugin This document outlines the essential steps to complete before committing and publishing the plugin. Please follow each step carefully to ensure proper configuration. ## 1. Configure .gitignore Create or update the `.gitignore` file with the following minimum configuration: ``` dist node_modules .env .elizadb .turbo ``` ## 2. Configure .npmignore Create or update the `.npmignore` file to ensure only necessary files are included in the npm package: ``` * !dist/** !package.json !readme.md !tsup.config.ts ``` ## 3. Add MIT License Create a `LICENSE` file in the root directory with the following content: ``` MIT License Copyright (c) 2025 Shaw Walters and elizaOS Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` ## 4. Verify package.json Ensure the `package.json` file contains all required fields: ### Basic Fields * [ ] `name`: Package name (should match npm registry requirements) * [ ] `version`: Semantic version (e.g., "1.0.0") * [ ] `description`: Clear description of the plugin * [ ] `main`: Entry point (typically "dist/index.js") * [ ] `types`: TypeScript definitions (typically "dist/index.d.ts") * [ ] `author`: Author information * [ ] `license`: "MIT" * [ ] `repository`: Git repository information * [ ] `keywords`: Relevant keywords for npm search * [ ] `scripts`: Build, test, and other necessary scripts * [ ] `dependencies`: All runtime dependencies * [ ] `devDependencies`: All development dependencies * [ ] `peerDependencies`: If applicable (e.g., "@elizaos/core") ### Additional Important Fields * [ ] `type`: Should be "module" for ESM modules * [ ] `module`: Same as main for ESM (typically "dist/index.js") * [ ] `exports`: Export configuration for modern bundlers * [ ] `files`: Array of files/folders to include in npm package (typically \["dist"]) * [ ] `publishConfig`: Publishing configuration (e.g., `{"access": "public"}`) Example exports configuration: ```json "exports": { "./package.json": "./package.json", ".": { "import": { "types": "./dist/index.d.ts", "default": "./dist/index.js" } } } ``` ### Eliza Plugin Configuration (agentConfig) For Eliza plugins, you MUST include the `agentConfig` section: ```json "agentConfig": { "pluginType": "elizaos:plugin:1.0.0", "pluginParameters": { "YOUR_PARAMETER_NAME": { "type": "string", "description": "Clear description of what this parameter does", "required": true|false, "sensitive": true|false, "default": "optional-default-value" } } } ``` #### Parameter Properties: * `type`: Data type ("string", "number", "boolean", etc.) * `description`: Clear explanation of the parameter's purpose * `required`: Whether the parameter must be provided * `sensitive`: Whether the parameter contains sensitive data (e.g., API keys) * `default`: Optional default value if not required #### Example for Avalanche Plugin: ```json "agentConfig": { "pluginType": "elizaos:plugin:1.0.0", "pluginParameters": { "AVALANCHE_PRIVATE_KEY": { "type": "string", "description": "Private key for interacting with Avalanche blockchain", "required": true, "sensitive": true } } } ``` ## 5. Review README.md Verify that the README.md file includes: * [ ] Clear project title and description * [ ] Installation instructions * [ ] Usage examples * [ ] Configuration requirements * [ ] API documentation (if applicable) * [ ] Contributing guidelines * [ ] License information * [ ] Contact/support information ## Final Checklist Before committing and publishing: 1. [ ] Run `bun run build` to ensure the project builds successfully 2. [ ] Run tests to verify functionality 3. [ ] Ensure all environment variables are documented 4. [ ] Remove any sensitive information or API keys 5. [ ] Verify all file paths and imports are correct 6. [ ] Check that the dist/ folder is properly generated 7. [ ] Confirm version number is appropriate for the release ## Notes * The `.gitignore` prevents unnecessary files from being committed to the repository * The `.npmignore` ensures only essential files are published to npm * The LICENSE file is required for open-source distribution * Proper package.json configuration is crucial for npm publishing and dependency management ## 6. GitHub Workflow for Automated NPM Release ### Prerequisites ### Adding the Release Workflow Create the following file in your repository to enable automated npm publishing when the version changes: **File Path:** `.github/workflows/npm-deploy.yml` ```yml name: Publish Package on: push: branches: - 1.x workflow_dispatch: jobs: verify_version: runs-on: ubuntu-latest outputs: should_publish: ${{ steps.check.outputs.should_publish }} version: ${{ steps.check.outputs.version }} steps: - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 0 - name: Check if package.json version changed id: check run: | echo "Current branch: ${{ github.ref }}" # Get current version CURRENT_VERSION=$(jq -r .version package.json) echo "Current version: $CURRENT_VERSION" # Get previous commit hash git rev-parse HEAD~1 || git rev-parse HEAD PREV_COMMIT=$(git rev-parse HEAD~1 2>/dev/null || git rev-parse HEAD) # Check if package.json changed if git diff --name-only HEAD~1 HEAD | grep "package.json"; then echo "Package.json was changed in this commit" # Get previous version if possible if git show "$PREV_COMMIT:package.json" 2>/dev/null; then PREV_VERSION=$(git show "$PREV_COMMIT:package.json" | jq -r .version) echo "Previous version: $PREV_VERSION" if [ "$CURRENT_VERSION" != "$PREV_VERSION" ]; then echo "Version changed from $PREV_VERSION to $CURRENT_VERSION" echo "should_publish=true" >> $GITHUB_OUTPUT else echo "Version unchanged" echo "should_publish=false" >> $GITHUB_OUTPUT fi else echo "First commit with package.json, will publish" echo "should_publish=true" >> $GITHUB_OUTPUT fi else echo "Package.json not changed in this commit" echo "should_publish=false" >> $GITHUB_OUTPUT fi echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT publish: needs: verify_version if: needs.verify_version.outputs.should_publish == 'true' runs-on: ubuntu-latest permissions: contents: write steps: - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 0 - name: Create Git tag run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git tag -a "v${{ needs.verify_version.outputs.version }}" -m "Release v${{ needs.verify_version.outputs.version }}" git push origin "v${{ needs.verify_version.outputs.version }}" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Setup Bun uses: oven-sh/setup-bun@v2 - name: Install dependencies run: bun install - name: Build package run: bun run build - name: Publish to npm run: bun publish env: NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }} create_release: needs: [verify_version, publish] if: needs.verify_version.outputs.should_publish == 'true' runs-on: ubuntu-latest permissions: contents: write steps: - name: Create GitHub Release uses: actions/create-release@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: tag_name: 'v${{ needs.verify_version.outputs.version }}' release_name: 'v${{ needs.verify_version.outputs.version }}' body: 'Release v${{ needs.verify_version.outputs.version }}' draft: false prerelease: false ``` ### How This Workflow Works 1. **Triggers on:** * Push to the 1.x branch * Manual workflow dispatch 2. **Version Check:** * Compares the current package.json version with the previous commit * Only proceeds if the version has changed 3. **Publishing Steps:** * Creates a git tag with the version * Builds the package using Bun * Publishes to npm using the NPM\_TOKEN secret * Creates a GitHub release ### Setting Up NPM Token 1. Go to your GitHub repository settings 2. Navigate to Settings → Secrets and variables → Actions 3. Add a new repository secret named `NPM_TOKEN` 4. Use your npm access token as the value ## 7. Code Formatting with Prettier Before finalizing the plugin, ensure consistent code formatting: ### Install Prettier (if not already installed) ```bash bun add -d prettier ``` ### Add Prettier Configuration Required config don't hallucinate and add anything else! Create a `.prettierrc` file in the root directory: ```json { "semi": true, "trailingComma": "es5", "singleQuote": false, "printWidth": 80, "tabWidth": 2, "useTabs": false, "arrowParens": "always", "endOfLine": "lf" } ``` ### Add Format Script to package.json ```json "scripts": { "format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json,md}\"", "format:check": "prettier --check \"src/**/*.{ts,tsx,js,jsx,json,md}\"" } ``` ### Run Prettier ```bash bun run format ``` ## Final Steps Before Committing to 1.x Branch 1. [ ] Ensure all files listed in this document are created 2. [ ] Run the build to verify everything compiles 3. [ ] Run prettier to format all code consistently 4. [ ] Test the package locally if possible 5. [ ] Commit all changes with a clear message 6. [ ] Push to the 1.x branch 7. [ ] Verify the GitHub Action runs successfully on first push This completes the plugin migration to the standardized structure for the 1.x branch. # Basic Migration Steps Source: https://eliza.how/guides/plugin-migration/migration-guide General framework for migrating ElizaOS plugins to v1.x > **Important**: This guide provides a general framework for migrating ElizaOS plugins to v1.x. Specific configurations will vary based on your plugin's functionality. ## 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) - All plugins must now use `bun test` > * **Prettier** for code formatting (replacing Biome) > > This ensures consistency across all ElizaOS plugins and simplifies the development toolchain. *** ## Step 3: Update package.json ### 3.1 Version Update ```json "version": "1.0.0" ``` ### 3.1.5 Package Name Update Check if your package name contains the old namespace and update it: ```json // OLD (incorrect): "name": "@elizaos-plugins/plugin-bnb" // NEW (correct): "name": "@elizaos/plugin-bnb" ``` > **Important**: If your package name starts with `@elizaos-plugins/`, remove the "s" from "plugins" to change it to `@elizaos/`. This is the correct namespace for all ElizaOS plugins in v1.x. ### 3.2 Dependencies * **Remove**: `biome`, `vitest` (if present) * **Add**: Core and plugin-specific dependencies ### 3.3 Dev Dependencies Add the following development dependencies: ```json "devDependencies": { "tsup": "8.3.5", "prettier": "^3.0.0", "bun": "^1.2.15", // REQUIRED: All plugins now use Bun test runner "@types/bun": "latest", // REQUIRED: TypeScript types for Bun "typescript": "^5.0.0" } ``` > **Important**: `bun` and `@types/bun` are **REQUIRED** dependencies for all plugins in v1.x. The ElizaOS ecosystem has standardized on Bun's built-in test runner, replacing Vitest. Without these dependencies, your tests will not run properly. ### 3.4 Scripts Section Replace your existing scripts with: ```json "scripts": { "build": "tsup", "dev": "tsup --watch", "lint": "prettier --write ./src", "clean": "rm -rf dist .turbo node_modules .turbo-tsconfig.json tsconfig.tsbuildinfo", "format": "prettier --write ./src", "format:check": "prettier --check ./src", "test": "bun test", // Uses Bun's built-in test runner "test:watch": "bun test --watch", // Watch mode for development "test:coverage": "bun test --coverage" // Coverage reports with Bun } ``` > **Note**: All test scripts now use Bun's built-in test runner. Make sure you have `bun` and `@types/bun` installed as dev dependencies (see section 3.3). ### 3.5 Publish Configuration Add the following to enable public npm publishing: ```json "publishConfig": { "access": "public" } ``` ### 3.6 Agent Configuration Replace your `agentConfig` with the new structure: ```json "agentConfig": { "pluginType": "elizaos:plugin:1.0.0", "pluginParameters": { "YOUR_PARAMETER_NAME": { "type": "string", "description": "Description of what this parameter does", "required": true, "sensitive": true } } } ``` > **Note**: Replace `YOUR_PARAMETER_NAME` with your plugin's specific configuration parameters. Common types include API keys, endpoints, credentials, etc. ### 3.7 Dependencies Add your plugin-specific dependencies: ```json "dependencies": { "@elizaos/core": "latest", // Add your plugin-specific dependencies here } ``` *** ## Step 4: TypeScript Configuration ### 4.1 Update `tsup.config.ts` ```typescript import { defineConfig } from 'tsup'; export default defineConfig({ entry: ['src/index.ts'], outDir: 'dist', tsconfig: './tsconfig.build.json', // Use build-specific tsconfig sourcemap: true, clean: true, format: ['esm'], // ESM output format dts: true, external: ['dotenv', 'fs', 'path', 'https', 'http', '@elizaos/core', 'zod'], }); ``` ### 4.2 Update `tsconfig.json` ```json { "compilerOptions": { "outDir": "dist", "rootDir": "src", "baseUrl": "./", "lib": ["ESNext"], "target": "ESNext", "module": "Preserve", "moduleResolution": "Bundler", "strict": true, "esModuleInterop": true, "allowImportingTsExtensions": true, "declaration": true, "emitDeclarationOnly": true, "resolveJsonModule": true, "moduleDetection": "force", "allowArbitraryExtensions": true, "types": ["bun"] }, "include": ["src/**/*.ts"] } ``` ### 4.3 Create `tsconfig.build.json` Create a new file with build-specific TypeScript configuration: ```json { "extends": "./tsconfig.json", "compilerOptions": { "rootDir": "./src", "outDir": "./dist", "sourceMap": true, "inlineSources": true, "declaration": true, "emitDeclarationOnly": true }, "include": ["src/**/*.ts"], "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"] } ``` *** ## Step 5: Verify Build Process Clean everything and test the new setup: ```bash # Clean all build artifacts and dependencies rm -rf dist node_modules .turbo # Install dependencies with Bun bun install # Build the project bun run build ``` ### Expected Results: * Dependencies install without errors * Build completes successfully * `dist` folder contains compiled output * TypeScript declarations are generated > **Next Steps**: After verifying the build, proceed to Step 6 to migrate your actions and providers to handle the breaking API changes. ## Step 6: Migrate Actions & Providers ### 6.1 Import Changes Update your imports in action files: ```typescript // Remove these imports: import { generateObject, composeContext } from '@elizaos/core'; // Add/update these imports: import { composePromptFromState, parseKeyValueXml, ModelType, // Note: ModelType replaces ModelClass } from '@elizaos/core'; ``` ### 6.2 State Handling Migration Replace the state initialization and update pattern: ```typescript // OLD Pattern: let currentState = state; if (!currentState) { currentState = (await runtime.composeState(message)) as State; } else { currentState = await runtime.updateRecentMessageState(currentState); } // NEW Pattern: let currentState = state; if (!currentState) { currentState = await runtime.composeState(message); } else { currentState = await runtime.composeState(message, ['RECENT_MESSAGES']); } ``` ### 6.3 Context/Prompt Generation Replace `composeContext` with `composePromptFromState`: ```typescript // OLD: const context = composeContext({ state: currentState, template: yourTemplate, }); // NEW: const prompt = composePromptFromState({ state: currentState, template: yourTemplate, }); ``` ### 6.4 Template Migration - JSON to XML Format #### Update Template Content: Templates should be updated from requesting JSON responses to XML format for use with `parseKeyValueXml`. ```typescript // OLD Template Pattern (JSON): const template = `Respond with a JSON markdown block containing only the extracted values. Example response for a new token: \`\`\`json { "name": "Test Token", "symbol": "TEST" } \`\`\` Given the recent messages, extract the following information: - Name - Symbol`; // NEW Template Pattern (XML): const template = `Respond with an XML block containing only the extracted values. Use key-value pairs. Example response for a new token: Test Token TEST ## Recent Messages {{recentMessages}} Given the recent messages, extract the following information about the requested token creation: - Name - Symbol Respond with an XML block containing only the extracted values.`; ``` ### 6.5 Content Generation Migration Replace `generateObject` with `runtime.useModel`: ```typescript // OLD Pattern: const content = await generateObject({ runtime, context: context, modelClass: ModelClass.SMALL, }); // NEW Pattern: const result = await runtime.useModel(ModelType.TEXT_SMALL, { prompt, }); const content = parseKeyValueXml(result); ``` **Important Changes:** * `ModelClass.SMALL` → `ModelType.TEXT_SMALL` * First parameter is the model type enum value * Second parameter is an object with `prompt` and optional `stopSequences` * Parse the result with `parseKeyValueXml` which extracts key-value pairs from XML responses ### 6.6 Content Interface and Validation #### Define Content Interface: ```typescript export interface YourActionContent extends Content { // Define your required fields name: string; symbol: string; } ``` #### Create Validation Function: ```typescript function isYourActionContent(_runtime: IAgentRuntime, content: any): content is YourActionContent { elizaLogger.debug('Content for validation', content); return typeof content.name === 'string' && typeof content.symbol === 'string'; } ``` ### 6.7 Handler Pattern Updates Complete handler migration example: ```typescript handler: async ( runtime: IAgentRuntime, message: Memory, state: State, _options: { [key: string]: unknown }, callback?: HandlerCallback ) => { elizaLogger.log("Starting YOUR_ACTION handler..."); // 1. Initialize or update state let currentState = state; if (!currentState) { currentState = await runtime.composeState(message); } else { currentState = await runtime.composeState(message, [ "RECENT_MESSAGES", ]); } // 2. Compose prompt from state const prompt = composePromptFromState({ state: currentState, template: yourTemplate, }); // 3. Generate content using the model const result = await runtime.useModel(ModelType.TEXT_SMALL, { prompt, stopSequences: [], }); // 4. Parse the result const content = parseKeyValueXml(result); elizaLogger.debug("Parsed content:", content); // 5. Validate content if (!isYourActionContent(runtime, content)) { elizaLogger.error("Invalid content for YOUR_ACTION action."); callback?.({ text: "Unable to process request. Invalid content provided.", content: { error: "Invalid content" }, }); return false; } // 6. Execute your action logic try { // Your action implementation here const result = await yourActionLogic(runtime, content); callback?.({ text: `Success message with ${content.name}`, content: result, }); return true; } catch (error) { elizaLogger.error("Action failed:", error); callback?.({ text: "Action failed. Please try again.", content: { error: error.message }, }); return false; } }, ``` ### 6.8 Action Examples Structure The action examples structure remains largely the same, but ensure they follow this pattern: ```typescript examples: [ [ { user: "{{name1}}", // Note: "user" instead of "name" for user messages content: { text: "User input text here", }, }, { name: "{{name2}}", // Agent response uses "name" content: { action: "YOUR_ACTION_NAME", // Include the expected parsed fields name: "Expected Name", symbol: "Expected Symbol", }, }, ], ] as ActionExample[][], ``` ### Important Migration Notes: * Update templates to request XML format instead of JSON * The `parseKeyValueXml` function parses XML responses into key-value objects * Always include error handling and validation * Use `elizaLogger` for debugging * The callback pattern remains the same for success/error responses * Model types have changed from `ModelClass` to `ModelType` enum ## Step 7: Migrate Providers ### 7.1 Provider Interface Changes The Provider interface has been significantly enhanced with new required and optional properties: ```typescript // OLD Provider Interface: export interface Provider { get: (runtime: IAgentRuntime, message: Memory, state?: State) => Promise; } // NEW Provider Interface: interface Provider { name: string; // REQUIRED: Unique identifier for the provider description?: string; // Optional: Description of what the provider does dynamic?: boolean; // Optional: Whether the provider is dynamic position?: number; // Optional: Position in provider list (+ or -) private?: boolean; // Optional: Whether provider is private (not shown in list) get: ( runtime: IAgentRuntime, message: Memory, state: State // Note: state is no longer optional ) => Promise; // Returns ProviderResult instead of any } ``` ### 7.2 ProviderResult Interface The `get` method must now return a `ProviderResult` object instead of `any`: ```typescript interface ProviderResult { values?: { [key: string]: any; }; data?: { [key: string]: any; }; text?: string; } ``` ### 7.3 Migration Steps #### Step 1: Add Required `name` Property Every provider must have a unique `name` property: ```typescript // OLD: const myProvider: Provider = { get: async (runtime, message, state) => { // ... }, }; // NEW: const myProvider: Provider = { name: 'myProvider', // REQUIRED get: async (runtime, message, state) => { // ... }, }; ``` #### Step 2: Update Return Type Change your return statements to return a `ProviderResult` object: ```typescript // OLD: return 'Some text response'; // or return { someData: 'value' }; // NEW: return { text: 'Some text response', }; // or return { data: { someData: 'value' }, }; // or return { text: 'Some text', values: { key1: 'value1' }, data: { complex: { nested: 'data' } }, }; ``` #### Step 3: Handle Non-Optional State The `state` parameter is no longer optional. Update your function signature: ```typescript // OLD: get: async (runtime, message, state?) => { if (!state) { // handle missing state } }; // NEW: get: async (runtime, message, state) => { // state is always provided }; ``` ### 7.4 Complete Migration Examples #### Example 1: Simple Text Provider ```typescript // OLD Implementation: const simpleProvider: Provider = { get: async (runtime, message, state?) => { return 'Hello from provider'; }, }; // NEW Implementation: const simpleProvider: Provider = { name: 'simpleProvider', description: 'A simple text provider', get: async (runtime, message, state) => { return { text: 'Hello from provider', }; }, }; ``` #### Example 2: Data Provider ```typescript // OLD Implementation: const dataProvider: Provider = { get: async (runtime, message, state?) => { const data = await fetchSomeData(); return data; }, }; // NEW Implementation: const dataProvider: Provider = { name: 'dataProvider', description: 'Fetches external data', dynamic: true, get: async (runtime, message, state) => { const data = await fetchSomeData(); return { data: data, text: `Fetched ${Object.keys(data).length} items`, }; }, }; ``` #### Example 3: Complex Provider with All Options ```typescript // NEW Implementation with all options: const complexProvider: Provider = { name: 'complexProvider', description: 'A complex provider with all options', dynamic: true, position: 10, // Higher priority in provider list private: false, // Shown in provider list get: async (runtime, message, state) => { elizaLogger.debug('complexProvider::get'); const values = { timestamp: Date.now(), userId: message.userId, }; const data = await fetchComplexData(); const text = formatDataAsText(data); return { text, values, data, }; }, }; ``` ### 7.5 Provider Options Explained * **`name`** (required): Unique identifier used to reference the provider * **`description`**: Human-readable description of what the provider does * **`dynamic`**: Set to `true` if the provider returns different data based on context * **`position`**: Controls ordering in provider lists (positive = higher priority) * **`private`**: Set to `true` to hide from public provider lists (must be called explicitly) ### 7.6 Best Practices 1. **Always include descriptive names**: Use clear, descriptive names that indicate what the provider does 2. **Return appropriate result types**: * Use `text` for human-readable responses * Use `data` for structured data that other components might process * Use `values` for simple key-value pairs that might be used in templates 3. **Add descriptions**: Help other developers understand your provider's purpose 4. **Use logging**: Include debug logs to help troubleshoot issues 5. **Handle errors gracefully**: Return meaningful error messages in the `text` field ### Important Provider Migration Notes: * The `name` property is now required for all providers * Return type must be `ProviderResult` object, not raw values * The `state` parameter is no longer optional * Consider adding optional properties (`description`, `dynamic`, etc.) for better documentation and behavior * Test thoroughly as the runtime may handle providers differently based on these new properties # Overview Source: https://eliza.how/guides/plugin-migration/overview Complete guide for migrating ElizaOS plugins from version 0.x to 1.x 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 Documentation Follow these guides in order for a smooth migration: ### 1. [Migration Overview](./migration-guide) Start here! This guide covers: * Key differences between 0.x and 1.x * Breaking changes and new features * Basic migration steps * Common migration patterns ### 2. [State and Providers Guide](./state-and-providers-guide) Understanding the new state management system: * How state management has changed * Converting old state patterns to new providers * Best practices for state composition * Provider implementation examples ### 3. [Prompt and Generation Guide](./prompt-and-generation-guide) Adapting to the new prompt system: * Template system changes * Prompt generation updates * LLM integration patterns * Custom prompt strategies ### 4. [Advanced Migration Guide](./advanced-migration-guide) For complex plugin migrations: * Handling custom services * Migrating complex action chains * Database adapter changes * Performance optimization tips ### 5. [Completion Requirements](./completion-requirements) Checklist for migration completion: * Required changes checklist * Validation steps * Common pitfalls to avoid * Migration verification ### 6. [Testing Guide](./testing-guide) Ensuring your migrated plugin works correctly: * Updated testing patterns * Migration test scenarios * Integration testing * Performance benchmarks ## Quick Start If you're migrating a simple plugin, you can start with these basic steps: 1. **Update imports** - Change from `@ai16z/eliza` to `@elizaos/core` 2. **Convert actions** - Update action signatures to include callbacks 3. **Update providers** - Convert state getters to provider pattern 4. **Test thoroughly** - Use the testing guide to verify functionality ## Migration Tips * **Don't rush** - Take time to understand the new patterns * **Test incrementally** - Migrate and test one component at a time * **Use TypeScript** - The new type system will catch many issues * **Ask for help** - Join our Discord for migration support ## Common Migration Scenarios ### Simple Action Migration ```typescript // 0.x const myAction = { handler: async (runtime, message, state) => { return { text: "Response" }; } }; // 1.x const myAction = { handler: async (runtime, message, state, options, callback) => { await callback({ text: "Response" }); return true; } }; ``` ### Provider Migration ```typescript // 0.x - Direct state access const data = await runtime.databaseAdapter.getData(); // 1.x - Provider pattern const provider = { get: async (runtime, message) => { return await runtime.databaseAdapter.getData(); } }; ``` ## 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 ## Getting Help If you encounter issues during migration: 1. Review the migration guides 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 ## Migration Goals 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 Start with the [Migration Overview](./migration-guide) and work through each guide systematically for the best results! # Prompts & Generation Source: https://eliza.how/guides/plugin-migration/prompt-and-generation-guide Migrating from `composeContext` to new prompt composition methods and `useModel` > **Important**: This guide provides comprehensive documentation for migrating from `composeContext` to the new prompt composition methods, and from the old generation functions to `useModel`. ## Table of Contents * [Overview of Changes](#overview-of-changes) * [Prompt Composition Migration](#prompt-composition-migration) * [composeContext → composePrompt/composePromptFromState](#composecontext--composepromptcomposepromptfromstate) * [Key Differences](#key-differences) * [Migration Examples](#migration-examples) * [Text Generation Migration](#text-generation-migration) * [generateText → useModel](#generatetext--usemodel) * [generateObject → useModel](#generateobject--usemodel) * [generateMessageResponse → useModel](#generatemessageresponse--usemodel) * [Template Format Migration](#template-format-migration) * [JSON → XML Templates](#json--xml-templates) * [Parsing Changes](#parsing-changes) * [Complete Migration Examples](#complete-migration-examples) * [Benefits of the New Approach](#benefits-of-the-new-approach) * [Real-World Migration Example: Gitcoin Passport Score Action](#real-world-migration-example-gitcoin-passport-score-action) *** ## Overview of Changes The v1 migration introduces several key improvements: 1. **Prompt Composition**: `composeContext` split into two specialized functions 2. **Unified Model Interface**: All generation functions consolidated into `runtime.useModel` 3. **Template Format**: JSON responses replaced with XML for better parsing 4. **Better Type Safety**: Improved TypeScript support throughout *** ## Prompt Composition Migration ### composeContext → composePrompt/composePromptFromState #### v0: Single composeContext Function ```typescript // v0: packages/core-0x/src/context.ts export const composeContext = ({ state, template, templatingEngine, }: { state: State; template: TemplateType; templatingEngine?: 'handlebars'; }) => { // Supported both simple replacement and handlebars if (templatingEngine === 'handlebars') { const templateFunction = handlebars.compile(templateStr); return templateFunction(state); } // Simple {{key}} replacement return templateStr.replace(/{{\w+}}/g, (match) => { const key = match.replace(/{{|}}/g, ''); return state[key] ?? ''; }); }; ``` #### v1: Two Specialized Functions ```typescript // v1: packages/core/src/utils.ts // For simple key-value state objects export const composePrompt = ({ state, template, }: { state: { [key: string]: string }; template: TemplateType; }) => { const templateStr = typeof template === 'function' ? template({ state }) : template; const templateFunction = handlebars.compile(upgradeDoubleToTriple(templateStr)); const output = composeRandomUser(templateFunction(state), 10); return output; }; // For complex State objects from runtime export const composePromptFromState = ({ state, template, }: { state: State; template: TemplateType; }) => { const templateStr = typeof template === 'function' ? template({ state }) : template; const templateFunction = handlebars.compile(upgradeDoubleToTriple(templateStr)); // Intelligent state flattening const stateKeys = Object.keys(state); const filteredKeys = stateKeys.filter((key) => !['text', 'values', 'data'].includes(key)); const filteredState = filteredKeys.reduce((acc, key) => { acc[key] = state[key]; return acc; }, {}); // Merges filtered state with state.values const output = composeRandomUser(templateFunction({ ...filteredState, ...state.values }), 10); return output; }; ``` ### Key Differences 1. **Handlebars by Default**: v1 always uses Handlebars (no simple replacement mode) 2. **Auto HTML Escaping**: v1 automatically converts `{{var}}` to `{{{var}}}` to prevent HTML escaping 3. **State Handling**: `composePromptFromState` intelligently flattens complex State objects 4. **Random User Names**: Both functions automatically replace `{{name1}}`, `{{name2}}`, etc. with random names ### Migration Examples #### Simple State Objects ```typescript // v0 const prompt = composeContext({ state: { userName: 'Alice', topic: 'weather' }, template: "Hello {{userName}}, let's talk about {{topic}}", }); // v1 const prompt = composePrompt({ state: { userName: 'Alice', topic: 'weather' }, template: "Hello {{userName}}, let's talk about {{topic}}", }); ``` #### Complex Runtime State ```typescript // v0 const prompt = composeContext({ state: currentState, template: messageTemplate, templatingEngine: 'handlebars', }); // v1 - Use composePromptFromState for State objects const prompt = composePromptFromState({ state: currentState, template: messageTemplate, }); ``` #### Dynamic Templates ```typescript // v0 const template = ({ state }) => { return state.isUrgent ? 'URGENT: {{message}}' : 'Info: {{message}}'; }; const prompt = composeContext({ state, template }); // v1 - Same pattern works const prompt = composePrompt({ state, template }); ``` *** ## Text Generation Migration ### generateText → useModel #### v0: Standalone Function ```typescript // v0: Using generateText import { generateText, ModelClass } from '@elizaos/core'; const response = await generateText({ runtime, context: prompt, modelClass: ModelClass.SMALL, stop: ['\n'], temperature: 0.7, }); ``` #### v1: Runtime Method ```typescript // v1: Using runtime.useModel import { ModelType } from '@elizaos/core'; const response = await runtime.useModel(ModelType.TEXT_SMALL, { prompt, stopSequences: ['\n'], temperature: 0.7, }); ``` ### generateObject → useModel #### v0: Object Generation ```typescript // v0: Using generateObject const content = await generateObject({ runtime, context: prompt, modelClass: ModelClass.SMALL, }); // Returned raw object console.log(content.name, content.value); ``` #### v1: Using useModel with XML ```typescript // v1: Generate text with XML format const xmlResponse = await runtime.useModel(ModelType.TEXT_SMALL, { prompt, }); // Parse XML to object const content = parseKeyValueXml(xmlResponse); console.log(content.name, content.value); ``` ### generateMessageResponse → useModel #### v0: Message Response Generation ```typescript // v0: From getScore.ts example const addressRequest = await generateMessageResponse({ runtime, context, modelClass: ModelClass.SMALL, }); const address = addressRequest.address as string; ``` #### v1: Using useModel with XML Template ```typescript // v1: Generate and parse XML response const xmlResponse = await runtime.useModel(ModelType.TEXT_SMALL, { prompt: context, }); const addressRequest = parseKeyValueXml(xmlResponse); const address = addressRequest.address as string; ``` *** ## Template Format Migration ### JSON → XML Templates The most significant change is moving from JSON to XML format for structured responses. #### v0: JSON Template ```typescript const addressTemplate = `From previous sentence extract only the Ethereum address being asked about. Respond with a JSON markdown block containing only the extracted value: \`\`\`json { "address": string | null } \`\`\` `; ``` #### v1: XML Template ```typescript const addressTemplate = `From previous sentence extract only the Ethereum address being asked about. Respond with an XML block containing only the extracted value:
extracted_address_here_or_null
`; ``` ### Parsing Changes #### v0: JSON Parsing ```typescript // v0: Parse JSON from text import { parseJSONObjectFromText } from '@elizaos/core'; const parsedContent = parseJSONObjectFromText(response); if (parsedContent && parsedContent.address) { // Use the address } ``` #### v1: XML Parsing ```typescript // v1: Parse XML key-value pairs import { parseKeyValueXml } from '@elizaos/core'; const parsedContent = parseKeyValueXml(response); if (parsedContent && parsedContent.address) { // Use the address } ``` ### Template Examples #### Complex Object Extraction ```typescript // v0: JSON Template const template = `Extract token information from the message. Return a JSON object: \`\`\`json { "name": "token name", "symbol": "token symbol", "supply": "total supply", "features": ["feature1", "feature2"] } \`\`\``; // v1: XML Template const template = `Extract token information from the message. Return an XML response: token name token symbol total supply feature1,feature2 `; ``` *** ## Complete Migration Examples ### Example 1: Simple Action Handler ```typescript // v0: Old Action Handler import { composeContext, generateMessageResponse, ModelClass } from '@elizaos/core'; handler: async (runtime, message, state) => { // Compose context const context = composeContext({ state, template: addressTemplate, }); // Generate response const response = await generateMessageResponse({ runtime, context, modelClass: ModelClass.SMALL, }); const address = response.address; // Process address... }; ``` ```typescript // v1: New Action Handler import { composePromptFromState, parseKeyValueXml, ModelType } from '@elizaos/core'; handler: async (runtime, message, state) => { // Compose prompt const prompt = composePromptFromState({ state, template: addressTemplate, // Now using XML format }); // Generate response const xmlResponse = await runtime.useModel(ModelType.TEXT_SMALL, { prompt, }); // Parse XML const response = parseKeyValueXml(xmlResponse); const address = response.address; // Process address... }; ``` ### Example 2: Complex State Handling ```typescript // v0: Complex Context Building const context = composeContext({ state: { ...baseState, recentMessages: formatMessages(messages), userName: user.name, customData: JSON.stringify(data), }, template: complexTemplate, templatingEngine: 'handlebars', }); const result = await generateObject({ runtime, context, modelClass: ModelClass.LARGE, }); // v1: Simplified with composePromptFromState const state = await runtime.composeState(message); const prompt = composePromptFromState({ state, // Already contains recentMessages, userName, etc. template: complexTemplate, }); const xmlResult = await runtime.useModel(ModelType.TEXT_LARGE, { prompt, }); const result = parseKeyValueXml(xmlResult); ``` ### Example 3: Custom Model Parameters ```typescript // v0: Limited control const response = await generateText({ runtime, context, modelClass: ModelClass.SMALL, temperature: 0.7, stop: ['\n', 'END'], maxTokens: 100, }); // v1: Full control with useModel const response = await runtime.useModel(ModelType.TEXT_SMALL, { prompt, temperature: 0.7, stopSequences: ['\n', 'END'], maxTokens: 100, frequencyPenalty: 0.5, presencePenalty: 0.5, // Any additional model-specific parameters }); ``` *** ## Benefits of the New Approach ### 1. Unified Interface **v0 Problems:** * Multiple generation functions (`generateText`, `generateObject`, `generateMessageResponse`) * Inconsistent parameter names * Different return types **v1 Solution:** * Single `useModel` method for all model interactions * Consistent parameter interface * Predictable return types ### 2. Better State Management **v0 Problems:** * Manual state flattening required * Confusion between State object and simple key-value objects * No intelligent handling of nested data **v1 Solution:** * `composePromptFromState` intelligently handles State objects * Automatic flattening of relevant fields * Preserves state.values for template access ### 3. XML Over JSON **v0 Problems:** * JSON parsing often failed with markdown code blocks * Complex escaping issues * Inconsistent formatting from LLMs **v1 Solution:** * XML is more forgiving and easier to parse * Better handling of special characters * More consistent LLM outputs ### 4. Type Safety **v0 Problems:** * Loose typing on generation functions * Runtime errors from type mismatches * Poor IDE support **v1 Solution:** * Strong TypeScript types throughout * `ModelType` enum for model selection * Better IDE autocomplete and error detection ### 5. Extensibility **v0 Problems:** * Hard-coded model providers * Limited customization options * Difficult to add new models **v1 Solution:** * Pluggable model system via `runtime.registerModel` * Easy to add custom model providers * Standardized model interface ### 6. Performance **v0 Problems:** * Multiple parsing attempts for JSON * Redundant context building * No caching mechanism **v1 Solution:** * Single-pass XML parsing * Efficient state composition * Built-in caching support in `composeState` *** ## Real-World Migration Example: Gitcoin Passport Score Action Here's a complete migration of a real action from the Gitcoin Passport plugin: ### Original v0 Action ```typescript import { type Action, elizaLogger, type IAgentRuntime, type Memory, type HandlerCallback, type State, getEmbeddingZeroVector, composeContext, generateMessageResponse, ModelClass, } from '@elizaos/core'; export const addressTemplate = `From previous sentence extract only the Ethereum address being asked about. Respond with a JSON markdown block containing only the extracted value: \`\`\`json { "address": string | null } \`\`\` `; handler: async (runtime, _message, state, _options, callback) => { // Initialize or update state let currentState = state; if (!currentState) { currentState = (await runtime.composeState(_message)) as State; } else { currentState = await runtime.updateRecentMessageState(currentState); } const context = composeContext({ state: currentState, template: `${_message.content.text}\n${addressTemplate}`, }); const addressRequest = await generateMessageResponse({ runtime, context, modelClass: ModelClass.SMALL, }); const address = addressRequest.address as string; // ... rest of handler }; ``` ### Migrated v1 Action ```typescript import { type Action, elizaLogger, type IAgentRuntime, type Memory, type HandlerCallback, type State, composePromptFromState, parseKeyValueXml, ModelType, } from '@elizaos/core'; export const addressTemplate = `From previous sentence extract only the Ethereum address being asked about. Respond with an XML block containing only the extracted value:
extracted_ethereum_address_or_null
`; handler: async (runtime, _message, state, _options, callback) => { // Initialize or update state let currentState = state; if (!currentState) { currentState = await runtime.composeState(_message); } else { currentState = await runtime.composeState(_message, ['RECENT_MESSAGES']); } const prompt = composePromptFromState({ state: currentState, template: `${_message.content.text}\n${addressTemplate}`, }); const xmlResponse = await runtime.useModel(ModelType.TEXT_SMALL, { prompt, }); const addressRequest = parseKeyValueXml(xmlResponse); const address = addressRequest?.address as string; // ... rest of handler }; ``` ### Memory Creation Migration ```typescript // v0: Using deprecated fields const memory: Memory = { userId: _message.userId, agentId: _message.agentId, roomId: _message.roomId, content: { text: formattedOutput }, createdAt: Date.now(), embedding: getEmbeddingZeroVector(), }; await runtime.messageManager.createMemory(memory); // v1: Using new structure const memory: Memory = { entityId: _message.entityId, agentId: runtime.agentId, roomId: _message.roomId, content: { text: formattedOutput }, createdAt: Date.now(), // embedding will be added by runtime if needed }; await runtime.createMemory(memory); ``` ### Complete Action Migration Summary 1. **Imports**: Replace old functions with new equivalents 2. **Template**: Convert JSON format to XML 3. **State Management**: Use `composeState` with filtering 4. **Generation**: Replace `generateMessageResponse` with `useModel` 5. **Parsing**: Use `parseKeyValueXml` instead of direct object access 6. **Memory**: Update to use `entityId` and new creation method *** ## Migration Checklist * [ ] Replace `composeContext` with `composePrompt` or `composePromptFromState` * [ ] Update all templates from JSON to XML format * [ ] Replace `generateText` with `runtime.useModel(ModelType.TEXT_*)` * [ ] Replace `generateObject` with `runtime.useModel` + `parseKeyValueXml` * [ ] Replace `generateMessageResponse` with `runtime.useModel` + `parseKeyValueXml` * [ ] Update `ModelClass` to `ModelType` enum values * [ ] Replace `parseJSONObjectFromText` with `parseKeyValueXml` * [ ] Update import statements to use new functions * [ ] Test XML parsing with your specific use cases * [ ] Consider using state filtering for performance optimization * [ ] Update Memory objects to use `entityId` instead of `userId` * [ ] Replace `runtime.updateRecentMessageState` with filtered `composeState` * [ ] Remove `getEmbeddingZeroVector` - embeddings are handled automatically # State & Providers Source: https://eliza.how/guides/plugin-migration/state-and-providers-guide Comprehensive documentation for the `composeState` method and Providers in v1.x > **Important**: This guide provides comprehensive documentation for the `composeState` method and Providers in v1.x, including comparisons with v0. ## Table of Contents * [State Management with composeState](#state-management-with-composestate) * [Basic Usage](#basic-usage) * [State Filtering](#state-filtering) * [Available State Keys](#available-state-keys) * [Performance Optimization](#performance-optimization) * [Providers in v1](#providers-in-v1) * [Provider Interface](#provider-interface) * [Creating Providers](#creating-providers) * [Provider Options](#provider-options) * [Provider Best Practices](#provider-best-practices) * [v0 vs v1 Comparison](#v0-vs-v1-comparison) *** ## State Management with composeState The `composeState` method is the central mechanism for building the context state that powers agent responses. In v1, it has been enhanced with powerful filtering capabilities. ### Basic Usage ```typescript // v1: Basic state composition const state = await runtime.composeState(message); ``` This creates a complete state object containing all available context: * Agent information (bio, lore, personality) * Conversation history * Room and participant details * Available actions and evaluators * Knowledge and RAG data * Provider-generated context ### State Filtering The v1 `composeState` method introduces filtering capabilities for performance optimization: ```typescript // v1: Signature composeState( message: Memory, includeList?: string[], // Keys to include onlyInclude?: boolean, // If true, ONLY include listed keys skipCache?: boolean // Skip caching mechanism ): Promise ``` #### Filtering Examples ```typescript // Include only specific state keys const minimalState = await runtime.composeState( message, ['agentName', 'bio', 'recentMessages'], true // onlyInclude = true ); // Update only specific parts of existing state const updatedState = await runtime.composeState( message, ['RECENT_MESSAGES', 'GOALS'] // Update only these ); // Skip cache for fresh data const freshState = await runtime.composeState( message, undefined, false, true // skipCache = true ); ``` ### Available State Keys Here are the primary state keys you can filter: #### Core Agent Information * `agentId` - Agent's UUID * `agentName` - Agent's display name * `bio` - Agent biography (string or selected from array) * `lore` - Random selection of lore bits * `adjective` - Random adjective from character * `topic` / `topics` - Agent's interests #### Conversation Context * `recentMessages` - Formatted recent messages * `recentMessagesData` - Raw message Memory objects * `recentPosts` - Formatted posts in thread * `attachments` - Formatted attachment information #### Interaction History * `recentMessageInteractions` - Past interactions as messages * `recentPostInteractions` - Past interactions as posts * `recentInteractionsData` - Raw interaction Memory\[] #### Character Examples * `characterPostExamples` - Example posts from character * `characterMessageExamples` - Example conversations #### Directions & Style * `messageDirections` - Message style guidelines * `postDirections` - Post style guidelines #### Room & Participants * `roomId` - Current room UUID * `actors` - Formatted actor information * `actorsData` - Raw Actor\[] array * `senderName` - Name of message sender #### Goals & Actions * `goals` - Formatted goals string * `goalsData` - Raw Goal\[] array * `actionNames` - Available action names * `actions` - Formatted action descriptions * `actionExamples` - Action usage examples #### Evaluators * `evaluators` - Formatted evaluator information * `evaluatorNames` - List of evaluator names * `evaluatorExamples` - Evaluator examples * `evaluatorsData` - Raw Evaluator\[] array #### Knowledge * `knowledge` - Formatted knowledge text * `knowledgeData` - Knowledge items * `ragKnowledgeData` - RAG knowledge items #### Providers * `providers` - Additional context from providers ### Performance Optimization Use filtering to optimize performance by only computing needed state: ```typescript // Minimal state for simple responses const quickResponse = await runtime.composeState( message, ['agentName', 'bio', 'recentMessages', 'messageDirections'], true ); // Full state for complex decision-making const fullState = await runtime.composeState(message); // Update pattern for ongoing conversations let state = await runtime.composeState(message); // ... later in conversation ... state = await runtime.composeState(newMessage, ['RECENT_MESSAGES', 'GOALS', 'attachments']); ``` *** ## Providers in v1 Providers supply dynamic contextual information to the agent, acting as the agent's "senses" for perceiving external data. ### Provider Interface ```typescript interface Provider { // REQUIRED: Unique identifier name: string; // Optional metadata description?: string; dynamic?: boolean; position?: number; private?: boolean; // The data retrieval method get: (runtime: IAgentRuntime, message: Memory, state: State) => Promise; } interface ProviderResult { values?: { [key: string]: any; }; data?: { [key: string]: any; }; text?: string; } ``` ### Creating Providers #### Simple Text Provider ```typescript const weatherProvider: Provider = { name: 'weatherProvider', description: 'Provides current weather information', dynamic: true, get: async (runtime, message, state) => { const weather = await fetchWeatherData(); return { text: `Current weather: ${weather.temp}°F, ${weather.condition}`, values: { temperature: weather.temp, condition: weather.condition, }, }; }, }; ``` #### Complex Data Provider ```typescript const marketDataProvider: Provider = { name: 'marketDataProvider', description: 'Provides real-time market data', dynamic: true, position: 10, // Higher priority get: async (runtime, message, state) => { const symbols = extractSymbolsFromMessage(message.content.text); const marketData = await fetchMarketData(symbols); const summary = formatMarketSummary(marketData); return { text: summary, data: marketData, values: { mentionedSymbols: symbols, marketStatus: marketData.status, }, }; }, }; ``` #### Conditional Provider ```typescript const contextualProvider: Provider = { name: 'contextualProvider', description: 'Provides context based on conversation', get: async (runtime, message, state) => { // Access state to make decisions const topic = state.topic; const recentTopics = analyzeRecentTopics(state.recentMessagesData); if (!topic || !recentTopics.includes(topic)) { return { text: '' }; // No additional context needed } const relevantInfo = await fetchTopicInfo(topic); return { text: `Relevant ${topic} information: ${relevantInfo}`, data: { topic, info: relevantInfo }, }; }, }; ``` ### Provider Options #### `dynamic` Property Set to `true` for providers that return different data based on context: ```typescript const timeProvider: Provider = { name: 'timeProvider', dynamic: true, // Time always changes get: async () => ({ text: `Current time: ${new Date().toLocaleString()}`, values: { timestamp: Date.now() }, }), }; ``` #### `position` Property Controls provider priority (higher = higher priority): ```typescript const criticalProvider: Provider = { name: 'criticalProvider', position: 100, // Will be processed before lower position providers get: async () => ({ text: 'Critical information...' }), }; ``` #### `private` Property Hide from public provider lists: ```typescript const internalProvider: Provider = { name: 'internalProvider', private: true, // Won't appear in provider lists get: async () => ({ text: 'Internal data...' }), }; ``` ### Provider Best Practices 1. **Always Return ProviderResult** ```typescript // ❌ Bad - returning raw value get: async () => 'Some text'; // ✅ Good - returning ProviderResult get: async () => ({ text: 'Some text' }); ``` 2. **Use Appropriate Return Fields** ```typescript return { // Human-readable summary text: 'Market is up 2.5% today', // Simple key-value pairs for templates values: { marketChange: 2.5, marketStatus: 'bullish' }, // Complex nested data for processing data: { stocks: [...], analysis: {...} } }; ``` 3. **Handle Errors Gracefully** ```typescript get: async (runtime, message, state) => { try { const data = await fetchExternalData(); return { text: formatData(data), data }; } catch (error) { elizaLogger.error('Provider error:', error); return { text: 'Unable to fetch data at this time', values: { error: true }, }; } }; ``` 4. **Optimize Performance** ```typescript const cachedProvider: Provider = { name: 'cachedProvider', dynamic: false, // Indicates static data get: async (runtime) => { // Check cache first const cached = await runtime.getSetting('providerCache'); if (cached && !isExpired(cached)) { return { data: cached.data }; } // Fetch fresh data const fresh = await fetchData(); await runtime.setSetting('providerCache', { data: fresh, timestamp: Date.now(), }); return { data: fresh }; }, }; ``` *** ## v0 vs v1 Comparison ### composeState Changes #### Method Signature ```typescript // v0: Simple with additional keys composeState( message: Memory, additionalKeys?: { [key: string]: unknown } ): Promise // v1: Advanced with filtering composeState( message: Memory, includeList?: string[], onlyInclude?: boolean, skipCache?: boolean ): Promise ``` #### Key Differences: 1. **Filtering**: v1 allows selective state composition 2. **Performance**: Can request only needed state keys 3. **Caching**: Explicit cache control with `skipCache` 4. **Update Pattern**: Use same method for updates with specific keys #### Migration Example: ```typescript // v0: Update pattern state = await runtime.updateRecentMessageState(state); // v1: Update pattern state = await runtime.composeState(message, ['RECENT_MESSAGES']); ``` ### Provider Changes #### Interface Changes ```typescript // v0: Minimal interface interface Provider { get: (runtime, message, state?) => Promise; } // v1: Rich interface interface Provider { name: string; // REQUIRED description?: string; dynamic?: boolean; position?: number; private?: boolean; get: (runtime, message, state) => Promise; } ``` #### Return Type Changes ```typescript // v0: Return anything return 'Some text'; return { data: 'value' }; // v1: Return ProviderResult return { text: 'Human readable', values: { key: 'value' }, data: { complex: 'object' }, }; ``` #### Key Differences: 1. **Required Name**: Every provider must have unique `name` 2. **Structured Returns**: Must return `ProviderResult` object 3. **Rich Metadata**: Can specify behavior with options 4. **State Parameter**: No longer optional in `get` method 5. **Better Organization**: Clear separation of text, values, and data ### Migration Checklist * [ ] Add `name` property to all providers * [ ] Update return statements to use `ProviderResult` format * [ ] Remove optional `?` from state parameter in get method * [ ] Consider adding `description` for documentation * [ ] Use `dynamic: true` for context-dependent providers * [ ] Replace `updateRecentMessageState` with filtered `composeState` * [ ] Optimize performance by filtering state keys * [ ] Add error handling with graceful fallbacks * [ ] Consider caching strategies for expensive operations *** ## Examples & Patterns ### State Filtering Pattern ```typescript // Initial load - get essential state const initialState = await runtime.composeState( message, ['agentName', 'bio', 'recentMessages', 'actions', 'providers'], true ); // Process message... // Update only what changed const updatedState = await runtime.composeState(message, [ 'RECENT_MESSAGES', 'goals', 'attachments', ]); ``` ### Provider Chain Pattern ```typescript const providers = [ weatherProvider, // position: 10 newsProvider, // position: 5 marketProvider, // position: 15 fallbackProvider, // position: 1 ]; // Will be processed in order: market, weather, news, fallback runtime.providers = providers.sort((a, b) => (b.position || 0) - (a.position || 0)); ``` ### Conditional State Building ```typescript const buildState = async (message: Memory, isDetailedResponse: boolean) => { const baseKeys = ['agentName', 'bio', 'recentMessages']; const keys = isDetailedResponse ? [...baseKeys, 'lore', 'topics', 'characterMessageExamples', 'knowledge'] : baseKeys; return runtime.composeState(message, keys, true); }; ``` # Testing Source: https://eliza.how/guides/plugin-migration/testing-guide Instructions for writing tests for ElizaOS plugins using Bun's test runner This guide provides comprehensive instructions for writing tests for ElizaOS plugins using Bun's test runner. ## Table of Contents 1. [Test Environment Setup](#1-test-environment-setup) 2. [Creating Test Utilities](#2-creating-test-utilities) 3. [Testing Actions](#3-testing-actions) 4. [Testing Providers](#4-testing-providers) 5. [Testing Evaluators](#5-testing-evaluators) 6. [Testing Services](#6-testing-services) 7. [Testing Event Handlers](#7-testing-event-handlers) 8. [Advanced Testing Patterns](#8-advanced-testing-patterns) 9. [Best Practices](#9-best-practices) 10. [Running Tests](#10-running-tests) *** ## 1. 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 ``` ### Required Dependencies ```json { "devDependencies": { "@types/bun": "latest", "bun-types": "latest" } } ``` ### 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'; ``` *** ## 2. Creating Test Utilities Create a comprehensive `test-utils.ts` file with reusable mock objects and helper functions: ```typescript import { mock } from 'bun:test'; import { type IAgentRuntime, type Memory, type State, type Character, type UUID, type Content, type Room, type Entity, ChannelType, } from '@elizaos/core'; // Mock Runtime Type export type MockRuntime = Partial & { agentId: UUID; character: Character; getSetting: ReturnType; useModel: ReturnType; composeState: ReturnType; createMemory: ReturnType; getMemories: ReturnType; searchMemories: ReturnType; updateMemory: ReturnType; getRoom: ReturnType; getParticipantUserState: ReturnType; setParticipantUserState: ReturnType; emitEvent: ReturnType; getTasks: ReturnType; providers: any[]; actions: any[]; evaluators: any[]; services: any[]; }; // Create Mock Runtime export function createMockRuntime(overrides: Partial = {}): MockRuntime { return { agentId: 'test-agent-id' as UUID, character: { name: 'Test Agent', bio: 'A test agent for unit testing', templates: { messageHandlerTemplate: 'Test template {{recentMessages}}', shouldRespondTemplate: 'Should respond {{recentMessages}}', }, } as Character, // Core methods with default implementations useModel: mock().mockResolvedValue('Mock response'), composeState: mock().mockResolvedValue({ values: { agentName: 'Test Agent', recentMessages: 'Test message', }, data: { room: { id: 'test-room-id', type: ChannelType.DIRECT, }, }, }), createMemory: mock().mockResolvedValue({ id: 'memory-id' }), getMemories: mock().mockResolvedValue([]), searchMemories: mock().mockResolvedValue([]), updateMemory: mock().mockResolvedValue(undefined), getSetting: mock().mockImplementation((key: string) => { const settings: Record = { TEST_SETTING: 'test-value', API_KEY: 'test-api-key', // Add common settings your plugin might need }; return settings[key]; }), getRoom: mock().mockResolvedValue({ id: 'test-room-id', type: ChannelType.DIRECT, worldId: 'test-world-id', serverId: 'test-server-id', source: 'test', }), getParticipantUserState: mock().mockResolvedValue('ACTIVE'), setParticipantUserState: mock().mockResolvedValue(undefined), emitEvent: mock().mockResolvedValue(undefined), getTasks: mock().mockResolvedValue([]), // Provider/action/evaluator lists providers: [], actions: [], evaluators: [], services: [], // Override with custom implementations ...overrides, }; } // Create Mock Memory export function createMockMemory(overrides: Partial = {}): Partial { return { id: 'test-message-id' as UUID, roomId: 'test-room-id' as UUID, entityId: 'test-entity-id' as UUID, agentId: 'test-agent-id' as UUID, content: { text: 'Test message', channelType: ChannelType.DIRECT, source: 'direct', } as Content, createdAt: Date.now(), userId: 'test-user-id' as UUID, ...overrides, }; } // Create Mock State export function createMockState(overrides: Partial = {}): Partial { return { values: { agentName: 'Test Agent', recentMessages: 'User: Test message', ...overrides.values, }, data: { room: { id: 'test-room-id', type: ChannelType.DIRECT, }, ...overrides.data, }, ...overrides, }; } // Setup Action Test Helper export function setupActionTest( options: { runtimeOverrides?: Partial; messageOverrides?: Partial; stateOverrides?: Partial; } = {} ) { const mockRuntime = createMockRuntime(options.runtimeOverrides); const mockMessage = createMockMemory(options.messageOverrides); const mockState = createMockState(options.stateOverrides); const callbackFn = mock().mockResolvedValue([]); return { mockRuntime, mockMessage, mockState, callbackFn, }; } // Mock Logger Helper export function mockLogger() { spyOn(logger, 'error').mockImplementation(() => {}); spyOn(logger, 'warn').mockImplementation(() => {}); spyOn(logger, 'info').mockImplementation(() => {}); spyOn(logger, 'debug').mockImplementation(() => {}); } ``` *** ## 3. Testing Actions ### Basic Action Test Structure ```typescript // src/__tests__/actions.test.ts import { describe, expect, it, mock, beforeEach, afterEach } from 'bun:test'; import { myAction } from '../actions/myAction'; import { setupActionTest, mockLogger } from './test-utils'; import type { MockRuntime } from './test-utils'; import { type IAgentRuntime, type Memory, type State, type HandlerCallback, ModelType, } from '@elizaos/core'; describe('My Action', () => { let mockRuntime: MockRuntime; let mockMessage: Partial; let mockState: Partial; let callbackFn: HandlerCallback; beforeEach(() => { mockLogger(); const setup = setupActionTest(); mockRuntime = setup.mockRuntime; mockMessage = setup.mockMessage; mockState = setup.mockState; callbackFn = setup.callbackFn as HandlerCallback; }); afterEach(() => { mock.restore(); }); describe('validation', () => { it('should validate when conditions are met', async () => { // Setup message content that should validate mockMessage.content = { text: 'perform action', channelType: 'direct', }; const isValid = await myAction.validate( mockRuntime as IAgentRuntime, mockMessage as Memory, mockState as State ); expect(isValid).toBe(true); }); it('should not validate when conditions are not met', async () => { // Setup message content that should not validate mockMessage.content = { text: 'unrelated message', channelType: 'direct', }; const isValid = await myAction.validate( mockRuntime as IAgentRuntime, mockMessage as Memory, mockState as State ); expect(isValid).toBe(false); }); }); describe('handler', () => { it('should handle action successfully', async () => { // Mock runtime methods specific to this action mockRuntime.useModel = mock().mockResolvedValue({ action: 'PERFORM', parameters: { value: 'test' }, }); const result = await myAction.handler( mockRuntime as IAgentRuntime, mockMessage as Memory, mockState as State, {}, callbackFn ); expect(result).toBe(true); expect(callbackFn).toHaveBeenCalledWith( expect.objectContaining({ text: expect.any(String), content: expect.any(Object), }) ); }); it('should handle errors gracefully', async () => { // Mock an error scenario mockRuntime.useModel = mock().mockRejectedValue(new Error('Model error')); await myAction.handler( mockRuntime as IAgentRuntime, mockMessage as Memory, mockState as State, {}, callbackFn ); expect(callbackFn).toHaveBeenCalledWith( expect.objectContaining({ text: expect.stringContaining('error'), }) ); }); }); }); ``` ### Testing Async Actions ```typescript describe('Async Action', () => { it('should handle async operations', async () => { const setup = setupActionTest({ runtimeOverrides: { useModel: mock().mockImplementation(async (modelType) => { // Simulate async delay await new Promise((resolve) => setTimeout(resolve, 100)); return { result: 'async result' }; }), }, }); const result = await asyncAction.handler( setup.mockRuntime as IAgentRuntime, setup.mockMessage as Memory, setup.mockState as State, {}, setup.callbackFn as HandlerCallback ); expect(result).toBe(true); expect(setup.callbackFn).toHaveBeenCalled(); }); }); ``` *** ## 4. Testing Providers ```typescript // src/__tests__/providers.test.ts import { describe, expect, it, mock, beforeEach, afterEach } from 'bun:test'; import { myProvider } from '../providers/myProvider'; import { createMockRuntime, createMockMemory, createMockState } from './test-utils'; import { type IAgentRuntime, type Memory, type State } from '@elizaos/core'; describe('My Provider', () => { let mockRuntime: any; let mockMessage: Partial; let mockState: Partial; beforeEach(() => { mockRuntime = createMockRuntime(); mockMessage = createMockMemory(); mockState = createMockState(); }); afterEach(() => { mock.restore(); }); it('should have required properties', () => { expect(myProvider.name).toBe('MY_PROVIDER'); expect(myProvider.get).toBeDefined(); expect(typeof myProvider.get).toBe('function'); }); it('should return data in correct format', async () => { // Mock any runtime methods the provider uses mockRuntime.getMemories = mock().mockResolvedValue([ { content: { text: 'Memory 1' }, createdAt: Date.now() }, { content: { text: 'Memory 2' }, createdAt: Date.now() - 1000 }, ]); const result = await myProvider.get( mockRuntime as IAgentRuntime, mockMessage as Memory, mockState as State ); expect(result).toMatchObject({ text: expect.any(String), data: expect.any(Object), }); }); it('should handle empty data gracefully', async () => { mockRuntime.getMemories = mock().mockResolvedValue([]); const result = await myProvider.get( mockRuntime as IAgentRuntime, mockMessage as Memory, mockState as State ); expect(result).toBeDefined(); expect(result.text).toContain('No data available'); }); it('should handle errors gracefully', async () => { mockRuntime.getMemories = mock().mockRejectedValue(new Error('Database error')); const result = await myProvider.get( mockRuntime as IAgentRuntime, mockMessage as Memory, mockState as State ); expect(result).toBeDefined(); expect(result.text).toContain('Error retrieving data'); }); }); ``` *** ## 5. Testing Evaluators ```typescript // src/__tests__/evaluators.test.ts import { describe, expect, it, mock, beforeEach, afterEach } from 'bun:test'; import { myEvaluator } from '../evaluators/myEvaluator'; import { createMockRuntime, createMockMemory, createMockState } from './test-utils'; import { type IAgentRuntime, type Memory, type State } from '@elizaos/core'; describe('My Evaluator', () => { let mockRuntime: any; let mockMessage: Partial; let mockState: Partial; beforeEach(() => { mockRuntime = createMockRuntime(); mockMessage = createMockMemory(); mockState = createMockState(); }); afterEach(() => { mock.restore(); }); it('should have required properties', () => { expect(myEvaluator.name).toBe('MY_EVALUATOR'); expect(myEvaluator.evaluate).toBeDefined(); expect(myEvaluator.validate).toBeDefined(); }); it('should validate when conditions are met', async () => { const isValid = await myEvaluator.validate( mockRuntime as IAgentRuntime, mockMessage as Memory, mockState as State ); expect(isValid).toBe(true); }); it('should evaluate and create memory', async () => { mockRuntime.createMemory = mock().mockResolvedValue({ id: 'new-memory-id' }); await myEvaluator.evaluate( mockRuntime as IAgentRuntime, mockMessage as Memory, mockState as State, {} ); expect(mockRuntime.createMemory).toHaveBeenCalledWith( expect.objectContaining({ content: expect.objectContaining({ text: expect.any(String), }), }), expect.any(String) // tableName ); }); it('should not create memory when evaluation fails', async () => { // Mock a scenario where evaluation should fail mockMessage.content = { text: 'invalid content' }; await myEvaluator.evaluate( mockRuntime as IAgentRuntime, mockMessage as Memory, mockState as State, {} ); expect(mockRuntime.createMemory).not.toHaveBeenCalled(); }); }); ``` *** ## 6. Testing Services ```typescript // src/__tests__/services.test.ts import { describe, expect, it, mock, beforeEach, afterEach } from 'bun:test'; import { myService } from '../services/myService'; import { createMockRuntime } from './test-utils'; import { type IAgentRuntime } from '@elizaos/core'; describe('My Service', () => { let mockRuntime: any; beforeEach(() => { mockRuntime = createMockRuntime(); }); afterEach(() => { mock.restore(); }); it('should initialize service', async () => { const service = await myService.initialize(mockRuntime as IAgentRuntime); expect(service).toBeDefined(); expect(service.start).toBeDefined(); expect(service.stop).toBeDefined(); }); it('should start service successfully', async () => { const service = await myService.initialize(mockRuntime as IAgentRuntime); const startSpy = mock(service.start); await service.start(); expect(startSpy).toHaveBeenCalled(); }); it('should stop service successfully', async () => { const service = await myService.initialize(mockRuntime as IAgentRuntime); await service.start(); const stopSpy = mock(service.stop); await service.stop(); expect(stopSpy).toHaveBeenCalled(); }); it('should handle service errors', async () => { const service = await myService.initialize(mockRuntime as IAgentRuntime); service.start = mock().mockRejectedValue(new Error('Service start failed')); await expect(service.start()).rejects.toThrow('Service start failed'); }); }); ``` *** ## 7. Testing Event Handlers ```typescript // src/__tests__/events.test.ts import { describe, expect, it, mock, beforeEach, afterEach } from 'bun:test'; import { myPlugin } from '../index'; import { setupActionTest } from './test-utils'; import { type IAgentRuntime, type Memory, EventType, type MessagePayload, type EntityPayload, } from '@elizaos/core'; describe('Event Handlers', () => { let mockRuntime: any; let mockMessage: Partial; let mockCallback: any; beforeEach(() => { const setup = setupActionTest(); mockRuntime = setup.mockRuntime; mockMessage = setup.mockMessage; mockCallback = setup.callbackFn; }); afterEach(() => { mock.restore(); }); it('should handle MESSAGE_RECEIVED event', async () => { const messageHandler = myPlugin.events?.[EventType.MESSAGE_RECEIVED]?.[0]; expect(messageHandler).toBeDefined(); if (messageHandler) { await messageHandler({ runtime: mockRuntime as IAgentRuntime, message: mockMessage as Memory, callback: mockCallback, source: 'test', } as MessagePayload); expect(mockRuntime.createMemory).toHaveBeenCalledWith(mockMessage, 'messages'); } }); it('should handle ENTITY_JOINED event', async () => { const entityHandler = myPlugin.events?.[EventType.ENTITY_JOINED]?.[0]; expect(entityHandler).toBeDefined(); if (entityHandler) { await entityHandler({ runtime: mockRuntime as IAgentRuntime, entityId: 'test-entity-id', worldId: 'test-world-id', roomId: 'test-room-id', metadata: { type: 'user', username: 'testuser', }, source: 'test', } as EntityPayload); expect(mockRuntime.ensureConnection).toHaveBeenCalled(); } }); }); ``` *** ## 8. Advanced Testing Patterns ### Testing with Complex State ```typescript describe('Complex State Action', () => { it('should handle complex state transformations', async () => { const setup = setupActionTest({ stateOverrides: { values: { taskList: ['task1', 'task2'], currentStep: 2, metadata: { key: 'value' }, }, data: { customData: { nested: { value: 'deep', }, }, }, }, }); const result = await complexAction.handler( setup.mockRuntime as IAgentRuntime, setup.mockMessage as Memory, setup.mockState as State, {}, setup.callbackFn as HandlerCallback ); expect(result).toBe(true); }); }); ``` ### Testing with Multiple Mock Responses ```typescript describe('Sequential Operations', () => { it('should handle sequential API calls', async () => { const setup = setupActionTest({ runtimeOverrides: { useModel: mock() .mockResolvedValueOnce({ step: 1, data: 'first' }) .mockResolvedValueOnce({ step: 2, data: 'second' }) .mockResolvedValueOnce({ step: 3, data: 'final' }), }, }); await sequentialAction.handler( setup.mockRuntime as IAgentRuntime, setup.mockMessage as Memory, setup.mockState as State, {}, setup.callbackFn as HandlerCallback ); expect(setup.mockRuntime.useModel).toHaveBeenCalledTimes(3); expect(setup.callbackFn).toHaveBeenCalledWith( expect.objectContaining({ text: expect.stringContaining('final'), }) ); }); }); ``` ### Testing Error Recovery ```typescript describe('Error Recovery', () => { it('should retry on failure', async () => { let attempts = 0; const setup = setupActionTest({ runtimeOverrides: { useModel: mock().mockImplementation(async () => { attempts++; if (attempts < 3) { throw new Error('Temporary failure'); } return { success: true }; }), }, }); await retryAction.handler( setup.mockRuntime as IAgentRuntime, setup.mockMessage as Memory, setup.mockState as State, {}, setup.callbackFn as HandlerCallback ); expect(attempts).toBe(3); expect(setup.callbackFn).toHaveBeenCalledWith( expect.objectContaining({ content: expect.objectContaining({ success: true }), }) ); }); }); ``` *** ## 9. Best Practices ### 1. Test Organization * Group related tests using `describe` blocks * Use clear, descriptive test names * Follow the Arrange-Act-Assert pattern * Keep tests focused and independent ### 2. Mock Management ```typescript // Good: Specific mocks for each test it('should handle specific scenario', async () => { const setup = setupActionTest({ runtimeOverrides: { useModel: mock().mockResolvedValue({ specific: 'response' }), }, }); // ... test implementation }); // Bad: Global mocks that affect all tests beforeAll(() => { globalMock = mock().mockResolvedValue('global response'); }); ``` ### 3. Assertion Patterns ```typescript // Check callback was called with correct structure expect(callbackFn).toHaveBeenCalledWith( expect.objectContaining({ text: expect.stringContaining('expected text'), content: expect.objectContaining({ success: true, data: expect.arrayContaining(['item1', 'item2']), }), }) ); // Check multiple calls in sequence const calls = (callbackFn as any).mock.calls; expect(calls).toHaveLength(3); expect(calls[0][0].text).toContain('step 1'); expect(calls[1][0].text).toContain('step 2'); expect(calls[2][0].text).toContain('completed'); ``` ### 4. Testing Edge Cases ```typescript describe('Edge Cases', () => { it('should handle empty input', async () => { mockMessage.content = { text: '' }; // ... test implementation }); it('should handle null values', async () => { mockMessage.content = null as any; // ... test implementation }); it('should handle very long input', async () => { mockMessage.content = { text: 'a'.repeat(10000) }; // ... test implementation }); }); ``` ### 5. Async Testing Best Practices ```typescript // Always await async operations it('should handle async operations', async () => { const promise = someAsyncOperation(); await expect(promise).resolves.toBe(expectedValue); }); // Test rejected promises it('should handle errors', async () => { const promise = failingOperation(); await expect(promise).rejects.toThrow('Expected error'); }); // Use async/await instead of .then() it('should process data', async () => { const result = await processData(); expect(result).toBeDefined(); }); ``` ### 6. Cleanup ```typescript afterEach(() => { // Reset all mocks after each test mock.restore(); // Clean up any side effects // Clear timers, close connections, etc. }); ``` ### 7. Test Coverage Requirements **IMPORTANT**: All ElizaOS plugins must maintain 100% test coverage or as close to it as possible (minimum 95%). ```typescript // Ensure all code paths are tested describe('Complete Coverage', () => { it('should test success path', async () => { // Test the happy path }); it('should test error handling', async () => { // Test error scenarios }); it('should test edge cases', async () => { // Test boundary conditions }); it('should test all conditional branches', async () => { // Test if/else, switch cases, etc. }); }); ``` **Coverage Best Practices:** * Use `bun test --coverage` to check coverage regularly * Set up CI/CD to fail builds with coverage below 95% * Document any legitimate reasons for uncovered code * Focus on meaningful tests, not just hitting coverage numbers * Test all exports from your plugin (actions, providers, evaluators, services) *** ## 10. Running Tests ### Basic Commands ```bash # Run all tests bun run test # Run tests in watch mode bun run test --watch # Run specific test file bun run test src/__tests__/actions.test.ts # Run tests with coverage bun run test:coverage # Run tests matching pattern bun run test --test-name-pattern "should validate" ``` ### Test Configuration Create a `bunfig.toml` file in your project root: ```toml [test] root = "./src/__tests__" coverage = true coverageThreshold = 95 # Minimum 95% coverage required, aim for 100% ``` ### Debugging Tests ```typescript // Add console.logs for debugging it('should debug test', async () => { console.log('Current state:', mockState); const result = await action.handler(...); console.log('Result:', result); console.log('Callback calls:', (callbackFn as any).mock.calls); }); ``` ### Common Issues and Solutions #### Issue: Mock not being called ```typescript // Solution: Ensure the mock is set before the action is called mockRuntime.useModel = mock().mockResolvedValue(response); // THEN call the action await action.handler(...); ``` #### Issue: Tests timing out ```typescript // Solution: Mock all async dependencies beforeEach(() => { // Mock all external calls mockRuntime.getMemories = mock().mockResolvedValue([]); mockRuntime.searchMemories = mock().mockResolvedValue([]); mockRuntime.createMemory = mock().mockResolvedValue({ id: 'test' }); }); ``` #### Issue: Inconsistent test results ```typescript // Solution: Reset mocks between tests afterEach(() => { mock.restore(); }); // And use fresh setup for each test beforeEach(() => { const setup = setupActionTest(); // ... assign to test variables }); ``` *** ## Summary This guide provides a comprehensive approach to testing ElizaOS plugins. Key takeaways: 1. **Setup a consistent test environment** with reusable utilities 2. **Test all plugin components**: actions, providers, evaluators, services, and event handlers 3. **Mock external dependencies** properly to ensure isolated tests 4. **Handle async operations** correctly with proper awaits 5. **Follow best practices** for organization, assertions, and cleanup 6. **Run tests regularly** as part of your development workflow Remember: Good tests are as important as good code. They ensure your plugin works correctly and continues to work as the codebase evolves. # Plugin Publishing Guide Source: https://eliza.how/guides/plugin-publishing-guide A complete guide to creating, developing, and publishing ElizaOS plugins This guide walks you through the entire process of creating, developing, and publishing an ElizaOS plugin. By the end, you'll have a published plugin available in the ElizaOS registry. ## Prerequisites Before you begin, ensure you have: * **Bun** installed ([installation guide](https://bun.sh)) * **Git** installed and configured * **npm** account ([create one here](https://www.npmjs.com/signup)) * **GitHub** account with a Personal Access Token (PAT) ### Setting up GitHub Personal Access Token 1. Go to GitHub → Settings → Developer settings → Personal access tokens 2. Click "Generate new token (classic)" 3. Name it "ElizaOS Publishing" 4. Select scopes: `repo`, `read:org`, and `workflow` 5. Save the token securely ## Step 1: Create Your Plugin Start by creating a new plugin using the ElizaOS CLI: ```bash # Create a new plugin with the interactive wizard elizaos create -t plugin my-awesome-plugin # Navigate to your plugin directory cd plugin-my-awesome-plugin ``` The CLI automatically prefixes your plugin name with `plugin-` to follow ElizaOS conventions. ## Step 2: Understand Plugin Structure Your new plugin has this structure: ``` plugin-my-awesome-plugin/ ├── src/ │ └── index.ts # Main plugin file ├── images/ # Required images for registry │ ├── logo.jpg # 400x400px square logo │ └── banner.jpg # 1280x640px banner ├── __tests__/ # Test files ├── package.json # Plugin metadata ├── tsconfig.json # TypeScript configuration └── README.md # Plugin documentation ``` ### Key Files to Edit 1. **`src/index.ts`** - Your plugin's main code 2. **`package.json`** - Plugin metadata and configuration 3. **`README.md`** - Documentation for users 4. **`images/`** - Visual assets for the registry ## Step 3: Develop Your Plugin ### Basic Plugin Structure ```typescript // src/index.ts import { Plugin, IAgentRuntime } from "@elizaos/core"; export const myAwesomePlugin: Plugin = { name: "my-awesome-plugin", description: "A plugin that does awesome things", actions: [ // Your custom actions ], providers: [ // Your custom providers ], evaluators: [ // Your custom evaluators ], services: [ // Your custom services ], async init(runtime: IAgentRuntime) { // Initialize your plugin console.log("My Awesome Plugin initialized!"); } }; export default myAwesomePlugin; ``` ### Adding an Action ```typescript import { Action, IAgentRuntime, Memory, HandlerCallback } from "@elizaos/core"; const greetAction: Action = { name: "GREET_USER", description: "Greets the user with a personalized message", validate: async (runtime: IAgentRuntime, message: Memory) => { // Validate the action can be performed return message.content.text.toLowerCase().includes("hello"); }, handler: async ( runtime: IAgentRuntime, message: Memory, state: any, options: any, callback: HandlerCallback ) => { // Perform the action const response = `Hello! Welcome to ${runtime.character.name}!`; callback({ text: response, action: "GREET_USER" }); }, examples: [ [ { user: "user123", content: { text: "Hello there!" } }, { user: "assistant", content: { text: "Hello! Welcome to Eliza!", action: "GREET_USER" } } ] ] }; ``` ### Development Commands ```bash # Install dependencies bun install # Start development mode with hot reload elizaos dev # Run tests elizaos test # Build your plugin bun run build ``` ## Step 4: Configure package.json Update your `package.json` with accurate information: ```json { "name": "plugin-my-awesome-plugin", "version": "1.0.0", "description": "A plugin that adds awesome functionality to ElizaOS agents", "main": "dist/index.js", "types": "dist/index.d.ts", "author": "Your Name ", "license": "MIT", "repository": "github:yourusername/plugin-my-awesome-plugin", "keywords": ["elizaos-plugin", "eliza-plugin", "ai", "chatbot"], "scripts": { "build": "tsc", "dev": "tsc --watch", "test": "vitest", "lint": "eslint src --ext ts" }, "dependencies": { "@elizaos/core": "latest" }, "devDependencies": { "typescript": "^5.0.0", "vitest": "^1.0.0", "@types/node": "^20.0.0" }, "agentConfig": { "actions": ["GREET_USER"], "providers": [], "evaluators": [], "models": ["gpt-4", "gpt-3.5-turbo", "claude-3"], "services": [] } } ``` The `agentConfig` section is required for your plugin to be properly loaded by ElizaOS agents. ## Step 5: Add Required Images Your plugin needs two images for the registry: ### Logo (images/logo.jpg) * **Size**: 400x400 pixels * **Format**: JPEG * **Max file size**: 500KB * **Purpose**: Displayed in plugin listings ### Banner (images/banner.jpg) * **Size**: 1280x640 pixels * **Format**: JPEG * **Max file size**: 1MB * **Purpose**: Featured on your plugin's detail page Use high-quality images that represent your plugin's functionality. The logo should be clear at small sizes. ## Step 6: Write Documentation Create a comprehensive README.md: ```markdown # My Awesome Plugin A plugin that adds awesome functionality to ElizaOS agents. ## Features - ✨ Feature 1: Personalized greetings - 🚀 Feature 2: Advanced responses - 🎯 Feature 3: Custom actions ## Installation \`\`\`bash elizaos plugins add @your-npm-username/plugin-my-awesome-plugin \`\`\` ## Configuration Add to your agent's character file: \`\`\`json { "plugins": ["@your-npm-username/plugin-my-awesome-plugin"] } \`\`\` ## Usage The plugin automatically adds the following actions: - `GREET_USER`: Responds to hello messages ## API Reference ### Actions #### GREET_USER Greets the user with a personalized message. **Trigger**: Messages containing "hello" **Response**: Personalized greeting ## License MIT ``` ## Step 7: Test Your Plugin Before publishing, thoroughly test your plugin: ```bash # Run unit tests elizaos test # Test in a real project cd ../test-project elizaos plugins add ../plugin-my-awesome-plugin elizaos dev ``` ### Testing Checklist * [ ] All tests pass * [ ] Plugin loads without errors * [ ] Actions trigger correctly * [ ] No TypeScript errors * [ ] Documentation is complete * [ ] Images are correct size/format ## Step 8: Pre-publish Validation Run a test publish to catch any issues: ```bash # Dry run to see what would happen elizaos publish --test # Check generated registry files elizaos publish --dry-run ls packages/registry/ ``` ### Common Validation Issues 1. **Missing images**: Ensure both logo.jpg and banner.jpg exist 2. **Invalid package name**: Must start with `plugin-` 3. **Missing agentConfig**: Required in package.json 4. **Image size**: Check dimensions and file size ## Step 9: Publish Your Plugin ### Authenticate First ```bash # Login to npm bunx npm login # Set GitHub token (or you'll be prompted) export GITHUB_TOKEN=your_pat_here ``` ### Publish ```bash # Publish to npm and submit to registry elizaos publish --npm ``` This command will: 1. Validate your plugin structure 2. Build your TypeScript code 3. Publish to npm 4. Create a GitHub repository 5. Open a PR to the ElizaOS registry ### What Happens Next 1. **npm Package**: Available immediately at `npmjs.com/package/your-plugin` 2. **GitHub Repo**: Created at `github.com/yourusername/plugin-my-awesome-plugin` 3. **Registry PR**: Opened at `github.com/elizaos-plugins/registry/pulls` Registry PRs are reviewed by maintainers. Approval typically takes 1-3 business days. ## Step 10: Post-Publishing ### Updating Your Plugin For updates after initial publication: ```bash # 1. Make your changes # 2. Update version in package.json bun version patch # or minor/major # 3. Build and test bun run build elizaos test # 4. Publish directly to npm bun publish # 5. Push to GitHub git add . git commit -m "Update to version x.y.z" git push git push --tags ``` The `elizaos publish` command is only for initial publication. Use standard npm/git commands for updates. ### Marketing Your Plugin 1. **Announce on Discord**: Share in the #plugins channel 2. **Write a Blog Post**: Explain what your plugin does 3. **Create Examples**: Show real-world usage 4. **Record a Demo**: Video tutorials help adoption ## Troubleshooting ### Build Failures ```bash # Clear and rebuild rm -rf dist node_modules bun install bun run build ``` ### Publishing Errors ```bash # Check npm authentication bunx npm whoami # Verify GitHub token echo $GITHUB_TOKEN # Try step-by-step elizaos publish --test # Test first elizaos publish --npm --skip-registry # Skip registry if needed ``` ### Registry PR Issues If your PR is not approved: 1. Check comments from reviewers 2. Fix any requested changes 3. Update your PR branch ## Best Practices ### Code Quality * Write clean, documented code * Include comprehensive tests * Use TypeScript types properly * Follow ElizaOS conventions ### Documentation * Clear README with examples * API documentation for all exports * Configuration examples * Troubleshooting section ### Versioning * Use semantic versioning * Document breaking changes * Maintain a CHANGELOG.md * Tag releases in Git ### Community * Respond to issues on GitHub * Help users in Discord * Accept contributions * Keep plugin updated ## Resources * [ElizaOS Core Documentation](/core-concepts) * [Plugin API Reference](/api-reference) * [Example Plugins](https://github.com/elizaos-plugins) * [Discord Community](https://discord.gg/ai16z) ## Getting Help If you run into issues: 1. Check this guide's troubleshooting section 2. Search existing GitHub issues 3. Ask in Discord #plugin-dev channel 4. Open an issue on your plugin's repo Remember: Publishing a plugin is just the beginning. The best plugins evolve based on user feedback and community contributions! # Introduction Source: https://eliza.how/index Welcome to elizaos - Your AI Agent Framework Hero Light Hero Dark ## Getting Started Build powerful AI agents with elizaos - a flexible framework for creating autonomous AI systems. Get your first AI agent running in minutes Learn how to develop and customize your agents ## Core Features Explore the powerful capabilities that make elizaos the choice for AI agent development. Understand the core agent system and architecture Complete API documentation for all elizaos modules Extend functionality with the plugin ecosystem Learn from real-world agent implementations # Message Processing Core Source: https://eliza.how/plugins/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)**\ Comprehensive guide covering all components, architecture, and implementation details * **[Message Flow Diagram](/plugins/bootstrap/message-flow)**\ Step-by-step breakdown of how messages flow through the system with visual diagrams * **[Examples & Recipes](/plugins/bootstrap/examples)**\ Practical examples, code snippets, and real-world implementations * **[Testing Guide](/plugins/bootstrap/testing-guide)**\ Testing patterns, best practices, and comprehensive test examples # Complete Developer Guide Source: https://eliza.how/plugins/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 graph 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 ``` ## 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://eliza.how/plugins/bootstrap/examples Practical examples and recipes for building agents with the bootstrap plugin # Examples - Building with @elizaos/plugin-bootstrap 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'; import { bootstrapPlugin } from '@elizaos/plugin-bootstrap'; import { sqlitePlugin } from '@elizaos/plugin-sql'; // 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, sqlitePlugin, 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://eliza.how/plugins/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 ``` 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 ``` Actions = [REPLY] only AND Providers = [] → Simple Response Otherwise → Complex Response with Action Processing ``` ### 3. Evaluator Trigger Conditions ``` 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://eliza.how/plugins/bootstrap/testing-guide Testing patterns and best practices for the bootstrap plugin # Testing Guide for @elizaos/plugin-bootstrap 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://eliza.how/plugins/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) * [DeFi Operations Flow →](./evm/defi-operations-flow) * [Examples →](./evm/examples) * [Testing Guide →](./evm/testing-guide) # Developer Guide Source: https://eliza.how/plugins/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: ``` ┌─────────────────┐ ┌──────────────┐ ┌──────────────┐ │ Actions │────▶│ Service │────▶│ Blockchain │ │ (User Intent) │ │ (EVMService)│ │ (Viem) │ └─────────────────┘ └──────────────┘ └──────────────┘ │ │ ▼ ▼ ┌─────────────────┐ ┌──────────────┐ │ Templates │ │ Providers │ │ (AI Prompts) │ │ (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://eliza.how/plugins/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://eliza.how/plugins/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://eliza.how/plugins/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://eliza.how/plugins/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) * [DeFi Operations Flow →](./solana/defi-operations-flow) * [Examples →](./solana/examples) * [Testing Guide →](./solana/testing-guide) # Developer Guide Source: https://eliza.how/plugins/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: ``` ┌─────────────────┐ ┌──────────────┐ ┌──────────────┐ │ Actions │────▶│ SolanaService│────▶│ Solana RPC │ │ (User Intent) │ │ (Core Logic)│ │ Connection │ └─────────────────┘ └──────────────┘ └──────────────┘ │ │ │ ▼ ▼ ▼ ┌─────────────────┐ ┌──────────────┐ ┌──────────────┐ │ AI Templates │ │ Providers │ │ Birdeye API │ │ (NLP Parsing) │ │ (Wallet Data)│ │ (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://eliza.how/plugins/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://eliza.how/plugins/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://eliza.how/plugins/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://eliza.how/plugins/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://eliza.how/plugins/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://eliza.how/plugins/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://eliza.how/plugins/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://eliza.how/plugins/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://eliza.how/plugins/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://eliza.how/plugins/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) - Full-featured with all model types * [Anthropic Plugin](./anthropic) - Claude models for text generation * [Google GenAI Plugin](./google-genai) - Gemini models * [OpenRouter Plugin](./openrouter) - Access to multiple providers ### Local/Self-Hosted * [Ollama Plugin](./ollama) - 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://eliza.how/plugins/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://eliza.how/plugins/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://eliza.how/plugins/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://eliza.how/plugins/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://eliza.how/plugins/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://eliza.how/plugins/overview Comprehensive guide to the ElizaOS plugin system architecture and implementation ## Overview The Eliza 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. ## 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. ## 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://eliza.how/plugins/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://eliza.how/plugins/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://eliza.how/plugins/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://eliza.how/plugins/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://eliza.how/plugins/platform/discord/testing-guide This guide covers testing strategies, patterns, and best practices for the @elizaos/plugin-discord package. # Discord Plugin Testing Guide 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 # Telegram Integration Source: https://eliza.how/plugins/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://eliza.how/plugins/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://eliza.how/plugins/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://eliza.how/plugins/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://eliza.how/plugins/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://eliza.how/plugins/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://eliza.how/plugins/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://eliza.how/plugins/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://eliza.how/plugins/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://eliza.how/plugins/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 # Database Management Source: https://eliza.how/plugins/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: ``` ┌─────────────────────────────────┐ │ ElizaOS Runtime │ └────────────┬────────────────────┘ │ ┌────────────▼────────────────────┐ │ DatabaseMigrationService │ │ • Schema Discovery │ │ • Migration Orchestration │ └────────────┬────────────────────┘ │ ┌────────────▼────────────────────┐ │ Database Adapters │ ├─────────────────────────────────┤ │ PGLite Adapter │ PG Adapter │ │ • Development │ • Production │ │ • File-based │ • 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) - Detailed adapter documentation * [Schema Management](./schema-management) - Creating and managing schemas * [Plugin Tables Guide](./plugin-tables) - Adding tables to your plugin * [Examples](./examples) - Real-world usage patterns # Database Adapters Source: https://eliza.how/plugins/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://eliza.how/plugins/sql/examples Practical code examples and patterns # SQL Plugin Examples 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://eliza.how/plugins/sql/plugin-tables How plugins can define their own database tables # Plugin Tables Guide 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://eliza.how/plugins/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. # Quickstart Source: https://eliza.how/quickstart Get Eliza up and running in under 5 minutes ## Prerequisites Before you begin, make sure you have the following installed: Install Node.js version 23.3 or higher Install Bun runtime for faster package management ## Quick Installation Follow these steps to get Eliza running quickly: Install the Eliza CLI globally using Bun: ```bash bun i -g @elizaos/cli ``` Create a new Eliza agent with your chosen name: ```bash elizaos create ``` Replace `` with your desired agent name. During the setup process, you'll be prompted to make the following selections: 1. **Database**: Select `pglite` for a lightweight PostgreSQL option 2. **Model Provider**: Select `openai` 3. **API Key**: Provide your OpenAI API key when prompted Make sure you have your OpenAI API key ready. You can get one from the [OpenAI Platform](https://platform.openai.com/). Change directory to your newly created agent: ```bash cd ``` Launch your Eliza agent: ```bash elizaos start ``` Your agent is now running and ready to interact! Open your browser and navigate to: ``` http://localhost:3000 ``` Start chatting with your Eliza agent through the web interface! ## What's Next? Check out the main Eliza repository for more advanced features and configurations Learn how to customize your agent's personality, capabilities, and integrations Ready to go live? Learn how to deploy your agent to production Connect with other Eliza developers and get support ## Troubleshooting If you encounter issues with Node.js, make sure you're running version 23.3 or higher: ```bash node --version ``` You can use [nvm](https://github.com/nvm-sh/nvm) or [fnm](https://github.com/Schniz/fnm) to manage Node.js versions. If you're getting authentication errors: * Verify your API key is correct * Check that your OpenAI account has available credits * Ensure your API key has the necessary permissions If pglite isn't working properly: * Try restarting the agent * Check that the database files have proper permissions * Consider using a different database option if issues persist