design-agent
Version:
Universal AI Design Review Agent - CLI tool for scanning code for design drift
284 lines (240 loc) • 7.08 kB
JavaScript
/**
* Error handling utilities for design agent
* Provides comprehensive error handling and recovery
*/
export class DesignAgentError extends Error {
constructor(message, code, context = {}) {
super(message);
this.name = 'DesignAgentError';
this.code = code;
this.context = context;
this.timestamp = new Date().toISOString();
}
}
export class ValidationError extends DesignAgentError {
constructor(message, field, value) {
super(message, 'VALIDATION_ERROR', { field, value });
this.name = 'ValidationError';
}
}
export class AdapterError extends DesignAgentError {
constructor(message, adapter, file) {
super(message, 'ADAPTER_ERROR', { adapter, file });
this.name = 'AdapterError';
}
}
export class ConfigError extends DesignAgentError {
constructor(message, configPath) {
super(message, 'CONFIG_ERROR', { configPath });
this.name = 'ConfigError';
}
}
export function handleError(error, context = {}) {
const errorInfo = {
name: error.name,
message: error.message,
code: error.code || 'UNKNOWN_ERROR',
timestamp: new Date().toISOString(),
context: { ...context, ...error.context },
stack: error.stack
};
// Log error based on severity
if (error instanceof ValidationError) {
console.error('❌ Validation Error:', error.message);
if (error.context.field) {
console.error(` Field: ${error.context.field}`);
}
} else if (error instanceof AdapterError) {
console.error('❌ Adapter Error:', error.message);
if (error.context.adapter) {
console.error(` Adapter: ${error.context.adapter}`);
}
} else if (error instanceof ConfigError) {
console.error('❌ Config Error:', error.message);
if (error.context.configPath) {
console.error(` Config: ${error.context.configPath}`);
}
} else {
console.error('❌ Unexpected Error:', error.message);
}
return errorInfo;
}
export function createErrorHandler(options = {}) {
const {
logErrors = true,
throwOnCritical = true,
maxRetries = 3,
retryDelay = 1000
} = options;
return {
async executeWithRetry(fn, context = {}) {
let lastError;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error;
if (logErrors) {
console.warn(`⚠️ Attempt ${attempt} failed:`, error.message);
}
if (attempt < maxRetries) {
await new Promise(resolve => setTimeout(resolve, retryDelay * attempt));
}
}
}
if (throwOnCritical) {
throw lastError;
}
return null;
},
async executeWithFallback(fn, fallbackFn, context = {}) {
try {
return await fn();
} catch (error) {
if (logErrors) {
console.warn('⚠️ Primary function failed, using fallback:', error.message);
}
try {
return await fallbackFn();
} catch (fallbackError) {
if (logErrors) {
console.error('❌ Fallback function also failed:', fallbackError.message);
}
throw fallbackError;
}
}
}
};
}
export function validateAndThrow(condition, message, code, context) {
if (!condition) {
throw new DesignAgentError(message, code, context);
}
}
export function safeExecute(fn, defaultValue = null) {
try {
return fn();
} catch (error) {
console.warn('⚠️ Safe execution failed:', error.message);
return defaultValue;
}
}
export function createTimeoutPromise(timeoutMs, message = 'Operation timed out') {
return new Promise((_, reject) => {
setTimeout(() => {
reject(new DesignAgentError(message, 'TIMEOUT_ERROR'));
}, timeoutMs);
});
}
export function withTimeout(fn, timeoutMs, message) {
return Promise.race([
fn(),
createTimeoutPromise(timeoutMs, message)
]);
}
export function createCircuitBreaker(options = {}) {
const {
failureThreshold = 5,
resetTimeout = 60000,
monitoringPeriod = 10000
} = options;
let failures = 0;
let lastFailureTime = 0;
let state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
return {
async execute(fn) {
if (state === 'OPEN') {
if (Date.now() - lastFailureTime > resetTimeout) {
state = 'HALF_OPEN';
} else {
throw new DesignAgentError('Circuit breaker is OPEN', 'CIRCUIT_BREAKER_OPEN');
}
}
try {
const result = await fn();
if (state === 'HALF_OPEN') {
state = 'CLOSED';
failures = 0;
}
return result;
} catch (error) {
failures++;
lastFailureTime = Date.now();
if (failures >= failureThreshold) {
state = 'OPEN';
}
throw error;
}
},
getState() {
return {
state,
failures,
lastFailureTime
};
}
};
}
export function createRateLimiter(options = {}) {
const {
maxRequests = 100,
windowMs = 60000,
blockDuration = 300000
} = options;
const requests = [];
let blockedUntil = 0;
return {
async execute(fn) {
const now = Date.now();
if (now < blockedUntil) {
throw new DesignAgentError('Rate limit exceeded', 'RATE_LIMIT_EXCEEDED');
}
// Clean old requests
const cutoff = now - windowMs;
while (requests.length > 0 && requests[0] < cutoff) {
requests.shift();
}
if (requests.length >= maxRequests) {
blockedUntil = now + blockDuration;
throw new DesignAgentError('Rate limit exceeded', 'RATE_LIMIT_EXCEEDED');
}
requests.push(now);
return await fn();
},
getStats() {
const now = Date.now();
const cutoff = now - windowMs;
const recentRequests = requests.filter(time => time > cutoff);
return {
requestsInWindow: recentRequests.length,
maxRequests,
windowMs,
blockedUntil: Math.max(0, blockedUntil - now)
};
}
};
}
export function generateErrorReport(errors) {
if (errors.length === 0) {
return '## Error Report\n\n✅ No errors occurred during execution.\n';
}
let report = '## Error Report\n\n';
report += `**Total Errors:** ${errors.length}\n\n`;
const byType = {};
errors.forEach(error => {
const type = error.name || 'Unknown';
if (!byType[type]) byType[type] = [];
byType[type].push(error);
});
for (const [type, typeErrors] of Object.entries(byType)) {
report += `### ${type} (${typeErrors.length})\n\n`;
typeErrors.forEach(error => {
report += `- **${error.timestamp}** - ${error.message}\n`;
if (error.context && Object.keys(error.context).length > 0) {
report += ` - Context: ${JSON.stringify(error.context)}\n`;
}
});
report += '\n';
}
return report;
}