diff --git a/crates/ruvector-cli/sql/hooks_schema.sql b/crates/ruvector-cli/sql/hooks_schema.sql new file mode 100644 index 000000000..cdb8421d4 --- /dev/null +++ b/crates/ruvector-cli/sql/hooks_schema.sql @@ -0,0 +1,380 @@ +-- RuVector Hooks Intelligence Schema +-- PostgreSQL schema for self-learning hooks with pgvector support +-- Requires: ruvector extension (CREATE EXTENSION ruvector CASCADE) + +-- ============================================================================ +-- Q-Learning Patterns Table +-- Stores state-action pairs with Q-values for agent routing decisions +-- ============================================================================ +CREATE TABLE IF NOT EXISTS ruvector_hooks_patterns ( + id SERIAL PRIMARY KEY, + state TEXT NOT NULL, + action TEXT NOT NULL, + q_value REAL DEFAULT 0.0, + visits INTEGER DEFAULT 0, + last_update TIMESTAMPTZ DEFAULT NOW(), + UNIQUE(state, action) +); + +CREATE INDEX IF NOT EXISTS idx_patterns_state ON ruvector_hooks_patterns(state); +CREATE INDEX IF NOT EXISTS idx_patterns_q_value ON ruvector_hooks_patterns(q_value DESC); + +-- ============================================================================ +-- Vector Memory Table +-- Semantic memory with pgvector embeddings for context retrieval +-- ============================================================================ +CREATE TABLE IF NOT EXISTS ruvector_hooks_memories ( + id SERIAL PRIMARY KEY, + memory_type TEXT NOT NULL, + content TEXT NOT NULL, + embedding ruvector, -- Uses native ruvector type + metadata JSONB DEFAULT '{}', + created_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_memories_type ON ruvector_hooks_memories(memory_type); +CREATE INDEX IF NOT EXISTS idx_memories_created ON ruvector_hooks_memories(created_at DESC); +-- Note: HNSW index on embedding created after extension is ready +-- CREATE INDEX IF NOT EXISTS idx_memories_embedding ON ruvector_hooks_memories +-- USING hnsw (embedding ruvector_cosine_ops) WITH (m = 16, ef_construction = 64); + +-- ============================================================================ +-- Learning Trajectories Table +-- Records of state-action-reward sequences for reinforcement learning +-- ============================================================================ +CREATE TABLE IF NOT EXISTS ruvector_hooks_trajectories ( + id SERIAL PRIMARY KEY, + state TEXT NOT NULL, + action TEXT NOT NULL, + outcome TEXT, + reward REAL DEFAULT 0.0, + context JSONB DEFAULT '{}', + created_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_trajectories_state ON ruvector_hooks_trajectories(state); +CREATE INDEX IF NOT EXISTS idx_trajectories_reward ON ruvector_hooks_trajectories(reward DESC); +CREATE INDEX IF NOT EXISTS idx_trajectories_created ON ruvector_hooks_trajectories(created_at DESC); + +-- ============================================================================ +-- Error Patterns Table +-- Learned error patterns with suggested fixes +-- ============================================================================ +CREATE TABLE IF NOT EXISTS ruvector_hooks_errors ( + id SERIAL PRIMARY KEY, + code TEXT NOT NULL UNIQUE, + error_type TEXT NOT NULL, + message TEXT, + fixes TEXT[] DEFAULT '{}', + occurrences INTEGER DEFAULT 1, + last_seen TIMESTAMPTZ DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_errors_code ON ruvector_hooks_errors(code); +CREATE INDEX IF NOT EXISTS idx_errors_type ON ruvector_hooks_errors(error_type); + +-- ============================================================================ +-- File Sequences Table +-- Tracks file edit sequences for predicting next files +-- ============================================================================ +CREATE TABLE IF NOT EXISTS ruvector_hooks_file_sequences ( + id SERIAL PRIMARY KEY, + from_file TEXT NOT NULL, + to_file TEXT NOT NULL, + count INTEGER DEFAULT 1, + last_seen TIMESTAMPTZ DEFAULT NOW(), + UNIQUE(from_file, to_file) +); + +CREATE INDEX IF NOT EXISTS idx_sequences_from ON ruvector_hooks_file_sequences(from_file); +CREATE INDEX IF NOT EXISTS idx_sequences_count ON ruvector_hooks_file_sequences(count DESC); + +-- ============================================================================ +-- Swarm Agents Table +-- Registered agents in the swarm with performance metrics +-- ============================================================================ +CREATE TABLE IF NOT EXISTS ruvector_hooks_swarm_agents ( + id TEXT PRIMARY KEY, + agent_type TEXT NOT NULL, + capabilities TEXT[] DEFAULT '{}', + success_rate REAL DEFAULT 1.0, + task_count INTEGER DEFAULT 0, + status TEXT DEFAULT 'active', + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_agents_type ON ruvector_hooks_swarm_agents(agent_type); +CREATE INDEX IF NOT EXISTS idx_agents_status ON ruvector_hooks_swarm_agents(status); + +-- ============================================================================ +-- Swarm Edges Table +-- Coordination edges between agents +-- ============================================================================ +CREATE TABLE IF NOT EXISTS ruvector_hooks_swarm_edges ( + id SERIAL PRIMARY KEY, + source_agent TEXT NOT NULL REFERENCES ruvector_hooks_swarm_agents(id) ON DELETE CASCADE, + target_agent TEXT NOT NULL REFERENCES ruvector_hooks_swarm_agents(id) ON DELETE CASCADE, + weight REAL DEFAULT 1.0, + coordination_count INTEGER DEFAULT 1, + last_coordination TIMESTAMPTZ DEFAULT NOW(), + UNIQUE(source_agent, target_agent) +); + +CREATE INDEX IF NOT EXISTS idx_edges_source ON ruvector_hooks_swarm_edges(source_agent); +CREATE INDEX IF NOT EXISTS idx_edges_target ON ruvector_hooks_swarm_edges(target_agent); + +-- ============================================================================ +-- Session Stats Table +-- Global statistics for the intelligence layer +-- ============================================================================ +CREATE TABLE IF NOT EXISTS ruvector_hooks_stats ( + id INTEGER PRIMARY KEY DEFAULT 1, + session_count INTEGER DEFAULT 0, + last_session TIMESTAMPTZ DEFAULT NOW(), + total_edits INTEGER DEFAULT 0, + total_commands INTEGER DEFAULT 0, + total_errors_learned INTEGER DEFAULT 0, + CHECK (id = 1) -- Single row table +); + +INSERT INTO ruvector_hooks_stats (id) VALUES (1) ON CONFLICT (id) DO NOTHING; + +-- ============================================================================ +-- Helper Functions +-- ============================================================================ + +-- Update Q-value using Q-learning formula +CREATE OR REPLACE FUNCTION ruvector_hooks_update_q( + p_state TEXT, + p_action TEXT, + p_reward REAL, + p_alpha REAL DEFAULT 0.1 +) RETURNS VOID AS $$ +BEGIN + INSERT INTO ruvector_hooks_patterns (state, action, q_value, visits, last_update) + VALUES (p_state, p_action, p_reward * p_alpha, 1, NOW()) + ON CONFLICT (state, action) DO UPDATE SET + q_value = ruvector_hooks_patterns.q_value + p_alpha * (p_reward - ruvector_hooks_patterns.q_value), + visits = ruvector_hooks_patterns.visits + 1, + last_update = NOW(); +END; +$$ LANGUAGE plpgsql; + +-- Get best action for state +CREATE OR REPLACE FUNCTION ruvector_hooks_best_action( + p_state TEXT, + p_actions TEXT[] +) RETURNS TABLE(action TEXT, q_value REAL, confidence REAL) AS $$ +BEGIN + RETURN QUERY + SELECT + p.action, + p.q_value, + CASE WHEN p.q_value > 0 THEN LEAST(p.q_value, 1.0) ELSE 0.0 END as confidence + FROM ruvector_hooks_patterns p + WHERE p.state = p_state + AND p.action = ANY(p_actions) + ORDER BY p.q_value DESC + LIMIT 1; + + -- If no match found, return first action with 0 confidence + IF NOT FOUND THEN + RETURN QUERY SELECT p_actions[1], 0.0::REAL, 0.0::REAL; + END IF; +END; +$$ LANGUAGE plpgsql; + +-- Remember content with embedding +CREATE OR REPLACE FUNCTION ruvector_hooks_remember( + p_type TEXT, + p_content TEXT, + p_embedding REAL[] DEFAULT NULL, + p_metadata JSONB DEFAULT '{}' +) RETURNS INTEGER AS $$ +DECLARE + v_id INTEGER; +BEGIN + INSERT INTO ruvector_hooks_memories (memory_type, content, embedding, metadata) + VALUES (p_type, p_content, + CASE WHEN p_embedding IS NOT NULL THEN p_embedding::TEXT::ruvector ELSE NULL END, + p_metadata) + RETURNING id INTO v_id; + + -- Cleanup old memories (keep last 5000) + DELETE FROM ruvector_hooks_memories + WHERE id IN ( + SELECT id FROM ruvector_hooks_memories + ORDER BY created_at ASC + OFFSET 5000 + ); + + RETURN v_id; +END; +$$ LANGUAGE plpgsql; + +-- Recall from memory using semantic search +CREATE OR REPLACE FUNCTION ruvector_hooks_recall( + p_query_embedding REAL[], + p_limit INTEGER DEFAULT 5 +) RETURNS TABLE( + id INTEGER, + memory_type TEXT, + content TEXT, + metadata JSONB, + similarity REAL +) AS $$ +BEGIN + RETURN QUERY + SELECT + m.id, + m.memory_type, + m.content, + m.metadata, + 1.0 - (m.embedding <=> p_query_embedding::TEXT::ruvector) as similarity + FROM ruvector_hooks_memories m + WHERE m.embedding IS NOT NULL + ORDER BY m.embedding <=> p_query_embedding::TEXT::ruvector + LIMIT p_limit; +END; +$$ LANGUAGE plpgsql; + +-- Record file sequence +CREATE OR REPLACE FUNCTION ruvector_hooks_record_sequence( + p_from_file TEXT, + p_to_file TEXT +) RETURNS VOID AS $$ +BEGIN + INSERT INTO ruvector_hooks_file_sequences (from_file, to_file, count, last_seen) + VALUES (p_from_file, p_to_file, 1, NOW()) + ON CONFLICT (from_file, to_file) DO UPDATE SET + count = ruvector_hooks_file_sequences.count + 1, + last_seen = NOW(); +END; +$$ LANGUAGE plpgsql; + +-- Get suggested next files +CREATE OR REPLACE FUNCTION ruvector_hooks_suggest_next( + p_file TEXT, + p_limit INTEGER DEFAULT 3 +) RETURNS TABLE(to_file TEXT, count INTEGER) AS $$ +BEGIN + RETURN QUERY + SELECT fs.to_file, fs.count + FROM ruvector_hooks_file_sequences fs + WHERE fs.from_file = p_file + ORDER BY fs.count DESC + LIMIT p_limit; +END; +$$ LANGUAGE plpgsql; + +-- Record error pattern +CREATE OR REPLACE FUNCTION ruvector_hooks_record_error( + p_code TEXT, + p_type TEXT, + p_message TEXT DEFAULT NULL +) RETURNS VOID AS $$ +BEGIN + INSERT INTO ruvector_hooks_errors (code, error_type, message, occurrences, last_seen) + VALUES (p_code, p_type, p_message, 1, NOW()) + ON CONFLICT (code) DO UPDATE SET + occurrences = ruvector_hooks_errors.occurrences + 1, + last_seen = NOW(), + message = COALESCE(p_message, ruvector_hooks_errors.message); +END; +$$ LANGUAGE plpgsql; + +-- Register swarm agent +CREATE OR REPLACE FUNCTION ruvector_hooks_swarm_register( + p_id TEXT, + p_type TEXT, + p_capabilities TEXT[] DEFAULT '{}' +) RETURNS VOID AS $$ +BEGIN + INSERT INTO ruvector_hooks_swarm_agents (id, agent_type, capabilities) + VALUES (p_id, p_type, p_capabilities) + ON CONFLICT (id) DO UPDATE SET + agent_type = p_type, + capabilities = p_capabilities, + updated_at = NOW(); +END; +$$ LANGUAGE plpgsql; + +-- Record swarm coordination +CREATE OR REPLACE FUNCTION ruvector_hooks_swarm_coordinate( + p_source TEXT, + p_target TEXT, + p_weight REAL DEFAULT 1.0 +) RETURNS VOID AS $$ +BEGIN + INSERT INTO ruvector_hooks_swarm_edges (source_agent, target_agent, weight, coordination_count) + VALUES (p_source, p_target, p_weight, 1) + ON CONFLICT (source_agent, target_agent) DO UPDATE SET + weight = (ruvector_hooks_swarm_edges.weight + p_weight) / 2, + coordination_count = ruvector_hooks_swarm_edges.coordination_count + 1, + last_coordination = NOW(); +END; +$$ LANGUAGE plpgsql; + +-- Get swarm stats +CREATE OR REPLACE FUNCTION ruvector_hooks_swarm_stats() +RETURNS TABLE( + agent_count INTEGER, + edge_count INTEGER, + avg_success_rate REAL +) AS $$ +BEGIN + RETURN QUERY + SELECT + (SELECT COUNT(*)::INTEGER FROM ruvector_hooks_swarm_agents WHERE status = 'active'), + (SELECT COUNT(*)::INTEGER FROM ruvector_hooks_swarm_edges), + (SELECT COALESCE(AVG(success_rate), 0.0)::REAL FROM ruvector_hooks_swarm_agents WHERE status = 'active'); +END; +$$ LANGUAGE plpgsql; + +-- Increment session count +CREATE OR REPLACE FUNCTION ruvector_hooks_session_start() +RETURNS VOID AS $$ +BEGIN + UPDATE ruvector_hooks_stats + SET session_count = session_count + 1, + last_session = NOW() + WHERE id = 1; +END; +$$ LANGUAGE plpgsql; + +-- Get full stats +CREATE OR REPLACE FUNCTION ruvector_hooks_get_stats() +RETURNS TABLE( + patterns INTEGER, + memories INTEGER, + trajectories INTEGER, + errors INTEGER, + sessions INTEGER, + agents INTEGER, + edges INTEGER +) AS $$ +BEGIN + RETURN QUERY + SELECT + (SELECT COUNT(*)::INTEGER FROM ruvector_hooks_patterns), + (SELECT COUNT(*)::INTEGER FROM ruvector_hooks_memories), + (SELECT COUNT(*)::INTEGER FROM ruvector_hooks_trajectories), + (SELECT COUNT(*)::INTEGER FROM ruvector_hooks_errors), + (SELECT session_count FROM ruvector_hooks_stats WHERE id = 1), + (SELECT COUNT(*)::INTEGER FROM ruvector_hooks_swarm_agents), + (SELECT COUNT(*)::INTEGER FROM ruvector_hooks_swarm_edges); +END; +$$ LANGUAGE plpgsql; + +-- ============================================================================ +-- Comments +-- ============================================================================ +COMMENT ON TABLE ruvector_hooks_patterns IS 'Q-learning patterns for agent routing decisions'; +COMMENT ON TABLE ruvector_hooks_memories IS 'Semantic memory with vector embeddings'; +COMMENT ON TABLE ruvector_hooks_trajectories IS 'Reinforcement learning trajectories'; +COMMENT ON TABLE ruvector_hooks_errors IS 'Learned error patterns and fixes'; +COMMENT ON TABLE ruvector_hooks_file_sequences IS 'File edit sequence predictions'; +COMMENT ON TABLE ruvector_hooks_swarm_agents IS 'Registered swarm agents'; +COMMENT ON TABLE ruvector_hooks_swarm_edges IS 'Agent coordination graph'; +COMMENT ON TABLE ruvector_hooks_stats IS 'Global intelligence statistics'; diff --git a/npm/packages/cli/package.json b/npm/packages/cli/package.json index ae2fa8bc2..20c2a7389 100644 --- a/npm/packages/cli/package.json +++ b/npm/packages/cli/package.json @@ -35,8 +35,12 @@ "dependencies": { "commander": "^12.0.0" }, + "optionalDependencies": { + "pg": "^8.11.0" + }, "devDependencies": { "@types/node": "^20.0.0", + "@types/pg": "^8.11.0", "typescript": "^5.0.0" } } diff --git a/npm/packages/cli/src/cli.ts b/npm/packages/cli/src/cli.ts index 183cda3e7..e5fc09b00 100644 --- a/npm/packages/cli/src/cli.ts +++ b/npm/packages/cli/src/cli.ts @@ -3,15 +3,16 @@ * RuVector CLI - Command-line interface for RuVector vector database * * This CLI provides access to hooks, memory, learning, and swarm commands. + * Supports PostgreSQL storage (preferred) with JSON fallback. + * + * Set RUVECTOR_POSTGRES_URL or DATABASE_URL for PostgreSQL support. */ import { program } from 'commander'; import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; - -// Intelligence data path -const INTEL_PATH = path.join(os.homedir(), '.ruvector', 'intelligence.json'); +import { createStorageSync, StorageBackend, JsonStorage } from './storage.js'; interface QPattern { state: string; diff --git a/npm/packages/cli/src/storage.ts b/npm/packages/cli/src/storage.ts new file mode 100644 index 000000000..f2738e26a --- /dev/null +++ b/npm/packages/cli/src/storage.ts @@ -0,0 +1,731 @@ +/** + * RuVector Hooks Storage Layer + * + * Supports PostgreSQL (preferred) with JSON fallback + * Uses ruvector extension for vector operations and pgvector-compatible storage + */ + +import * as fs from 'fs'; +import * as path from 'path'; +import * as os from 'os'; + +// Storage interface for hooks intelligence data +export interface QPattern { + state: string; + action: string; + q_value: number; + visits: number; + last_update: number; +} + +export interface MemoryEntry { + id: string; + memory_type: string; + content: string; + embedding: number[]; + metadata: Record; + timestamp: number; +} + +export interface Trajectory { + id: string; + state: string; + action: string; + outcome: string; + reward: number; + timestamp: number; +} + +export interface ErrorPattern { + code: string; + error_type: string; + message: string; + fixes: string[]; + occurrences: number; +} + +export interface SwarmAgent { + id: string; + agent_type: string; + capabilities: string[]; + success_rate: number; + task_count: number; + status: string; +} + +export interface SwarmEdge { + source: string; + target: string; + weight: number; + coordination_count: number; +} + +export interface IntelligenceStats { + total_patterns: number; + total_memories: number; + total_trajectories: number; + total_errors: number; + session_count: number; + last_session: number; +} + +export interface StorageBackend { + // Core operations + connect(): Promise; + disconnect(): Promise; + isConnected(): boolean; + + // Q-Learning + updateQ(state: string, action: string, reward: number): Promise; + getQ(state: string, action: string): Promise; + getBestAction(state: string, actions: string[]): Promise<{ action: string; confidence: number }>; + + // Memory + remember(type: string, content: string, embedding: number[], metadata: Record): Promise; + recall(queryEmbedding: number[], topK: number): Promise; + + // Trajectories + recordTrajectory(state: string, action: string, outcome: string, reward: number): Promise; + + // Errors + recordError(code: string, errorType: string, message: string): Promise; + getErrorFixes(code: string): Promise; + + // File sequences + recordSequence(fromFile: string, toFile: string): Promise; + getNextFiles(file: string, limit: number): Promise>; + + // Swarm + registerAgent(id: string, type: string, capabilities: string[]): Promise; + coordinateAgents(source: string, target: string, weight: number): Promise; + getSwarmStats(): Promise<{ agents: number; edges: number; avgSuccess: number }>; + + // Stats + sessionStart(): Promise; + getStats(): Promise; +} + +// ============================================================================ +// JSON Storage Backend (Fallback) +// ============================================================================ + +const JSON_PATH = path.join(os.homedir(), '.ruvector', 'intelligence.json'); + +interface JsonData { + patterns: Record; + memories: MemoryEntry[]; + trajectories: Trajectory[]; + errors: Record; + file_sequences: Array<{ from_file: string; to_file: string; count: number }>; + agents: Record; + edges: SwarmEdge[]; + stats: IntelligenceStats; +} + +export class JsonStorage implements StorageBackend { + private data: JsonData; + private alpha = 0.1; + + constructor() { + this.data = this.load(); + } + + private load(): JsonData { + try { + if (fs.existsSync(JSON_PATH)) { + return JSON.parse(fs.readFileSync(JSON_PATH, 'utf-8')); + } + } catch {} + return { + patterns: {}, + memories: [], + trajectories: [], + errors: {}, + file_sequences: [], + agents: {}, + edges: [], + stats: { + total_patterns: 0, + total_memories: 0, + total_trajectories: 0, + total_errors: 0, + session_count: 0, + last_session: 0 + } + }; + } + + private save(): void { + const dir = path.dirname(JSON_PATH); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + fs.writeFileSync(JSON_PATH, JSON.stringify(this.data, null, 2)); + } + + private now(): number { + return Math.floor(Date.now() / 1000); + } + + async connect(): Promise { + // JSON storage is always available + } + + async disconnect(): Promise { + this.save(); + } + + isConnected(): boolean { + return true; + } + + async updateQ(state: string, action: string, reward: number): Promise { + const key = `${state}|${action}`; + if (!this.data.patterns[key]) { + this.data.patterns[key] = { state, action, q_value: 0, visits: 0, last_update: 0 }; + } + const p = this.data.patterns[key]; + p.q_value = p.q_value + this.alpha * (reward - p.q_value); + p.visits++; + p.last_update = this.now(); + this.data.stats.total_patterns = Object.keys(this.data.patterns).length; + this.save(); + } + + async getQ(state: string, action: string): Promise { + const key = `${state}|${action}`; + return this.data.patterns[key]?.q_value ?? 0; + } + + async getBestAction(state: string, actions: string[]): Promise<{ action: string; confidence: number }> { + let bestAction = actions[0] ?? ''; + let bestQ = -Infinity; + for (const action of actions) { + const q = await this.getQ(state, action); + if (q > bestQ) { + bestQ = q; + bestAction = action; + } + } + return { action: bestAction, confidence: bestQ > 0 ? Math.min(bestQ, 1) : 0 }; + } + + async remember(type: string, content: string, embedding: number[], metadata: Record): Promise { + const id = `mem_${this.now()}`; + this.data.memories.push({ + id, + memory_type: type, + content, + embedding, + metadata, + timestamp: this.now() + }); + if (this.data.memories.length > 5000) { + this.data.memories.splice(0, 1000); + } + this.data.stats.total_memories = this.data.memories.length; + this.save(); + return id; + } + + async recall(queryEmbedding: number[], topK: number): Promise { + const similarity = (a: number[], b: number[]): number => { + if (a.length !== b.length) return 0; + const dot = a.reduce((sum, v, i) => sum + v * b[i], 0); + const normA = Math.sqrt(a.reduce((sum, v) => sum + v * v, 0)); + const normB = Math.sqrt(b.reduce((sum, v) => sum + v * v, 0)); + return normA > 0 && normB > 0 ? dot / (normA * normB) : 0; + }; + + return this.data.memories + .filter(m => m.embedding && m.embedding.length > 0) + .map(m => ({ score: similarity(queryEmbedding, m.embedding), memory: m })) + .sort((a, b) => b.score - a.score) + .slice(0, topK) + .map(r => r.memory); + } + + async recordTrajectory(state: string, action: string, outcome: string, reward: number): Promise { + const id = `traj_${this.now()}`; + await this.updateQ(state, action, reward); + this.data.trajectories.push({ id, state, action, outcome, reward, timestamp: this.now() }); + if (this.data.trajectories.length > 1000) { + this.data.trajectories.splice(0, 200); + } + this.data.stats.total_trajectories = this.data.trajectories.length; + this.save(); + return id; + } + + async recordError(code: string, errorType: string, message: string): Promise { + if (!this.data.errors[code]) { + this.data.errors[code] = { code, error_type: errorType, message, fixes: [], occurrences: 0 }; + } + this.data.errors[code].occurrences++; + this.data.stats.total_errors = Object.keys(this.data.errors).length; + this.save(); + } + + async getErrorFixes(code: string): Promise { + return this.data.errors[code] ?? null; + } + + async recordSequence(fromFile: string, toFile: string): Promise { + const existing = this.data.file_sequences.find( + s => s.from_file === fromFile && s.to_file === toFile + ); + if (existing) { + existing.count++; + } else { + this.data.file_sequences.push({ from_file: fromFile, to_file: toFile, count: 1 }); + } + this.save(); + } + + async getNextFiles(file: string, limit: number): Promise> { + return this.data.file_sequences + .filter(s => s.from_file === file) + .sort((a, b) => b.count - a.count) + .slice(0, limit) + .map(s => ({ file: s.to_file, count: s.count })); + } + + async registerAgent(id: string, type: string, capabilities: string[]): Promise { + this.data.agents[id] = { + id, + agent_type: type, + capabilities, + success_rate: 1.0, + task_count: 0, + status: 'active' + }; + this.save(); + } + + async coordinateAgents(source: string, target: string, weight: number): Promise { + const existing = this.data.edges.find(e => e.source === source && e.target === target); + if (existing) { + existing.weight = (existing.weight + weight) / 2; + existing.coordination_count++; + } else { + this.data.edges.push({ source, target, weight, coordination_count: 1 }); + } + this.save(); + } + + async getSwarmStats(): Promise<{ agents: number; edges: number; avgSuccess: number }> { + const agents = Object.keys(this.data.agents).length; + const edges = this.data.edges.length; + const avgSuccess = agents > 0 + ? Object.values(this.data.agents).reduce((sum, a) => sum + a.success_rate, 0) / agents + : 0; + return { agents, edges, avgSuccess }; + } + + async sessionStart(): Promise { + this.data.stats.session_count++; + this.data.stats.last_session = this.now(); + this.save(); + } + + async getStats(): Promise { + return this.data.stats; + } +} + +// ============================================================================ +// PostgreSQL Storage Backend +// ============================================================================ + +export class PostgresStorage implements StorageBackend { + private pool: any = null; + private connectionString: string; + private connected = false; + + constructor(connectionString?: string) { + this.connectionString = connectionString || + process.env.RUVECTOR_POSTGRES_URL || + process.env.DATABASE_URL || + 'postgresql://localhost:5432/ruvector'; + } + + async connect(): Promise { + try { + // Dynamic import of pg to avoid bundling issues + const pg = await import('pg'); + this.pool = new pg.Pool({ connectionString: this.connectionString }); + + // Test connection + const client = await this.pool.connect(); + await client.query('SELECT 1'); + client.release(); + + // Initialize schema + await this.initSchema(); + this.connected = true; + } catch (err) { + this.connected = false; + throw err; + } + } + + async disconnect(): Promise { + if (this.pool) { + await this.pool.end(); + this.pool = null; + this.connected = false; + } + } + + isConnected(): boolean { + return this.connected && this.pool !== null; + } + + private async query(sql: string, params?: any[]): Promise { + if (!this.pool) throw new Error('Not connected'); + const result = await this.pool.query(sql, params); + return result.rows; + } + + private async initSchema(): Promise { + // Create tables if they don't exist + await this.query(` + CREATE TABLE IF NOT EXISTS ruvector_hooks_patterns ( + id SERIAL PRIMARY KEY, + state TEXT NOT NULL, + action TEXT NOT NULL, + q_value REAL DEFAULT 0.0, + visits INTEGER DEFAULT 0, + last_update TIMESTAMPTZ DEFAULT NOW(), + UNIQUE(state, action) + ) + `); + + await this.query(` + CREATE TABLE IF NOT EXISTS ruvector_hooks_memories ( + id SERIAL PRIMARY KEY, + memory_type TEXT NOT NULL, + content TEXT NOT NULL, + embedding REAL[], + metadata JSONB DEFAULT '{}', + created_at TIMESTAMPTZ DEFAULT NOW() + ) + `); + + await this.query(` + CREATE TABLE IF NOT EXISTS ruvector_hooks_trajectories ( + id SERIAL PRIMARY KEY, + state TEXT NOT NULL, + action TEXT NOT NULL, + outcome TEXT, + reward REAL DEFAULT 0.0, + created_at TIMESTAMPTZ DEFAULT NOW() + ) + `); + + await this.query(` + CREATE TABLE IF NOT EXISTS ruvector_hooks_errors ( + id SERIAL PRIMARY KEY, + code TEXT NOT NULL UNIQUE, + error_type TEXT NOT NULL, + message TEXT, + fixes TEXT[] DEFAULT '{}', + occurrences INTEGER DEFAULT 1 + ) + `); + + await this.query(` + CREATE TABLE IF NOT EXISTS ruvector_hooks_file_sequences ( + id SERIAL PRIMARY KEY, + from_file TEXT NOT NULL, + to_file TEXT NOT NULL, + count INTEGER DEFAULT 1, + UNIQUE(from_file, to_file) + ) + `); + + await this.query(` + CREATE TABLE IF NOT EXISTS ruvector_hooks_swarm_agents ( + id TEXT PRIMARY KEY, + agent_type TEXT NOT NULL, + capabilities TEXT[] DEFAULT '{}', + success_rate REAL DEFAULT 1.0, + task_count INTEGER DEFAULT 0, + status TEXT DEFAULT 'active' + ) + `); + + await this.query(` + CREATE TABLE IF NOT EXISTS ruvector_hooks_swarm_edges ( + id SERIAL PRIMARY KEY, + source_agent TEXT NOT NULL, + target_agent TEXT NOT NULL, + weight REAL DEFAULT 1.0, + coordination_count INTEGER DEFAULT 1, + UNIQUE(source_agent, target_agent) + ) + `); + + await this.query(` + CREATE TABLE IF NOT EXISTS ruvector_hooks_stats ( + id INTEGER PRIMARY KEY DEFAULT 1, + session_count INTEGER DEFAULT 0, + last_session TIMESTAMPTZ DEFAULT NOW(), + CHECK (id = 1) + ) + `); + + await this.query(` + INSERT INTO ruvector_hooks_stats (id) VALUES (1) ON CONFLICT (id) DO NOTHING + `); + } + + async updateQ(state: string, action: string, reward: number): Promise { + await this.query(` + INSERT INTO ruvector_hooks_patterns (state, action, q_value, visits, last_update) + VALUES ($1, $2, $3 * 0.1, 1, NOW()) + ON CONFLICT (state, action) DO UPDATE SET + q_value = ruvector_hooks_patterns.q_value + 0.1 * ($3 - ruvector_hooks_patterns.q_value), + visits = ruvector_hooks_patterns.visits + 1, + last_update = NOW() + `, [state, action, reward]); + } + + async getQ(state: string, action: string): Promise { + const rows = await this.query<{ q_value: number }>( + 'SELECT q_value FROM ruvector_hooks_patterns WHERE state = $1 AND action = $2', + [state, action] + ); + return rows[0]?.q_value ?? 0; + } + + async getBestAction(state: string, actions: string[]): Promise<{ action: string; confidence: number }> { + const rows = await this.query<{ action: string; q_value: number }>( + `SELECT action, q_value FROM ruvector_hooks_patterns + WHERE state = $1 AND action = ANY($2) + ORDER BY q_value DESC LIMIT 1`, + [state, actions] + ); + + if (rows.length > 0) { + const q = rows[0].q_value; + return { action: rows[0].action, confidence: q > 0 ? Math.min(q, 1) : 0 }; + } + return { action: actions[0] ?? '', confidence: 0 }; + } + + async remember(type: string, content: string, embedding: number[], metadata: Record): Promise { + const rows = await this.query<{ id: number }>( + `INSERT INTO ruvector_hooks_memories (memory_type, content, embedding, metadata) + VALUES ($1, $2, $3, $4) RETURNING id`, + [type, content, embedding, JSON.stringify(metadata)] + ); + + // Cleanup old memories + await this.query(` + DELETE FROM ruvector_hooks_memories WHERE id IN ( + SELECT id FROM ruvector_hooks_memories ORDER BY created_at ASC OFFSET 5000 + ) + `); + + return `mem_${rows[0].id}`; + } + + async recall(queryEmbedding: number[], topK: number): Promise { + // Use cosine similarity via array operations + // Note: For optimal performance, use pgvector extension with <=> operator + const rows = await this.query<{ + id: number; + memory_type: string; + content: string; + embedding: number[]; + metadata: any; + created_at: Date; + }>(` + SELECT id, memory_type, content, embedding, metadata, + EXTRACT(EPOCH FROM created_at)::BIGINT as timestamp + FROM ruvector_hooks_memories + WHERE embedding IS NOT NULL + ORDER BY created_at DESC + LIMIT $1 + `, [topK * 10]); + + // Client-side similarity ranking (for optimal: use pgvector) + const similarity = (a: number[], b: number[]): number => { + if (!a || !b || a.length !== b.length) return 0; + const dot = a.reduce((sum, v, i) => sum + v * b[i], 0); + const normA = Math.sqrt(a.reduce((sum, v) => sum + v * v, 0)); + const normB = Math.sqrt(b.reduce((sum, v) => sum + v * v, 0)); + return normA > 0 && normB > 0 ? dot / (normA * normB) : 0; + }; + + return rows + .map(r => ({ + score: similarity(queryEmbedding, r.embedding), + entry: { + id: `mem_${r.id}`, + memory_type: r.memory_type, + content: r.content, + embedding: r.embedding, + metadata: r.metadata || {}, + timestamp: Math.floor(new Date(r.created_at).getTime() / 1000) + } + })) + .sort((a, b) => b.score - a.score) + .slice(0, topK) + .map(r => r.entry); + } + + async recordTrajectory(state: string, action: string, outcome: string, reward: number): Promise { + await this.updateQ(state, action, reward); + + const rows = await this.query<{ id: number }>( + `INSERT INTO ruvector_hooks_trajectories (state, action, outcome, reward) + VALUES ($1, $2, $3, $4) RETURNING id`, + [state, action, outcome, reward] + ); + + // Cleanup old trajectories + await this.query(` + DELETE FROM ruvector_hooks_trajectories WHERE id IN ( + SELECT id FROM ruvector_hooks_trajectories ORDER BY created_at ASC OFFSET 1000 + ) + `); + + return `traj_${rows[0].id}`; + } + + async recordError(code: string, errorType: string, message: string): Promise { + await this.query(` + INSERT INTO ruvector_hooks_errors (code, error_type, message, occurrences) + VALUES ($1, $2, $3, 1) + ON CONFLICT (code) DO UPDATE SET + occurrences = ruvector_hooks_errors.occurrences + 1, + message = COALESCE($3, ruvector_hooks_errors.message) + `, [code, errorType, message]); + } + + async getErrorFixes(code: string): Promise { + const rows = await this.query( + 'SELECT code, error_type, message, fixes, occurrences FROM ruvector_hooks_errors WHERE code = $1', + [code] + ); + return rows[0] ?? null; + } + + async recordSequence(fromFile: string, toFile: string): Promise { + await this.query(` + INSERT INTO ruvector_hooks_file_sequences (from_file, to_file, count) + VALUES ($1, $2, 1) + ON CONFLICT (from_file, to_file) DO UPDATE SET + count = ruvector_hooks_file_sequences.count + 1 + `, [fromFile, toFile]); + } + + async getNextFiles(file: string, limit: number): Promise> { + const rows = await this.query<{ to_file: string; count: number }>( + `SELECT to_file, count FROM ruvector_hooks_file_sequences + WHERE from_file = $1 ORDER BY count DESC LIMIT $2`, + [file, limit] + ); + return rows.map(r => ({ file: r.to_file, count: r.count })); + } + + async registerAgent(id: string, type: string, capabilities: string[]): Promise { + await this.query(` + INSERT INTO ruvector_hooks_swarm_agents (id, agent_type, capabilities) + VALUES ($1, $2, $3) + ON CONFLICT (id) DO UPDATE SET + agent_type = $2, + capabilities = $3 + `, [id, type, capabilities]); + } + + async coordinateAgents(source: string, target: string, weight: number): Promise { + await this.query(` + INSERT INTO ruvector_hooks_swarm_edges (source_agent, target_agent, weight, coordination_count) + VALUES ($1, $2, $3, 1) + ON CONFLICT (source_agent, target_agent) DO UPDATE SET + weight = (ruvector_hooks_swarm_edges.weight + $3) / 2, + coordination_count = ruvector_hooks_swarm_edges.coordination_count + 1 + `, [source, target, weight]); + } + + async getSwarmStats(): Promise<{ agents: number; edges: number; avgSuccess: number }> { + const rows = await this.query<{ agents: number; edges: number; avg_success: number }>(` + SELECT + (SELECT COUNT(*)::INTEGER FROM ruvector_hooks_swarm_agents WHERE status = 'active') as agents, + (SELECT COUNT(*)::INTEGER FROM ruvector_hooks_swarm_edges) as edges, + (SELECT COALESCE(AVG(success_rate), 0)::REAL FROM ruvector_hooks_swarm_agents WHERE status = 'active') as avg_success + `); + const r = rows[0]; + return { agents: r?.agents ?? 0, edges: r?.edges ?? 0, avgSuccess: r?.avg_success ?? 0 }; + } + + async sessionStart(): Promise { + await this.query(` + UPDATE ruvector_hooks_stats SET session_count = session_count + 1, last_session = NOW() WHERE id = 1 + `); + } + + async getStats(): Promise { + const rows = await this.query<{ + patterns: number; + memories: number; + trajectories: number; + errors: number; + session_count: number; + last_session: Date; + }>(` + SELECT + (SELECT COUNT(*)::INTEGER FROM ruvector_hooks_patterns) as patterns, + (SELECT COUNT(*)::INTEGER FROM ruvector_hooks_memories) as memories, + (SELECT COUNT(*)::INTEGER FROM ruvector_hooks_trajectories) as trajectories, + (SELECT COUNT(*)::INTEGER FROM ruvector_hooks_errors) as errors, + s.session_count, + s.last_session + FROM ruvector_hooks_stats s WHERE s.id = 1 + `); + + const r = rows[0]; + return { + total_patterns: r?.patterns ?? 0, + total_memories: r?.memories ?? 0, + total_trajectories: r?.trajectories ?? 0, + total_errors: r?.errors ?? 0, + session_count: r?.session_count ?? 0, + last_session: r?.last_session ? Math.floor(new Date(r.last_session).getTime() / 1000) : 0 + }; + } +} + +// ============================================================================ +// Storage Factory +// ============================================================================ + +export async function createStorage(): Promise { + // Try PostgreSQL first if configured + const pgUrl = process.env.RUVECTOR_POSTGRES_URL || process.env.DATABASE_URL; + + if (pgUrl) { + try { + const pg = new PostgresStorage(pgUrl); + await pg.connect(); + console.error('🐘 Connected to PostgreSQL'); + return pg; + } catch (err) { + console.error('⚠️ PostgreSQL unavailable, falling back to JSON storage'); + } + } + + // Fallback to JSON + const json = new JsonStorage(); + await json.connect(); + return json; +} + +export function createStorageSync(): StorageBackend { + // Synchronous version - always returns JSON storage + // Use createStorage() for async with PostgreSQL support + return new JsonStorage(); +}