Spaces:
Paused
Paused
| /** | |
| * TaskRecorder - Observes, learns, and suggests automation | |
| * | |
| * CRITICAL RULE: Agents NEVER commit real tasks without user approval | |
| * | |
| * Features: | |
| * - Observes all tasks performed by users/agents | |
| * - Learns patterns from repeated tasks | |
| * - Suggests automation after observing N times | |
| * - Requires explicit user approval before automation | |
| * - Never auto-executes real tasks | |
| */ | |
| import { unifiedMemorySystem } from './UnifiedMemorySystem.js'; | |
| import { projectMemory } from '../../services/project/ProjectMemory.js'; | |
| import { eventBus } from '../EventBus.js'; | |
| import { getDatabase } from '../../database/index.js'; | |
| import { v4 as uuidv4 } from 'uuid'; | |
| export interface TaskObservation { | |
| id: string; | |
| taskType: string; | |
| taskSignature: string; // Hash of task parameters | |
| userId: string; | |
| orgId: string; | |
| timestamp: Date; | |
| duration?: number; | |
| success: boolean; | |
| result?: any; | |
| context?: Record<string, any>; | |
| params?: any; | |
| } | |
| export interface TaskPattern { | |
| taskSignature: string; | |
| taskType: string; | |
| frequency: number; | |
| firstSeen: Date; | |
| lastSeen: Date; | |
| successRate: number; | |
| averageDuration?: number; | |
| contexts: Record<string, number>; // Context patterns | |
| suggestedAutomation?: AutomationSuggestion; | |
| } | |
| export interface AutomationSuggestion { | |
| id: string; | |
| taskSignature: string; | |
| taskType: string; | |
| confidence: number; // 0-1, based on pattern strength | |
| observedCount: number; // How many times observed | |
| suggestedAction: string; // Description of what would be automated | |
| requiresApproval: boolean; // Always true for real tasks | |
| estimatedBenefit: string; // Time saved, etc. | |
| createdAt: Date; | |
| status: 'pending' | 'approved' | 'rejected' | 'active'; | |
| approvedBy?: string; | |
| approvedAt?: Date; | |
| } | |
| export interface TaskExecutionRequest { | |
| suggestionId: string; | |
| taskSignature: string; | |
| taskType: string; | |
| params: any; | |
| requestedBy: string; | |
| requiresApproval: boolean; // Always true for real tasks | |
| } | |
| export class TaskRecorder { | |
| private observations: Map<string, TaskObservation[]> = new Map(); // signature -> observations | |
| private patterns: Map<string, TaskPattern> = new Map(); // signature -> pattern | |
| private suggestions: Map<string, AutomationSuggestion> = new Map(); // suggestionId -> suggestion | |
| private readonly MIN_OBSERVATIONS_FOR_SUGGESTION = 3; // Suggest after 3 observations | |
| private readonly MIN_CONFIDENCE_FOR_SUGGESTION = 0.7; // 70% success rate minimum | |
| private db: any; | |
| constructor() { | |
| this.db = getDatabase(); | |
| this.initializeDatabase(); | |
| this.setupEventListeners(); | |
| } | |
| /** | |
| * Initialize database tables for task recording | |
| */ | |
| private initializeDatabase(): void { | |
| try { | |
| // Create task_observations table | |
| this.db.run(` | |
| CREATE TABLE IF NOT EXISTS task_observations ( | |
| id TEXT PRIMARY KEY, | |
| task_type TEXT NOT NULL, | |
| task_signature TEXT NOT NULL, | |
| user_id TEXT NOT NULL, | |
| org_id TEXT NOT NULL, | |
| timestamp TEXT NOT NULL, | |
| duration INTEGER, | |
| success INTEGER NOT NULL, | |
| result TEXT, | |
| context TEXT, | |
| created_at TEXT DEFAULT CURRENT_TIMESTAMP | |
| ) | |
| `); | |
| // Create task_patterns table | |
| this.db.run(` | |
| CREATE TABLE IF NOT EXISTS task_patterns ( | |
| task_signature TEXT PRIMARY KEY, | |
| task_type TEXT NOT NULL, | |
| frequency INTEGER NOT NULL DEFAULT 1, | |
| first_seen TEXT NOT NULL, | |
| last_seen TEXT NOT NULL, | |
| success_rate REAL NOT NULL, | |
| average_duration REAL, | |
| contexts TEXT, | |
| created_at TEXT DEFAULT CURRENT_TIMESTAMP, | |
| updated_at TEXT DEFAULT CURRENT_TIMESTAMP | |
| ) | |
| `); | |
| // Create automation_suggestions table | |
| this.db.run(` | |
| CREATE TABLE IF NOT EXISTS automation_suggestions ( | |
| id TEXT PRIMARY KEY, | |
| task_signature TEXT NOT NULL, | |
| task_type TEXT NOT NULL, | |
| confidence REAL NOT NULL, | |
| observed_count INTEGER NOT NULL, | |
| suggested_action TEXT NOT NULL, | |
| requires_approval INTEGER NOT NULL DEFAULT 1, | |
| estimated_benefit TEXT, | |
| status TEXT NOT NULL DEFAULT 'pending', | |
| approved_by TEXT, | |
| approved_at TEXT, | |
| created_at TEXT DEFAULT CURRENT_TIMESTAMP, | |
| FOREIGN KEY (task_signature) REFERENCES task_patterns(task_signature) | |
| ) | |
| `); | |
| // Create task_executions table (for approved automations) | |
| this.db.run(` | |
| CREATE TABLE IF NOT EXISTS task_executions ( | |
| id TEXT PRIMARY KEY, | |
| suggestion_id TEXT, | |
| task_signature TEXT NOT NULL, | |
| task_type TEXT NOT NULL, | |
| params TEXT NOT NULL, | |
| requested_by TEXT NOT NULL, | |
| approved_by TEXT NOT NULL, | |
| executed_at TEXT NOT NULL, | |
| success INTEGER, | |
| result TEXT, | |
| FOREIGN KEY (suggestion_id) REFERENCES automation_suggestions(id) | |
| ) | |
| `); | |
| console.log('✅ TaskRecorder database initialized'); | |
| } catch (error) { | |
| console.error('❌ Failed to initialize TaskRecorder database:', error); | |
| } | |
| } | |
| /** | |
| * Setup event listeners for task observation | |
| */ | |
| private setupEventListeners(): void { | |
| // Listen for MCP tool executions | |
| eventBus.onEvent('mcp.tool.executed', async (event: any) => { | |
| await this.observeTask({ | |
| taskType: event.tool || 'unknown', | |
| taskSignature: this.generateSignature(event.tool, event.payload), | |
| userId: event.userId || 'system', | |
| orgId: event.orgId || 'default', | |
| timestamp: new Date(), | |
| success: event.success !== false, | |
| result: event.result, | |
| context: { | |
| source: 'mcp_tool', | |
| tool: event.tool, | |
| payload: event.payload | |
| } | |
| }); | |
| }); | |
| // Listen for autonomous agent tasks | |
| eventBus.onEvent('autonomous.task.executed', async (event: any) => { | |
| await this.observeTask({ | |
| taskType: event.taskType || 'autonomous_task', | |
| taskSignature: this.generateSignature(event.taskType, event.payload), | |
| userId: event.userId || 'system', | |
| orgId: event.orgId || 'default', | |
| timestamp: new Date(), | |
| success: event.success !== false, | |
| result: event.result, | |
| context: { | |
| source: 'autonomous_agent', | |
| taskType: event.taskType | |
| } | |
| }); | |
| }); | |
| console.log('👁️ TaskRecorder event listeners setup complete'); | |
| } | |
| /** | |
| * Observe a task execution | |
| */ | |
| async observeTask(observation: Omit<TaskObservation, 'id'>): Promise<void> { | |
| const id = `obs-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; | |
| const fullObservation: TaskObservation = { ...observation, id }; | |
| // Store in memory | |
| const signature = observation.taskSignature; | |
| if (!this.observations.has(signature)) { | |
| this.observations.set(signature, []); | |
| } | |
| this.observations.get(signature)!.push(fullObservation); | |
| // Persist to database | |
| try { | |
| this.db.run(` | |
| INSERT INTO task_observations | |
| (id, task_type, task_signature, user_id, org_id, timestamp, duration, success, result, context) | |
| VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) | |
| `, [ | |
| id, | |
| observation.taskType, | |
| signature, | |
| observation.userId, | |
| observation.orgId, | |
| observation.timestamp.toISOString(), | |
| observation.duration || null, | |
| observation.success ? 1 : 0, | |
| observation.result ? JSON.stringify(observation.result) : null, | |
| observation.context ? JSON.stringify(observation.context) : null | |
| ]); | |
| } catch (error) { | |
| console.error('Failed to persist task observation:', error); | |
| } | |
| // Update pattern | |
| await this.updatePattern(fullObservation); | |
| // Check if we should suggest automation | |
| await this.checkAndSuggestAutomation(signature); | |
| console.log(`👁️ [TaskRecorder] Observed: ${observation.taskType} (${signature.substring(0, 8)}...)`); | |
| } | |
| /** | |
| * Update pattern from observation | |
| */ | |
| private async updatePattern(observation: TaskObservation): Promise<void> { | |
| const signature = observation.taskSignature; | |
| const existing = this.patterns.get(signature); | |
| if (existing) { | |
| // Update existing pattern | |
| existing.frequency += 1; | |
| existing.lastSeen = observation.timestamp; | |
| existing.successRate = this.calculateSuccessRate(signature); | |
| existing.averageDuration = this.calculateAverageDuration(signature); | |
| // Update context patterns | |
| if (observation.context) { | |
| for (const [key, value] of Object.entries(observation.context)) { | |
| const contextKey = `${key}:${value}`; | |
| existing.contexts[contextKey] = (existing.contexts[contextKey] || 0) + 1; | |
| } | |
| } | |
| // Update database | |
| try { | |
| this.db.run(` | |
| UPDATE task_patterns | |
| SET frequency = ?, | |
| last_seen = ?, | |
| success_rate = ?, | |
| average_duration = ?, | |
| contexts = ?, | |
| updated_at = CURRENT_TIMESTAMP | |
| WHERE task_signature = ? | |
| `, [ | |
| existing.frequency, | |
| existing.lastSeen.toISOString(), | |
| existing.successRate, | |
| existing.averageDuration || null, | |
| JSON.stringify(existing.contexts), | |
| signature | |
| ]); | |
| } catch (error) { | |
| console.error('Failed to update pattern:', error); | |
| } | |
| } else { | |
| // Create new pattern | |
| const pattern: TaskPattern = { | |
| taskSignature: signature, | |
| taskType: observation.taskType, | |
| frequency: 1, | |
| firstSeen: observation.timestamp, | |
| lastSeen: observation.timestamp, | |
| successRate: observation.success ? 1.0 : 0.0, | |
| averageDuration: observation.duration, | |
| contexts: observation.context ? | |
| Object.fromEntries(Object.entries(observation.context).map(([k, v]) => [`${k}:${v}`, 1])) : | |
| {} | |
| }; | |
| this.patterns.set(signature, pattern); | |
| // Insert into database | |
| try { | |
| this.db.run(` | |
| INSERT INTO task_patterns | |
| (task_signature, task_type, frequency, first_seen, last_seen, success_rate, average_duration, contexts) | |
| VALUES (?, ?, ?, ?, ?, ?, ?, ?) | |
| `, [ | |
| signature, | |
| pattern.taskType, | |
| pattern.frequency, | |
| pattern.firstSeen.toISOString(), | |
| pattern.lastSeen.toISOString(), | |
| pattern.successRate, | |
| pattern.averageDuration || null, | |
| JSON.stringify(pattern.contexts) | |
| ]); | |
| } catch (error) { | |
| console.error('Failed to insert pattern:', error); | |
| } | |
| } | |
| } | |
| /** | |
| * Check if automation should be suggested | |
| */ | |
| private async checkAndSuggestAutomation(signature: string): Promise<void> { | |
| const pattern = this.patterns.get(signature); | |
| if (!pattern) return; | |
| // Check if we've observed enough times | |
| if (pattern.frequency < this.MIN_OBSERVATIONS_FOR_SUGGESTION) { | |
| return; | |
| } | |
| // Check if success rate is high enough | |
| if (pattern.successRate < this.MIN_CONFIDENCE_FOR_SUGGESTION) { | |
| return; | |
| } | |
| // Check if suggestion already exists | |
| const existingSuggestion = Array.from(this.suggestions.values()) | |
| .find(s => s.taskSignature === signature && s.status === 'pending'); | |
| if (existingSuggestion) { | |
| return; // Already suggested | |
| } | |
| // Create automation suggestion | |
| const suggestion: AutomationSuggestion = { | |
| id: `sug-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, | |
| taskSignature: signature, | |
| taskType: pattern.taskType, | |
| confidence: pattern.successRate, | |
| observedCount: pattern.frequency, | |
| suggestedAction: `Automate "${pattern.taskType}" task (observed ${pattern.frequency} times with ${(pattern.successRate * 100).toFixed(0)}% success rate)`, | |
| requiresApproval: true, // ALWAYS require approval for real tasks | |
| estimatedBenefit: pattern.averageDuration | |
| ? `Saves ~${pattern.averageDuration}ms per execution` | |
| : 'Reduces manual repetition', | |
| createdAt: new Date(), | |
| status: 'pending' | |
| }; | |
| this.suggestions.set(suggestion.id, suggestion); | |
| pattern.suggestedAutomation = suggestion; | |
| // Persist suggestion | |
| try { | |
| this.db.run(` | |
| INSERT INTO automation_suggestions | |
| (id, task_signature, task_type, confidence, observed_count, suggested_action, requires_approval, estimated_benefit, status) | |
| VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) | |
| `, [ | |
| suggestion.id, | |
| signature, | |
| suggestion.taskType, | |
| suggestion.confidence, | |
| suggestion.observedCount, | |
| suggestion.suggestedAction, | |
| 1, // requires_approval = true | |
| suggestion.estimatedBenefit, | |
| 'pending' | |
| ]); | |
| } catch (error) { | |
| console.error('Failed to persist suggestion:', error); | |
| } | |
| // Emit event for UI notification | |
| eventBus.emit('taskrecorder.suggestion.created', { | |
| suggestion, | |
| pattern | |
| }); | |
| // Log to ProjectMemory | |
| try { | |
| projectMemory.logLifecycleEvent({ | |
| eventType: 'other', | |
| status: 'success', | |
| details: { | |
| component: 'TaskRecorder', | |
| action: 'automation_suggested', | |
| suggestionId: suggestion.id, | |
| taskType: pattern.taskType, | |
| observedCount: pattern.frequency, | |
| confidence: pattern.successRate | |
| } | |
| }); | |
| } catch (err) { | |
| console.warn('Could not log suggestion to ProjectMemory:', err); | |
| } | |
| console.log(`💡 [TaskRecorder] Automation suggested: ${suggestion.suggestedAction}`); | |
| } | |
| /** | |
| * Request task execution (requires approval) | |
| */ | |
| async requestTaskExecution(request: TaskExecutionRequest): Promise<{ approved: boolean; executionId?: string }> { | |
| // CRITICAL: Always require approval for real tasks | |
| if (!request.requiresApproval) { | |
| throw new Error('All real tasks require approval - cannot execute without approval'); | |
| } | |
| const suggestion = this.suggestions.get(request.suggestionId); | |
| if (!suggestion) { | |
| throw new Error(`Suggestion ${request.suggestionId} not found`); | |
| } | |
| // Check if suggestion is approved | |
| if (suggestion.status !== 'approved') { | |
| return { | |
| approved: false, | |
| executionId: undefined | |
| }; | |
| } | |
| // Execute task (with approval) | |
| const executionId = `exec-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; | |
| try { | |
| this.db.run(` | |
| INSERT INTO task_executions | |
| (id, suggestion_id, task_signature, task_type, params, requested_by, approved_by, executed_at) | |
| VALUES (?, ?, ?, ?, ?, ?, ?, ?) | |
| `, [ | |
| executionId, | |
| request.suggestionId, | |
| request.taskSignature, | |
| request.taskType, | |
| JSON.stringify(request.params), | |
| request.requestedBy, | |
| suggestion.approvedBy || 'system', | |
| new Date().toISOString() | |
| ]); | |
| // Emit execution event | |
| eventBus.emit('taskrecorder.execution.started', { | |
| executionId, | |
| request, | |
| suggestion | |
| }); | |
| return { | |
| approved: true, | |
| executionId | |
| }; | |
| } catch (error) { | |
| console.error('Failed to record execution:', error); | |
| throw error; | |
| } | |
| } | |
| /** | |
| * Approve automation suggestion | |
| */ | |
| async approveSuggestion(suggestionId: string, approvedBy: string): Promise<void> { | |
| const suggestion = this.suggestions.get(suggestionId); | |
| if (!suggestion) { | |
| throw new Error(`Suggestion ${suggestionId} not found`); | |
| } | |
| suggestion.status = 'approved'; | |
| suggestion.approvedBy = approvedBy; | |
| suggestion.approvedAt = new Date(); | |
| // Update database | |
| try { | |
| this.db.run(` | |
| UPDATE automation_suggestions | |
| SET status = 'approved', | |
| approved_by = ?, | |
| approved_at = ? | |
| WHERE id = ? | |
| `, [ | |
| approvedBy, | |
| suggestion.approvedAt.toISOString(), | |
| suggestionId | |
| ]); | |
| } catch (error) { | |
| console.error('Failed to update suggestion:', error); | |
| } | |
| // Emit event | |
| eventBus.emit('taskrecorder.suggestion.approved', { | |
| suggestion | |
| }); | |
| console.log(`✅ [TaskRecorder] Suggestion approved: ${suggestion.suggestedAction}`); | |
| } | |
| /** | |
| * Reject automation suggestion | |
| */ | |
| async rejectSuggestion(suggestionId: string, rejectedBy: string): Promise<void> { | |
| const suggestion = this.suggestions.get(suggestionId); | |
| if (!suggestion) { | |
| throw new Error(`Suggestion ${suggestionId} not found`); | |
| } | |
| suggestion.status = 'rejected'; | |
| // Update database | |
| try { | |
| this.db.run(` | |
| UPDATE automation_suggestions | |
| SET status = 'rejected' | |
| WHERE id = ? | |
| `, [suggestionId]); | |
| } catch (error) { | |
| console.error('Failed to update suggestion:', error); | |
| } | |
| console.log(`❌ [TaskRecorder] Suggestion rejected: ${suggestion.suggestedAction}`); | |
| } | |
| /** | |
| * Get all pending suggestions | |
| */ | |
| getPendingSuggestions(): AutomationSuggestion[] { | |
| return Array.from(this.suggestions.values()) | |
| .filter(s => s.status === 'pending') | |
| .sort((a, b) => b.confidence - a.confidence); | |
| } | |
| /** | |
| * Get pattern by signature | |
| */ | |
| getPattern(signature: string): TaskPattern | undefined { | |
| return this.patterns.get(signature); | |
| } | |
| /** | |
| * Get all patterns | |
| */ | |
| getAllPatterns(): TaskPattern[] { | |
| return Array.from(this.patterns.values()) | |
| .sort((a, b) => b.frequency - a.frequency); | |
| } | |
| /** | |
| * Generate signature from task type and params | |
| */ | |
| private generateSignature(taskType: string, params: any): string { | |
| const normalized = { | |
| type: taskType, | |
| params: this.normalizeParams(params) | |
| }; | |
| const str = JSON.stringify(normalized); | |
| // Use simple hash for signature | |
| return `sig-${this.simpleHash(str)}`; | |
| } | |
| /** | |
| * Normalize params for consistent signatures | |
| */ | |
| private normalizeParams(params: any): any { | |
| if (!params || typeof params !== 'object') { | |
| return params; | |
| } | |
| const normalized: any = {}; | |
| for (const [key, value] of Object.entries(params)) { | |
| // Ignore timestamps, IDs, and other variable fields | |
| if (['timestamp', 'id', 'createdAt', 'updatedAt', 'userId', 'orgId'].includes(key)) { | |
| continue; | |
| } | |
| normalized[key] = value; | |
| } | |
| return normalized; | |
| } | |
| /** | |
| * Simple hash function | |
| */ | |
| private simpleHash(str: string): string { | |
| let hash = 0; | |
| for (let i = 0; i < str.length; i++) { | |
| const char = str.charCodeAt(i); | |
| hash = ((hash << 5) - hash) + char; | |
| hash = hash & hash; // Convert to 32bit integer | |
| } | |
| return Math.abs(hash).toString(36); | |
| } | |
| /** | |
| * Calculate success rate for a pattern | |
| */ | |
| private calculateSuccessRate(signature: string): number { | |
| const observations = this.observations.get(signature) || []; | |
| if (observations.length === 0) return 0; | |
| const successful = observations.filter(o => o.success).length; | |
| return successful / observations.length; | |
| } | |
| /** | |
| * Calculate average duration for a pattern | |
| */ | |
| private calculateAverageDuration(signature: string): number | undefined { | |
| const observations = this.observations.get(signature) || []; | |
| const withDuration = observations.filter(o => o.duration !== undefined); | |
| if (withDuration.length === 0) return undefined; | |
| const sum = withDuration.reduce((acc, o) => acc + (o.duration || 0), 0); | |
| return sum / withDuration.length; | |
| } | |
| } | |
| // Singleton instance | |
| let taskRecorderInstance: TaskRecorder | null = null; | |
| export function getTaskRecorder(): TaskRecorder { | |
| if (!taskRecorderInstance) { | |
| taskRecorderInstance = new TaskRecorder(); | |
| } | |
| return taskRecorderInstance; | |
| } | |