fix: v2.22.0 - stable multi-agent workflow (API compatibility & architecture refactor)

This commit is contained in:
Wonseok Jung
2026-04-30 00:58:00 +09:00
parent 7430c91177
commit c69ba168fa
5 changed files with 195 additions and 56 deletions
+1 -1
View File
@@ -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",
+28 -30
View File
@@ -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}`
+72
View File
@@ -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<string> {
// 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} 모델을 더 똑똑한 것으로 변경해 보세요.`);
}
}
}
+83 -22
View File
@@ -6,27 +6,45 @@ export abstract class BaseAgent {
protected async callLLM(persona: string, prompt: string, signal?: AbortSignal): Promise<string> {
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<string>;
@@ -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 <plan>.`;
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 <blueprint> 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<string> {
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<string> {
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<string> {
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);
}
}
+11 -3
View File
@@ -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. 위 에러 메시지를 개발자에게 전달해 주세요.'
};
}
}