diff --git a/package.json b/package.json index f08a6fc..0619346 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "g1nation", "displayName": "G1nation", "description": "100% local AI coding agent for VS Code. Create files, edit code, run commands, and work offline with Ollama or LM Studio.", - "version": "2.13.0", + "version": "2.22.0", "publisher": "connectailab", "license": "MIT", "icon": "assets/icon.png", diff --git a/src/agent.ts b/src/agent.ts index 2d90e5c..8c1242f 100644 --- a/src/agent.ts +++ b/src/agent.ts @@ -20,6 +20,7 @@ import { validatePath, sanitizeCommand } from './security'; import { TransactionManager } from './core/transaction'; import { SessionManager } from './core/session'; import { PlannerAgent, ResearcherAgent, WriterAgent } from './agents/factory'; +import { AgentWorkflowManager } from './agents/AgentWorkflowManager'; import { ErrorTranslator } from './core/errorHandler'; import { AgentExecutionError, @@ -534,42 +535,38 @@ export class AgentExecutor { options: any ) { if (!this.webview) return; - this.stop(); // Abort any previous run + this.stop(); this.abortController = new AbortController(); const signal = this.abortController.signal; - this.statusBarManager.updateStatus(AgentStatus.Thinking, 'Multi-Agent Workflow Started'); + this.statusBarManager.updateStatus(AgentStatus.Thinking, 'Multi-Agent Workflow Running'); this.webview.postMessage({ type: 'streamStart' }); try { - // Instantiate decoupled agents - const planner = new PlannerAgent(modelName); - const researcher = new ResearcherAgent(modelName); - const writer = new WriterAgent(modelName); + let brainContext = 'No specific context available'; + try { + const activeBrain = getActiveBrainProfile(); + const brainFiles = findBrainFiles(activeBrain.localBrainPath); + brainContext = `Brain: ${activeBrain.name}, Files: ${brainFiles.length}`; + } catch (ctxErr) { + logError('Failed to load brain context for agents', ctxErr); + } - // Prepare Context - const activeBrain = getActiveBrainProfile(); - const brainFiles = findBrainFiles(activeBrain.localBrainPath); - const brainContext = `Brain: ${activeBrain.name}, Files: ${brainFiles.length}`; + // 워크플로우 매니저에게 실행 위임 (Strict Synchronization & Contract) + const finalReport = await AgentWorkflowManager.runStrictWorkflow( + prompt, + modelName, + brainContext, + signal, + (step, msg) => { + this.webview.postMessage({ type: 'autoContinue', value: `${step}: ${msg}` }); + // 각 단계별 시작을 알림 + this.webview.postMessage({ type: 'streamChunk', value: `\n\n> **[${step}]** ${msg}\n\n` }); + } + ); - // --- Phase 1: Planner --- if (signal.aborted) return; - this.webview.postMessage({ type: 'autoContinue', value: 'Planner: 전략 수립 중...' }); - const plan = await planner.execute(prompt, brainContext, signal); - this.webview.postMessage({ type: 'streamChunk', value: `\n\n### 📝 작업 계획 (Execution Plan)\n${plan}\n\n` }); - - // --- Phase 2: Researcher --- - if (signal.aborted) return; - this.webview.postMessage({ type: 'autoContinue', value: 'Researcher: 지식 검색 중...' }); - const research = await researcher.execute(plan, brainContext, signal); - this.webview.postMessage({ type: 'streamChunk', value: `\n\n### 🔍 분석 결과 (Research Findings)\n*(정보 수집 및 정제 완료)*\n\n` }); - - // --- Phase 3: Writer --- - if (signal.aborted) return; - this.webview.postMessage({ type: 'autoContinue', value: 'Writer: 보고서 작성 중...' }); - const finalReport = await writer.execute(research, prompt, signal); - if (signal.aborted) return; this.webview.postMessage({ type: 'streamChunk', value: `\n\n--- \n\n${finalReport}` }); this.webview.postMessage({ type: 'streamEnd' }); @@ -577,16 +574,17 @@ export class AgentExecutor { this.emitHistoryChanged(); this.statusBarManager.updateStatus(AgentStatus.Success, 'Workflow Complete'); - this.webview.postMessage({ type: 'autoContinue', value: '✅ 분석이 완료되었습니다!' }); + this.webview.postMessage({ type: 'autoContinue', value: '✅ 모든 분석이 성공적으로 완료되었습니다.' }); } catch (error: any) { + if (error.name === 'AbortError' || error.message?.includes('cancelled')) { + this.statusBarManager.updateStatus(AgentStatus.Idle, 'Workflow Cancelled'); + return; + } const friendly = ErrorTranslator.translate(error); logError('Workflow failed', error); - // Clear autoContinue state by sending empty value or specific type this.webview.postMessage({ type: 'autoContinue', value: '' }); - - // Format error using guideline-compliant UI (Red color scheme) this.webview.postMessage({ type: 'error', value: `### ${friendly.title}\n\n**상태:** ${friendly.message}\n\n**해결 방법:** ${friendly.action}` diff --git a/src/agents/AgentWorkflowManager.ts b/src/agents/AgentWorkflowManager.ts new file mode 100644 index 0000000..b95a427 --- /dev/null +++ b/src/agents/AgentWorkflowManager.ts @@ -0,0 +1,72 @@ +import * as vscode from 'vscode'; +import { PlannerAgent, ResearcherAgent, WriterAgent } from './factory'; + +/** + * 에이전트 간의 데이터 인계 계약(Contract) 정의 + */ +export interface AgentResult { + step: string; + content: string; + timestamp: number; + success: boolean; +} + +export class AgentWorkflowManager { + /** + * 멀티 에이전트 워크플로우를 강력한 동기화(Synchronization) 하에 실행합니다. + */ + public static async runStrictWorkflow( + prompt: string, + modelName: string, + brainContext: string, + signal: AbortSignal, + onProgress: (step: string, message: string) => void + ): Promise { + + // 1. 에이전트 인스턴스화 + const planner = new PlannerAgent(modelName); + const researcher = new ResearcherAgent(modelName); + const writer = new WriterAgent(modelName); + + try { + // --- Phase 1: Planner (Decomposition & Strategy) --- + if (signal.aborted) throw new Error('AbortError'); + onProgress('Planner', '전략 분석 및 작업 분해 중...'); + const plan = await planner.execute(prompt, brainContext, signal); + this.validateResult(plan, 'Planner'); + + // --- Phase 2: Researcher (Fact Harvesting) --- + if (signal.aborted) throw new Error('AbortError'); + // 동기화를 위한 의도적 미세 지연 (서버 부하 분산) + await new Promise(r => setTimeout(r, 800)); + onProgress('Researcher', '데이터 수집 및 핵심 정보 추출 중...'); + const research = await researcher.execute(plan, brainContext, signal); + this.validateResult(research, 'Researcher'); + + // --- Phase 3: Writer (Final Synthesis) --- + if (signal.aborted) throw new Error('AbortError'); + await new Promise(r => setTimeout(r, 800)); + onProgress('Writer', '수집된 정보를 바탕으로 최종 리포트 작성 중...'); + const finalReport = await writer.execute(research, prompt, signal); + this.validateResult(finalReport, 'Writer'); + + return finalReport; + + } catch (error: any) { + if (error.name === 'AbortError' || error.message.includes('cancelled')) { + throw error; + } + throw new Error(`[Workflow Manager] ${error.message}`); + } + } + + /** + * 데이터 정합성(Data Integrity) 검증 + */ + private static validateResult(data: string, step: string) { + if (!data || data.trim().length < 20) { + const preview = data ? `(Content: "${data.substring(0, 100)}...")` : '(Empty Response)'; + throw new Error(`${step} 단계에서 생성된 데이터가 불충분합니다. ${preview} 모델을 더 똑똑한 것으로 변경해 보세요.`); + } + } +} diff --git a/src/agents/factory.ts b/src/agents/factory.ts index a526a9c..b46c33a 100644 --- a/src/agents/factory.ts +++ b/src/agents/factory.ts @@ -6,27 +6,45 @@ export abstract class BaseAgent { protected async callLLM(persona: string, prompt: string, signal?: AbortSignal): Promise { const { ollamaUrl } = getConfig(); + if (!ollamaUrl) { + throw new Error('Ollama URL이 설정되지 않았습니다. 설정을 확인해주세요.'); + } + + if (typeof fetch === 'undefined') { + throw new Error('이 환경에서는 fetch 함수를 사용할 수 없습니다. Node.js 버전을 확인하거나 polyfill이 필요합니다.'); + } + const messages = [ { role: 'system', content: persona }, { role: 'user', content: prompt } ]; - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), 45000); // Increased to 45s for complex tasks + // 엔진 자동 감지 (Ollama vs OpenAI/LM Studio) + const isOllama = ollamaUrl.includes(':11434') || ollamaUrl.includes('ollama'); + const endpoint = isOllama ? `${ollamaUrl}/api/chat` : `${ollamaUrl}/v1/chat/completions`; - // Combine external signal with local timeout - const combinedSignal = signal ? - anySignal([signal, controller.signal]) : - controller.signal; + let lastError: any; + for (let attempt = 1; attempt <= 3; attempt++) { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 45000); + const combinedSignal = signal ? anySignal([signal, controller.signal]) : controller.signal; - try { - const response = await fetch(`${ollamaUrl}/api/chat`, { + try { + if (attempt > 1) await new Promise(resolve => setTimeout(resolve, 1000 * attempt)); + + const response = await fetch(endpoint, { method: 'POST', - body: JSON.stringify({ + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(isOllama ? { model: this.modelName, messages, stream: false, options: { temperature: 0.3 } + } : { + model: this.modelName, + messages, + stream: false, + temperature: 0.3 }), signal: combinedSignal }); @@ -37,15 +55,25 @@ export abstract class BaseAgent { throw new Error(`Agent API Error: ${response.statusText} (${response.status})`); } - const data = await response.json() as any; - return data.message?.content || data.choices?.[0]?.message?.content || ''; - } catch (error: any) { - clearTimeout(timeoutId); - if (error.name === 'AbortError') { - throw new Error('Agent request was cancelled or timed out.'); + const data = await response.json() as any; + + // 강력한 응답 추출 (Multi-path parsing) + let content = ''; + if (data.message?.content) content = data.message.content; + else if (data.choices?.[0]?.message?.content) content = data.choices[0].message.content; + else if (data.choices?.[0]?.text) content = data.choices[0].text; + else if (data.response) content = data.response; + else if (typeof data === 'string') content = data; + + return content || ''; + } catch (error: any) { + clearTimeout(timeoutId); + lastError = error; + if (error.name === 'AbortError') break; + if (attempt === 3) break; } - throw error; } + throw lastError; } abstract execute(input: string, context?: string, signal?: AbortSignal): Promise; @@ -65,22 +93,55 @@ function anySignal(signals: AbortSignal[]): AbortSignal { } export class PlannerAgent extends BaseAgent { - private readonly persona = `You are the [Planner Agent]. Analyze the request and output a structured .`; + private readonly persona = `You are the [Master Strategist & Planner]. +Your sole purpose is to transform vague requests into flawless, high-resolution execution blueprints. +- THINKING PROCESS: You must analyze the request from multiple angles (technical, logical, structural). +- OUTPUT RULE: You MUST output a structured using Markdown. +- COMPONENTS: Each blueprint must have [Objective], [Core Challenges], [Data Requirements], and [Step-by-Step Research Tasks]. +- CONSTRAINT: Do not be vague. Use professional terminology. If the request is too simple, expand it with relevant technical considerations.`; + async execute(input: string, brainContext?: string, signal?: AbortSignal): Promise { - return this.callLLM(this.persona, `Request: ${input}\nContext: ${brainContext}`, signal); + const wrappedInput = `### SYSTEM INSTRUCTION: GENERATE EXECUTION BLUEPRINT +1. Target Goal: ${input} +2. Available Knowledge Base: ${brainContext} +3. Mission: Create a comprehensive research roadmap.`; + return this.callLLM(this.persona, wrappedInput, signal); } } export class ResearcherAgent extends BaseAgent { - private readonly persona = `You are the [Researcher Agent]. Gather facts based on the plan.`; + private readonly persona = `You are the [Senior Technical Researcher]. +Your mission is to extract, filter, and synthesize critical data based on a strategic blueprint. +- DATA INTEGRITY: Only provide high-quality, verified-style information. +- FORMAT: Use [Key Facts], [Technical Deep-Dive], and [Summary of Knowledge] sections. +- CRITICAL THINKING: Identify gaps in the plan and provide extra insights to fill those gaps. +- NO FLUFF: Be concise but extremely dense with information.`; + async execute(input: string, brainContext?: string, signal?: AbortSignal): Promise { - return this.callLLM(this.persona, `Plan: ${input}\nContext: ${brainContext}`, signal); + const wrappedInput = `### SYSTEM INSTRUCTION: DATA HARVESTING +1. Blueprint to Follow: ${input} +2. Contextual Constraints: ${brainContext} +3. Mission: Provide a dense summary of facts and technical insights.`; + return this.callLLM(this.persona, wrappedInput, signal); } } export class WriterAgent extends BaseAgent { - private readonly persona = `You are the [Writer Agent]. Synthesize research into a final report.`; + private readonly persona = `You are the [Lead Synthesis Writer & Editor]. +Your goal is to produce a state-of-the-art final report that wows the user. +- TONE: Authoritative yet accessible. Professional developer/consultant style. +- STRUCTURE: Use an executive summary, detailed analysis sections, and a "Final Recommendation" block. +- LANGUAGE: Always respond in the user's language (KOREAN). +- POLISHING: Ensure logical flow between sections. Make it look like a premium report.`; + async execute(input: string, originalRequest?: string, signal?: AbortSignal): Promise { - return this.callLLM(this.persona, `Data: ${input}\nOriginal Request: ${originalRequest}`, signal); + // Fix 3: Trim input if it's too long (Basic Context Diet) + const trimmedData = input.length > 8000 ? input.substring(0, 8000) + '... [Data Trimmed for Performance]' : input; + + const wrappedInput = `### SYSTEM INSTRUCTION: FINAL SYNTHESIS +1. Gathered Research Data: ${trimmedData} +2. User's Original Objective: ${originalRequest} +3. Mission: Write the definitive final report in KOREAN.`; + return this.callLLM(this.persona, wrappedInput, signal); } } diff --git a/src/core/errorHandler.ts b/src/core/errorHandler.ts index c8fc5e9..17f8ee2 100644 --- a/src/core/errorHandler.ts +++ b/src/core/errorHandler.ts @@ -16,6 +16,14 @@ export class ErrorTranslator { }; } + if (msg.includes('유효한 데이터를 생성하지 못했습니다') || msg.includes('incomplete inference')) { + return { + title: '🧩 추론 분석 실패 (Inference Failed)', + message: '에이전트가 단계를 분석하는 중 유효한 응답을 생성하지 못했습니다.', + action: '1. 성능이 더 좋은 모델로 변경\n2. 질문 내용을 더 상세하게 작성\n3. 다시 시도' + }; + } + if (msg.includes('timeout')) { return { title: '⏱️ 응답 시간 초과 (Timeout)', @@ -33,9 +41,9 @@ export class ErrorTranslator { } return { - title: '⚠️ 알 수 없는 오류', - message: '작업 중 예상치 못한 문제가 발생했습니다.', - action: '로그를 확인하거나 확장을 재시작해보세요.' + title: '⚠️ 시스템 내부 오류', + message: `작업 중 예상치 못한 문제가 발생했습니다.\n(Error: ${error.message || error})`, + action: '1. 확장을 재시작하거나 로그를 확인해 주세요.\n2. 위 에러 메시지를 개발자에게 전달해 주세요.' }; } }