diff --git a/PATCHNOTES.md b/PATCHNOTES.md index 0fb1e63..bfe526c 100644 --- a/PATCHNOTES.md +++ b/PATCHNOTES.md @@ -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 diff --git a/package.json b/package.json index f995cae..66c257b 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/agent.ts b/src/agent.ts index 682e753..ae311de 100644 --- a/src/agent.ts +++ b/src/agent.ts @@ -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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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(); - 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 { - const projectPath = await this.resolveProjectReference(prompt); - if (projectPath && this.isProjectAnalysisRequest(prompt.toLowerCase())) { - return await this.buildProjectAnalysisReply(projectPath); - } - return '๋ฐฉ๊ธˆ ๋‹ต๋ณ€์€ ์ž˜๋ชป๋œ ์‘๋‹ต์ž…๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž์˜ ๋ง์€ โ€œ๋‹ค์Œ ์ง€์‹œ๋ฅผ ๋‹ฌ๋ผโ€๊ฐ€ ์•„๋‹ˆ๋ผ ์ง€๊ธˆ ๋ฐ”๋กœ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•˜๋Š” ์ž‘์—… ์ง€์‹œ์ž…๋‹ˆ๋‹ค. ์ œ๊ฐ€ ๋จผ์ € ๊ด€๋ จ ์ž๋ฃŒ๋ฅผ ํ™•์ธํ•˜๊ณ , ํ™•์ธํ•œ ๋‚ด์šฉ ๊ธฐ์ค€์œผ๋กœ ๋‹ต๋ณ€ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค. ๊ฐ€๋Šฅํ•˜๋ฉด ํ”„๋กœ์ ํŠธ๋ช… ๋Œ€์‹  ์ •ํ™•ํ•œ ํด๋” ๊ฒฝ๋กœ๋ฅผ ํ•จ๊ป˜ ์ฃผ์‹œ๋ฉด ๋” ์•ˆ์ •์ ์œผ๋กœ ๋ถ„์„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.'; }