Astreus - Docs
Guides

Creating Custom Plugins

Creating Custom Plugins

This guide shows you how to create custom plugins for Astreus agents. With the agent-centric architecture, plugins are tools that extend agent capabilities through the agent.addTool() method.

Plugin Architecture

A plugin in Astreus is essentially a Tool that provides:

  • Specific functionality (e.g., API calls, data processing, external integrations)
  • Type-safe parameters with JSON Schema validation
  • Structured responses with success/error handling
  • Agent integration through the tool interface

Basic Plugin Structure

Here's the basic structure of a custom plugin:

import { Tool } from '@astreus-ai/astreus';

const myCustomTool: Tool = {
  name: 'tool_name',
  description: 'What this tool does',
  parameters: [
    {
      name: 'param_name',
      type: 'string',
      description: 'Parameter description',
      required: true
    }
  ],
  execute: async (params: any) => {
    try {
      // Tool implementation
      return {
        success: true,
        data: result
      };
    } catch (error) {
      return {
        success: false,
        error: error.message
      };
    }
  }
};

Step-by-Step Plugin Creation

Step 1: Define Your Tool

Let's create a weather plugin as an example:

import { Tool } from '@astreus-ai/astreus';

const weatherTool: Tool = {
  name: 'get_weather',
  description: 'Get current weather information for a specific location',
  parameters: [
    {
      name: 'location',
      type: 'string',
      description: 'The city and country (e.g., "London, UK")',
      required: true
    },
    {
      name: 'units',
      type: 'string',
      description: 'Temperature units',
      required: false,
      default: 'celsius'
    }
  ],
  execute: async (params: { location: string; units?: string }) => {
    try {
      // Simulate API call to weather service
      const response = await fetch(
        `https://api.openweathermap.org/data/2.5/weather?q=${params.location}&appid=${process.env.OPENWEATHER_API_KEY}&units=${params.units === 'fahrenheit' ? 'imperial' : 'metric'}`
      );
      
      if (!response.ok) {
        throw new Error(`Weather API error: ${response.statusText}`);
      }
      
      const data = await response.json();
      
      return {
        success: true,
        data: {
          location: data.name,
          country: data.sys.country,
          temperature: Math.round(data.main.temp),
          condition: data.weather[0].description,
          humidity: data.main.humidity,
          windSpeed: data.wind.speed,
          units: params.units || 'celsius'
        }
      };
    } catch (error) {
      return {
        success: false,
        error: `Failed to get weather for ${params.location}: ${error.message}`
      };
    }
  }
};

Step 2: Create a Plugin Factory

For reusable plugins, create a factory function:

interface WeatherPluginConfig {
  apiKey: string;
  defaultUnits?: 'celsius' | 'fahrenheit';
  timeout?: number;
}

function createWeatherPlugin(config: WeatherPluginConfig) {
  const weatherTool: Tool = {
    name: 'get_weather',
    description: 'Get current weather information for a specific location',
    parameters: [
      {
        name: 'location',
        type: 'string',
        description: 'The city and country (e.g., "London, UK")',
        required: true
      },
      {
        name: 'units',
        type: 'string',
        description: 'Temperature units',
        required: false
      }
    ],
    execute: async (params: { location: string; units?: string }) => {
      const units = params.units || config.defaultUnits || 'celsius';
      
      try {
        const controller = new AbortController();
        const timeoutId = setTimeout(() => controller.abort(), config.timeout || 10000);
        
        const response = await fetch(
          `https://api.openweathermap.org/data/2.5/weather?q=${params.location}&appid=${config.apiKey}&units=${units === 'fahrenheit' ? 'imperial' : 'metric'}`,
          { signal: controller.signal }
        );
        
        clearTimeout(timeoutId);
        
        if (!response.ok) {
          const errorData = await response.json();
          throw new Error(errorData.message || `HTTP ${response.status}`);
        }
        
        const data = await response.json();
        
        return {
          success: true,
          data: {
            location: data.name,
            country: data.sys.country,
            temperature: Math.round(data.main.temp),
            condition: data.weather[0].description,
            humidity: data.main.humidity,
            windSpeed: data.wind.speed,
            pressure: data.main.pressure,
            units: units
          }
        };
      } catch (error) {
        if (error.name === 'AbortError') {
          return {
            success: false,
            error: `Weather request timed out for ${params.location}`
          };
        }
        
        return {
          success: false,
          error: `Failed to get weather for ${params.location}: ${error.message}`
        };
      }
    }
  };

  return {
    getTool: () => weatherTool,
    name: 'weather',
    version: '1.0.0',
    description: 'Weather information plugin using OpenWeatherMap API'
  };
}

Step 3: Use Your Plugin with an Agent

import { createAgent, createProvider, createMemory, createDatabase } from '@astreus-ai/astreus';

async function createWeatherAgent() {
  const db = await createDatabase();
  const memory = await createMemory({ database: db });
  const provider = createProvider({ type: 'openai', model: 'gpt-4o-mini' });

  const agent = await createAgent({
    name: 'WeatherAssistant',
    provider: provider,
    memory: memory,
    systemPrompt: "You are a helpful weather assistant. Use the weather tool to provide current weather information when asked."
  });

  // Create and add the weather plugin
  const weatherPlugin = createWeatherPlugin({
    apiKey: process.env.OPENWEATHER_API_KEY!,
    defaultUnits: 'celsius',
    timeout: 5000
  });

  agent.addTool(weatherPlugin.getTool());

  return agent;
}

// Use the weather agent
const agent = await createWeatherAgent();

const response = await agent.chat({
  stream: true,
  message: "What's the weather like in Tokyo?",
  sessionId: "weather-session",
  onChunk: (chunk) => console.log(chunk)
});

Advanced Plugin Examples

Database Plugin

A plugin that provides database operations:

import { Tool } from '@astreus-ai/astreus';
import { Pool } from 'pg';

interface DatabasePluginConfig {
  connectionString: string;
  maxConnections?: number;
}

function createDatabasePlugin(config: DatabasePluginConfig) {
  const pool = new Pool({
    connectionString: config.connectionString,
    max: config.maxConnections || 10
  });

  const queryTool: Tool = {
    name: 'database_query',
    description: 'Execute a SELECT query on the database',
    parameters: [
      {
        name: 'query',
        type: 'string',
        description: 'SQL SELECT query to execute',
        required: true
      },
      {
        name: 'params',
        type: 'array',
        description: 'Query parameters for prepared statements',
        required: false
      }
    ],
    execute: async (params: { query: string; params?: string[] }) => {
      try {
        // Basic SQL injection protection
        if (!params.query.trim().toLowerCase().startsWith('select')) {
          throw new Error('Only SELECT queries are allowed');
        }

        const result = await pool.query(params.query, params.params);
        
        return {
          success: true,
          data: {
            rows: result.rows,
            rowCount: result.rowCount,
            fields: result.fields?.map(f => f.name)
          }
        };
      } catch (error) {
        return {
          success: false,
          error: `Database query failed: ${error.message}`
        };
      }
    }
  };

  const insertTool: Tool = {
    name: 'database_insert',
    description: 'Insert data into a database table',
    parameters: [
      {
        name: 'table',
        type: 'string',
        description: 'Table name to insert into',
        required: true
      },
      {
        name: 'data',
        type: 'object',
        description: 'Data to insert as key-value pairs',
        required: true
      }
    ],
    execute: async (params: { table: string; data: Record<string, any> }) => {
      try {
        const columns = Object.keys(params.data);
        const values = Object.values(params.data);
        const placeholders = values.map((_, i) => `$${i + 1}`).join(', ');
        
        const query = `INSERT INTO ${params.table} (${columns.join(', ')}) VALUES (${placeholders}) RETURNING *`;
        const result = await pool.query(query, values);
        
        return {
          success: true,
          data: {
            insertedRow: result.rows[0],
            rowCount: result.rowCount
          }
        };
      } catch (error) {
        return {
          success: false,
          error: `Database insert failed: ${error.message}`
        };
      }
    }
  };

  return {
    getTools: () => [queryTool, insertTool],
    name: 'database',
    version: '1.0.0',
    description: 'Database operations plugin',
    cleanup: () => pool.end()
  };
}

File Operations Plugin

A plugin for file system operations:

import { Tool } from '@astreus-ai/astreus';
import { promises as fs } from 'fs';
import path from 'path';

interface FilePluginConfig {
  allowedDirectories: string[];
  maxFileSize?: number;
}

function createFilePlugin(config: FilePluginConfig) {
  const validatePath = (filePath: string): boolean => {
    const resolvedPath = path.resolve(filePath);
    return config.allowedDirectories.some(dir => 
      resolvedPath.startsWith(path.resolve(dir))
    );
  };

  const readFileTool: Tool = {
    name: 'read_file',
    description: 'Read the contents of a text file',
    parameters: [
      {
        name: 'filePath',
        type: 'string',
        description: 'Path to the file to read',
        required: true
      },
      {
        name: 'encoding',
        type: 'string',
        description: 'File encoding',
        required: false,
        default: 'utf8'
      }
    ],
    execute: async (params: { filePath: string; encoding?: string }) => {
      try {
        if (!validatePath(params.filePath)) {
          throw new Error('File path not allowed');
        }

        const stats = await fs.stat(params.filePath);
        
        if (config.maxFileSize && stats.size > config.maxFileSize) {
          throw new Error(`File too large (${stats.size} bytes, max ${config.maxFileSize})`);
        }

        const content = await fs.readFile(params.filePath, params.encoding as any || 'utf8');
        
        return {
          success: true,
          data: {
            content,
            size: stats.size,
            lastModified: stats.mtime,
            encoding: params.encoding || 'utf8'
          }
        };
      } catch (error) {
        return {
          success: false,
          error: `Failed to read file: ${error.message}`
        };
      }
    }
  };

  const writeFileTool: Tool = {
    name: 'write_file',
    description: 'Write content to a text file',
    parameters: [
      {
        name: 'filePath',
        type: 'string',
        description: 'Path to the file to write',
        required: true
      },
      {
        name: 'content',
        type: 'string',
        description: 'Content to write to the file',
        required: true
      },
      {
        name: 'encoding',
        type: 'string',
        description: 'File encoding',
        required: false,
        default: 'utf8'
      }
    ],
    execute: async (params: { filePath: string; content: string; encoding?: string }) => {
      try {
        if (!validatePath(params.filePath)) {
          throw new Error('File path not allowed');
        }

        await fs.writeFile(params.filePath, params.content, params.encoding || 'utf8');
        const stats = await fs.stat(params.filePath);
        
        return {
          success: true,
          data: {
            filePath: params.filePath,
            size: stats.size,
            lastModified: stats.mtime
          }
        };
      } catch (error) {
        return {
          success: false,
          error: `Failed to write file: ${error.message}`
        };
      }
    }
  };

  const listDirectoryTool: Tool = {
    name: 'list_directory',
    description: 'List files and directories in a directory',
    parameters: [
      {
        name: 'directoryPath',
        type: 'string',
        description: 'Path to the directory to list',
        required: true
      },
      {
        name: 'includeHidden',
        type: 'boolean',
        description: 'Include hidden files and directories',
        required: false,
        default: false
      }
    ],
    execute: async (params: { directoryPath: string; includeHidden?: boolean }) => {
      try {
        if (!validatePath(params.directoryPath)) {
          throw new Error('Directory path not allowed');
        }

        const entries = await fs.readdir(params.directoryPath, { withFileTypes: true });
        
        const items = await Promise.all(
          entries
            .filter(entry => params.includeHidden || !entry.name.startsWith('.'))
            .map(async (entry) => {
              const fullPath = path.join(params.directoryPath, entry.name);
              const stats = await fs.stat(fullPath);
              
              return {
                name: entry.name,
                type: entry.isDirectory() ? 'directory' : 'file',
                size: stats.size,
                lastModified: stats.mtime
              };
            })
        );
        
        return {
          success: true,
          data: {
            directory: params.directoryPath,
            items: items
          }
        };
      } catch (error) {
        return {
          success: false,
          error: `Failed to list directory: ${error.message}`
        };
      }
    }
  };

  return {
    getTools: () => [readFileTool, writeFileTool, listDirectoryTool],
    name: 'file-operations',
    version: '1.0.0',
    description: 'File system operations plugin'
  };
}

HTTP Client Plugin

A plugin for making HTTP requests:

import { Tool } from '@astreus-ai/astreus';

interface HttpPluginConfig {
  timeout?: number;
  maxResponseSize?: number;
  allowedDomains?: string[];
}

function createHttpPlugin(config: HttpPluginConfig = {}) {
  const validateUrl = (url: string): boolean => {
    if (!config.allowedDomains) return true;
    
    try {
      const urlObj = new URL(url);
      return config.allowedDomains.some(domain => 
        urlObj.hostname === domain || urlObj.hostname.endsWith(`.${domain}`)
      );
    } catch {
      return false;
    }
  };

  const httpRequestTool: Tool = {
    name: 'http_request',
    description: 'Make an HTTP request to a URL',
    parameters: [
      {
        name: 'url',
        type: 'string',
        description: 'URL to make the request to',
        required: true
      },
      {
        name: 'method',
        type: 'string',
        description: 'HTTP method',
        required: false,
        default: 'GET'
      },
      {
        name: 'headers',
        type: 'object',
        description: 'HTTP headers as key-value pairs',
        required: false
      },
      {
        name: 'body',
        type: 'string',
        description: 'Request body (for POST, PUT, PATCH)',
        required: false
      }
    ],
    execute: async (params: { 
      url: string; 
      method?: string; 
      headers?: Record<string, string>; 
      body?: string 
    }) => {
      try {
        if (!validateUrl(params.url)) {
          throw new Error('URL not allowed');
        }

        const controller = new AbortController();
        const timeoutId = setTimeout(() => controller.abort(), config.timeout || 10000);

        const response = await fetch(params.url, {
          method: params.method || 'GET',
          headers: params.headers,
          body: params.body,
          signal: controller.signal
        });

        clearTimeout(timeoutId);

        // Check response size
        const contentLength = response.headers.get('content-length');
        if (config.maxResponseSize && contentLength && parseInt(contentLength) > config.maxResponseSize) {
          throw new Error(`Response too large (${contentLength} bytes)`);
        }

        const responseText = await response.text();
        
        // Try to parse as JSON, fallback to text
        let responseData;
        try {
          responseData = JSON.parse(responseText);
        } catch {
          responseData = responseText;
        }

        return {
          success: true,
          data: {
            status: response.status,
            statusText: response.statusText,
            headers: Object.fromEntries(response.headers.entries()),
            data: responseData
          }
        };
      } catch (error) {
        if (error.name === 'AbortError') {
          return {
            success: false,
            error: 'Request timed out'
          };
        }
        
        return {
          success: false,
          error: `HTTP request failed: ${error.message}`
        };
      }
    }
  };

  return {
    getTool: () => httpRequestTool,
    name: 'http-client',
    version: '1.0.0',
    description: 'HTTP client plugin for making web requests'
  };
}

Plugin Best Practices

1. Parameter Validation

Always validate input parameters:

const tool: Tool = {
  name: 'example_tool',
  // ... other properties
  execute: async (params: any) => {
    // Validate required parameters
    if (!params.requiredParam) {
      return {
        success: false,
        error: 'Missing required parameter: requiredParam'
      };
    }

    // Validate parameter types
    if (typeof params.stringParam !== 'string') {
      return {
        success: false,
        error: 'Parameter stringParam must be a string'
      };
    }

    // Validate parameter values
    if (params.numberParam < 0 || params.numberParam > 100) {
      return {
        success: false,
        error: 'Parameter numberParam must be between 0 and 100'
      };
    }

    // Continue with tool logic...
  }
};

2. Error Handling

Implement comprehensive error handling:

const tool: Tool = {
  name: 'robust_tool',
  // ... other properties
  execute: async (params: any) => {
    try {
      // Main tool logic
      const result = await someAsyncOperation(params);
      
      return {
        success: true,
        data: result
      };
    } catch (error) {
      // Log error for debugging
      console.error('Tool execution failed:', error);
      
      // Return user-friendly error message
      return {
        success: false,
        error: error instanceof Error ? error.message : 'Unknown error occurred'
      };
    }
  }
};

3. Timeout and Resource Management

Implement timeouts and resource limits:

const tool: Tool = {
  name: 'resource_aware_tool',
  // ... other properties
  execute: async (params: any) => {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), 30000); // 30 second timeout

    try {
      const result = await fetch(params.url, {
        signal: controller.signal
      });
      
      clearTimeout(timeoutId);
      
      return {
        success: true,
        data: await result.json()
      };
    } catch (error) {
      clearTimeout(timeoutId);
      
      if (error.name === 'AbortError') {
        return {
          success: false,
          error: 'Operation timed out'
        };
      }
      
      return {
        success: false,
        error: error.message
      };
    }
  }
};

4. Configuration and Environment Variables

Use configuration for flexibility:

interface PluginConfig {
  apiKey: string;
  baseUrl?: string;
  timeout?: number;
  retries?: number;
}

function createConfigurablePlugin(config: PluginConfig) {
  const tool: Tool = {
    name: 'configurable_tool',
    // ... other properties
    execute: async (params: any) => {
      const options = {
        timeout: config.timeout || 10000,
        retries: config.retries || 3,
        baseUrl: config.baseUrl || 'https://api.example.com'
      };

      // Use configuration in tool logic
      // ...
    }
  };

  return {
    getTool: () => tool,
    name: 'configurable-plugin',
    version: '1.0.0'
  };
}

// Usage with environment variables
const plugin = createConfigurablePlugin({
  apiKey: process.env.API_KEY!,
  baseUrl: process.env.API_BASE_URL,
  timeout: parseInt(process.env.API_TIMEOUT || '10000'),
  retries: parseInt(process.env.API_RETRIES || '3')
});

Testing Your Plugin

Create tests for your plugin:

import { describe, it, expect } from '@jest/globals';

describe('WeatherPlugin', () => {
  const plugin = createWeatherPlugin({
    apiKey: 'test-api-key',
    defaultUnits: 'celsius'
  });

  const tool = plugin.getTool();

  it('should have correct tool properties', () => {
    expect(tool.name).toBe('get_weather');
    expect(tool.description).toBeDefined();
    expect(tool.parameters).toBeDefined();
    expect(tool.execute).toBeInstanceOf(Function);
  });

  it('should validate required parameters', async () => {
    const result = await tool.execute({});
    
    expect(result.success).toBe(false);
    expect(result.error).toContain('location');
  });

  it('should handle API errors gracefully', async () => {
    // Mock fetch to return error
    global.fetch = jest.fn().mockRejectedValue(new Error('Network error'));
    
    const result = await tool.execute({ location: 'London' });
    
    expect(result.success).toBe(false);
    expect(result.error).toContain('Network error');
  });
});

Publishing Your Plugin

To share your plugin with others:

1. Create a Package

{
  "name": "@your-org/astreus-weather-plugin",
  "version": "1.0.0",
  "description": "Weather plugin for Astreus agents",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "scripts": {
    "build": "tsc",
    "test": "jest"
  },
  "peerDependencies": {
    "@astreus-ai/astreus": "^1.0.0"
  },
  "devDependencies": {
    "@astreus-ai/astreus": "^1.0.0",
    "typescript": "^5.0.0",
    "@types/node": "^20.0.0"
  }
}

2. Export Your Plugin

// src/index.ts
export { createWeatherPlugin } from './weather-plugin';
export type { WeatherPluginConfig } from './weather-plugin';

3. Add Documentation

Create a README.md with usage examples and API documentation.

4. Publish to npm

npm publish

Next Steps

Now that you know how to create custom plugins:

  1. Identify use cases specific to your application
  2. Create reusable plugins for common operations
  3. Test thoroughly with different scenarios
  4. Document your plugins for team members
  5. Consider publishing useful plugins for the community

For more examples, check out the official plugins in the Astreus ecosystem.

How is this guide?