AX Agent 도구·스킬 정합성 재구성 및 실행 품질 보강

변경 목적:
- AX Agent의 도구 이름, 내부 설정, 스킬 정책, 실행 루프 사이의 불일치를 줄이고 전체 동작 품질을 높인다.
- claw-code 수준의 일관된 동작 품질을 참고하되 AX 구조에 맞는 고유한 카탈로그·정규화 레이어로 재구성한다.

핵심 수정사항:
- 도구 canonical id, legacy alias, 탭 노출, 설정 카테고리, read-only 분류를 중앙 카탈로그로 통합했다.
- ToolRegistry, AgentLoopService, 병렬 실행 분류, 권한 처리, 훅 처리, 스킬 allowed-tools 해석이 같은 이름 체계를 사용하도록 정리했다.
- Agent 설정/일반 설정/도움말의 도구 카드와 훅 편집기, 스킬 설명을 현재 런타임 구조에 맞게 갱신했다.
- 컨텍스트 압축, intent gate, spawn agents, session learning, model prompt adapter, workspace context 관련 변경과 테스트 추가를 함께 반영했다.
- 문서 이력과 비교/로드맵 문서를 최신 상태로 갱신했다.

검증 결과:
- dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify_toolcat\ -p:IntermediateOutputPath=obj\verify_toolcat\ : 경고 0 / 오류 0
- dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter AgentToolCatalogTests -p:OutputPath=bin\verify_toolcat_tests\ -p:IntermediateOutputPath=obj\verify_toolcat_tests\ : 통과 8
This commit is contained in:
2026-04-14 17:52:46 +09:00
parent fa33b98f7e
commit 8cb08576d5
200 changed files with 13522 additions and 5764 deletions

View File

@@ -6,6 +6,7 @@ using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using AxCopilot.Services;
using AxCopilot.Services.Agent;
using AxCopilot.ViewModels;
namespace AxCopilot.Views;
@@ -429,73 +430,21 @@ public partial class SettingsWindow : Window
{
if (AgentEtcContent == null) return;
// 도구 목록 데이터 (카테고리별)
var toolGroups = new (string Category, string Icon, string IconColor, (string Name, string Desc)[] Tools)[]
{
("파일/검색", "\uE8B7", "#F59E0B", new[]
using var registry = Services.Agent.ToolRegistry.CreateDefault();
var toolGroups = registry.All
.Select(tool => new { Tool = tool, Meta = AgentToolCatalog.GetMetadata(tool.Name) })
.GroupBy(x => x.Meta.SettingsCategory, StringComparer.OrdinalIgnoreCase)
.Select(group =>
{
("file_read", "파일 읽기 ? 텍스트/바이너리 파일의 내용을 읽습니다"),
("file_write", "파일 쓰기 ? 새 파일을 생성하거나 기존 파일을 덮어씁니다"),
("file_edit", "파일 편집 ? 줄 번호 기반으로 파일의 특정 부분을 수정합니다"),
("glob", "파일 패턴 검색 ? 글로브 패턴으로 파일을 찾습니다 (예: **/*.cs)"),
("grep_tool", "텍스트 검색 ? 정규식으로 파일 내용을 검색합니다"),
("folder_map", "폴더 구조 ? 프로젝트의 디렉토리 트리를 조회합니다"),
("document_read", "문서 읽기 ? PDF, DOCX 등 문서 파일의 텍스트를 추출합니다"),
}),
("프로세스/빌드", "\uE756", "#06B6D4", new[]
{
("process", "프로세스 실행 ? 외부 명령/프로그램을 실행합니다"),
("build_run", "빌드/테스트 ? 프로젝트 타입을 감지하여 빌드/테스트를 실행합니다"),
("dev_env_detect", "개발 환경 감지 ? IDE, 런타임, 빌드 도구를 자동으로 인식합니다"),
}),
("코드 분석", "\uE943", "#818CF8", new[]
{
("search_codebase", "코드 키워드 검색 ? TF-IDF 기반으로 관련 코드를 찾습니다"),
("code_review", "AI 코드 리뷰 ? Git diff 분석, 파일 정적 검사, PR 요약"),
("lsp", "LSP 인텔리전스 ? 정의 이동, 참조 검색, 심볼 목록 (C#/TS/Python)"),
("test_loop", "테스트 루프 ? 테스트 생성/실행/분석 자동화"),
("git_tool", "Git 작업 ? status, log, diff, commit 등 Git 명령 실행"),
("snippet_runner", "코드 실행 ? C#/Python/JavaScript 스니펫 즉시 실행"),
}),
("문서 생성", "\uE8A5", "#34D399", new[]
{
("excel_create", "Excel 생성 ? .xlsx 스프레드시트를 생성합니다"),
("docx_create", "Word 생성 ? .docx 문서를 생성합니다"),
("csv_create", "CSV 생성 ? CSV 파일을 생성합니다"),
("markdown_create", "마크다운 생성 ? .md 파일을 생성합니다"),
("html_create", "HTML 생성 ? HTML 파일을 생성합니다"),
("chart_create", "차트 생성 ? 데이터 시각화 차트를 생성합니다"),
("batch_create", "배치 생성 ? 스크립트 파일을 생성합니다"),
("document_review", "문서 검증 ? 문서 품질을 검사합니다"),
("format_convert", "포맷 변환 ? 문서 형식을 변환합니다"),
}),
("데이터 처리", "\uE9F5", "#F59E0B", new[]
{
("json_tool", "JSON 처리 ? JSON 파싱, 변환, 검증, 포맷팅"),
("regex_tool", "정규식 ? 정규식 테스트, 추출, 치환"),
("diff_tool", "텍스트 비교 ? 두 파일/텍스트 비교, 통합 diff 출력"),
("base64_tool", "인코딩 ? Base64/URL 인코딩, 디코딩"),
("hash_tool", "해시 계산 ? MD5/SHA256 해시 계산 (파일/텍스트)"),
("datetime_tool", "날짜/시간 ? 날짜 변환, 타임존, 기간 계산"),
}),
("시스템/환경", "\uE770", "#06B6D4", new[]
{
("clipboard_tool", "클립보드 ? Windows 클립보드 읽기/쓰기 (텍스트/이미지)"),
("notify_tool", "알림 ? Windows 시스템 알림 전송"),
("env_tool", "환경변수 ? 환경변수 읽기/설정 (프로세스 범위)"),
("zip_tool", "압축 ? 파일 압축(zip) / 해제"),
("http_tool", "HTTP ? 로컬/사내 HTTP API 호출 (GET/POST)"),
("sql_tool", "SQLite ? SQLite DB 쿼리 실행 (로컬 파일)"),
}),
("에이전트", "\uE99A", "#F472B6", new[]
{
("spawn_agent", "서브에이전트 ? 하위 작업을 병렬로 실행하는 서브에이전트를 생성합니다"),
("wait_agents", "에이전트 대기 ? 실행 중인 서브에이전트의 결과를 수집합니다"),
("memory", "에이전트 메모리 ? 프로젝트 규칙, 선호도를 저장/검색합니다"),
("skill_manager", "스킬 관리 ? 스킬 목록 조회, 상세 정보, 재로드"),
("project_rules", "프로젝트 지침 ? AGENTS.md 개발 지침을 읽고 씁니다"),
}),
};
var meta = group.First().Meta;
var tools = group
.OrderBy(x => x.Tool.Name, StringComparer.OrdinalIgnoreCase)
.Select(x => (Name: x.Tool.Name, Description: x.Tool.Description))
.ToArray();
return (Category: meta.SettingsCategory, Icon: meta.SettingsIcon, IconColor: meta.SettingsIconColor, Tools: tools);
})
.OrderBy(group => group.Category, StringComparer.OrdinalIgnoreCase)
.ToList();
// 도구 목록을 섹션으로 그룹화
var toolCards = new List<UIElement>();
@@ -599,8 +548,10 @@ public partial class SettingsWindow : Window
});
// 도구 아이템
foreach (var (name, toolDesc) in group.Tools)
foreach (var toolInfo in group.Tools)
{
var name = toolInfo.Name;
var toolDesc = toolInfo.Description;
var row = new Grid { Margin = new Thickness(0, 3, 0, 3) };
row.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Auto) });
row.ColumnDefinitions.Add(new ColumnDefinition());
@@ -666,7 +617,7 @@ public partial class SettingsWindow : Window
skillItems.Add(new TextBlock
{
Text = "/ 명령으로 호출할 수 있는 스킬 목록입니다. 앱 내장 + 사용자 추가 스킬이 포함됩니다.\n" +
"(스킬은 사용자가 직접 /명령어를 입력해야 실행됩니다. LLM이 자동 호출하지 않습니다.)",
"(직접 호출 가능한 스킬과 런타임 정책에 연결되는 스킬을 함께 표시합니다.)",
FontSize = 11,
Foreground = new SolidColorBrush(subtleText),
Margin = new Thickness(2, 0, 0, 10),
@@ -1817,6 +1768,7 @@ public partial class SettingsWindow : Window
EncryptedModelName = Services.CryptoService.EncryptIfEnabled(dlg.ModelName, IsEncryptionEnabled),
Service = currentService,
ExecutionProfile = dlg.ExecutionProfile,
PromptFamily = dlg.PromptFamily,
Endpoint = dlg.Endpoint,
ApiKey = dlg.ApiKey,
AllowInsecureTls = dlg.AllowInsecureTls,
@@ -1841,7 +1793,7 @@ public partial class SettingsWindow : Window
var dlg = new ModelRegistrationDialog(currentService, row.Alias, currentModel,
row.Endpoint, row.ApiKey, row.AllowInsecureTls,
row.AuthType ?? "bearer", row.Cp4dUrl ?? "", row.Cp4dUsername ?? "", cp4dPw,
row.ExecutionProfile ?? "balanced");
row.ExecutionProfile ?? "balanced", row.PromptFamily ?? "");
dlg.Owner = this;
if (dlg.ShowDialog() == true)
{
@@ -1849,6 +1801,7 @@ public partial class SettingsWindow : Window
row.EncryptedModelName = Services.CryptoService.EncryptIfEnabled(dlg.ModelName, IsEncryptionEnabled);
row.Service = currentService;
row.ExecutionProfile = dlg.ExecutionProfile;
row.PromptFamily = dlg.PromptFamily;
row.Endpoint = dlg.Endpoint;
row.ApiKey = dlg.ApiKey;
row.AllowInsecureTls = dlg.AllowInsecureTls;
@@ -1956,52 +1909,17 @@ public partial class SettingsWindow : Window
var app = System.Windows.Application.Current as App;
var settings = app?.SettingsService?.Settings.Llm;
using var tools = Services.Agent.ToolRegistry.CreateDefault();
_disabledTools = new HashSet<string>(settings?.DisabledTools ?? new(), StringComparer.OrdinalIgnoreCase);
_disabledTools = new HashSet<string>(AgentToolCatalog.CanonicalizeMany(settings?.DisabledTools ?? new()), StringComparer.OrdinalIgnoreCase);
var disabled = _disabledTools;
// 카테고리 매핑
var categories = new Dictionary<string, List<Services.Agent.IAgentTool>>
{
["파일/검색"] = new(),
["문서 생성"] = new(),
["문서 품질"] = new(),
["코드/개발"] = new(),
["데이터/유틸"] = new(),
["시스템"] = new(),
};
var toolCategoryMap = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
// 파일/검색
["file_read"] = "파일/검색", ["file_write"] = "파일/검색", ["file_edit"] = "파일/검색",
["glob"] = "파일/검색", ["grep"] = "파일/검색", ["folder_map"] = "파일/검색",
["document_read"] = "파일/검색", ["file_watch"] = "파일/검색",
// 문서 생성
["excel_skill"] = "문서 생성", ["docx_skill"] = "문서 생성", ["csv_skill"] = "문서 생성",
["markdown_skill"] = "문서 생성", ["html_skill"] = "문서 생성", ["chart_skill"] = "문서 생성",
["batch_skill"] = "문서 생성", ["pptx_skill"] = "문서 생성",
["document_planner"] = "문서 생성", ["document_assembler"] = "문서 생성",
// 문서 품질
["document_review"] = "문서 품질", ["format_convert"] = "문서 품질",
["template_render"] = "문서 품질", ["text_summarize"] = "문서 품질",
// 코드/개발
["dev_env_detect"] = "코드/개발", ["build_run"] = "코드/개발", ["git_tool"] = "코드/개발",
["lsp"] = "코드/개발", ["sub_agent"] = "코드/개발", ["wait_agents"] = "코드/개발",
["code_search"] = "코드/개발", ["test_loop"] = "코드/개발",
["code_review"] = "코드/개발", ["project_rule"] = "코드/개발",
// 시스템
["process"] = "시스템", ["skill_manager"] = "시스템", ["memory"] = "시스템",
["clipboard"] = "시스템", ["notify"] = "시스템", ["env"] = "시스템",
["image_analyze"] = "시스템",
};
var categories = new Dictionary<string, List<Services.Agent.IAgentTool>>(StringComparer.OrdinalIgnoreCase);
foreach (var tool in tools.All)
{
var cat = toolCategoryMap.TryGetValue(tool.Name, out var c) ? c : "데이터/유틸";
if (categories.ContainsKey(cat))
categories[cat].Add(tool);
else
categories["데이터/유틸"].Add(tool);
var cat = AgentToolCatalog.GetMetadata(tool.Name).SettingsCategory;
if (!categories.ContainsKey(cat))
categories[cat] = new List<Services.Agent.IAgentTool>();
categories[cat].Add(tool);
}
var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue;
@@ -3368,11 +3286,11 @@ public partial class SettingsWindow : Window
stack.Children.Add(new TextBlock { Text = "대상 도구 (* = 모든 도구)", FontSize = 12, Foreground = subFgBrush, Margin = new Thickness(0, 10, 0, 4) });
var toolBox = new TextBox
{
Text = existing?.ToolName ?? "*", FontSize = 13,
Text = AgentToolCatalog.CanonicalizeHookTarget(existing?.ToolName ?? "*"), FontSize = 13,
Foreground = fgBrush, Background = itemBg,
BorderBrush = borderBrush, Padding = new Thickness(12, 8, 12, 8),
};
var toolHolder = CreatePlaceholder("예: file_write, grep_tool", subFgBrush, existing?.ToolName ?? "*");
var toolHolder = CreatePlaceholder("예: file_write, grep", subFgBrush, AgentToolCatalog.CanonicalizeHookTarget(existing?.ToolName ?? "*"));
toolBox.TextChanged += (_, _) => toolHolder.Visibility = string.IsNullOrEmpty(toolBox.Text) ? Visibility.Visible : Visibility.Collapsed;
var toolGrid = new Grid();
toolGrid.Children.Add(toolBox);
@@ -3469,7 +3387,7 @@ public partial class SettingsWindow : Window
var entry = new Models.AgentHookEntry
{
Name = nameBox.Text.Trim(),
ToolName = string.IsNullOrWhiteSpace(toolBox.Text) ? "*" : toolBox.Text.Trim(),
ToolName = AgentToolCatalog.CanonicalizeHookTarget(toolBox.Text),
Timing = preRadio.IsChecked == true ? "pre" : "post",
ScriptPath = pathBox.Text.Trim(),
Arguments = argsBox.Text.Trim(),
@@ -3498,6 +3416,7 @@ public partial class SettingsWindow : Window
if (HookListPanel == null) return;
HookListPanel.Children.Clear();
_vm.Service.Settings.Llm.AgentHooks = AgentToolCatalog.CanonicalizeHooks(_vm.Service.Settings.Llm.AgentHooks);
var hooks = _vm.Service.Settings.Llm.AgentHooks;
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.Black;
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
@@ -3895,7 +3814,7 @@ public partial class SettingsWindow : Window
// 도구 비활성 목록 저장
if (_toolCardsLoaded)
llm.DisabledTools = _disabledTools.ToList();
llm.DisabledTools = AgentToolCatalog.CanonicalizeMany(_disabledTools).ToList();
}
private void AddSnippet_Click(object sender, RoutedEventArgs e)
@@ -4168,5 +4087,3 @@ public partial class SettingsWindow : Window
base.OnClosed(e);
}
}