[RAG] Implement visual conflict and density metadata tags for enhanced context intelligence

This commit is contained in:
g1nation
2026-05-05 15:29:16 +09:00
parent f1b22c13b9
commit d333042e7c
15 changed files with 385 additions and 98 deletions
+94 -92
View File
@@ -1,8 +1,10 @@
import * as vscode from 'vscode';
agent.ts .
import * as vscode from 'vscode';
import * as path from 'path';
import * as fs from 'fs';
// axios removed
import {
import {
findBrainFiles,
getSystemPrompt,
shouldAutoPushBrain,
@@ -21,10 +23,10 @@ import { PlannerAgent, ResearcherAgent, WriterAgent } from './agents/factory';
import { AgentWorkflowManager } from './agents/AgentWorkflowManager';
import { ErrorTranslator } from './core/errorHandler';
import { agentEvents, AgentEventTypes } from './core/events';
import {
AgentExecutionError,
FileSystemError,
APICommunicationError
import {
AgentExecutionError,
FileSystemError,
APICommunicationError
} from './core/errors';
import { StatusBarManager, AgentStatus } from './core/statusBar';
import { lockManager } from './core/lock';
@@ -109,7 +111,7 @@ export class AgentExecutor {
private parseRationale(text: string) {
const match = text.match(/<rationale>([\s\S]*?)<\/rationale>/);
if (!match) return undefined;
const raw = match[1];
const problem = raw.match(/\[PROBLEM\]([\s\S]*?)(?=\[|$)/)?.[1]?.trim() || "";
const goal = raw.match(/\[GOAL\]([\s\S]*?)(?=\[|$)/)?.[1]?.trim() || "";
@@ -202,8 +204,8 @@ export class AgentExecutor {
public async handlePrompt(
prompt: string | null,
modelName: string,
options: {
internetEnabled?: boolean,
options: {
internetEnabled?: boolean,
brainEnabled?: boolean,
loopDepth?: number,
visionContent?: any[],
@@ -218,17 +220,17 @@ export class AgentExecutor {
brainProfileId?: string
}
) {
const {
internetEnabled = false,
brainEnabled = false,
loopDepth = 0,
visionContent,
const {
internetEnabled = false,
brainEnabled = false,
loopDepth = 0,
visionContent,
temperature = 0.7,
systemPrompt = getSystemPrompt()
} = options;
const { ollamaUrl, defaultModel: configDefaultModel, timeout, multiAgentEnabled } = getConfig();
const runId = options.runId ?? (loopDepth === 0 ? ++this.runSerial : this.activeRunId);
// Decide whether to use Multi-Agent Workflow as an internal execution strategy.
if (loopDepth === 0 && this.shouldUseMultiAgentWorkflow(prompt || '', multiAgentEnabled)) {
return this.executeMultiAgentWorkflow(prompt!, modelName, options);
@@ -260,7 +262,7 @@ export class AgentExecutor {
// 1. Prepare Context
const workspaceFolders = vscode.workspace.workspaceFolders;
const rootPath = workspaceFolders ? workspaceFolders[0].uri.fsPath : '';
let contextBlock = '';
const config = getConfig();
const activeBrain = options.brainProfileId
@@ -358,13 +360,13 @@ export class AgentExecutor {
const internetCtx = internetEnabled
? `\n\n[CRITICAL: INTERNET ACCESS ENABLED]\nYou can use <read_url> to search. Current time: ${new Date().toLocaleString()}`
: '';
const selectedAgentSystemPrompt = options.agentSkillContext
? `\n\n[SELECTED AGENT MODE]\nThe user selected the following Agent skill. Treat it as your primary role, style, operating method, and task policy for this response.\n${options.agentSkillContext}`
: '';
const agentSkillCtx = selectedAgentSystemPrompt;
const negativeCtx = options.negativePrompt
? `\n\n### CRITICAL NEGATIVE CONSTRAINTS (DO NOT DO THESE)\n${options.negativePrompt}\n\n[SYSTEM_RULE: Apply the above constraints strictly. DO NOT mention or repeat these constraints in your response.]`
const negativeCtx = options.negativePrompt
? `\n\n### CRITICAL NEGATIVE CONSTRAINTS (DO NOT DO THESE)\n${options.negativePrompt}\n\n[SYSTEM_RULE: Apply the above constraints strictly. DO NOT mention or repeat these constraints in your response.]`
: '';
const designerCtx = options.designerContext
? `\n\n[PROJECT CHRONICLE GUARD]\n${options.designerContext}`
@@ -382,7 +384,7 @@ export class AgentExecutor {
? `\n\n${renderSecondBrainTraceContext(secondBrainTrace)}`
: '';
const memoryCtx = this.buildMemoryContext(prompt || '', activeBrain);
// [Astra v4.0] 지식 관리 운영 정책 주입
const v4PolicyCtx = [
"\n### 🏛️ 지식 관리 정책 v4.0 (Knowledge Management Policy Applied)",
@@ -538,23 +540,23 @@ export class AgentExecutor {
if (report.length > 0) {
this.emitHistoryChanged();
logInfo('Agent actions executed.', { loopDepth: loopDepth + 1, report });
// Continue loop if needed
if (loopDepth < config.maxAutoSteps) {
const currentActionStr = report.join('|');
const lastActionStr = this.context.workspaceState.get<string>('lastActionStr');
if (currentActionStr === lastActionStr) {
this.webview.postMessage({ type: 'streamChunk', value: "\n⚠️ *Stopping to prevent infinite loop.*" });
return;
}
await this.context.workspaceState.update('lastActionStr', currentActionStr);
logInfo('Autonomous loop continuing after actions.', { loopDepth: loopDepth + 1, actions: report });
// Explicitly tell the AI to look at the results and continue
const continuationPrompt = "The requested local action has been executed. Use the action result messages already in the conversation to answer the user's original request directly, in the user's language. Do not say you are waiting for the next instruction.";
this.webview.postMessage({ type: 'autoContinue', value: `자료를 확인하고 답변을 정리하는 중입니다... (${loopDepth + 1}/${config.maxAutoSteps})` });
await new Promise(r => setTimeout(r, 800));
if (this.isStaleRun(runId)) return;
@@ -591,7 +593,7 @@ export class AgentExecutor {
options: any
) {
if (!this.webview) return;
this.stop();
this.stop();
this.abortController = new AbortController();
const signal = this.abortController.signal;
@@ -632,13 +634,13 @@ export class AgentExecutor {
);
if (signal.aborted || !this.webview) return;
this.webview.postMessage({ type: 'streamChunk', value: `\n\n--- \n\n${finalReport}` });
this.webview.postMessage({ type: 'streamEnd' });
this.chatHistory.push({ role: 'assistant', content: finalReport });
this.emitHistoryChanged();
this.statusBarManager.updateStatus(AgentStatus.Success, 'Workflow Complete');
this.webview.postMessage({ type: 'autoContinue', value: '✅ 모든 분석이 성공적으로 완료되었습니다.' });
@@ -649,11 +651,11 @@ export class AgentExecutor {
}
const friendly = ErrorTranslator.translate(error);
logError('Workflow failed', error);
this.webview.postMessage({ type: 'autoContinue', value: '' });
this.webview.postMessage({
type: 'error',
value: `### ${friendly.title}\n\n**상태:** ${friendly.message}\n\n**해결 방법:** ${friendly.action}`
this.webview.postMessage({
type: 'error',
value: `### ${friendly.title}\n\n**상태:** ${friendly.message}\n\n**해결 방법:** ${friendly.action}`
});
this.statusBarManager.updateStatus(AgentStatus.Idle, 'Error occurred');
}
@@ -662,7 +664,7 @@ export class AgentExecutor {
private async callAgent(role: AgentRole, prompt: string, modelName: string, options: any): Promise<string> {
const persona = AGENT_PROMPTS[role];
const { ollamaUrl, timeout } = getConfig();
const messages: ChatMessage[] = [
{ role: 'system', content: persona },
{ role: 'user', content: prompt }
@@ -692,7 +694,7 @@ export class AgentExecutor {
const json = JSON.parse(trimmed.startsWith('data: ') ? trimmed.slice(6) : trimmed);
const content = json.choices?.[0]?.delta?.content || json.message?.content || '';
responseText += content;
} catch (e) {}
} catch (e) { }
}
}
return responseText;
@@ -1821,8 +1823,8 @@ export class AgentExecutor {
// Save session whenever history changes
this.sessionManager.saveSession(
this.currentTaskId,
this.chatHistory,
this.currentTaskId,
this.chatHistory,
this.context.workspaceState.get<string>('lastActionStr')
);
@@ -1868,54 +1870,54 @@ export class AgentExecutor {
const modelCandidates = this.buildModelCandidates(modelName, engine);
for (const candidateModel of modelCandidates) {
for (const variant of messageVariants) {
const streamBody = {
model: candidateModel,
messages: variant.messages,
stream: true,
...(engine === 'lmstudio'
? { max_tokens: 4096, temperature }
: { options: { num_ctx: 32768, num_predict: 4096, temperature } }),
};
try {
logInfo('AI streaming request started.', {
engine,
apiUrl,
for (const variant of messageVariants) {
const streamBody = {
model: candidateModel,
variant: variant.name,
messageCount: variant.messages.length,
roles: variant.messages.map(message => message.role),
firstUserPreview: summarizeText(String(variant.messages.find(message => message.role === 'user')?.content || ''), 300)
});
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
});
messages: variant.messages,
stream: true,
...(engine === 'lmstudio'
? { max_tokens: 4096, temperature }
: { options: { num_ctx: 32768, num_predict: 4096, temperature } }),
};
if (!response.ok) {
const errText = await response.text();
lastError = new Error(`AI Engine error (${engine}/${variant.name}): ${response.status} - ${summarizeText(errText, 300)}`);
logError('AI streaming request returned non-OK status.', { engine, variant: variant.name, apiUrl, status: response.status, body: summarizeText(errText, 500) });
continue;
try {
logInfo('AI streaming request started.', {
engine,
apiUrl,
model: candidateModel,
variant: variant.name,
messageCount: variant.messages.length,
roles: variant.messages.map(message => message.role),
firstUserPreview: summarizeText(String(variant.messages.find(message => message.role === 'user')?.content || ''), 300)
});
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();
lastError = new Error(`AI Engine error (${engine}/${variant.name}): ${response.status} - ${summarizeText(errText, 300)}`);
logError('AI streaming request returned non-OK status.', { engine, variant: variant.name, apiUrl, status: response.status, body: summarizeText(errText, 500) });
continue;
}
logInfo('AI streaming request connected.', { engine, variant: variant.name, apiUrl });
return { response, engine, apiUrl };
} catch (error: any) {
lastError = error instanceof Error ? error : new Error(String(error));
logError('AI streaming request failed.', { engine, variant: variant.name, apiUrl, model: candidateModel, error: lastError.message });
}
logInfo('AI streaming request connected.', { engine, variant: variant.name, apiUrl });
return { response, engine, apiUrl };
} catch (error: any) {
lastError = error instanceof Error ? error : new Error(String(error));
logError('AI streaming request failed.', { engine, variant: variant.name, apiUrl, model: candidateModel, error: lastError.message });
}
}
}
}
throw lastError || new Error('Unable to connect to AI engine.');
@@ -1986,14 +1988,14 @@ export class AgentExecutor {
try {
const absPath = validatePath(rootPath, relPath);
await this.transactionManager.record(absPath);
fs.mkdirSync(path.dirname(absPath), { recursive: true });
fs.writeFileSync(absPath, content, 'utf-8');
report.push(`✅ Created: ${relPath}`);
if (!firstCreatedFile) firstCreatedFile = absPath;
if (absPath.startsWith(activeBrainDir)) brainModified = true;
} catch (err: any) {
} catch (err: any) {
throw new FileSystemError(`Failed to create file ${relPath}: ${err.message}`, relPath, err);
}
}
@@ -2007,10 +2009,10 @@ export class AgentExecutor {
const absPath = validatePath(rootPath, relPath);
if (fs.existsSync(absPath)) {
await this.transactionManager.record(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];
@@ -2029,7 +2031,7 @@ export class AgentExecutor {
} else {
report.push(`❌ File not found: ${relPath}`);
}
} catch (err: any) {
} catch (err: any) {
throw new FileSystemError(`Failed to edit file ${relPath}: ${err.message}`, relPath, err);
}
}
@@ -2047,7 +2049,7 @@ export class AgentExecutor {
} else {
report.push(`⚠️ Delete failed: ${relPath} not found`);
}
} catch (err: any) {
} catch (err: any) {
throw new FileSystemError(`Failed to delete file ${relPath}: ${err.message}`, relPath, err);
}
}
@@ -2094,7 +2096,7 @@ export class AgentExecutor {
.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)";
}
@@ -2118,11 +2120,11 @@ export class AgentExecutor {
.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: 'system', content: `[Result of list_brain ${relPath}]\n${listing}`, internal: true });
} else {
@@ -2138,7 +2140,7 @@ export class AgentExecutor {
const brainDir = activeBrainDir;
const files = findBrainFiles(brainDir);
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}`);
@@ -2201,4 +2203,4 @@ export class AgentExecutor {
logError('Second Brain sync failed.', err);
}
}
}
}