UNPKG

aiwg

Version:

Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo

347 lines (295 loc) 9.34 kB
/** * AutonomousThinkingEngine — Self-directed task generation with safety constraints. * * Periodically reviews project state and proposes bounded improvement tasks. * Off by default. When enabled, constrained by allowlist, budget cap, * daily task limit, and optional human approval gate. * * @implements Plan: Daemon Starter — Autonomous Mode */ import { EventEmitter } from 'events'; /** * @typedef {Object} AutonomousConfig * @property {boolean} enabled * @property {number} thinking_interval_minutes * @property {number} max_daily_tasks * @property {number} budget_cap_usd * @property {boolean} require_approval * @property {string[]} allowed_actions * @property {string[]} blocked_actions */ /** * @typedef {Object} AutonomousEngineOptions * @property {import('../ralph-external/daemon-supervisor.mjs').DaemonSupervisor} supervisor * @property {AutonomousConfig} config * @property {import('../messaging/room-manager.mjs').RoomManager} [roomManager] */ /** * @typedef {Object} Proposal * @property {string} id * @property {string} action * @property {string} description * @property {string} prompt * @property {number} estimatedMinutes * @property {number} estimatedCostUsd * @property {'pending'|'approved'|'rejected'|'submitted'} status * @property {string} createdAt */ export class AutonomousEngine extends EventEmitter { /** @type {import('../ralph-external/daemon-supervisor.mjs').DaemonSupervisor} */ #supervisor; /** @type {AutonomousConfig} */ #config; /** @type {import('../messaging/room-manager.mjs').RoomManager|null} */ #roomManager; /** @type {NodeJS.Timeout|null} */ #timer = null; /** @type {number} */ #dailyTaskCount = 0; /** @type {number} */ #dailySpendUsd = 0; /** @type {string|null} */ #lastResetDate = null; /** @type {Map<string, Proposal>} */ #proposals = new Map(); /** @type {number} */ #proposalCounter = 0; /** @type {boolean} */ #running = false; /** * @param {AutonomousEngineOptions} options */ constructor(options) { super(); this.#supervisor = options.supervisor; this.#config = options.config; this.#roomManager = options.roomManager || null; } /** * Start the autonomous thinking loop. */ start() { if (this.#running) return; if (!this.#config.enabled) return; this.#running = true; const intervalMs = (this.#config.thinking_interval_minutes || 30) * 60 * 1000; this.#timer = setInterval(() => { this.#think().catch(err => { console.error(`[autonomous] Thinking cycle error: ${err.message}`); }); }, intervalMs); console.log(`[autonomous] Started (interval: ${this.#config.thinking_interval_minutes}min, ` + `max daily: ${this.#config.max_daily_tasks}, budget: $${this.#config.budget_cap_usd})`); } /** * Stop the autonomous thinking loop. */ stop() { if (this.#timer) { clearInterval(this.#timer); this.#timer = null; } this.#running = false; } /** * Enable autonomous mode at runtime. */ enable() { this.#config.enabled = true; if (!this.#running) this.start(); } /** * Disable autonomous mode at runtime. */ disable() { this.#config.enabled = false; this.stop(); } /** * Approve a pending proposal. * * @param {string} proposalId * @returns {boolean} */ async approveProposal(proposalId) { const proposal = this.#proposals.get(proposalId); if (!proposal || proposal.status !== 'pending') return false; proposal.status = 'approved'; await this.#submitProposal(proposal); return true; } /** * Reject a pending proposal. * * @param {string} proposalId * @param {string} [reason] * @returns {boolean} */ rejectProposal(proposalId, reason) { const proposal = this.#proposals.get(proposalId); if (!proposal || proposal.status !== 'pending') return false; proposal.status = 'rejected'; this.emit('proposal:rejected', { proposal, reason }); return true; } /** * Run one thinking cycle. */ async #think() { this.#resetDailyCountersIfNeeded(); // Check daily limits if (this.#dailyTaskCount >= (this.#config.max_daily_tasks || 10)) { console.log('[autonomous] Daily task limit reached, skipping'); return; } if (this.#dailySpendUsd >= (this.#config.budget_cap_usd || 5)) { console.log('[autonomous] Daily budget cap reached, skipping'); return; } // Generate a thinking prompt const prompt = this.#buildThinkingPrompt(); try { // Use the supervisor to run a lightweight analysis task const result = await this.#supervisor.submit({ prompt, priority: 1, // Lowest priority — interactive and scheduled tasks first metadata: { source: 'autonomous', type: 'thinking', }, }); // The task output should contain a JSON proposal // For now, track that we submitted a thinking task this.#dailyTaskCount++; this.emit('think:completed', { taskId: result?.loopId || result?.taskId }); } catch (error) { console.error(`[autonomous] Failed to submit thinking task: ${error.message}`); } } /** * Build the prompt for a thinking cycle. * * @returns {string} */ #buildThinkingPrompt() { const allowed = (this.#config.allowed_actions || []).join(', '); const blocked = (this.#config.blocked_actions || []).join(', '); return `You are the autonomous maintenance daemon for this project. Review the following and propose ONE specific, bounded improvement: - Check .aiwg/ artifact health (missing fields, stale dates) - Check recent git log (last 24h) for incomplete work - Check test coverage gaps - Check documentation drift Constraints: - You may ONLY perform actions in this allowlist: [${allowed}] - You may NOT perform: [${blocked}] - Keep the task under 10 minutes estimated execution time - Keep estimated cost under $${this.#config.budget_cap_usd || 5} Respond with ONLY a JSON object (no markdown fences): { "action": "one of the allowed actions", "description": "what you want to do and why", "prompt": "the exact prompt to execute the action", "estimatedMinutes": 5, "estimatedCostUsd": 0.50, "skip": false, "skipReason": "only if skip is true — why nothing needs doing" } If the project is in good shape and nothing needs doing, set skip: true.`; } /** * Submit an approved proposal as a task. * * @param {Proposal} proposal */ async #submitProposal(proposal) { // Validate against constraints if (!this.#validateProposal(proposal)) { proposal.status = 'rejected'; return; } try { const result = await this.#supervisor.submit({ prompt: proposal.prompt, priority: 1, metadata: { source: 'autonomous', type: 'proposal', proposalId: proposal.id, action: proposal.action, }, }); proposal.status = 'submitted'; this.#dailyTaskCount++; this.#dailySpendUsd += proposal.estimatedCostUsd || 0; this.emit('proposal:submitted', { proposal, taskId: result?.loopId || result?.taskId, }); } catch (error) { console.error(`[autonomous] Failed to submit proposal ${proposal.id}: ${error.message}`); } } /** * Validate a proposal against safety constraints. * * @param {Proposal} proposal * @returns {boolean} */ #validateProposal(proposal) { const allowed = this.#config.allowed_actions || []; const blocked = this.#config.blocked_actions || []; if (blocked.includes(proposal.action)) { console.warn(`[autonomous] Blocked action: ${proposal.action}`); return false; } if (allowed.length > 0 && !allowed.includes(proposal.action)) { console.warn(`[autonomous] Action not in allowlist: ${proposal.action}`); return false; } if ((this.#dailySpendUsd + (proposal.estimatedCostUsd || 0)) > (this.#config.budget_cap_usd || 5)) { console.warn('[autonomous] Proposal would exceed daily budget'); return false; } return true; } /** * Reset daily counters at midnight. */ #resetDailyCountersIfNeeded() { const today = new Date().toISOString().slice(0, 10); if (this.#lastResetDate !== today) { this.#dailyTaskCount = 0; this.#dailySpendUsd = 0; this.#lastResetDate = today; } } /** * Get pending proposals. * * @returns {Proposal[]} */ getPendingProposals() { return [...this.#proposals.values()].filter(p => p.status === 'pending'); } /** * Get status summary. * * @returns {Object} */ getStatus() { return { enabled: this.#config.enabled, running: this.#running, dailyTaskCount: this.#dailyTaskCount, dailyTaskLimit: this.#config.max_daily_tasks || 10, dailySpendUsd: this.#dailySpendUsd, budgetCapUsd: this.#config.budget_cap_usd || 5, pendingProposals: this.getPendingProposals().length, allowedActions: this.#config.allowed_actions || [], blockedActions: this.#config.blocked_actions || [], requireApproval: this.#config.require_approval || false, }; } } export default AutonomousEngine;