astrabot 0.1.0
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/README.md +411 -0
- package/ai/ai.config.ts +27 -0
- package/ai/auto-retry.ts +117 -0
- package/ai/config-loader.ts +132 -0
- package/ai/index.ts +4 -0
- package/ai/retry-prompt.ts +30 -0
- package/bin/astra +2 -0
- package/core/retry/error-classifier.ts +208 -0
- package/core/retry/index.ts +29 -0
- package/core/retry/retry-config.ts +142 -0
- package/core/retry/retry-engine.ts +215 -0
- package/game/index.html +573 -0
- package/game/neon-breaker.html +1037 -0
- package/index.ts +140 -0
- package/modes/agent/action-tracker.ts +47 -0
- package/modes/agent/agent-tools.ts +338 -0
- package/modes/agent/approval.ts +184 -0
- package/modes/agent/diff-view.ts +34 -0
- package/modes/agent/orchestrator.ts +234 -0
- package/modes/agent/tool-executor.ts +993 -0
- package/modes/agent/types.ts +68 -0
- package/modes/ask/orchestrator.ts +230 -0
- package/modes/auto.ts +88 -0
- package/modes/cli.ts +43 -0
- package/modes/multi/agent-pool-manager.ts +337 -0
- package/modes/multi/examples.ts +441 -0
- package/modes/multi/message-broker.ts +179 -0
- package/modes/multi/multi-agent-orchestrator.ts +891 -0
- package/modes/multi/orchestrator.ts +414 -0
- package/modes/multi/types.ts +245 -0
- package/modes/multi/workflow-builder.ts +569 -0
- package/modes/plan/orchestrator.ts +198 -0
- package/modes/plan/planner.ts +121 -0
- package/modes/plan/selection.ts +43 -0
- package/modes/plan/types.ts +13 -0
- package/modes/plan/web-tools.ts +132 -0
- package/modes/setup.ts +210 -0
- package/package.json +62 -0
- package/session/index.ts +45 -0
- package/session/session-context.ts +188 -0
- package/session/session-manager.ts +374 -0
- package/session/session-tools.ts +109 -0
- package/session/store.ts +278 -0
- package/tsconfig.json +30 -0
- package/tui/spinner.ts +182 -0
- package/tui/terminal-md.ts +17 -0
- package/tui/wakeup.ts +231 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { confirm, isCancel } from "@clack/prompts";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
|
|
4
|
+
function extractMessage(error: unknown): string {
|
|
5
|
+
if (error instanceof Error) {
|
|
6
|
+
const firstLine = error.message.split("\n")[0]?.trim();
|
|
7
|
+
if (firstLine) return firstLine;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
if (typeof error === "string" && error.trim()) {
|
|
11
|
+
return error.trim();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return "The AI provider returned an error.";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function promptToRetryAiCall(
|
|
18
|
+
context: string,
|
|
19
|
+
error: unknown,
|
|
20
|
+
): Promise<boolean> {
|
|
21
|
+
console.log(chalk.red(`\n${context}`));
|
|
22
|
+
console.log(chalk.dim(extractMessage(error)));
|
|
23
|
+
|
|
24
|
+
const retry = await confirm({
|
|
25
|
+
message: "Try again?",
|
|
26
|
+
initialValue: true,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
return !isCancel(retry) && !!retry;
|
|
30
|
+
}
|
package/bin/astra
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error Classification Module
|
|
3
|
+
*
|
|
4
|
+
* Analyzes errors to determine if they are retryable and
|
|
5
|
+
* suggests appropriate retry delays.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { ErrorCategory, type ClassifiedError } from './retry-config';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* HTTP status codes that indicate rate limiting
|
|
12
|
+
*/
|
|
13
|
+
const RATE_LIMIT_STATUS_CODES = [429, 503];
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* HTTP status codes that indicate server errors (potentially transient)
|
|
17
|
+
*/
|
|
18
|
+
const SERVER_ERROR_STATUS_CODES = [500, 502, 504];
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* HTTP status codes that indicate authentication failures
|
|
22
|
+
*/
|
|
23
|
+
const AUTH_STATUS_CODES = [401, 403];
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Network error codes that indicate connectivity issues
|
|
27
|
+
*/
|
|
28
|
+
const NETWORK_ERROR_CODES = [
|
|
29
|
+
'ECONNRESET',
|
|
30
|
+
'ECONNREFUSED',
|
|
31
|
+
'ETIMEDOUT',
|
|
32
|
+
'ENOTFOUND',
|
|
33
|
+
'EAI_AGAIN',
|
|
34
|
+
'ENETUNREACH',
|
|
35
|
+
'EHOSTUNREACH',
|
|
36
|
+
'EPIPE',
|
|
37
|
+
'ECONNABORTED',
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Error message patterns that indicate specific error categories
|
|
42
|
+
*/
|
|
43
|
+
const ERROR_PATTERNS: { pattern: RegExp; category: ErrorCategory }[] = [
|
|
44
|
+
// Rate limiting
|
|
45
|
+
{ pattern: /rate\s*limit/i, category: ErrorCategory.RATE_LIMIT },
|
|
46
|
+
{ pattern: /too\s*many\s*requests/i, category: ErrorCategory.RATE_LIMIT },
|
|
47
|
+
{ pattern: /throttl/i, category: ErrorCategory.RATE_LIMIT },
|
|
48
|
+
{ pattern: /quota\s*exceeded/i, category: ErrorCategory.RATE_LIMIT },
|
|
49
|
+
|
|
50
|
+
// Network errors
|
|
51
|
+
{ pattern: /network\s*error/i, category: ErrorCategory.NETWORK },
|
|
52
|
+
{ pattern: /connection\s*refused/i, category: ErrorCategory.NETWORK },
|
|
53
|
+
{ pattern: /connection\s*reset/i, category: ErrorCategory.NETWORK },
|
|
54
|
+
{ pattern: /dns\s*error/i, category: ErrorCategory.NETWORK },
|
|
55
|
+
{ pattern: /socket\s*hang\s*up/i, category: ErrorCategory.NETWORK },
|
|
56
|
+
|
|
57
|
+
// Timeout errors
|
|
58
|
+
{ pattern: /timeout/i, category: ErrorCategory.TIMEOUT },
|
|
59
|
+
{ pattern: /timed?\s*out/i, category: ErrorCategory.TIMEOUT },
|
|
60
|
+
{ pattern: /deadline\s*exceeded/i, category: ErrorCategory.TIMEOUT },
|
|
61
|
+
|
|
62
|
+
// Authentication errors
|
|
63
|
+
{ pattern: /unauthorized/i, category: ErrorCategory.AUTH },
|
|
64
|
+
{ pattern: /forbidden/i, category: ErrorCategory.AUTH },
|
|
65
|
+
{ pattern: /invalid\s*api\s*key/i, category: ErrorCategory.AUTH },
|
|
66
|
+
{ pattern: /authentication\s*failed/i, category: ErrorCategory.AUTH },
|
|
67
|
+
{ pattern: /api\s*key\s*invalid/i, category: ErrorCategory.AUTH },
|
|
68
|
+
|
|
69
|
+
// Permanent errors
|
|
70
|
+
{ pattern: /not\s*found/i, category: ErrorCategory.PERMANENT },
|
|
71
|
+
{ pattern: /invalid\s*request/i, category: ErrorCategory.PERMANENT },
|
|
72
|
+
{ pattern: /bad\s*request/i, category: ErrorCategory.PERMANENT },
|
|
73
|
+
{ pattern: /malformed/i, category: ErrorCategory.PERMANENT },
|
|
74
|
+
{ pattern: /unsupported/i, category: ErrorCategory.PERMANENT },
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Extract status code from various error formats
|
|
79
|
+
*/
|
|
80
|
+
function extractStatusCode(error: Error): number | undefined {
|
|
81
|
+
// Direct status code property
|
|
82
|
+
if ('status' in error && typeof error.status === 'number') {
|
|
83
|
+
return error.status;
|
|
84
|
+
}
|
|
85
|
+
if ('statusCode' in error && typeof error.statusCode === 'number') {
|
|
86
|
+
return error.statusCode;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Extract from error message
|
|
90
|
+
const statusMatch = error.message.match(/\b(\d{3})\b/);
|
|
91
|
+
if (statusMatch) {
|
|
92
|
+
return parseInt(statusMatch[1]!, 10);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Extract error code from various error formats
|
|
100
|
+
*/
|
|
101
|
+
function extractErrorCode(error: Error): string | undefined {
|
|
102
|
+
if ('code' in error && typeof error.code === 'string') {
|
|
103
|
+
return error.code;
|
|
104
|
+
}
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Determine if an error category is retryable
|
|
110
|
+
*/
|
|
111
|
+
function isRetryableCategory(category: ErrorCategory): boolean {
|
|
112
|
+
switch (category) {
|
|
113
|
+
case ErrorCategory.TRANSIENT:
|
|
114
|
+
case ErrorCategory.RATE_LIMIT:
|
|
115
|
+
case ErrorCategory.NETWORK:
|
|
116
|
+
case ErrorCategory.TIMEOUT:
|
|
117
|
+
case ErrorCategory.UNKNOWN:
|
|
118
|
+
return true;
|
|
119
|
+
case ErrorCategory.PERMANENT:
|
|
120
|
+
case ErrorCategory.AUTH:
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Get suggested delay for an error category
|
|
127
|
+
*/
|
|
128
|
+
function getSuggestedDelay(category: ErrorCategory): number {
|
|
129
|
+
switch (category) {
|
|
130
|
+
case ErrorCategory.RATE_LIMIT:
|
|
131
|
+
return 5000; // 5 seconds for rate limits
|
|
132
|
+
case ErrorCategory.NETWORK:
|
|
133
|
+
return 2000; // 2 seconds for network issues
|
|
134
|
+
case ErrorCategory.TIMEOUT:
|
|
135
|
+
return 3000; // 3 seconds for timeouts
|
|
136
|
+
case ErrorCategory.TRANSIENT:
|
|
137
|
+
case ErrorCategory.UNKNOWN:
|
|
138
|
+
return 1000; // 1 second for transient/unknown
|
|
139
|
+
case ErrorCategory.PERMANENT:
|
|
140
|
+
case ErrorCategory.AUTH:
|
|
141
|
+
return 0; // No delay for permanent errors
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Classify an error to determine retry behavior
|
|
147
|
+
*/
|
|
148
|
+
export function classifyError(error: Error): ClassifiedError {
|
|
149
|
+
const message = error.message || 'Unknown error';
|
|
150
|
+
const statusCode = extractStatusCode(error);
|
|
151
|
+
const errorCode = extractErrorCode(error);
|
|
152
|
+
|
|
153
|
+
let category: ErrorCategory = ErrorCategory.UNKNOWN;
|
|
154
|
+
|
|
155
|
+
// Check HTTP status codes first
|
|
156
|
+
if (statusCode) {
|
|
157
|
+
if (RATE_LIMIT_STATUS_CODES.includes(statusCode)) {
|
|
158
|
+
category = ErrorCategory.RATE_LIMIT;
|
|
159
|
+
} else if (AUTH_STATUS_CODES.includes(statusCode)) {
|
|
160
|
+
category = ErrorCategory.AUTH;
|
|
161
|
+
} else if (SERVER_ERROR_STATUS_CODES.includes(statusCode)) {
|
|
162
|
+
category = ErrorCategory.TRANSIENT;
|
|
163
|
+
} else if (statusCode >= 400 && statusCode < 500) {
|
|
164
|
+
category = ErrorCategory.PERMANENT;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Check error codes
|
|
169
|
+
if (errorCode && NETWORK_ERROR_CODES.includes(errorCode)) {
|
|
170
|
+
category = ErrorCategory.NETWORK;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Check error message patterns (if not already classified)
|
|
174
|
+
if (category === ErrorCategory.UNKNOWN) {
|
|
175
|
+
for (const { pattern, category: cat } of ERROR_PATTERNS) {
|
|
176
|
+
if (pattern.test(message)) {
|
|
177
|
+
category = cat;
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const isRetryable = isRetryableCategory(category);
|
|
184
|
+
const suggestedDelayMs = getSuggestedDelay(category);
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
originalError: error,
|
|
188
|
+
category,
|
|
189
|
+
message,
|
|
190
|
+
statusCode,
|
|
191
|
+
isRetryable,
|
|
192
|
+
suggestedDelayMs,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Check if an error is retryable
|
|
198
|
+
*/
|
|
199
|
+
export function isRetryable(error: Error): boolean {
|
|
200
|
+
return classifyError(error).isRetryable;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Get retry delay for an error
|
|
205
|
+
*/
|
|
206
|
+
export function getRetryDelay(error: Error): number {
|
|
207
|
+
return classifyError(error).suggestedDelayMs;
|
|
208
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retry Module Public API
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export {
|
|
6
|
+
ErrorCategory,
|
|
7
|
+
type ClassifiedError,
|
|
8
|
+
type RetryConfig,
|
|
9
|
+
type RetryStats,
|
|
10
|
+
type RetryResult,
|
|
11
|
+
DEFAULT_RETRY_CONFIG,
|
|
12
|
+
AGGRESSIVE_RETRY_CONFIG,
|
|
13
|
+
CONSERVATIVE_RETRY_CONFIG,
|
|
14
|
+
NO_RETRY_CONFIG,
|
|
15
|
+
mergeRetryConfig,
|
|
16
|
+
} from './retry-config';
|
|
17
|
+
|
|
18
|
+
export {
|
|
19
|
+
classifyError,
|
|
20
|
+
isRetryable,
|
|
21
|
+
getRetryDelay,
|
|
22
|
+
} from './error-classifier';
|
|
23
|
+
|
|
24
|
+
export {
|
|
25
|
+
withRetry,
|
|
26
|
+
withRetryOrNull,
|
|
27
|
+
createRetryWrapper,
|
|
28
|
+
RetryPresets,
|
|
29
|
+
} from './retry-engine';
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retry Configuration Module
|
|
3
|
+
*
|
|
4
|
+
* Provides configurable retry policies with exponential backoff,
|
|
5
|
+
* jitter, and error classification for resilient execution.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Error classification for determining retry behavior
|
|
10
|
+
*/
|
|
11
|
+
export enum ErrorCategory {
|
|
12
|
+
/** Transient errors that are likely to succeed on retry */
|
|
13
|
+
TRANSIENT = 'transient',
|
|
14
|
+
/** Permanent errors that will never succeed */
|
|
15
|
+
PERMANENT = 'permanent',
|
|
16
|
+
/** Rate limiting errors - need longer backoff */
|
|
17
|
+
RATE_LIMIT = 'rate_limit',
|
|
18
|
+
/** Network connectivity issues */
|
|
19
|
+
NETWORK = 'network',
|
|
20
|
+
/** Authentication/authorization failures */
|
|
21
|
+
AUTH = 'auth',
|
|
22
|
+
/** Timeout errors */
|
|
23
|
+
TIMEOUT = 'timeout',
|
|
24
|
+
/** Unknown errors - treat as transient by default */
|
|
25
|
+
UNKNOWN = 'unknown',
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Classified error with metadata for retry decisions
|
|
30
|
+
*/
|
|
31
|
+
export interface ClassifiedError {
|
|
32
|
+
originalError: Error;
|
|
33
|
+
category: ErrorCategory;
|
|
34
|
+
message: string;
|
|
35
|
+
statusCode?: number;
|
|
36
|
+
isRetryable: boolean;
|
|
37
|
+
suggestedDelayMs: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Retry configuration options
|
|
42
|
+
*/
|
|
43
|
+
export interface RetryConfig {
|
|
44
|
+
/** Maximum number of retry attempts (0 = no retries) */
|
|
45
|
+
maxRetries: number;
|
|
46
|
+
/** Base delay in ms before first retry */
|
|
47
|
+
baseDelayMs: number;
|
|
48
|
+
/** Maximum delay in ms between retries */
|
|
49
|
+
maxDelayMs: number;
|
|
50
|
+
/** Exponential backoff multiplier */
|
|
51
|
+
backoffMultiplier: number;
|
|
52
|
+
/** Add random jitter to prevent thundering herd */
|
|
53
|
+
jitter: boolean;
|
|
54
|
+
/** Maximum jitter in ms */
|
|
55
|
+
maxJitterMs: number;
|
|
56
|
+
/** Timeout for individual attempts in ms */
|
|
57
|
+
attemptTimeoutMs?: number;
|
|
58
|
+
/** Custom error classifier */
|
|
59
|
+
errorClassifier?: (error: Error) => ErrorCategory;
|
|
60
|
+
/** Callback invoked before each retry */
|
|
61
|
+
onRetry?: (attempt: number, error: ClassifiedError, delayMs: number) => void | Promise<void>;
|
|
62
|
+
/** Callback invoked when all retries are exhausted */
|
|
63
|
+
onExhausted?: (error: ClassifiedError, totalAttempts: number) => void | Promise<void>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Retry execution statistics
|
|
68
|
+
*/
|
|
69
|
+
export interface RetryStats {
|
|
70
|
+
totalAttempts: number;
|
|
71
|
+
totalRetries: number;
|
|
72
|
+
totalDelayMs: number;
|
|
73
|
+
errors: ClassifiedError[];
|
|
74
|
+
succeeded: boolean;
|
|
75
|
+
finalAttemptNumber: number;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Result of a retryable operation
|
|
80
|
+
*/
|
|
81
|
+
export interface RetryResult<T> {
|
|
82
|
+
result: T;
|
|
83
|
+
stats: RetryStats;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Default retry configuration
|
|
88
|
+
*/
|
|
89
|
+
export const DEFAULT_RETRY_CONFIG: RetryConfig = {
|
|
90
|
+
maxRetries: 3,
|
|
91
|
+
baseDelayMs: 1000,
|
|
92
|
+
maxDelayMs: 30000,
|
|
93
|
+
backoffMultiplier: 2,
|
|
94
|
+
jitter: true,
|
|
95
|
+
maxJitterMs: 1000,
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Aggressive retry configuration for critical operations
|
|
100
|
+
*/
|
|
101
|
+
export const AGGRESSIVE_RETRY_CONFIG: RetryConfig = {
|
|
102
|
+
maxRetries: 5,
|
|
103
|
+
baseDelayMs: 500,
|
|
104
|
+
maxDelayMs: 60000,
|
|
105
|
+
backoffMultiplier: 2,
|
|
106
|
+
jitter: true,
|
|
107
|
+
maxJitterMs: 2000,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Conservative retry configuration for sensitive operations
|
|
112
|
+
*/
|
|
113
|
+
export const CONSERVATIVE_RETRY_CONFIG: RetryConfig = {
|
|
114
|
+
maxRetries: 2,
|
|
115
|
+
baseDelayMs: 2000,
|
|
116
|
+
maxDelayMs: 10000,
|
|
117
|
+
backoffMultiplier: 3,
|
|
118
|
+
jitter: false,
|
|
119
|
+
maxJitterMs: 500,
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* No retry configuration
|
|
124
|
+
*/
|
|
125
|
+
export const NO_RETRY_CONFIG: RetryConfig = {
|
|
126
|
+
maxRetries: 0,
|
|
127
|
+
baseDelayMs: 0,
|
|
128
|
+
maxDelayMs: 0,
|
|
129
|
+
backoffMultiplier: 1,
|
|
130
|
+
jitter: false,
|
|
131
|
+
maxJitterMs: 0,
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Merge user config with defaults
|
|
136
|
+
*/
|
|
137
|
+
export function mergeRetryConfig(userConfig: Partial<RetryConfig>): RetryConfig {
|
|
138
|
+
return {
|
|
139
|
+
...DEFAULT_RETRY_CONFIG,
|
|
140
|
+
...userConfig,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retry Engine Module
|
|
3
|
+
*
|
|
4
|
+
* Core retry execution logic with exponential backoff, jitter,
|
|
5
|
+
* and comprehensive error handling.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
type RetryConfig,
|
|
10
|
+
type RetryStats,
|
|
11
|
+
type RetryResult,
|
|
12
|
+
type ClassifiedError,
|
|
13
|
+
DEFAULT_RETRY_CONFIG,
|
|
14
|
+
mergeRetryConfig,
|
|
15
|
+
} from './retry-config';
|
|
16
|
+
import { classifyError } from './error-classifier';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Sleep for a specified duration
|
|
20
|
+
*/
|
|
21
|
+
function sleep(ms: number): Promise<void> {
|
|
22
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Calculate delay for a retry attempt using exponential backoff
|
|
27
|
+
*/
|
|
28
|
+
function calculateDelay(
|
|
29
|
+
attempt: number,
|
|
30
|
+
baseDelayMs: number,
|
|
31
|
+
maxDelayMs: number,
|
|
32
|
+
backoffMultiplier: number,
|
|
33
|
+
jitter: boolean,
|
|
34
|
+
maxJitterMs: number,
|
|
35
|
+
classifiedError: ClassifiedError,
|
|
36
|
+
): number {
|
|
37
|
+
// Use suggested delay from error classification as base
|
|
38
|
+
const errorDelay = classifiedError.suggestedDelayMs;
|
|
39
|
+
const effectiveBase = Math.max(baseDelayMs, errorDelay);
|
|
40
|
+
|
|
41
|
+
// Calculate exponential backoff
|
|
42
|
+
const backoffDelay = effectiveBase * Math.pow(backoffMultiplier, attempt - 1);
|
|
43
|
+
|
|
44
|
+
// Cap at max delay
|
|
45
|
+
let delay = Math.min(backoffDelay, maxDelayMs);
|
|
46
|
+
|
|
47
|
+
// Add jitter if enabled
|
|
48
|
+
if (jitter && maxJitterMs > 0) {
|
|
49
|
+
const jitterAmount = Math.random() * maxJitterMs;
|
|
50
|
+
delay += jitterAmount;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return Math.round(delay);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Execute an operation with automatic retry on failure
|
|
58
|
+
*/
|
|
59
|
+
export async function withRetry<T>(
|
|
60
|
+
operation: () => Promise<T>,
|
|
61
|
+
config: Partial<RetryConfig> = {},
|
|
62
|
+
): Promise<RetryResult<T>> {
|
|
63
|
+
const fullConfig = mergeRetryConfig(config);
|
|
64
|
+
const stats: RetryStats = {
|
|
65
|
+
totalAttempts: 0,
|
|
66
|
+
totalRetries: 0,
|
|
67
|
+
totalDelayMs: 0,
|
|
68
|
+
errors: [],
|
|
69
|
+
succeeded: false,
|
|
70
|
+
finalAttemptNumber: 0,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
let lastError: ClassifiedError | null = null;
|
|
74
|
+
|
|
75
|
+
for (let attempt = 1; attempt <= fullConfig.maxRetries + 1; attempt++) {
|
|
76
|
+
stats.totalAttempts++;
|
|
77
|
+
stats.finalAttemptNumber = attempt;
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
// Execute the operation with optional timeout
|
|
81
|
+
let result: T;
|
|
82
|
+
|
|
83
|
+
if (fullConfig.attemptTimeoutMs) {
|
|
84
|
+
result = await Promise.race([
|
|
85
|
+
operation(),
|
|
86
|
+
new Promise<never>((_, reject) => {
|
|
87
|
+
setTimeout(() => {
|
|
88
|
+
reject(new Error(`Operation timed out after ${fullConfig.attemptTimeoutMs}ms`));
|
|
89
|
+
}, fullConfig.attemptTimeoutMs);
|
|
90
|
+
}),
|
|
91
|
+
]);
|
|
92
|
+
} else {
|
|
93
|
+
result = await operation();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Success!
|
|
97
|
+
stats.succeeded = true;
|
|
98
|
+
return { result, stats };
|
|
99
|
+
|
|
100
|
+
} catch (error) {
|
|
101
|
+
const classifiedError = classifyError(error instanceof Error ? error : new Error(String(error)));
|
|
102
|
+
lastError = classifiedError;
|
|
103
|
+
stats.errors.push(classifiedError);
|
|
104
|
+
|
|
105
|
+
// Check if we should retry
|
|
106
|
+
const isLastAttempt = attempt > fullConfig.maxRetries;
|
|
107
|
+
|
|
108
|
+
if (!classifiedError.isRetryable || isLastAttempt) {
|
|
109
|
+
// Don't retry permanent errors or if we've exhausted retries
|
|
110
|
+
if (isLastAttempt && fullConfig.onExhausted) {
|
|
111
|
+
await fullConfig.onExhausted(classifiedError, stats.totalAttempts);
|
|
112
|
+
}
|
|
113
|
+
throw classifiedError.originalError;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Calculate delay before next retry
|
|
117
|
+
const delayMs = calculateDelay(
|
|
118
|
+
attempt,
|
|
119
|
+
fullConfig.baseDelayMs,
|
|
120
|
+
fullConfig.maxDelayMs,
|
|
121
|
+
fullConfig.backoffMultiplier,
|
|
122
|
+
fullConfig.jitter,
|
|
123
|
+
fullConfig.maxJitterMs,
|
|
124
|
+
classifiedError,
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
stats.totalRetries++;
|
|
128
|
+
stats.totalDelayMs += delayMs;
|
|
129
|
+
|
|
130
|
+
// Call onRetry callback if provided
|
|
131
|
+
if (fullConfig.onRetry) {
|
|
132
|
+
await fullConfig.onRetry(attempt, classifiedError, delayMs);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Wait before retrying
|
|
136
|
+
await sleep(delayMs);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// This should never be reached, but just in case
|
|
141
|
+
throw lastError?.originalError || new Error('Retry exhausted');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Execute an operation with retry, returning the result or null on failure
|
|
146
|
+
*/
|
|
147
|
+
export async function withRetryOrNull<T>(
|
|
148
|
+
operation: () => Promise<T>,
|
|
149
|
+
config: Partial<RetryConfig> = {},
|
|
150
|
+
): Promise<T | null> {
|
|
151
|
+
try {
|
|
152
|
+
const { result } = await withRetry(operation, config);
|
|
153
|
+
return result;
|
|
154
|
+
} catch {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Create a retry wrapper for a function
|
|
161
|
+
*/
|
|
162
|
+
export function createRetryWrapper<TArgs extends unknown[], TReturn>(
|
|
163
|
+
fn: (...args: TArgs) => Promise<TReturn>,
|
|
164
|
+
config: Partial<RetryConfig> = {},
|
|
165
|
+
): (...args: TArgs) => Promise<TReturn> {
|
|
166
|
+
return async (...args: TArgs): Promise<TReturn> => {
|
|
167
|
+
const { result } = await withRetry(() => fn(...args), config);
|
|
168
|
+
return result;
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Retry configuration presets for common scenarios
|
|
174
|
+
*/
|
|
175
|
+
export const RetryPresets = {
|
|
176
|
+
/** For AI API calls - moderate retries with backoff */
|
|
177
|
+
aiCall: {
|
|
178
|
+
maxRetries: 3,
|
|
179
|
+
baseDelayMs: 1000,
|
|
180
|
+
maxDelayMs: 30000,
|
|
181
|
+
backoffMultiplier: 2,
|
|
182
|
+
jitter: true,
|
|
183
|
+
maxJitterMs: 1000,
|
|
184
|
+
},
|
|
185
|
+
|
|
186
|
+
/** For tool execution - fewer retries, shorter delays */
|
|
187
|
+
toolExecution: {
|
|
188
|
+
maxRetries: 2,
|
|
189
|
+
baseDelayMs: 500,
|
|
190
|
+
maxDelayMs: 5000,
|
|
191
|
+
backoffMultiplier: 2,
|
|
192
|
+
jitter: false,
|
|
193
|
+
maxJitterMs: 0,
|
|
194
|
+
},
|
|
195
|
+
|
|
196
|
+
/** For network operations - more retries, longer delays */
|
|
197
|
+
network: {
|
|
198
|
+
maxRetries: 5,
|
|
199
|
+
baseDelayMs: 2000,
|
|
200
|
+
maxDelayMs: 60000,
|
|
201
|
+
backoffMultiplier: 2,
|
|
202
|
+
jitter: true,
|
|
203
|
+
maxJitterMs: 2000,
|
|
204
|
+
},
|
|
205
|
+
|
|
206
|
+
/** For critical operations - aggressive retries */
|
|
207
|
+
critical: {
|
|
208
|
+
maxRetries: 5,
|
|
209
|
+
baseDelayMs: 1000,
|
|
210
|
+
maxDelayMs: 60000,
|
|
211
|
+
backoffMultiplier: 2,
|
|
212
|
+
jitter: true,
|
|
213
|
+
maxJitterMs: 1500,
|
|
214
|
+
},
|
|
215
|
+
} as const;
|