22. Plugin Architecture
Chapter 22 of 24 · 20 min
Plugins let users extend your framework without modifying core code. Built well, they enable customization. Built poorly, they create maintenance nightmares.
Plugin Interface Definition
Define clear contracts:
from abc import ABC, abstractmethod
from typing import Any, Protocol
class AgentPlugin(Protocol):
"""Protocol that all plugins must implement."""
@property
def name(self) -> str:
"""Unique identifier for this plugin."""
...
@property
def version(self) -> str:
"""Semantic version string."""
...
async def initialize(self, config: dict[str, Any]) -> None:
"""Called once when plugin is loaded."""
...
async def process(self, input_data: Any) -> Any:
"""Process input and return output."""
...
Plugin Lifecycle
class PluginManager:
def __init__(self):
self._plugins: dict[str, AgentPlugin] = {}
self._hooks: dict[str, list[callable]] = {
"pre_process": [],
"post_process": [],
}
def register(self, plugin: AgentPlugin):
if plugin.name in self._plugins:
raise ValueError(f"Plugin {plugin.name} already registered")
self._plugins[plugin.name] = plugin
# Run plugin initialization
asyncio.create_task(plugin.initialize({}))
def register_hook(self, hook_name: str, callback: callable):
if hook_name not in self._hooks:
raise ValueError(f"Unknown hook: {hook_name}")
self._hooks[hook_name].append(callback)
async def run_hooks(self, hook_name: str, *args, **kwargs):
for callback in self._hooks.get(hook_name, []):
await callback(*args, **kwargs)
Sandboxing Plugins
Plugins run with the same privileges as your framework unless sandboxed:
import sys
import tracemalloc
class SandboxedPlugin:
"""Wrapper that monitors plugin resource usage."""
def __init__(self, plugin: AgentPlugin, memory_limit_mb: int = 512):
self.plugin = plugin
self.memory_limit = memory_limit_mb * 1024 * 1024
async def process(self, input_data: Any) -> Any:
tracemalloc.start()
try:
result = await self.plugin.process(input_data)
current, peak = tracemalloc.get_traced_memory()
if peak > self.memory_limit:
raise RuntimeError(
f"Plugin {self.plugin.name} exceeded memory limit: "
f"{peak / 1024 / 1024:.1f}MB > {self.memory_limit / 1024 / 1024}MB"
)
return result
finally:
tracemalloc.stop()
Plugin Loading
import importlib
def load_plugin_from_module(module_path: str) -> AgentPlugin:
"""Load a plugin from a dotted module path."""
module = importlib.import_module(module_path)
# Find the plugin class (convention: class name ends with Plugin)
for name in dir(module):
obj = getattr(module, name)
if isinstance(obj, type) and name.endswith('Plugin'):
return obj()
raise ValueError(f"No Plugin class found in {module_path}")
EXERCISE
Implement a plugin that intercepts all agent messages and logs them. Add memory and CPU limits. Verify that a misbehaving plugin can't crash the host process.