Skip to main content

Plugin SDK

A plugin bundles tools, hooks, and personality contributions into a single npm package that users can install without editing the core. The plugin SDK lets you publish reusable Ethos extensions.

The Plugin interface

interface Plugin {
name: string;
version: string;
description?: string;
tools?: Tool[];
hooks?: HookRegistration[];
personalityContributions?: PersonalityContribution[];
onLoad?(registry: PluginRegistry): Promise<void>;
onUnload?(): Promise<void>;
}

A minimal plugin

import type { Plugin } from '@ethosagent/types';
import { weatherTool } from './tools/weather';
import { locationHook } from './hooks/location';

export const weatherPlugin: Plugin = {
name: '@myorg/ethos-weather',
version: '1.0.0',
description: 'Adds weather lookup tools and location context injection',
tools: [weatherTool],
hooks: [locationHook],
};

Tools

Tools registered via plugin follow the same Tool<TArgs> interface as standalone tools:

import type { Tool, ToolResult } from '@ethosagent/types';

interface WeatherArgs {
city: string;
units?: 'celsius' | 'fahrenheit';
}

export const weatherTool: Tool<WeatherArgs> = {
name: 'get_weather',
description: 'Returns current weather for a city.',
toolset: 'web',
maxResultChars: 500,

inputSchema: {
type: 'object',
required: ['city'],
properties: {
city: { type: 'string' },
units: { type: 'string', enum: ['celsius', 'fahrenheit'] },
},
},

isAvailable() {
return Boolean(process.env.OPENWEATHER_API_KEY);
},

async execute(args: WeatherArgs): Promise<ToolResult> {
const key = process.env.OPENWEATHER_API_KEY;
const unit = args.units === 'fahrenheit' ? 'imperial' : 'metric';
const url = `https://api.openweathermap.org/data/2.5/weather?q=${encodeURIComponent(args.city)}&units=${unit}&appid=${key}`;
const res = await fetch(url);
if (!res.ok) return { ok: false, error: `Weather API error: ${res.status}`, code: 'API_ERROR' };
const data = await res.json();
const symbol = unit === 'imperial' ? '°F' : '°C';
return {
ok: true,
value: `${data.name}: ${data.main.temp}${symbol}, ${data.weather[0].description}`,
};
},
};

Hooks

Hooks registered via plugin use the same three execution models (Void, Modifying, Claiming) as core hooks. See Hook Registry for details.

import type { HookRegistration } from '@ethosagent/types';

export const locationHook: HookRegistration = {
point: 'before_llm_call',
model: 'modifying',
handler: async (ctx) => {
const location = await detectLocation();
if (!location) return null;
return {
systemPromptAddition: `User's current location: ${location.city}, ${location.country}.`,
};
},
};

onLoad and onUnload

Use onLoad for initialization — connecting to external services, validating API keys, registering event listeners:

export const analyticsPlugin: Plugin = {
name: '@myorg/ethos-analytics',
version: '1.0.0',

async onLoad(registry) {
await analyticsClient.connect();
registry.on('tool_end', (event) => {
analyticsClient.track('tool_used', { tool: event.toolName, ok: event.ok });
});
},

async onUnload() {
await analyticsClient.disconnect();
},
};

Packaging

package.json for a plugin:

{
"name": "@myorg/ethos-weather",
"version": "1.0.0",
"type": "module",
"main": "./dist/index.js",
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
}
},
"peerDependencies": {
"@ethosagent/types": ">=0.1.0"
},
"devDependencies": {
"@ethosagent/types": "workspace:*",
"typescript": "^5"
}
}

Note: @ethosagent/types is a peer dependency, not a regular dependency. This ensures plugins use the same types instance as the host app.

Loading plugins

In ~/.ethos/config.yaml:

plugins:
- "@myorg/ethos-weather"
- "/path/to/local/plugin"

PluginRegistry.load() in apps/ethos/src/wiring.ts reads this list and calls plugin.onLoad() for each entry.

Publishing

pnpm build
npm publish --access public

Users install with:

npm install @myorg/ethos-weather

Then add to config.yaml and restart.