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

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;