feat: massive Ruflo-inspired upgrade — plugin system, multi-agent swarm, hooks, enhanced memory

New systems (src/plugins/):
  - Plugin.js: lifecycle hooks (onLoad, onUnload, onConfigChange) + BasePlugin
  - PluginManager.js: fault-isolated extension point dispatch with metrics
  - PluginLoader.js: dependency-resolving batch loader with health checks
  - ExtensionPoints.js: 16 standard extension point names

New systems (src/bot/):
  - hooks.js: HookManager with pre/post tool, pre/post AI, session lifecycle
  - memory-backend.js: JSONBackend (typed entries + LRU) + InMemoryBackend (ephemeral with TTL)

New systems (src/agents/):
  - Agent.js: typed agents with capabilities, status tracking
  - Task.js: DAG-compatible tasks with priorities, dependencies, rollback
  - SwarmCoordinator.js: multi-agent orchestration (simple/hierarchical/swarm topologies)
  - agents/index.js: 9 agent roles + AgentOrchestrator

Bot integration (src/bot/index.js):
  - 6 new Ruflo-inspired tools: swarm_spawn, swarm_execute, swarm_distribute, swarm_state, swarm_terminate
  - Plugin system, hook system, swarm initialized in initBot
  - Pre/post tool hooks wired into tool execution
  - Ephemeral + persistent memory backends
  - Agent orchestrator with 9 specialized agent types
  - Graceful shutdown: all systems cleanup, conversation flush, pidfile release
  - Return object exposes pluginManager, swarm, hookManager, memBackend, agentOrchestrator, getState

This brings Ruflo's multi-agent architecture, plugin extensibility, hook-based lifecycle, and typed memory to zCode.
This commit is contained in:
admin
2026-05-06 09:22:21 +00:00
Unverified
parent 321279b430
commit dcd01da1b1
11 changed files with 1981 additions and 56 deletions

View File

@@ -18,6 +18,18 @@ import { createSessionState } from './session-state.js';
import { detectIntent } from './intent-detector.js';
import { streamChatWithRetry } from './stream-handler.js';
// ── Ruflo-inspired systems: plugins, hooks, swarm, enhanced memory ──
import { PluginManager } from '../plugins/PluginManager.js';
import { PluginLoader } from '../plugins/PluginLoader.js';
import { BasePlugin } from '../plugins/Plugin.js';
import { EXTENSION_POINTS } from '../plugins/ExtensionPoints.js';
import { hookManager, HOOK_TYPES } from './hooks.js';
import { initAgents, AgentOrchestrator } from '../agents/index.js';
import { Agent } from '../agents/Agent.js';
import { Task } from '../agents/Task.js';
import { SwarmCoordinator } from '../agents/SwarmCoordinator.js';
import { JSONBackend, InMemoryBackend, MEMORY_TYPES } from './memory-backend.js';
// ── Pidfile lock: prevent duplicate instances ──
const PIDFILE = path.join(process.env.HOME || '/tmp', '.zcode-bot.pid');
function acquirePidfile() {
@@ -208,6 +220,64 @@ export async function initBot(config, api, tools, skills, agents) {
toolMap: new Map((tools || []).map(t => [t.name, t])),
};
// ── Ruflo-inspired Plugin System ──
const pluginManager = new PluginManager({ coreVersion: '3.0.0' });
const pluginLoader = new PluginLoader(pluginManager);
await pluginManager.initialize();
svc.pluginManager = pluginManager;
svc.pluginLoader = pluginLoader;
// ── Ruflo-inspired Hook System ──
await hookManager.initialize?.();
svc.hooks = hookManager;
// ── Ruflo-inspired Swarm Coordinator ──
const swarm = new SwarmCoordinator({ topology: 'simple', maxAgents: 10 });
await swarm.initialize();
svc.swarm = swarm;
// ── Enhanced Memory Backend (JSON-based with typed entries + search) ──
const memBackend = new JSONBackend(path.join(process.cwd(), 'data', 'memory.json'), 500);
await memBackend.initialize();
svc.memBackend = memBackend;
// ── Ephemeral Agent Context (RAM-only, auto-evict) ──
const ephemeralMem = new InMemoryBackend(200, 30 * 60 * 1000);
svc.ephemeralMem = ephemeralMem;
// ── Agent Orchestrator (replaces simple agent map) ──
const agentOrchestrator = new AgentOrchestrator(agents || [], { topology: 'simple', maxAgents: 10 });
svc.agentOrchestrator = agentOrchestrator;
// ── Register default plugin hooks ──
// Pre-tool hook: log tool execution, check permissions
hookManager.register(HOOK_TYPES.PRE_TOOL, 'pre-tool-logger', async (ctx) => {
logger.info(`🔧 Hook: pre-tool ${ctx.toolName}`);
return true;
}, { priority: 10 });
// Post-tool hook: cache results, update metrics
hookManager.register(HOOK_TYPES.POST_TOOL, 'post-tool-cache', async (ctx) => {
if (ctx.toolName === 'file_read' && ctx.result) {
sessionState.cacheRead(ctx.toolName, ctx.result);
}
return true;
}, { priority: 5 });
// Pre-AI hook: check memory context
hookManager.register(HOOK_TYPES.PRE_AI, 'pre-ai-memory', async (ctx) => {
// Could inject memory context into AI prompt here
return true;
}, { priority: 5 });
// Post-AI hook: self-learning trigger
hookManager.register(HOOK_TYPES.POST_AI, 'post-ai-selflearn', async (ctx) => {
if (ctx.response && ctx.userMessage) {
selfLearn(ctx.userMessage, ctx.response, memory);
}
return true;
}, { priority: 1 });
// ── Tool definitions for the AI API (OpenAI function-calling format) ──
// Defined at startBot scope so delegate handler can access them
const TOOL_DEFS = {
@@ -361,6 +431,40 @@ export async function initBot(config, api, tools, skills, agents) {
input: { type: 'string' },
}, required: ['skill'] },
},
swarm_spawn: {
description: 'Spawn a new agent in the swarm for parallel task execution',
parameters: { type: 'object', properties: {
type: { type: 'string', enum: ['coder', 'reviewer', 'tester', 'architect', 'devops', 'security', 'researcher', 'designer', 'coordinator'], description: 'Agent type' },
name: { type: 'string', description: 'Agent name' },
capabilities: { type: 'array', items: { type: 'string' }, description: 'Agent capabilities' },
}, required: ['type'] },
},
swarm_execute: {
description: 'Execute a task with a specific swarm agent',
parameters: { type: 'object', properties: {
agent_id: { type: 'string', description: 'Agent ID' },
description: { type: 'string', description: 'Task description' },
type: { type: 'string', description: 'Task type' },
priority: { type: 'string', enum: ['high', 'medium', 'low'], description: 'Task priority' },
dependencies: { type: 'array', items: { type: 'string' }, description: 'Task dependencies' },
}, required: ['agent_id', 'description'] },
},
swarm_distribute: {
description: 'Distribute multiple tasks across swarm agents',
parameters: { type: 'object', properties: {
tasks: { type: 'array', items: { type: 'object' }, description: 'Array of {agent_id, description, type, priority, dependencies}' },
}, required: ['tasks'] },
},
swarm_state: {
description: 'Get current swarm state and metrics',
parameters: { type: 'object', properties: {} },
},
swarm_terminate: {
description: 'Terminate a swarm agent',
parameters: { type: 'object', properties: {
agent_id: { type: 'string', description: 'Agent ID to terminate' },
}, required: ['agent_id'] },
},
};
// ── AI chat with agentic tool loop ──
@@ -761,11 +865,61 @@ export async function initBot(config, api, tools, skills, agents) {
{ role: 'user', content: args.input || 'Please analyze the code and provide your expert review.' },
];
const result = await chatWithAI(skillMessages, { maxTokens: 4096 });
return `📚 **${skill.name}**:\n${result}`;
return `📚 **${skill.name}**:\\n${result}`;
} catch (e) {
return `❌ Skill ${skill.name} error: ${e.message}`;
}
},
swarm_spawn: async (args) => {
try {
const agent = await svc.swarm.spawnAgent({
type: args.type,
name: args.name || args.type,
capabilities: args.capabilities || [],
});
return `✅ Spawned agent: **${agent.name}** (id: ${agent.id}, type: ${agent.type})`;
} catch (e) { return `❌ Swarm spawn error: ${e.message}`; }
},
swarm_execute: async (args) => {
try {
const task = new Task({
type: args.type || 'generic',
description: args.description,
priority: args.priority || 'medium',
dependencies: args.dependencies || [],
});
const result = await svc.swarm.executeTask(args.agent_id, task);
return `✅ Task completed on agent ${args.agent_id}: ${JSON.stringify(result)}`;
} catch (e) { return `❌ Swarm execute error: ${e.message}`; }
},
swarm_distribute: async (args) => {
try {
const taskObjs = (args.tasks || []).map((t, i) => new Task({
id: t.id || `task_${i}`,
type: t.type || 'generic',
description: t.description || '',
priority: t.priority || 'medium',
dependencies: t.dependencies || [],
assignedTo: t.agent_id,
}));
const assignments = await svc.swarm.distributeTasks(taskObjs);
return `✅ Distributed ${assignments.length} tasks:\\n${assignments.map(a =>
` - ${a.taskId}${a.agentId || 'no agent'}${a.error ? ' (' + a.error + ')' : ''}`
).join('\\n')}`;
} catch (e) { return `❌ Swarm distribute error: ${e.message}`; }
},
swarm_state: async () => {
try {
const state = svc.swarm.getSwarmState();
return `🤖 **Swarm State**\\n\\nTopology: ${state.topology}\\nAgents: ${state.agents}\\nBy status: ${JSON.stringify(state.byStatus)}\\nBy type: ${JSON.stringify(state.byType)}`;
} catch (e) { return `❌ Swarm state error: ${e.message}`; }
},
swarm_terminate: async (args) => {
try {
await svc.swarm.terminateAgent(args.agent_id);
return `✅ Agent ${args.agent_id} terminated`;
} catch (e) { return `❌ Swarm terminate error: ${e.message}`; }
},
};
// ── Create grammy bot ──
@@ -1207,15 +1361,7 @@ export async function initBot(config, api, tools, skills, agents) {
logger.error('Unhandled rejection:', reason?.message || reason);
});
// ── Graceful shutdown: flush conversation history ──
const shutdown = async (signal) => {
logger.info(`🛑 Shutting down (${signal})...`);
await conversation.flush();
releasePidfile();
process.exit(0);
};
process.on('SIGINT', () => shutdown('SIGINT'));
process.on('SIGTERM', () => shutdown('SIGTERM'));
// ── Graceful shutdown is defined at end of initBot (requires full `svc`) ──
acquirePidfile();
@@ -1316,10 +1462,52 @@ export async function initBot(config, api, tools, skills, agents) {
}
}
// ── Graceful shutdown: cleanup all systems ──
const gracefulShutdown = async (signal) => {
logger.info(`🛑 Shutting down (${signal})...`);
// Flush conversation history
try { await conversation.flush(); } catch (e) { logger.warn(`Conversation flush: ${e.message}`); }
// Cleanup swarm
if (svc.swarm && typeof svc.swarm.shutdown === 'function') {
try { await svc.swarm.shutdown(); } catch (e) { logger.warn(`Swarm shutdown: ${e.message}`); }
}
// Cleanup plugin manager
if (svc.pluginManager && typeof svc.pluginManager.shutdown === 'function') {
try { await svc.pluginManager.shutdown(); } catch (e) { logger.warn(`Plugin shutdown: ${e.message}`); }
}
// Cleanup memory backends
if (svc.memBackend && typeof svc.memBackend.shutdown === 'function') {
try { await svc.memBackend.shutdown(); } catch (e) { logger.warn(`Memory shutdown: ${e.message}`); }
}
// Cleanup hooks
if (svc.hooks && typeof svc.hooks.shutdown === 'function') {
try { await svc.hooks.shutdown(); } catch (e) { logger.warn(`Hooks shutdown: ${e.message}`); }
}
// Release pidfile
releasePidfile();
// Stop webhook polling
try { await bot.stop(); } catch {}
// Close HTTP server
try { await new Promise(r => httpServer.close(r)); } catch {}
logger.info('✓ Shutdown complete');
process.exit(0);
};
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
process.on('uncaughtException', (e) => { logger.error('💥 Uncaught:', e.message, e.stack); gracefulShutdown('uncaught'); });
process.on('unhandledRejection', (e) => { logger.error('💥 Unhandled Rejection:', e.message); gracefulShutdown('unhandledRejection'); });
return {
send: (chatId, text) => bot.api.sendMessage(chatId, markdownToHtml(text), { parse_mode: 'HTML' }),
ws: (chatId, msg) => wsClients.get(chatId)?.send(JSON.stringify(msg)),
waitForMessages: async () => { await new Promise(() => {}); },
getConnections: () => wsClients.size,
// Expose new systems for external use
pluginManager: svc.pluginManager,
swarm: svc.swarm,
hookManager: svc.hooks,
memBackend: svc.memBackend,
agentOrchestrator: svc.agentOrchestrator,
getState: () => ({ tools: svc.tools.length, skills: svc.skills.length, agents: svc.agents.length, plugins: svc.pluginManager?.getPlugins()?.length || 0, wsClients: wsClients.size }),
};
}