UNPKG

termcode

Version:

Superior terminal AI coding agent with enterprise-grade security, intelligent error recovery, performance monitoring, and plugin system - Advanced Claude Code alternative

321 lines (304 loc) 12.5 kB
import { getProvider } from "../providers/index.js"; import { loadConfig } from "../state/config.js"; import { estimateTokens } from "../util/costs.js"; import { log } from "../util/logging.js"; const defaultHybridConfig = { enabled: false, localProvider: "ollama", localModel: "llama3.1:8b", cloudProvider: "openai", cloudModel: "gpt-4o-mini", maxLocalTokens: 8000, compressionRatio: 0.3, // compress to 30% of original summarizationThreshold: 10 // summarize if more than 10 chunks }; // Check if local model is available export async function checkLocalAvailability(provider) { try { const providerInstance = getProvider(provider); const healthCheck = await providerInstance.healthCheck(); return healthCheck.status === "healthy"; } catch (error) { return false; } } // Summarize context using local model async function summarizeWithLocal(chunks, task, config) { const localProvider = getProvider(config.localProvider); // Combine chunks into groups for summarization const chunkGroups = []; const maxChunksPerGroup = 5; for (let i = 0; i < chunks.length; i += maxChunksPerGroup) { const group = chunks.slice(i, i + maxChunksPerGroup); const combinedText = group.map(chunk => `File: ${chunk.file} (lines ${chunk.start}-${chunk.end})\n${chunk.text}`).join("\n\n"); chunkGroups.push(combinedText); } const summaries = []; for (const groupText of chunkGroups) { const summarizePrompt = `Summarize this code context for the task "${task}". Focus on relevant details, key functions, patterns, and dependencies. Be concise but preserve important technical details. Context: ${groupText} Summary:`; try { const summary = await localProvider.chat([ { role: "user", content: summarizePrompt } ], { model: config.localModel, temperature: 0.1 }); summaries.push(summary); log.step("Local summarization", `Compressed ${estimateTokens(groupText)}${estimateTokens(summary)} tokens`); } catch (error) { log.warn("Local summarization failed for chunk group:", error); // Fallback: use original text (truncated) summaries.push(groupText.substring(0, 1000) + "..."); } } return summaries.join("\n\n"); } // Enhanced context distillation using local model async function distillContext(chunks, task, config) { const localProvider = getProvider(config.localProvider); // Create a context distillation prompt const contextText = chunks.slice(0, 20).map(chunk => // Limit to prevent overwhelm `${chunk.file}:${chunk.start}-${chunk.end}: ${chunk.text.substring(0, 500)}`).join("\n\n"); const distillPrompt = `You are a code context analyzer. Your job is to extract and synthesize the most relevant information from this codebase context for the given task. Task: ${task} Context from codebase: ${contextText} Please provide a distilled analysis focusing on: 1. Relevant files and their purposes 2. Key functions/classes/patterns that relate to the task 3. Dependencies and imports that matter 4. Existing code patterns to follow 5. Potential conflicts or considerations Keep your response concise but technically precise:`; try { const distilled = await localProvider.chat([ { role: "user", content: distillPrompt } ], { model: config.localModel, temperature: 0.2 }); const originalTokens = estimateTokens(contextText); const distilledTokens = estimateTokens(distilled); const compressionRatio = distilledTokens / originalTokens; log.step("Context distillation", `${originalTokens}${distilledTokens} tokens (${(compressionRatio * 100).toFixed(1)}% compression)`); return distilled; } catch (error) { log.warn("Context distillation failed:", error); // Fallback to basic summarization return await summarizeWithLocal(chunks, task, config); } } // Process task using hybrid local + cloud approach export async function processHybrid(task, repoPath, chunks = [], userConfig) { const startTime = Date.now(); const config = { ...defaultHybridConfig, ...userConfig }; if (!config.enabled) { // Fall back to cloud-only processing return processCloudOnly(task, repoPath, chunks, config); } // Check if local model is available const localAvailable = await checkLocalAvailability(config.localProvider); if (!localAvailable) { log.warn("Local model not available, falling back to cloud-only"); return processCloudOnly(task, repoPath, chunks, config); } // Estimate total context size const contextText = chunks.map(c => c.text).join("\n"); const totalTokens = estimateTokens(contextText + task); // If context is small enough for local processing, do it locally if (totalTokens <= config.maxLocalTokens && chunks.length < config.summarizationThreshold) { log.info("Context small enough for local-only processing"); return processLocalOnly(task, repoPath, chunks, config); } log.info(`Processing hybrid: ${chunks.length} chunks, ${totalTokens} tokens`); // Use local model to distill context const distilledContext = await distillContext(chunks, task, config); const compressedTokens = estimateTokens(distilledContext); const tokensCompressed = totalTokens - compressedTokens; // Send distilled context + task to cloud model const cloudProvider = getProvider(config.cloudProvider); const hybridPrompt = `You are an expert software developer. You have been provided with a distilled analysis of a codebase and need to complete a specific task. CODEBASE ANALYSIS: ${distilledContext} TASK: ${task} Please provide a complete solution that: 1. Follows the existing code patterns shown in the analysis 2. Integrates properly with the existing codebase 3. Includes proper error handling and documentation 4. Uses the identified dependencies and imports appropriately Your response should be production-ready code with clear explanations of any changes made.`; try { const finalResponse = await cloudProvider.chat([ { role: "user", content: hybridPrompt } ], { model: config.cloudModel, temperature: 0.2 }); // Calculate cost savings (approximate) const { calculateCost } = await import("../util/costs.js"); const cloudCostFull = await calculateCost(config.cloudProvider, config.cloudModel, totalTokens, estimateTokens(finalResponse)); const cloudCostHybrid = await calculateCost(config.cloudProvider, config.cloudModel, compressedTokens, estimateTokens(finalResponse)); const costSaved = cloudCostFull - cloudCostHybrid; const processingTime = Date.now() - startTime; log.success(`Hybrid processing complete: ${(costSaved * 100).toFixed(2)}¢ saved, ${tokensCompressed} tokens compressed`); return { method: "hybrid", localSummary: distilledContext.substring(0, 200) + "...", finalResponse, costSaved, tokensCompressed, processingTime }; } catch (error) { log.error("Hybrid processing failed:", error); // Fallback to cloud-only with truncated context return processCloudOnly(task, repoPath, chunks.slice(0, 10), config); } } // Process entirely with local model async function processLocalOnly(task, repoPath, chunks, config) { const startTime = Date.now(); const localProvider = getProvider(config.localProvider); const contextText = chunks.map(chunk => `${chunk.file}:${chunk.start}: ${chunk.text}`).join("\n\n"); const prompt = `You are an expert software developer working on a coding task. Context from codebase: ${contextText} Task: ${task} Please provide a complete solution:`; try { const response = await localProvider.chat([ { role: "user", content: prompt } ], { model: config.localModel, temperature: 0.2 }); const processingTime = Date.now() - startTime; return { method: "local-only", finalResponse: response, costSaved: 0, // Local is free tokensCompressed: 0, processingTime }; } catch (error) { log.error("Local processing failed:", error); throw error; } } // Process entirely with cloud model (fallback) async function processCloudOnly(task, repoPath, chunks, config) { const startTime = Date.now(); const cloudProvider = getProvider(config.cloudProvider); // Limit context to prevent exceeding limits const limitedChunks = chunks.slice(0, 15); const contextText = limitedChunks.map(chunk => `${chunk.file}:${chunk.start}: ${chunk.text.substring(0, 800)}`).join("\n\n"); const prompt = `You are an expert software developer working on a coding task. Context from codebase: ${contextText} Task: ${task} Please provide a complete solution:`; try { const response = await cloudProvider.chat([ { role: "user", content: prompt } ], { model: config.cloudModel, temperature: 0.2 }); const processingTime = Date.now() - startTime; return { method: "cloud-only", finalResponse: response, costSaved: 0, tokensCompressed: 0, processingTime }; } catch (error) { log.error("Cloud processing failed:", error); throw error; } } // Get hybrid processing recommendations export async function getHybridRecommendations(task, contextSize) { const config = defaultHybridConfig; const localAvailable = await checkLocalAvailability(config.localProvider); if (!localAvailable) { return { recommended: false, reasoning: "Local model (Ollama) is not available. Install and run 'ollama serve' to enable hybrid processing.", estimatedSavings: 0, requirements: [ "Install Ollama (https://ollama.ai)", "Download a model: ollama pull llama3.1:8b", "Start server: ollama serve" ] }; } if (contextSize < config.maxLocalTokens) { return { recommended: false, reasoning: "Context is small enough for efficient cloud processing. Hybrid mode provides minimal benefit for simple tasks.", estimatedSavings: 0, requirements: [] }; } // Estimate potential savings const { calculateCost } = await import("../util/costs.js"); const fullCloudCost = await calculateCost("openai", "gpt-4o-mini", contextSize, contextSize * 0.5); const hybridCost = await calculateCost("openai", "gpt-4o-mini", contextSize * 0.3, contextSize * 0.5); const estimatedSavings = fullCloudCost - hybridCost; return { recommended: true, reasoning: `Large context (${contextSize} tokens) can benefit from local summarization. Estimated ${((1 - hybridCost / fullCloudCost) * 100).toFixed(0)}% cost reduction.`, estimatedSavings, requirements: [ "Ollama running locally", "llama3.1:8b model downloaded" ] }; } // Load hybrid configuration export async function loadHybridConfig() { try { const config = await loadConfig(); const hybrid = config?.hybrid || {}; return { ...defaultHybridConfig, ...hybrid }; } catch (error) { return defaultHybridConfig; } } // Save hybrid configuration export async function saveHybridConfig(hybridConfig) { try { const config = await loadConfig(); if (!config) throw new Error("No base config found"); const updatedConfig = { ...config, hybrid: { ...defaultHybridConfig, ...config.hybrid, ...hybridConfig } }; const { saveConfig } = await import("../state/config.js"); await saveConfig(updatedConfig); } catch (error) { log.error("Failed to save hybrid config:", error); throw error; } }