fix: v2.22.0 - stable multi-agent workflow (API compatibility & architecture refactor)
This commit is contained in:
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user