import * as vscode from 'vscode'; import * as path from 'path'; import * as fs from 'fs'; // axios removed import { getConfig, _getBrainDir, findBrainFiles, EXCLUDED_DIRS, SYSTEM_PROMPT, shouldAutoPushBrain, getSecondBrainRepo } from './utils'; import { validatePath, sanitizeCommand } from './security'; export interface ChatMessage { role: 'user' | 'assistant' | 'system'; content: string | any[]; } export class AgentExecutor { private chatHistory: ChatMessage[] = []; private abortController: AbortController | null = null; private webview: vscode.Webview | undefined; constructor( private context: vscode.ExtensionContext ) {} public setWebview(webview: vscode.Webview) { this.webview = webview; } public getHistory() { return this.chatHistory; } public setHistory(history: ChatMessage[]) { this.chatHistory = history; } public clearHistory() { this.chatHistory = []; } public stop() { if (this.abortController) { this.abortController.abort(); this.abortController = null; } } public async handlePrompt( prompt: string | null, modelName: string, options: { internetEnabled?: boolean, brainEnabled?: boolean, loopDepth?: number, visionContent?: any[], temperature?: number, systemPrompt?: string } ) { const { internetEnabled = false, brainEnabled = false, loopDepth = 0, visionContent, temperature = 0.7, systemPrompt = SYSTEM_PROMPT } = options; if (!this.webview) return; try { // 1. Prepare Context const workspaceFolders = vscode.workspace.workspaceFolders; const rootPath = workspaceFolders ? workspaceFolders[0].uri.fsPath : ''; let contextBlock = ''; const config = getConfig(); const editor = vscode.window.activeTextEditor; if (editor && editor.document.uri.scheme === 'file') { const text = editor.document.getText(); const name = path.basename(editor.document.fileName); if (text.trim().length > 0 && text.length < config.maxContextSize) { contextBlock = `\n\n[Currently open file: ${name}]\n\`\`\`\n${text}\n\`\`\``; } } // 2. Setup History if (prompt !== null) { this.chatHistory.push({ role: 'user', content: prompt }); this.webview.postMessage({ type: 'streamChunk', value: '' }); // Trigger UI update if needed } // 3. API Request Setup const { ollamaUrl, defaultModel, timeout } = getConfig(); const reqMessages = [...this.chatHistory]; // Handle Vision Content Injection if (visionContent && reqMessages.length > 0) { const lastUserIdx = reqMessages.map(m => m.role).lastIndexOf('user'); if (lastUserIdx >= 0) { reqMessages[lastUserIdx] = { role: 'user', content: visionContent }; } } // Inject System Directives if (reqMessages.length > 0) { const internetCtx = internetEnabled ? `\n\n[CRITICAL: INTERNET ACCESS ENABLED]\nYou can use to search. Current time: ${new Date().toLocaleString()}` : ''; const fullSystemPrompt = `${systemPrompt}${internetCtx}\n\n[CONTEXT]\n${contextBlock}\n${internetCtx}`; const firstUserIdx = reqMessages.findIndex(m => m.role === 'user'); if (firstUserIdx >= 0) { let content = reqMessages[firstUserIdx].content; if (typeof content === 'string') { reqMessages[firstUserIdx].content = `${fullSystemPrompt}\n\n[USER QUERY]\n${content}`; if (loopDepth > 0) { reqMessages[firstUserIdx].content = `[Autonomous Step ${loopDepth}/${config.maxAutoSteps}]\n${reqMessages[firstUserIdx].content}`; } } } } // 4. Call AI Engine const isLMStudio = ollamaUrl.includes('1234') || ollamaUrl.includes('v1') || ollamaUrl.includes('localhost'); // Note: Many users use LM Studio on localhost, we'll try to be smart or fallback to Ollama format if it fails. const apiUrl = isLMStudio ? (ollamaUrl.endsWith('/v1') ? `${ollamaUrl}/chat/completions` : `${ollamaUrl}/v1/chat/completions`) : `${ollamaUrl}/api/chat`; this.abortController = new AbortController(); const streamBody = { model: modelName || defaultModel, messages: reqMessages, stream: true, ...(isLMStudio ? { max_tokens: 4096, temperature } : { options: { num_ctx: 32768, num_predict: 4096, temperature } }), }; const response = await fetch(apiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' }, body: JSON.stringify(streamBody), signal: this.abortController.signal, keepalive: true }); if (!response.ok) { const errText = await response.text(); throw new Error(`AI Engine error: ${response.status} - ${errText}`); } let aiResponseText = ''; const reader = response.body?.getReader(); if (!reader) throw new Error("Response body is not readable."); if (loopDepth === 0) this.webview.postMessage({ type: 'streamStart' }); let buffer = ''; const decoder = new TextDecoder(); try { while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop() || ''; for (const line of lines) { const trimmed = line.trim(); if (!trimmed || trimmed === 'data: [DONE]') continue; try { const raw = trimmed.startsWith('data: ') ? trimmed.slice(6) : trimmed; const json = JSON.parse(raw); const token = isLMStudio ? json.choices?.[0]?.delta?.content || '' : json.message?.content || json.response || ''; if (token) { aiResponseText += token; this.webview?.postMessage({ type: 'streamChunk', value: token }); } } catch (e) {} } } } catch (err: any) { if (err.name === 'AbortError') { console.log('[Agent] Generation aborted by user.'); } else { console.error('[Agent] Stream reading error:', err); this.webview?.postMessage({ type: 'error', value: `Connection lost: ${err.message}` }); } } // Final buffer processing if (buffer.trim() && buffer.trim() !== 'data: [DONE]') { try { const trimmed = buffer.trim(); const raw = trimmed.startsWith('data: ') ? trimmed.slice(6) : trimmed; const json = JSON.parse(raw); const token = isLMStudio ? json.choices?.[0]?.delta?.content || '' : json.message?.content || json.response || ''; if (token) { aiResponseText += token; this.webview?.postMessage({ type: 'streamChunk', value: token }); } } catch (e) {} } if (loopDepth === 0) this.webview.postMessage({ type: 'streamEnd' }); this.chatHistory.push({ role: 'assistant', content: aiResponseText }); // 5. Execute Actions const report = await this.executeActions(aiResponseText, rootPath); if (report.length > 0) { const reportMsg = `\n\n> ⚙️ **System Action Report** (${loopDepth + 1}/${config.maxAutoSteps})\n> ${report.join("\n> ")}\n\n`; this.webview.postMessage({ type: 'streamChunk', value: reportMsg }); // Continue loop if needed if (loopDepth < config.maxAutoSteps) { const currentActionStr = report.join('|'); const lastActionStr = this.context.workspaceState.get('lastActionStr'); if (currentActionStr === lastActionStr) { this.webview.postMessage({ type: 'streamChunk', value: "\n⚠️ *Stopping to prevent infinite loop.*" }); if (loopDepth === 0) this.webview.postMessage({ type: 'streamEnd' }); return; } await this.context.workspaceState.update('lastActionStr', currentActionStr); // Explicitly tell the AI to look at the results and continue const continuationPrompt = "I have executed your actions. Above is the result. Please analyze it and provide the next step or the final answer."; this.webview.postMessage({ type: 'autoContinue', value: `Thinking... (${loopDepth + 1}/${config.maxAutoSteps})` }); await new Promise(r => setTimeout(r, 800)); await this.handlePrompt(continuationPrompt, modelName, { ...options, loopDepth: loopDepth + 1 }); } } } catch (error: any) { this.webview.postMessage({ type: "error", value: `[Agent Error]: ${error.message}` }); } } private async executeActions(aiMessage: string, rootPath: string): Promise { const report: string[] = []; let brainModified = false; let firstCreatedFile: string | undefined; // Action 1: Create File const createRegex = /([\s\S]*?)<\/create_file>/gi; let match; while ((match = createRegex.exec(aiMessage)) !== null) { const relPath = match[1].trim(); const content = match[2].trim(); try { const absPath = validatePath(rootPath, relPath); fs.mkdirSync(path.dirname(absPath), { recursive: true }); fs.writeFileSync(absPath, content, 'utf-8'); report.push(`✅ Created: ${relPath}`); if (!firstCreatedFile) firstCreatedFile = absPath; if (absPath.startsWith(_getBrainDir())) brainModified = true; } catch (err: any) { report.push(`❌ Error Creating ${relPath}: ${err.message}`); } } // Action 2: Edit File const editRegex = /([\s\S]*?)<\/edit_file>/gi; while ((match = editRegex.exec(aiMessage)) !== null) { const relPath = match[1].trim(); const editContent = match[2].trim(); try { const absPath = validatePath(rootPath, relPath); if (fs.existsSync(absPath)) { let currentContent = fs.readFileSync(absPath, 'utf-8'); const searchMatch = editContent.match(/([\s\S]*?)<\/search>\s*([\s\S]*?)<\/replace>/i); if (searchMatch) { const searchStr = searchMatch[1]; const replaceStr = searchMatch[2]; if (currentContent.includes(searchStr)) { currentContent = currentContent.replace(searchStr, replaceStr); fs.writeFileSync(absPath, currentContent, 'utf-8'); report.push(`📝 Updated: ${relPath}`); } else { report.push(`⚠️ Search string not found in ${relPath}`); } } else { fs.writeFileSync(absPath, editContent, 'utf-8'); report.push(`📝 Updated (Full): ${relPath}`); } if (absPath.startsWith(_getBrainDir())) brainModified = true; } else { report.push(`❌ File not found: ${relPath}`); } } catch (err: any) { report.push(`❌ Error Editing ${relPath}: ${err.message}`); } } // Action 3: Delete File const deleteRegex = /(?:<\/delete_file>)?/gi; while ((match = deleteRegex.exec(aiMessage)) !== null) { const relPath = match[1].trim(); try { const absPath = validatePath(rootPath, relPath); if (fs.existsSync(absPath)) { fs.unlinkSync(absPath); report.push(`🗑 Deleted: ${relPath}`); } else { report.push(`⚠️ Delete failed: ${relPath} not found`); } } catch (err: any) { report.push(`❌ Error Deleting ${relPath}: ${err.message}`); } } // Action 4: Read File const readRegex = /(?:<\/read_file>)?/gi; while ((match = readRegex.exec(aiMessage)) !== null) { const relPath = match[1].trim(); try { const absPath = validatePath(rootPath, relPath); if (fs.existsSync(absPath)) { const content = fs.readFileSync(absPath, 'utf-8'); const preview = content.length > 8000 ? content.slice(0, 8000) + "\n... (truncated)" : content; report.push(`📖 Read: ${relPath}`); this.chatHistory.push({ role: 'user', content: `[Result of read_file ${relPath}]\n\`\`\`\n${preview}\n\`\`\`` }); } else { report.push(`❌ Read failed: ${relPath} not found`); } } catch (err: any) { report.push(`❌ Error Reading ${relPath}: ${err.message}`); } } // Action 5: Run Command const cmdRegex = /([\s\S]*?)<\/run_command>/gi; while ((match = cmdRegex.exec(aiMessage)) !== null) { const cmd = match[1].trim(); try { const safeCmd = sanitizeCommand(cmd); const terminal = vscode.window.terminals.find(t => t.name === 'G1nation Terminal') || vscode.window.createTerminal({ name: 'G1nation Terminal', cwd: rootPath }); terminal.show(); terminal.sendText(safeCmd); report.push(`🚀 Executed: ${safeCmd}`); } catch (err: any) { report.push(`❌ Blocked: ${err.message}`); } } // Action 6: List Files const listRegex = /(?:<\/list_files>)?/gi; while ((match = listRegex.exec(aiMessage)) !== null) { const relPath = match[1].trim() || '.'; try { const absPath = validatePath(rootPath, relPath); if (fs.existsSync(absPath) && fs.statSync(absPath).isDirectory()) { const entries = fs.readdirSync(absPath, { withFileTypes: true }); let listing = entries .filter(e => !e.name.startsWith('.') && !EXCLUDED_DIRS.has(e.name)) .map(e => e.isDirectory() ? `${e.name}/` : e.name) .join('\n'); if (listing.length > 5000) { listing = listing.slice(0, 5000) + "\n... (truncated for context)"; } report.push(`📂 Listed: ${relPath}`); this.chatHistory.push({ role: 'user', content: `[Result of list_files ${relPath}]\n${listing}` }); } } catch (err: any) { report.push(`❌ Listing failed: ${err.message}`); } } // Action 7: Second Brain Knowledge (List/Read) const listBrainRegex = /(?:<\/list_brain>)?/gi; while ((match = listBrainRegex.exec(aiMessage)) !== null) { const relPath = match[1].trim() || '.'; try { const brainDir = _getBrainDir(); const absPath = path.join(brainDir, relPath); if (fs.existsSync(absPath) && fs.statSync(absPath).isDirectory()) { const entries = fs.readdirSync(absPath, { withFileTypes: true }); let listing = entries .filter(e => !e.name.startsWith('.') && !EXCLUDED_DIRS.has(e.name)) .map(e => e.isDirectory() ? `${e.name}/` : e.name) .join('\n'); if (listing.length > 5000) { listing = listing.slice(0, 5000) + "\n... (truncated for context)"; } report.push(`🧠 Brain Listed: ${relPath}`); this.chatHistory.push({ role: 'user', content: `[Result of list_brain ${relPath}]\n${listing}` }); } else { report.push(`❌ Brain List failed: ${relPath} not found`); } } catch (err: any) { report.push(`❌ Error Listing Brain: ${err.message}`); } } const brainRegex = /([\s\S]*?)<\/read_brain>/gi; while ((match = brainRegex.exec(aiMessage)) !== null) { const fileName = match[1].trim(); try { const brainDir = _getBrainDir(); const files = findBrainFiles(brainDir); // Look for direct match or path match const targetFile = files.find((f: string) => path.basename(f) === fileName || f.endsWith(fileName)); if (targetFile && fs.existsSync(targetFile)) { const content = fs.readFileSync(targetFile, 'utf-8'); report.push(`🧠 Brain Read: ${fileName}`); this.chatHistory.push({ role: 'user', content: `[Result of read_brain ${fileName}]\n\`\`\`\n${content}\n\`\`\`` }); } else { report.push(`❌ Brain Read failed: ${fileName} not found in Second Brain`); } } catch (err: any) { report.push(`❌ Error Reading Brain: ${err.message}`); } } // Action 8: Read URL (Simple implementation) const urlRegex = /([\s\S]*?)<\/read_url>/gi; while ((match = urlRegex.exec(aiMessage)) !== null) { const url = match[1].trim(); try { const res = await fetch(url, { signal: AbortSignal.timeout(10000) }); const text = await res.text(); // Simple HTML to text-ish conversion const content = text.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim(); const preview = content.length > 5000 ? content.slice(0, 5000) + "\n... (truncated)" : content; report.push(`🌐 Read URL: ${url}`); this.chatHistory.push({ role: 'user', content: `[Result of read_url ${url}]\n${preview}` }); } catch (err: any) { report.push(`❌ URL Read failed: ${err.message}`); } } if (firstCreatedFile) { vscode.window.showTextDocument(vscode.Uri.file(firstCreatedFile), { preview: false }); } // Brain Sync Logic if (brainModified && shouldAutoPushBrain() && getSecondBrainRepo()) { this.syncBrain(); } return report; } private syncBrain() { try { const brainDir = _getBrainDir(); const { execSync } = require('child_process'); execSync(`git add .`, { cwd: brainDir }); execSync(`git commit -m "[G1nation] Knowledge Update"`, { cwd: brainDir }); execSync(`git push`, { cwd: brainDir }); } catch (err) { console.error('[Agent] Sync failed:', err); } } }