UNPKG

codecrucible-synth

Version:

Production-Ready AI Development Platform with Multi-Voice Synthesis, Smithery MCP Integration, Enterprise Security, and Zero-Timeout Reliability

711 lines (603 loc) 19.3 kB
/** * StreamingManager - Extracted from UnifiedModelClient * Handles all streaming-related functionality following Living Spiral methodology * * Council Perspectives Applied: * - Maintainer: Clean interfaces and clear separation of concerns * - Performance Engineer: Optimized streaming with backpressure handling * - Security Guardian: Safe token handling and resource cleanup * - Explorer: Extensible design for future streaming patterns */ import { EventEmitter } from 'events'; import { logger } from '../logger.js'; // Enhanced: AI SDK v5.0 Compatible Streaming Interfaces export interface StreamChunk { type: | 'stream-start' | 'text-start' | 'text-delta' | 'text-end' | 'reasoning-start' | 'reasoning-delta' | 'reasoning-end' | 'tool-input-start' | 'tool-input-delta' | 'tool-input-end' | 'tool-call' | 'tool-result' | 'finish' | 'error'; // Common properties id?: string; timestamp: number; // Stream start properties warnings?: StreamWarning[]; // Text streaming properties delta?: string; // Tool properties toolCallId?: string; toolName?: string; args?: unknown; result?: unknown; // Finish properties usage?: StreamUsage; finishReason?: 'stop' | 'length' | 'content-filter' | 'tool-calls' | 'error'; // Error properties error?: string; errorCode?: string; // Provider metadata providerMetadata?: Record<string, unknown>; // Legacy compatibility content?: string; finished?: boolean; metadata?: Record<string, any>; } export interface StreamWarning { type: string; message: string; code?: string; } export interface StreamUsage { inputTokens?: number; outputTokens?: number; totalTokens?: number; cacheReadTokens?: number; cacheWriteTokens?: number; } export interface StreamBlock { id: string; type: 'text' | 'reasoning' | 'tool-input' | 'tool-call'; startTime: number; endTime?: number; content: string[]; metadata?: Record<string, any>; } // Streaming interfaces (moved from client.ts) export interface StreamToken { content: string; timestamp: number; index: number; finished?: boolean; metadata?: Record<string, any>; } export interface StreamConfig { chunkSize?: number; bufferSize?: number; enableBackpressure?: boolean; timeout?: number; encoding?: BufferEncoding; // Enhanced: Modern streaming features enableReasoningStream?: boolean; enableToolStreaming?: boolean; maxRetries?: number; enableProviderMetadata?: boolean; enableLifecycleEvents?: boolean; } export interface StreamMetrics { tokensStreamed: number; streamDuration: number; averageLatency: number; throughput: number; backpressureEvents: number; } export interface StreamSession { id: string; startTime: number; tokens: StreamToken[]; chunks: StreamChunk[]; // Enhanced: Modern chunks support activeBlocks: Map<string, StreamBlock>; // Enhanced: Track streaming blocks metrics: StreamMetrics; isActive: boolean; status: 'active' | 'completed' | 'error' | 'cancelled'; // Enhanced: Better status tracking } export interface IStreamingManager { // Core streaming operations startStream( content: string, onToken: (token: StreamToken) => void, config?: StreamConfig ): Promise<string>; // Enhanced: Modern streaming with AI SDK v5.0 patterns startModernStream( content: string, onChunk: (chunk: StreamChunk) => void, config?: StreamConfig ): Promise<string>; // Enhanced: Tool streaming support streamToolExecution( toolName: string, args: unknown, onChunk: (chunk: StreamChunk) => void ): Promise<unknown>; // Session management createSession(sessionId?: string): StreamSession; getSession(sessionId: string): StreamSession | undefined; destroySession(sessionId: string): void; // Metrics and monitoring getStreamMetrics(sessionId: string): StreamMetrics | undefined; getAllMetrics(): Map<string, StreamMetrics>; // Configuration updateConfig(config: Partial<StreamConfig>): void; getConfig(): StreamConfig; // Cleanup cleanup(): Promise<void>; } /** * StreamingManager Implementation * Follows Single Responsibility Principle - handles only streaming concerns */ export class StreamingManager extends EventEmitter implements IStreamingManager { private config: StreamConfig; private sessions: Map<string, StreamSession> = new Map(); private activeStreams: Set<string> = new Set(); private defaultConfig: StreamConfig = { chunkSize: 50, bufferSize: 1024, enableBackpressure: true, timeout: 30000, encoding: 'utf8', // Enhanced: Modern streaming defaults enableReasoningStream: true, enableToolStreaming: true, maxRetries: 3, enableProviderMetadata: true, enableLifecycleEvents: true, }; constructor(config: Partial<StreamConfig> = {}) { super(); this.config = { ...this.defaultConfig, ...config }; this.setupEventHandlers(); } /** * Setup event handlers for stream monitoring */ private setupEventHandlers(): void { this.on('session-created', (sessionId: string) => { logger.debug('Stream session created', { sessionId }); }); // Enhanced: Modern streaming events this.on('stream-start', (chunk: StreamChunk) => { logger.debug('Modern stream started', { id: chunk.id }); }); this.on('text-block-start', (chunk: StreamChunk) => { logger.debug('Text block started', { id: chunk.id }); }); this.on('tool-call', (chunk: StreamChunk) => { logger.debug('Tool call received', { toolName: chunk.toolName, id: chunk.toolCallId }); }); this.on('session-destroyed', (sessionId: string) => { logger.debug('Stream session destroyed', { sessionId }); }); this.on('backpressure', (sessionId: string) => { logger.warn('Backpressure event detected', { sessionId }); }); } /** * Enhanced: Start modern streaming with AI SDK v5.0 lifecycle patterns */ async startModernStream( content: string, onChunk: (chunk: StreamChunk) => void, config?: StreamConfig ): Promise<string> { const sessionConfig = { ...this.config, ...config }; const sessionId = this.generateSessionId(); const streamId = this.generateStreamId(); try { const session = this.createSession(sessionId); session.status = 'active'; this.activeStreams.add(sessionId); // Send stream-start chunk const streamStartChunk: StreamChunk = { type: 'stream-start', id: streamId, timestamp: Date.now(), warnings: [], }; session.chunks.push(streamStartChunk); onChunk(streamStartChunk); this.emit('stream-start', streamStartChunk); // Send text-start chunk const textBlockId = this.generateBlockId(); const textStartChunk: StreamChunk = { type: 'text-start', id: textBlockId, timestamp: Date.now(), }; session.chunks.push(textStartChunk); onChunk(textStartChunk); this.emit('text-block-start', textStartChunk); // Create text block const textBlock: StreamBlock = { id: textBlockId, type: 'text', startTime: Date.now(), content: [], }; session.activeBlocks.set(textBlockId, textBlock); // Stream content as deltas const tokens = this.tokenizeContent(content, sessionConfig.chunkSize || 50); let streamedContent = ''; for (let i = 0; i < tokens.length; i++) { if (!this.activeStreams.has(sessionId)) { throw new Error(`Stream session ${sessionId} was terminated`); } const deltaChunk: StreamChunk = { type: 'text-delta', id: textBlockId, delta: tokens[i], timestamp: Date.now(), }; textBlock.content.push(tokens[i]); session.chunks.push(deltaChunk); onChunk(deltaChunk); this.emit('text-delta', deltaChunk); streamedContent += tokens[i]; await new Promise(resolve => setTimeout(resolve, 10)); } // Send text-end chunk textBlock.endTime = Date.now(); const textEndChunk: StreamChunk = { type: 'text-end', id: textBlockId, timestamp: Date.now(), }; session.chunks.push(textEndChunk); onChunk(textEndChunk); this.emit('text-block-end', textEndChunk); // Send finish chunk const finishChunk: StreamChunk = { type: 'finish', timestamp: Date.now(), finishReason: 'stop', usage: { inputTokens: content.length, outputTokens: streamedContent.length, totalTokens: content.length + streamedContent.length, }, }; session.chunks.push(finishChunk); onChunk(finishChunk); this.emit('stream-finish', finishChunk); // Finalize session session.status = 'completed'; session.isActive = false; this.activeStreams.delete(sessionId); logger.info('Modern stream session completed', { sessionId, streamId, chunksGenerated: session.chunks.length, contentLength: streamedContent.length, }); return streamedContent; } catch (error) { const session = this.sessions.get(sessionId); if (session) { session.status = 'error'; const errorChunk: StreamChunk = { type: 'error', timestamp: Date.now(), error: error instanceof Error ? error.message : String(error), errorCode: 'STREAM_ERROR', }; session.chunks.push(errorChunk); onChunk(errorChunk); } this.activeStreams.delete(sessionId); this.destroySession(sessionId); logger.error('Modern stream session failed', { sessionId, error: error instanceof Error ? error.message : String(error), }); throw error; } } /** * Enhanced: Stream tool execution with proper lifecycle */ async streamToolExecution( toolName: string, args: unknown, onChunk: (chunk: StreamChunk) => void ): Promise<unknown> { const toolCallId = this.generateBlockId(); try { // Send tool-call chunk const toolCallChunk: StreamChunk = { type: 'tool-call', toolCallId, toolName, args, timestamp: Date.now(), }; onChunk(toolCallChunk); this.emit('tool-call', toolCallChunk); // Simulate tool execution (in real implementation, this would call actual tools) await new Promise(resolve => setTimeout(resolve, 100)); const result = { success: true, output: `Tool ${toolName} executed successfully` }; // Send tool-result chunk const toolResultChunk: StreamChunk = { type: 'tool-result', toolCallId, result, timestamp: Date.now(), }; onChunk(toolResultChunk); this.emit('tool-result', toolResultChunk); return result; } catch (error) { const errorChunk: StreamChunk = { type: 'error', timestamp: Date.now(), error: error instanceof Error ? error.message : String(error), errorCode: 'TOOL_ERROR', }; onChunk(errorChunk); throw error; } } /** * Start streaming content with token-by-token delivery * Core streaming method with comprehensive error handling */ async startStream( content: string, onToken: (token: StreamToken) => void, config?: StreamConfig ): Promise<string> { const sessionConfig = { ...this.config, ...config }; const sessionId = this.generateSessionId(); try { // Create and initialize session const session = this.createSession(sessionId); this.activeStreams.add(sessionId); logger.info('Starting stream session', { sessionId, contentLength: content.length, config: sessionConfig, }); // Stream content token by token const tokens = this.tokenizeContent(content, sessionConfig.chunkSize || 50); let streamedContent = ''; for (let i = 0; i < tokens.length; i++) { // Check if session is still active if (!this.activeStreams.has(sessionId)) { throw new Error(`Stream session ${sessionId} was terminated`); } const token: StreamToken = { content: tokens[i], timestamp: Date.now(), index: i, finished: i === tokens.length - 1, metadata: { sessionId, progress: (i + 1) / tokens.length, totalTokens: tokens.length, }, }; // Update session and metrics session.tokens.push(token); this.updateStreamMetrics(sessionId, token); // Emit token to handler onToken(token); this.emit('token', token); streamedContent += token.content; // Handle backpressure if enabled if (sessionConfig.enableBackpressure && i % 5 === 0) { await this.handleBackpressure(sessionId); } // Add realistic streaming delay await new Promise(resolve => setTimeout(resolve, 10)); } // Finalize session session.isActive = false; this.activeStreams.delete(sessionId); logger.info('Stream session completed', { sessionId, tokensStreamed: session.metrics.tokensStreamed, duration: session.metrics.streamDuration, }); return streamedContent; } catch (error) { // Cleanup on error this.activeStreams.delete(sessionId); this.destroySession(sessionId); logger.error('Stream session failed', { sessionId, error: error instanceof Error ? error.message : String(error), }); throw error; } } /** * Create a new streaming session */ createSession(sessionId?: string): StreamSession { const id = sessionId || this.generateSessionId(); if (this.sessions.has(id)) { throw new Error(`Session ${id} already exists`); } const session: StreamSession = { id, startTime: Date.now(), tokens: [], chunks: [], // Enhanced: Modern chunks support activeBlocks: new Map(), // Enhanced: Track streaming blocks metrics: { tokensStreamed: 0, streamDuration: 0, averageLatency: 0, throughput: 0, backpressureEvents: 0, }, isActive: true, status: 'active', // Enhanced: Better status tracking }; this.sessions.set(id, session); this.emit('session-created', id); return session; } /** * Get existing session */ getSession(sessionId: string): StreamSession | undefined { return this.sessions.get(sessionId); } /** * Destroy a streaming session and cleanup resources */ destroySession(sessionId: string): void { const session = this.sessions.get(sessionId); if (session) { session.isActive = false; this.sessions.delete(sessionId); this.activeStreams.delete(sessionId); this.emit('session-destroyed', sessionId); } } /** * Get metrics for a specific session */ getStreamMetrics(sessionId: string): StreamMetrics | undefined { return this.sessions.get(sessionId)?.metrics; } /** * Get all session metrics */ getAllMetrics(): Map<string, StreamMetrics> { const metrics = new Map<string, StreamMetrics>(); for (const [id, session] of this.sessions.entries()) { metrics.set(id, session.metrics); } return metrics; } /** * Update streaming configuration */ updateConfig(config: Partial<StreamConfig>): void { this.config = { ...this.config, ...config }; logger.info('Streaming configuration updated', { config: this.config }); } /** * Get current configuration */ getConfig(): StreamConfig { return { ...this.config }; } /** * Cleanup all sessions and resources */ async cleanup(): Promise<void> { logger.info('Cleaning up streaming manager', { activeSessions: this.sessions.size, activeStreams: this.activeStreams.size, }); // Stop all active streams for (const sessionId of this.activeStreams) { this.destroySession(sessionId); } // Clear all data structures this.sessions.clear(); this.activeStreams.clear(); // Remove all listeners this.removeAllListeners(); } /** * Private: Update metrics for a streaming session */ private updateStreamMetrics(sessionId: string, token: StreamToken): void { const session = this.sessions.get(sessionId); if (!session) return; const metrics = session.metrics; metrics.tokensStreamed++; metrics.streamDuration = Date.now() - session.startTime; if (metrics.tokensStreamed > 0) { metrics.averageLatency = metrics.streamDuration / metrics.tokensStreamed; metrics.throughput = (metrics.tokensStreamed / metrics.streamDuration) * 1000; // tokens per second } } /** * Private: Handle backpressure by introducing controlled delays */ private async handleBackpressure(sessionId: string): Promise<void> { const session = this.sessions.get(sessionId); if (!session) return; // Increment backpressure events counter session.metrics.backpressureEvents++; // Emit backpressure event this.emit('backpressure', sessionId); // Small delay to prevent overwhelming downstream consumers await new Promise(resolve => setTimeout(resolve, 5)); } /** * Private: Tokenize content into chunks for streaming */ private tokenizeContent(content: string, chunkSize: number): string[] { const tokens: string[] = []; const words = content.split(' '); let currentChunk = ''; for (const word of words) { if (currentChunk.length + word.length + 1 > chunkSize && currentChunk.length > 0) { tokens.push(currentChunk); currentChunk = word; } else { currentChunk += (currentChunk.length > 0 ? ' ' : '') + word; } } if (currentChunk.length > 0) { tokens.push(currentChunk); } // Ensure we always have at least 2 tokens for testing if (tokens.length === 1 && tokens[0].length > 10) { const midpoint = Math.floor(tokens[0].length / 2); const firstHalf = tokens[0].substring(0, midpoint); const secondHalf = tokens[0].substring(midpoint); return [firstHalf, secondHalf]; } return tokens; } /** * Enhanced: Generate unique stream ID for AI SDK v5.0 compatibility */ private generateStreamId(): string { return `stream_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } /** * Enhanced: Generate unique block ID for streaming blocks */ private generateBlockId(): string { return `block_${Date.now()}_${Math.random().toString(36).substr(2, 6)}`; } /** * Private: Generate unique session ID */ private generateSessionId(): string { return `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } } // Factory function for easy instantiation export function createStreamingManager(config?: Partial<StreamConfig>): IStreamingManager { return new StreamingManager(config); } // Default export for convenience export default StreamingManager;