Files
flynn/src/models/retry.ts
T
William Valentin 4316dbd3be feat: add P2 features — retry policy, prompt templating, usage tracking, tech debt cleanup
- Extract shared splitMessage() into channels/utils.ts (dedup 4 adapters)
- Add Slack user name resolution with caching (users.info API)
- Add withRetry() with exponential backoff + jitter, isRetryable() filter
- Wire retry config into ModelRouter.chat() (non-streaming only)
- Add assembleSystemPrompt() multi-file template system (SOUL/AGENTS/IDENTITY/USER/TOOLS.md)
- Add usage tracking accumulators in NativeAgent + AgentOrchestrator
- Add estimateCost() with per-model pricing table
- Add /usage TUI command with full usage report formatting
- Add retrySchema and promptSchema to config schema

Tests: 569 passing, typecheck clean
2026-02-06 15:12:35 -08:00

72 lines
2.3 KiB
TypeScript

export interface RetryConfig {
/** Maximum number of retry attempts (default: 3). Does not count the initial attempt. */
maxRetries: number;
/** Initial delay in milliseconds before first retry (default: 1000). */
initialDelayMs: number;
/** Multiplier applied to delay after each retry (default: 2). */
backoffMultiplier: number;
/** Maximum delay in milliseconds (default: 30000). */
maxDelayMs: number;
/** Errors matching these patterns should NOT be retried (e.g. auth errors, invalid requests). */
nonRetryablePatterns: string[];
}
export const DEFAULT_RETRY_CONFIG: RetryConfig = {
maxRetries: 3,
initialDelayMs: 1000,
backoffMultiplier: 2,
maxDelayMs: 30000,
nonRetryablePatterns: [
'invalid_api_key',
'authentication',
'unauthorized',
'invalid_request',
'context_length_exceeded',
'content_policy',
],
};
export function isRetryable(error: Error, nonRetryablePatterns: string[]): boolean {
const msg = error.message.toLowerCase();
return !nonRetryablePatterns.some(pattern => msg.includes(pattern.toLowerCase()));
}
export async function withRetry<T>(
fn: () => Promise<T>,
config: RetryConfig = DEFAULT_RETRY_CONFIG,
label?: string,
): Promise<T> {
let lastError: Error | undefined;
for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error instanceof Error ? error : new Error(String(error));
// Don't retry non-retryable errors
if (!isRetryable(lastError, config.nonRetryablePatterns)) {
throw lastError;
}
// Don't retry if we've exhausted attempts
if (attempt >= config.maxRetries) {
throw lastError;
}
// Calculate delay with exponential backoff + jitter
const baseDelay = config.initialDelayMs * Math.pow(config.backoffMultiplier, attempt);
const delay = Math.min(baseDelay, config.maxDelayMs);
const jitter = delay * (0.5 + Math.random() * 0.5); // 50-100% of delay
console.warn(
`[retry] ${label ?? 'operation'} attempt ${attempt + 1}/${config.maxRetries} failed: ${lastError.message}. Retrying in ${Math.round(jitter)}ms...`,
);
await new Promise(resolve => setTimeout(resolve, jitter));
}
}
throw lastError ?? new Error('Retry failed with no error');
}