This document provides architectural observations and recommendations based on the codebase review.
Clean Module Separation
agents/, tools/, config/, hooks/, utils/Strong Type Safety
Excellent Test Coverage
Plugin Architecture
@opencode-ai/pluginConfiguration System
Resource Management
Initialization Performance
Error Handling
Observability
src/agents/src/tools/lsp/client.ts (LSPServerManager)src/background/ (event handlers)src/agents/ (agent variants)src/config/ (config merging)Problem: Resources created but never cleaned up
Solution: Implement a resource manager
// src/utils/resource-manager.ts
export class ResourceManager {
private resources = new Map<string, Disposable>();
register(id: string, resource: Disposable): void {
this.resources.set(id, resource);
}
async dispose(id?: string): Promise<void> {
if (id) {
const resource = this.resources.get(id);
if (resource) {
await resource.dispose();
this.resources.delete(id);
}
} else {
// Dispose all
for (const [id, resource] of this.resources) {
await resource.dispose();
this.resources.delete(id);
}
}
}
}
interface Disposable {
dispose(): Promise<void> | void;
}
Usage:
// In src/index.ts
const resourceManager = new ResourceManager();
resourceManager.register('lsp', lspManager);
resourceManager.register('background', backgroundManager);
// On plugin shutdown
await resourceManager.dispose();
Problem: No protection against cascading failures (LSP, MCP)
Solution: Implement circuit breaker pattern
// src/utils/circuit-breaker.ts
export class CircuitBreaker {
private failures = 0;
private lastFailureTime = 0;
private state: 'closed' | 'open' | 'half-open' = 'closed';
constructor(
private readonly threshold = 5,
private readonly timeout = 60000, // 1 minute
) {}
async execute<T>(fn: () => Promise<T>): Promise<T> {
if (this.state === 'open') {
if (Date.now() - this.lastFailureTime > this.timeout) {
this.state = 'half-open';
} else {
throw new Error('Circuit breaker is open');
}
}
try {
const result = await fn();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
private onSuccess(): void {
this.failures = 0;
this.state = 'closed';
}
private onFailure(): void {
this.failures++;
this.lastFailureTime = Date.now();
if (this.failures >= this.threshold) {
this.state = 'open';
}
}
}
Usage:
// Wrap LSP calls
const lspBreaker = new CircuitBreaker();
async function safeLspCall<T>(fn: () => Promise<T>): Promise<T> {
return lspBreaker.execute(fn);
}
Problem: No retry logic for transient failures
Solution: Exponential backoff utility
// src/utils/retry.ts
export async function retry<T>(
fn: () => Promise<T>,
options: {
maxAttempts?: number;
initialDelay?: number;
maxDelay?: number;
backoffFactor?: number;
} = {},
): Promise<T> {
const {
maxAttempts = 3,
initialDelay = 100,
maxDelay = 5000,
backoffFactor = 2,
} = options;
let lastError: Error | undefined;
let delay = initialDelay;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error instanceof Error ? error : new Error(String(error));
if (attempt < maxAttempts) {
await new Promise(resolve => setTimeout(resolve, delay));
delay = Math.min(delay * backoffFactor, maxDelay);
}
}
}
throw lastError;
}
Problem: Tight coupling through direct imports
Solution: Event bus for decoupled communication
// src/utils/event-bus.ts
type EventHandler<T = unknown> = (data: T) => void | Promise<void>;
export class EventBus {
private handlers = new Map<string, EventHandler[]>();
on<T>(event: string, handler: EventHandler<T>): () => void {
if (!this.handlers.has(event)) {
this.handlers.set(event, []);
}
this.handlers.get(event)!.push(handler as EventHandler);
// Return unsubscribe function
return () => this.off(event, handler);
}
off<T>(event: string, handler: EventHandler<T>): void {
const handlers = this.handlers.get(event);
if (handlers) {
const index = handlers.indexOf(handler as EventHandler);
if (index > -1) {
handlers.splice(index, 1);
}
}
}
async emit<T>(event: string, data: T): Promise<void> {
const handlers = this.handlers.get(event);
if (handlers) {
await Promise.all(handlers.map(h => h(data)));
}
}
}
Usage:
// Global event bus
const eventBus = new EventBus();
// In background manager
eventBus.emit('task:completed', { taskId: task.id, result });
// In hooks
eventBus.on('task:completed', async ({ taskId, result }) => {
// React to task completion
});
Problem: No way to monitor plugin health
Solution: Health check aggregator
// src/utils/health.ts
export interface HealthCheck {
name: string;
check(): Promise<HealthStatus>;
}
export interface HealthStatus {
healthy: boolean;
message?: string;
details?: Record<string, unknown>;
}
export class HealthMonitor {
private checks = new Map<string, HealthCheck>();
register(check: HealthCheck): void {
this.checks.set(check.name, check);
}
async checkAll(): Promise<Record<string, HealthStatus>> {
const results: Record<string, HealthStatus> = {};
for (const [name, check] of this.checks) {
try {
results[name] = await check.check();
} catch (error) {
results[name] = {
healthy: false,
message: error instanceof Error ? error.message : String(error),
};
}
}
return results;
}
async isHealthy(): Promise<boolean> {
const results = await this.checkAll();
return Object.values(results).every(r => r.healthy);
}
}
Usage:
// Register health checks
healthMonitor.register({
name: 'lsp',
async check() {
const clientCount = lspManager.getClientCount();
return {
healthy: clientCount < 10,
details: { clientCount },
};
},
});
healthMonitor.register({
name: 'background-tasks',
async check() {
const activeCount = backgroundManager.getActiveCount();
return {
healthy: activeCount < 50,
details: { activeCount },
};
},
});
Current: Eager initialization of all subsystems Recommended: Lazy initialization for optional features
class LazyService<T> {
private instance?: T;
constructor(private factory: () => T) {}
get(): T {
if (!this.instance) {
this.instance = this.factory();
}
return this.instance;
}
}
// Usage
const lspTools = new LazyService(() => import('./tools/lsp'));
Use Case: Reusable objects (LSP clients, background tasks)
class ObjectPool<T> {
private available: T[] = [];
private inUse = new Set<T>();
constructor(
private factory: () => T,
private maxSize: number,
) {}
acquire(): T {
let obj = this.available.pop();
if (!obj && this.inUse.size < this.maxSize) {
obj = this.factory();
}
if (obj) {
this.inUse.add(obj);
}
return obj!;
}
release(obj: T): void {
this.inUse.delete(obj);
this.available.push(obj);
}
}
Use Case: Config files, agent prompts
class TTLCache<K, V> {
private cache = new Map<K, { value: V; expiry: number }>();
constructor(private ttl: number) {}
set(key: K, value: V): void {
this.cache.set(key, {
value,
expiry: Date.now() + this.ttl,
});
}
get(key: K): V | undefined {
const entry = this.cache.get(key);
if (!entry) return undefined;
if (Date.now() > entry.expiry) {
this.cache.delete(key);
return undefined;
}
return entry.value;
}
clear(): void {
this.cache.clear();
}
}
Plugin Metrics
Agent Metrics
Tool Metrics
Background Task Metrics
LSP Metrics
// src/utils/metrics.ts
export class MetricsCollector {
private counters = new Map<string, number>();
private gauges = new Map<string, number>();
private histograms = new Map<string, number[]>();
increment(name: string, value = 1): void {
this.counters.set(name, (this.counters.get(name) ?? 0) + value);
}
gauge(name: string, value: number): void {
this.gauges.set(name, value);
}
histogram(name: string, value: number): void {
if (!this.histograms.has(name)) {
this.histograms.set(name, []);
}
this.histograms.get(name)!.push(value);
}
getStats(): Record<string, unknown> {
return {
counters: Object.fromEntries(this.counters),
gauges: Object.fromEntries(this.gauges),
histograms: Object.fromEntries(
Array.from(this.histograms.entries()).map(([name, values]) => [
name,
{
count: values.length,
avg: values.reduce((a, b) => a + b, 0) / values.length,
min: Math.min(...values),
max: Math.max(...values),
},
]),
),
};
}
}
// Global metrics instance
export const metrics = new MetricsCollector();
// src/__tests__/performance.test.ts
import { describe, expect, test } from 'bun:test';
describe('Performance', () => {
test('plugin initialization should complete within 100ms', async () => {
const start = performance.now();
await initPlugin();
const duration = performance.now() - start;
expect(duration).toBeLessThan(100);
});
test('background task launch should complete within 50ms', async () => {
const start = performance.now();
backgroundManager.launch({ /* ... */ });
const duration = performance.now() - start;
expect(duration).toBeLessThan(50);
});
});
// src/__tests__/integration.test.ts
test('end-to-end task flow', async () => {
const task = backgroundManager.launch({
agent: 'explorer',
prompt: 'test',
description: 'test task',
parentSessionId: 'test',
});
expect(task.status).toBe('pending');
const completed = await backgroundManager.waitForCompletion(task.id, 30000);
expect(completed?.status).toBeOneOf(['completed', 'failed']);
});
test('handle 100 concurrent background tasks', async () => {
const tasks = Array.from({ length: 100 }, (_, i) =>
backgroundManager.launch({
agent: 'explorer',
prompt: `task ${i}`,
description: `test task ${i}`,
parentSessionId: 'test',
})
);
expect(tasks).toHaveLength(100);
expect(backgroundManager.getActiveCount()).toBeLessThanOrEqual(10);
});
const LIMITS = {
MAX_BACKGROUND_TASKS: 100,
MAX_LSP_CLIENTS: 10,
MAX_TASK_HISTORY: 100,
MAX_SESSION_MEMORY: 1000, // messages
MAX_LOG_SIZE: 10_000_000, // 10MB
};
The codebase has a solid architectural foundation with good separation of concerns and strong type safety. The main areas for improvement are:
Implementing these patterns will make the plugin more robust, performant, and production-ready.