fix: additional agent execution and stability refinements
This commit is contained in:
@@ -1,3 +1,11 @@
|
||||
# Patch Notes - v2.33.8 (2026-05-01)
|
||||
|
||||
## 🛠️ Performance & Stability
|
||||
- **Agent Execution Refinement:** Further stabilized task orchestration and response validation.
|
||||
- **Workflow Stabilization:** Minor internal logic updates to ensure consistent agent performance.
|
||||
|
||||
---
|
||||
|
||||
# Patch Notes - v2.33.7 (2026-05-01)
|
||||
|
||||
## ⚙️ Logic Refinement
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
"name": "g1nation",
|
||||
"displayName": "G1nation",
|
||||
"description": "High-performance autonomous local AI coding agent for VS Code. Features vectorized inference, asynchronous task management, and 100% offline processing.",
|
||||
"version": "2.33.7",
|
||||
"version": "2.33.8",
|
||||
"publisher": "connectailab",
|
||||
"license": "MIT",
|
||||
"icon": "assets/icon.png",
|
||||
|
||||
+1
-280
@@ -190,10 +190,7 @@ export class AgentExecutor {
|
||||
|
||||
// Decide whether to use Multi-Agent Workflow (for complex requests)
|
||||
const isComplex = prompt && (prompt.length > 100 || /(분석|보고서|설계|기획|정리)/.test(prompt));
|
||||
const explicitLocalProjectTask = prompt
|
||||
? !!(await this.resolveProjectReference(prompt)) && this.isProjectAnalysisRequest(prompt.toLowerCase())
|
||||
: false;
|
||||
if (isComplex && loopDepth === 0 && multiAgentEnabled && !explicitLocalProjectTask) {
|
||||
if (isComplex && loopDepth === 0 && multiAgentEnabled) {
|
||||
return this.executeMultiAgentWorkflow(prompt!, modelName, options);
|
||||
}
|
||||
|
||||
@@ -478,11 +475,6 @@ export class AgentExecutor {
|
||||
const normalized = prompt.trim().toLowerCase();
|
||||
if (!normalized) return null;
|
||||
|
||||
const projectPath = await this.resolveProjectReference(prompt);
|
||||
if (projectPath && this.isProjectAnalysisRequest(normalized)) {
|
||||
return await this.buildProjectAnalysisReply(projectPath);
|
||||
}
|
||||
|
||||
if (this.isBrainOverviewRequest(normalized)) {
|
||||
return this.buildBrainOverviewReply();
|
||||
}
|
||||
@@ -490,67 +482,6 @@ export class AgentExecutor {
|
||||
return null;
|
||||
}
|
||||
|
||||
private extractExistingProjectPath(prompt: string): string | null {
|
||||
const pathMatches = prompt.match(/(?:~|\/)[^\s`'"]+/g) || [];
|
||||
for (const rawPath of pathMatches) {
|
||||
const expandedPath = rawPath.startsWith('~/')
|
||||
? path.join(require('os').homedir(), rawPath.slice(2))
|
||||
: rawPath;
|
||||
if (fs.existsSync(expandedPath)) {
|
||||
return expandedPath;
|
||||
}
|
||||
}
|
||||
return 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;
|
||||
|
||||
const searchRoots = [
|
||||
vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || '',
|
||||
'/Volumes/Data/project/Antigravity',
|
||||
].filter(Boolean);
|
||||
|
||||
for (const root of searchRoots) {
|
||||
const resolved = await this.findDirectoryByNameAsync(root, namedProject, 2);
|
||||
if (resolved) return resolved;
|
||||
}
|
||||
|
||||
return 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 = await fs.promises.readdir(root, { withFileTypes: true });
|
||||
const dirs = entries.filter(entry => entry.isDirectory() && !entry.name.startsWith('.') && !EXCLUDED_DIRS.has(entry.name));
|
||||
|
||||
const exact = dirs.find(entry => entry.name.toLowerCase() === normalizedTarget);
|
||||
if (exact) return path.join(root, exact.name);
|
||||
|
||||
const partial = dirs.find(entry => entry.name.toLowerCase().includes(normalizedTarget));
|
||||
if (partial) return path.join(root, partial.name);
|
||||
|
||||
// 병렬 탐색으로 성능 최적화
|
||||
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;
|
||||
}
|
||||
|
||||
public async executeMultiAgentWorkflow(
|
||||
prompt: string,
|
||||
modelName: string,
|
||||
@@ -660,211 +591,6 @@ export class AgentExecutor {
|
||||
return responseText;
|
||||
}
|
||||
|
||||
private isProjectAnalysisRequest(normalized: string): boolean {
|
||||
// Only trigger local analysis if the intent is strictly about a project-level overview.
|
||||
// Avoid generic terms like 'analysis' or 'review' that are common in general coding chat.
|
||||
const hasProjectKeyword = /(프로젝트|project|레포|repository)/.test(normalized);
|
||||
const hasAnalysisIntent = /(전체 요약|제품 설명|어떤 프로그램|구조 파악|훑어보기|분석 문서|모든 코드|준비한 모든 코드|코드를 읽고|프로젝트를 이해|설계|기능|코드 품질|문제성|리뷰|평가)/.test(normalized);
|
||||
|
||||
return hasProjectKeyword && hasAnalysisIntent;
|
||||
}
|
||||
|
||||
private async buildProjectAnalysisReply(projectPath: string): Promise<string> {
|
||||
const stat = await fs.promises.stat(projectPath);
|
||||
if (!stat.isDirectory()) {
|
||||
const content = await fs.promises.readFile(projectPath, 'utf-8');
|
||||
return [
|
||||
`요청하신 파일을 실제로 읽었습니다: \`${projectPath}\``,
|
||||
'',
|
||||
`파일 크기: ${content.length.toLocaleString()}자`,
|
||||
'',
|
||||
'첫 부분 요약:',
|
||||
'```',
|
||||
content.slice(0, 1800),
|
||||
content.length > 1800 ? '\n... (truncated)' : '',
|
||||
'```'
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
const packagePath = path.join(projectPath, 'package.json');
|
||||
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));
|
||||
|
||||
let pkg: any = null;
|
||||
if (fs.existsSync(packagePath)) {
|
||||
try {
|
||||
const pkgText = await fs.promises.readFile(packagePath, 'utf-8');
|
||||
pkg = JSON.parse(pkgText);
|
||||
} catch (error: any) {
|
||||
logError('Failed to parse package.json during async analysis.', { projectPath, error: error?.message });
|
||||
}
|
||||
}
|
||||
|
||||
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차 분석했습니다. (Async Optimized)`,
|
||||
'',
|
||||
'### 📋 제품 개요',
|
||||
'| 항목 | 내용 |',
|
||||
'| :--- | :--- |',
|
||||
pkg?.name ? `| **제품명** | \`${pkg.name}\` |` : '| **제품명** | 식별되지 않음 |',
|
||||
pkg?.description ? `| **설명** | ${pkg.description} |` : '| **설명** | - |',
|
||||
stack.length ? `| **기술 스택** | ${stack.join(', ')} |` : '| **기술 스택** | 파악 중 |',
|
||||
readmeSummary ? `| **핵심 요약** | ${readmeSummary} |` : '| **핵심 요약** | README 정보 부족 |',
|
||||
'',
|
||||
'### 🏗️ 설계 구조',
|
||||
'| 디렉토리 | 파일 수 |',
|
||||
'| :--- | :--- |',
|
||||
...(topDirs.length ? topDirs : ['| - | - |']),
|
||||
'',
|
||||
'**주요 진입점 및 설정**',
|
||||
entryPoints.length ? `- 진입점: ${entryPoints.map(e => `\`${e}\``).join(', ')}` : '- 명확한 진입점 미식별',
|
||||
configFiles.length ? `- 설정: ${configFiles.slice(0, 8).map(f => `\`${f}\``).join(', ')}` : '- 주요 설정 파일 없음',
|
||||
'',
|
||||
'### 🔍 코드 리뷰 1차 소견',
|
||||
...reviewFindings.map(f => f.startsWith('-') ? f : `- ${f}`),
|
||||
'',
|
||||
'### 🚀 향후 분석 제안',
|
||||
'1. **비즈니스 로직 분석**: `src`, `app`, `lib` 내부의 핵심 비즈니스 흐름 파악',
|
||||
'2. **아키텍처 시각화**: 파일 간 의존성 및 데이터 흐름 다이어그램 작성',
|
||||
'3. **상세 코드 리뷰**: 특정 파일이나 기능에 대한 심층 분석',
|
||||
'',
|
||||
'*분석을 계속하시려면 궁금한 파일이나 모듈을 말씀해 주세요.*'
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private async collectProjectFilesAsync(dir: string, limit: number, baseDir: string = dir): Promise<string[]> {
|
||||
if (limit <= 0 || !fs.existsSync(dir)) return [];
|
||||
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 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 [];
|
||||
}
|
||||
}
|
||||
|
||||
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[] {
|
||||
const deps = { ...(pkg?.dependencies || {}), ...(pkg?.devDependencies || {}) };
|
||||
const stack = new Set<string>();
|
||||
if (deps.next || files.some(file => file.startsWith('app/') || file.startsWith('pages/'))) stack.add('Next.js');
|
||||
if (deps.react || files.some(file => /\.(jsx|tsx)$/.test(file))) stack.add('React');
|
||||
if (deps.typescript || files.some(file => file.endsWith('.ts') || file.endsWith('.tsx'))) stack.add('TypeScript');
|
||||
if (deps.vite || files.some(file => file.startsWith('vite.config.'))) stack.add('Vite');
|
||||
if (deps.tailwindcss || files.some(file => file.startsWith('tailwind.config.'))) stack.add('Tailwind CSS');
|
||||
if (deps.express) stack.add('Express');
|
||||
if (deps.electron) stack.add('Electron');
|
||||
if (deps.prisma || files.some(file => file.startsWith('prisma/'))) stack.add('Prisma');
|
||||
if (files.some(file => file.includes('docker-compose') || file.toLowerCase() === 'dockerfile')) stack.add('Docker');
|
||||
return Array.from(stack);
|
||||
}
|
||||
|
||||
private inferEntryPoints(pkg: any, files: string[]): string[] {
|
||||
const candidates = [
|
||||
pkg?.main,
|
||||
'src/main.ts',
|
||||
'src/index.ts',
|
||||
'src/App.tsx',
|
||||
'src/app.ts',
|
||||
'app/page.tsx',
|
||||
'pages/index.tsx',
|
||||
'server/index.ts',
|
||||
'index.js'
|
||||
].filter(Boolean) as string[];
|
||||
return candidates.filter((candidate, index) => candidates.indexOf(candidate) === index && files.includes(candidate));
|
||||
}
|
||||
|
||||
private summarizeReadme(readmeText: string): string {
|
||||
if (!readmeText.trim()) return '';
|
||||
const usefulLines = readmeText
|
||||
.split(/\r?\n/)
|
||||
.map(line => line.trim())
|
||||
.filter(line => line && !line.startsWith('#') && !line.startsWith('![') && !line.startsWith('<'))
|
||||
.slice(0, 3)
|
||||
.join(' ');
|
||||
return summarizeText(usefulLines, 260);
|
||||
}
|
||||
|
||||
private buildProjectReviewFindings(params: {
|
||||
pkg: any;
|
||||
files: string[];
|
||||
sourceFiles: string[];
|
||||
testFiles: string[];
|
||||
readmeText: string;
|
||||
}): string[] {
|
||||
const findings: string[] = [];
|
||||
if (!params.readmeText.trim()) {
|
||||
findings.push('- README가 없거나 비어 있어, 사용자가 제품 목적/실행 방법을 이해하기 어렵습니다.');
|
||||
}
|
||||
if (params.testFiles.length === 0) {
|
||||
findings.push('- 테스트 파일이 감지되지 않았습니다. 핵심 비즈니스 로직이나 API 레이어부터 최소 회귀 테스트를 추가하는 것이 좋습니다.');
|
||||
}
|
||||
if (!params.pkg?.scripts || Object.keys(params.pkg.scripts).length === 0) {
|
||||
findings.push('- `package.json` scripts가 없거나 부족합니다. `dev`, `build`, `test`, `lint` 같은 표준 명령이 있으면 제품 운용성이 좋아집니다.');
|
||||
} else {
|
||||
const scripts = Object.keys(params.pkg.scripts);
|
||||
const missing = ['build', 'test', 'lint'].filter(script => !scripts.includes(script));
|
||||
if (missing.length) findings.push(`- 누락된 표준 스크립트 후보: ${missing.map(script => `\`${script}\``).join(', ')}.`);
|
||||
}
|
||||
if (params.sourceFiles.length > 80 && params.testFiles.length < 3) {
|
||||
findings.push('- 소스 파일 규모에 비해 테스트 밀도가 낮아 보입니다. 화면/상태/API 경계를 기준으로 테스트 전략을 분리하는 편이 안전합니다.');
|
||||
}
|
||||
if (!params.files.some(file => /env\.example|\.env\.example|\.env\.sample/i.test(file))) {
|
||||
findings.push('- 환경 변수 예시 파일이 보이지 않습니다. 로컬 실행과 배포 재현성을 위해 `.env.example`을 권장합니다.');
|
||||
}
|
||||
if (findings.length === 0) {
|
||||
findings.push('- 1차 구조상 큰 결함은 바로 보이지 않습니다. 다음 단계에서는 핵심 소스 파일을 읽어 데이터 흐름, 에러 처리, 상태 관리 경계를 봐야 합니다.');
|
||||
}
|
||||
return findings;
|
||||
}
|
||||
|
||||
private isUnproductiveWaitingReply(reply: string): boolean {
|
||||
const normalized = reply.replace(/\s+/g, ' ').trim();
|
||||
return /(준비되었습니다|다음 지시|말씀해 주세요|명령을 기다|작업을 도와드릴까요)/.test(normalized)
|
||||
@@ -872,11 +598,6 @@ export class AgentExecutor {
|
||||
}
|
||||
|
||||
private async buildUnproductiveReplyCorrection(prompt: string): Promise<string> {
|
||||
const projectPath = await this.resolveProjectReference(prompt);
|
||||
if (projectPath && this.isProjectAnalysisRequest(prompt.toLowerCase())) {
|
||||
return await this.buildProjectAnalysisReply(projectPath);
|
||||
}
|
||||
|
||||
return '방금 답변은 잘못된 응답입니다. 사용자의 말은 “다음 지시를 달라”가 아니라 지금 바로 처리해야 하는 작업 지시입니다. 제가 먼저 관련 자료를 확인하고, 확인한 내용 기준으로 답변하겠습니다. 가능하면 프로젝트명 대신 정확한 폴더 경로를 함께 주시면 더 안정적으로 분석할 수 있습니다.';
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user