@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.
- package/LICENSE +21 -0
- package/dist/node/index.cjs +1 -0
- package/dist/node/index.d.ts +1724 -0
- package/dist/node/index.d.ts.map +1 -0
- package/dist/node/index.js +2 -0
- package/dist/node/index.js.map +1 -0
- package/package.json +48 -0
- package/src/conversation-history/__tests__/conversation-history-plugin.test.ts +221 -0
- package/src/conversation-history/__tests__/history-storages.test.ts +115 -0
- package/src/conversation-history/conversation-history-helpers.ts +120 -0
- package/src/conversation-history/conversation-history-plugin.ts +294 -0
- package/src/conversation-history/index.ts +11 -0
- package/src/conversation-history/storages/database-storage.ts +96 -0
- package/src/conversation-history/storages/file-storage.ts +95 -0
- package/src/conversation-history/storages/index.ts +3 -0
- package/src/conversation-history/storages/memory-storage.ts +44 -0
- package/src/conversation-history/types.ts +64 -0
- package/src/error-handling/__tests__/error-handling-plugin.test.ts +201 -0
- package/src/error-handling/context-adapter.ts +48 -0
- package/src/error-handling/error-handling-helpers.ts +53 -0
- package/src/error-handling/error-handling-plugin.ts +293 -0
- package/src/error-handling/index.ts +9 -0
- package/src/error-handling/types.ts +82 -0
- package/src/execution-analytics/__tests__/execution-analytics-plugin.test.ts +224 -0
- package/src/execution-analytics/analytics-aggregation.ts +88 -0
- package/src/execution-analytics/execution-analytics-helpers.ts +83 -0
- package/src/execution-analytics/execution-analytics-plugin.ts +315 -0
- package/src/execution-analytics/index.ts +9 -0
- package/src/execution-analytics/types.ts +97 -0
- package/src/index.ts +8 -0
- package/src/limits/__tests__/limits-plugin.test.ts +712 -0
- package/src/limits/index.ts +9 -0
- package/src/limits/limits-helpers.ts +185 -0
- package/src/limits/limits-plugin.ts +196 -0
- package/src/limits/types.ts +73 -0
- package/src/limits/validation.ts +81 -0
- package/src/logging/__tests__/formatters.test.ts +48 -0
- package/src/logging/__tests__/logging-plugin.test.ts +464 -0
- package/src/logging/__tests__/logging-storages.test.ts +95 -0
- package/src/logging/formatters.ts +28 -0
- package/src/logging/index.ts +15 -0
- package/src/logging/logging-helpers.ts +223 -0
- package/src/logging/logging-plugin.ts +288 -0
- package/src/logging/storages/console-storage.ts +44 -0
- package/src/logging/storages/file-storage.ts +44 -0
- package/src/logging/storages/index.ts +4 -0
- package/src/logging/storages/remote-storage.ts +78 -0
- package/src/logging/storages/silent-storage.ts +18 -0
- package/src/logging/types.ts +106 -0
- package/src/performance/__tests__/memory-storage.test.ts +86 -0
- package/src/performance/__tests__/performance-plugin.test.ts +208 -0
- package/src/performance/__tests__/system-metrics-collector.test.ts +33 -0
- package/src/performance/collectors/system-metrics-collector.ts +69 -0
- package/src/performance/index.ts +12 -0
- package/src/performance/performance-helpers.ts +86 -0
- package/src/performance/performance-plugin.ts +274 -0
- package/src/performance/storages/index.ts +1 -0
- package/src/performance/storages/memory-storage.ts +88 -0
- package/src/performance/types.ts +160 -0
- package/src/usage/__tests__/aggregate-usage-stats.test.ts +136 -0
- package/src/usage/__tests__/memory-storage.test.ts +83 -0
- package/src/usage/__tests__/silent-storage.test.ts +44 -0
- package/src/usage/__tests__/usage-plugin-helpers.test.ts +155 -0
- package/src/usage/__tests__/usage-plugin.test.ts +358 -0
- package/src/usage/aggregate-usage-stats.ts +142 -0
- package/src/usage/index.ts +14 -0
- package/src/usage/storages/file-storage.ts +115 -0
- package/src/usage/storages/index.ts +4 -0
- package/src/usage/storages/memory-storage.ts +61 -0
- package/src/usage/storages/remote-storage.ts +143 -0
- package/src/usage/storages/silent-storage.ts +38 -0
- package/src/usage/types.ts +132 -0
- package/src/usage/usage-plugin-helpers.ts +116 -0
- package/src/usage/usage-plugin.ts +296 -0
- package/src/webhook/__tests__/webhook-plugin.test.ts +560 -0
- package/src/webhook/http-client.ts +141 -0
- package/src/webhook/index.ts +9 -0
- package/src/webhook/transformer.ts +209 -0
- package/src/webhook/types.ts +201 -0
- package/src/webhook/webhook-helpers.ts +60 -0
- package/src/webhook/webhook-plugin.ts +298 -0
- 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
|
+
}
|