diff --git a/src/agents/Agent.js b/src/agents/Agent.js index 9c2e4d86..ca0f6513 100644 --- a/src/agents/Agent.js +++ b/src/agents/Agent.js @@ -47,9 +47,11 @@ export class Agent { this._taskCount++; try { - const result = typeof task.execute === 'function' - ? await task.execute(this) - : { status: 'completed', output: null }; + const result = typeof task._customExecute === 'function' + ? await task._customExecute(this) + : typeof task.execute === 'function' + ? await task.execute(this) + : { status: 'completed', output: null }; this.status = 'idle'; return result; } catch (err) { diff --git a/src/agents/Task.js b/src/agents/Task.js index 0950e9a4..ce67371f 100644 --- a/src/agents/Task.js +++ b/src/agents/Task.js @@ -127,7 +127,18 @@ export class Task { /** Sort tasks by priority (high first) */ static sortByPriority(tasks) { - return [...tasks].sort((a, b) => b.getPriorityValue() - a.getPriorityValue()); + return [...tasks].sort((a, b) => { + const pa = typeof a.getPriorityValue === 'function' ? a.getPriorityValue() : Task._priorityValue(a.priority); + const pb = typeof b.getPriorityValue === 'function' ? b.getPriorityValue() : Task._priorityValue(b.priority); + return pb - pa; + }); + } + + static _priorityValue(p) { + if (p === TASK_PRIORITIES.HIGH) return 3; + if (p === TASK_PRIORITIES.NORMAL) return 2; + if (p === TASK_PRIORITIES.LOW) return 1; + return 2; } /** Resolve execution order respecting dependencies (topological sort) */ diff --git a/src/agents/index.js b/src/agents/index.js index e29b2557..258f052a 100644 --- a/src/agents/index.js +++ b/src/agents/index.js @@ -5,6 +5,7 @@ import { logger } from '../utils/logger.js'; import { Agent } from './Agent.js'; +import { Task } from './Task.js'; import { SwarmCoordinator } from './SwarmCoordinator.js'; const AGENT_DEFINITIONS = [ @@ -141,7 +142,7 @@ export class AgentOrchestrator { async executeMultiAgent(tasks, context = {}) { const taskObjects = tasks.map((t, i) => { const def = this.agentMap.get(t.agentId); - return { + return new Task({ id: t.id || `task_${i}`, type: def?.type || 'generic', description: t.description || '', @@ -150,7 +151,7 @@ export class AgentOrchestrator { requiredCapabilities: def?.capabilities || [], assignedTo: t.agentId, agentId: t.agentId, - }; + }); }); // Distribute and execute @@ -160,14 +161,13 @@ export class AgentOrchestrator { for (const { agentId, taskId } of assignments) { if (!agentId) continue; const task = taskObjects.find(t => t.id === taskId); - const result = await this.swarm.executeTask(agentId, { - ...task, - execute: async () => ({ - status: 'completed', - agentId, - output: `Task '${task.description}' executed by ${this.agentMap.get(agentId)?.name}`, - }), + // Attach execute handler for the agent to call + task._customExecute = async () => ({ + status: 'completed', + agentId, + output: `Task '${task.description}' executed by ${this.agentMap.get(agentId)?.name}`, }); + const result = await this.swarm.executeTask(agentId, task); results.push({ agentId, taskId, result }); } diff --git a/src/bot/memory-backend.js b/src/bot/memory-backend.js index 0d83f1e7..5ba7233a 100644 --- a/src/bot/memory-backend.js +++ b/src/bot/memory-backend.js @@ -157,7 +157,7 @@ export class JSONBackend { return scored.slice(0, limit).map(s => s.entry); } - async delete(id) { +async delete(id) { this._entries.delete(id); this._markDirty(); } @@ -170,11 +170,26 @@ export class JSONBackend { } async flush() { - if (this._saveTimer) { - clearTimeout(this._saveTimer); - this._saveTimer = null; + clearTimeout(this._saveTimer); + await this._save(); + } + + getAll() { + // Group entries by type + const grouped = { + lesson: [], gotcha: [], pattern: [], preference: [], discovery: [], context: [], + ephemeral: [], skill: [], conversation: [], error: [] + }; + for (const entry of this._entries.values()) { + if (grouped[entry.type]) { + grouped[entry.type].push(entry); + } } - if (!this._dirty) return; + return grouped; + } + + async flush() { + clearTimeout(this._saveTimer); await this._save(); } diff --git a/test-ruflo-smoke.mjs b/test-ruflo-smoke.mjs new file mode 100644 index 00000000..e56348bb --- /dev/null +++ b/test-ruflo-smoke.mjs @@ -0,0 +1,231 @@ +/** + * Smoke test for Ruflo-inspired systems + * Exercises: PluginManager, PluginLoader, HookManager, Agent, Task, SwarmCoordinator, Memory + */ +let passed = 0, failed = 0; +const assert = (msg, cond) => { if (cond) { passed++; } else { failed++; console.error(`❌ ${msg}`); } }; + +// ── 1. Plugin System ── +console.log('\n🧩 Plugin System'); +const { PluginManager, PLUGIN_STATES } = await import('./src/plugins/PluginManager.js'); +const { PluginLoader } = await import('./src/plugins/PluginLoader.js'); +const { BasePlugin } = await import('./src/plugins/Plugin.js'); +const { EXTENSION_POINTS } = await import('./src/plugins/ExtensionPoints.js'); + +const pm = new PluginManager({ coreVersion: '3.0.0' }); +await pm.initialize(); +assert('PluginManager initializes', pm.isInitialized() === true); + +// Register a test plugin +class TestPlugin extends BasePlugin { + constructor() { super({ id: 'test-plugin', name: 'Test Plugin', version: '1.0.0' }); } + async _onInitialize() { this._loaded = true; } + async _onShutdown() { this._unloaded = true; } +} + +const testPlugin = new TestPlugin(); +await pm.loadPlugin(testPlugin); +assert('Plugin loaded', pm.getPlugin('test-plugin')?.id === 'test-plugin'); +// Register an extension point on the plugin +testPlugin.registerExtensionPoint('pre_tool', async (ctx) => ({ handled: true, toolName: ctx.toolName })); +assert('Plugin registers extension points', testPlugin.getExtensionPoints().length > 0); +// Invoke extension point +const results = await pm.invokeExtensionPoint(EXTENSION_POINTS.PRE_TOOL, { toolName: 'test' }); +assert('Extension point invocation returns array', Array.isArray(results)); + +// PluginLoader +const loader = new PluginLoader(pm); +assert('PluginLoader created', loader._manager === pm); + +// Load with loader +const testPlugin2 = new TestPlugin(); +testPlugin2.id = 'test-plugin-2'; +testPlugin2.name = 'Test Plugin 2'; +await loader.loadPlugin(testPlugin2); +assert('Loader loads plugin', pm.getPlugin('test-plugin-2')?.id === 'test-plugin-2'); + +// Load multiple +const p3 = new (class extends BasePlugin { + constructor() { super({ id: 'test-plugin-3', name: 'Test Plugin 3', version: '1.0.0' }); } +})(); +await loader.loadPlugins([p3]); +assert('Loader loads multiple plugins', pm.getPluginCount() >= 3); + +// Unload +await pm.unloadPlugin('test-plugin'); +assert('Plugin unloaded', pm.getPlugin('test-plugin') === null); + +// ── 2. Hook System ── +console.log('\n🔗 Hook System'); +const { hookManager, HOOK_TYPES, HookManager } = await import('./src/bot/hooks.js'); + +let preToolFired = false; +hookManager.register(HOOK_TYPES.PRE_TOOL, 'test-hook', async (ctx) => { + preToolFired = true; + return true; +}); +assert('Hook registered', hookManager._hooks.get(HOOK_TYPES.PRE_TOOL)?.length > 0); + +const hookCtx = { toolName: 'bash', args: { command: 'echo hi' } }; +await hookManager.execute(HOOK_TYPES.PRE_TOOL, hookCtx); +assert('Pre-tool hook fires', preToolFired); + +// Maintenance +assert('Hook registered in map', hookManager._hooks.has(HOOK_TYPES.PRE_TOOL)); + +// ── 3. Agent System ── +console.log('\n🤖 Agent System'); +const { Agent } = await import('./src/agents/Agent.js'); +const { Task, TASK_PRIORITIES, TASK_STATUSES } = await import('./src/agents/Task.js'); +const { SwarmCoordinator } = await import('./src/agents/SwarmCoordinator.js'); +const { initAgents, AgentOrchestrator } = await import('./src/agents/index.js'); + +// Agent creation +const coder = new Agent({ id: 'coder-1', type: 'coder', name: 'Coder Alpha', capabilities: ['code', 'refactor'] }); +assert('Agent created with id', coder.id === 'coder-1'); +assert('Agent type set', coder.type === 'coder'); +assert('Agent capabilities stored', coder.capabilities.length === 2); +assert('Agent starts idle', coder.status === 'idle'); +assert('Agent idle getter', coder.idle === true); + +// Agent canHandleTask +const codeTask = { requiredCapabilities: ['code'] }; +const reviewTask = { requiredCapabilities: ['review'] }; +assert('Agent can handle matching task', coder.canHandleTask(codeTask)); +assert('Agent cannot handle mismatched task', coder.canHandleTask(reviewTask) === false); +assert('Agent has capability', coder.hasCapability('code')); + +// Task creation + +// Task creation +const task1 = new Task({ id: 'task-1', type: 'code', description: 'Write parser', priority: TASK_PRIORITIES.HIGH }); +assert('Task created with id', task1.id === 'task-1'); +assert('Task priority high', task1.priority === TASK_PRIORITIES.HIGH); + +const task2 = new Task({ id: 'task-2', type: 'review', description: 'Review parser', priority: TASK_PRIORITIES.NORMAL, dependencies: ['task-1'] }); +assert('Task dependencies', task2.dependencies.length === 1); + +// Task status transitions +task1.start(); +assert('Task started, status in_progress', task1.status === TASK_STATUSES.IN_PROGRESS); +task1.complete({ output: 'parser written' }); +assert('Task completed', task1.status === TASK_STATUSES.COMPLETED); +assert('Task result stored', task1._result?.output === 'parser written'); + +// Task fail +task2.start(); +task2.fail({ message: 'design review needed' }); +assert('Task failed', task2.status === TASK_STATUSES.FAILED); +assert('Task error stored', task2.error?.includes('design review')); + +// ── 4. Swarm Coordinator ── +console.log('\n🌐 Swarm Coordinator'); +const swarm = new SwarmCoordinator({ topology: 'simple', maxAgents: 5 }); +await swarm.initialize(); +assert('Swarm initialized', swarm.initialized === true); + +// Spawn an agent +const agent = await swarm.spawnAgent({ type: 'coder', name: 'Swarm Coder', capabilities: ['code'] }); +assert('Swarm agent spawned', agent.id?.startsWith('agent_')); +assert('Swarm agent type', agent.type === 'coder'); + +// Spawn another +const reviewer = await swarm.spawnAgent({ type: 'reviewer', name: 'Swarm Reviewer' }); +assert('Second agent spawned', reviewer.id !== agent.id); + +// Execute a task +const execTask = new Task({ type: 'code', description: 'Write tests', priority: TASK_PRIORITIES.HIGH }); +const execResult1 = await swarm.executeTask(agent.id, execTask); +assert('Swarm task executed', execResult1 !== undefined); + +// Distribute tasks +const distResult = await swarm.distributeTasks([ + new Task({ type: 'code', description: 'Feature X', priority: TASK_PRIORITIES.HIGH, assignedTo: agent.id }), + new Task({ type: 'review', description: 'Review X', priority: TASK_PRIORITIES.NORMAL, assignedTo: reviewer.id }), +]); +assert('Swarm distribute returns array', Array.isArray(distResult)); + +// Swarm state +const state = swarm.getSwarmState(); +assert('Swarm state has topology', state.topology === 'simple'); +assert('Swarm state has agents count', state.agents > 0); +assert('Swarm state has byStatus', typeof state.byStatus === 'object'); + +// Terminate agent +await swarm.terminateAgent(agent.id); +const stateAfter = swarm.getSwarmState(); +assert('Agent terminated reduces count', stateAfter.agents === state.agents - 1); + +// Shutdown +await swarm.shutdown(); +assert('Swarm shutdown resets initialized', swarm.initialized === false); + +// ── 5. Agent Orchestrator ── +console.log('\n🎭 Agent Orchestrator'); +const agentsFromInit = await initAgents(); +assert('initAgents returns array', Array.isArray(agentsFromInit)); +assert('initAgents has agents', agentsFromInit.length > 0); + +const orchestra = new AgentOrchestrator(agentsFromInit, { topology: 'simple', maxAgents: 10 }); +await orchestra.swarm.initialize(); +assert('Orchestrator created', orchestra.agentDefs.length > 0); +assert('Orchestrator has agentMap', orchestra.agentMap.size > 0); + +// Execute a task with an agent +const execResult = await orchestra.execute('coder', 'Write a parser'); +assert('Orchestrator execute returns result', execResult.success === true); +assert('Orchestrator execute returns agent name', typeof execResult.agent === 'string'); + +// Multi-agent execution +const multiResult = await orchestra.executeMultiAgent([ + { agentId: 'coder', description: 'Write tests' }, + { agentId: 'reviewer', description: 'Review code' }, +]); +assert('Multi-agent execution returns array', Array.isArray(multiResult)); +assert('Multi-agent execution has results', multiResult.length > 0); +assert('Multi-agent execution results have taskIds', multiResult[0].taskId); + +// ── 6. Memory Backend ── +// Memory Backend +const { JSONBackend, InMemoryBackend, MEMORY_TYPES } = await import('./src/bot/memory-backend.js'); +const fs = await import('fs'); +const os = await import('os'); +const path = await import('path'); + +const memPath = path.join(os.tmpdir(), `zcode-mem-test-${Date.now()}.json`); +const jmem = new JSONBackend(memPath, 100); +await jmem.initialize(); +console.log('DEBUG: jmem._loaded =', jmem._loaded); +console.log('DEBUG: jmem._entries.size =', jmem._entries.size); +assert('JSONBackend initializes', jmem._loaded === true); + +await jmem.store({ type: MEMORY_TYPES.FACT, key: 'language', value: 'JavaScript' }); +await jmem.store({ type: MEMORY_TYPES.LESSON, key: 'language', value: 'JavaScript' }); +const retrieved = await jmem.retrieve('language'); +console.log('DEBUG: retrieved =', retrieved); +assert('JSONBackend stores and retrieves fact', retrieved?.value === 'JavaScript'); + +await jmem.store({ type: MEMORY_TYPES.PATTERN, key: 'naming', description: 'camelCase for vars' }); +const all = jmem.getAll(); +assert('JSONBackend getAll returns object', typeof all === 'object'); +assert('JSONBackend has lesson', all.lesson?.length >= 1); + +// InMemoryBackend +const imem = new InMemoryBackend(50, 5000); // 5 second TTL +console.log('DEBUG: InMemoryBackend created, count =', imem.getCount()); +await imem.store({ id: 'session', data: 'test' }); +console.log('DEBUG: after store, count =', imem.getCount()); +const session = await imem.retrieve('session'); +console.log('DEBUG: session =', session); +assert('InMemoryBackend stores and retrieves', session?.data === 'test'); + +// Wait for TTL to expire +await new Promise(r => setTimeout(r, 100)); +const count = imem.getCount(); +assert('InMemoryBackend has count', count >= 0); + +// ── RESULTS ── +console.log(`\n${'═'.repeat(50)}`); +console.log(`📊 RESULTS: ${passed} passed, ${failed} failed out of ${passed + failed} assertions`); +if (failed > 0) process.exit(1); +console.log('✅ ALL SMOKE TESTS PASSED');