[Architecture] G1nation V2 Refactor

This commit is contained in:
2026-04-24 18:23:21 +09:00
parent 48f41e98c2
commit 0e20dff154
5 changed files with 895 additions and 2103 deletions
+305
View File
@@ -0,0 +1,305 @@
import * as vscode from 'vscode';
import * as path from 'path';
import * as fs from 'fs';
import axios from 'axios';
import {
getConfig,
_getBrainDir,
EXCLUDED_DIRS,
MAX_CONTEXT_SIZE,
MAX_AUTO_AGENT_STEPS,
SYSTEM_PROMPT,
shouldAutoPushBrain,
getSecondBrainRepo
} from './utils';
import { validatePath, sanitizeCommand } from './security';
export interface ChatMessage {
role: 'user' | 'assistant' | 'system';
content: string | any[];
}
export class AgentExecutor {
constructor(
private context: vscode.ExtensionContext,
private webview: vscode.Webview | undefined,
private chatHistory: ChatMessage[],
private abortController: 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 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 < MAX_CONTEXT_SIZE) {
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 <read_url> 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}/${MAX_AUTO_AGENT_STEPS}]\n${reqMessages[firstUserIdx].content}`;
}
}
}
}
// 4. Call AI Engine
const isLMStudio = ollamaUrl.includes('1234') || ollamaUrl.includes('v1');
const apiUrl = isLMStudio ? `${ollamaUrl}/v1/chat/completions` : `${ollamaUrl}/api/chat`;
const streamBody = {
model: modelName || defaultModel,
messages: reqMessages,
stream: true,
...(isLMStudio
? { max_tokens: 4096, temperature }
: { options: { num_ctx: 16384, num_predict: 4096, temperature } }),
};
if (loopDepth === 0) this.webview.postMessage({ type: 'streamStart' });
const response = await axios.post(apiUrl, streamBody, {
timeout,
responseType: 'stream',
signal: this.abortController?.signal
});
let aiResponseText = '';
await new Promise<void>((resolve, reject) => {
const stream = response.data;
let buffer = '';
stream.on('data', (chunk: Buffer) => {
buffer += chunk.toString();
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (!line.trim() || line.trim() === 'data: [DONE]') continue;
try {
const raw = line.startsWith('data: ') ? line.slice(6) : line;
const json = JSON.parse(raw);
const token = isLMStudio ? json.choices?.[0]?.delta?.content || '' : json.message?.content || '';
if (token) {
aiResponseText += token;
this.webview?.postMessage({ type: 'streamChunk', value: token });
}
} catch {}
}
});
stream.on('end', () => resolve());
stream.on('error', (err: any) => reject(err));
});
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---\n**[Agent Action Report] (${loopDepth + 1}/${MAX_AUTO_AGENT_STEPS})**\n${report.join("\n")}`;
this.webview.postMessage({ type: 'streamChunk', value: reportMsg });
// Continue loop if needed
if (loopDepth < MAX_AUTO_AGENT_STEPS) {
const currentActionStr = report.join('|');
const lastActionStr = this.context.workspaceState.get<string>('lastActionStr');
if (currentActionStr === lastActionStr) {
this.webview.postMessage({ type: "streamChunk", value: "\n\n**[Loop Detected]** AI is repeating actions. Diverging..." });
this.chatHistory.push({ role: "user", content: "[System] Action repeated. Try a different strategy." });
}
await this.context.workspaceState.update('lastActionStr', currentActionStr);
this.webview.postMessage({ type: 'autoContinue', value: `Thinking... (${loopDepth + 1}/${MAX_AUTO_AGENT_STEPS})` });
await new Promise(r => setTimeout(r, 800));
await this.handlePrompt(null, 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<string[]> {
const report: string[] = [];
let brainModified = false;
let firstCreatedFile: string | undefined;
// Action 1: Create File
const createRegex = /<create_file\s+path=['"]?([^'"]+)['"]?>([\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 = /<edit_file\s+path=['"]?([^'"]+)['"]?>([\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(/<search>([\s\S]*?)<\/search>\s*<replace>([\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: Read File
const readRegex = /<read_file\s+path=['"]?([^'"]+)['"]?\s*\/?>(?:<\/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 4: Run Command
const cmdRegex = /<run_command>([\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 5: List Files
const listRegex = /<list_files\s+path=['"]?([^'"]+)['"]?\s*\/?>(?:<\/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 });
const listing = entries
.filter(e => !e.name.startsWith('.') && !EXCLUDED_DIRS.has(e.name))
.map(e => e.isDirectory() ? `${e.name}/` : e.name)
.join('\n');
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}`); }
}
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 {}
}
}