UNPKG

claude-flow-novice

Version:

Claude Flow Novice - Advanced orchestration platform for multi-agent AI workflows with CFN Loop architecture Includes Local RuVector Accelerator and all CFN skills for complete functionality.

507 lines (506 loc) 18 kB
/** * DatabaseHandoff.ts - Standard database handoff patterns with cross-database correlation * * Features: * - Cross-database correlation via task_id * - Transaction management (begin/commit/rollback) * - Query builder with standard correlation * - Connection pooling (PostgreSQL, SQLite) * - Automatic retry on transient failures */ import { Pool as PgPool } from 'pg'; import sqlite3 from 'sqlite3'; import { StandardAdapter, JSONLogger } from './StandardAdapter.js'; /** * DatabaseHandoff - Reference implementation for cross-database correlation * * @example * ```typescript * // PostgreSQL example * const pgHandoff = new DatabaseHandoff({ * type: 'postgresql', * pg: { * host: 'localhost', * port: 5432, * database: 'cfn_db', * user: 'cfn_user', * password: 'secret', * }, * }, { * task_id: 'task-123', * agent_id: 'agent-456', * }); * * await pgHandoff.initialize(); * * // Create handoff with automatic correlation * const handoff = await pgHandoff.createHandoff({ * source_agent_id: 'agent-456', * target_agent_id: 'agent-789', * payload: { data: 'example' }, * }); * * // Query by task_id (cross-database correlation) * const handoffs = await pgHandoff.getHandoffsByTaskId('task-123'); * * // Transaction example * await pgHandoff.withTransaction(async (tx) => { * await tx.query('INSERT INTO tasks ...'); * await tx.query('UPDATE agents ...'); * // Automatic commit on success, rollback on error * }); * ``` */ export class DatabaseHandoff { config; adapter; logger; // Connection pools pg_pool; sqlite_db; // Initialization state initialized = false; constructor(config, context){ this.config = config; this.logger = context.logger || new JSONLogger(); this.adapter = new StandardAdapter({ task_id: context.task_id, agent_id: context.agent_id, logger: this.logger }); } /** * Initialize database connection and schema */ async initialize() { if (this.initialized) { return; } try { if (this.config.type === 'postgresql') { await this.initializePostgreSQL(); } else if (this.config.type === 'sqlite') { await this.initializeSQLite(); } else { throw new Error(`Unsupported database type: ${this.config.type}`); } await this.ensureSchema(); this.initialized = true; this.logger.info('Database handoff initialized', { task_id: this.adapter.getContext().task_id, database_type: this.config.type }); } catch (error) { this.logger.error('Failed to initialize database', { task_id: this.adapter.getContext().task_id, error: error instanceof Error ? error.message : String(error) }); throw error; } } /** * Create a new handoff record */ async createHandoff(params) { this.ensureInitialized(); const handoff_id = this.generateHandoffId(); const { task_id } = this.adapter.getContext(); const handoff = { handoff_id, task_id, source_agent_id: params.source_agent_id, target_agent_id: params.target_agent_id, status: 'pending', payload: params.payload, metadata: params.metadata, created_at: new Date(), updated_at: new Date() }; return await this.adapter.withRetry(async ()=>{ if (this.config.type === 'postgresql') { return await this.createHandoffPostgreSQL(handoff); } else { return await this.createHandoffSQLite(handoff); } }); } /** * Get handoff by ID */ async getHandoff(handoff_id) { this.ensureInitialized(); return await this.adapter.withRetry(async ()=>{ if (this.config.type === 'postgresql') { return await this.getHandoffPostgreSQL(handoff_id); } else { return await this.getHandoffSQLite(handoff_id); } }); } /** * Get all handoffs for a task (cross-database correlation) */ async getHandoffsByTaskId(task_id) { this.ensureInitialized(); return await this.adapter.withRetry(async ()=>{ if (this.config.type === 'postgresql') { return await this.getHandoffsByTaskIdPostgreSQL(task_id); } else { return await this.getHandoffsByTaskIdSQLite(task_id); } }); } /** * Update handoff status */ async updateHandoffStatus(handoff_id, status, metadata) { this.ensureInitialized(); await this.adapter.withRetry(async ()=>{ if (this.config.type === 'postgresql') { await this.updateHandoffStatusPostgreSQL(handoff_id, status, metadata); } else { await this.updateHandoffStatusSQLite(handoff_id, status, metadata); } }); this.logger.info('Handoff status updated', { handoff_id, status, task_id: this.adapter.getContext().task_id }); } /** * Execute queries within a transaction * Automatically commits on success, rolls back on error */ async withTransaction(callback) { this.ensureInitialized(); if (this.config.type === 'postgresql') { return await this.withTransactionPostgreSQL(callback); } else { return await this.withTransactionSQLite(callback); } } /** * Close all database connections */ async close() { try { if (this.pg_pool) { await this.pg_pool.end(); this.logger.info('PostgreSQL connection pool closed'); } if (this.sqlite_db) { await this.sqlite_db.close(); this.logger.info('SQLite connection closed'); } this.initialized = false; } catch (error) { this.logger.error('Error closing database connections', { error: error instanceof Error ? error.message : String(error) }); throw error; } } // --- PostgreSQL Implementation --- async initializePostgreSQL() { if (!this.config.pg) { throw new Error('PostgreSQL configuration missing'); } this.pg_pool = new PgPool({ host: this.config.pg.host, port: this.config.pg.port, database: this.config.pg.database, user: this.config.pg.user, password: this.config.pg.password, max: this.config.pg.max_connections || 10, idleTimeoutMillis: 30000, connectionTimeoutMillis: 5000 }); // Test connection const client = await this.pg_pool.connect(); client.release(); } async createHandoffPostgreSQL(handoff) { const query = ` INSERT INTO handoffs ( handoff_id, task_id, source_agent_id, target_agent_id, status, payload, metadata, created_at, updated_at ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING * `; const values = [ handoff.handoff_id, handoff.task_id, handoff.source_agent_id, handoff.target_agent_id || null, handoff.status, JSON.stringify(handoff.payload), handoff.metadata ? JSON.stringify(handoff.metadata) : null, handoff.created_at, handoff.updated_at ]; const result = await this.pg_pool.query(query, values); return this.rowToHandoff(result.rows[0]); } async getHandoffPostgreSQL(handoff_id) { const query = 'SELECT * FROM handoffs WHERE handoff_id = $1'; const result = await this.pg_pool.query(query, [ handoff_id ]); return result.rows.length > 0 ? this.rowToHandoff(result.rows[0]) : null; } async getHandoffsByTaskIdPostgreSQL(task_id) { const query = 'SELECT * FROM handoffs WHERE task_id = $1 ORDER BY created_at DESC'; const result = await this.pg_pool.query(query, [ task_id ]); return result.rows.map((row)=>this.rowToHandoff(row)); } async updateHandoffStatusPostgreSQL(handoff_id, status, metadata) { const updates = [ 'status = $2', 'updated_at = $3' ]; const values = [ handoff_id, status, new Date() ]; if (status === 'completed') { updates.push('completed_at = $4'); values.push(new Date()); } if (metadata) { const idx = values.length + 1; updates.push(`metadata = $${idx}`); values.push(JSON.stringify(metadata)); } const query = `UPDATE handoffs SET ${updates.join(', ')} WHERE handoff_id = $1`; await this.pg_pool.query(query, values); } async withTransactionPostgreSQL(callback) { const client = await this.pg_pool.connect(); try { await client.query('BEGIN'); this.logger.debug('Transaction started (PostgreSQL)'); const tx = new TransactionClient(client, this.logger); const result = await callback(tx); await client.query('COMMIT'); this.logger.debug('Transaction committed (PostgreSQL)'); return result; } catch (error) { await client.query('ROLLBACK'); this.logger.warn('Transaction rolled back (PostgreSQL)', { error: error instanceof Error ? error.message : String(error) }); throw error; } finally{ client.release(); } } // --- SQLite Implementation --- async initializeSQLite() { if (!this.config.sqlite) { throw new Error('SQLite configuration missing'); } const sqlite = await import('sqlite'); this.sqlite_db = await sqlite.open({ filename: this.config.sqlite.filepath, driver: sqlite3.Database }); } async createHandoffSQLite(handoff) { const query = ` INSERT INTO handoffs ( handoff_id, task_id, source_agent_id, target_agent_id, status, payload, metadata, created_at, updated_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) `; await this.sqlite_db.run(query, [ handoff.handoff_id, handoff.task_id, handoff.source_agent_id, handoff.target_agent_id || null, handoff.status, JSON.stringify(handoff.payload), handoff.metadata ? JSON.stringify(handoff.metadata) : null, handoff.created_at.toISOString(), handoff.updated_at.toISOString() ]); return handoff; } async getHandoffSQLite(handoff_id) { const query = 'SELECT * FROM handoffs WHERE handoff_id = ?'; const row = await this.sqlite_db.get(query, [ handoff_id ]); return row ? this.rowToHandoff(row) : null; } async getHandoffsByTaskIdSQLite(task_id) { const query = 'SELECT * FROM handoffs WHERE task_id = ? ORDER BY created_at DESC'; const rows = await this.sqlite_db.all(query, [ task_id ]); return rows.map((row)=>this.rowToHandoff(row)); } async updateHandoffStatusSQLite(handoff_id, status, metadata) { let query = 'UPDATE handoffs SET status = ?, updated_at = ?'; const values = [ status, new Date().toISOString() ]; if (status === 'completed') { query += ', completed_at = ?'; values.push(new Date().toISOString()); } if (metadata) { query += ', metadata = ?'; values.push(JSON.stringify(metadata)); } query += ' WHERE handoff_id = ?'; values.push(handoff_id); await this.sqlite_db.run(query, values); } async withTransactionSQLite(callback) { try { await this.sqlite_db.run('BEGIN TRANSACTION'); this.logger.debug('Transaction started (SQLite)'); const tx = new TransactionClient(this.sqlite_db, this.logger); const result = await callback(tx); await this.sqlite_db.run('COMMIT'); this.logger.debug('Transaction committed (SQLite)'); return result; } catch (error) { await this.sqlite_db.run('ROLLBACK'); this.logger.warn('Transaction rolled back (SQLite)', { error: error instanceof Error ? error.message : String(error) }); throw error; } } // --- Schema Management --- async ensureSchema() { const schema = ` CREATE TABLE IF NOT EXISTS handoffs ( handoff_id TEXT PRIMARY KEY, task_id TEXT NOT NULL, source_agent_id TEXT NOT NULL, target_agent_id TEXT, status TEXT NOT NULL, payload TEXT NOT NULL, metadata TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, completed_at TEXT ); CREATE INDEX IF NOT EXISTS idx_handoffs_task_id ON handoffs(task_id); CREATE INDEX IF NOT EXISTS idx_handoffs_status ON handoffs(status); CREATE INDEX IF NOT EXISTS idx_handoffs_created_at ON handoffs(created_at); `; if (this.config.type === 'postgresql') { // PostgreSQL schema (adjust types) const pgSchema = schema.replace(/TEXT/g, 'VARCHAR(255)').replace(/payload VARCHAR\(255\)/g, 'payload JSONB').replace(/metadata VARCHAR\(255\)/g, 'metadata JSONB').replace(/created_at VARCHAR\(255\)/g, 'created_at TIMESTAMP').replace(/updated_at VARCHAR\(255\)/g, 'updated_at TIMESTAMP').replace(/completed_at VARCHAR\(255\)/g, 'completed_at TIMESTAMP'); const statements = pgSchema.split(';').filter((s)=>s.trim()); for (const stmt of statements){ await this.pg_pool.query(stmt); } } else { // SQLite schema const statements = schema.split(';').filter((s)=>s.trim()); for (const stmt of statements){ await this.sqlite_db.run(stmt); } } } // --- Helper Methods --- ensureInitialized() { if (!this.initialized) { throw new Error('DatabaseHandoff not initialized. Call initialize() first.'); } } generateHandoffId() { const timestamp = Date.now(); const random = Math.random().toString(36).substring(2, 10); return `handoff-${timestamp}-${random}`; } rowToHandoff(row) { return { handoff_id: row.handoff_id, task_id: row.task_id, source_agent_id: row.source_agent_id, target_agent_id: row.target_agent_id, status: row.status, payload: typeof row.payload === 'string' ? JSON.parse(row.payload) : row.payload, metadata: row.metadata ? typeof row.metadata === 'string' ? JSON.parse(row.metadata) : row.metadata : undefined, created_at: new Date(row.created_at), updated_at: new Date(row.updated_at), completed_at: row.completed_at ? new Date(row.completed_at) : undefined }; } } /** * Transaction client for safe query execution within transactions */ export class TransactionClient { client; logger; constructor(client, logger){ this.client = client; this.logger = logger; } async query(sql, params) { this.logger.debug('Executing query in transaction', { sql }); if ('query' in this.client) { // PostgreSQL const result = await this.client.query(sql, params); return result; } else { // SQLite if (sql.trim().toUpperCase().startsWith('SELECT')) { return await this.client.all(sql, params); } else { return await this.client.run(sql, params); } } } } /** * USAGE EXAMPLE - Before (Ad-hoc database access): * * ```typescript * // ❌ No correlation, no transaction safety, manual connection management * import { Pool } from 'pg'; * * const pool = new Pool({ ... }); * * async function createTask(data: any) { * const client = await pool.connect(); * try { * await client.query('BEGIN'); * await client.query('INSERT INTO tasks ...'); * await client.query('INSERT INTO task_metadata ...'); * await client.query('COMMIT'); * } catch (err) { * await client.query('ROLLBACK'); * throw err; * } finally { * client.release(); * } * } * ``` * * USAGE EXAMPLE - After (Standardized with correlation): * * ```typescript * // ✅ Automatic correlation, transaction safety, retry logic * const handoff = new DatabaseHandoff({ * type: 'postgresql', * pg: { host: 'localhost', port: 5432, database: 'cfn', user: 'user', password: 'pass' }, * }, { * task_id: 'task-123', * agent_id: 'agent-456', * }); * * await handoff.initialize(); * * async function createTask(data: any) { * await handoff.withTransaction(async (tx) => { * await tx.query('INSERT INTO tasks (task_id, data) VALUES ($1, $2)', ['task-123', data]); * await tx.query('INSERT INTO task_metadata (task_id, source) VALUES ($1, $2)', ['task-123', 'agent-456']); * // Auto-commit on success, auto-rollback on error * }); * } * ``` */ //# sourceMappingURL=DatabaseHandoff.js.map