0625d457be
- Add TypeScript project structure with Bun runtime - Implement CLI commands for session, project, stats, and optimization - Add SQLite database integration with prepared statements - Include AGENTS.md for development guidelines - Add Makefile for common development tasks - Configure ESLint and TypeScript with strict mode
394 lines
14 KiB
TypeScript
394 lines
14 KiB
TypeScript
import { Database } from '../database/Database';
|
|
import { OptimizationRecommendation } from '../models';
|
|
import { TokenTracker } from './TokenTracker';
|
|
import { SessionManager } from './SessionManager';
|
|
|
|
export class WorkflowOptimizer {
|
|
private db: Database;
|
|
private tokenTracker: TokenTracker;
|
|
private sessionManager: SessionManager;
|
|
|
|
constructor(db: Database, tokenTracker: TokenTracker, sessionManager: SessionManager) {
|
|
this.db = db;
|
|
this.tokenTracker = tokenTracker;
|
|
this.sessionManager = sessionManager;
|
|
}
|
|
|
|
async analyzeWorkflowEfficiency(projectId?: string): Promise<{
|
|
score: number;
|
|
recommendations: OptimizationRecommendation[];
|
|
insights: string[];
|
|
}> {
|
|
const recommendations: OptimizationRecommendation[] = [];
|
|
const insights: string[] = [];
|
|
let score = 100;
|
|
|
|
// Analyze session patterns
|
|
const sessionAnalysis = await this.analyzeSessionPatterns(projectId);
|
|
score -= sessionAnalysis.penalties;
|
|
recommendations.push(...sessionAnalysis.recommendations);
|
|
insights.push(...sessionAnalysis.insights);
|
|
|
|
// Analyze token usage
|
|
const tokenAnalysis = await this.analyzeTokenUsage();
|
|
score -= tokenAnalysis.penalties;
|
|
recommendations.push(...tokenAnalysis.recommendations);
|
|
insights.push(...tokenAnalysis.insights);
|
|
|
|
// Analyze scheduling efficiency
|
|
const schedulingAnalysis = await this.analyzeSchedulingEfficiency();
|
|
score -= schedulingAnalysis.penalties;
|
|
recommendations.push(...schedulingAnalysis.recommendations);
|
|
insights.push(...schedulingAnalysis.insights);
|
|
|
|
return {
|
|
score: Math.max(0, Math.min(100, score)),
|
|
recommendations: this.prioritizeRecommendations(recommendations),
|
|
insights
|
|
};
|
|
}
|
|
|
|
async suggestOptimalSession(taskDescription: string, complexity: 'simple' | 'medium' | 'complex'): Promise<{
|
|
modelType: 'opus' | 'sonnet';
|
|
estimatedTokens: number;
|
|
batchingSuggestions: string[];
|
|
preparationTips: string[];
|
|
}> {
|
|
const modelType = this.recommendModel(taskDescription, complexity);
|
|
const estimatedTokens = this.estimateTokenUsage(taskDescription, complexity, modelType);
|
|
|
|
return {
|
|
modelType,
|
|
estimatedTokens,
|
|
batchingSuggestions: this.generateBatchingSuggestions(taskDescription),
|
|
preparationTips: this.generatePreparationTips(complexity)
|
|
};
|
|
}
|
|
|
|
async generateOptimalSchedule(upcomingTasks: Array<{
|
|
description: string;
|
|
complexity: 'simple' | 'medium' | 'complex';
|
|
estimatedDuration: number; // minutes
|
|
priority: 'high' | 'medium' | 'low';
|
|
}>): Promise<{
|
|
schedule: Array<{
|
|
task: string;
|
|
recommendedTime: Date;
|
|
modelType: 'opus' | 'sonnet';
|
|
grouping: string;
|
|
}>;
|
|
totalEstimatedTokens: number;
|
|
optimizationNotes: string[];
|
|
}> {
|
|
const schedule = [];
|
|
let totalTokens = 0;
|
|
const optimizationNotes = [];
|
|
|
|
// Group tasks by complexity and type
|
|
const groupedTasks = this.groupTasksByType(upcomingTasks);
|
|
|
|
// Get current window status
|
|
const currentWindow = await this.sessionManager.getCurrentWindow();
|
|
const nextWindow = await this.sessionManager.getNextAvailableWindow();
|
|
|
|
let currentTime = currentWindow ? new Date() : (nextWindow || new Date());
|
|
|
|
for (const [groupName, tasks] of Object.entries(groupedTasks)) {
|
|
for (const task of tasks) {
|
|
const suggestion = await this.suggestOptimalSession(task.description, task.complexity);
|
|
|
|
schedule.push({
|
|
task: task.description,
|
|
recommendedTime: new Date(currentTime),
|
|
modelType: suggestion.modelType,
|
|
grouping: groupName
|
|
});
|
|
|
|
totalTokens += suggestion.estimatedTokens;
|
|
currentTime = new Date(currentTime.getTime() + task.estimatedDuration * 60 * 1000);
|
|
}
|
|
}
|
|
|
|
// Add optimization notes
|
|
if (totalTokens > 400000) {
|
|
optimizationNotes.push('High token usage expected. Consider splitting across multiple 5-hour windows');
|
|
}
|
|
|
|
const opusTasks = upcomingTasks.filter(t => t.complexity === 'complex').length;
|
|
if (opusTasks > 3) {
|
|
optimizationNotes.push('Multiple complex tasks detected. Consider using overlap hack for extended sessions');
|
|
}
|
|
|
|
return {
|
|
schedule,
|
|
totalEstimatedTokens: totalTokens,
|
|
optimizationNotes
|
|
};
|
|
}
|
|
|
|
async detectWorkflowAnomalies(): Promise<{
|
|
anomalies: Array<{
|
|
type: 'token_spike' | 'session_fragmentation' | 'model_misuse' | 'scheduling_conflict';
|
|
severity: 'low' | 'medium' | 'high';
|
|
description: string;
|
|
recommendation: string;
|
|
}>;
|
|
trends: Array<{
|
|
metric: string;
|
|
trend: 'improving' | 'declining' | 'stable';
|
|
change: number;
|
|
}>;
|
|
}> {
|
|
const anomalies = [];
|
|
const trends = [];
|
|
|
|
// Detect token spikes
|
|
const recentUsage = await this.tokenTracker.getUsageStats(7);
|
|
const avgDailyTokens = recentUsage.reduce((sum, day) => sum + day.totalTokens, 0) / 7;
|
|
|
|
if (avgDailyTokens > 100000) {
|
|
anomalies.push({
|
|
type: 'token_spike',
|
|
severity: 'high',
|
|
description: `Daily token usage (${avgDailyTokens.toLocaleString()}) is unusually high`,
|
|
recommendation: 'Review recent sessions for inefficiencies and consider using /compact more frequently'
|
|
});
|
|
}
|
|
|
|
// Detect session fragmentation
|
|
const recentSessions = await this.sessionManager.getSessionHistory(20);
|
|
const shortSessions = recentSessions.filter(s => {
|
|
const duration = s.endTime ? (s.endTime.getTime() - s.startTime.getTime()) / (1000 * 60) : 0;
|
|
return duration < 10;
|
|
}).length;
|
|
|
|
if (shortSessions > recentSessions.length * 0.5) {
|
|
anomalies.push({
|
|
type: 'session_fragmentation',
|
|
severity: 'medium',
|
|
description: `${shortSessions} out of ${recentSessions.length} recent sessions were shorter than 10 minutes`,
|
|
recommendation: 'Consider batching similar tasks into longer, more focused sessions'
|
|
});
|
|
}
|
|
|
|
// Detect model misuse
|
|
const opusHeavyDays = recentUsage.filter(day => day.opusSessions > day.sonnetSessions).length;
|
|
if (opusHeavyDays > recentUsage.length * 0.6) {
|
|
anomalies.push({
|
|
type: 'model_misuse',
|
|
severity: 'medium',
|
|
description: 'Opus is being used more frequently than Sonnet',
|
|
recommendation: 'Reserve Opus for complex tasks and use Sonnet for routine implementation work'
|
|
});
|
|
}
|
|
|
|
// Calculate trends
|
|
if (recentUsage.length >= 2) {
|
|
const recentAvg = recentUsage.slice(0, 3).reduce((sum, day) => sum + day.optimizationScore, 0) / 3;
|
|
const olderAvg = recentUsage.slice(3, 6).reduce((sum, day) => sum + day.optimizationScore, 0) / Math.min(3, recentUsage.length - 3);
|
|
|
|
if (recentAvg > olderAvg + 5) {
|
|
trends.push({ metric: 'optimization_score', trend: 'improving', change: recentAvg - olderAvg });
|
|
} else if (recentAvg < olderAvg - 5) {
|
|
trends.push({ metric: 'optimization_score', trend: 'declining', change: olderAvg - recentAvg });
|
|
} else {
|
|
trends.push({ metric: 'optimization_score', trend: 'stable', change: 0 });
|
|
}
|
|
}
|
|
|
|
return { anomalies, trends };
|
|
}
|
|
|
|
private async analyzeSessionPatterns(projectId?: string): Promise<{
|
|
penalties: number;
|
|
recommendations: OptimizationRecommendation[];
|
|
insights: string[];
|
|
}> {
|
|
const penalties = 0;
|
|
const recommendations: OptimizationRecommendation[] = [];
|
|
const insights: string[] = [];
|
|
|
|
const sessions = projectId
|
|
? await this.db.all('SELECT * FROM sessions WHERE projectId = ? ORDER BY startTime DESC LIMIT 10', [projectId])
|
|
: await this.sessionManager.getSessionHistory(10);
|
|
|
|
// Analyze session length distribution
|
|
const sessionLengths = sessions.map(session => {
|
|
const endTime = (session as { endTime?: string | Date }).endTime || new Date();
|
|
const endTimeDate = typeof endTime === 'string' ? new Date(endTime) : endTime;
|
|
return (endTimeDate.getTime() - new Date((session as { startTime: string }).startTime).getTime()) / (1000 * 60); // minutes
|
|
});
|
|
|
|
const avgLength = sessionLengths.reduce((sum, length) => sum + length, 0) / sessionLengths.length;
|
|
|
|
if (avgLength < 15) {
|
|
recommendations.push({
|
|
type: 'workflow',
|
|
priority: 'medium',
|
|
message: 'Average session length is quite short. Consider batching related tasks.'
|
|
});
|
|
}
|
|
|
|
insights.push(`Average session duration: ${avgLength.toFixed(1)} minutes`);
|
|
|
|
return { penalties, recommendations, insights };
|
|
}
|
|
|
|
private async analyzeTokenUsage(): Promise<{
|
|
penalties: number;
|
|
recommendations: OptimizationRecommendation[];
|
|
insights: string[];
|
|
}> {
|
|
const penalties = 0;
|
|
const recommendations: OptimizationRecommendation[] = [];
|
|
const insights: string[] = [];
|
|
|
|
const usageStats = await this.tokenTracker.getUsageStats(7);
|
|
usageStats.reduce((sum, day) => sum + day.totalTokens, 0);
|
|
const avgTokensPerSession = usageStats.reduce((sum, day) => sum + (day.totalTokens / Math.max(1, day.sessionsCount)), 0) / usageStats.length;
|
|
|
|
if (avgTokensPerSession > 50000) {
|
|
recommendations.push({
|
|
type: 'token',
|
|
priority: 'high',
|
|
message: 'High token usage per session detected. Consider using /compact more frequently.',
|
|
potentialSavings: avgTokensPerSession * 0.3
|
|
});
|
|
}
|
|
|
|
insights.push(`Average tokens per session: ${avgTokensPerSession.toLocaleString()}`);
|
|
|
|
return { penalties, recommendations, insights };
|
|
}
|
|
|
|
private async analyzeSchedulingEfficiency(): Promise<{
|
|
penalties: number;
|
|
recommendations: OptimizationRecommendation[];
|
|
insights: string[];
|
|
}> {
|
|
const penalties = 0;
|
|
const recommendations: OptimizationRecommendation[] = [];
|
|
const insights: string[] = [];
|
|
|
|
const currentWindow = await this.sessionManager.getCurrentWindow();
|
|
|
|
if (!currentWindow) {
|
|
recommendations.push({
|
|
type: 'scheduling',
|
|
priority: 'medium',
|
|
message: 'No active 5-hour window. Consider using the overlap hack to prepare for intensive sessions.'
|
|
});
|
|
}
|
|
|
|
insights.push(currentWindow ? 'Currently within an active 5-hour window' : 'No active window detected');
|
|
|
|
return { penalties, recommendations, insights };
|
|
}
|
|
|
|
private recommendModel(taskDescription: string, complexity: 'simple' | 'medium' | 'complex'): 'opus' | 'sonnet' {
|
|
if (complexity === 'complex') return 'opus';
|
|
if (complexity === 'simple') return 'sonnet';
|
|
|
|
// For medium complexity, analyze task description
|
|
const complexKeywords = ['architecture', 'design', 'refactor', 'optimize', 'debug complex', 'implement system'];
|
|
const hasComplexKeywords = complexKeywords.some(keyword =>
|
|
taskDescription.toLowerCase().includes(keyword)
|
|
);
|
|
|
|
return hasComplexKeywords ? 'opus' : 'sonnet';
|
|
}
|
|
|
|
private estimateTokenUsage(taskDescription: string, complexity: 'simple' | 'medium' | 'complex', modelType: 'opus' | 'sonnet'): number {
|
|
const baseTokens = {
|
|
simple: { sonnet: 5000, opus: 8000 },
|
|
medium: { sonnet: 15000, opus: 25000 },
|
|
complex: { sonnet: 30000, opus: 50000 }
|
|
};
|
|
|
|
return baseTokens[complexity][modelType];
|
|
}
|
|
|
|
private generateBatchingSuggestions(taskDescription: string): string[] {
|
|
const suggestions = [
|
|
'Prepare all related files and context before starting',
|
|
'Group similar implementation tasks together',
|
|
'Plan testing and linting as part of the same session'
|
|
];
|
|
|
|
if (taskDescription.includes('test')) {
|
|
suggestions.push('Include test writing in the same session as implementation');
|
|
}
|
|
|
|
if (taskDescription.includes('refactor')) {
|
|
suggestions.push('Batch related refactoring tasks to maintain context');
|
|
}
|
|
|
|
return suggestions;
|
|
}
|
|
|
|
private generatePreparationTips(complexity: 'simple' | 'medium' | 'complex'): string[] {
|
|
const tips = {
|
|
simple: [
|
|
'Have specific files ready to reference',
|
|
'Prepare clear, concise instructions'
|
|
],
|
|
medium: [
|
|
'Review relevant documentation beforehand',
|
|
'Prepare test cases or expected outcomes',
|
|
'Consider edge cases in advance'
|
|
],
|
|
complex: [
|
|
'Create a detailed step-by-step plan',
|
|
'Prepare comprehensive project context',
|
|
'Have fallback approaches ready',
|
|
'Schedule adequate time for implementation'
|
|
]
|
|
};
|
|
|
|
return tips[complexity];
|
|
}
|
|
|
|
private groupTasksByType(tasks: Array<{
|
|
description: string;
|
|
complexity: 'simple' | 'medium' | 'complex';
|
|
estimatedDuration: number;
|
|
priority: 'high' | 'medium' | 'low';
|
|
}>): Record<string, Array<{
|
|
description: string;
|
|
complexity: 'simple' | 'medium' | 'complex';
|
|
estimatedDuration: number;
|
|
priority: 'high' | 'medium' | 'low';
|
|
}>> {
|
|
return tasks.reduce((groups, task) => {
|
|
const category = this.categorizeTask(task);
|
|
if (!groups[category]) groups[category] = [];
|
|
groups[category].push(task);
|
|
return groups;
|
|
}, {} as Record<string, Array<{
|
|
description: string;
|
|
complexity: 'simple' | 'medium' | 'complex';
|
|
estimatedDuration: number;
|
|
priority: 'high' | 'medium' | 'low';
|
|
}>>);
|
|
}
|
|
|
|
private categorizeTask(task: {
|
|
description: string;
|
|
complexity: 'simple' | 'medium' | 'complex';
|
|
estimatedDuration: number;
|
|
priority: 'high' | 'medium' | 'low';
|
|
}): string {
|
|
if (task.complexity === 'complex') return 'Complex Architecture';
|
|
if (task.description.includes('test')) return 'Testing';
|
|
if (task.description.includes('fix') || task.description.includes('bug')) return 'Bug Fixes';
|
|
if (task.description.includes('feature')) return 'Feature Development';
|
|
return 'General Tasks';
|
|
}
|
|
|
|
private prioritizeRecommendations(recommendations: OptimizationRecommendation[]): OptimizationRecommendation[] {
|
|
return recommendations.sort((a, b) => {
|
|
const priorityOrder = { high: 3, medium: 2, low: 1 };
|
|
return priorityOrder[b.priority] - priorityOrder[a.priority];
|
|
});
|
|
}
|
|
} |