@aleph-ai/tinyaleph 1.3.0 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/core/errors.js ADDED
@@ -0,0 +1,587 @@
1
+ /**
2
+ * Centralized Error Handling for TinyAleph
3
+ *
4
+ * Provides a unified error handling system with:
5
+ * - Error categorization and classification
6
+ * - Automatic retry logic for transient errors
7
+ * - Error aggregation and reporting
8
+ * - User-friendly error messages
9
+ * - Async error boundary wrappers
10
+ *
11
+ * Browser-compatible: No Node.js-specific dependencies.
12
+ * Extracted from apps/sentient/lib/error-handler.js for library reuse.
13
+ */
14
+
15
+ // Simple EventEmitter that works in both browser and Node.js
16
+ class SimpleEventEmitter {
17
+ constructor() {
18
+ this._events = new Map();
19
+ }
20
+
21
+ on(event, listener) {
22
+ if (!this._events.has(event)) {
23
+ this._events.set(event, []);
24
+ }
25
+ this._events.get(event).push(listener);
26
+ return this;
27
+ }
28
+
29
+ off(event, listener) {
30
+ if (!this._events.has(event)) return this;
31
+ const listeners = this._events.get(event);
32
+ const idx = listeners.indexOf(listener);
33
+ if (idx !== -1) listeners.splice(idx, 1);
34
+ return this;
35
+ }
36
+
37
+ emit(event, ...args) {
38
+ if (!this._events.has(event)) return false;
39
+ for (const listener of this._events.get(event)) {
40
+ listener(...args);
41
+ }
42
+ return true;
43
+ }
44
+
45
+ removeAllListeners(event) {
46
+ if (event) {
47
+ this._events.delete(event);
48
+ } else {
49
+ this._events.clear();
50
+ }
51
+ return this;
52
+ }
53
+ }
54
+
55
+ // ============================================================================
56
+ // LOG LEVELS
57
+ // ============================================================================
58
+
59
+ const LogLevel = {
60
+ TRACE: 0,
61
+ DEBUG: 1,
62
+ INFO: 2,
63
+ WARN: 3,
64
+ ERROR: 4,
65
+ FATAL: 5,
66
+ SILENT: 6
67
+ };
68
+
69
+ const LogLevelNames = Object.fromEntries(
70
+ Object.entries(LogLevel).map(([k, v]) => [v, k])
71
+ );
72
+
73
+ // ============================================================================
74
+ // ERROR CATEGORIES
75
+ // ============================================================================
76
+
77
+ const ErrorCategory = {
78
+ NETWORK: 'network', // Network/transport errors
79
+ AUTHENTICATION: 'auth', // Auth/credential errors
80
+ VALIDATION: 'validation', // Input validation errors
81
+ RESOURCE: 'resource', // Resource not found/unavailable
82
+ PERMISSION: 'permission', // Permission denied
83
+ TIMEOUT: 'timeout', // Operation timeouts
84
+ RATE_LIMIT: 'rate_limit', // Rate limiting
85
+ INTERNAL: 'internal', // Internal/unexpected errors
86
+ EXTERNAL: 'external', // External service errors
87
+ USER: 'user', // User-caused errors
88
+ CONFIGURATION: 'config', // Configuration errors
89
+ LLM: 'llm', // LLM-specific errors
90
+ MEMORY: 'memory', // Memory/state errors
91
+ OSCILLATOR: 'oscillator' // Oscillator/dynamics errors
92
+ };
93
+
94
+ // ============================================================================
95
+ // CUSTOM ERROR CLASSES
96
+ // ============================================================================
97
+
98
+ /**
99
+ * Base error class with category and metadata
100
+ */
101
+ class AlephError extends Error {
102
+ constructor(message, options = {}) {
103
+ super(message);
104
+ this.name = 'AlephError';
105
+ this.category = options.category || ErrorCategory.INTERNAL;
106
+ this.code = options.code || 'UNKNOWN_ERROR';
107
+ this.retryable = options.retryable ?? false;
108
+ this.metadata = options.metadata || {};
109
+ this.originalError = options.cause || null;
110
+ this.timestamp = Date.now();
111
+ this.userMessage = options.userMessage || this.getDefaultUserMessage();
112
+
113
+ // Capture stack trace
114
+ if (Error.captureStackTrace) {
115
+ Error.captureStackTrace(this, AlephError);
116
+ }
117
+ }
118
+
119
+ getDefaultUserMessage() {
120
+ switch (this.category) {
121
+ case ErrorCategory.NETWORK:
122
+ return 'Network connection error. Please check your connection.';
123
+ case ErrorCategory.AUTHENTICATION:
124
+ return 'Authentication failed. Please check your credentials.';
125
+ case ErrorCategory.RATE_LIMIT:
126
+ return 'Too many requests. Please wait a moment.';
127
+ case ErrorCategory.TIMEOUT:
128
+ return 'Operation timed out. Please try again.';
129
+ case ErrorCategory.LLM:
130
+ return 'AI service error. Please try again later.';
131
+ default:
132
+ return 'An unexpected error occurred.';
133
+ }
134
+ }
135
+
136
+ toJSON() {
137
+ return {
138
+ name: this.name,
139
+ message: this.message,
140
+ category: this.category,
141
+ code: this.code,
142
+ retryable: this.retryable,
143
+ metadata: this.metadata,
144
+ userMessage: this.userMessage,
145
+ timestamp: this.timestamp,
146
+ stack: this.stack
147
+ };
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Network-related errors
153
+ */
154
+ class NetworkError extends AlephError {
155
+ constructor(message, options = {}) {
156
+ super(message, {
157
+ ...options,
158
+ category: ErrorCategory.NETWORK,
159
+ retryable: options.retryable ?? true
160
+ });
161
+ this.name = 'NetworkError';
162
+ this.statusCode = options.statusCode;
163
+ }
164
+ }
165
+
166
+ /**
167
+ * LLM-related errors
168
+ */
169
+ class LLMError extends AlephError {
170
+ constructor(message, options = {}) {
171
+ super(message, {
172
+ ...options,
173
+ category: ErrorCategory.LLM
174
+ });
175
+ this.name = 'LLMError';
176
+ this.provider = options.provider;
177
+ this.model = options.model;
178
+ }
179
+ }
180
+
181
+ /**
182
+ * Validation errors
183
+ */
184
+ class ValidationError extends AlephError {
185
+ constructor(message, options = {}) {
186
+ super(message, {
187
+ ...options,
188
+ category: ErrorCategory.VALIDATION,
189
+ retryable: false
190
+ });
191
+ this.name = 'ValidationError';
192
+ this.fields = options.fields || [];
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Timeout errors
198
+ */
199
+ class TimeoutError extends AlephError {
200
+ constructor(message, options = {}) {
201
+ super(message, {
202
+ ...options,
203
+ category: ErrorCategory.TIMEOUT,
204
+ retryable: true
205
+ });
206
+ this.name = 'TimeoutError';
207
+ this.timeout = options.timeout;
208
+ this.operation = options.operation;
209
+ }
210
+ }
211
+
212
+ // ============================================================================
213
+ // ERROR HANDLER
214
+ // ============================================================================
215
+
216
+ /**
217
+ * Centralized Error Handler
218
+ */
219
+ class ErrorHandler extends SimpleEventEmitter {
220
+ constructor(options = {}) {
221
+ super();
222
+
223
+ this.logger = options.logger || null;
224
+
225
+ // Error aggregation
226
+ this.errors = [];
227
+ this.maxErrors = options.maxErrors || 1000;
228
+
229
+ // Error rate tracking
230
+ this.errorRates = new Map(); // category -> count in window
231
+ this.rateWindow = options.rateWindow || 60000; // 1 minute
232
+
233
+ // User message handlers
234
+ this.userMessageHandlers = new Map();
235
+
236
+ // Recovery handlers
237
+ this.recoveryHandlers = new Map();
238
+
239
+ // Setup default handlers
240
+ this.setupDefaultHandlers();
241
+ }
242
+
243
+ /**
244
+ * Set the logger instance
245
+ * @param {Object} logger - Logger instance with error(), warn(), info() methods
246
+ */
247
+ setLogger(logger) {
248
+ this.logger = logger;
249
+ }
250
+
251
+ setupDefaultHandlers() {
252
+ // Register recovery handlers for retryable errors
253
+ this.registerRecoveryHandler(ErrorCategory.NETWORK, async (error, context) => {
254
+ // Default: wait and retry
255
+ await new Promise(r => setTimeout(r, 1000));
256
+ return { retry: true };
257
+ });
258
+
259
+ this.registerRecoveryHandler(ErrorCategory.RATE_LIMIT, async (error, context) => {
260
+ // Wait for rate limit reset
261
+ const waitTime = error.metadata?.retryAfter || 5000;
262
+ await new Promise(r => setTimeout(r, waitTime));
263
+ return { retry: true };
264
+ });
265
+ }
266
+
267
+ /**
268
+ * Handle an error
269
+ * @param {Error} error - Error to handle
270
+ * @param {Object} context - Error context
271
+ * @returns {Object} Handler result
272
+ */
273
+ async handle(error, context = {}) {
274
+ // Normalize to AlephError
275
+ const alephError = this.normalize(error);
276
+
277
+ // Log the error
278
+ if (this.logger) {
279
+ this.logger.error(alephError.message, {
280
+ category: alephError.category,
281
+ code: alephError.code,
282
+ retryable: alephError.retryable,
283
+ metadata: alephError.metadata,
284
+ stack: alephError.stack
285
+ });
286
+ }
287
+
288
+ // Record error
289
+ this.recordError(alephError);
290
+
291
+ // Update error rate
292
+ this.updateErrorRate(alephError.category);
293
+
294
+ // Emit event
295
+ this.emit('error', alephError, context);
296
+
297
+ // Try recovery if retryable
298
+ if (alephError.retryable && context.canRetry !== false) {
299
+ const recovery = await this.attemptRecovery(alephError, context);
300
+ if (recovery.retry) {
301
+ return { handled: true, retry: true, error: alephError };
302
+ }
303
+ }
304
+
305
+ return {
306
+ handled: true,
307
+ retry: false,
308
+ error: alephError,
309
+ userMessage: alephError.userMessage
310
+ };
311
+ }
312
+
313
+ /**
314
+ * Normalize any error to AlephError
315
+ * @param {Error} error - Error to normalize
316
+ * @returns {AlephError}
317
+ */
318
+ normalize(error) {
319
+ if (error instanceof AlephError) {
320
+ return error;
321
+ }
322
+
323
+ // Classify based on error properties
324
+ let category = ErrorCategory.INTERNAL;
325
+ let retryable = false;
326
+ let code = 'UNKNOWN_ERROR';
327
+
328
+ // Check for network errors
329
+ if (error.code === 'ENOTFOUND' || error.code === 'ECONNREFUSED' ||
330
+ error.code === 'ECONNRESET' || error.message.includes('network')) {
331
+ category = ErrorCategory.NETWORK;
332
+ retryable = true;
333
+ code = error.code || 'NETWORK_ERROR';
334
+ }
335
+
336
+ // Check for timeout errors
337
+ if (error.name === 'TimeoutError' || error.code === 'ETIMEDOUT' ||
338
+ error.message.includes('timeout')) {
339
+ category = ErrorCategory.TIMEOUT;
340
+ retryable = true;
341
+ code = 'TIMEOUT';
342
+ }
343
+
344
+ // Check for rate limit errors
345
+ if (error.status === 429 || error.message.includes('rate limit')) {
346
+ category = ErrorCategory.RATE_LIMIT;
347
+ retryable = true;
348
+ code = 'RATE_LIMITED';
349
+ }
350
+
351
+ // Check for auth errors
352
+ if (error.status === 401 || error.status === 403 ||
353
+ error.message.includes('unauthorized') || error.message.includes('forbidden')) {
354
+ category = ErrorCategory.AUTHENTICATION;
355
+ code = 'AUTH_FAILED';
356
+ }
357
+
358
+ return new AlephError(error.message, {
359
+ category,
360
+ code,
361
+ retryable,
362
+ cause: error,
363
+ metadata: { originalName: error.name, originalCode: error.code }
364
+ });
365
+ }
366
+
367
+ /**
368
+ * Record error for aggregation
369
+ * @param {AlephError} error - Error to record
370
+ */
371
+ recordError(error) {
372
+ this.errors.push({
373
+ ...error.toJSON(),
374
+ handledAt: Date.now()
375
+ });
376
+
377
+ if (this.errors.length > this.maxErrors) {
378
+ this.errors.shift();
379
+ }
380
+ }
381
+
382
+ /**
383
+ * Update error rate tracking
384
+ * @param {string} category - Error category
385
+ */
386
+ updateErrorRate(category) {
387
+ const now = Date.now();
388
+ const key = `${category}:${Math.floor(now / this.rateWindow)}`;
389
+
390
+ this.errorRates.set(key, (this.errorRates.get(key) || 0) + 1);
391
+
392
+ // Clean old windows
393
+ for (const [k] of this.errorRates) {
394
+ const windowTime = parseInt(k.split(':')[1]) * this.rateWindow;
395
+ if (now - windowTime > this.rateWindow * 2) {
396
+ this.errorRates.delete(k);
397
+ }
398
+ }
399
+ }
400
+
401
+ /**
402
+ * Get current error rate for a category
403
+ * @param {string} category - Error category
404
+ * @returns {number}
405
+ */
406
+ getErrorRate(category) {
407
+ const now = Date.now();
408
+ const currentWindow = Math.floor(now / this.rateWindow);
409
+ const key = `${category}:${currentWindow}`;
410
+ return this.errorRates.get(key) || 0;
411
+ }
412
+
413
+ /**
414
+ * Register a recovery handler for a category
415
+ * @param {string} category - Error category
416
+ * @param {Function} handler - Recovery handler
417
+ */
418
+ registerRecoveryHandler(category, handler) {
419
+ this.recoveryHandlers.set(category, handler);
420
+ }
421
+
422
+ /**
423
+ * Attempt recovery for an error
424
+ * @param {AlephError} error - Error to recover from
425
+ * @param {Object} context - Error context
426
+ * @returns {Promise<Object>}
427
+ */
428
+ async attemptRecovery(error, context) {
429
+ const handler = this.recoveryHandlers.get(error.category);
430
+
431
+ if (!handler) {
432
+ return { retry: false };
433
+ }
434
+
435
+ try {
436
+ return await handler(error, context);
437
+ } catch (recoveryError) {
438
+ if (this.logger) {
439
+ this.logger.warn('Recovery failed', {
440
+ originalError: error.code,
441
+ recoveryError: recoveryError.message
442
+ });
443
+ }
444
+ return { retry: false };
445
+ }
446
+ }
447
+
448
+ /**
449
+ * Get error statistics
450
+ * @returns {Object}
451
+ */
452
+ getStats() {
453
+ const categories = {};
454
+ for (const error of this.errors) {
455
+ categories[error.category] = (categories[error.category] || 0) + 1;
456
+ }
457
+
458
+ return {
459
+ totalErrors: this.errors.length,
460
+ byCategory: categories,
461
+ recentErrors: this.errors.slice(-10),
462
+ errorRates: Object.fromEntries(this.errorRates)
463
+ };
464
+ }
465
+ }
466
+
467
+ // ============================================================================
468
+ // ASYNC WRAPPERS
469
+ // ============================================================================
470
+
471
+ /**
472
+ * Wrap an async function with error handling
473
+ * @param {Function} fn - Async function to wrap
474
+ * @param {ErrorHandler} handler - Error handler
475
+ * @param {Object} options - Options
476
+ * @returns {Function}
477
+ */
478
+ function withErrorHandling(fn, handler, options = {}) {
479
+ const maxRetries = options.maxRetries || 3;
480
+ const context = options.context || {};
481
+
482
+ return async function(...args) {
483
+ let lastError;
484
+
485
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
486
+ try {
487
+ return await fn.apply(this, args);
488
+ } catch (error) {
489
+ lastError = error;
490
+
491
+ const result = await handler.handle(error, {
492
+ ...context,
493
+ attempt,
494
+ maxRetries,
495
+ canRetry: attempt < maxRetries
496
+ });
497
+
498
+ if (!result.retry) {
499
+ throw handler.normalize(error);
500
+ }
501
+
502
+ // Apply backoff
503
+ const backoff = options.backoff || ((a) => Math.pow(2, a) * 100);
504
+ await new Promise(r => setTimeout(r, backoff(attempt)));
505
+ }
506
+ }
507
+
508
+ throw handler.normalize(lastError);
509
+ };
510
+ }
511
+
512
+ /**
513
+ * Create an async error boundary
514
+ * @param {Function} fn - Async function
515
+ * @param {Object} options - Options
516
+ * @returns {Promise}
517
+ */
518
+ async function errorBoundary(fn, options = {}) {
519
+ const handler = options.handler;
520
+ const fallback = options.fallback;
521
+
522
+ if (!handler) {
523
+ throw new Error('errorBoundary requires an error handler');
524
+ }
525
+
526
+ try {
527
+ return await fn();
528
+ } catch (error) {
529
+ const result = await handler.handle(error, options.context);
530
+
531
+ if (fallback !== undefined) {
532
+ return typeof fallback === 'function' ? fallback(error) : fallback;
533
+ }
534
+
535
+ throw result.error;
536
+ }
537
+ }
538
+
539
+ /**
540
+ * Wrap a promise with timeout
541
+ * @param {Promise} promise - Promise to wrap
542
+ * @param {number} timeout - Timeout in ms
543
+ * @param {string} operation - Operation name
544
+ * @returns {Promise}
545
+ */
546
+ function withTimeout(promise, timeout, operation = 'operation') {
547
+ return Promise.race([
548
+ promise,
549
+ new Promise((_, reject) => {
550
+ setTimeout(() => {
551
+ reject(new TimeoutError(`${operation} timed out after ${timeout}ms`, {
552
+ timeout,
553
+ operation
554
+ }));
555
+ }, timeout);
556
+ })
557
+ ]);
558
+ }
559
+
560
+ // ============================================================================
561
+ // EXPORTS
562
+ // ============================================================================
563
+
564
+ module.exports = {
565
+ // Levels and categories
566
+ LogLevel,
567
+ LogLevelNames,
568
+ ErrorCategory,
569
+
570
+ // Error classes
571
+ AlephError,
572
+ NetworkError,
573
+ LLMError,
574
+ ValidationError,
575
+ TimeoutError,
576
+
577
+ // Event emitter (browser-compatible)
578
+ SimpleEventEmitter,
579
+
580
+ // Error handler
581
+ ErrorHandler,
582
+
583
+ // Utilities
584
+ withErrorHandling,
585
+ errorBoundary,
586
+ withTimeout
587
+ };