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:
208
src/bot/index.js
208
src/bot/index.js
@@ -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 }),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user