# 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
## 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
`,
// 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
`,
// 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
`,
},
};
```
### 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
`,
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
`,
},
};
```
## 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-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-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
`,
},
// 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
`,
// 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
`,
// 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
`,
},
};
```
### 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
`,
},
};
```
## 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