How Extensions Work
Overview
AgentOS uses a sophisticated extension system that allows developers to add new capabilities without modifying core code. Extensions can provide tools, guardrails, workflows, and more.
Extension Architecture
Core Components
1. Extension Pack
A collection of related extensions distributed together:
export interface ExtensionPack {
name: string;
version?: string;
descriptors: ExtensionDescriptor[];
}
2. Extension Descriptor
Metadata and payload for a single extension:
export interface ExtensionDescriptor<TPayload = unknown> {
id: string; // Unique identifier
kind: ExtensionKind; // 'tool', 'guardrail', etc.
payload: TPayload; // The actual implementation
priority?: number; // Loading priority (higher = later)
enableByDefault?: boolean; // Auto-enable when loaded
metadata?: Record<string, unknown>;
onActivate?: (context) => void; // Lifecycle hook
onDeactivate?: (context) => void; // Lifecycle hook
}
3. Extension Manager
Coordinates loading and lifecycle:
const extensionManager = new ExtensionManager();
await extensionManager.loadManifest(context);
Loading Extensions
Method 1: Factory Function (In-Process)
const config: AgentOSConfig = {
extensionManifest: {
packs: [
{
factory: () => createSearchExtension({
// ... options
})
}
]
}
};
Method 2: Curated Registry (Recommended)
Load all official extensions at once via createCuratedManifest():
import { createCuratedManifest } from '@framers/agentos-extensions-registry';
const manifest = await createCuratedManifest({
tools: 'all', // or ['web-search', 'giphy'] for selective
channels: 'none', // or ['telegram', 'discord'] or 'all'
secrets: {
'serper.apiKey': process.env.SERPER_API_KEY!,
'giphy.apiKey': process.env.GIPHY_API_KEY!,
},
});
const config: AgentOSConfig = {
extensionManifest: manifest,
};
Only extensions whose npm packages are installed will load — missing packages are skipped silently via tryImport().
Options:
| Option | Type | Default | Description |
|---|---|---|---|
tools | string[] | 'all' | 'none' | 'all' | Tool extensions to enable |
channels | ChannelPlatform[] | 'all' | 'none' | 'all' | Messaging channels to enable |
secrets | Record<string, string> | {} | API keys (falls back to env vars) |
logger | RegistryLogger | console | Custom logger |
basePriority | number | 0 | Base priority offset |
overrides | Record<string, ExtensionOverrideConfig> | — | Per-extension overrides |
Method 3: NPM Package
const config: AgentOSConfig = {
extensionManifest: {
packs: [
{
package: '@framers/agentos-ext-search',
version: '^1.0.0',
options: { /* config */ }
}
]
}
};
Method 4: Local Module
const config: AgentOSConfig = {
extensionManifest: {
packs: [
{
module: './extensions/my-extension',
options: { /* config */ }
}
]
}
};
Secrets & API Keys
When a descriptor needs a credential, declare it and read the value from the lifecycle context:
const descriptor = {
id: 'example.search',
kind: EXTENSION_KIND_TOOL,
payload: searchTool,
requiredSecrets: [{ id: 'openai.apiKey' }],
onActivate: (ctx) => {
searchTool.setApiKey(ctx.getSecret?.('openai.apiKey'));
},
};
- Secret IDs correspond to entries in
packages/agentos/src/config/extension-secrets.json. - Hosts can supply the values via environment variables or the AgentOS client UI.
- Optional secrets can be marked with
optional: trueto allow degraded operation.
Priority & Overriding
Extension Priority
Extensions with higher priority load later and can override earlier ones:
{
id: 'webSearch',
kind: 'tool',
priority: 10, // Default priority
payload: new WebSearchTool()
}
// Later extension can override:
{
id: 'webSearch', // Same ID
kind: 'tool',
priority: 20, // Higher priority wins
payload: new EnhancedWebSearchTool()
}
Stacking
Multiple descriptors with the same ID form a stack. The highest priority becomes active:
// Registry maintains stack:
webSearch: [
{ priority: 10, payload: BasicSearch }, // Inactive
{ priority: 20, payload: EnhancedSearch } // Active
]
Manual Overrides
Configure overrides in the manifest:
{
extensionManifest: {
packs: [/* ... */],
overrides: {
tools: {
'webSearch': {
enabled: false, // Disable specific tool
priority: 100, // Override priority
options: { /* */ } // Custom options
}
}
}
}
}
Loading Multiple Extensions
Parallel Loading
Multiple extension packs load in parallel:
{
extensionManifest: {
packs: [
{ factory: () => createSearchExtension(opts1) },
{ factory: () => createDatabaseExtension(opts2) },
{ factory: () => createWeatherExtension(opts3) }
]
}
}
Load Order
- Extensions load based on manifest order
- Within each pack, descriptors apply by priority
- Lifecycle hooks (
onActivate) run after registration - Higher priority extensions can override lower ones
Extension Lifecycle
1. Discovery
ExtensionManager reads manifest and identifies packs to load.
2. Loading
for (const entry of manifest.packs) {
const pack = await resolvePack(entry);
await registerPack(pack, entry, context);
}
3. Registration
Each descriptor registers with its kind-specific registry:
const toolRegistry = extensionManager.getRegistry('tool');
await toolRegistry.register(descriptor, context);
4. Activation
onActivate hook runs when descriptor becomes active:
{
onActivate: async (ctx) => {
ctx.logger?.info('Tool activated');
await initializeResources();
}
}
5. Runtime
Tools available to agents via ToolExecutor:
const tool = toolExecutor.getTool('webSearch');
const result = await tool.execute(input, context);
6. Deactivation
onDeactivate runs when overridden or removed:
{
onDeactivate: async (ctx) => {
await cleanupResources();
}
}
Example: Complete Setup
Using the Registry (Recommended)
import { AgentOS } from '@framers/agentos';
import { createCuratedManifest } from '@framers/agentos-extensions-registry';
const agentos = new AgentOS();
// Load all available extensions in one call
const manifest = await createCuratedManifest({
tools: 'all',
channels: ['telegram'],
secrets: {
'serper.apiKey': process.env.SERPER_API_KEY!,
'telegram.botToken': process.env.TELEGRAM_BOT_TOKEN!,
},
overrides: {
'cli-executor': { enabled: false }, // Disable specific extensions
},
});
await agentos.initialize({
defaultPersonaId: 'v_researcher',
extensionManifest: manifest,
modelProviderManagerConfig: {
providers: [/* ... */]
}
});
const gmi = await agentos.createGMI('v_researcher');
// Agent can now use webSearch, researchAggregator, giphy, news, etc.
Using Individual Extensions
import { AgentOS } from '@framers/agentos';
import searchExtension from '@framers/agentos-ext-web-search';
const agentos = new AgentOS();
await agentos.initialize({
defaultPersonaId: 'v_researcher',
extensionManifest: {
packs: [
{
factory: () => searchExtension({
manifestEntry: {} as any,
source: { sourceName: '@framers/agentos-ext-web-search' },
options: {
search: {
provider: 'serper',
apiKey: process.env.SERPER_API_KEY,
}
}
}),
priority: 10,
enabled: true
}
],
overrides: {
tools: {
'researchAggregator': { enabled: true, priority: 15 },
'factCheck': { enabled: false }
}
}
},
modelProviderManagerConfig: {
providers: [/* ... */]
}
});
const gmi = await agentos.createGMI('v_researcher');
Extension Discovery
Runtime Discovery
// Get all registered tools
const toolRegistry = extensionManager.getRegistry('tool');
const activeToos = toolRegistry.listActive();
// Check if specific tool exists
const hasSearch = toolRegistry.has('webSearch');
// Get tool metadata
const descriptor = toolRegistry.get('webSearch');
Agent Discovery
Agents discover available tools through GMI configuration:
const gmi = await gmiManager.createGMI({
personaId: 'researcher',
toolIds: ['webSearch', 'researchAggregator'] // Explicit tools
});
Best Practices
1. Namespace Your IDs
Use reverse domain notation:
id: 'com.yourcompany.ext.toolname'
2. Version Compatibility
Specify AgentOS version requirements:
{
"agentosVersion": "^2.0.0"
}
3. Configuration Validation
Validate options in factory:
export function createExtensionPack(context: ExtensionPackContext) {
const { options = {} } = context;
if (!options.apiKey && !process.env.API_KEY) {
throw new Error('API key required');
}
// ... create pack
}
4. Graceful Degradation
Handle missing dependencies:
async execute(input, context) {
if (!this.service.isConfigured()) {
return {
success: false,
error: 'Service not configured',
details: { helpUrl: 'https://...' }
};
}
// ... normal execution
}
5. Resource Cleanup
Always cleanup in deactivate:
onDeactivate: async (ctx) => {
await this.closeConnections();
await this.saveState();
this.clearCache();
}
Debugging Extensions
Enable Logging
{
extensionManifest: {
packs: [/* ... */]
},
debugging: {
logExtensionEvents: true,
logToolCalls: true
}
}
Extension Events
Listen to extension events:
extensionManager.on((event) => {
console.log('Extension event:', event.type, event.source);
});
Tool Execution Tracking
const toolOrchestrator = new ToolOrchestrator({
logToolCalls: true,
onToolCall: (tool, input, result) => {
console.log(`Tool ${tool.name}:`, { input, result });
}
});
Security Considerations
1. Permission Scoping
Tools declare required permissions:
readonly permissions = {
requiredScopes: ['internet.access', 'file.read'],
requiredCapabilities: ['web-search']
};
2. API Key Security
Never hardcode keys:
// ❌ Bad
apiKey: 'sk-1234567890'
// ✅ Good
apiKey: process.env.API_KEY
3. Input Validation
Always validate tool inputs:
async execute(input: any, context: ToolExecutionContext) {
// Validate against schema
const valid = ajv.validate(this.inputSchema, input);
if (!valid) {
return { success: false, error: 'Invalid input' };
}
// ... proceed
}
Future Roadmap
- Extension Marketplace: Browse and install from UI
- Hot Reloading: Update extensions without restart
- Sandboxing: Isolate extension execution
- Dependency Resolution: Automatic dependency management
- Visual Editor: Create extensions without code