feat: implement next-gen vectorized engine, async architecture, and modernization roadmap v2.32.0
This commit is contained in:
+75
-57
@@ -22,6 +22,7 @@ import { SessionManager } from './core/session';
|
||||
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,
|
||||
@@ -148,6 +149,7 @@ export class AgentExecutor {
|
||||
public async approveTransaction() {
|
||||
if (!this.transactionManager.isActive()) return;
|
||||
this.transactionManager.commit();
|
||||
agentEvents.emit(AgentEventTypes.TRANSACTION_COMMITTED);
|
||||
this.statusBarManager.updateStatus(AgentStatus.Success, 'Changes committed.');
|
||||
this.webview?.postMessage({ type: 'streamChunk', value: '\n✅ **작업이 승인되어 반영되었습니다.**' });
|
||||
}
|
||||
@@ -155,6 +157,7 @@ export class AgentExecutor {
|
||||
public async rejectTransaction() {
|
||||
if (!this.transactionManager.isActive()) return;
|
||||
this.transactionManager.rollback();
|
||||
agentEvents.emit(AgentEventTypes.TRANSACTION_ROLLED_BACK);
|
||||
this.statusBarManager.updateStatus(AgentStatus.Idle, 'Changes rolled back.');
|
||||
this.webview?.postMessage({ type: 'streamChunk', value: '\n❌ **작업이 거부되어 모든 변경사항이 취소되었습니다.**' });
|
||||
}
|
||||
@@ -399,7 +402,7 @@ export class AgentExecutor {
|
||||
|
||||
if (report.length === 0 && loopDepth === 0 && this.isUnproductiveWaitingReply(aiResponseText)) {
|
||||
assistantMessage.internal = false;
|
||||
const correctedReply = this.buildUnproductiveReplyCorrection(prompt || '');
|
||||
const correctedReply = await this.buildUnproductiveReplyCorrection(prompt || '');
|
||||
assistantMessage.content = correctedReply;
|
||||
this.emitHistoryChanged();
|
||||
this.webview.postMessage({ type: 'streamChunk', value: correctedReply });
|
||||
@@ -459,9 +462,9 @@ export class AgentExecutor {
|
||||
const normalized = prompt.trim().toLowerCase();
|
||||
if (!normalized) return null;
|
||||
|
||||
const projectPath = this.resolveProjectReference(prompt);
|
||||
const projectPath = await this.resolveProjectReference(prompt);
|
||||
if (projectPath && this.isProjectAnalysisRequest(normalized)) {
|
||||
return this.buildProjectAnalysisReply(projectPath);
|
||||
return await this.buildProjectAnalysisReply(projectPath);
|
||||
}
|
||||
|
||||
if (this.isBrainOverviewRequest(normalized)) {
|
||||
@@ -484,12 +487,12 @@ export class AgentExecutor {
|
||||
return null;
|
||||
}
|
||||
|
||||
private resolveProjectReference(prompt: string): string | null {
|
||||
private async resolveProjectReference(prompt: string): Promise<string | null> {
|
||||
const explicitPath = this.extractExistingProjectPath(prompt);
|
||||
if (explicitPath) return explicitPath;
|
||||
|
||||
const namedProject = prompt.match(/([A-Za-z0-9._-]+)\s*(?:프로젝트|project)/i)?.[1];
|
||||
if (!namedProject) return null; // No project keyword found, do not attempt to guess.
|
||||
if (!namedProject) return null;
|
||||
|
||||
const searchRoots = [
|
||||
vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || '',
|
||||
@@ -497,35 +500,38 @@ export class AgentExecutor {
|
||||
].filter(Boolean);
|
||||
|
||||
for (const root of searchRoots) {
|
||||
const resolved = this.findDirectoryByName(root, namedProject, 2); // Depth reduced to 2 for performance and accuracy.
|
||||
const resolved = await this.findDirectoryByNameAsync(root, namedProject, 2);
|
||||
if (resolved) return resolved;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private findDirectoryByName(root: string, targetName: string, maxDepth: number): string | null {
|
||||
/**
|
||||
* 비차단(Non-blocking) 방식의 디렉토리 검색 (Step 2 최적화)
|
||||
*/
|
||||
private async findDirectoryByNameAsync(root: string, targetName: string, maxDepth: number): Promise<string | null> {
|
||||
if (!root || maxDepth < 0 || !fs.existsSync(root)) return null;
|
||||
const normalizedTarget = targetName.toLowerCase();
|
||||
|
||||
try {
|
||||
const entries = fs.readdirSync(root, { withFileTypes: true })
|
||||
.filter(entry => entry.isDirectory() && !entry.name.startsWith('.') && !EXCLUDED_DIRS.has(entry.name));
|
||||
const entries = await fs.promises.readdir(root, { withFileTypes: true });
|
||||
const dirs = entries.filter(entry => entry.isDirectory() && !entry.name.startsWith('.') && !EXCLUDED_DIRS.has(entry.name));
|
||||
|
||||
const exact = entries.find(entry => entry.name.toLowerCase() === normalizedTarget);
|
||||
const exact = dirs.find(entry => entry.name.toLowerCase() === normalizedTarget);
|
||||
if (exact) return path.join(root, exact.name);
|
||||
|
||||
const partial = entries.find(entry => entry.name.toLowerCase().includes(normalizedTarget));
|
||||
const partial = dirs.find(entry => entry.name.toLowerCase().includes(normalizedTarget));
|
||||
if (partial) return path.join(root, partial.name);
|
||||
|
||||
for (const entry of entries) {
|
||||
const found = this.findDirectoryByName(path.join(root, entry.name), targetName, maxDepth - 1);
|
||||
if (found) return found;
|
||||
}
|
||||
} catch (error: any) {
|
||||
logError('Project name search failed.', { root, targetName, error: error?.message || String(error) });
|
||||
}
|
||||
// 병렬 탐색으로 성능 최적화
|
||||
const searchPromises = dirs.map(dir => this.findDirectoryByNameAsync(path.join(root, dir.name), targetName, maxDepth - 1));
|
||||
const results = await Promise.all(searchPromises);
|
||||
return results.find(res => res !== null) || null;
|
||||
|
||||
} catch (error: any) {
|
||||
logError('Async project search failed.', { root, targetName, error: error?.message });
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -559,13 +565,13 @@ export class AgentExecutor {
|
||||
brainContext,
|
||||
signal,
|
||||
(step, msg) => {
|
||||
this.webview.postMessage({ type: 'autoContinue', value: `${step}: ${msg}` });
|
||||
this.webview?.postMessage({ type: 'autoContinue', value: `${step}: ${msg}` });
|
||||
// 각 단계별 시작을 알림
|
||||
this.webview.postMessage({ type: 'streamChunk', value: `\n\n> **[${step}]** ${msg}\n\n` });
|
||||
this.webview?.postMessage({ type: 'streamChunk', value: `\n\n> **[${step}]** ${msg}\n\n` });
|
||||
}
|
||||
);
|
||||
|
||||
if (signal.aborted) return;
|
||||
if (signal.aborted || !this.webview) return;
|
||||
|
||||
this.webview.postMessage({ type: 'streamChunk', value: `\n\n--- \n\n${finalReport}` });
|
||||
this.webview.postMessage({ type: 'streamEnd' });
|
||||
@@ -641,10 +647,10 @@ export class AgentExecutor {
|
||||
return hasProjectKeyword && hasAnalysisIntent;
|
||||
}
|
||||
|
||||
private buildProjectAnalysisReply(projectPath: string): string {
|
||||
const stat = fs.statSync(projectPath);
|
||||
private async buildProjectAnalysisReply(projectPath: string): Promise<string> {
|
||||
const stat = await fs.promises.stat(projectPath);
|
||||
if (!stat.isDirectory()) {
|
||||
const content = fs.readFileSync(projectPath, 'utf-8');
|
||||
const content = await fs.promises.readFile(projectPath, 'utf-8');
|
||||
return [
|
||||
`요청하신 파일을 실제로 읽었습니다: \`${projectPath}\``,
|
||||
'',
|
||||
@@ -659,8 +665,8 @@ export class AgentExecutor {
|
||||
}
|
||||
|
||||
const packagePath = path.join(projectPath, 'package.json');
|
||||
const readmePath = this.findFirstExisting(projectPath, ['README.md', 'readme.md', 'README.MD']);
|
||||
const files = this.collectProjectFiles(projectPath, 600);
|
||||
const readmePath = await this.findFirstExistingAsync(projectPath, ['README.md', 'readme.md', 'README.MD']);
|
||||
const files = await this.collectProjectFilesAsync(projectPath, 600);
|
||||
const sourceFiles = files.filter(file => /\/src\/|\/app\/|\/pages\/|\/components\/|\/lib\//.test(file));
|
||||
const testFiles = files.filter(file => /\.(test|spec)\.[jt]sx?$|\/__tests__\//.test(file));
|
||||
const configFiles = files.filter(file => /(^|\/)(package\.json|tsconfig\.json|vite\.config\.|next\.config\.|tailwind\.config\.|eslint\.config\.|\.eslintrc|dockerfile|docker-compose|README\.md)/i.test(file));
|
||||
@@ -668,21 +674,25 @@ export class AgentExecutor {
|
||||
let pkg: any = null;
|
||||
if (fs.existsSync(packagePath)) {
|
||||
try {
|
||||
pkg = JSON.parse(fs.readFileSync(packagePath, 'utf-8'));
|
||||
const pkgText = await fs.promises.readFile(packagePath, 'utf-8');
|
||||
pkg = JSON.parse(pkgText);
|
||||
} catch (error: any) {
|
||||
logError('Failed to parse package.json during local project analysis.', { projectPath, error: error?.message || String(error) });
|
||||
logError('Failed to parse package.json during async analysis.', { projectPath, error: error?.message });
|
||||
}
|
||||
}
|
||||
|
||||
const readmeText = readmePath ? fs.readFileSync(readmePath, 'utf-8') : '';
|
||||
const topDirs = this.summarizeTopDirectories(projectPath);
|
||||
const readmeText = readmePath ? await fs.promises.readFile(readmePath, 'utf-8') : '';
|
||||
const topDirs = await this.summarizeTopDirectoriesAsync(projectPath);
|
||||
const stack = this.inferStack(pkg, files);
|
||||
const entryPoints = this.inferEntryPoints(pkg, files);
|
||||
const readmeSummary = this.summarizeReadme(readmeText);
|
||||
const reviewFindings = this.buildProjectReviewFindings({ pkg, files, sourceFiles, testFiles, readmeText });
|
||||
|
||||
// Step 3: 데이터 준비 완료 이벤트 발행 (Observer Pattern)
|
||||
agentEvents.emit(AgentEventTypes.DATA_READY, { projectPath, filesCount: files.length });
|
||||
|
||||
return [
|
||||
`제가 실제로 \`${projectPath}\` 폴더를 읽고 1차 분석했습니다.`,
|
||||
`제가 실제로 \`${projectPath}\` 폴더를 읽고 1차 분석했습니다. (Async Optimized)`,
|
||||
'',
|
||||
'### 📋 제품 개요',
|
||||
'| 항목 | 내용 |',
|
||||
@@ -713,7 +723,7 @@ export class AgentExecutor {
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
private findFirstExisting(basePath: string, names: string[]): string | null {
|
||||
private async findFirstExistingAsync(basePath: string, names: string[]): Promise<string | null> {
|
||||
for (const name of names) {
|
||||
const candidate = path.join(basePath, name);
|
||||
if (fs.existsSync(candidate)) return candidate;
|
||||
@@ -721,34 +731,42 @@ export class AgentExecutor {
|
||||
return null;
|
||||
}
|
||||
|
||||
private collectProjectFiles(dir: string, limit: number, baseDir: string = dir): string[] {
|
||||
private async collectProjectFilesAsync(dir: string, limit: number, baseDir: string = dir): Promise<string[]> {
|
||||
if (limit <= 0 || !fs.existsSync(dir)) return [];
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true })
|
||||
.filter(entry => !entry.name.startsWith('.') && !EXCLUDED_DIRS.has(entry.name))
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
const results: string[] = [];
|
||||
try {
|
||||
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
||||
const filtered = entries
|
||||
.filter(entry => !entry.name.startsWith('.') && !EXCLUDED_DIRS.has(entry.name))
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
const results: string[] = [];
|
||||
|
||||
for (const entry of entries) {
|
||||
if (results.length >= limit) break;
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
results.push(...this.collectProjectFiles(fullPath, limit - results.length, baseDir));
|
||||
} else {
|
||||
results.push(path.relative(baseDir, fullPath));
|
||||
for (const entry of filtered) {
|
||||
if (results.length >= limit) break;
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
results.push(...await this.collectProjectFilesAsync(fullPath, limit - results.length, baseDir));
|
||||
} else {
|
||||
results.push(path.relative(baseDir, fullPath));
|
||||
}
|
||||
}
|
||||
return results.slice(0, limit);
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return results.slice(0, limit);
|
||||
}
|
||||
|
||||
private summarizeTopDirectories(projectPath: string): string[] {
|
||||
return fs.readdirSync(projectPath, { withFileTypes: true })
|
||||
.filter(entry => entry.isDirectory() && !entry.name.startsWith('.') && !EXCLUDED_DIRS.has(entry.name))
|
||||
.slice(0, 12)
|
||||
.map(entry => {
|
||||
const count = this.collectProjectFiles(path.join(projectPath, entry.name), 200, projectPath).length;
|
||||
return `| \`${entry.name}/\` | 약 ${count}개 |`;
|
||||
});
|
||||
private async summarizeTopDirectoriesAsync(projectPath: string): Promise<string[]> {
|
||||
const entries = await fs.promises.readdir(projectPath, { withFileTypes: true });
|
||||
const dirs = entries.filter(entry => entry.isDirectory() && !entry.name.startsWith('.') && !EXCLUDED_DIRS.has(entry.name));
|
||||
|
||||
const topDirs = dirs.slice(0, 12);
|
||||
const results = await Promise.all(topDirs.map(async entry => {
|
||||
const files = await this.collectProjectFilesAsync(path.join(projectPath, entry.name), 200, projectPath);
|
||||
return `| \`${entry.name}/\` | 약 ${files.length}개 |`;
|
||||
}));
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private inferStack(pkg: any, files: string[]): string[] {
|
||||
@@ -831,10 +849,10 @@ export class AgentExecutor {
|
||||
&& !/<(list_files|read_file|list_brain|read_brain|run_command|edit_file|create_file)/i.test(reply);
|
||||
}
|
||||
|
||||
private buildUnproductiveReplyCorrection(prompt: string): string {
|
||||
const projectPath = this.resolveProjectReference(prompt);
|
||||
private async buildUnproductiveReplyCorrection(prompt: string): Promise<string> {
|
||||
const projectPath = await this.resolveProjectReference(prompt);
|
||||
if (projectPath && this.isProjectAnalysisRequest(prompt.toLowerCase())) {
|
||||
return this.buildProjectAnalysisReply(projectPath);
|
||||
return await this.buildProjectAnalysisReply(projectPath);
|
||||
}
|
||||
|
||||
return '방금 답변은 잘못된 응답입니다. 사용자의 말은 “다음 지시를 달라”가 아니라 지금 바로 처리해야 하는 작업 지시입니다. 제가 먼저 관련 자료를 확인하고, 확인한 내용 기준으로 답변하겠습니다. 가능하면 프로젝트명 대신 정확한 폴더 경로를 함께 주시면 더 안정적으로 분석할 수 있습니다.';
|
||||
|
||||
Reference in New Issue
Block a user