@robota-sdk/agent-plugin 3.0.0-beta.64

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.
Files changed (82) hide show
  1. package/LICENSE +21 -0
  2. package/dist/node/index.cjs +1 -0
  3. package/dist/node/index.d.ts +1724 -0
  4. package/dist/node/index.d.ts.map +1 -0
  5. package/dist/node/index.js +2 -0
  6. package/dist/node/index.js.map +1 -0
  7. package/package.json +48 -0
  8. package/src/conversation-history/__tests__/conversation-history-plugin.test.ts +221 -0
  9. package/src/conversation-history/__tests__/history-storages.test.ts +115 -0
  10. package/src/conversation-history/conversation-history-helpers.ts +120 -0
  11. package/src/conversation-history/conversation-history-plugin.ts +294 -0
  12. package/src/conversation-history/index.ts +11 -0
  13. package/src/conversation-history/storages/database-storage.ts +96 -0
  14. package/src/conversation-history/storages/file-storage.ts +95 -0
  15. package/src/conversation-history/storages/index.ts +3 -0
  16. package/src/conversation-history/storages/memory-storage.ts +44 -0
  17. package/src/conversation-history/types.ts +64 -0
  18. package/src/error-handling/__tests__/error-handling-plugin.test.ts +201 -0
  19. package/src/error-handling/context-adapter.ts +48 -0
  20. package/src/error-handling/error-handling-helpers.ts +53 -0
  21. package/src/error-handling/error-handling-plugin.ts +293 -0
  22. package/src/error-handling/index.ts +9 -0
  23. package/src/error-handling/types.ts +82 -0
  24. package/src/execution-analytics/__tests__/execution-analytics-plugin.test.ts +224 -0
  25. package/src/execution-analytics/analytics-aggregation.ts +88 -0
  26. package/src/execution-analytics/execution-analytics-helpers.ts +83 -0
  27. package/src/execution-analytics/execution-analytics-plugin.ts +315 -0
  28. package/src/execution-analytics/index.ts +9 -0
  29. package/src/execution-analytics/types.ts +97 -0
  30. package/src/index.ts +8 -0
  31. package/src/limits/__tests__/limits-plugin.test.ts +712 -0
  32. package/src/limits/index.ts +9 -0
  33. package/src/limits/limits-helpers.ts +185 -0
  34. package/src/limits/limits-plugin.ts +196 -0
  35. package/src/limits/types.ts +73 -0
  36. package/src/limits/validation.ts +81 -0
  37. package/src/logging/__tests__/formatters.test.ts +48 -0
  38. package/src/logging/__tests__/logging-plugin.test.ts +464 -0
  39. package/src/logging/__tests__/logging-storages.test.ts +95 -0
  40. package/src/logging/formatters.ts +28 -0
  41. package/src/logging/index.ts +15 -0
  42. package/src/logging/logging-helpers.ts +223 -0
  43. package/src/logging/logging-plugin.ts +288 -0
  44. package/src/logging/storages/console-storage.ts +44 -0
  45. package/src/logging/storages/file-storage.ts +44 -0
  46. package/src/logging/storages/index.ts +4 -0
  47. package/src/logging/storages/remote-storage.ts +78 -0
  48. package/src/logging/storages/silent-storage.ts +18 -0
  49. package/src/logging/types.ts +106 -0
  50. package/src/performance/__tests__/memory-storage.test.ts +86 -0
  51. package/src/performance/__tests__/performance-plugin.test.ts +208 -0
  52. package/src/performance/__tests__/system-metrics-collector.test.ts +33 -0
  53. package/src/performance/collectors/system-metrics-collector.ts +69 -0
  54. package/src/performance/index.ts +12 -0
  55. package/src/performance/performance-helpers.ts +86 -0
  56. package/src/performance/performance-plugin.ts +274 -0
  57. package/src/performance/storages/index.ts +1 -0
  58. package/src/performance/storages/memory-storage.ts +88 -0
  59. package/src/performance/types.ts +160 -0
  60. package/src/usage/__tests__/aggregate-usage-stats.test.ts +136 -0
  61. package/src/usage/__tests__/memory-storage.test.ts +83 -0
  62. package/src/usage/__tests__/silent-storage.test.ts +44 -0
  63. package/src/usage/__tests__/usage-plugin-helpers.test.ts +155 -0
  64. package/src/usage/__tests__/usage-plugin.test.ts +358 -0
  65. package/src/usage/aggregate-usage-stats.ts +142 -0
  66. package/src/usage/index.ts +14 -0
  67. package/src/usage/storages/file-storage.ts +115 -0
  68. package/src/usage/storages/index.ts +4 -0
  69. package/src/usage/storages/memory-storage.ts +61 -0
  70. package/src/usage/storages/remote-storage.ts +143 -0
  71. package/src/usage/storages/silent-storage.ts +38 -0
  72. package/src/usage/types.ts +132 -0
  73. package/src/usage/usage-plugin-helpers.ts +116 -0
  74. package/src/usage/usage-plugin.ts +296 -0
  75. package/src/webhook/__tests__/webhook-plugin.test.ts +560 -0
  76. package/src/webhook/http-client.ts +141 -0
  77. package/src/webhook/index.ts +9 -0
  78. package/src/webhook/transformer.ts +209 -0
  79. package/src/webhook/types.ts +201 -0
  80. package/src/webhook/webhook-helpers.ts +60 -0
  81. package/src/webhook/webhook-plugin.ts +298 -0
  82. package/src/webhook/webhook-queue.ts +148 -0
@@ -0,0 +1,201 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { ErrorHandlingPlugin } from '../error-handling-plugin';
3
+ import { ConfigurationError } from '@robota-sdk/agent-core';
4
+
5
+ describe('ErrorHandlingPlugin', () => {
6
+ describe('constructor', () => {
7
+ it('initializes with simple strategy', () => {
8
+ const plugin = new ErrorHandlingPlugin({ strategy: 'simple' });
9
+ expect(plugin.name).toBe('ErrorHandlingPlugin');
10
+ expect(plugin.version).toBe('1.0.0');
11
+ });
12
+
13
+ it('throws on missing strategy', () => {
14
+ expect(() => new ErrorHandlingPlugin({ strategy: '' as any })).toThrow(ConfigurationError);
15
+ });
16
+
17
+ it('throws on invalid strategy', () => {
18
+ expect(() => new ErrorHandlingPlugin({ strategy: 'invalid' as any })).toThrow(
19
+ ConfigurationError,
20
+ );
21
+ });
22
+
23
+ it('throws on negative maxRetries', () => {
24
+ expect(() => new ErrorHandlingPlugin({ strategy: 'simple', maxRetries: -1 })).toThrow(
25
+ ConfigurationError,
26
+ );
27
+ });
28
+
29
+ it('throws on non-positive retryDelay', () => {
30
+ expect(() => new ErrorHandlingPlugin({ strategy: 'simple', retryDelay: 0 })).toThrow(
31
+ ConfigurationError,
32
+ );
33
+ });
34
+ });
35
+
36
+ describe('handleError', () => {
37
+ it('handles error with simple strategy', async () => {
38
+ const plugin = new ErrorHandlingPlugin({ strategy: 'simple' });
39
+ await expect(plugin.handleError(new Error('test'))).resolves.not.toThrow();
40
+ });
41
+
42
+ it('handles error with silent strategy', async () => {
43
+ const plugin = new ErrorHandlingPlugin({ strategy: 'silent' });
44
+ await expect(plugin.handleError(new Error('test'))).resolves.not.toThrow();
45
+ });
46
+
47
+ it('invokes custom error handler when provided', async () => {
48
+ const customHandler = vi.fn().mockResolvedValue(undefined);
49
+ const plugin = new ErrorHandlingPlugin({
50
+ strategy: 'simple',
51
+ customErrorHandler: customHandler,
52
+ });
53
+
54
+ const error = new Error('custom test');
55
+ await plugin.handleError(error, { executionId: 'e1' });
56
+
57
+ expect(customHandler).toHaveBeenCalledWith(error, { executionId: 'e1' });
58
+ });
59
+
60
+ it('falls back to strategy when custom handler throws', async () => {
61
+ const customHandler = vi.fn().mockRejectedValue(new Error('handler fail'));
62
+ const plugin = new ErrorHandlingPlugin({
63
+ strategy: 'simple',
64
+ customErrorHandler: customHandler,
65
+ });
66
+
67
+ await expect(plugin.handleError(new Error('test'))).resolves.not.toThrow();
68
+ });
69
+ });
70
+
71
+ describe('circuit breaker', () => {
72
+ it('opens circuit after reaching failure threshold', async () => {
73
+ const plugin = new ErrorHandlingPlugin({
74
+ strategy: 'circuit-breaker',
75
+ failureThreshold: 3,
76
+ });
77
+
78
+ for (let i = 0; i < 3; i++) {
79
+ await plugin.handleError(new Error(`error ${i}`));
80
+ }
81
+
82
+ const stats = plugin.getStats();
83
+ expect(stats.circuitBreakerOpen).toBe(true);
84
+ expect(stats.failureCount).toBe(3);
85
+ });
86
+
87
+ it('does not open circuit below threshold', async () => {
88
+ const plugin = new ErrorHandlingPlugin({
89
+ strategy: 'circuit-breaker',
90
+ failureThreshold: 5,
91
+ });
92
+
93
+ await plugin.handleError(new Error('error'));
94
+ await plugin.handleError(new Error('error'));
95
+
96
+ const stats = plugin.getStats();
97
+ expect(stats.circuitBreakerOpen).toBe(false);
98
+ expect(stats.failureCount).toBe(2);
99
+ });
100
+
101
+ it('resets circuit breaker manually', async () => {
102
+ const plugin = new ErrorHandlingPlugin({
103
+ strategy: 'circuit-breaker',
104
+ failureThreshold: 2,
105
+ });
106
+
107
+ await plugin.handleError(new Error('e1'));
108
+ await plugin.handleError(new Error('e2'));
109
+ expect(plugin.getStats().circuitBreakerOpen).toBe(true);
110
+
111
+ plugin.resetCircuitBreaker();
112
+ const stats = plugin.getStats();
113
+ expect(stats.circuitBreakerOpen).toBe(false);
114
+ expect(stats.failureCount).toBe(0);
115
+ expect(stats.lastFailureTime).toBe(0);
116
+ });
117
+ });
118
+
119
+ describe('executeWithRetry', () => {
120
+ it('succeeds on first attempt', async () => {
121
+ const plugin = new ErrorHandlingPlugin({
122
+ strategy: 'simple',
123
+ maxRetries: 2,
124
+ retryDelay: 1,
125
+ });
126
+
127
+ const fn = vi.fn().mockResolvedValue('success');
128
+ const result = await plugin.executeWithRetry(fn);
129
+
130
+ expect(result).toBe('success');
131
+ expect(fn).toHaveBeenCalledTimes(1);
132
+ });
133
+
134
+ it('retries and succeeds on later attempt', async () => {
135
+ const plugin = new ErrorHandlingPlugin({
136
+ strategy: 'simple',
137
+ maxRetries: 3,
138
+ retryDelay: 1,
139
+ });
140
+
141
+ const fn = vi
142
+ .fn()
143
+ .mockRejectedValueOnce(new Error('fail 1'))
144
+ .mockRejectedValueOnce(new Error('fail 2'))
145
+ .mockResolvedValue('success');
146
+
147
+ const result = await plugin.executeWithRetry(fn);
148
+ expect(result).toBe('success');
149
+ expect(fn).toHaveBeenCalledTimes(3);
150
+ });
151
+
152
+ it('throws after exhausting retries', async () => {
153
+ const plugin = new ErrorHandlingPlugin({
154
+ strategy: 'simple',
155
+ maxRetries: 1,
156
+ retryDelay: 1,
157
+ });
158
+
159
+ const fn = vi.fn().mockRejectedValue(new Error('always fail'));
160
+
161
+ await expect(plugin.executeWithRetry(fn)).rejects.toThrow('Operation failed after 1 retries');
162
+ expect(fn).toHaveBeenCalledTimes(2); // initial + 1 retry
163
+ });
164
+
165
+ it('blocks when circuit breaker is open', async () => {
166
+ const plugin = new ErrorHandlingPlugin({
167
+ strategy: 'circuit-breaker',
168
+ failureThreshold: 1,
169
+ maxRetries: 0,
170
+ retryDelay: 1,
171
+ circuitBreakerTimeout: 60000,
172
+ });
173
+
174
+ // Open the circuit breaker
175
+ await plugin.handleError(new Error('trip'));
176
+ expect(plugin.getStats().circuitBreakerOpen).toBe(true);
177
+
178
+ const fn = vi.fn().mockResolvedValue('ok');
179
+ await expect(plugin.executeWithRetry(fn)).rejects.toThrow('Operation failed after 0 retries');
180
+ expect(fn).not.toHaveBeenCalled();
181
+ });
182
+ });
183
+
184
+ describe('getStats', () => {
185
+ it('returns initial stats', () => {
186
+ const plugin = new ErrorHandlingPlugin({ strategy: 'simple' });
187
+ const stats = plugin.getStats();
188
+
189
+ expect(stats.failureCount).toBe(0);
190
+ expect(stats.circuitBreakerOpen).toBe(false);
191
+ expect(stats.lastFailureTime).toBe(0);
192
+ });
193
+ });
194
+
195
+ describe('destroy', () => {
196
+ it('completes without error', async () => {
197
+ const plugin = new ErrorHandlingPlugin({ strategy: 'simple' });
198
+ await expect(plugin.destroy()).resolves.not.toThrow();
199
+ });
200
+ });
201
+ });
@@ -0,0 +1,48 @@
1
+ /**
2
+ * ErrorHandling Plugin - Context adapter utilities for Facade pattern
3
+ *
4
+ * REASON: TypeScript exactOptionalPropertyTypes strict mode requires special handling for optional properties
5
+ * ALTERNATIVES_CONSIDERED: Bracket notation (rejected by TS), type assertions (rejected), interface modification (breaks compatibility), union types (causes dependencies), removing context (loses debugging)
6
+ * TODO: Consider unified error context type system across all plugins
7
+ */
8
+
9
+ import type { IErrorHandlingContextData, IErrorContextAdapter } from './types';
10
+ import type { TErrorContextData } from '@robota-sdk/agent-core';
11
+
12
+ /**
13
+ * Convert IErrorHandlingContextData to ErrorContextData-compatible format for PluginError
14
+ * REASON: Simple object spread with known properties to avoid index signature conflicts
15
+ * ALTERNATIVES_CONSIDERED: Complex type adapters (unnecessary), Object.assign (verbose), bracket notation (rejected by TS), type assertions (reduces safety)
16
+ * TODO: Consider unified error context type system across all plugins
17
+ */
18
+ export function toErrorContext(context: IErrorHandlingContextData): IErrorContextAdapter {
19
+ return {
20
+ ...(context.executionId && { executionId: context.executionId }),
21
+ ...(context.sessionId && { sessionId: context.sessionId }),
22
+ ...(context.userId && { userId: context.userId }),
23
+ ...(context.attempt !== undefined && { attempt: context.attempt }),
24
+ ...(context.originalError && { originalError: context.originalError }),
25
+ };
26
+ }
27
+
28
+ /**
29
+ * Safe context extraction for PluginError with nested context structure
30
+ *
31
+ * REASON: PluginError context needs flexible data structure for debugging information
32
+ * ALTERNATIVES_CONSIDERED:
33
+ * 1. Strict primitive types (loses debugging information)
34
+ * 2. Interface definitions (too rigid for error contexts)
35
+ * 3. Union types (becomes unwieldy for error data)
36
+ * 4. Generic constraints (too complex for error handling)
37
+ * 5. Type assertions (decreases type safety)
38
+ * TODO: Consider standardized error context interface if patterns emerge
39
+ */
40
+ export function createPluginErrorContext(
41
+ context: IErrorHandlingContextData,
42
+ additionalData?: TErrorContextData,
43
+ ): TErrorContextData {
44
+ return {
45
+ ...toErrorContext(context),
46
+ ...(additionalData && { ...additionalData }),
47
+ };
48
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Error Handling Plugin - Validation and strategy handler helpers.
3
+ *
4
+ * Extracted from error-handling-plugin.ts to keep each file under 300 lines.
5
+ * @internal
6
+ */
7
+
8
+ import { ConfigurationError } from '@robota-sdk/agent-core';
9
+ import type { IErrorHandlingPluginOptions } from './types';
10
+
11
+ /** Validate ErrorHandlingPlugin constructor options. @internal */
12
+ export function validateErrorHandlingOptions(options: IErrorHandlingPluginOptions): void {
13
+ if (!options.strategy) {
14
+ throw new ConfigurationError('Error handling strategy is required');
15
+ }
16
+
17
+ if (!['simple', 'circuit-breaker', 'exponential-backoff', 'silent'].includes(options.strategy)) {
18
+ throw new ConfigurationError('Invalid error handling strategy', {
19
+ validStrategies: ['simple', 'circuit-breaker', 'exponential-backoff', 'silent'],
20
+ provided: options.strategy,
21
+ });
22
+ }
23
+
24
+ if (options.maxRetries !== undefined && options.maxRetries < 0) {
25
+ throw new ConfigurationError('Max retries must be non-negative');
26
+ }
27
+
28
+ if (options.retryDelay !== undefined && options.retryDelay <= 0) {
29
+ throw new ConfigurationError('Retry delay must be positive');
30
+ }
31
+ }
32
+
33
+ /** Resolve retry delay based on strategy and attempt number. @internal */
34
+ export function resolveRetryDelay(strategy: string, baseDelay: number, attempt: number): number {
35
+ return strategy === 'exponential-backoff' ? baseDelay * Math.pow(2, attempt - 1) : baseDelay;
36
+ }
37
+
38
+ /** Resolve true if the circuit breaker timeout has not yet elapsed. @internal */
39
+ export function isCircuitBreakerStillOpen(
40
+ circuitBreakerOpen: boolean,
41
+ lastFailureTime: number,
42
+ circuitBreakerTimeout: number,
43
+ ): { open: boolean; shouldReset: boolean } {
44
+ if (!circuitBreakerOpen) return { open: false, shouldReset: false };
45
+ const timeoutPassed = Date.now() - lastFailureTime > circuitBreakerTimeout;
46
+ if (timeoutPassed) return { open: false, shouldReset: true };
47
+ return { open: true, shouldReset: false };
48
+ }
49
+
50
+ /** Sleep for a given number of milliseconds. @internal */
51
+ export function sleep(ms: number): Promise<void> {
52
+ return new Promise((resolve) => setTimeout(resolve, ms));
53
+ }
@@ -0,0 +1,293 @@
1
+ import {
2
+ AbstractPlugin,
3
+ PluginCategory,
4
+ PluginPriority,
5
+ createLogger,
6
+ type ILogger,
7
+ PluginError,
8
+ } from '@robota-sdk/agent-core';
9
+
10
+ // Import from Facade pattern modules for type safety
11
+ import type {
12
+ IErrorHandlingContextData,
13
+ IErrorHandlingPluginOptions,
14
+ IErrorHandlingPluginStats,
15
+ } from './types';
16
+ import { toErrorContext, createPluginErrorContext } from './context-adapter';
17
+ import {
18
+ validateErrorHandlingOptions,
19
+ resolveRetryDelay,
20
+ isCircuitBreakerStillOpen,
21
+ sleep,
22
+ } from './error-handling-helpers';
23
+
24
+ const DEFAULT_MAX_RETRIES = 3;
25
+ const DEFAULT_RETRY_DELAY_MS = 1000;
26
+ const DEFAULT_FAILURE_THRESHOLD = 5;
27
+ const DEFAULT_CIRCUIT_BREAKER_TIMEOUT_MS = 60000;
28
+
29
+ /**
30
+ * Provides configurable error recovery using one of four strategies:
31
+ * simple logging, circuit breaker, exponential backoff, or silent.
32
+ *
33
+ * The circuit breaker opens after
34
+ * {@link IErrorHandlingPluginOptions.failureThreshold | failureThreshold}
35
+ * consecutive failures and automatically resets after
36
+ * {@link IErrorHandlingPluginOptions.circuitBreakerTimeout | circuitBreakerTimeout} ms.
37
+ * An optional custom error handler can be injected for application-specific
38
+ * recovery logic.
39
+ *
40
+ * @extends AbstractPlugin
41
+ * @see IErrorHandlingPluginOptions - configuration options
42
+ * @see IErrorHandlingContextData - error context contract
43
+ *
44
+ * @example
45
+ * ```ts
46
+ * const plugin = new ErrorHandlingPlugin({
47
+ * strategy: 'circuit-breaker',
48
+ * failureThreshold: 5,
49
+ * circuitBreakerTimeout: 60000,
50
+ * });
51
+ * const result = await plugin.executeWithRetry(() => fetchData());
52
+ * ```
53
+ */
54
+ export class ErrorHandlingPlugin extends AbstractPlugin<
55
+ IErrorHandlingPluginOptions,
56
+ IErrorHandlingPluginStats
57
+ > {
58
+ name = 'ErrorHandlingPlugin';
59
+ version = '1.0.0';
60
+
61
+ private pluginOptions: Required<Omit<IErrorHandlingPluginOptions, 'customErrorHandler'>> & {
62
+ customErrorHandler?: (error: Error, context: IErrorHandlingContextData) => Promise<void>;
63
+ };
64
+ private logger: ILogger;
65
+ private failureCount = 0;
66
+ private circuitBreakerOpen = false;
67
+ private lastFailureTime = 0;
68
+
69
+ constructor(options: IErrorHandlingPluginOptions) {
70
+ super();
71
+ this.logger = createLogger('ErrorHandlingPlugin');
72
+
73
+ // Validate options
74
+ validateErrorHandlingOptions(options);
75
+
76
+ // Set defaults
77
+ this.pluginOptions = {
78
+ enabled: options.enabled ?? true,
79
+ strategy: options.strategy,
80
+ maxRetries: options.maxRetries ?? DEFAULT_MAX_RETRIES,
81
+ retryDelay: options.retryDelay ?? DEFAULT_RETRY_DELAY_MS,
82
+ logErrors: options.logErrors ?? true,
83
+ failureThreshold: options.failureThreshold ?? DEFAULT_FAILURE_THRESHOLD,
84
+ circuitBreakerTimeout: options.circuitBreakerTimeout ?? DEFAULT_CIRCUIT_BREAKER_TIMEOUT_MS,
85
+ // Add plugin options defaults
86
+ category: options.category ?? PluginCategory.ERROR_HANDLING,
87
+ priority: options.priority ?? PluginPriority.HIGH,
88
+ moduleEvents: options.moduleEvents ?? [],
89
+ subscribeToAllModuleEvents: options.subscribeToAllModuleEvents ?? false,
90
+ ...(options.customErrorHandler && { customErrorHandler: options.customErrorHandler }),
91
+ };
92
+
93
+ this.logger.info('ErrorHandlingPlugin initialized', {
94
+ strategy: this.pluginOptions.strategy,
95
+ maxRetries: this.pluginOptions.maxRetries,
96
+ failureThreshold: this.pluginOptions.failureThreshold,
97
+ });
98
+ }
99
+
100
+ /**
101
+ * Dispatches the error to the active strategy handler. If a custom error
102
+ * handler is configured, it takes precedence over strategy-specific handling.
103
+ */
104
+ async handleError(error: Error, context: IErrorHandlingContextData = {}): Promise<void> {
105
+ if (this.pluginOptions.logErrors) {
106
+ this.logger.error('Error occurred', {
107
+ error: error.message,
108
+ stack: error.stack,
109
+ context: context,
110
+ });
111
+ }
112
+
113
+ // Custom error handler takes precedence
114
+ if (this.pluginOptions.customErrorHandler) {
115
+ try {
116
+ await this.pluginOptions.customErrorHandler(error, context);
117
+ return;
118
+ } catch (handlerError) {
119
+ this.logger.error('Custom error handler failed', {
120
+ handlerError: handlerError instanceof Error ? handlerError.message : String(handlerError),
121
+ });
122
+ }
123
+ }
124
+
125
+ // Apply strategy-specific handling
126
+ switch (this.pluginOptions.strategy) {
127
+ case 'circuit-breaker':
128
+ await this.handleCircuitBreaker(error, context);
129
+ break;
130
+ case 'exponential-backoff':
131
+ await this.handleExponentialBackoff(error, context);
132
+ break;
133
+ case 'simple':
134
+ await this.handleSimple(error, context);
135
+ break;
136
+ case 'silent':
137
+ // Silent mode - do nothing
138
+ break;
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Executes a function with automatic retries up to
144
+ * {@link IErrorHandlingPluginOptions.maxRetries | maxRetries}. The delay
145
+ * between retries follows the configured strategy (fixed for simple /
146
+ * circuit-breaker, doubling for exponential-backoff).
147
+ *
148
+ * @throws PluginError after all retry attempts are exhausted
149
+ */
150
+ async executeWithRetry<T>(
151
+ fn: () => Promise<T>,
152
+ context: IErrorHandlingContextData = {},
153
+ ): Promise<T> {
154
+ let lastError: Error | undefined;
155
+ let attempt = 0;
156
+
157
+ while (attempt <= this.pluginOptions.maxRetries) {
158
+ try {
159
+ // Check circuit breaker
160
+ if (this.pluginOptions.strategy === 'circuit-breaker') {
161
+ const cbState = isCircuitBreakerStillOpen(
162
+ this.circuitBreakerOpen,
163
+ this.lastFailureTime,
164
+ this.pluginOptions.circuitBreakerTimeout,
165
+ );
166
+ if (cbState.shouldReset) {
167
+ this.circuitBreakerOpen = false;
168
+ this.failureCount = 0;
169
+ this.logger.info('Circuit breaker timeout passed, attempting to close');
170
+ }
171
+ if (cbState.open) {
172
+ throw new PluginError('Circuit breaker is open', this.name, toErrorContext(context));
173
+ }
174
+ }
175
+
176
+ const result = await fn();
177
+
178
+ // Reset failure count on success
179
+ if (attempt > 0) {
180
+ this.failureCount = 0;
181
+ this.circuitBreakerOpen = false;
182
+ this.logger.info('Operation succeeded after retry', {
183
+ attempt: attempt,
184
+ context: context,
185
+ });
186
+ }
187
+
188
+ return result;
189
+ } catch (error) {
190
+ lastError = error instanceof Error ? error : new Error(String(error));
191
+ attempt++;
192
+
193
+ if (attempt <= this.pluginOptions.maxRetries) {
194
+ await this.handleError(lastError, { ...context, attempt });
195
+
196
+ // Calculate delay
197
+ const delay = resolveRetryDelay(
198
+ this.pluginOptions.strategy,
199
+ this.pluginOptions.retryDelay,
200
+ attempt,
201
+ );
202
+
203
+ this.logger.debug('Retrying operation', {
204
+ attempt: attempt,
205
+ delay: delay,
206
+ context: context,
207
+ });
208
+ await sleep(delay);
209
+ } else {
210
+ await this.handleError(lastError, { ...context, finalAttempt: true });
211
+ }
212
+ }
213
+ }
214
+
215
+ throw new PluginError(
216
+ `Operation failed after ${this.pluginOptions.maxRetries} retries`,
217
+ this.name,
218
+ createPluginErrorContext(context, {
219
+ originalError: lastError?.message || 'Unknown error',
220
+ }),
221
+ );
222
+ }
223
+
224
+ /**
225
+ * Resets the circuit breaker to closed state, clearing failure count and
226
+ * last failure timestamp.
227
+ */
228
+ resetCircuitBreaker(): void {
229
+ this.failureCount = 0;
230
+ this.circuitBreakerOpen = false;
231
+ this.lastFailureTime = 0;
232
+ this.logger.info('Circuit breaker reset');
233
+ }
234
+
235
+ /**
236
+ * Get error handling statistics
237
+ */
238
+ override getStats(): IErrorHandlingPluginStats {
239
+ const base = super.getStats();
240
+ return {
241
+ ...base,
242
+ failureCount: this.failureCount,
243
+ circuitBreakerOpen: this.circuitBreakerOpen,
244
+ lastFailureTime: this.lastFailureTime,
245
+ totalRetries: 0, // TODO: Track total retries
246
+ successfulRecoveries: 0, // TODO: Track successful recoveries
247
+ };
248
+ }
249
+
250
+ /**
251
+ * Cleanup resources
252
+ */
253
+ async destroy(): Promise<void> {
254
+ this.logger.info('ErrorHandlingPlugin destroyed');
255
+ }
256
+
257
+ private async handleSimple(error: Error, context: IErrorHandlingContextData): Promise<void> {
258
+ // Simple logging - no additional logic
259
+ this.logger.debug('Simple error handling applied', {
260
+ error: error.message,
261
+ context: context,
262
+ });
263
+ }
264
+
265
+ private async handleCircuitBreaker(
266
+ _error: Error,
267
+ context: IErrorHandlingContextData,
268
+ ): Promise<void> {
269
+ this.failureCount++;
270
+ this.lastFailureTime = Date.now();
271
+
272
+ if (this.failureCount >= this.pluginOptions.failureThreshold) {
273
+ this.circuitBreakerOpen = true;
274
+ this.logger.warn('Circuit breaker opened', {
275
+ failureCount: this.failureCount,
276
+ threshold: this.pluginOptions.failureThreshold,
277
+ context: context,
278
+ });
279
+ }
280
+ }
281
+
282
+ private async handleExponentialBackoff(
283
+ _error: Error,
284
+ context: IErrorHandlingContextData,
285
+ ): Promise<void> {
286
+ this.failureCount++;
287
+ this.logger.debug('Exponential backoff error handling applied', {
288
+ error: _error.message,
289
+ failureCount: this.failureCount,
290
+ context: context,
291
+ });
292
+ }
293
+ }
@@ -0,0 +1,9 @@
1
+ export { ErrorHandlingPlugin } from './error-handling-plugin';
2
+ export { toErrorContext, createPluginErrorContext } from './context-adapter';
3
+ export type {
4
+ TErrorHandlingStrategy,
5
+ IErrorHandlingContextData,
6
+ IErrorHandlingPluginOptions,
7
+ IErrorHandlingPluginStats,
8
+ IErrorContextAdapter,
9
+ } from './types';
@@ -0,0 +1,82 @@
1
+ /**
2
+ * ErrorHandling Plugin - Type definitions for Facade pattern implementation
3
+ *
4
+ * REASON: Complex ErrorContextData compatibility issues require type adapters and unified error handling
5
+ * ALTERNATIVES_CONSIDERED:
6
+ * 1. Modify ErrorContextData type globally (breaks other components)
7
+ * 2. Use type assertions everywhere (reduces type safety)
8
+ * 3. Create complex union types (causes circular dependencies)
9
+ * 4. Redesign PluginError constructor (breaks existing contracts)
10
+ * 5. Remove context from error handling (loses debugging capability)
11
+ * TODO: Consider unified error context system across all plugins
12
+ */
13
+
14
+ /**
15
+ * Error handling strategy types
16
+ */
17
+ export type TErrorHandlingStrategy =
18
+ | 'simple'
19
+ | 'circuit-breaker'
20
+ | 'exponential-backoff'
21
+ | 'silent';
22
+
23
+ /**
24
+ * Base error context for internal operations
25
+ * REASON: Optional properties and index signature for compatibility with both exactOptionalPropertyTypes and logger constraints
26
+ * ALTERNATIVES_CONSIDERED: Union types (breaks interface compatibility), explicit undefined (still triggers index signature rules), removing optional properties (breaks existing usage), type assertions (loses safety), intersection types (complex propagation)
27
+ * TODO: Consider unified error context type system across all plugins
28
+ */
29
+ export interface IErrorHandlingContextData {
30
+ executionId?: string;
31
+ sessionId?: string;
32
+ userId?: string;
33
+ attempt?: number;
34
+ finalAttempt?: boolean;
35
+ originalError?: string;
36
+ [key: string]: string | number | boolean | undefined;
37
+ }
38
+
39
+ import type { IPluginOptions, IPluginStats } from '@robota-sdk/agent-core';
40
+
41
+ /**
42
+ * Configuration options for error handling plugin
43
+ */
44
+ export interface IErrorHandlingPluginOptions extends IPluginOptions {
45
+ /** Error handling strategy to use */
46
+ strategy: TErrorHandlingStrategy;
47
+ /** Maximum number of retry attempts */
48
+ maxRetries?: number;
49
+ /** Initial delay between retries in milliseconds */
50
+ retryDelay?: number;
51
+ /** Whether to log errors */
52
+ logErrors?: boolean;
53
+ /** Circuit breaker failure threshold */
54
+ failureThreshold?: number;
55
+ /** Circuit breaker timeout in milliseconds */
56
+ circuitBreakerTimeout?: number;
57
+ /** Custom error handler function */
58
+ customErrorHandler?: (error: Error, context: IErrorHandlingContextData) => Promise<void>;
59
+ }
60
+
61
+ /**
62
+ * Error handling plugin statistics
63
+ */
64
+ export interface IErrorHandlingPluginStats extends IPluginStats {
65
+ failureCount: number;
66
+ circuitBreakerOpen: boolean;
67
+ lastFailureTime: number;
68
+ totalRetries: number;
69
+ successfulRecoveries: number;
70
+ }
71
+
72
+ /**
73
+ * Error context adapter for PluginError compatibility
74
+ */
75
+ export interface IErrorContextAdapter {
76
+ originalError?: string;
77
+ executionId?: string;
78
+ sessionId?: string;
79
+ userId?: string;
80
+ attempt?: number;
81
+ [key: string]: string | number | boolean | Date | Error | string[] | undefined;
82
+ }