@compilr-dev/agents 0.3.1 → 0.3.2

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.
@@ -1,16 +1,131 @@
1
1
  /**
2
- * Utility functions
2
+ * Utility functions for @compilr-dev/agents
3
3
  */
4
4
  /**
5
5
  * Generate a unique ID for tool uses
6
6
  */
7
7
  export declare function generateId(): string;
8
8
  /**
9
- * Sleep for a specified duration
9
+ * Sleep for a specified duration, respecting AbortSignal
10
+ *
11
+ * @param ms - Duration in milliseconds
12
+ * @param signal - Optional AbortSignal to cancel sleep
13
+ * @throws Error if signal is aborted
10
14
  */
11
- export declare function sleep(ms: number): Promise<void>;
15
+ export declare function sleep(ms: number, signal?: AbortSignal): Promise<void>;
12
16
  /**
13
- * Retry a function with exponential backoff
17
+ * Configuration for LLM retry behavior
18
+ */
19
+ export interface RetryConfig {
20
+ /**
21
+ * Enable/disable automatic retry.
22
+ * @default true
23
+ */
24
+ enabled?: boolean;
25
+ /**
26
+ * Maximum number of retry attempts (not including initial attempt).
27
+ * Total attempts = maxAttempts + 1
28
+ * @default 10
29
+ */
30
+ maxAttempts?: number;
31
+ /**
32
+ * Base delay in milliseconds for exponential backoff.
33
+ * Actual delay = min(baseDelayMs * 2^attempt, maxDelayMs) + jitter
34
+ * @default 1000
35
+ */
36
+ baseDelayMs?: number;
37
+ /**
38
+ * Maximum delay in milliseconds (cap for exponential growth).
39
+ * @default 30000
40
+ */
41
+ maxDelayMs?: number;
42
+ }
43
+ /**
44
+ * Default retry configuration
45
+ */
46
+ export declare const DEFAULT_RETRY_CONFIG: Required<RetryConfig>;
47
+ /**
48
+ * Options for the withRetry function
49
+ */
50
+ export interface WithRetryOptions<E extends Error> {
51
+ /**
52
+ * Maximum retry attempts (default: 10)
53
+ */
54
+ maxAttempts?: number;
55
+ /**
56
+ * Base delay in milliseconds (default: 1000)
57
+ */
58
+ baseDelayMs?: number;
59
+ /**
60
+ * Maximum delay cap in milliseconds (default: 30000)
61
+ */
62
+ maxDelayMs?: number;
63
+ /**
64
+ * Function to determine if an error is retryable
65
+ */
66
+ isRetryable?: (error: E) => boolean;
67
+ /**
68
+ * Callback invoked before each retry attempt
69
+ */
70
+ onRetry?: (attempt: number, maxAttempts: number, error: E, delayMs: number) => void;
71
+ /**
72
+ * Callback invoked when all retries are exhausted
73
+ */
74
+ onExhausted?: (attempts: number, error: E) => void;
75
+ /**
76
+ * AbortSignal to cancel retries
77
+ */
78
+ signal?: AbortSignal;
79
+ }
80
+ /**
81
+ * Calculate delay with exponential backoff and jitter
82
+ *
83
+ * @param attempt - Current attempt number (0-indexed)
84
+ * @param baseDelayMs - Base delay in milliseconds
85
+ * @param maxDelayMs - Maximum delay cap in milliseconds
86
+ * @returns Delay in milliseconds
87
+ */
88
+ export declare function calculateBackoffDelay(attempt: number, baseDelayMs?: number, maxDelayMs?: number): number;
89
+ /**
90
+ * Execute an async function with automatic retry on failure
91
+ *
92
+ * @param fn - Async function to execute
93
+ * @param options - Retry options
94
+ * @returns Result of the function
95
+ * @throws Last error if all retries exhausted
96
+ *
97
+ * @example
98
+ * ```typescript
99
+ * const result = await withRetry(
100
+ * async () => {
101
+ * const response = await fetch(url);
102
+ * if (!response.ok) throw new Error(`HTTP ${response.status}`);
103
+ * return response.json();
104
+ * },
105
+ * {
106
+ * maxAttempts: 5,
107
+ * isRetryable: (error) => error.message.includes('HTTP 5'),
108
+ * onRetry: (attempt, max, error, delay) => {
109
+ * console.log(`Retry ${attempt}/${max} in ${delay}ms: ${error.message}`);
110
+ * },
111
+ * }
112
+ * );
113
+ * ```
114
+ */
115
+ export declare function withRetry<T, E extends Error = Error>(fn: () => Promise<T>, options?: WithRetryOptions<E>): Promise<T>;
116
+ /**
117
+ * Create a retryable async generator for streaming responses
118
+ *
119
+ * This is specifically designed for streaming LLM responses where we need to
120
+ * retry the entire stream if it fails partway through.
121
+ *
122
+ * @param fn - Function that returns an async iterable
123
+ * @param options - Retry options
124
+ * @returns Async generator that retries on failure
125
+ */
126
+ export declare function withRetryGenerator<T, E extends Error = Error>(fn: () => AsyncIterable<T>, options?: WithRetryOptions<E>): AsyncGenerator<T, void, undefined>;
127
+ /**
128
+ * @deprecated Use withRetry instead. This function is kept for backward compatibility.
14
129
  */
15
130
  export declare function retry<T>(fn: () => Promise<T>, options?: {
16
131
  maxRetries?: number;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Utility functions
2
+ * Utility functions for @compilr-dev/agents
3
3
  */
4
4
  /**
5
5
  * Generate a unique ID for tool uses
@@ -8,30 +8,181 @@ export function generateId() {
8
8
  return `toolu_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 9)}`;
9
9
  }
10
10
  /**
11
- * Sleep for a specified duration
11
+ * Sleep for a specified duration, respecting AbortSignal
12
+ *
13
+ * @param ms - Duration in milliseconds
14
+ * @param signal - Optional AbortSignal to cancel sleep
15
+ * @throws Error if signal is aborted
12
16
  */
13
- export function sleep(ms) {
14
- return new Promise((resolve) => setTimeout(resolve, ms));
17
+ export function sleep(ms, signal) {
18
+ return new Promise((resolve, reject) => {
19
+ if (signal?.aborted) {
20
+ reject(new Error('Aborted'));
21
+ return;
22
+ }
23
+ const timeout = setTimeout(resolve, ms);
24
+ if (signal) {
25
+ const abortHandler = () => {
26
+ clearTimeout(timeout);
27
+ reject(new Error('Aborted'));
28
+ };
29
+ signal.addEventListener('abort', abortHandler, { once: true });
30
+ // Clean up abort listener when timer completes
31
+ const cleanup = () => {
32
+ signal.removeEventListener('abort', abortHandler);
33
+ resolve();
34
+ };
35
+ clearTimeout(timeout);
36
+ setTimeout(cleanup, ms);
37
+ }
38
+ });
15
39
  }
16
40
  /**
17
- * Retry a function with exponential backoff
41
+ * Default retry configuration
18
42
  */
19
- export async function retry(fn, options = {}) {
20
- const { maxRetries = 3, baseDelay = 1000, maxDelay = 10000 } = options;
43
+ export const DEFAULT_RETRY_CONFIG = {
44
+ enabled: true,
45
+ maxAttempts: 10,
46
+ baseDelayMs: 1000,
47
+ maxDelayMs: 30000,
48
+ };
49
+ /**
50
+ * Calculate delay with exponential backoff and jitter
51
+ *
52
+ * @param attempt - Current attempt number (0-indexed)
53
+ * @param baseDelayMs - Base delay in milliseconds
54
+ * @param maxDelayMs - Maximum delay cap in milliseconds
55
+ * @returns Delay in milliseconds
56
+ */
57
+ export function calculateBackoffDelay(attempt, baseDelayMs = DEFAULT_RETRY_CONFIG.baseDelayMs, maxDelayMs = DEFAULT_RETRY_CONFIG.maxDelayMs) {
58
+ // Exponential backoff: baseDelay * 2^attempt
59
+ const exponentialDelay = baseDelayMs * Math.pow(2, attempt);
60
+ // Cap at maxDelay
61
+ const cappedDelay = Math.min(exponentialDelay, maxDelayMs);
62
+ // Add jitter (0 to 50% of base delay)
63
+ const jitter = Math.random() * baseDelayMs * 0.5;
64
+ return Math.floor(cappedDelay + jitter);
65
+ }
66
+ /**
67
+ * Execute an async function with automatic retry on failure
68
+ *
69
+ * @param fn - Async function to execute
70
+ * @param options - Retry options
71
+ * @returns Result of the function
72
+ * @throws Last error if all retries exhausted
73
+ *
74
+ * @example
75
+ * ```typescript
76
+ * const result = await withRetry(
77
+ * async () => {
78
+ * const response = await fetch(url);
79
+ * if (!response.ok) throw new Error(`HTTP ${response.status}`);
80
+ * return response.json();
81
+ * },
82
+ * {
83
+ * maxAttempts: 5,
84
+ * isRetryable: (error) => error.message.includes('HTTP 5'),
85
+ * onRetry: (attempt, max, error, delay) => {
86
+ * console.log(`Retry ${attempt}/${max} in ${delay}ms: ${error.message}`);
87
+ * },
88
+ * }
89
+ * );
90
+ * ```
91
+ */
92
+ export async function withRetry(fn, options = {}) {
93
+ const { maxAttempts = DEFAULT_RETRY_CONFIG.maxAttempts, baseDelayMs = DEFAULT_RETRY_CONFIG.baseDelayMs, maxDelayMs = DEFAULT_RETRY_CONFIG.maxDelayMs, isRetryable = () => true, onRetry, onExhausted, signal, } = options;
21
94
  let lastError;
22
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
95
+ let attempt = 0;
96
+ while (attempt <= maxAttempts) {
97
+ // Check for abort before each attempt
98
+ if (signal?.aborted) {
99
+ throw new Error('Aborted');
100
+ }
23
101
  try {
24
102
  return await fn();
25
103
  }
26
104
  catch (error) {
27
- lastError = error instanceof Error ? error : new Error(String(error));
28
- if (attempt < maxRetries) {
29
- const delay = Math.min(baseDelay * Math.pow(2, attempt), maxDelay);
30
- await sleep(delay);
105
+ lastError = error;
106
+ // Check if error is retryable
107
+ if (!isRetryable(lastError)) {
108
+ throw lastError;
109
+ }
110
+ // Check if we have attempts remaining
111
+ if (attempt >= maxAttempts) {
112
+ onExhausted?.(attempt + 1, lastError);
113
+ throw lastError;
114
+ }
115
+ // Calculate delay and notify
116
+ const delayMs = calculateBackoffDelay(attempt, baseDelayMs, maxDelayMs);
117
+ onRetry?.(attempt + 1, maxAttempts, lastError, delayMs);
118
+ // Wait before next attempt
119
+ await sleep(delayMs, signal);
120
+ attempt++;
121
+ }
122
+ }
123
+ // This should never be reached, but TypeScript needs it
124
+ throw lastError ?? new Error('Retry failed with no error');
125
+ }
126
+ /**
127
+ * Create a retryable async generator for streaming responses
128
+ *
129
+ * This is specifically designed for streaming LLM responses where we need to
130
+ * retry the entire stream if it fails partway through.
131
+ *
132
+ * @param fn - Function that returns an async iterable
133
+ * @param options - Retry options
134
+ * @returns Async generator that retries on failure
135
+ */
136
+ export async function* withRetryGenerator(fn, options = {}) {
137
+ const { maxAttempts = DEFAULT_RETRY_CONFIG.maxAttempts, baseDelayMs = DEFAULT_RETRY_CONFIG.baseDelayMs, maxDelayMs = DEFAULT_RETRY_CONFIG.maxDelayMs, isRetryable = () => true, onRetry, onExhausted, signal, } = options;
138
+ let attempt = 0;
139
+ while (attempt <= maxAttempts) {
140
+ // Check for abort before each attempt
141
+ if (signal?.aborted) {
142
+ throw new Error('Aborted');
143
+ }
144
+ try {
145
+ // Yield all items from the generator
146
+ for await (const item of fn()) {
147
+ // Check for abort during iteration
148
+ if (signal?.aborted) {
149
+ throw new Error('Aborted');
150
+ }
151
+ yield item;
152
+ }
153
+ // Successfully completed, return
154
+ return;
155
+ }
156
+ catch (error) {
157
+ const typedError = error;
158
+ // Check if error is retryable
159
+ if (!isRetryable(typedError)) {
160
+ throw typedError;
161
+ }
162
+ // Check if we have attempts remaining
163
+ if (attempt >= maxAttempts) {
164
+ onExhausted?.(attempt + 1, typedError);
165
+ throw typedError;
31
166
  }
167
+ // Calculate delay and notify
168
+ const delayMs = calculateBackoffDelay(attempt, baseDelayMs, maxDelayMs);
169
+ onRetry?.(attempt + 1, maxAttempts, typedError, delayMs);
170
+ // Wait before next attempt
171
+ await sleep(delayMs, signal);
172
+ attempt++;
32
173
  }
33
174
  }
34
- throw lastError ?? new Error('Retry failed with no error captured');
175
+ }
176
+ /**
177
+ * @deprecated Use withRetry instead. This function is kept for backward compatibility.
178
+ */
179
+ export async function retry(fn, options = {}) {
180
+ const { maxRetries = 3, baseDelay = 1000, maxDelay = 10000 } = options;
181
+ return withRetry(fn, {
182
+ maxAttempts: maxRetries,
183
+ baseDelayMs: baseDelay,
184
+ maxDelayMs: maxDelay,
185
+ });
35
186
  }
36
187
  /**
37
188
  * Truncate a string to a maximum length
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@compilr-dev/agents",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "Lightweight multi-LLM agent library for building CLI AI assistants",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -75,5 +75,8 @@
75
75
  "typescript": "^5.3.0",
76
76
  "typescript-eslint": "^8.48.0",
77
77
  "vitest": "^3.2.4"
78
+ },
79
+ "dependencies": {
80
+ "@google/genai": "^1.38.0"
78
81
  }
79
82
  }