import * as vscode from 'vscode'; import { getConfig } from '../config'; import { AgentExecuteOptions } from '../lib/engine'; export abstract class BaseAgent { constructor(protected readonly modelName: string) {} 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 } ]; // 엔진 자동 감지 (Ollama vs OpenAI/LM Studio) const isOllama = ollamaUrl.includes(':11434') || ollamaUrl.includes('ollama'); const endpoint = isOllama ? `${ollamaUrl}/api/chat` : `${ollamaUrl}/v1/chat/completions`; // 컨텍스트 초과 방지를 위해 출력 토큰 상한을 항상 명시한다 (서브에이전트 중간 산출물용). const { contextLength, maxOutputTokens } = getConfig(); const numCtx = Math.max(2048, contextLength); const outCap = Math.max(256, maxOutputTokens); 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 { if (attempt > 1) await new Promise(resolve => setTimeout(resolve, 1000 * attempt)); const response = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(isOllama ? { model: this.modelName, messages, stream: false, options: { temperature: 0.3, num_ctx: numCtx, num_predict: outCap } } : { model: this.modelName, messages, stream: false, temperature: 0.3, max_tokens: outCap }), signal: combinedSignal }); clearTimeout(timeoutId); if (!response.ok) { throw new Error(`Agent API Error: ${response.statusText} (${response.status})`); } 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 lastError; } abstract execute(input: string, context?: string, signal?: AbortSignal, options?: AgentExecuteOptions): Promise; } // Helper to combine signals (since AbortSignal.any is not always available in older Node) function anySignal(signals: AbortSignal[]): AbortSignal { const controller = new AbortController(); for (const signal of signals) { if (signal.aborted) { controller.abort(); return signal; } signal.addEventListener('abort', () => controller.abort(), { once: true }); } return controller.signal; } export class PlannerAgent extends BaseAgent { 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, options?: AgentExecuteOptions): Promise { const wrappedInput = `### SYSTEM INSTRUCTION: GENERATE EXECUTION BLUEPRINT 1. Target Goal: ${input} 2. Available Knowledge Base & Policy: ${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 [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, options?: AgentExecuteOptions): Promise { const wrappedInput = `### SYSTEM INSTRUCTION: DATA HARVESTING 1. Blueprint to Follow: ${input} 2. Contextual Constraints & Policy: ${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 [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. - SELF-CORRECTION: When a [REFLECTION CRITIQUE] block is provided, you MUST address each listed gap, contradiction, or missing-evidence item explicitly before producing the final report. Do not silently ignore the critique.`; async execute(input: string, originalRequest?: string, signal?: AbortSignal, options?: AgentExecuteOptions): Promise { // [Astra v4.0] Advisor 모드 처리 if (options?.config?.role === 'advisor') { const advisorPersona = `You are the [Strategic Proactive Advisor]. Analyze the provided report and suggest 3 high-impact next actions for the user. - Focus on decision forks, risk mitigation, or immediate implementation steps. - Be extremely concrete and actionable. - Respond in KOREAN.`; return this.callLLM(advisorPersona, input, 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 policy = options?.context || ''; const reflection = options?.priorResults?.reflection; // Reflector 결과가 있으면 별도 블록으로 주입. 길이 4000자 cap (Writer 입력 비대화 방지). const reflectionBlock = reflection && reflection.trim().length > 0 ? `\n5. [REFLECTION CRITIQUE — must be addressed]:\n${reflection.length > 4000 ? reflection.substring(0, 4000) + '... [Critique Trimmed]' : reflection}` : ''; const wrappedInput = `### SYSTEM INSTRUCTION: FINAL SYNTHESIS 1. Gathered Research Data: ${trimmedData} 2. User's Original Objective: ${originalRequest} 3. Applied Knowledge & Filtering Policy: ${policy} 4. Mission: Write the definitive final report in KOREAN.${reflectionBlock}`; return this.callLLM(this.persona, wrappedInput, signal); } } export class ReflectorAgent extends BaseAgent { private readonly persona = `You are the [Internal Critic & Self-Reflection Officer]. Your sole role is META-COGNITION: stress-test the plan and the research output BEFORE the Writer commits to a final report. - POSTURE: Skeptical, rigorous, blunt. You are looking for what is WRONG, not what is right. - DO NOT: rewrite the report, add new content, or speculate beyond the evidence provided. - DO: surface gaps, unsupported claims, contradictions, drift from the original objective, and missing perspectives. - OUTPUT STRICTLY in this Markdown shape (Korean): ## 🧭 Alignment with Objective - <원래 요청 대비 일치/이탈 평가> ## 🕳 Gaps & Missing Evidence - ## ⚖️ Contradictions / Conflicts - ## 🚨 Unsupported / Weak Claims - <근거가 빈약하거나 일반화된 진술> ## ✅ Guidance for Writer - - CONSTRAINT: 최대 500단어. 새 지식을 만들지 말고, 제공된 자료에서만 판단할 것.`; async execute(input: string, _context?: string, signal?: AbortSignal, options?: AgentExecuteOptions): Promise { const plan = options?.priorResults?.plan || '(plan unavailable)'; const research = input; const originalPrompt = options?.priorResults?.originalPrompt || '(original prompt unavailable)'; const brainContext = options?.context || ''; // Reflector 는 중간 단계이므로 비대한 입력을 방지하기 위해 각 섹션을 cap. const cap = (s: string, n: number) => s.length > n ? s.substring(0, n) + '... [trimmed]' : s; const wrappedInput = `### SYSTEM INSTRUCTION: SELF-REFLECTION PASS 1. Original User Objective: ${cap(originalPrompt, 1500)} 2. Planner Blueprint: ${cap(plan, 3000)} 3. Researcher Output (to be critiqued): ${cap(research, 5000)} 4. Knowledge / Brain Context (for cross-check only — do not invent beyond this): ${cap(brainContext, 2000)} 5. Mission: Run a single rigorous reflection pass and output the structured critique exactly as specified by your persona.`; return this.callLLM(this.persona, wrappedInput, signal); } }