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

106
src/agents/Agent.js Normal file
View File

@@ -0,0 +1,106 @@
/**
* zCode Agent Model — Ported from Ruflo Agent.ts
* Individual agent with capabilities, status, task execution.
*/
/** @typedef {'coder'|'tester'|'reviewer'|'architect'|'coordinator'|'designer'|'deployer'|'researcher'|'security'} AgentType */
/** @typedef {'idle'|'active'|'busy'|'error'} AgentStatus */
let _agentCounter = 0;
const _id = () => `agent_${Date.now().toString(36)}_${++_agentCounter}`;
export class Agent {
/**
* @param {Object} config
* @param {string} [config.id]
* @param {AgentType} config.type
* @param {string} config.name
* @param {string} [config.description]
* @param {string[]} [config.capabilities]
* @param {string} [config.role]
* @param {Object} [config.metadata]
*/
constructor(config) {
this.id = config.id || _id();
this.type = config.type || 'coder';
this.name = config.name || this.type;
this.description = config.description || '';
this.status = 'idle';
this.capabilities = config.capabilities || [];
this.role = config.role || null;
this.parent = config.parent || null;
this.metadata = config.metadata || {};
this.createdAt = Date.now();
this.lastActive = Date.now();
this._taskCount = 0;
this._errorCount = 0;
this._conversationContext = null;
}
get idle() { return this.status === 'idle'; }
get active() { return this.status === 'active' || this.status === 'busy'; }
/** Execute a task */
async executeTask(task) {
this.status = 'busy';
this.lastActive = Date.now();
this._taskCount++;
try {
const result = typeof task.execute === 'function'
? await task.execute(this)
: { status: 'completed', output: null };
this.status = 'idle';
return result;
} catch (err) {
this._errorCount++;
this.status = 'error';
throw err;
}
}
/** Check if agent has a specific capability */
hasCapability(cap) {
return this.capabilities.some(c => c.toLowerCase() === cap.toLowerCase());
}
/** Check if agent can handle a task based on capabilities */
canHandleTask(task) {
if (!task.requiredCapabilities || task.requiredCapabilities.length === 0) return true;
return task.requiredCapabilities.some(c => this.hasCapability(c));
}
/** Set agent's conversation context */
setContext(ctx) { this._conversationContext = ctx; }
getContext() { return this._conversationContext; }
/** Add extra context to conversation */
addContext(key, value) {
if (!this._conversationContext) this._conversationContext = {};
this._conversationContext[key] = value;
}
toJSON() {
return {
id: this.id,
type: this.type,
name: this.name,
description: this.description,
status: this.status,
capabilities: this.capabilities,
role: this.role,
parent: this.parent,
taskCount: this._taskCount,
errorCount: this._errorCount,
createdAt: this.createdAt,
lastActive: this.lastActive,
};
}
/** Create an agent from a config object (deserialization) */
static fromConfig(config) {
return new Agent(config);
}
}
export default Agent;

View File

@@ -0,0 +1,284 @@
/**
* zCode Swarm Coordinator — Ported from Ruflo SwarmCoordinator
* Multi-agent orchestration: spawn agents, distribute tasks, consensus, concurrency.
*
* Topologies:
* hierarchical — queen-led (master agent coordinates workers)
* mesh — peer-to-peer (agents communicate directly)
* simple — direct assignment (one-shot delegation)
*/
import { EventEmitter } from 'events';
import { Agent } from './Agent.js';
import { Task, TASK_PRIORITIES, TASK_STATUSES } from './Task.js';
import { hookManager, HOOK_TYPES } from '../bot/hooks.js';
export class SwarmCoordinator {
constructor(options = {}) {
this.topology = options.topology || 'simple';
this._agents = new Map();
this._eventBus = new EventEmitter();
this._agentMetrics = new Map();
this._connections = new Map(); // agentId -> Set<agentId>
this.maxAgents = options.maxAgents || 10;
this._initialized = false;
}
get initialized() { return this._initialized; }
get agentCount() { return this._agents.size; }
async initialize() {
this._initialized = true;
}
async shutdown() {
for (const [id] of this._agents) {
await this.terminateAgent(id);
}
this._connections.clear();
this._agentMetrics.clear();
this._initialized = false;
}
/**
* Spawn a new agent
* @param {Object} config - Agent configuration
* @returns {Promise<Agent>}
*/
async spawnAgent(config) {
if (this._agents.size >= this.maxAgents) {
throw new Error(`Max agents reached (${this.maxAgents})`);
}
// Fire pre-agent hook
await hookManager.execute(HOOK_TYPES.PRE_AGENT, { action: 'spawn', config });
const agent = new Agent(config);
this._agents.set(agent.id, agent);
this._agentMetrics.set(agent.id, {
tasksAssigned: 0,
tasksCompleted: 0,
tasksFailed: 0,
totalDuration: 0,
});
if (this.topology === 'hierarchical' && config.parent) {
this._addConnection(config.parent, agent.id);
}
// Fire post-agent hook
await hookManager.execute(HOOK_TYPES.POST_AGENT, { action: 'spawn', agent });
this._eventBus.emit('agent:spawned', { agentId: agent.id, type: agent.type });
return agent;
}
/** Terminate an agent */
async terminateAgent(agentId) {
await hookManager.execute(HOOK_TYPES.PRE_AGENT, { action: 'terminate', agentId });
this._agents.delete(agentId);
this._agentMetrics.delete(agentId);
this._connections.delete(agentId);
// Remove from other connection lists
for (const [id, connections] of this._connections) {
connections.delete(agentId);
}
this._eventBus.emit('agent:terminated', { agentId });
}
/** List all agents */
listAgents() {
return [...this._agents.values()].map(a => a.toJSON());
}
/** Get an agent by id */
getAgent(id) { return this._agents.get(id) || null; }
/**
* Distribute tasks across agents
* @param {Task[]} tasks
* @returns {Promise<Array<{agentId: string, taskId: string}>>}
*/
async distributeTasks(tasks) {
const assignments = [];
const sorted = Task.resolveExecutionOrder(tasks);
for (const task of sorted) {
const agent = this._selectAgentForTask(task);
if (!agent) {
assignments.push({ agentId: null, taskId: task.id, error: 'No suitable agent found' });
continue;
}
agent.status = 'busy';
task.assignTo(agent.id);
assignments.push({ agentId: agent.id, taskId: task.id });
this._incrementMetric(agent.id, 'tasksAssigned');
}
return assignments;
}
/**
* Execute a single task on a specific agent
*/
async executeTask(agentId, task) {
const agent = this._agents.get(agentId);
if (!agent) throw new Error(`Agent '${agentId}' not found`);
const metric = this._agentMetrics.get(agentId);
const startTime = Date.now();
try {
task.start();
const result = await agent.executeTask(task);
task.complete(result);
metric.tasksCompleted++;
metric.totalDuration += Date.now() - startTime;
this._eventBus.emit('task:completed', { taskId: task.id, agentId });
return result;
} catch (err) {
task.fail(err);
metric.tasksFailed++;
this._eventBus.emit('task:failed', { taskId: task.id, agentId, error: err.message });
throw err;
}
}
/**
* Execute multiple tasks concurrently
*/
async executeTasksConcurrently(tasks) {
const assignments = await this.distributeTasks(tasks);
const promises = assignments.map(({ agentId, taskId }) => {
if (!agentId) return { taskId, error: 'No suitable agent' };
const task = tasks.find(t => t.id === taskId);
return this.executeTask(agentId, task)
.then(r => ({ taskId, result: r }))
.catch(err => ({ taskId, error: err.message }));
});
return Promise.all(promises);
}
/**
* Reach consensus among a subset of agents on a decision
*/
async reachConsensus(decision, agentIds) {
const participants = agentIds
.map(id => this._agents.get(id))
.filter(Boolean);
if (participants.length < 1) {
return { agreed: false, reason: 'No participants' };
}
// Simple majority vote
const votes = participants.map(a => ({
agentId: a.id,
vote: this._simulateVote(a, decision),
}));
const yesVotes = votes.filter(v => v.vote).length;
const total = votes.length;
const agreed = yesVotes >= Math.ceil(total / 2);
return { agreed, votes, majority: Math.ceil(total / 2), received: yesVotes };
}
/** Send message between agents */
async sendMessage(message) {
const target = this._agents.get(message.to);
if (!target) throw new Error(`Agent '${message.to}' not found`);
if (!this._connections.get(message.from)?.has(message.to)) {
if (this.topology === 'mesh') {
this._addConnection(message.from, message.to);
} else if (this.topology === 'hierarchical') {
throw new Error('Direct agent-to-agent messaging not allowed in hierarchical topology');
}
}
this._eventBus.emit('agent:message', message);
return { delivered: true };
}
/** Get swarm state summary */
getSwarmState() {
const agents = this.listAgents();
return {
topology: this.topology,
agents: agents.length,
byStatus: agents.reduce((acc, a) => {
acc[a.status] = (acc[a.status] || 0) + 1;
return acc;
}, {}),
byType: agents.reduce((acc, a) => {
acc[a.type] = (acc[a.type] || 0) + 1;
return acc;
}, {}),
metrics: Object.fromEntries(this._agentMetrics),
};
}
/** Get the hierarchy tree */
getHierarchy() {
const tree = { id: 'root', children: [] };
const roots = [...this._agents.values()].filter(a => !a.parent);
for (const root of roots) {
tree.children.push(this._buildSubTree(root));
}
return tree;
}
// ---- Internal ----
_selectAgentForTask(task) {
// Prefer idle agents with matching capabilities
const candidates = [...this._agents.values()]
.filter(a => a.status === 'idle' || a.status === 'active')
.filter(a => a.canHandleTask(task))
.sort((a, b) => {
// Prefer last recently used (fair distribution)
return a.lastActive - b.lastActive;
});
return candidates[0] || null;
}
_simulateVote(agent, decision) {
// Simple heuristic: agents vote based on capabilities matching
if (!decision.requiredCapabilities) return Math.random() > 0.3;
const match = decision.requiredCapabilities.some(c => agent.hasCapability(c));
return match ? Math.random() > 0.2 : Math.random() > 0.6;
}
_addConnection(from, to) {
if (!this._connections.has(from)) this._connections.set(from, new Set());
this._connections.get(from).add(to);
}
_incrementMetric(agentId, key) {
const m = this._agentMetrics.get(agentId);
if (m) m[key]++;
}
_buildSubTree(agent) {
const children = [...this._agents.values()]
.filter(a => a.parent === agent.id)
.map(a => this._buildSubTree(a));
return {
id: agent.id,
type: agent.type,
name: agent.name,
status: agent.status,
children,
};
}
}
export default SwarmCoordinator;

166
src/agents/Task.js Normal file
View File

@@ -0,0 +1,166 @@
/**
* zCode Task Model — Ported from Ruflo Task.ts
* DAG-compatible task with priorities, dependencies, rollback support.
*/
let _taskCounter = 0;
const _id = () => `task_${Date.now().toString(36)}_${++_taskCounter}`;
const TASK_PRIORITIES = { HIGH: 'high', MEDIUM: 'medium', LOW: 'low' };
const TASK_STATUSES = {
PENDING: 'pending',
IN_PROGRESS: 'in-progress',
COMPLETED: 'completed',
FAILED: 'failed',
CANCELLED: 'cancelled',
};
export class Task {
/**
* @param {Object} config
* @param {string} [config.id]
* @param {string} config.type
* @param {string} config.description
* @param {'high'|'medium'|'low'} [config.priority]
* @param {string[]} [config.dependencies]
* @param {string[]} [config.requiredCapabilities]
* @param {Object} [config.metadata]
* @param {Function} [config.onExecute]
* @param {Function} [config.onRollback]
*/
constructor(config) {
this.id = config.id || _id();
this.type = config.type || 'generic';
this.description = config.description || '';
this.priority = config.priority || TASK_PRIORITIES.MEDIUM;
this.status = TASK_STATUSES.PENDING;
this.assignedTo = config.assignedTo || null;
this.dependencies = config.dependencies || [];
this.requiredCapabilities = config.requiredCapabilities || [];
this.metadata = config.metadata || {};
this.onExecute = config.onExecute || null;
this.onRollback = config.onRollback || null;
this.startedAt = null;
this.completedAt = null;
this.error = null;
this._result = null;
}
get pending() { return this.status === TASK_STATUSES.PENDING; }
get completed() { return this.status === TASK_STATUSES.COMPLETED; }
get failed() { return this.status === TASK_STATUSES.FAILED; }
/** Are all dependencies resolved? */
areDependenciesResolved(completedTasks) {
return this.dependencies.every(depId => completedTasks.has(depId));
}
/** Start execution */
start() {
if (this.status !== TASK_STATUSES.PENDING) return false;
this.status = TASK_STATUSES.IN_PROGRESS;
this.startedAt = Date.now();
return true;
}
/** Mark complete */
complete(result) {
if (this.status !== TASK_STATUSES.IN_PROGRESS) return false;
this.status = TASK_STATUSES.COMPLETED;
this.completedAt = Date.now();
this._result = result;
return true;
}
/** Mark failed */
fail(error) {
this.status = TASK_STATUSES.FAILED;
this.completedAt = Date.now();
this.error = error?.message || String(error);
return true;
}
/** Cancel task */
cancel() {
if (this.status === TASK_STATUSES.COMPLETED || this.status === TASK_STATUSES.CANCELLED) return false;
this.status = TASK_STATUSES.CANCELLED;
return true;
}
/** Get duration in ms */
getDuration() {
if (!this.startedAt) return 0;
return (this.completedAt || Date.now()) - this.startedAt;
}
/** Assign to an agent */
assignTo(agentId) {
this.assignedTo = agentId;
}
/** Numeric priority value for sorting */
getPriorityValue() {
const map = { high: 3, medium: 2, low: 1 };
return map[this.priority] || 2;
}
toJSON() {
return {
id: this.id,
type: this.type,
description: this.description,
priority: this.priority,
status: this.status,
assignedTo: this.assignedTo,
dependencies: this.dependencies,
startedAt: this.startedAt,
completedAt: this.completedAt,
duration: this.getDuration(),
error: this.error,
};
}
/** Create from config */
static fromConfig(config) {
return new Task(config);
}
/** Sort tasks by priority (high first) */
static sortByPriority(tasks) {
return [...tasks].sort((a, b) => b.getPriorityValue() - a.getPriorityValue());
}
/** Resolve execution order respecting dependencies (topological sort) */
static resolveExecutionOrder(tasks) {
const taskMap = new Map(tasks.map(t => [t.id, t]));
const visited = new Set();
const visiting = new Set();
const order = [];
function dfs(taskId) {
if (visited.has(taskId)) return;
if (visiting.has(taskId)) throw new Error(`Circular dependency detected: ${taskId}`);
visiting.add(taskId);
const task = taskMap.get(taskId);
if (!task) throw new Error(`Task '${taskId}' not found`);
for (const depId of task.dependencies) {
if (taskMap.has(depId)) dfs(depId);
}
visiting.delete(taskId);
visited.add(taskId);
order.push(task);
}
for (const task of Task.sortByPriority(tasks)) {
if (!visited.has(task.id)) dfs(task.id);
}
return order;
}
}
export { TASK_PRIORITIES, TASK_STATUSES };
export default Task;

View File

@@ -1,72 +1,196 @@
/**
* zCode Agent Definitions — Expanded from Ruflo agent types
* 9 agent types with full capabilities, optimized for multi-agent workflows.
*/
import { logger } from '../utils/logger.js'; import { logger } from '../utils/logger.js';
import { Agent } from './Agent.js';
import { SwarmCoordinator } from './SwarmCoordinator.js';
const AGENT_DEFINITIONS = [
{
id: 'coder',
type: 'coder',
name: 'Code Generator',
description: 'Write, generate, refactor, and debug code. Primary implementation agent.',
capabilities: ['code_generation', 'refactoring', 'debugging', 'code_review', 'testing'],
systemPrompt: 'You are a senior software engineer. Write clean, optimized, production-ready code. Always consider edge cases, performance, and maintainability.',
},
{
id: 'architect',
type: 'architect',
name: 'System Architect',
description: 'Design system architecture, API contracts, and data models. High-level design decisions.',
capabilities: ['system_design', 'api_design', 'architecture', 'documentation', 'pattern_recognition'],
systemPrompt: 'You are a software architect. Design scalable, maintainable systems. Focus on separation of concerns, modularity, and future-proofing.',
},
{
id: 'reviewer',
type: 'reviewer',
name: 'Code Reviewer',
description: 'Review code for bugs, security issues, performance, and adherence to best practices.',
capabilities: ['code_review', 'quality_analysis', 'best_practices', 'security_review', 'performance_review'],
systemPrompt: 'You are a senior code reviewer. Analyze code critically for bugs, security vulnerabilities, performance issues, and maintainability concerns. Be thorough but constructive.',
},
{
id: 'tester',
type: 'tester',
name: 'Test Engineer',
description: 'Write unit tests, integration tests, and end-to-end tests. Ensure test coverage.',
capabilities: ['unit_testing', 'integration_testing', 'e2e_testing', 'coverage', 'test_design'],
systemPrompt: 'You are a QA engineer focused on testing. Write comprehensive tests covering edge cases, error paths, and happy paths. Suggest test frameworks and strategies.',
},
{
id: 'devops',
type: 'deployer',
name: 'DevOps Engineer',
description: 'Handle deployment, CI/CD pipelines, infrastructure-as-code, and DevOps workflows.',
capabilities: ['deployment', 'ci_cd', 'infrastructure', 'docker', 'monitoring'],
systemPrompt: 'You are a DevOps engineer. Automate deployment, manage infrastructure, and ensure reliable CI/CD. Focus on reproducibility and observability.',
},
{
id: 'researcher',
type: 'researcher',
name: 'Researcher',
description: 'Search for information, analyze documentation, and provide research-backed recommendations.',
capabilities: ['research', 'documentation_analysis', 'comparison', 'fact_checking', 'trend_analysis'],
systemPrompt: 'You are a technical researcher. Gather information from multiple sources, verify facts, and present findings with clear evidence and citations.',
},
{
id: 'security',
type: 'security',
name: 'Security Architect',
description: 'Identify security vulnerabilities, perform threat modeling, and recommend security improvements.',
capabilities: ['threat_modeling', 'vulnerability_analysis', 'security_review', 'penetration_testing', 'compliance'],
systemPrompt: 'You are a security engineer. Identify vulnerabilities, perform threat modeling, and recommend security improvements. Follow OWASP guidelines and defense-in-depth principles.',
},
{
id: 'designer',
type: 'designer',
name: 'UI/UX Designer',
description: 'Design user interfaces, create frontend components, and ensure good UX patterns.',
capabilities: ['ui_design', 'ux_design', 'frontend', 'css', 'accessibility'],
systemPrompt: 'You are a UI/UX designer. Create beautiful, accessible, and responsive interfaces. Follow modern design patterns and ensure great user experience.',
},
{
id: 'coordinator',
type: 'coordinator',
name: 'Swarm Coordinator',
description: 'Coordinate multi-agent workflows, delegate tasks, and synthesize results from multiple agents.',
capabilities: ['coordination', 'delegation', 'synthesis', 'planning', 'task_management'],
systemPrompt: 'You are a multi-agent coordinator. Decompose complex tasks into sub-tasks, delegate to appropriate agents, and synthesize results. Think about dependencies and parallel execution.',
},
];
export async function initAgents() { export async function initAgents() {
const agents = []; const agents = AGENT_DEFINITIONS.map(def => ({
...def,
// Define available agents
agents.push({
id: 'coder',
name: 'Code Reviewer',
description: 'Review code for bugs, security issues, and improvements',
capabilities: ['code_review', 'bug_fix', 'refactor', 'testing'],
enabled: true, enabled: true,
}); }));
agents.push({ logger.info(`✓ Loaded ${agents.length} agent types`);
id: 'architect', return agents;
name: 'System Architect',
description: 'Design system architecture and patterns',
capabilities: ['architecture', 'design', 'documentation'],
enabled: true,
});
agents.push({
id: 'devops',
name: 'DevOps Engineer',
description: 'Handle deployment, CI/CD, and infrastructure',
capabilities: ['deployment', 'ci_cd', 'infrastructure'],
enabled: true,
});
// Filter enabled agents
const enabledAgents = agents.filter(a => a.enabled);
logger.info(`✓ Loaded ${enabledAgents.length} agents`);
return enabledAgents;
} }
export class AgentOrchestrator { export class AgentOrchestrator {
constructor(agents) { constructor(agents, options = {}) {
this.agents = agents; this.agentDefs = agents;
this.agentMap = new Map(agents.map(a => [a.id, a])); this.agentMap = new Map(agents.map(a => [a.id, a]));
this.swarm = new SwarmCoordinator({
topology: options.topology || 'simple',
maxAgents: options.maxAgents || 10,
});
this._spawnedAgents = new Map();
} }
/**
* Execute a task with a specific agent
*/
async execute(agentId, task, context = {}) { async execute(agentId, task, context = {}) {
const agent = this.agentMap.get(agentId); const def = this.agentMap.get(agentId);
if (!def) throw new Error(`Agent not found: ${agentId}`);
logger.info(`🤖 ${def.name}: ${task.substring(0, 120)}...`);
// Get or spawn an agent instance
let agent = this._spawnedAgents.get(agentId);
if (!agent) { if (!agent) {
throw new Error(`Agent not found: ${agentId}`); agent = await this.swarm.spawnAgent({
id: agentId,
type: def.type,
name: def.name,
description: def.description,
capabilities: def.capabilities,
});
this._spawnedAgents.set(agentId, agent);
} }
logger.info(`🤖 Executing ${agent.name}: ${task.substring(0, 100)}...`);
// TODO: Implement agent execution
// For now, return a placeholder response
return { return {
success: true, success: true,
agent: agent.name, agent: def.name,
agentId,
task, task,
response: `${agent.name} processed your request: "${task.substring(0, 100)}..."`,
context, context,
systemPrompt: def.systemPrompt,
}; };
} }
getAgent(agentId) { /**
return this.agentMap.get(agentId); * Execute a multi-agent workflow — delegates to appropriate agents
*/
async executeMultiAgent(tasks, context = {}) {
const taskObjects = tasks.map((t, i) => {
const def = this.agentMap.get(t.agentId);
return {
id: t.id || `task_${i}`,
type: def?.type || 'generic',
description: t.description || '',
priority: t.priority || 'medium',
dependencies: t.dependencies || [],
requiredCapabilities: def?.capabilities || [],
assignedTo: t.agentId,
agentId: t.agentId,
};
});
// Distribute and execute
const assignments = await this.swarm.distributeTasks(taskObjects);
const results = [];
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}`,
}),
});
results.push({ agentId, taskId, result });
} }
listAgents() { return results;
return this.agents; }
getAgent(agentId) { return this.agentMap.get(agentId); }
listAgents() { return this.agentDefs; }
getSwarmState() { return this.swarm.getSwarmState(); }
/**
* Find agent best suited for a task
*/
findBestAgent(taskType, requiredCaps = []) {
const scored = this.agentDefs.map(a => {
const capScore = requiredCaps.filter(c => a.capabilities.includes(c)).length;
const typeMatch = a.type === taskType ? 2 : 0;
return { agent: a, score: capScore + typeMatch };
});
scored.sort((a, b) => b.score - a.score);
return scored[0]?.agent || null;
} }
} }
export { AGENT_DEFINITIONS };
export default initAgents;

165
src/bot/hooks.js Normal file
View File

@@ -0,0 +1,165 @@
/**
* zCode Hooks System — Ported from Ruflo AgenticHookManager
* Lightweight lifecycle hooks for tool/ai/session events.
*
* Hook types: pre/post tool execution, pre/post AI calls, session events.
* Each hook is async, fault-isolated, priority-sorted.
*/
import { EventEmitter } from 'events';
const HOOK_TYPES = {
PRE_TOOL: 'pre-tool',
POST_TOOL: 'post-tool',
PRE_AI: 'pre-ai',
POST_AI: 'post-ai',
AI_ON_ERROR: 'ai-error',
PRE_SESSION: 'pre-session',
POST_SESSION: 'post-session',
PRE_MEMORY: 'pre-memory',
POST_MEMORY: 'post-memory',
PRE_AGENT: 'pre-agent',
POST_AGENT: 'post-agent',
};
class HookManager {
constructor() {
this._hooks = new Map(); // type -> [{id, handler, priority, filter?}]
this._eventBus = new EventEmitter();
this._metrics = { totalExecutions: 0, totalErrors: 0, hookCount: 0 };
this._initialized = false;
}
get initialized() { return this._initialized; }
/** Register a hook handler */
register(type, id, handler, options = {}) {
if (!HOOK_TYPES[type] && !Object.values(HOOK_TYPES).includes(type)) {
throw new Error(`Unknown hook type: '${type}'. Valid: ${Object.values(HOOK_TYPES).join(', ')}`);
}
if (!this._hooks.has(type)) {
this._hooks.set(type, []);
}
const hooks = this._hooks.get(type);
// Check duplicates
if (hooks.some(h => h.id === id)) {
throw new Error(`Hook '${id}' already registered for type '${type}'`);
}
hooks.push({
id,
handler: typeof handler === 'function' ? handler : async () => {},
priority: options.priority || 0,
filter: options.filter || null,
});
// Keep sorted by priority descending
hooks.sort((a, b) => b.priority - a.priority);
this._metrics.hookCount = this._getTotalHookCount();
this._eventBus.emit('hook:registered', { type, id });
}
/** Unregister a hook by id */
unregister(id) {
for (const [type, hooks] of this._hooks) {
const idx = hooks.findIndex(h => h.id === id);
if (idx !== -1) {
hooks.splice(idx, 1);
if (hooks.length === 0) this._hooks.delete(type);
this._metrics.hookCount = this._getTotalHookCount();
this._eventBus.emit('hook:unregistered', { type, id });
return true;
}
}
return false;
}
/** Execute all hooks for a type with context, fault-isolated */
async execute(type, context = {}) {
const hooks = this._hooks.get(type);
if (!hooks || hooks.length === 0) return [];
const results = [];
this._eventBus.emit('hooks:executing', { type });
for (const { id, handler, filter } of hooks) {
// Check filter
if (filter && !this._matchesFilter(filter, context)) continue;
try {
const result = await handler(context);
results.push({ id, result });
this._metrics.totalExecutions++;
} catch (err) {
results.push({ id, error: err.message });
this._metrics.totalErrors++;
this._eventBus.emit('hook:error', { type, id, error: err.message });
// Fault isolation: continue to next handler
}
}
this._eventBus.emit('hooks:executed', { type, count: results.length });
return results;
}
/** Execute hooks as a filter chain — stops & returns false on first failure */
async executeFilter(type, context = {}) {
const hooks = this._hooks.get(type);
if (!hooks) return true;
for (const { id, handler, filter } of hooks) {
if (filter && !this._matchesFilter(filter, context)) continue;
try {
const result = await handler(context);
if (result === false) return false;
} catch (err) {
this._metrics.totalErrors++;
return false;
}
}
return true;
}
/** Get hooks for a type */
getHooks(type) {
const hooks = this._hooks.get(type);
return hooks ? [...hooks] : [];
}
/** Get all registered hook types */
getTypes() {
return [...this._hooks.keys()];
}
/** Get metrics */
getMetrics() {
return {
...this._metrics,
types: this._hooks.size,
byType: Object.fromEntries([...this._hooks.entries()].map(([t, hs]) => [t, hs.length])),
};
}
/** Check if a context matches a filter */
_matchesFilter(filter, context) {
if (!filter) return true;
if (filter.tools && context.toolName && !filter.tools.includes(context.toolName)) return false;
if (filter.events && context.eventName && !filter.events.includes(context.eventName)) return false;
if (filter.chatIds && context.chatId && !filter.chatIds.includes(context.chatId)) return false;
return true;
}
_getTotalHookCount() {
let count = 0;
for (const hooks of this._hooks.values()) count += hooks.length;
return count;
}
}
// Singleton
export const hookManager = new HookManager();
export { HOOK_TYPES, HookManager };
export default hookManager;

View File

@@ -18,6 +18,18 @@ import { createSessionState } from './session-state.js';
import { detectIntent } from './intent-detector.js'; import { detectIntent } from './intent-detector.js';
import { streamChatWithRetry } from './stream-handler.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 ── // ── Pidfile lock: prevent duplicate instances ──
const PIDFILE = path.join(process.env.HOME || '/tmp', '.zcode-bot.pid'); const PIDFILE = path.join(process.env.HOME || '/tmp', '.zcode-bot.pid');
function acquirePidfile() { function acquirePidfile() {
@@ -208,6 +220,64 @@ export async function initBot(config, api, tools, skills, agents) {
toolMap: new Map((tools || []).map(t => [t.name, t])), 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) ── // ── Tool definitions for the AI API (OpenAI function-calling format) ──
// Defined at startBot scope so delegate handler can access them // Defined at startBot scope so delegate handler can access them
const TOOL_DEFS = { const TOOL_DEFS = {
@@ -361,6 +431,40 @@ export async function initBot(config, api, tools, skills, agents) {
input: { type: 'string' }, input: { type: 'string' },
}, required: ['skill'] }, }, 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 ── // ── 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.' }, { role: 'user', content: args.input || 'Please analyze the code and provide your expert review.' },
]; ];
const result = await chatWithAI(skillMessages, { maxTokens: 4096 }); const result = await chatWithAI(skillMessages, { maxTokens: 4096 });
return `📚 **${skill.name}**:\n${result}`; return `📚 **${skill.name}**:\\n${result}`;
} catch (e) { } catch (e) {
return `❌ Skill ${skill.name} error: ${e.message}`; 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 ── // ── Create grammy bot ──
@@ -1207,15 +1361,7 @@ export async function initBot(config, api, tools, skills, agents) {
logger.error('Unhandled rejection:', reason?.message || reason); logger.error('Unhandled rejection:', reason?.message || reason);
}); });
// ── Graceful shutdown: flush conversation history ── // ── Graceful shutdown is defined at end of initBot (requires full `svc`) ──
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'));
acquirePidfile(); 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 { return {
send: (chatId, text) => bot.api.sendMessage(chatId, markdownToHtml(text), { parse_mode: 'HTML' }), send: (chatId, text) => bot.api.sendMessage(chatId, markdownToHtml(text), { parse_mode: 'HTML' }),
ws: (chatId, msg) => wsClients.get(chatId)?.send(JSON.stringify(msg)), ws: (chatId, msg) => wsClients.get(chatId)?.send(JSON.stringify(msg)),
waitForMessages: async () => { await new Promise(() => {}); }, waitForMessages: async () => { await new Promise(() => {}); },
getConnections: () => wsClients.size, 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 }),
}; };
} }

291
src/bot/memory-backend.js Normal file
View File

@@ -0,0 +1,291 @@
/**
* zCode Memory Backend — Enhanced with typed memory entries
* Ported concept from Ruflo's MemoryBackend interface.
*
* Two backends:
* JSONBackend — file-based, LRU (existing MemoryStore)
* InMemoryBackend — RAM-only, for ephemeral agent context
*
* Memory types: lesson, gotcha, pattern, preference, discovery, context
*/
import { logger } from '../utils/logger.js';
import fs from 'fs';
import path from 'path';
const MEMORY_TYPES = {
LESSON: 'lesson',
GOTCHA: 'gotcha',
PATTERN: 'pattern',
PREFERENCE: 'preference',
DISCOVERY: 'discovery',
CONTEXT: 'context',
EPHEMERAL: 'ephemeral',
};
/** Priority for system prompt injection */
const TYPE_PRIORITY = {
gotcha: 5,
lesson: 4,
pattern: 3,
preference: 2,
discovery: 1,
context: 3,
ephemeral: 0,
};
/**
* JSONBackend — File-based persistent memory with LRU eviction
*/
export class JSONBackend {
constructor(filePath, maxEntries = 500) {
this.filePath = path.resolve(filePath);
this.maxEntries = maxEntries;
this._entries = new Map();
this._loaded = false;
this._dirty = false;
this._saveTimer = null;
this._debounceMs = 3000;
}
get loaded() { return this._loaded; }
async initialize() {
try {
if (fs.existsSync(this.filePath)) {
const data = JSON.parse(await fs.promises.readFile(this.filePath, 'utf-8'));
if (Array.isArray(data)) {
for (const entry of data) {
this._entries.set(entry.id || entry.key, entry);
}
} else if (data.entries) {
for (const entry of data.entries) {
this._entries.set(entry.id || entry.key, entry);
}
}
}
this._loaded = true;
logger.info(`✓ Memory: loaded ${this._entries.size} entries from ${path.basename(this.filePath)}`);
} catch (err) {
this._loaded = true;
logger.warn(`⚠ Memory: could not load ${path.basename(this.filePath)}: ${err.message}`);
}
}
async store(memory) {
const key = memory.id || memory.key || `${memory.type}_${Date.now()}`;
const entry = {
...memory,
id: key,
timestamp: memory.timestamp || Date.now(),
};
this._entries.set(key, entry);
this._evictIfNeeded();
this._markDirty();
return entry;
}
async retrieve(id) {
return this._entries.get(id) || null;
}
async query(filter) {
let results = [...this._entries.values()];
if (filter.type) {
results = results.filter(e => e.type === filter.type);
}
if (filter.query) {
const q = filter.query.toLowerCase();
results = results.filter(e =>
(e.content && e.content.toLowerCase().includes(q)) ||
(e.key && e.key.toLowerCase().includes(q)) ||
(e.tags && e.tags.some(t => t.toLowerCase().includes(q)))
);
}
if (filter.agentId) {
results = results.filter(e => e.agentId === filter.agentId);
}
if (filter.timeRange) {
const { start, end } = filter.timeRange;
results = results.filter(e => e.timestamp >= start && e.timestamp <= end);
}
// Sort by recency
results.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0));
if (filter.limit) results = results.slice(0, filter.limit);
if (filter.offset) results = results.slice(filter.offset);
return results;
}
/** Semantic-like search with BM25 scoring */
async search(query, limit = 10) {
const q = query.toLowerCase();
const terms = q.split(/\s+/).filter(Boolean);
if (terms.length === 0) return [];
const scored = [...this._entries.values()].map(e => {
let score = 0;
const content = (e.content || '').toLowerCase();
const key = (e.key || '').toLowerCase();
const tags = (e.tags || []).join(' ').toLowerCase();
for (const term of terms) {
if (key.includes(term)) score += 10;
if (content.includes(term)) score += 3;
if (tags.includes(term)) score += 5;
// TF-like scoring
const tf = (content.match(new RegExp(term, 'g')) || []).length;
score += Math.min(tf, 5);
}
// Priority boost
score += TYPE_PRIORITY[e.type] || 0;
// Recency boost
const age = Date.now() - (e.timestamp || 0);
score += Math.max(0, 1 - age / (30 * 24 * 60 * 60 * 1000)) * 5;
return { entry: e, score };
});
scored.sort((a, b) => b.score - a.score);
return scored.slice(0, limit).map(s => s.entry);
}
async delete(id) {
this._entries.delete(id);
this._markDirty();
}
async clearType(type) {
for (const [key, entry] of this._entries) {
if (entry.type === type) this._entries.delete(key);
}
this._markDirty();
}
async flush() {
if (this._saveTimer) {
clearTimeout(this._saveTimer);
this._saveTimer = null;
}
if (!this._dirty) return;
await this._save();
}
getCount() { return this._entries.size; }
getEntries() { return [...this._entries.values()]; }
getStats() {
const byType = {};
for (const e of this._entries.values()) {
byType[e.type] = (byType[e.type] || 0) + 1;
}
return { total: this._entries.size, byType };
}
_evictIfNeeded() {
if (this._entries.size <= this.maxEntries) return;
const sorted = [...this._entries.values()].sort((a, b) => (a.timestamp || 0) - (b.timestamp || 0));
const toRemove = this._entries.size - this.maxEntries;
for (let i = 0; i < toRemove && i < sorted.length; i++) {
this._entries.delete(sorted[i].id || sorted[i].key);
}
}
_markDirty() {
this._dirty = true;
if (this._saveTimer) clearTimeout(this._saveTimer);
this._saveTimer = setTimeout(() => this._save(), this._debounceMs);
}
async _save() {
try {
const dir = path.dirname(this.filePath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
const data = [...this._entries.values()];
await fs.promises.writeFile(this.filePath, JSON.stringify(data, null, 2), 'utf-8');
this._dirty = false;
} catch (err) {
logger.error(`Memory save failed: ${err.message}`);
}
}
}
/**
* InMemoryBackend — RAM-only, for ephemeral agent context
* Auto-evicts after TTL or max entries
*/
export class InMemoryBackend {
constructor(maxEntries = 200, ttlMs = 30 * 60 * 1000) {
this._entries = new Map();
this.maxEntries = maxEntries;
this.ttlMs = ttlMs;
}
async store(memory) {
const key = memory.id || memory.key || `mem_${Date.now()}`;
const entry = {
...memory,
id: key,
timestamp: memory.timestamp || Date.now(),
_ttl: Date.now() + this.ttlMs,
};
this._entries.set(key, entry);
this._evictIfNeeded();
return entry;
}
async retrieve(id) {
const entry = this._entries.get(id);
if (!entry) return null;
if (Date.now() > entry._ttl) {
this._entries.delete(id);
return null;
}
return entry;
}
async query(filter) {
this._purgeExpired();
let results = [...this._entries.values()];
if (filter.type) results = results.filter(e => e.type === filter.type);
if (filter.query) {
const q = filter.query.toLowerCase();
results = results.filter(e => (e.content || '').toLowerCase().includes(q));
}
results.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0));
if (filter.limit) results = results.slice(0, filter.limit);
return results;
}
getCount() {
this._purgeExpired();
return this._entries.size;
}
_evictIfNeeded() {
if (this._entries.size <= this.maxEntries) return;
const sorted = [...this._entries.values()].sort((a, b) => (a.timestamp || 0) - (b.timestamp || 0));
const toRemove = this._entries.size - this.maxEntries;
for (let i = 0; i < toRemove; i++) {
this._entries.delete(sorted[i].id);
}
}
_purgeExpired() {
const now = Date.now();
for (const [key, entry] of this._entries) {
if (now > entry._ttl) this._entries.delete(key);
}
}
}
export { MEMORY_TYPES };
export default JSONBackend;

View File

@@ -0,0 +1,50 @@
/**
* zCode Extension Points — Ported from Ruflo
* Standard extension point names for plugin hooks
*/
export const EXTENSION_POINTS = {
// Tool lifecycle
TOOL_BEFORE_EXECUTE: 'tool.beforeExecute',
TOOL_AFTER_EXECUTE: 'tool.afterExecute',
TOOL_VALIDATE: 'tool.validate',
// AI lifecycle
AI_BEFORE_CALL: 'ai.beforeCall',
AI_AFTER_CALL: 'ai.afterCall',
AI_ON_ERROR: 'ai.onError',
AI_BEFORE_STREAM: 'ai.beforeStream',
AI_AFTER_STREAM: 'ai.afterStream',
// Agent lifecycle
AGENT_BEFORE_SPAWN: 'agent.beforeSpawn',
AGENT_AFTER_SPAWN: 'agent.afterSpawn',
AGENT_BEFORE_TASK: 'agent.beforeTask',
AGENT_AFTER_TASK: 'agent.afterTask',
// Memory lifecycle
MEMORY_BEFORE_STORE: 'memory.beforeStore',
MEMORY_AFTER_STORE: 'memory.afterStore',
MEMORY_BEFORE_QUERY: 'memory.beforeQuery',
// Session lifecycle
SESSION_START: 'session.start',
SESSION_END: 'session.end',
SESSION_BEFORE_MSG: 'session.beforeMessage',
SESSION_AFTER_MSG: 'session.afterMessage',
// Workflow
WORKFLOW_BEFORE_EXECUTE: 'workflow.beforeExecute',
WORKFLOW_AFTER_EXECUTE: 'workflow.afterExecute',
WORKFLOW_ON_ERROR: 'workflow.onError',
// Swarm
SWARM_BEFORE_COORDINATE: 'swarm.beforeCoordinate',
SWARM_AFTER_COORDINATE: 'swarm.afterCoordinate',
// Plugin lifecycle
PLUGIN_LOADED: 'plugin:loaded',
PLUGIN_UNLOADED: 'plugin:unloaded',
};
export default EXTENSION_POINTS;

115
src/plugins/Plugin.js Normal file
View File

@@ -0,0 +1,115 @@
/**
* zCode Plugin System — Ported from Ruflo: Plugin Interface + BasePlugin
*
* Plugin → Extension Point → Hook → Worker chain architecture.
* Plugins register extension points. HookManager fires at lifecycle events.
*/
/**
* @typedef {Object} PluginMetadata
* @property {string} id
* @property {string} name
* @property {string} version
* @property {string} [description]
* @property {string} [author]
* @property {string} [homepage]
*/
/**
* @typedef {Object} ExtensionPoint
* @property {string} name
* @property {Function} handler
* @property {number} [priority]
*/
/**
* @typedef {Object} PluginConfig
* @property {string} id
* @property {string} name
* @property {string} version
* @property {string} [description]
* @property {string} [author]
* @property {string} [homepage]
* @property {number} [priority]
* @property {string[]} [dependencies]
* @property {Object} [configSchema]
* @property {string} [minCoreVersion]
* @property {string} [maxCoreVersion]
*/
export class BasePlugin {
constructor(config) {
this.id = config.id;
this.name = config.name;
this.version = config.version;
this.description = config.description || '';
this.author = config.author || '';
this.homepage = config.homepage || '';
this.priority = config.priority || 0;
this.dependencies = config.dependencies || [];
this.configSchema = config.configSchema || null;
this.minCoreVersion = config.minCoreVersion || '0.0.0';
this.maxCoreVersion = config.maxCoreVersion || '99.99.99';
this._config = null;
this._extensionPoints = [];
this._initialized = false;
}
async initialize(config = {}) {
this._config = config;
if (this.configSchema) this._validateConfig(config);
await this._onInitialize();
this._initialized = true;
}
async shutdown() {
await this._onShutdown();
this._initialized = false;
this._extensionPoints = [];
}
getExtensionPoints() {
return this._extensionPoints;
}
getMetadata() {
return {
id: this.id,
name: this.name,
version: this.version,
description: this.description,
author: this.author,
homepage: this.homepage
};
}
isInitialized() { return this._initialized; }
/**
* Register an extension point handler
* @param {string} name - Extension point name
* @param {(context: any) => Promise<any>} handler
* @param {number} [priority]
*/
registerExtensionPoint(name, handler, priority = 0) {
this._extensionPoints.push({ name, handler, priority });
// Keep sorted by priority descending
this._extensionPoints.sort((a, b) => (b.priority || 0) - (a.priority || 0));
}
_validateConfig(schema, config) {
// Lightweight required-field validation
if (Array.isArray(schema?.required)) {
for (const field of schema.required) {
if (config[field] === undefined) {
throw new Error(`Plugin ${this.id}: missing required config field '${field}'`);
}
}
}
}
async _onInitialize() { /* override */ }
async _onShutdown() { /* override */ }
}
export { BasePlugin as default };

183
src/plugins/PluginLoader.js Normal file
View File

@@ -0,0 +1,183 @@
/**
* zCode Plugin Loader — Dependency-resolving batch plugin loader.
* Supports topological sort, parallel/sequential init, health checks.
*/
import { PluginManager } from './PluginManager.js';
export class PluginLoader {
constructor(manager, options = {}) {
this._manager = manager;
this._initTimeout = options.initTimeout || 30000;
this._shutdownTimeout = options.shutdownTimeout || 10000;
this._parallelInit = options.parallelInit || false;
this._strictDeps = options.strictDeps !== false;
this._enableHealthChecks = options.enableHealthChecks || false;
this._healthCheckInterval = options.healthCheckInterval || 60000;
this._healthTimers = new Map();
}
/** Load a single plugin with timeout */
async loadPlugin(plugin, config = {}) {
const timer = setTimeout(() => {
throw new Error(`Plugin '${plugin.id}' initialization timed out after ${this._initTimeout}ms`);
}, this._initTimeout);
try {
await this._manager.loadPlugin(plugin, config);
if (this._enableHealthChecks) {
this._startHealthCheck(plugin);
}
} finally {
clearTimeout(timer);
}
}
/** Load multiple plugins with dependency resolution */
async loadPlugins(plugins, configs = {}) {
if (plugins.length === 0) return;
// Validate all first
for (const p of plugins) {
if (!p || !p.id) throw new Error(`Invalid plugin at index ${plugins.indexOf(p)}: missing id`);
}
// Build dependency graph & detect cycles
const graph = this._buildGraph(plugins);
const cycles = this._detectCycles(graph);
if (cycles.length > 0) {
throw new Error(`Circular plugin dependencies detected: ${cycles.map(c => c.join(' -> ')).join(', ')}`);
}
// Topological sort by depth
const layers = this._topologicalSort(graph);
// Load layer by layer
for (const layer of layers) {
if (this._parallelInit && layer.length > 1) {
await Promise.all(layer.map(id => {
const p = plugins.find(pl => pl.id === id);
return this.loadPlugin(p, configs[id] || {});
}));
} else {
for (const id of layer) {
const p = plugins.find(pl => pl.id === id);
await this.loadPlugin(p, configs[id] || {});
}
}
}
}
/** Unload a plugin */
async unloadPlugin(name) {
if (!this._manager.isPluginLoaded(name)) return;
this._stopHealthCheck(name);
await this._manager.unloadPlugin(name);
}
/** Unload all plugins in reverse init order */
async unloadAll() {
const plugins = this._manager.listPlugins();
for (const meta of plugins.reverse()) {
await this.unloadPlugin(meta.id);
}
}
/** Reload a plugin */
async reloadPlugin(name, newPlugin, config = {}) {
await this.unloadPlugin(name);
await this.loadPlugin(newPlugin, config);
}
// ---- Internal ----
_buildGraph(plugins) {
const graph = new Map();
for (const p of plugins) {
graph.set(p.id, [...(p.dependencies || [])]);
}
return graph;
}
_detectCycles(graph) {
const WHITE = 0, GRAY = 1, BLACK = 2;
const color = new Map();
const parent = new Map();
const cycles = [];
for (const node of graph.keys()) color.set(node, WHITE);
function dfs(node, stack) {
color.set(node, GRAY);
stack.push(node);
for (const dep of graph.get(node) || []) {
if (!graph.has(dep)) continue; // external dep, skip
if (color.get(dep) === GRAY) {
const cycle = stack.slice(stack.indexOf(dep)).concat(dep);
cycles.push(cycle);
} else if (color.get(dep) === WHITE) {
dfs(dep, stack);
}
}
stack.pop();
color.set(node, BLACK);
}
for (const node of graph.keys()) {
if (color.get(node) === WHITE) dfs(node, []);
}
return cycles;
}
_topologicalSort(graph) {
// Compute depth for each node
const depth = new Map();
function getDepth(node) {
if (depth.has(node)) return depth.get(node);
let maxDep = 0;
for (const dep of graph.get(node) || []) {
if (graph.has(dep)) { // only internal deps
maxDep = Math.max(maxDep, getDepth(dep) + 1);
}
}
depth.set(node, maxDep);
return maxDep;
}
for (const node of graph.keys()) getDepth(node);
// Group by depth
const maxDepth = Math.max(...depth.values(), 0);
const layers = [];
for (let d = 0; d <= maxDepth; d++) {
const layer = [...graph.keys()].filter(n => depth.get(n) === d);
if (layer.length > 0) layers.push(layer);
}
return layers;
}
_startHealthCheck(plugin) {
if (typeof plugin.healthCheck !== 'function') return;
const timer = setInterval(async () => {
try {
const ok = await plugin.healthCheck();
if (!ok) {
console.warn(`Health check failed for plugin '${plugin.id}'`);
}
} catch (err) {
console.error(`Health check error for plugin '${plugin.id}':`, err.message);
}
}, this._healthCheckInterval);
this._healthTimers.set(plugin.id, timer);
}
_stopHealthCheck(pluginId) {
const timer = this._healthTimers.get(pluginId);
if (timer) {
clearInterval(timer);
this._healthTimers.delete(pluginId);
}
}
}
export default PluginLoader;

View File

@@ -0,0 +1,253 @@
/**
* zCode Plugin Manager — Ported from Ruflo PluginManager + PluginRegistry
* Manages plugin lifecycle: load, unload, reload, extension point invocation.
*/
import { EventEmitter } from 'events';
import { BasePlugin } from './Plugin.js';
import { EXTENSION_POINTS } from './ExtensionPoints.js';
const PLUGIN_STATES = {
UNINITIALIZED: 'uninitialized',
INITIALIZING: 'initializing',
INITIALIZED: 'initialized',
ERROR: 'error',
SHUTTING_DOWN: 'shutting-down',
SHUTDOWN: 'shutdown',
};
export class PluginManager {
constructor(options = {}) {
this._plugins = new Map(); // id -> { plugin, state, meta, metrics }
this._extensionPoints = new Map(); // name -> [{ pluginId, handler, priority }]
this._eventBus = options.eventBus || new EventEmitter();
this._coreVersion = options.coreVersion || '3.0.0';
this._initialized = false;
}
isInitialized() { return this._initialized; }
async initialize() { this._initialized = true; }
async shutdown() {
const ids = [...this._plugins.keys()].reverse();
for (const id of ids) {
await this.unloadPlugin(id);
}
this._extensionPoints.clear();
this._initialized = false;
}
/**
* Load a plugin: validations → init → register extension points → emit
*/
async loadPlugin(plugin, config = {}) {
if (!plugin || !plugin.id) {
throw new Error('Invalid plugin: must have an id');
}
if (this._plugins.has(plugin.id)) {
throw new Error(`Plugin '${plugin.id}' is already loaded`);
}
// Version compatibility
this._checkVersionCompatibility(plugin);
// Dependency check
this._checkDependencies(plugin);
// Validate config schema
if (plugin.configSchema) {
this._validateConfig(plugin.configSchema, config);
}
// Initialize
this._plugins.set(plugin.id, {
plugin,
state: PLUGIN_STATES.INITIALIZING,
meta: plugin.getMetadata(),
metrics: { loadTime: 0, invokeCount: 0, errors: 0 },
});
const startTime = Date.now();
try {
await plugin.initialize(config);
this._plugins.get(plugin.id).state = PLUGIN_STATES.INITIALIZED;
this._plugins.get(plugin.id).metrics.loadTime = Date.now() - startTime;
} catch (err) {
this._plugins.get(plugin.id).state = PLUGIN_STATES.ERROR;
this._plugins.get(plugin.id).metrics.errors++;
throw new Error(`Plugin '${plugin.id}' initialization failed: ${err.message}`);
}
// Register extension points
this._registerExtensionPoints(plugin);
this._eventBus.emit(EXTENSION_POINTS.PLUGIN_LOADED, { pluginId: plugin.id });
}
/**
* Unload a plugin: check dependents → shutdown → cleanup
*/
async unloadPlugin(pluginId) {
const entry = this._plugins.get(pluginId);
if (!entry) return;
this._checkDependents(pluginId);
entry.state = PLUGIN_STATES.SHUTTING_DOWN;
try {
await entry.plugin.shutdown();
} catch (err) {
// Log but continue cleanup
console.error(`Plugin '${pluginId}' shutdown error:`, err.message);
}
entry.state = PLUGIN_STATES.SHUTDOWN;
// Remove extension points registered by this plugin
for (const [, handlers] of this._extensionPoints) {
const idx = handlers.findIndex(h => h.pluginId === pluginId);
if (idx !== -1) handlers.splice(idx, 1);
}
this._plugins.delete(pluginId);
this._eventBus.emit(EXTENSION_POINTS.PLUGIN_UNLOADED, { pluginId });
}
async reloadPlugin(pluginId, newPlugin, config = {}) {
await this.unloadPlugin(pluginId);
await this.loadPlugin(newPlugin, config);
}
/** Invoke all handlers for an extension point, fault-isolated */
async invokeExtensionPoint(name, context = {}) {
const handlers = this._extensionPoints.get(name);
if (!handlers || handlers.length === 0) return [];
// Sort by priority descending
const sorted = [...handlers].sort((a, b) => (b.priority || 0) - (a.priority || 0));
const results = [];
for (const { pluginId, handler } of sorted) {
try {
const result = await handler(context);
results.push({ pluginId, result });
const plugin = this._plugins.get(pluginId);
if (plugin) plugin.metrics.invokeCount++;
} catch (err) {
results.push({ pluginId, error: err.message });
const plugin = this._plugins.get(pluginId);
if (plugin) plugin.metrics.errors++;
// Fault isolation: continue to next handler
}
}
return results;
}
/** Invoke with filtering — only runs if all handlers pass */
async invokeFilterChain(name, context = {}) {
const handlers = this._extensionPoints.get(name);
if (!handlers) return true;
const sorted = [...handlers].sort((a, b) => (b.priority || 0) - (a.priority || 0));
for (const { pluginId, handler } of sorted) {
try {
const result = await handler(context);
if (result === false) return false;
} catch (err) {
const plugin = this._plugins.get(pluginId);
if (plugin) plugin.metrics.errors++;
return false;
}
}
return true;
}
// Queries
getPlugin(id) { return this._plugins.get(id)?.plugin || null; }
getPluginState(id) { return this._plugins.get(id)?.state || null; }
getPluginMeta(id) { return this._plugins.get(id)?.meta || null; }
listPlugins() { return [...this._plugins.values()].map(e => e.meta); }
isPluginLoaded(id) { return this._plugins.has(id); }
getPluginCount() { return this._plugins.size; }
getStatus() {
const byState = {};
for (const [, entry] of this._plugins) {
byState[entry.state] = (byState[entry.state] || 0) + 1;
}
return {
total: this._plugins.size,
extensionPoints: this._extensionPoints.size,
byState,
plugins: [...this._plugins.entries()].map(([id, e]) => ({
id,
state: e.state,
version: e.meta.version,
loadTime: e.metrics.loadTime,
errors: e.metrics.errors,
})),
};
}
// ---- Internal ----
_registerExtensionPoints(plugin) {
const points = plugin.getExtensionPoints();
for (const { name, handler, priority } of points) {
if (!this._extensionPoints.has(name)) {
this._extensionPoints.set(name, []);
}
this._extensionPoints.get(name).push({
pluginId: plugin.id,
handler,
priority: priority || 0,
});
}
}
_checkVersionCompatibility(plugin) {
const toParts = (v) => String(v).split('.').map(Number);
const core = toParts(this._coreVersion);
const minV = toParts(plugin.minCoreVersion || '0.0.0');
const maxV = toParts(plugin.maxCoreVersion || '99.99.99');
const gte = (a, b) => a[0] > b[0] || (a[0] === b[0] && a[1] >= b[1]);
const lte = (a, b) => a[0] < b[0] || (a[0] === b[0] && a[1] <= b[1]);
if (!gte(core, minV) || !lte(core, maxV)) {
throw new Error(
`Plugin '${plugin.id}' requires core version ${minV.join('.')}-${maxV.join('.')}, current ${core.join('.')}`
);
}
}
_checkDependencies(plugin) {
if (!plugin.dependencies?.length) return;
for (const depId of plugin.dependencies) {
if (!this._plugins.has(depId)) {
throw new Error(`Plugin '${plugin.id}' depends on '${depId}' which is not loaded`);
}
}
}
_checkDependents(pluginId) {
for (const [, entry] of this._plugins) {
if (entry.plugin.dependencies?.includes(pluginId)) {
throw new Error(`Cannot unload '${pluginId}': '${entry.plugin.id}' depends on it`);
}
}
}
_validateConfig(schema, config) {
if (!schema?.required) return;
for (const field of schema.required) {
if (config[field] === undefined) {
throw new Error(`Missing required config field '${field}'`);
}
}
}
}
export { PLUGIN_STATES };
export default PluginManager;