converse-mcp-server 1.0.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/.env.example +177 -0
- package/README.md +425 -0
- package/bin/converse.js +45 -0
- package/docs/API.md +897 -0
- package/docs/ARCHITECTURE.md +552 -0
- package/docs/EXAMPLES.md +736 -0
- package/package.json +101 -0
- package/src/config.js +521 -0
- package/src/continuationStore.js +340 -0
- package/src/index.js +216 -0
- package/src/providers/google.js +441 -0
- package/src/providers/index.js +87 -0
- package/src/providers/openai.js +348 -0
- package/src/providers/xai.js +305 -0
- package/src/router.js +497 -0
- package/src/systemPrompts.js +90 -0
- package/src/tools/chat.js +336 -0
- package/src/tools/consensus.js +478 -0
- package/src/tools/index.js +156 -0
- package/src/transport/httpTransport.js +548 -0
- package/src/utils/console.js +64 -0
- package/src/utils/contextProcessor.js +475 -0
- package/src/utils/errorHandler.js +555 -0
- package/src/utils/logger.js +450 -0
- package/src/utils/tokenLimiter.js +217 -0
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error Handler Utility
|
|
3
|
+
*
|
|
4
|
+
* Centralized error handling and response formatting for consistent error management
|
|
5
|
+
* across all modules. Provides structured error responses and proper error propagation.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createLogger } from './logger.js';
|
|
9
|
+
|
|
10
|
+
const logger = createLogger('errorHandler');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Standard error codes used throughout the application
|
|
14
|
+
*/
|
|
15
|
+
export const ERROR_CODES = {
|
|
16
|
+
// Configuration errors
|
|
17
|
+
CONFIGURATION_ERROR: 'CONFIGURATION_ERROR',
|
|
18
|
+
MISSING_CONFIG: 'MISSING_CONFIG',
|
|
19
|
+
INVALID_CONFIG: 'INVALID_CONFIG',
|
|
20
|
+
|
|
21
|
+
// Provider errors
|
|
22
|
+
PROVIDER_ERROR: 'PROVIDER_ERROR',
|
|
23
|
+
PROVIDER_NOT_FOUND: 'PROVIDER_NOT_FOUND',
|
|
24
|
+
PROVIDER_UNAVAILABLE: 'PROVIDER_UNAVAILABLE',
|
|
25
|
+
INVALID_API_KEY: 'INVALID_API_KEY',
|
|
26
|
+
API_QUOTA_EXCEEDED: 'API_QUOTA_EXCEEDED',
|
|
27
|
+
API_RATE_LIMIT: 'API_RATE_LIMIT',
|
|
28
|
+
|
|
29
|
+
// Tool errors
|
|
30
|
+
TOOL_ERROR: 'TOOL_ERROR',
|
|
31
|
+
TOOL_NOT_FOUND: 'TOOL_NOT_FOUND',
|
|
32
|
+
INVALID_TOOL_ARGS: 'INVALID_TOOL_ARGS',
|
|
33
|
+
TOOL_EXECUTION_FAILED: 'TOOL_EXECUTION_FAILED',
|
|
34
|
+
|
|
35
|
+
// Router errors
|
|
36
|
+
ROUTER_ERROR: 'ROUTER_ERROR',
|
|
37
|
+
INVALID_REQUEST: 'INVALID_REQUEST',
|
|
38
|
+
REQUEST_VALIDATION_FAILED: 'REQUEST_VALIDATION_FAILED',
|
|
39
|
+
|
|
40
|
+
// Context processing errors
|
|
41
|
+
CONTEXT_ERROR: 'CONTEXT_ERROR',
|
|
42
|
+
FILE_NOT_FOUND: 'FILE_NOT_FOUND',
|
|
43
|
+
FILE_ACCESS_DENIED: 'FILE_ACCESS_DENIED',
|
|
44
|
+
INVALID_FILE_TYPE: 'INVALID_FILE_TYPE',
|
|
45
|
+
FILE_TOO_LARGE: 'FILE_TOO_LARGE',
|
|
46
|
+
|
|
47
|
+
// Continuation store errors
|
|
48
|
+
CONTINUATION_ERROR: 'CONTINUATION_ERROR',
|
|
49
|
+
INVALID_CONTINUATION_ID: 'INVALID_CONTINUATION_ID',
|
|
50
|
+
CONTINUATION_NOT_FOUND: 'CONTINUATION_NOT_FOUND',
|
|
51
|
+
|
|
52
|
+
// Generic errors
|
|
53
|
+
UNKNOWN_ERROR: 'UNKNOWN_ERROR',
|
|
54
|
+
INTERNAL_ERROR: 'INTERNAL_ERROR',
|
|
55
|
+
VALIDATION_ERROR: 'VALIDATION_ERROR',
|
|
56
|
+
TIMEOUT_ERROR: 'TIMEOUT_ERROR',
|
|
57
|
+
NETWORK_ERROR: 'NETWORK_ERROR'
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Base error class for structured error handling
|
|
62
|
+
*/
|
|
63
|
+
export class ConverseMCPError extends Error {
|
|
64
|
+
constructor(message, code = ERROR_CODES.UNKNOWN_ERROR, details = {}, statusCode = 500) {
|
|
65
|
+
super(message);
|
|
66
|
+
this.name = 'ConverseMCPError';
|
|
67
|
+
this.code = code;
|
|
68
|
+
this.details = details;
|
|
69
|
+
this.statusCode = statusCode;
|
|
70
|
+
this.timestamp = new Date().toISOString();
|
|
71
|
+
|
|
72
|
+
// Capture stack trace
|
|
73
|
+
if (Error.captureStackTrace) {
|
|
74
|
+
Error.captureStackTrace(this, ConverseMCPError);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Convert error to JSON-serializable object
|
|
80
|
+
* @returns {object} Serializable error object
|
|
81
|
+
*/
|
|
82
|
+
toJSON() {
|
|
83
|
+
return {
|
|
84
|
+
name: this.name,
|
|
85
|
+
message: this.message,
|
|
86
|
+
code: this.code,
|
|
87
|
+
details: this.details,
|
|
88
|
+
statusCode: this.statusCode,
|
|
89
|
+
timestamp: this.timestamp,
|
|
90
|
+
stack: this.stack
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Create MCP-compatible error response
|
|
96
|
+
* @returns {object} MCP error response
|
|
97
|
+
*/
|
|
98
|
+
toMCPResponse() {
|
|
99
|
+
return {
|
|
100
|
+
content: [
|
|
101
|
+
{
|
|
102
|
+
type: 'text',
|
|
103
|
+
text: this.message
|
|
104
|
+
}
|
|
105
|
+
],
|
|
106
|
+
isError: true,
|
|
107
|
+
error: {
|
|
108
|
+
type: this.name,
|
|
109
|
+
code: this.code,
|
|
110
|
+
message: this.message,
|
|
111
|
+
details: this.details,
|
|
112
|
+
timestamp: this.timestamp
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Provider-specific error class
|
|
120
|
+
*/
|
|
121
|
+
export class ProviderError extends ConverseMCPError {
|
|
122
|
+
constructor(message, code = ERROR_CODES.PROVIDER_ERROR, details = {}, provider = 'unknown') {
|
|
123
|
+
super(message, code, { ...details, provider }, 503);
|
|
124
|
+
this.name = 'ProviderError';
|
|
125
|
+
this.provider = provider;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Tool-specific error class
|
|
131
|
+
*/
|
|
132
|
+
export class ToolError extends ConverseMCPError {
|
|
133
|
+
constructor(message, code = ERROR_CODES.TOOL_ERROR, details = {}, toolName = 'unknown') {
|
|
134
|
+
super(message, code, { ...details, toolName }, 400);
|
|
135
|
+
this.name = 'ToolError';
|
|
136
|
+
this.toolName = toolName;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Configuration error class
|
|
142
|
+
*/
|
|
143
|
+
export class ConfigurationError extends ConverseMCPError {
|
|
144
|
+
constructor(message, code = ERROR_CODES.CONFIGURATION_ERROR, details = {}) {
|
|
145
|
+
super(message, code, details, 500);
|
|
146
|
+
this.name = 'ConfigurationError';
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Validation error class
|
|
152
|
+
*/
|
|
153
|
+
export class ValidationError extends ConverseMCPError {
|
|
154
|
+
constructor(message, code = ERROR_CODES.VALIDATION_ERROR, details = {}) {
|
|
155
|
+
super(message, code, details, 400);
|
|
156
|
+
this.name = 'ValidationError';
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Context processing error class
|
|
162
|
+
*/
|
|
163
|
+
export class ContextError extends ConverseMCPError {
|
|
164
|
+
constructor(message, code = ERROR_CODES.CONTEXT_ERROR, details = {}) {
|
|
165
|
+
super(message, code, details, 400);
|
|
166
|
+
this.name = 'ContextError';
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Wrap and enhance existing errors
|
|
172
|
+
* @param {Error} originalError - Original error
|
|
173
|
+
* @param {string} message - New error message
|
|
174
|
+
* @param {string} code - Error code
|
|
175
|
+
* @param {object} details - Additional details
|
|
176
|
+
* @returns {ConverseMCPError} Enhanced error
|
|
177
|
+
*/
|
|
178
|
+
export function wrapError(originalError, message, code = ERROR_CODES.UNKNOWN_ERROR, details = {}) {
|
|
179
|
+
const enhancedDetails = {
|
|
180
|
+
...details,
|
|
181
|
+
originalError: {
|
|
182
|
+
name: originalError.name,
|
|
183
|
+
message: originalError.message,
|
|
184
|
+
code: originalError.code,
|
|
185
|
+
stack: originalError.stack
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const wrappedError = new ConverseMCPError(message, code, enhancedDetails);
|
|
190
|
+
wrappedError.cause = originalError;
|
|
191
|
+
|
|
192
|
+
return wrappedError;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Handle async function errors with proper logging
|
|
197
|
+
* @param {function} fn - Async function to wrap
|
|
198
|
+
* @param {string} operation - Operation name for logging
|
|
199
|
+
* @param {object} context - Additional context
|
|
200
|
+
* @returns {function} Wrapped function
|
|
201
|
+
*/
|
|
202
|
+
export function withErrorHandler(fn, operation = 'unknown', context = {}) {
|
|
203
|
+
return async (...args) => {
|
|
204
|
+
const operationLogger = logger.operation(operation);
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
operationLogger.debug('Starting operation', { data: context });
|
|
208
|
+
const result = await fn(...args);
|
|
209
|
+
operationLogger.debug('Operation completed successfully');
|
|
210
|
+
return result;
|
|
211
|
+
} catch (error) {
|
|
212
|
+
operationLogger.error('Operation failed', {
|
|
213
|
+
error,
|
|
214
|
+
data: { args: args.length, context }
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// Re-throw enhanced error if it's already structured
|
|
218
|
+
if (error instanceof ConverseMCPError) {
|
|
219
|
+
throw error;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Wrap unknown errors
|
|
223
|
+
throw wrapError(error, `${operation} failed: ${error.message}`, ERROR_CODES.INTERNAL_ERROR, context);
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Create MCP-compatible error response
|
|
230
|
+
* @param {Error} error - Error object
|
|
231
|
+
* @param {string} toolName - Tool name (optional)
|
|
232
|
+
* @param {object} context - Additional context
|
|
233
|
+
* @returns {object} MCP error response
|
|
234
|
+
*/
|
|
235
|
+
export function createMCPErrorResponse(error, toolName = null, context = {}) {
|
|
236
|
+
// If it's already a structured error, use its MCP response
|
|
237
|
+
if (error instanceof ConverseMCPError) {
|
|
238
|
+
const response = error.toMCPResponse();
|
|
239
|
+
if (toolName) {
|
|
240
|
+
response.error.toolName = toolName;
|
|
241
|
+
}
|
|
242
|
+
response.error.context = context;
|
|
243
|
+
return response;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Create structured response for unknown errors
|
|
247
|
+
const errorCode = error.code || ERROR_CODES.UNKNOWN_ERROR;
|
|
248
|
+
const message = toolName ? `Error in ${toolName}: ${error.message}` : error.message;
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
content: [
|
|
252
|
+
{
|
|
253
|
+
type: 'text',
|
|
254
|
+
text: message
|
|
255
|
+
}
|
|
256
|
+
],
|
|
257
|
+
isError: true,
|
|
258
|
+
error: {
|
|
259
|
+
type: error.name || 'Error',
|
|
260
|
+
code: errorCode,
|
|
261
|
+
message: error.message,
|
|
262
|
+
toolName,
|
|
263
|
+
context,
|
|
264
|
+
timestamp: new Date().toISOString(),
|
|
265
|
+
...(process.env.NODE_ENV === 'development' && error.stack && { stack: error.stack })
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Determine if error is recoverable
|
|
272
|
+
* @param {Error} error - Error to check
|
|
273
|
+
* @returns {boolean} True if error is recoverable
|
|
274
|
+
*/
|
|
275
|
+
export function isRecoverableError(error) {
|
|
276
|
+
if (error instanceof ConverseMCPError) {
|
|
277
|
+
return error.statusCode < 500 && error.code !== ERROR_CODES.INTERNAL_ERROR;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Check for known recoverable error patterns
|
|
281
|
+
const recoverablePatterns = [
|
|
282
|
+
/network/i,
|
|
283
|
+
/timeout/i,
|
|
284
|
+
/rate limit/i,
|
|
285
|
+
/quota/i,
|
|
286
|
+
/temporary/i
|
|
287
|
+
];
|
|
288
|
+
|
|
289
|
+
return recoverablePatterns.some(pattern => pattern.test(error.message));
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Log error with appropriate level based on severity
|
|
294
|
+
* @param {Error} error - Error to log
|
|
295
|
+
* @param {string} operation - Operation context
|
|
296
|
+
* @param {object} metadata - Additional metadata
|
|
297
|
+
*/
|
|
298
|
+
export function logError(error, operation = 'unknown', metadata = {}) {
|
|
299
|
+
const operationLogger = logger.operation(operation);
|
|
300
|
+
|
|
301
|
+
if (error instanceof ConverseMCPError) {
|
|
302
|
+
if (error.statusCode >= 500) {
|
|
303
|
+
operationLogger.error('Internal error occurred', { error, data: metadata });
|
|
304
|
+
} else if (error.statusCode >= 400) {
|
|
305
|
+
operationLogger.warn('Client error occurred', { error, data: metadata });
|
|
306
|
+
} else {
|
|
307
|
+
operationLogger.info('Handled error occurred', { error, data: metadata });
|
|
308
|
+
}
|
|
309
|
+
} else {
|
|
310
|
+
operationLogger.error('Unhandled error occurred', { error, data: metadata });
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Error aggregation for batch operations
|
|
316
|
+
*/
|
|
317
|
+
export class ErrorAggregator {
|
|
318
|
+
constructor(operation = 'batch-operation') {
|
|
319
|
+
this.operation = operation;
|
|
320
|
+
this.errors = [];
|
|
321
|
+
this.successes = [];
|
|
322
|
+
this.logger = logger.operation(operation);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Add success result
|
|
327
|
+
* @param {any} result - Success result
|
|
328
|
+
* @param {string} identifier - Result identifier
|
|
329
|
+
*/
|
|
330
|
+
addSuccess(result, identifier = null) {
|
|
331
|
+
this.successes.push({ result, identifier, timestamp: new Date().toISOString() });
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Add error result
|
|
336
|
+
* @param {Error} error - Error that occurred
|
|
337
|
+
* @param {string} identifier - Error identifier
|
|
338
|
+
*/
|
|
339
|
+
addError(error, identifier = null) {
|
|
340
|
+
this.errors.push({ error, identifier, timestamp: new Date().toISOString() });
|
|
341
|
+
this.logger.warn('Batch operation error', {
|
|
342
|
+
error,
|
|
343
|
+
data: { identifier, totalErrors: this.errors.length }
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Get summary of results
|
|
349
|
+
* @returns {object} Results summary
|
|
350
|
+
*/
|
|
351
|
+
getSummary() {
|
|
352
|
+
return {
|
|
353
|
+
operation: this.operation,
|
|
354
|
+
total: this.errors.length + this.successes.length,
|
|
355
|
+
successes: this.successes.length,
|
|
356
|
+
errors: this.errors.length,
|
|
357
|
+
hasErrors: this.errors.length > 0,
|
|
358
|
+
successRate: this.successes.length / (this.errors.length + this.successes.length)
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Throw aggregated error if there are any errors
|
|
364
|
+
* @param {string} message - Error message
|
|
365
|
+
*/
|
|
366
|
+
throwIfErrors(message = null) {
|
|
367
|
+
if (this.errors.length > 0) {
|
|
368
|
+
const defaultMessage = `${this.operation} completed with ${this.errors.length} errors`;
|
|
369
|
+
const errorMessage = message || defaultMessage;
|
|
370
|
+
|
|
371
|
+
const aggregatedError = new ConverseMCPError(
|
|
372
|
+
errorMessage,
|
|
373
|
+
ERROR_CODES.INTERNAL_ERROR,
|
|
374
|
+
{
|
|
375
|
+
summary: this.getSummary(),
|
|
376
|
+
errors: this.errors.map(e => ({
|
|
377
|
+
identifier: e.identifier,
|
|
378
|
+
error: e.error.message,
|
|
379
|
+
code: e.error.code
|
|
380
|
+
}))
|
|
381
|
+
}
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
throw aggregatedError;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Log final summary
|
|
390
|
+
*/
|
|
391
|
+
logSummary() {
|
|
392
|
+
const summary = this.getSummary();
|
|
393
|
+
|
|
394
|
+
if (summary.hasErrors) {
|
|
395
|
+
this.logger.warn('Batch operation completed with errors', { data: summary });
|
|
396
|
+
} else {
|
|
397
|
+
this.logger.info('Batch operation completed successfully', { data: summary });
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Retry utility with exponential backoff
|
|
404
|
+
* @param {function} fn - Function to retry
|
|
405
|
+
* @param {object} options - Retry options
|
|
406
|
+
* @returns {Promise} Function result
|
|
407
|
+
*/
|
|
408
|
+
export async function retryWithBackoff(fn, options = {}) {
|
|
409
|
+
const {
|
|
410
|
+
retries = 3,
|
|
411
|
+
delay = 1000,
|
|
412
|
+
backoffFactor = 2,
|
|
413
|
+
maxDelay = 10000,
|
|
414
|
+
operation = 'retry-operation'
|
|
415
|
+
} = options;
|
|
416
|
+
|
|
417
|
+
const operationLogger = logger.operation(operation);
|
|
418
|
+
let lastError;
|
|
419
|
+
|
|
420
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
421
|
+
try {
|
|
422
|
+
if (attempt > 0) {
|
|
423
|
+
operationLogger.debug(`Retry attempt ${attempt}/${retries}`);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return await fn();
|
|
427
|
+
|
|
428
|
+
} catch (error) {
|
|
429
|
+
lastError = error;
|
|
430
|
+
|
|
431
|
+
if (attempt === retries) {
|
|
432
|
+
operationLogger.error('All retry attempts failed', {
|
|
433
|
+
error,
|
|
434
|
+
data: { attempts: attempt + 1, maxRetries: retries }
|
|
435
|
+
});
|
|
436
|
+
break;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (!isRecoverableError(error)) {
|
|
440
|
+
operationLogger.warn('Non-recoverable error, stopping retries', { error });
|
|
441
|
+
break;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const currentDelay = Math.min(delay * Math.pow(backoffFactor, attempt), maxDelay);
|
|
445
|
+
operationLogger.debug(`Retrying in ${currentDelay}ms`, {
|
|
446
|
+
error: error.message,
|
|
447
|
+
data: { attempt: attempt + 1, delay: currentDelay }
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
await new Promise(resolve => setTimeout(resolve, currentDelay));
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
throw lastError;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Circuit breaker pattern implementation
|
|
459
|
+
*/
|
|
460
|
+
export class CircuitBreaker {
|
|
461
|
+
constructor(operation, options = {}) {
|
|
462
|
+
this.operation = operation;
|
|
463
|
+
this.failureThreshold = options.failureThreshold || 5;
|
|
464
|
+
this.resetTimeout = options.resetTimeout || 60000; // 1 minute
|
|
465
|
+
this.monitorTimeout = options.monitorTimeout || 10000; // 10 seconds
|
|
466
|
+
|
|
467
|
+
this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
|
|
468
|
+
this.failures = 0;
|
|
469
|
+
this.lastFailureTime = null;
|
|
470
|
+
this.nextAttempt = null;
|
|
471
|
+
|
|
472
|
+
this.logger = logger.operation(`circuit-breaker:${operation}`);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Execute function with circuit breaker protection
|
|
477
|
+
* @param {function} fn - Function to execute
|
|
478
|
+
* @returns {Promise} Function result
|
|
479
|
+
*/
|
|
480
|
+
async execute(fn) {
|
|
481
|
+
if (this.state === 'OPEN') {
|
|
482
|
+
if (Date.now() < this.nextAttempt) {
|
|
483
|
+
throw new ConverseMCPError(
|
|
484
|
+
`Circuit breaker is OPEN for ${this.operation}`,
|
|
485
|
+
ERROR_CODES.PROVIDER_UNAVAILABLE,
|
|
486
|
+
{ state: this.state, nextAttempt: this.nextAttempt }
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
this.state = 'HALF_OPEN';
|
|
491
|
+
this.logger.info('Circuit breaker transitioning to HALF_OPEN');
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
try {
|
|
495
|
+
const result = await fn();
|
|
496
|
+
|
|
497
|
+
if (this.state === 'HALF_OPEN') {
|
|
498
|
+
this.reset();
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
return result;
|
|
502
|
+
|
|
503
|
+
} catch (error) {
|
|
504
|
+
this.recordFailure();
|
|
505
|
+
throw error;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Record a failure
|
|
511
|
+
*/
|
|
512
|
+
recordFailure() {
|
|
513
|
+
this.failures++;
|
|
514
|
+
this.lastFailureTime = Date.now();
|
|
515
|
+
|
|
516
|
+
if (this.failures >= this.failureThreshold) {
|
|
517
|
+
this.state = 'OPEN';
|
|
518
|
+
this.nextAttempt = Date.now() + this.resetTimeout;
|
|
519
|
+
|
|
520
|
+
this.logger.warn('Circuit breaker opened due to failures', {
|
|
521
|
+
data: {
|
|
522
|
+
failures: this.failures,
|
|
523
|
+
threshold: this.failureThreshold,
|
|
524
|
+
resetTime: new Date(this.nextAttempt).toISOString()
|
|
525
|
+
}
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Reset circuit breaker
|
|
532
|
+
*/
|
|
533
|
+
reset() {
|
|
534
|
+
this.state = 'CLOSED';
|
|
535
|
+
this.failures = 0;
|
|
536
|
+
this.lastFailureTime = null;
|
|
537
|
+
this.nextAttempt = null;
|
|
538
|
+
|
|
539
|
+
this.logger.info('Circuit breaker reset to CLOSED');
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Get current status
|
|
544
|
+
* @returns {object} Circuit breaker status
|
|
545
|
+
*/
|
|
546
|
+
getStatus() {
|
|
547
|
+
return {
|
|
548
|
+
operation: this.operation,
|
|
549
|
+
state: this.state,
|
|
550
|
+
failures: this.failures,
|
|
551
|
+
lastFailureTime: this.lastFailureTime,
|
|
552
|
+
nextAttempt: this.nextAttempt
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
}
|