Skip to content

Source Code

The runnable TypeScript source for this lesson is in lessons/11-plugin-system/

Lesson 11: Plugin Architecture

Building a modular, extensible agent through plugins

What You'll Learn

  1. Plugin System Design: How to create a plugin architecture that enables extensibility
  2. Sandboxed Contexts: Providing isolated access to agent services
  3. Lifecycle Management: Properly initializing and cleaning up plugins
  4. Resource Tracking: Ensuring clean unloading of plugin resources
  5. Inter-Plugin Communication: How plugins can communicate through events

Why This Matters

A plugin system transforms your agent from a monolithic application into an extensible platform. Benefits include:

  • Modularity: Features can be developed, tested, and deployed independently
  • Third-party Extensions: Others can extend your agent without modifying core code
  • Clean Separation: Core functionality stays focused while plugins add extras
  • Easy Updates: Plugins can be updated or replaced without affecting other parts

Key Concepts

Plugin Interface

Every plugin must implement this interface:

interface Plugin {
  metadata: PluginMetadata;
  initialize(context: PluginContext): Promise<void>;
  cleanup?(): Promise<void>;
}

interface PluginMetadata {
  name: string;      // Unique identifier (lowercase, alphanumeric, hyphens)
  version: string;   // Semver version
  description?: string;
  dependencies?: PluginDependency[];
}

Plugin Context

The context is the plugin's interface to the agent:

interface PluginContext {
  // Hook registration
  registerHook(event, handler, options): void;

  // Tool registration
  registerTool(tool): void;

  // Configuration
  getConfig<T>(key): T | undefined;
  setConfig<T>(key, value): void;

  // Storage
  store(key, value): Promise<void>;
  retrieve<T>(key): Promise<T | undefined>;

  // Logging
  log(level, message, data?): void;

  // Inter-plugin communication
  emit(eventName, data): void;
  subscribe(eventName, handler): () => void;
}

Plugin Lifecycle

+--------------+
|  Registered  |  Plugin is known but not active
+------+-------+
       | enable()
       v
+--------------+
|   Loading    |  Running initialize()
+------+-------+
       |
       +-----------------+
       v                 v
+--------------+  +--------------+
|    Active    |  |    Error     |  Initialization failed
+------+-------+  +--------------+
       | disable()
       v
+--------------+
|  Unloading   |  Running cleanup()
+------+-------+
       |
       v
+--------------+
|   Disabled   |  Ready to re-enable or unregister
+--------------+

Files in This Lesson

File Purpose
types.ts Plugin interfaces and type definitions
plugin-context.ts Sandboxed context implementation
plugin-loader.ts Plugin discovery and loading
plugin-manager.ts Central plugin lifecycle manager
example-plugins/ Example plugin implementations
main.ts Demonstration of all concepts

Running This Lesson

npm run lesson:11

Code Examples

Creating a Simple Plugin

import type { Plugin, PluginContext } from './types.js';

export const myPlugin: Plugin = {
  metadata: {
    name: 'my-plugin',
    version: '1.0.0',
    description: 'A simple example plugin',
  },

  async initialize(context: PluginContext) {
    context.log('info', 'Plugin starting...');

    // Register a hook
    context.registerHook('tool.before', (event) => {
      context.log('debug', `Tool called: ${event.tool}`);
    });

    // Store some data
    await context.store('startedAt', Date.now());

    context.log('info', 'Plugin ready!');
  },

  async cleanup() {
    console.log('Cleaning up...');
  },
};

Registering a Tool

import { z } from 'zod';

async initialize(context: PluginContext) {
  context.registerTool({
    name: 'my_tool',
    description: 'Does something useful',
    parameters: z.object({
      input: z.string().describe('The input to process'),
    }),
    dangerLevel: 'safe',
    execute: async ({ input }) => {
      return {
        success: true,
        output: `Processed: ${input}`,
      };
    },
  });
}

Inter-Plugin Communication

// Plugin A - emitting events
context.emit('data.processed', { count: 42 });

// Plugin B - listening for events
context.subscribe('data.processed', (data) => {
  context.log('info', `Received: ${JSON.stringify(data)}`);
});

Using the Plugin Manager

import { PluginManager } from './plugin-manager.js';
import { myPlugin } from './my-plugin.js';

const manager = new PluginManager();

// Register
await manager.register(myPlugin);

// Enable
await manager.enable('my-plugin');

// Later, disable
await manager.disable('my-plugin');

// Unregister
await manager.unregister('my-plugin');

// Shutdown all
await manager.shutdown();

Example Plugins

Logger Plugin

Logs all agent events for debugging:

context.registerHook('tool.before', (event) => {
  context.log('debug', `Tool: ${event.tool}`);
}, { priority: 0 });  // Run first

Security Plugin

Blocks dangerous operations:

context.registerHook('tool.before', (event) => {
  if (isDangerous(event.args)) {
    event.preventDefault = true;
    context.emit('security.blocked', { reason: 'Dangerous operation' });
  }
}, { priority: 5, canModify: true });  // Run early, can block

Metrics Plugin

Collects performance metrics:

context.registerHook('tool.after', (event) => {
  recordDuration(event.tool, event.durationMs);
}, { priority: 50 });

// Expose metrics via custom tool
context.registerTool({
  name: 'get_metrics',
  // ...
});

Plugin Dependencies

Plugins can declare dependencies on other plugins:

metadata: {
  name: 'advanced-logging',
  version: '1.0.0',
  dependencies: [
    { name: 'logger', version: '^1.0.0' },
    { name: 'metrics', version: '^1.0.0', optional: true },
  ],
},

The plugin manager will: 1. Check dependencies before loading 2. Load plugins in dependency order 3. Report missing dependencies

Design Patterns

Namespace Isolation

Plugin tools are automatically namespaced:

context.registerTool({ name: 'my_tool' });
// Actually registered as: 'my-plugin:my_tool'

Resource Tracking

Everything a plugin registers is tracked:

interface PluginResources {
  hooks: string[];        // Registered hook IDs
  tools: string[];        // Registered tool names
  configKeys: string[];   // Set config keys
  storageKeys: string[];  // Stored data keys
  subscriptions: [];      // Event subscriptions
}

When the plugin is unloaded, all resources are automatically cleaned up.

Error Isolation

Plugin errors don't crash the agent:

try {
  await plugin.initialize(context);
} catch (err) {
  plugin.state = 'error';
  plugin.error = err;
  // Other plugins continue to work
}

Testing Plugins

import { describe, it, expect, beforeEach } from 'vitest';
import { PluginManager } from './plugin-manager.js';
import { myPlugin } from './my-plugin.js';

describe('My Plugin', () => {
  let manager: PluginManager;

  beforeEach(() => {
    manager = new PluginManager({ autoEnable: true });
  });

  it('initializes correctly', async () => {
    await manager.register(myPlugin);
    expect(manager.getState('my-plugin')).toBe('active');
  });

  it('registers expected hooks', async () => {
    await manager.register(myPlugin);
    const plugin = manager.get('my-plugin');
    expect(plugin?.resources.hooks.length).toBeGreaterThan(0);
  });
});

Common Issues

"Plugin already registered"

Each plugin name must be unique. Check if you're accidentally registering twice.

"Missing dependency"

Ensure dependent plugins are registered before the plugin that needs them.

"Initialization timeout"

Plugins have a default 5-second timeout. For slow initialization:

await manager.register(plugin, { initTimeout: 30000 });

"Resources not cleaned up"

Make sure to call manager.shutdown() or manager.unregister() when done.

Advanced: Skills System

The production agent extends plugins with Skills - markdown files that inject specialized prompts and workflows without writing code.

Skills vs Tools

Aspect Tools Skills
Format TypeScript functions Markdown files
Purpose Execute actions Inject context/workflows
Location tools/, plugins .skills/, skills/
Invocation LLM tool calls User command or trigger
Output Action results Prompt injection

Skill File Format

Skills are markdown files with YAML frontmatter:

---
name: code-review
description: Detailed code review workflow with security focus
triggers:
  - "review this code"
  - "check for security issues"
  - "audit this file"
tags: [review, security, quality]
---

# Code Review Skill

When reviewing code, follow this structured approach:

## 1. Security Analysis
- Check for injection vulnerabilities (SQL, XSS, command)
- Verify authentication/authorization
- Look for sensitive data exposure
- Review input validation

## 2. Code Quality
- Check naming conventions
- Review error handling
- Verify proper resource cleanup
- Look for code duplication

## 3. Output Format
Provide findings as:
- **Critical**: Security vulnerabilities
- **Warning**: Potential issues
- **Info**: Suggestions for improvement

Skills Manager

interface SkillDefinition {
  name: string;
  description: string;
  content: string;         // The markdown content
  triggers?: string[];     // Keywords that activate this skill
  tags?: string[];         // For discovery
  source: string;          // File path
  loadedAt: Date;
}

class SkillsManager {
  private skills = new Map<string, SkillDefinition>();
  private skillsDir: string;

  // Load skills from .skills/ directory
  async loadSkills(): Promise<void> {
    const files = await glob('**/*.md', { cwd: this.skillsDir });

    for (const file of files) {
      await this.loadSkillFile(join(this.skillsDir, file));
    }
  }

  // Parse skill file with frontmatter
  private async loadSkillFile(filePath: string): Promise<void> {
    const content = await readFile(filePath, 'utf-8');
    const { data: frontmatter, content: body } = parseFrontmatter(content);

    const skill: SkillDefinition = {
      name: frontmatter.name || basename(filePath, '.md'),
      description: frontmatter.description || '',
      content: body,
      triggers: frontmatter.triggers || [],
      tags: frontmatter.tags || [],
      source: filePath,
      loadedAt: new Date(),
    };

    this.skills.set(skill.name, skill);
  }

  // Find skills matching a query
  findMatchingSkills(query: string): SkillDefinition[] {
    const queryLower = query.toLowerCase();
    const matches: Array<{ skill: SkillDefinition; score: number }> = [];

    for (const skill of this.skills.values()) {
      let score = 0;

      // Check triggers
      for (const trigger of skill.triggers || []) {
        if (queryLower.includes(trigger.toLowerCase())) {
          score += 10;
        }
      }

      // Check tags
      for (const tag of skill.tags || []) {
        if (queryLower.includes(tag.toLowerCase())) {
          score += 5;
        }
      }

      // Check name
      if (queryLower.includes(skill.name.toLowerCase())) {
        score += 8;
      }

      if (score > 0) {
        matches.push({ skill, score });
      }
    }

    return matches
      .sort((a, b) => b.score - a.score)
      .map(m => m.skill);
  }

  // Get skill by name
  getSkill(name: string): SkillDefinition | undefined {
    return this.skills.get(name);
  }

  // List all skills
  listSkills(): SkillDefinition[] {
    return Array.from(this.skills.values());
  }
}

Skill Invocation

// Create skill tool for user invocation
function createSkillTool(skillsManager: SkillsManager) {
  return {
    name: 'invoke_skill',
    description: 'Load a skill to get specialized guidance for a task',
    parameters: {
      type: 'object',
      properties: {
        skill: { type: 'string', description: 'Skill name or search query' },
      },
      required: ['skill'],
    },
    async execute({ skill }) {
      // Try exact match first
      let skillDef = skillsManager.getSkill(skill);

      // Fall back to search
      if (!skillDef) {
        const matches = skillsManager.findMatchingSkills(skill);
        if (matches.length > 0) {
          skillDef = matches[0];
        }
      }

      if (!skillDef) {
        return `No skill found matching "${skill}". Available: ${
          skillsManager.listSkills().map(s => s.name).join(', ')
        }`;
      }

      // Return skill content for context injection
      return `## Skill: ${skillDef.name}\n\n${skillDef.content}`;
    },
  };
}

Auto-Suggestion

// Suggest skills based on user message
async function suggestSkills(
  message: string,
  skillsManager: SkillsManager
): Promise<string | null> {
  const matches = skillsManager.findMatchingSkills(message);

  if (matches.length === 0) return null;

  const suggestions = matches.slice(0, 3).map(s =>
    `- **${s.name}**: ${s.description}`
  ).join('\n');

  return `I found skills that might help:\n${suggestions}\n\nUse \`/skill <name>\` to activate.`;
}

Skill Events

type SkillEvent =
  | { type: 'skill.loaded'; name: string; source: string }
  | { type: 'skill.invoked'; name: string; user: string }
  | { type: 'skill.suggested'; names: string[]; query: string }
  | { type: 'skill.error'; name: string; error: string };

skillsManager.on((event) => {
  if (event.type === 'skill.invoked') {
    console.log(`Skill ${event.name} invoked`);
  }
});

Example Skills Directory

.skills/
├── code-review.md        # Detailed code review workflow
├── debugging.md          # Systematic debugging approach
├── refactoring.md        # Safe refactoring patterns
├── testing.md            # Test writing guidance
├── documentation.md      # Documentation standards
└── security/
    ├── audit.md          # Security audit checklist
    └── owasp.md          # OWASP top 10 checks

Next Steps

In Lesson 12: Rules & Instructions System, we'll build a system for managing dynamic configuration and rules that can modify agent behavior based on context.

The plugin system you learned here can be used to: - Add custom rule sources - Implement rule processors - Create rule-based hooks