@compilr-dev/agents 0.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/README.md +1277 -0
- package/dist/agent.d.ts +1272 -0
- package/dist/agent.js +1912 -0
- package/dist/anchors/builtin.d.ts +24 -0
- package/dist/anchors/builtin.js +61 -0
- package/dist/anchors/index.d.ts +6 -0
- package/dist/anchors/index.js +5 -0
- package/dist/anchors/manager.d.ts +115 -0
- package/dist/anchors/manager.js +412 -0
- package/dist/anchors/types.d.ts +168 -0
- package/dist/anchors/types.js +10 -0
- package/dist/context/index.d.ts +12 -0
- package/dist/context/index.js +10 -0
- package/dist/context/manager.d.ts +224 -0
- package/dist/context/manager.js +770 -0
- package/dist/context/types.d.ts +377 -0
- package/dist/context/types.js +7 -0
- package/dist/costs/index.d.ts +8 -0
- package/dist/costs/index.js +7 -0
- package/dist/costs/tracker.d.ts +121 -0
- package/dist/costs/tracker.js +295 -0
- package/dist/costs/types.d.ts +157 -0
- package/dist/costs/types.js +8 -0
- package/dist/errors.d.ts +178 -0
- package/dist/errors.js +249 -0
- package/dist/guardrails/builtin.d.ts +27 -0
- package/dist/guardrails/builtin.js +223 -0
- package/dist/guardrails/index.d.ts +6 -0
- package/dist/guardrails/index.js +5 -0
- package/dist/guardrails/manager.d.ts +117 -0
- package/dist/guardrails/manager.js +288 -0
- package/dist/guardrails/types.d.ts +159 -0
- package/dist/guardrails/types.js +7 -0
- package/dist/hooks/index.d.ts +31 -0
- package/dist/hooks/index.js +29 -0
- package/dist/hooks/manager.d.ts +147 -0
- package/dist/hooks/manager.js +600 -0
- package/dist/hooks/types.d.ts +368 -0
- package/dist/hooks/types.js +12 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.js +73 -0
- package/dist/mcp/client.d.ts +93 -0
- package/dist/mcp/client.js +287 -0
- package/dist/mcp/errors.d.ts +60 -0
- package/dist/mcp/errors.js +78 -0
- package/dist/mcp/index.d.ts +43 -0
- package/dist/mcp/index.js +45 -0
- package/dist/mcp/manager.d.ts +120 -0
- package/dist/mcp/manager.js +276 -0
- package/dist/mcp/tools.d.ts +54 -0
- package/dist/mcp/tools.js +99 -0
- package/dist/mcp/types.d.ts +150 -0
- package/dist/mcp/types.js +40 -0
- package/dist/memory/index.d.ts +8 -0
- package/dist/memory/index.js +7 -0
- package/dist/memory/loader.d.ts +114 -0
- package/dist/memory/loader.js +463 -0
- package/dist/memory/types.d.ts +182 -0
- package/dist/memory/types.js +8 -0
- package/dist/messages/index.d.ts +82 -0
- package/dist/messages/index.js +155 -0
- package/dist/permissions/index.d.ts +5 -0
- package/dist/permissions/index.js +4 -0
- package/dist/permissions/manager.d.ts +125 -0
- package/dist/permissions/manager.js +379 -0
- package/dist/permissions/types.d.ts +162 -0
- package/dist/permissions/types.js +7 -0
- package/dist/providers/claude.d.ts +90 -0
- package/dist/providers/claude.js +348 -0
- package/dist/providers/index.d.ts +8 -0
- package/dist/providers/index.js +11 -0
- package/dist/providers/mock.d.ts +133 -0
- package/dist/providers/mock.js +204 -0
- package/dist/providers/types.d.ts +168 -0
- package/dist/providers/types.js +4 -0
- package/dist/rate-limit/index.d.ts +45 -0
- package/dist/rate-limit/index.js +47 -0
- package/dist/rate-limit/limiter.d.ts +104 -0
- package/dist/rate-limit/limiter.js +326 -0
- package/dist/rate-limit/provider-wrapper.d.ts +112 -0
- package/dist/rate-limit/provider-wrapper.js +201 -0
- package/dist/rate-limit/retry.d.ts +108 -0
- package/dist/rate-limit/retry.js +287 -0
- package/dist/rate-limit/types.d.ts +181 -0
- package/dist/rate-limit/types.js +22 -0
- package/dist/rehearsal/file-analyzer.d.ts +22 -0
- package/dist/rehearsal/file-analyzer.js +351 -0
- package/dist/rehearsal/git-analyzer.d.ts +22 -0
- package/dist/rehearsal/git-analyzer.js +472 -0
- package/dist/rehearsal/index.d.ts +35 -0
- package/dist/rehearsal/index.js +36 -0
- package/dist/rehearsal/manager.d.ts +100 -0
- package/dist/rehearsal/manager.js +290 -0
- package/dist/rehearsal/types.d.ts +235 -0
- package/dist/rehearsal/types.js +8 -0
- package/dist/skills/index.d.ts +160 -0
- package/dist/skills/index.js +282 -0
- package/dist/state/agent-state.d.ts +41 -0
- package/dist/state/agent-state.js +88 -0
- package/dist/state/checkpointer.d.ts +110 -0
- package/dist/state/checkpointer.js +362 -0
- package/dist/state/errors.d.ts +66 -0
- package/dist/state/errors.js +88 -0
- package/dist/state/index.d.ts +35 -0
- package/dist/state/index.js +37 -0
- package/dist/state/serializer.d.ts +55 -0
- package/dist/state/serializer.js +172 -0
- package/dist/state/types.d.ts +312 -0
- package/dist/state/types.js +14 -0
- package/dist/tools/builtin/bash-output.d.ts +61 -0
- package/dist/tools/builtin/bash-output.js +90 -0
- package/dist/tools/builtin/bash.d.ts +150 -0
- package/dist/tools/builtin/bash.js +354 -0
- package/dist/tools/builtin/edit.d.ts +50 -0
- package/dist/tools/builtin/edit.js +215 -0
- package/dist/tools/builtin/glob.d.ts +62 -0
- package/dist/tools/builtin/glob.js +244 -0
- package/dist/tools/builtin/grep.d.ts +74 -0
- package/dist/tools/builtin/grep.js +363 -0
- package/dist/tools/builtin/index.d.ts +44 -0
- package/dist/tools/builtin/index.js +69 -0
- package/dist/tools/builtin/kill-shell.d.ts +44 -0
- package/dist/tools/builtin/kill-shell.js +80 -0
- package/dist/tools/builtin/read-file.d.ts +57 -0
- package/dist/tools/builtin/read-file.js +184 -0
- package/dist/tools/builtin/shell-manager.d.ts +176 -0
- package/dist/tools/builtin/shell-manager.js +337 -0
- package/dist/tools/builtin/task.d.ts +202 -0
- package/dist/tools/builtin/task.js +350 -0
- package/dist/tools/builtin/todo.d.ts +207 -0
- package/dist/tools/builtin/todo.js +453 -0
- package/dist/tools/builtin/utils.d.ts +27 -0
- package/dist/tools/builtin/utils.js +70 -0
- package/dist/tools/builtin/web-fetch.d.ts +96 -0
- package/dist/tools/builtin/web-fetch.js +290 -0
- package/dist/tools/builtin/write-file.d.ts +54 -0
- package/dist/tools/builtin/write-file.js +147 -0
- package/dist/tools/define.d.ts +60 -0
- package/dist/tools/define.js +65 -0
- package/dist/tools/index.d.ts +10 -0
- package/dist/tools/index.js +37 -0
- package/dist/tools/registry.d.ts +79 -0
- package/dist/tools/registry.js +151 -0
- package/dist/tools/types.d.ts +59 -0
- package/dist/tools/types.js +4 -0
- package/dist/tracing/hooks.d.ts +58 -0
- package/dist/tracing/hooks.js +377 -0
- package/dist/tracing/index.d.ts +51 -0
- package/dist/tracing/index.js +55 -0
- package/dist/tracing/logging.d.ts +78 -0
- package/dist/tracing/logging.js +310 -0
- package/dist/tracing/manager.d.ts +160 -0
- package/dist/tracing/manager.js +468 -0
- package/dist/tracing/otel.d.ts +102 -0
- package/dist/tracing/otel.js +246 -0
- package/dist/tracing/types.d.ts +346 -0
- package/dist/tracing/types.js +38 -0
- package/dist/utils/index.d.ts +23 -0
- package/dist/utils/index.js +44 -0
- package/package.json +79 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider Wrapper with Rate Limiting and Retry
|
|
3
|
+
*
|
|
4
|
+
* Wraps an LLMProvider with automatic rate limiting and retry functionality.
|
|
5
|
+
*/
|
|
6
|
+
import type { LLMProvider, Message, ChatOptions, StreamChunk } from '../providers/types.js';
|
|
7
|
+
import type { RateLimiter, RateLimitRetryConfig } from './types.js';
|
|
8
|
+
/**
|
|
9
|
+
* Wrapper that adds rate limiting and retry to any LLMProvider
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* const provider = new ClaudeProvider({ apiKey: 'xxx' });
|
|
14
|
+
* const wrapped = new RateLimitedProvider(provider, {
|
|
15
|
+
* rateLimit: {
|
|
16
|
+
* requestsPerMinute: 60,
|
|
17
|
+
* tokensPerMinute: 100000,
|
|
18
|
+
* },
|
|
19
|
+
* retry: {
|
|
20
|
+
* maxRetries: 3,
|
|
21
|
+
* baseDelayMs: 1000,
|
|
22
|
+
* },
|
|
23
|
+
* });
|
|
24
|
+
*
|
|
25
|
+
* // Use wrapped provider like normal
|
|
26
|
+
* const agent = new Agent({ provider: wrapped });
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export declare class RateLimitedProvider implements LLMProvider {
|
|
30
|
+
readonly name: string;
|
|
31
|
+
private readonly provider;
|
|
32
|
+
private readonly rateLimiter;
|
|
33
|
+
private readonly retryConfig;
|
|
34
|
+
constructor(provider: LLMProvider, config?: RateLimitRetryConfig);
|
|
35
|
+
/**
|
|
36
|
+
* Get the rate limiter instance for statistics
|
|
37
|
+
*/
|
|
38
|
+
getRateLimiter(): RateLimiter;
|
|
39
|
+
/**
|
|
40
|
+
* Send messages with rate limiting and retry
|
|
41
|
+
*/
|
|
42
|
+
chat(messages: Message[], options?: ChatOptions): AsyncIterable<StreamChunk>;
|
|
43
|
+
/**
|
|
44
|
+
* Count tokens with rate limiting
|
|
45
|
+
*/
|
|
46
|
+
countTokens(messages: Message[]): Promise<number>;
|
|
47
|
+
/**
|
|
48
|
+
* Estimate tokens from messages (rough approximation)
|
|
49
|
+
*/
|
|
50
|
+
private estimateTokens;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Create a rate-limited provider wrapper
|
|
54
|
+
*
|
|
55
|
+
* @param provider - The base LLM provider
|
|
56
|
+
* @param config - Rate limit and retry configuration
|
|
57
|
+
* @returns Wrapped provider with rate limiting and retry
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```typescript
|
|
61
|
+
* const provider = createClaudeProvider();
|
|
62
|
+
* const rateLimited = wrapWithRateLimit(provider, {
|
|
63
|
+
* rateLimit: { requestsPerMinute: 60 },
|
|
64
|
+
* retry: { maxRetries: 3 },
|
|
65
|
+
* });
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
export declare function wrapWithRateLimit(provider: LLMProvider, config?: RateLimitRetryConfig): RateLimitedProvider;
|
|
69
|
+
/**
|
|
70
|
+
* Default rate limits for known providers
|
|
71
|
+
*/
|
|
72
|
+
export declare const ProviderRateLimits: {
|
|
73
|
+
/**
|
|
74
|
+
* Anthropic Claude API limits (Tier 1)
|
|
75
|
+
* @see https://docs.anthropic.com/en/api/rate-limits
|
|
76
|
+
*/
|
|
77
|
+
readonly claude: {
|
|
78
|
+
readonly tier1: {
|
|
79
|
+
readonly requestsPerMinute: 50;
|
|
80
|
+
readonly tokensPerMinute: 40000;
|
|
81
|
+
};
|
|
82
|
+
readonly tier2: {
|
|
83
|
+
readonly requestsPerMinute: 1000;
|
|
84
|
+
readonly tokensPerMinute: 80000;
|
|
85
|
+
};
|
|
86
|
+
readonly tier3: {
|
|
87
|
+
readonly requestsPerMinute: 2000;
|
|
88
|
+
readonly tokensPerMinute: 160000;
|
|
89
|
+
};
|
|
90
|
+
readonly tier4: {
|
|
91
|
+
readonly requestsPerMinute: 4000;
|
|
92
|
+
readonly tokensPerMinute: 400000;
|
|
93
|
+
};
|
|
94
|
+
};
|
|
95
|
+
/**
|
|
96
|
+
* OpenAI GPT-4 API limits (approximate)
|
|
97
|
+
*/
|
|
98
|
+
readonly openai: {
|
|
99
|
+
readonly gpt4: {
|
|
100
|
+
readonly requestsPerMinute: 500;
|
|
101
|
+
readonly tokensPerMinute: 10000;
|
|
102
|
+
};
|
|
103
|
+
readonly gpt4Turbo: {
|
|
104
|
+
readonly requestsPerMinute: 500;
|
|
105
|
+
readonly tokensPerMinute: 30000;
|
|
106
|
+
};
|
|
107
|
+
readonly gpt35Turbo: {
|
|
108
|
+
readonly requestsPerMinute: 3500;
|
|
109
|
+
readonly tokensPerMinute: 90000;
|
|
110
|
+
};
|
|
111
|
+
};
|
|
112
|
+
};
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider Wrapper with Rate Limiting and Retry
|
|
3
|
+
*
|
|
4
|
+
* Wraps an LLMProvider with automatic rate limiting and retry functionality.
|
|
5
|
+
*/
|
|
6
|
+
import { createRateLimiter } from './limiter.js';
|
|
7
|
+
import { withRetry } from './retry.js';
|
|
8
|
+
/**
|
|
9
|
+
* Wrapper that adds rate limiting and retry to any LLMProvider
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* const provider = new ClaudeProvider({ apiKey: 'xxx' });
|
|
14
|
+
* const wrapped = new RateLimitedProvider(provider, {
|
|
15
|
+
* rateLimit: {
|
|
16
|
+
* requestsPerMinute: 60,
|
|
17
|
+
* tokensPerMinute: 100000,
|
|
18
|
+
* },
|
|
19
|
+
* retry: {
|
|
20
|
+
* maxRetries: 3,
|
|
21
|
+
* baseDelayMs: 1000,
|
|
22
|
+
* },
|
|
23
|
+
* });
|
|
24
|
+
*
|
|
25
|
+
* // Use wrapped provider like normal
|
|
26
|
+
* const agent = new Agent({ provider: wrapped });
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export class RateLimitedProvider {
|
|
30
|
+
name;
|
|
31
|
+
provider;
|
|
32
|
+
rateLimiter;
|
|
33
|
+
retryConfig;
|
|
34
|
+
constructor(provider, config = {}) {
|
|
35
|
+
this.provider = provider;
|
|
36
|
+
this.name = `${provider.name}:rate-limited`;
|
|
37
|
+
this.rateLimiter = config.rateLimit ? createRateLimiter(config.rateLimit) : createRateLimiter();
|
|
38
|
+
this.retryConfig = config.retry ?? {};
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Get the rate limiter instance for statistics
|
|
42
|
+
*/
|
|
43
|
+
getRateLimiter() {
|
|
44
|
+
return this.rateLimiter;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Send messages with rate limiting and retry
|
|
48
|
+
*/
|
|
49
|
+
async *chat(messages, options) {
|
|
50
|
+
// Estimate tokens for rate limiting (rough estimate: 4 chars = 1 token)
|
|
51
|
+
const estimatedInputTokens = this.estimateTokens(messages);
|
|
52
|
+
const estimatedOutputTokens = options?.maxTokens ?? 4096;
|
|
53
|
+
const estimatedTotalTokens = estimatedInputTokens + estimatedOutputTokens;
|
|
54
|
+
// Acquire rate limit token
|
|
55
|
+
const acquireResult = await this.rateLimiter.acquire(estimatedTotalTokens);
|
|
56
|
+
if (!acquireResult.acquired) {
|
|
57
|
+
throw new Error(`Failed to acquire rate limit token after ${String(acquireResult.waitedMs)}ms`);
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
// Use retry wrapper for the actual API call
|
|
61
|
+
// For streaming, we need to handle retry differently
|
|
62
|
+
let actualUsage;
|
|
63
|
+
const streamWithRetry = async function* (provider, retryConfig) {
|
|
64
|
+
// Collect chunks and re-yield them
|
|
65
|
+
// On retry, we restart from the beginning
|
|
66
|
+
const executeStream = async () => {
|
|
67
|
+
const chunks = [];
|
|
68
|
+
for await (const chunk of provider.chat(messages, options)) {
|
|
69
|
+
chunks.push(chunk);
|
|
70
|
+
if (chunk.type === 'done' && chunk.usage) {
|
|
71
|
+
actualUsage = chunk.usage;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return chunks;
|
|
75
|
+
};
|
|
76
|
+
// Wrap the stream execution in retry logic
|
|
77
|
+
const chunks = await withRetry(executeStream, retryConfig);
|
|
78
|
+
// Yield all collected chunks
|
|
79
|
+
for (const chunk of chunks) {
|
|
80
|
+
yield chunk;
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
// Yield chunks from the retried stream
|
|
84
|
+
yield* streamWithRetry(this.provider, this.retryConfig);
|
|
85
|
+
// Report actual usage to rate limiter
|
|
86
|
+
if (actualUsage) {
|
|
87
|
+
const totalTokens = actualUsage.inputTokens + actualUsage.outputTokens;
|
|
88
|
+
this.rateLimiter.reportUsage(totalTokens);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
// Release the concurrent slot
|
|
93
|
+
this.rateLimiter.release();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Count tokens with rate limiting
|
|
98
|
+
*/
|
|
99
|
+
async countTokens(messages) {
|
|
100
|
+
if (!this.provider.countTokens) {
|
|
101
|
+
// Fallback estimation
|
|
102
|
+
return this.estimateTokens(messages);
|
|
103
|
+
}
|
|
104
|
+
const countTokensFn = this.provider.countTokens.bind(this.provider);
|
|
105
|
+
return withRetry(() => countTokensFn(messages), this.retryConfig);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Estimate tokens from messages (rough approximation)
|
|
109
|
+
*/
|
|
110
|
+
estimateTokens(messages) {
|
|
111
|
+
let charCount = 0;
|
|
112
|
+
for (const msg of messages) {
|
|
113
|
+
if (typeof msg.content === 'string') {
|
|
114
|
+
charCount += msg.content.length;
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
for (const block of msg.content) {
|
|
118
|
+
switch (block.type) {
|
|
119
|
+
case 'text':
|
|
120
|
+
charCount += block.text.length;
|
|
121
|
+
break;
|
|
122
|
+
case 'tool_use':
|
|
123
|
+
charCount += JSON.stringify(block.input).length;
|
|
124
|
+
break;
|
|
125
|
+
case 'tool_result':
|
|
126
|
+
charCount += block.content.length;
|
|
127
|
+
break;
|
|
128
|
+
case 'thinking':
|
|
129
|
+
charCount += block.thinking.length;
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// Rough estimate: 4 characters per token
|
|
136
|
+
return Math.ceil(charCount / 4);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Create a rate-limited provider wrapper
|
|
141
|
+
*
|
|
142
|
+
* @param provider - The base LLM provider
|
|
143
|
+
* @param config - Rate limit and retry configuration
|
|
144
|
+
* @returns Wrapped provider with rate limiting and retry
|
|
145
|
+
*
|
|
146
|
+
* @example
|
|
147
|
+
* ```typescript
|
|
148
|
+
* const provider = createClaudeProvider();
|
|
149
|
+
* const rateLimited = wrapWithRateLimit(provider, {
|
|
150
|
+
* rateLimit: { requestsPerMinute: 60 },
|
|
151
|
+
* retry: { maxRetries: 3 },
|
|
152
|
+
* });
|
|
153
|
+
* ```
|
|
154
|
+
*/
|
|
155
|
+
export function wrapWithRateLimit(provider, config) {
|
|
156
|
+
return new RateLimitedProvider(provider, config);
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Default rate limits for known providers
|
|
160
|
+
*/
|
|
161
|
+
export const ProviderRateLimits = {
|
|
162
|
+
/**
|
|
163
|
+
* Anthropic Claude API limits (Tier 1)
|
|
164
|
+
* @see https://docs.anthropic.com/en/api/rate-limits
|
|
165
|
+
*/
|
|
166
|
+
claude: {
|
|
167
|
+
tier1: {
|
|
168
|
+
requestsPerMinute: 50,
|
|
169
|
+
tokensPerMinute: 40000,
|
|
170
|
+
},
|
|
171
|
+
tier2: {
|
|
172
|
+
requestsPerMinute: 1000,
|
|
173
|
+
tokensPerMinute: 80000,
|
|
174
|
+
},
|
|
175
|
+
tier3: {
|
|
176
|
+
requestsPerMinute: 2000,
|
|
177
|
+
tokensPerMinute: 160000,
|
|
178
|
+
},
|
|
179
|
+
tier4: {
|
|
180
|
+
requestsPerMinute: 4000,
|
|
181
|
+
tokensPerMinute: 400000,
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
/**
|
|
185
|
+
* OpenAI GPT-4 API limits (approximate)
|
|
186
|
+
*/
|
|
187
|
+
openai: {
|
|
188
|
+
gpt4: {
|
|
189
|
+
requestsPerMinute: 500,
|
|
190
|
+
tokensPerMinute: 10000,
|
|
191
|
+
},
|
|
192
|
+
gpt4Turbo: {
|
|
193
|
+
requestsPerMinute: 500,
|
|
194
|
+
tokensPerMinute: 30000,
|
|
195
|
+
},
|
|
196
|
+
gpt35Turbo: {
|
|
197
|
+
requestsPerMinute: 3500,
|
|
198
|
+
tokensPerMinute: 90000,
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Automatic Retry with Exponential Backoff
|
|
3
|
+
*
|
|
4
|
+
* Provides retry functionality for LLM API calls with:
|
|
5
|
+
* - Exponential backoff
|
|
6
|
+
* - Jitter to prevent thundering herd
|
|
7
|
+
* - Retry-After header support
|
|
8
|
+
* - Integration with rate limiter
|
|
9
|
+
*/
|
|
10
|
+
import type { RetryConfig, RateLimiter } from './types.js';
|
|
11
|
+
/**
|
|
12
|
+
* Retry a function with exponential backoff
|
|
13
|
+
*
|
|
14
|
+
* @param fn - Function to retry
|
|
15
|
+
* @param config - Retry configuration
|
|
16
|
+
* @returns Result of the function
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```typescript
|
|
20
|
+
* const result = await withRetry(
|
|
21
|
+
* () => provider.chat(messages, options),
|
|
22
|
+
* {
|
|
23
|
+
* maxRetries: 3,
|
|
24
|
+
* baseDelayMs: 1000,
|
|
25
|
+
* onRetry: (attempt, error, delay) => {
|
|
26
|
+
* console.log(`Retry ${attempt} after ${delay}ms: ${error.message}`);
|
|
27
|
+
* },
|
|
28
|
+
* }
|
|
29
|
+
* );
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export declare function withRetry<T>(fn: () => Promise<T>, config?: RetryConfig): Promise<T>;
|
|
33
|
+
/**
|
|
34
|
+
* Create a retry wrapper that also integrates with rate limiting
|
|
35
|
+
*
|
|
36
|
+
* @param rateLimiter - Rate limiter instance
|
|
37
|
+
* @param config - Retry configuration
|
|
38
|
+
* @returns Function wrapper with retry and rate limiting
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```typescript
|
|
42
|
+
* const limiter = createRateLimiter({ requestsPerMinute: 60 });
|
|
43
|
+
* const retryWithLimit = createRetryWithRateLimit(limiter, { maxRetries: 3 });
|
|
44
|
+
*
|
|
45
|
+
* const result = await retryWithLimit(
|
|
46
|
+
* () => provider.chat(messages, options),
|
|
47
|
+
* 1000 // estimated tokens
|
|
48
|
+
* );
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export declare function createRetryWithRateLimit(rateLimiter: RateLimiter, config?: RetryConfig): <T>(fn: () => Promise<T>, estimatedTokens?: number) => Promise<T>;
|
|
52
|
+
/**
|
|
53
|
+
* Retry configuration builder for common scenarios
|
|
54
|
+
*/
|
|
55
|
+
export declare const RetryPresets: {
|
|
56
|
+
/**
|
|
57
|
+
* Conservative retry: few retries, long delays
|
|
58
|
+
*/
|
|
59
|
+
readonly conservative: () => RetryConfig;
|
|
60
|
+
/**
|
|
61
|
+
* Aggressive retry: more retries, shorter delays
|
|
62
|
+
*/
|
|
63
|
+
readonly aggressive: () => RetryConfig;
|
|
64
|
+
/**
|
|
65
|
+
* No retry: fail immediately
|
|
66
|
+
*/
|
|
67
|
+
readonly none: () => RetryConfig;
|
|
68
|
+
/**
|
|
69
|
+
* Respect API limits: use Retry-After headers when available
|
|
70
|
+
*/
|
|
71
|
+
readonly respectful: () => RetryConfig;
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Retry statistics collector
|
|
75
|
+
*/
|
|
76
|
+
export declare class RetryStats {
|
|
77
|
+
private attempts;
|
|
78
|
+
private successes;
|
|
79
|
+
private failures;
|
|
80
|
+
private retries;
|
|
81
|
+
private totalDelayMs;
|
|
82
|
+
private lastError?;
|
|
83
|
+
/**
|
|
84
|
+
* Create retry config that tracks statistics
|
|
85
|
+
*/
|
|
86
|
+
createConfig(baseConfig?: RetryConfig): RetryConfig;
|
|
87
|
+
/**
|
|
88
|
+
* Record an attempt outcome
|
|
89
|
+
*/
|
|
90
|
+
recordAttempt(success: boolean): void;
|
|
91
|
+
/**
|
|
92
|
+
* Get statistics
|
|
93
|
+
*/
|
|
94
|
+
getStats(): {
|
|
95
|
+
attempts: number;
|
|
96
|
+
successes: number;
|
|
97
|
+
failures: number;
|
|
98
|
+
retries: number;
|
|
99
|
+
totalDelayMs: number;
|
|
100
|
+
successRate: number;
|
|
101
|
+
averageRetries: number;
|
|
102
|
+
lastError?: string;
|
|
103
|
+
};
|
|
104
|
+
/**
|
|
105
|
+
* Reset statistics
|
|
106
|
+
*/
|
|
107
|
+
reset(): void;
|
|
108
|
+
}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Automatic Retry with Exponential Backoff
|
|
3
|
+
*
|
|
4
|
+
* Provides retry functionality for LLM API calls with:
|
|
5
|
+
* - Exponential backoff
|
|
6
|
+
* - Jitter to prevent thundering herd
|
|
7
|
+
* - Retry-After header support
|
|
8
|
+
* - Integration with rate limiter
|
|
9
|
+
*/
|
|
10
|
+
import { isProviderError } from '../errors.js';
|
|
11
|
+
/**
|
|
12
|
+
* Default retry configuration
|
|
13
|
+
*/
|
|
14
|
+
const DEFAULT_RETRY_CONFIG = {
|
|
15
|
+
maxRetries: 3,
|
|
16
|
+
baseDelayMs: 1000,
|
|
17
|
+
maxDelayMs: 60000,
|
|
18
|
+
backoffMultiplier: 2,
|
|
19
|
+
jitter: true,
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Sleep for a specified duration
|
|
23
|
+
*/
|
|
24
|
+
function sleep(ms) {
|
|
25
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Default function to determine if an error is retryable
|
|
29
|
+
*/
|
|
30
|
+
function defaultIsRetryable(error) {
|
|
31
|
+
// Check for ProviderError with retryable status
|
|
32
|
+
if (isProviderError(error)) {
|
|
33
|
+
return error.isRetryable();
|
|
34
|
+
}
|
|
35
|
+
// Check for network/connection errors
|
|
36
|
+
const message = error.message.toLowerCase();
|
|
37
|
+
if (message.includes('network') ||
|
|
38
|
+
message.includes('connection') ||
|
|
39
|
+
message.includes('timeout') ||
|
|
40
|
+
message.includes('econnreset') ||
|
|
41
|
+
message.includes('econnrefused') ||
|
|
42
|
+
message.includes('socket hang up')) {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Extract Retry-After header value in milliseconds
|
|
49
|
+
*/
|
|
50
|
+
function extractRetryAfter(error) {
|
|
51
|
+
// Check if error has a response with Retry-After header
|
|
52
|
+
const anyError = error;
|
|
53
|
+
let retryAfter;
|
|
54
|
+
// Try to get from response headers
|
|
55
|
+
if (anyError.response?.headers?.get) {
|
|
56
|
+
retryAfter = anyError.response.headers.get('retry-after');
|
|
57
|
+
}
|
|
58
|
+
else if (anyError.headers?.['retry-after']) {
|
|
59
|
+
retryAfter = anyError.headers['retry-after'];
|
|
60
|
+
}
|
|
61
|
+
if (!retryAfter)
|
|
62
|
+
return undefined;
|
|
63
|
+
// Retry-After can be a number of seconds or an HTTP date
|
|
64
|
+
const seconds = parseInt(retryAfter, 10);
|
|
65
|
+
if (!isNaN(seconds)) {
|
|
66
|
+
return seconds * 1000;
|
|
67
|
+
}
|
|
68
|
+
// Try parsing as HTTP date
|
|
69
|
+
const date = new Date(retryAfter);
|
|
70
|
+
if (!isNaN(date.getTime())) {
|
|
71
|
+
return Math.max(0, date.getTime() - Date.now());
|
|
72
|
+
}
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Calculate delay for a retry attempt
|
|
77
|
+
*/
|
|
78
|
+
function calculateDelay(attempt, config, retryAfterMs) {
|
|
79
|
+
// Use Retry-After if available and reasonable
|
|
80
|
+
if (retryAfterMs !== undefined && retryAfterMs > 0 && retryAfterMs <= config.maxDelayMs) {
|
|
81
|
+
return retryAfterMs;
|
|
82
|
+
}
|
|
83
|
+
// Exponential backoff: baseDelay * (multiplier ^ attempt)
|
|
84
|
+
let delay = config.baseDelayMs * Math.pow(config.backoffMultiplier, attempt);
|
|
85
|
+
// Add jitter (±25%)
|
|
86
|
+
if (config.jitter) {
|
|
87
|
+
const jitterRange = delay * 0.25;
|
|
88
|
+
delay = delay + (Math.random() * 2 - 1) * jitterRange;
|
|
89
|
+
}
|
|
90
|
+
// Cap at maxDelay
|
|
91
|
+
return Math.min(delay, config.maxDelayMs);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Retry a function with exponential backoff
|
|
95
|
+
*
|
|
96
|
+
* @param fn - Function to retry
|
|
97
|
+
* @param config - Retry configuration
|
|
98
|
+
* @returns Result of the function
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* ```typescript
|
|
102
|
+
* const result = await withRetry(
|
|
103
|
+
* () => provider.chat(messages, options),
|
|
104
|
+
* {
|
|
105
|
+
* maxRetries: 3,
|
|
106
|
+
* baseDelayMs: 1000,
|
|
107
|
+
* onRetry: (attempt, error, delay) => {
|
|
108
|
+
* console.log(`Retry ${attempt} after ${delay}ms: ${error.message}`);
|
|
109
|
+
* },
|
|
110
|
+
* }
|
|
111
|
+
* );
|
|
112
|
+
* ```
|
|
113
|
+
*/
|
|
114
|
+
export async function withRetry(fn, config = {}) {
|
|
115
|
+
const mergedConfig = {
|
|
116
|
+
...DEFAULT_RETRY_CONFIG,
|
|
117
|
+
...config,
|
|
118
|
+
};
|
|
119
|
+
const isRetryable = config.isRetryable ?? defaultIsRetryable;
|
|
120
|
+
let lastError;
|
|
121
|
+
for (let attempt = 0; attempt <= mergedConfig.maxRetries; attempt++) {
|
|
122
|
+
try {
|
|
123
|
+
return await fn();
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
127
|
+
// Check if we should retry
|
|
128
|
+
const shouldRetry = attempt < mergedConfig.maxRetries && isRetryable(lastError);
|
|
129
|
+
if (!shouldRetry) {
|
|
130
|
+
throw lastError;
|
|
131
|
+
}
|
|
132
|
+
// Calculate delay
|
|
133
|
+
const retryAfterMs = extractRetryAfter(lastError);
|
|
134
|
+
const delayMs = calculateDelay(attempt, mergedConfig, retryAfterMs);
|
|
135
|
+
// Call onRetry callback
|
|
136
|
+
if (config.onRetry) {
|
|
137
|
+
config.onRetry(attempt + 1, lastError, delayMs);
|
|
138
|
+
}
|
|
139
|
+
// Wait before retrying
|
|
140
|
+
await sleep(delayMs);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// Should never reach here, but TypeScript needs it
|
|
144
|
+
throw lastError ?? new Error('Retry failed with no error captured');
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Create a retry wrapper that also integrates with rate limiting
|
|
148
|
+
*
|
|
149
|
+
* @param rateLimiter - Rate limiter instance
|
|
150
|
+
* @param config - Retry configuration
|
|
151
|
+
* @returns Function wrapper with retry and rate limiting
|
|
152
|
+
*
|
|
153
|
+
* @example
|
|
154
|
+
* ```typescript
|
|
155
|
+
* const limiter = createRateLimiter({ requestsPerMinute: 60 });
|
|
156
|
+
* const retryWithLimit = createRetryWithRateLimit(limiter, { maxRetries: 3 });
|
|
157
|
+
*
|
|
158
|
+
* const result = await retryWithLimit(
|
|
159
|
+
* () => provider.chat(messages, options),
|
|
160
|
+
* 1000 // estimated tokens
|
|
161
|
+
* );
|
|
162
|
+
* ```
|
|
163
|
+
*/
|
|
164
|
+
export function createRetryWithRateLimit(rateLimiter, config = {}) {
|
|
165
|
+
return async (fn, estimatedTokens) => {
|
|
166
|
+
// Acquire rate limit token before attempting
|
|
167
|
+
const acquireResult = await rateLimiter.acquire(estimatedTokens);
|
|
168
|
+
if (!acquireResult.acquired) {
|
|
169
|
+
throw new Error(`Failed to acquire rate limit token after ${String(acquireResult.waitedMs)}ms`);
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
// Execute with retry
|
|
173
|
+
return await withRetry(fn, config);
|
|
174
|
+
}
|
|
175
|
+
finally {
|
|
176
|
+
// Always release the concurrent slot
|
|
177
|
+
rateLimiter.release();
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Retry configuration builder for common scenarios
|
|
183
|
+
*/
|
|
184
|
+
export const RetryPresets = {
|
|
185
|
+
/**
|
|
186
|
+
* Conservative retry: few retries, long delays
|
|
187
|
+
*/
|
|
188
|
+
conservative: () => ({
|
|
189
|
+
maxRetries: 2,
|
|
190
|
+
baseDelayMs: 2000,
|
|
191
|
+
maxDelayMs: 30000,
|
|
192
|
+
backoffMultiplier: 3,
|
|
193
|
+
jitter: true,
|
|
194
|
+
}),
|
|
195
|
+
/**
|
|
196
|
+
* Aggressive retry: more retries, shorter delays
|
|
197
|
+
*/
|
|
198
|
+
aggressive: () => ({
|
|
199
|
+
maxRetries: 5,
|
|
200
|
+
baseDelayMs: 500,
|
|
201
|
+
maxDelayMs: 10000,
|
|
202
|
+
backoffMultiplier: 1.5,
|
|
203
|
+
jitter: true,
|
|
204
|
+
}),
|
|
205
|
+
/**
|
|
206
|
+
* No retry: fail immediately
|
|
207
|
+
*/
|
|
208
|
+
none: () => ({
|
|
209
|
+
maxRetries: 0,
|
|
210
|
+
}),
|
|
211
|
+
/**
|
|
212
|
+
* Respect API limits: use Retry-After headers when available
|
|
213
|
+
*/
|
|
214
|
+
respectful: () => ({
|
|
215
|
+
maxRetries: 3,
|
|
216
|
+
baseDelayMs: 1000,
|
|
217
|
+
maxDelayMs: 120000, // Allow longer waits based on Retry-After
|
|
218
|
+
backoffMultiplier: 2,
|
|
219
|
+
jitter: true,
|
|
220
|
+
}),
|
|
221
|
+
};
|
|
222
|
+
/**
|
|
223
|
+
* Retry statistics collector
|
|
224
|
+
*/
|
|
225
|
+
export class RetryStats {
|
|
226
|
+
attempts = 0;
|
|
227
|
+
successes = 0;
|
|
228
|
+
failures = 0;
|
|
229
|
+
retries = 0;
|
|
230
|
+
totalDelayMs = 0;
|
|
231
|
+
lastError;
|
|
232
|
+
/**
|
|
233
|
+
* Create retry config that tracks statistics
|
|
234
|
+
*/
|
|
235
|
+
createConfig(baseConfig = {}) {
|
|
236
|
+
return {
|
|
237
|
+
...baseConfig,
|
|
238
|
+
onRetry: (attempt, error, delayMs) => {
|
|
239
|
+
this.retries++;
|
|
240
|
+
this.totalDelayMs += delayMs;
|
|
241
|
+
this.lastError = error;
|
|
242
|
+
// Call original onRetry if provided
|
|
243
|
+
if (baseConfig.onRetry) {
|
|
244
|
+
baseConfig.onRetry(attempt, error, delayMs);
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Record an attempt outcome
|
|
251
|
+
*/
|
|
252
|
+
recordAttempt(success) {
|
|
253
|
+
this.attempts++;
|
|
254
|
+
if (success) {
|
|
255
|
+
this.successes++;
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
this.failures++;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Get statistics
|
|
263
|
+
*/
|
|
264
|
+
getStats() {
|
|
265
|
+
return {
|
|
266
|
+
attempts: this.attempts,
|
|
267
|
+
successes: this.successes,
|
|
268
|
+
failures: this.failures,
|
|
269
|
+
retries: this.retries,
|
|
270
|
+
totalDelayMs: this.totalDelayMs,
|
|
271
|
+
successRate: this.attempts > 0 ? this.successes / this.attempts : 0,
|
|
272
|
+
averageRetries: this.attempts > 0 ? this.retries / this.attempts : 0,
|
|
273
|
+
lastError: this.lastError?.message,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Reset statistics
|
|
278
|
+
*/
|
|
279
|
+
reset() {
|
|
280
|
+
this.attempts = 0;
|
|
281
|
+
this.successes = 0;
|
|
282
|
+
this.failures = 0;
|
|
283
|
+
this.retries = 0;
|
|
284
|
+
this.totalDelayMs = 0;
|
|
285
|
+
this.lastError = undefined;
|
|
286
|
+
}
|
|
287
|
+
}
|