Source Code
The runnable TypeScript source for this lesson is in
lessons/10-hook-system/
Lesson 10: Hook & Event System¶
Building extensible architecture through events and hooks
What You'll Learn¶
- Event-Driven Architecture: How components communicate through events instead of direct calls
- Hook System: Intercepting and modifying behavior without changing core code
- Priority Ordering: Ensuring predictable hook execution order
- Error Isolation: Preventing one hook from breaking others
- Performance Tracking: Monitoring hook execution times
Why This Matters¶
As your agent grows, you'll need to add features like: - Logging all tool calls - Blocking dangerous operations - Collecting metrics - Custom validation
Without hooks, you'd have to modify the core agent code for each feature. Hooks let you add these capabilities externally, keeping the core clean and maintainable.
Key Concepts¶
Events vs Hooks¶
| Aspect | Events | Hooks |
|---|---|---|
| Purpose | Observation | Interception |
| Can modify? | No | Yes (with canModify: true) |
| Timing | After the fact | Before/after |
| Use case | Logging, metrics | Security, validation |
Event Types¶
type AgentEvent =
| { type: 'tool.before'; tool: string; args: unknown }
| { type: 'tool.after'; tool: string; result: unknown; durationMs: number }
| { type: 'tool.error'; tool: string; error: Error }
| { type: 'session.start'; sessionId: string }
| { type: 'session.end'; sessionId: string; reason: string }
| { type: 'message.created'; role: string; content: string }
| { type: 'file.edited'; path: string; operation: string }
| { type: 'error'; error: Error; recoverable: boolean };
Hook Definition¶
interface Hook<T extends AgentEventType> {
id: string; // Unique identifier
event: T; // Event type to listen for
handler: Function; // Callback function
priority?: number; // Execution order (lower = earlier)
canModify?: boolean; // Can this hook modify the event?
}
Priority Ranges¶
| Range | Who Uses It | Example |
|---|---|---|
| 0-50 | System hooks | Logging (always runs first) |
| 50-150 | Plugin hooks | Metrics, validation |
| 150+ | User hooks | Custom business logic |
Files in This Lesson¶
| File | Purpose |
|---|---|
types.ts |
Event and hook type definitions |
event-bus.ts |
Typed EventEmitter for observation |
hook-registry.ts |
Hook registration and execution |
built-in-hooks.ts |
Logging, metrics, security hooks |
main.ts |
Demonstration of all concepts |
Running This Lesson¶
Code Examples¶
Basic Event Subscription¶
import { EventBus, createEvent } from './event-bus.js';
const bus = new EventBus();
// Subscribe to tool events
const subscription = bus.on('tool.before', (event) => {
console.log(`Tool ${event.tool} called with:`, event.args);
});
// Emit an event
await bus.emit(createEvent('tool.before', {
tool: 'bash',
args: { command: 'ls -la' }
}));
// Cleanup
subscription.unsubscribe();
Intercepting Hook¶
import { HookRegistry } from './hook-registry.js';
const registry = new HookRegistry();
// Block dangerous operations
registry.register({
id: 'security-block',
event: 'tool.before',
priority: 10,
canModify: true,
handler: (event) => {
if (event.tool === 'bash' && event.args.command.includes('rm -rf')) {
console.warn('Blocked dangerous command!');
event.preventDefault = true; // Stop execution
}
}
});
Collecting Metrics¶
import { registerMetricsHooks } from './built-in-hooks.js';
const metrics: Metric[] = [];
registerMetricsHooks(registry, {
prefix: 'myagent',
onMetric: (metric) => {
metrics.push(metric);
// Send to monitoring system
}
});
Complete Tool Wrapper¶
async function executeWithHooks(tool: string, args: unknown) {
// Before hook
const beforeEvent = { type: 'tool.before', tool, args };
await registry.execute(beforeEvent);
if (beforeEvent.preventDefault) {
return { blocked: true };
}
// Execute
const start = performance.now();
const result = await actualExecute(tool, args);
const durationMs = performance.now() - start;
// After hook
await registry.execute({
type: 'tool.after',
tool,
result,
durationMs
});
return result;
}
Architecture Diagram¶
+-------------------------------------------------------------+
| Agent Core |
| +----------+ +----------+ +----------+ |
| | Tools |---->| Events |---->| Hooks | |
| +----------+ +----------+ +----------+ |
| | | | |
+-------+----------------+----------------+----------------------+
| | |
| v v
| +----------+ +--------------+
| | EventBus | | HookRegistry |
| +----------+ +--------------+
| | |
v v v
+-------------------------------------------------------------+
| Plugin Layer |
| +----------+ +----------+ +----------+ +----------+ |
| | Logging | | Metrics | | Security | | Custom | |
| +----------+ +----------+ +----------+ +----------+ |
+-------------------------------------------------------------+
Error Handling Strategies¶
The hook registry supports three error strategies:
| Strategy | Behavior |
|---|---|
continue |
Log error, continue to next hook |
stop |
Stop execution on first error |
collect |
Collect all errors, return in result |
Design Decisions¶
Why Typed Events?¶
TypeScript's discriminated unions ensure you can only emit valid events and get proper type inference in handlers.
Why Priority Numbers?¶
Explicit priorities (vs. registration order) make behavior predictable and allow plugins to position themselves appropriately.
Why Error Isolation?¶
One buggy hook shouldn't crash the entire agent. Errors are caught and logged, but execution continues.
Common Patterns¶
Async Hooks¶
registry.register({
id: 'async-hook',
event: 'tool.after',
handler: async (event) => {
await sendToAnalytics(event); // Async is supported
}
});
Conditional Hooks¶
registry.register({
id: 'conditional',
event: 'tool.before',
handler: (event) => {
if (shouldSkip(event)) return; // Early return
// ... processing
}
});
Transforming Args¶
registry.register({
id: 'transform-args',
event: 'tool.before',
canModify: true,
handler: (event) => {
// Sanitize command before execution
event.modifiedArgs = {
...event.args,
command: sanitize(event.args.command)
};
}
});
Testing Hooks¶
import { describe, it, expect } from 'vitest';
import { HookRegistry } from './hook-registry.js';
describe('HookRegistry', () => {
it('executes hooks in priority order', async () => {
const registry = new HookRegistry();
const order: string[] = [];
registry.register({
id: 'second',
event: 'tool.before',
priority: 100,
handler: () => order.push('second')
});
registry.register({
id: 'first',
event: 'tool.before',
priority: 50,
handler: () => order.push('first')
});
await registry.execute({ type: 'tool.before', tool: 'test', args: {} });
expect(order).toEqual(['first', 'second']);
});
});
Next Steps¶
In Lesson 11: Plugin Architecture, we'll build on the hook system to create a full plugin system that can: - Discover and load plugins dynamically - Provide isolated contexts for plugins - Manage plugin lifecycle (init, cleanup) - Handle plugin dependencies
The hook system you learned here becomes the foundation for plugin communication!