UNPKG

mnemos-coder

Version:

CLI-based coding agent with graph-based execution loop and terminal UI

465 lines (461 loc) 17.4 kB
/** * Subagent Manager - Claude Code 스타일의 subagent 관리 시스템 * 작업을 분석하고 적절한 subagent에게 위임 */ import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; import { LLMClient } from '../llm-client.js'; import { MCPClient } from '../mcp-client-sdk.js'; import { GlobalConfig } from '../config/GlobalConfig.js'; import { DEFAULT_SUBAGENTS_CONFIG } from '../config/defaultSubagentsConfig.js'; import { getLogger } from '../utils/Logger.js'; import { SubagentRegistry } from './SubagentRegistry.js'; export class SubagentManager { subagents = new Map(); subagentConfigs = new Map(); activeTasks = new Map(); llmClient; sharedMCPClient; taskIdCounter = 0; externalConfig = null; registry = null; logger; constructor(sharedMCPClient) { const globalConfig = GlobalConfig.getInstance(); const llmConfig = globalConfig.getLLMConfig(); this.llmClient = new LLMClient(llmConfig); this.sharedMCPClient = sharedMCPClient || new MCPClient(); this.logger = getLogger('SubagentManager'); } /** * Get loaded subagents for external access */ getSubagents() { return this.subagents; } /** * 초기화 - 모든 사용 가능한 subagent 로드 */ async initialize() { this.logger.info('Initializing Subagent Manager'); // Load external configuration await this.loadExternalConfig(); // Load all subagents from config await this.loadSubagentsFromConfig(); // 프로젝트 레벨 subagent 로드 (.mnemos/agents/) await this.loadSubagentsFromDirectory('.mnemos/agents'); // 사용자 레벨 subagent 로드 (~/.mnemos/agents/) const userAgentsDir = path.join(os.homedir(), '.mnemos', 'agents'); await this.loadSubagentsFromDirectory(userAgentsDir); // Create SubagentRegistry with loaded configuration if (this.externalConfig) { this.registry = new SubagentRegistry(this.externalConfig); } this.logger.info(`Loaded ${this.subagentConfigs.size} subagents`); } /** * 디렉토리에서 subagent 설정 로드 */ async loadSubagentsFromDirectory(directory) { if (!fs.existsSync(directory)) { return; } const files = fs.readdirSync(directory); for (const file of files) { if (file.endsWith('.md')) { try { const filePath = path.join(directory, file); const config = await this.parseSubagentFile(filePath); if (config) { await this.loadSubagent(config); } } catch (error) { this.logger.warn(`Failed to load subagent from ${file}`, error); } } } } /** * Markdown 파일에서 subagent 설정 파싱 */ async parseSubagentFile(filePath) { const content = fs.readFileSync(filePath, 'utf-8'); // YAML frontmatter 파싱 const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/); if (!frontmatterMatch) { return null; } try { // 간단한 YAML 파싱 (실제로는 yaml 라이브러리 사용 권장) const frontmatter = this.parseSimpleYAML(frontmatterMatch[1]); const systemPrompt = frontmatterMatch[2].trim(); return { name: frontmatter.name, description: frontmatter.description, tools: frontmatter.tools ? frontmatter.tools.split(',').map((t) => t.trim()) : undefined, systemPrompt, maxIterations: frontmatter.maxIterations || 10, priority: frontmatter.priority || 5, tags: frontmatter.tags ? frontmatter.tags.split(',').map((t) => t.trim()) : [], enabled: frontmatter.enabled !== false, version: frontmatter.version || '1.0.0' }; } catch (error) { console.warn(`Failed to parse frontmatter in ${filePath}:`, error); return null; } } /** * 간단한 YAML 파싱 (키:값 형태만) */ parseSimpleYAML(yaml) { const result = {}; const lines = yaml.split('\n'); for (const line of lines) { const trimmed = line.trim(); if (trimmed && !trimmed.startsWith('#')) { const colonIndex = trimmed.indexOf(':'); if (colonIndex > 0) { const key = trimmed.substring(0, colonIndex).trim(); const value = trimmed.substring(colonIndex + 1).trim(); // 타입 변환 if (value === 'true') result[key] = true; else if (value === 'false') result[key] = false; else if (!isNaN(Number(value))) result[key] = Number(value); else result[key] = value; } } } return result; } /** * Load subagents from configuration file */ async loadSubagentsFromConfig() { if (!this.externalConfig || !this.externalConfig.subagents) { this.logger.warn('No subagent configuration found'); return; } for (const [key, config] of Object.entries(this.externalConfig.subagents)) { if (config.enabled !== false) { try { await this.loadSubagent(config); this.logger.debug(`Registered subagent: ${config.name} (${config.description})`); } catch (error) { this.logger.error(`Failed to load subagent ${key}`, error); } } } } /** * Create a generic subagent from config */ async createGenericSubagent(config) { const { GenericSubagent } = await import('./GenericSubagent.js'); return new GenericSubagent(config); } /** * Load external configuration from .mnemos/subagents-config.json */ async loadExternalConfig() { const configPath = path.join(process.cwd(), '.mnemos', 'subagents-config.json'); if (fs.existsSync(configPath)) { try { const configContent = fs.readFileSync(configPath, 'utf-8'); this.externalConfig = JSON.parse(configContent); this.logger.debug(`Loaded external subagent configuration from ${configPath}`); } catch (error) { this.logger.error('Failed to load external subagent config', error); } } else { // Create default config file if it doesn't exist await this.createDefaultConfigFile(configPath); } } /** * Create default configuration file */ async createDefaultConfigFile(configPath) { try { const configDir = path.dirname(configPath); if (!fs.existsSync(configDir)) { fs.mkdirSync(configDir, { recursive: true }); } // Use the comprehensive default configuration with all streamlined subagents fs.writeFileSync(configPath, JSON.stringify(DEFAULT_SUBAGENTS_CONFIG, null, 2)); this.logger.info(`Created default subagent configuration file at ${configPath}`); this.externalConfig = DEFAULT_SUBAGENTS_CONFIG; } catch (error) { this.logger.error('Failed to create default config file', error); } } /** * Get LLM config for a specific profile */ getLLMConfigForProfile(profileName) { if (!this.externalConfig?.llmConfig) { return null; } const profile = profileName || 'default'; return this.externalConfig.llmConfig[profile] || this.externalConfig.llmConfig.default; } // Removed getDefaultSubagentConfig - all configs come from JSON file now /** * Subagent 로드 */ async loadSubagent(config) { if (config.enabled === false) { return; } // Apply LLM config if specified if (config.llmProfile && this.externalConfig?.llmConfig) { const llmConfig = this.getLLMConfigForProfile(config.llmProfile); if (llmConfig) { config.llmConfig = llmConfig; } } this.subagentConfigs.set(config.name, config); // 실제 subagent 인스턴스는 필요할 때 생성 (lazy loading) // console.log is already done in loadSubagentsFromConfig } /** * 작업에 가장 적합한 subagent 찾기 */ async findBestSubagent(taskDescription) { const candidates = []; // LLM을 사용해서 각 subagent의 적합성 평가 for (const [name, config] of this.subagentConfigs) { if (!config.enabled) continue; try { const confidence = await this.evaluateSubagentFit(taskDescription, config); if (confidence > 0.3) { // 최소 임계값 candidates.push({ subagent: config, confidence, reasoning: `Task matches ${config.description} with ${Math.round(confidence * 100)}% confidence` }); } } catch (error) { this.logger.warn(`Failed to evaluate subagent ${name}`, error); } } // 가장 높은 confidence를 가진 subagent 선택 candidates.sort((a, b) => b.confidence - a.confidence); return candidates.length > 0 ? candidates[0] : null; } /** * LLM을 사용해서 subagent 적합성 평가 */ async evaluateSubagentFit(taskDescription, config) { const prompt = ` Task Description: "${taskDescription}" Subagent Information: - Name: ${config.name} - Description: ${config.description} - Available Tools: ${config.tools?.join(', ') || 'None specified'} - Tags: ${config.tags?.join(', ') || 'None'} Rate how well this subagent can handle the given task on a scale of 0.0 to 1.0, where: - 0.0 = Cannot handle this task at all - 0.5 = Can partially handle this task - 1.0 = Perfect match for this task Consider: 1. Task requirements vs subagent capabilities 2. Available tools vs needed tools 3. Subagent specialization vs task type Respond with only a number between 0.0 and 1.0:`; try { const messages = [ { role: 'system', content: 'You are an AI task routing specialist. Evaluate how well subagents match given tasks.' }, { role: 'user', content: prompt } ]; const response = await this.llmClient.chat(messages); const score = parseFloat(response.trim()); return isNaN(score) ? 0 : Math.max(0, Math.min(1, score)); } catch (error) { this.logger.warn('Failed to evaluate subagent fit with LLM, using fallback', error); return this.fallbackEvaluateSubagentFit(taskDescription, config); } } /** * LLM 실패 시 키워드 기반 fallback 평가 */ fallbackEvaluateSubagentFit(taskDescription, config) { const taskLower = taskDescription.toLowerCase(); const descriptionLower = (config.description || '').toLowerCase(); const tags = config.tags || []; let score = 0; // 설명에서 키워드 매치 const commonWords = this.getCommonWords(taskLower, descriptionLower); score += commonWords * 0.3; // 태그 매치 for (const tag of tags) { if (taskLower.includes(tag.toLowerCase())) { score += 0.2; } } // 특정 키워드 보너스 const keywordBonuses = { 'review': ['review', 'analyze', 'check', 'audit'], 'search': ['find', 'search', 'locate', 'discover'], 'test': ['test', 'testing', 'spec', 'coverage'], 'document': ['document', 'docs', 'readme', 'comment'], 'analyze': ['analyze', 'analysis', 'metrics', 'complexity'] }; for (const [category, keywords] of Object.entries(keywordBonuses)) { if (config.name.includes(category)) { for (const keyword of keywords) { if (taskLower.includes(keyword)) { score += 0.1; } } } } return Math.min(score, 1.0); } /** * 두 문자열 간의 공통 단어 비율 */ getCommonWords(str1, str2) { const words1 = new Set(str1.split(/\s+/).filter(w => w.length > 3)); const words2 = new Set(str2.split(/\s+/).filter(w => w.length > 3)); const intersection = new Set([...words1].filter(x => words2.has(x))); const union = new Set([...words1, ...words2]); return union.size > 0 ? intersection.size / union.size : 0; } /** * 작업 위임 */ async delegateTask(taskDescription, context) { const taskId = `task_${Date.now()}_${++this.taskIdCounter}`; // 적합한 subagent 찾기 const match = await this.findBestSubagent(taskDescription); if (!match) { throw new Error('No suitable subagent found for this task'); } const delegation = { taskId, targetSubagent: match.subagent.name, taskDescription, context, priority: match.subagent.priority || 5, createdAt: new Date() }; this.activeTasks.set(taskId, delegation); this.logger.debug(`Delegated task ${taskId} to ${match.subagent.name} (confidence: ${Math.round(match.confidence * 100)}%)`); this.logger.debug(`Reasoning: ${match.reasoning}`); return taskId; } /** * 작업 결과 조회 */ async getTaskResult(taskId) { return this.activeTasks.get(taskId) || null; } /** * 사용 가능한 subagent 목록 */ listAvailableSubagents() { return Array.from(this.subagentConfigs.values()).filter(config => config.enabled !== false); } /** * Get SubagentRegistry instance */ getRegistry() { return this.registry; } /** * Get dynamic Task tool specification */ getTaskToolSpec() { return this.registry?.getTaskToolSpec() || null; } /** * Get system prompt fragment with subagent information */ getSystemPromptFragment() { return this.registry?.getSystemPromptFragment() || ''; } /** * Validate subagent name */ validateSubagentName(name) { return this.registry?.validateSubagentName(name) || false; } /** * 특정 subagent로 실행 */ async *executeWithSubagent(subagentName, query, context) { const config = this.subagentConfigs.get(subagentName); if (!config) { yield { type: 'error', content: `Subagent ${subagentName} not found` }; return; } // Subagent 인스턴스 생성 또는 가져오기 let subagent = this.subagents.get(subagentName); if (!subagent) { subagent = await this.createSubagentInstance(config); this.subagents.set(subagentName, subagent); } // 초기화 및 실행 await subagent.initialize(context); yield { type: 'delegation', content: `Starting execution with subagent: ${subagentName}`, subagentName }; yield* subagent.execute(query, context); } /** * Subagent 인스턴스 생성 */ async createSubagentInstance(config) { // Always use GenericSubagent for config-driven subagents const { GenericSubagent } = await import('./GenericSubagent.js'); const subagent = new GenericSubagent(config, this.sharedMCPClient); // Initialize with context including available tools from config await subagent.initialize({ sessionId: `subagent-${Date.now()}`, taskDescription: '', workingDirectory: process.cwd(), mcpClient: this.sharedMCPClient, availableTools: config.tools || [] }); return subagent; } // Removed capitalizeFirst - no longer needed without builtin classes /** * 모든 subagent 종료 */ async shutdown() { this.logger.info('Shutting down all subagents'); for (const [name, subagent] of this.subagents) { try { await subagent.shutdown(); this.logger.debug(`Subagent ${name} shut down`); } catch (error) { this.logger.warn(`Failed to shut down subagent ${name}`, error); } } this.subagents.clear(); this.activeTasks.clear(); } } //# sourceMappingURL=SubagentManager.js.map