mnemos-coder
Version:
CLI-based coding agent with graph-based execution loop and terminal UI
465 lines (461 loc) • 17.4 kB
JavaScript
/**
* 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