UNPKG

claude-flow

Version:

Enterprise-grade AI agent orchestration with WASM-powered ReasoningBank memory and AgentDB vector database (always uses latest agentic-flow)

468 lines (395 loc) 12.9 kB
/** * Real-Time Query Control * Claude-Flow v2.5-alpha.130 * * Implements real-time control of running agent queries: * - Pause/resume execution * - Terminate agents dynamically * - Change model or permissions mid-execution * - Monitor agent status in real-time */ import { type Query, type PermissionMode, type ModelInfo } from '@anthropic-ai/claude-code/sdk'; import { EventEmitter } from 'events'; import { Logger } from '../core/logger.js'; export interface QueryControlOptions { allowPause?: boolean; allowModelChange?: boolean; allowPermissionChange?: boolean; monitoringInterval?: number; } export interface ControlledQuery { queryId: string; agentId: string; query: Query; status: 'running' | 'paused' | 'terminated' | 'completed' | 'failed'; isPaused: boolean; canControl: boolean; startTime: number; pausedAt?: number; resumedAt?: number; terminatedAt?: number; currentModel?: string; permissionMode?: PermissionMode; } export interface QueryControlCommand { type: 'pause' | 'resume' | 'terminate' | 'changeModel' | 'changePermissions'; queryId: string; params?: { model?: string; permissionMode?: PermissionMode; reason?: string; }; } export interface QueryStatusUpdate { queryId: string; status: ControlledQuery['status']; timestamp: number; metadata?: Record<string, any>; } /** * RealTimeQueryController - Control running queries dynamically * Enables pause, resume, terminate, and configuration changes during execution */ export class RealTimeQueryController extends EventEmitter { private logger: Logger; private controlledQueries: Map<string, ControlledQuery> = new Map(); private monitoringIntervals: Map<string, NodeJS.Timeout> = new Map(); private commandQueue: Map<string, QueryControlCommand[]> = new Map(); private options: QueryControlOptions; constructor(options: QueryControlOptions = {}) { super(); this.options = { allowPause: options.allowPause !== false, allowModelChange: options.allowModelChange !== false, allowPermissionChange: options.allowPermissionChange !== false, monitoringInterval: options.monitoringInterval || 1000 }; this.logger = new Logger( { level: 'info', format: 'text', destination: 'console' }, { component: 'RealTimeQueryController' } ); } /** * Register a query for control */ registerQuery(queryId: string, agentId: string, query: Query): ControlledQuery { const controlled: ControlledQuery = { queryId, agentId, query, status: 'running', isPaused: false, canControl: true, startTime: Date.now() }; this.controlledQueries.set(queryId, controlled); this.startMonitoring(queryId); this.logger.info('Query registered for control', { queryId, agentId }); this.emit('query:registered', { queryId, agentId }); return controlled; } /** * Pause a running query * Note: SDK interrupt() will stop the query, not pause it * True pause/resume requires custom implementation */ async pauseQuery(queryId: string, reason?: string): Promise<boolean> { if (!this.options.allowPause) { throw new Error('Pause is not enabled in controller options'); } const controlled = this.controlledQueries.get(queryId); if (!controlled) { throw new Error(`Query not found: ${queryId}`); } if (controlled.isPaused || controlled.status !== 'running') { this.logger.warn('Query is not in a state to be paused', { queryId, status: controlled.status, isPaused: controlled.isPaused }); return false; } try { // SDK doesn't support true pause, so we interrupt // In a real implementation, we'd need to track state and resume await controlled.query.interrupt(); controlled.isPaused = true; controlled.status = 'paused'; controlled.pausedAt = Date.now(); this.logger.info('Query paused', { queryId, reason }); this.emit('query:paused', { queryId, reason }); return true; } catch (error) { this.logger.error('Failed to pause query', { queryId, error: error instanceof Error ? error.message : String(error) }); throw error; } } /** * Resume a paused query * Note: Actual resume requires storing state and restarting */ async resumeQuery(queryId: string): Promise<boolean> { const controlled = this.controlledQueries.get(queryId); if (!controlled) { throw new Error(`Query not found: ${queryId}`); } if (!controlled.isPaused || controlled.status !== 'paused') { this.logger.warn('Query is not paused', { queryId, status: controlled.status }); return false; } // In a real implementation, we'd resume from saved state // For now, mark as resumed controlled.isPaused = false; controlled.status = 'running'; controlled.resumedAt = Date.now(); this.logger.info('Query resumed', { queryId }); this.emit('query:resumed', { queryId }); return true; } /** * Terminate a query immediately */ async terminateQuery(queryId: string, reason?: string): Promise<boolean> { const controlled = this.controlledQueries.get(queryId); if (!controlled) { throw new Error(`Query not found: ${queryId}`); } if (controlled.status === 'terminated') { return true; } try { await controlled.query.interrupt(); controlled.status = 'terminated'; controlled.terminatedAt = Date.now(); this.stopMonitoring(queryId); this.logger.info('Query terminated', { queryId, reason }); this.emit('query:terminated', { queryId, reason }); return true; } catch (error) { this.logger.error('Failed to terminate query', { queryId, error: error instanceof Error ? error.message : String(error) }); throw error; } } /** * Change model for a running query */ async changeModel(queryId: string, model: string): Promise<boolean> { if (!this.options.allowModelChange) { throw new Error('Model change is not enabled in controller options'); } const controlled = this.controlledQueries.get(queryId); if (!controlled) { throw new Error(`Query not found: ${queryId}`); } if (controlled.status !== 'running') { throw new Error('Can only change model for running queries'); } try { await controlled.query.setModel(model); controlled.currentModel = model; this.logger.info('Model changed for query', { queryId, model }); this.emit('query:modelChanged', { queryId, model }); return true; } catch (error) { this.logger.error('Failed to change model', { queryId, model, error: error instanceof Error ? error.message : String(error) }); throw error; } } /** * Change permission mode for a running query */ async changePermissionMode(queryId: string, mode: PermissionMode): Promise<boolean> { if (!this.options.allowPermissionChange) { throw new Error('Permission change is not enabled in controller options'); } const controlled = this.controlledQueries.get(queryId); if (!controlled) { throw new Error(`Query not found: ${queryId}`); } if (controlled.status !== 'running') { throw new Error('Can only change permissions for running queries'); } try { await controlled.query.setPermissionMode(mode); controlled.permissionMode = mode; this.logger.info('Permission mode changed for query', { queryId, mode }); this.emit('query:permissionChanged', { queryId, mode }); return true; } catch (error) { this.logger.error('Failed to change permission mode', { queryId, mode, error: error instanceof Error ? error.message : String(error) }); throw error; } } /** * Get supported models for a query */ async getSupportedModels(queryId: string): Promise<ModelInfo[]> { const controlled = this.controlledQueries.get(queryId); if (!controlled) { throw new Error(`Query not found: ${queryId}`); } try { return await controlled.query.supportedModels(); } catch (error) { this.logger.error('Failed to get supported models', { queryId }); throw error; } } /** * Execute a control command */ async executeCommand(command: QueryControlCommand): Promise<boolean> { this.logger.debug('Executing control command', { command }); switch (command.type) { case 'pause': return this.pauseQuery(command.queryId, command.params?.reason); case 'resume': return this.resumeQuery(command.queryId); case 'terminate': return this.terminateQuery(command.queryId, command.params?.reason); case 'changeModel': if (!command.params?.model) { throw new Error('Model parameter required for changeModel command'); } return this.changeModel(command.queryId, command.params.model); case 'changePermissions': if (!command.params?.permissionMode) { throw new Error('Permission mode required for changePermissions command'); } return this.changePermissionMode(command.queryId, command.params.permissionMode); default: throw new Error(`Unknown command type: ${(command as any).type}`); } } /** * Queue a command for execution */ queueCommand(command: QueryControlCommand): void { const queue = this.commandQueue.get(command.queryId) || []; queue.push(command); this.commandQueue.set(command.queryId, queue); this.emit('command:queued', command); } /** * Process queued commands for a query */ async processQueuedCommands(queryId: string): Promise<void> { const queue = this.commandQueue.get(queryId); if (!queue || queue.length === 0) { return; } this.logger.debug('Processing queued commands', { queryId, commandCount: queue.length }); while (queue.length > 0) { const command = queue.shift()!; try { await this.executeCommand(command); } catch (error) { this.logger.error('Failed to execute queued command', { queryId, command, error: error instanceof Error ? error.message : String(error) }); } } this.commandQueue.delete(queryId); } /** * Get query status */ getQueryStatus(queryId: string): ControlledQuery | undefined { return this.controlledQueries.get(queryId); } /** * Get all controlled queries */ getAllQueries(): Map<string, ControlledQuery> { return new Map(this.controlledQueries); } /** * Start monitoring a query */ private startMonitoring(queryId: string): void { const interval = setInterval(() => { const controlled = this.controlledQueries.get(queryId); if (!controlled) { this.stopMonitoring(queryId); return; } const update: QueryStatusUpdate = { queryId, status: controlled.status, timestamp: Date.now(), metadata: { isPaused: controlled.isPaused, duration: Date.now() - controlled.startTime } }; this.emit('query:status', update); }, this.options.monitoringInterval); this.monitoringIntervals.set(queryId, interval); } /** * Stop monitoring a query */ private stopMonitoring(queryId: string): void { const interval = this.monitoringIntervals.get(queryId); if (interval) { clearInterval(interval); this.monitoringIntervals.delete(queryId); } } /** * Unregister a query */ unregisterQuery(queryId: string): void { this.stopMonitoring(queryId); this.controlledQueries.delete(queryId); this.commandQueue.delete(queryId); this.logger.info('Query unregistered', { queryId }); this.emit('query:unregistered', { queryId }); } /** * Cleanup completed queries */ cleanup(olderThan: number = 3600000): void { const cutoff = Date.now() - olderThan; for (const [queryId, controlled] of this.controlledQueries.entries()) { const endTime = controlled.terminatedAt || controlled.startTime; if (controlled.status === 'completed' || controlled.status === 'terminated') { if (endTime < cutoff) { this.unregisterQuery(queryId); } } } } /** * Shutdown controller */ shutdown(): void { // Stop all monitoring for (const queryId of this.monitoringIntervals.keys()) { this.stopMonitoring(queryId); } // Clear all data this.controlledQueries.clear(); this.commandQueue.clear(); this.logger.info('Query controller shutdown complete'); } }