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
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
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');
|
||||
}
|
||||
Reference in New Issue
Block a user