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.`; 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 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.`; return this.callLLM(this.persona, wrappedInput, signal); } }