using System.Linq; using System.Text; using System.Text.Json; namespace AxCopilot.Services.Agent; /// /// 스킬 관리 에이전트 도구. /// 로드된 스킬 목록 조회, 스킬 정보 확인, 스킬 실행을 지원합니다. /// public class SkillManagerTool : IAgentTool { // Phase 17-D: context:fork 스킬 격리 실행기 (AgentLoopService에서 주입) private Func>? _forkRunner; /// context:fork 스킬 실행기를 주입합니다 (AgentLoopService 생성자에서 호출). public void SetForkRunner(Func> runner) => _forkRunner = runner; public string Name => "skill_manager"; public string Description => "마크다운 기반 스킬(워크플로우)을 관리하고 실행합니다.\n" + "- list: 사용 가능한 스킬 목록 조회\n" + "- info: 특정 스킬의 상세 정보 확인\n" + "- exec: 스킬 실행 (context:fork 스킬은 격리된 컨텍스트에서 실행)\n" + "- reload: 스킬 폴더를 다시 스캔하여 새 스킬 로드"; public ToolParameterSchema Parameters => new() { Properties = new() { ["action"] = new ToolProperty { Type = "string", Description = "list (목록), info (상세정보), exec (실행), reload (재로드)", Enum = ["list", "info", "exec", "reload"] }, ["skill_name"] = new ToolProperty { Type = "string", Description = "스킬 이름 (info / exec 액션에서 사용)" }, ["arguments"] = new ToolProperty { Type = "string", Description = "스킬에 전달할 인자 문자열 (exec 액션에서 선택적으로 사용)" }, }, Required = ["action"] }; public async Task ExecuteAsync(JsonElement args, AgentContext context, CancellationToken ct = default) { if (!(context.Llm?.EnableSkillSystem ?? true)) return ToolResult.Ok("스킬 시스템이 비활성 상태입니다. 설정 → AX Agent → 공통에서 활성화하세요."); var action = args.TryGetProperty("action", out var a) ? a.GetString() ?? "" : ""; var skillName = args.TryGetProperty("skill_name", out var s) ? s.GetString() ?? "" : ""; var arguments = args.TryGetProperty("arguments", out var g) ? g.GetString() ?? "" : ""; return action switch { "list" => ListSkills(), "info" => InfoSkill(skillName), "exec" => await ExecSkillAsync(skillName, arguments, context, ct), "reload" => ReloadSkills(context), _ => ToolResult.Fail($"지원하지 않는 action: {action}. list, info, exec, reload 중 선택하세요.") }; } private static ToolResult ListSkills() { // Phase 17-D3: UserInvocable=false 스킬은 내부 전용이므로 목록에서 제외 var skills = SkillService.Skills .Where(s => s.IsUserInvocable()) .ToList(); if (skills.Count == 0) return ToolResult.Ok("로드된 스킬이 없습니다. %APPDATA%\\AxCopilot\\skills\\에 *.skill.md 파일을 추가하세요."); var sb = new StringBuilder(); sb.AppendLine($"사용 가능한 스킬 ({skills.Count}개):\n"); foreach (var skill in skills) { sb.AppendLine($" /{skill.Name} — {skill.Label}"); sb.AppendLine($" {skill.Description}"); sb.AppendLine(); } sb.AppendLine("슬래시 명령어(/{name})로 호출하거나, 대화에서 해당 워크플로우를 요청할 수 있습니다."); return ToolResult.Ok(sb.ToString()); } private static ToolResult InfoSkill(string name) { if (string.IsNullOrEmpty(name)) return ToolResult.Fail("skill_name이 필요합니다."); var skill = SkillService.Find(name); if (skill == null) return ToolResult.Fail($"'{name}' 스킬을 찾을 수 없습니다. skill_manager(action: list)로 목록을 확인하세요."); var sb = new StringBuilder(); sb.AppendLine($"스킬 상세: {skill.Label} (/{skill.Name})"); sb.AppendLine($"설명: {skill.Description}"); sb.AppendLine($"파일: {skill.FilePath}"); sb.AppendLine($"\n--- 시스템 프롬프트 ---\n{skill.SystemPrompt}"); return ToolResult.Ok(sb.ToString()); } private static ToolResult ReloadSkills(AgentContext context) { var customFolder = context.Llm?.SkillsFolderPath ?? ""; SkillService.LoadSkills(customFolder); return ToolResult.Ok($"스킬 재로드 완료. {SkillService.Skills.Count}개 로드됨."); } /// /// Phase 17-D: 스킬을 실행합니다. /// context:fork 스킬은 격리된 LLM 컨텍스트에서 실행합니다. /// 일반 스킬은 시스템 프롬프트를 반환하여 에이전트가 컨텍스트로 활용합니다. /// private async Task ExecSkillAsync( string name, string arguments, AgentContext context, CancellationToken ct) { if (string.IsNullOrWhiteSpace(name)) return ToolResult.Fail("skill_name이 필요합니다. skill_manager(action: list)로 이름을 확인하세요."); var skill = SkillService.Find(name); if (skill == null) return ToolResult.Fail($"'{name}' 스킬을 찾을 수 없습니다. skill_manager(action: list)로 목록을 확인하세요."); if (!skill.IsAvailable) return ToolResult.Fail($"스킬 '{skill.Label}'의 실행 환경이 충족되지 않습니다. {skill.UnavailableHint}"); // 인자 치환 + 인라인 명령 실행 var preparedBody = await SkillService.PrepareSkillBodyAsync( skill, arguments, context.WorkFolder ?? "", ct); // context:fork → 격리된 컨텍스트에서 실행 if (skill.IsForkContext && _forkRunner != null) { var forkResult = await _forkRunner(skill, preparedBody, ct); return ToolResult.Ok($"[Fork 스킬 결과: {skill.Label}]\n\n{forkResult}"); } // 일반 스킬 → 시스템 프롬프트 + 준비된 본문 반환 (에이전트가 컨텍스트로 사용) var sb = new StringBuilder(); sb.AppendLine($"[스킬 '{skill.Label}' 활성화됨]"); if (!string.IsNullOrWhiteSpace(skill.SystemPrompt)) { sb.AppendLine("\n--- 스킬 지시사항 ---"); sb.AppendLine(skill.SystemPrompt); } if (!string.IsNullOrWhiteSpace(preparedBody) && preparedBody != skill.SystemPrompt) { sb.AppendLine("\n--- 실행 내용 ---"); sb.AppendLine(preparedBody); } return ToolResult.Ok(sb.ToString()); } }