Skip to main content

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
})
}
]
}
};

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:

OptionTypeDefaultDescription
toolsstring[] | 'all' | 'none''all'Tool extensions to enable
channelsChannelPlatform[] | 'all' | 'none''all'Messaging channels to enable
secretsRecord<string, string>{}API keys (falls back to env vars)
loggerRegistryLoggerconsoleCustom logger
basePrioritynumber0Base priority offset
overridesRecord<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: true to 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

  1. Extensions load based on manifest order
  2. Within each pack, descriptors apply by priority
  3. Lifecycle hooks (onActivate) run after registration
  4. 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

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