UNPKG

mcp-subagents

Version:

Multi-Agent AI Orchestration via Model Context Protocol - Access specialized CLI AI agents (Aider, Qwen, Gemini, Goose, etc.) with intelligent fallback and configuration

298 lines 10.8 kB
/** * Chunked Output Manager * * Manages task output in chunks for memory efficiency. * Instead of storing every line individually, output is stored in chunks * with a circular buffer to prevent unbounded memory growth. */ export class ChunkedOutput { chunks = new Map(); nextChunkId = 0; totalLines = 0; maxChunks; chunkSize; currentChunk = null; lastUpdateTime = Date.now(); newLineCallbacks = []; constructor(options = {}) { this.maxChunks = options.maxChunks || 100; // Limit to 100 chunks this.chunkSize = options.chunkSize || 100; // 100 lines per chunk } /** * Add new output lines */ addLines(lines) { if (lines.length === 0) return; for (const line of lines) { // Create new chunk if needed if (!this.currentChunk || this.currentChunk.lines.length >= this.chunkSize) { this.createNewChunk(); } // Add line to current chunk this.currentChunk.lines.push(line); this.currentChunk.endLineNumber = this.totalLines; this.totalLines++; } // Update timestamp and notify callbacks this.lastUpdateTime = Date.now(); const callbacks = [...this.newLineCallbacks]; this.newLineCallbacks = []; callbacks.forEach(callback => callback()); } /** * Get output lines with pagination and search */ getLines(options = {}) { const { offset = 0, limit = 20, fromEnd = true, maxChars, search, context = 0, outputMode = 'content', ignoreCase = false, matchNumbers = false } = options; // Convert all chunks to flat array for processing const allLines = this.getAllLines(); if (search) { return this.searchLines(allLines, search, { context, outputMode, ignoreCase, matchNumbers, limit, ...(maxChars !== undefined && { maxChars }) }); } // Handle pagination let startIdx; let endIdx; if (fromEnd) { // Count from end startIdx = Math.max(0, allLines.length - offset - limit); endIdx = Math.max(0, allLines.length - offset); } else { // Count from beginning startIdx = Math.min(offset, allLines.length); endIdx = Math.min(offset + limit, allLines.length); } let resultLines = allLines.slice(startIdx, endIdx); // Apply character limit if specified if (maxChars && maxChars > 0) { let totalChars = 0; const limitedLines = []; for (const line of resultLines) { if (totalChars + line.length > maxChars) { break; } limitedLines.push(line); totalChars += line.length + 1; // +1 for newline } resultLines = limitedLines; } return { lines: resultLines, totalLines: this.totalLines, hasMore: endIdx < allLines.length || (fromEnd && startIdx > 0) }; } /** * Get total number of output lines */ getTotalLines() { return this.totalLines; } /** * Get memory usage statistics */ getStats() { const chunkIds = Array.from(this.chunks.keys()).sort((a, b) => a - b); const memoryBytes = Array.from(this.chunks.values()) .reduce((sum, chunk) => sum + chunk.lines.join('').length, 0); const result = { totalLines: this.totalLines, totalChunks: this.chunks.size, memoryEstimate: this.formatBytes(memoryBytes) }; if (chunkIds.length > 0) { result.oldestChunk = chunkIds[0]; result.newestChunk = chunkIds[chunkIds.length - 1]; } return result; } /** * Get lines with streaming support - can wait for new output */ async getLinesStreaming(options = {}) { const { lastSeenLine = -1, waitForNew = false, maxWaitTime = 5000, includeLineNumbers = false, limit = 20, ...outputOptions } = options; // First check if we have new lines since lastSeenLine if (lastSeenLine < this.totalLines - 1) { // We have new lines, return them immediately const newLinesCount = lastSeenLine === -1 ? this.totalLines : this.totalLines - lastSeenLine - 1; const result = this.getLines({ ...outputOptions, offset: lastSeenLine + 1, limit, fromEnd: false }); return { lines: result.lines.map((line, index) => ({ lineNumber: lastSeenLine + 1 + index, content: line, timestamp: this.lastUpdateTime })), totalLines: this.totalLines, hasMore: result.hasMore, lastSeenLine: Math.min(lastSeenLine + result.lines.length, this.totalLines - 1), newLinesCount }; } // No new lines available if (!waitForNew) { return { lines: [], totalLines: this.totalLines, hasMore: false, lastSeenLine: this.totalLines - 1, newLinesCount: 0 }; } // Wait for new lines const waitPromise = new Promise((resolve) => { this.newLineCallbacks.push(resolve); }); const timeoutPromise = new Promise((resolve) => { setTimeout(resolve, maxWaitTime); }); // Wait for either new lines or timeout await Promise.race([waitPromise, timeoutPromise]); // Check again for new lines return this.getLinesStreaming({ ...options, waitForNew: false // Don't wait again }); } /** * Get the last update timestamp */ getLastUpdateTime() { return this.lastUpdateTime; } /** * Create a new chunk and manage circular buffer */ createNewChunk() { // Remove oldest chunk if we've hit the limit if (this.chunks.size >= this.maxChunks) { const oldestChunkId = Math.min(...this.chunks.keys()); this.chunks.delete(oldestChunkId); } // Create new chunk this.currentChunk = { id: this.nextChunkId++, timestamp: Date.now(), lines: [], startLineNumber: this.totalLines, endLineNumber: this.totalLines }; this.chunks.set(this.currentChunk.id, this.currentChunk); } /** * Convert all chunks to flat array of lines */ getAllLines() { const chunkIds = Array.from(this.chunks.keys()).sort((a, b) => a - b); const lines = []; for (const chunkId of chunkIds) { const chunk = this.chunks.get(chunkId); if (chunk) { lines.push(...chunk.lines); } } return lines; } /** * Search through lines with context and formatting options */ searchLines(lines, pattern, options) { const { context, outputMode, ignoreCase, matchNumbers, limit, maxChars } = options; try { const regex = new RegExp(pattern, ignoreCase ? 'gi' : 'g'); const matches = []; // Find all matches lines.forEach((line, index) => { const lineMatches = Array.from(line.matchAll(regex)); for (const match of lineMatches) { matches.push({ lineIndex: index, line, match: match[0] }); } }); if (outputMode === 'count') { return { lines: [`Total matches: ${matches.length}`], totalLines: this.totalLines, hasMore: false, searchMatches: matches.length }; } if (outputMode === 'matches_only') { const matchStrings = matches.map(m => m.match); return { lines: matchStrings.slice(0, limit), totalLines: this.totalLines, hasMore: matchStrings.length > (limit || 20), searchMatches: matches.length }; } // Content mode with context const resultLines = []; const addedLines = new Set(); let totalChars = 0; for (const match of matches) { if (limit && resultLines.length >= limit) break; if (maxChars && totalChars >= maxChars) break; // Add context lines around match const startLine = Math.max(0, match.lineIndex - context); const endLine = Math.min(lines.length - 1, match.lineIndex + context); for (let i = startLine; i <= endLine; i++) { if (!addedLines.has(i)) { const line = lines[i]; const linePrefix = matchNumbers ? `${i + 1}: ` : ''; const fullLine = linePrefix + line; if (maxChars && totalChars + fullLine.length > maxChars) break; resultLines.push(fullLine); addedLines.add(i); totalChars += fullLine.length + 1; } } } return { lines: resultLines, totalLines: this.totalLines, hasMore: matches.length > (limit || 20) || resultLines.length < matches.length, searchMatches: matches.length }; } catch (error) { // Invalid regex pattern return { lines: [`Error: Invalid search pattern: ${error instanceof Error ? error.message : 'Unknown error'}`], totalLines: this.totalLines, hasMore: false, searchMatches: 0 }; } } /** * Format bytes as human-readable string */ formatBytes(bytes) { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`; } } //# sourceMappingURL=chunked-output.js.map