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:
- Identify use cases specific to your application
- Create reusable plugins for common operations
- Test thoroughly with different scenarios
- Document your plugins for team members
- Consider publishing useful plugins for the community
For more examples, check out the official plugins in the Astreus ecosystem.
How is this guide?