UNPKG

aiwg

Version:

Cognitive architecture for AI-augmented software development with structured memory, ensemble validation, and closed-loop correction. FAIR-aligned artifacts, 84% cost reduction via human-in-the-loop, standards adopted by 100+ organizations.

311 lines (271 loc) 8.41 kB
/** * Automation Engine - Trigger-action rules for the daemon * * Processes configurable rules that map daemon events to agent actions. * Supports approval gates, cooldowns, and condition evaluation. * * @implements @.aiwg/requirements/use-cases/UC-AUTO-001.md * @tests @test/unit/daemon/automation-engine.test.js */ import { EventEmitter } from 'node:events'; export class AutomationEngine extends EventEmitter { constructor(options = {}) { super(); this.rules = []; this.supervisor = options.supervisor || null; this.enabled = options.enabled !== false; this.cooldowns = new Map(); // ruleId -> lastFiredAt this.executionLog = []; this.maxLogEntries = options.maxLogEntries || 500; this.approvalHandler = options.approvalHandler || null; } /** * Load rules from configuration * @param {Array} rules - Array of rule definitions */ loadRules(rules) { this.rules = []; for (const rule of rules) { const validated = this._validateRule(rule); if (validated) { this.rules.push(validated); } } this.emit('rules:loaded', { count: this.rules.length }); } /** * Process an incoming event against all rules * @param {object} event - Event to process */ async processEvent(event) { if (!this.enabled) return; const matchingRules = this.rules.filter((rule) => this._matchesRule(rule, event) ); for (const rule of matchingRules) { await this._executeRule(rule, event); } } /** * Enable/disable the engine */ setEnabled(enabled) { this.enabled = enabled; this.emit('engine:toggled', { enabled }); } /** * Get engine status */ getStatus() { return { enabled: this.enabled, ruleCount: this.rules.length, rules: this.rules.map((r) => ({ id: r.id, trigger: r.trigger, action: r.action, enabled: r.enabled !== false, cooldownMs: r.cooldownMs || 0, requiresApproval: r.requiresApproval || false, })), recentExecutions: this.executionLog.slice(-10), }; } /** * Get a specific rule by id */ getRule(id) { return this.rules.find((r) => r.id === id) || null; } /** * Enable or disable a specific rule */ setRuleEnabled(id, enabled) { const rule = this.rules.find((r) => r.id === id); if (!rule) return false; rule.enabled = enabled; this.emit('rule:toggled', { id, enabled }); return true; } /** * Get execution log */ getExecutionLog(limit = 50) { return this.executionLog.slice(-limit); } /** * Get rule count */ get ruleCount() { return this.rules.length; } // --- Private methods --- _validateRule(rule) { if (!rule.id || typeof rule.id !== 'string') { this.emit('rule:invalid', { rule, error: 'Missing or invalid rule id' }); return null; } if (!rule.trigger) { this.emit('rule:invalid', { rule, error: 'Missing trigger' }); return null; } if (!rule.action) { this.emit('rule:invalid', { rule, error: 'Missing action' }); return null; } return { id: rule.id, trigger: rule.trigger, action: rule.action, enabled: rule.enabled !== false, cooldownMs: rule.cooldownMs || 0, requiresApproval: rule.requiresApproval || false, conditions: rule.conditions || [], description: rule.description || '', }; } _matchesRule(rule, event) { if (rule.enabled === false) return false; // Check trigger type match if (rule.trigger.type && rule.trigger.type !== event.type) { return false; } // Check trigger source match if (rule.trigger.source && rule.trigger.source !== event.source) { return false; } // Check trigger pattern (regex on event type or source) if (rule.trigger.pattern) { const regex = new RegExp(rule.trigger.pattern); const matchTarget = `${event.type || ''}:${event.source || ''}`; if (!regex.test(matchTarget)) { return false; } } // Check conditions for (const condition of rule.conditions || []) { if (!this._evaluateCondition(condition, event)) { return false; } } // Check cooldown if (rule.cooldownMs > 0) { const lastFired = this.cooldowns.get(rule.id); if (lastFired && Date.now() - lastFired < rule.cooldownMs) { return false; } } return true; } _evaluateCondition(condition, event) { const { field, operator, value } = condition; // Navigate to the field value in the event const fieldValue = this._getNestedField(event, field); switch (operator) { case 'eq': case 'equals': return fieldValue === value; case 'neq': case 'not_equals': return fieldValue !== value; case 'contains': return typeof fieldValue === 'string' && fieldValue.includes(value); case 'matches': return typeof fieldValue === 'string' && new RegExp(value).test(fieldValue); case 'gt': return typeof fieldValue === 'number' && fieldValue > value; case 'lt': return typeof fieldValue === 'number' && fieldValue < value; case 'gte': return typeof fieldValue === 'number' && fieldValue >= value; case 'lte': return typeof fieldValue === 'number' && fieldValue <= value; case 'exists': return fieldValue !== undefined && fieldValue !== null; case 'not_exists': return fieldValue === undefined || fieldValue === null; default: return false; } } _getNestedField(obj, path) { const parts = path.split('.'); let current = obj; for (const part of parts) { if (current === undefined || current === null) return undefined; current = current[part]; } return current; } async _executeRule(rule, event) { // Check approval if (rule.requiresApproval) { if (this.approvalHandler) { const approved = await this.approvalHandler(rule, event); if (!approved) { this._logExecution(rule, event, 'denied'); return; } } else { // No approval handler, skip rule this._logExecution(rule, event, 'skipped_no_approver'); return; } } // Update cooldown this.cooldowns.set(rule.id, Date.now()); try { if (rule.action.type === 'agent' && this.supervisor) { // Submit to agent supervisor const prompt = this._interpolateTemplate(rule.action.prompt, event); const task = this.supervisor.submit(prompt, { agent: rule.action.agent, priority: rule.action.priority || 0, metadata: { ruleId: rule.id, eventType: event.type }, }); this._logExecution(rule, event, 'submitted', { taskId: task.id }); this.emit('rule:executed', { ruleId: rule.id, taskId: task.id, event }); } else if (rule.action.type === 'notify') { this.emit('rule:notify', { ruleId: rule.id, message: this._interpolateTemplate(rule.action.message, event), channel: rule.action.channel, event, }); this._logExecution(rule, event, 'notified'); } else if (rule.action.type === 'webhook') { this.emit('rule:webhook', { ruleId: rule.id, url: rule.action.url, payload: event, }); this._logExecution(rule, event, 'webhook_sent'); } else { this._logExecution(rule, event, 'unknown_action_type'); } } catch (err) { this._logExecution(rule, event, 'error', { error: err.message }); this.emit('rule:error', { ruleId: rule.id, error: err.message }); } } _interpolateTemplate(template, event) { if (!template) return ''; return template.replace(/\{\{(\w+(?:\.\w+)*)\}\}/g, (match, path) => { const value = this._getNestedField(event, path); return value !== undefined ? String(value) : match; }); } _logExecution(rule, event, status, extra = {}) { const entry = { ruleId: rule.id, eventType: event.type, status, timestamp: new Date().toISOString(), ...extra, }; this.executionLog.push(entry); if (this.executionLog.length > this.maxLogEntries) { this.executionLog.shift(); } } }