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

@@ -35,6 +35,11 @@ public class SubAgentTool : IAgentTool
Type = "string",
Description = "A unique sub-agent identifier used by wait_agents."
},
["profile"] = new ToolProperty
{
Type = "string",
Description = "Execution profile: researcher (default, read-only), coder (can edit/build), writer (doc creation), reviewer (code review), planner (task decomposition)."
},
},
Required = new() { "task", "id" }
};
@@ -46,6 +51,7 @@ public class SubAgentTool : IAgentTool
{
var task = args.SafeTryGetProperty("task", out var t) ? t.SafeGetString() ?? "" : "";
var id = args.SafeTryGetProperty("id", out var i) ? i.SafeGetString() ?? "" : "";
var profileName = args.SafeTryGetProperty("profile", out var p) ? p.SafeGetString() : null;
if (string.IsNullOrWhiteSpace(task) || string.IsNullOrWhiteSpace(id))
return Task.FromResult(ToolResult.Fail("task and id are required."));
@@ -80,7 +86,7 @@ public class SubAgentTool : IAgentTool
{
try
{
var result = await RunSubAgentAsync(id, task, context, cts.Token).ConfigureAwait(false);
var result = await RunSubAgentAsync(id, task, context, profileName, cts.Token).ConfigureAwait(false);
subTask.Result = result;
subTask.Success = true;
NotifyStatus(new SubAgentStatusEvent
@@ -144,11 +150,15 @@ public class SubAgentTool : IAgentTool
$"Sub-agent '{id}' started.\nTask: {task}\nUse wait_agents later to collect the result."));
}
private static async Task<string> RunSubAgentAsync(string id, string task, AgentContext parentContext, CancellationToken ct)
private static async Task<string> RunSubAgentAsync(string id, string task, AgentContext parentContext, string? profileName, CancellationToken ct)
{
var settings = CreateSubAgentSettings(parentContext);
var profile = SubAgentProfileCatalog.Get(profileName);
var settings = CreateSubAgentSettings(parentContext, profile);
using var llm = new LlmService(settings);
using var tools = await CreateSubAgentRegistryAsync(settings).ConfigureAwait(false);
// P2: 프로파일별 temperature override
if (profile.TemperatureOverride.HasValue)
llm.PushInferenceOverride(temperature: profile.TemperatureOverride.Value);
using var tools = await CreateSubAgentRegistryAsync(settings, profile).ConfigureAwait(false);
var loop = new AgentLoopService(llm, tools, settings)
{
@@ -160,7 +170,7 @@ public class SubAgentTool : IAgentTool
new()
{
Role = "system",
Content = BuildSubAgentSystemPrompt(task, parentContext),
Content = BuildSubAgentSystemPrompt(task, parentContext, profile),
},
new()
{
@@ -189,93 +199,151 @@ public class SubAgentTool : IAgentTool
return sb.ToString().TrimEnd();
}
private static SettingsService CreateSubAgentSettings(AgentContext parentContext)
private static SettingsService CreateSubAgentSettings(AgentContext parentContext, SubAgentProfile profile)
{
var settings = new SettingsService();
settings.Load();
var llm = settings.Settings.Llm;
llm.WorkFolder = parentContext.WorkFolder;
llm.FilePermission = "Deny";
llm.FilePermission = profile.FilePermission;
llm.AgentHooks = new();
llm.ToolPermissions = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
llm.DisabledTools = new List<string>
{
"spawn_agent",
"wait_agents",
"file_write",
"file_edit",
"process",
"build_run",
"snippet_runner",
"memory",
"notify",
"open_external",
"user_ask",
"checkpoint",
"diff_preview",
"playbook",
"http_tool",
"clipboard",
"sql_tool",
};
llm.DisabledTools = profile.DisabledToolNames.ToList();
return settings;
}
private static async Task<ToolRegistry> CreateSubAgentRegistryAsync(SettingsService settings)
/// <summary>도구 이름 → 팩토리 매핑 (인스턴스를 필요한 것만 생성).</summary>
private static readonly Dictionary<string, Func<IAgentTool>> ToolFactories =
new(StringComparer.OrdinalIgnoreCase)
{
["file_read"] = () => new FileReadTool(),
["glob"] = () => new GlobTool(),
["grep"] = () => new GrepTool(),
["folder_map"] = () => new FolderMapTool(),
["document_read"] = () => new DocumentReaderTool(),
["dev_env_detect"] = () => new DevEnvDetectTool(),
["git_tool"] = () => new GitTool(),
["lsp_code_intel"] = () => new LspTool(),
["code_search"] = () => new CodeSearchTool(),
["code_review"] = () => new CodeReviewTool(),
["project_rule"] = () => new ProjectRuleTool(),
["skill_manager"] = () => new SkillManagerTool(),
["json_tool"] = () => new JsonTool(),
["regex_tool"] = () => new RegexTool(),
["diff_tool"] = () => new DiffTool(),
["base64_tool"] = () => new Base64Tool(),
["hash_tool"] = () => new HashTool(),
["datetime_tool"] = () => new DateTimeTool(),
["math_tool"] = () => new MathTool(),
["xml_tool"] = () => new XmlTool(),
["multi_read"] = () => new MultiReadTool(),
["file_info"] = () => new FileInfoTool(),
["document_review"] = () => new DocumentReviewTool(),
// coder 프로파일용
["file_write"] = () => new FileWriteTool(),
["file_edit"] = () => new FileEditTool(),
["build_run"] = () => new BuildRunTool(),
["process"] = () => new ProcessTool(),
["test_loop"] = () => new TestLoopTool(),
["snippet_runner"] = () => new SnippetRunnerTool(),
// writer 프로파일용
["html_create"] = () => new HtmlSkill(),
["docx_create"] = () => new DocxSkill(),
["markdown_create"] = () => new MarkdownSkill(),
["csv_create"] = () => new CsvSkill(),
["excel_create"] = () => new ExcelSkill(),
["pptx_create"] = () => new PptxSkill(),
["document_plan"] = () => new DocumentPlannerTool(),
};
private static async Task<ToolRegistry> CreateSubAgentRegistryAsync(SettingsService settings, SubAgentProfile profile)
{
var registry = new ToolRegistry();
registry.Register(new FileReadTool());
registry.Register(new GlobTool());
registry.Register(new GrepTool());
registry.Register(new FolderMapTool());
registry.Register(new DocumentReaderTool());
registry.Register(new DevEnvDetectTool());
registry.Register(new GitTool());
registry.Register(new LspTool());
registry.Register(new CodeSearchTool());
registry.Register(new CodeReviewTool());
registry.Register(new ProjectRuleTool());
registry.Register(new SkillManagerTool());
registry.Register(new JsonTool());
registry.Register(new RegexTool());
registry.Register(new DiffTool());
registry.Register(new Base64Tool());
registry.Register(new HashTool());
registry.Register(new DateTimeTool());
registry.Register(new MathTool());
registry.Register(new XmlTool());
registry.Register(new MultiReadTool());
registry.Register(new FileInfoTool());
registry.Register(new DocumentReviewTool());
// 필요한 도구만 인스턴스 생성 (기존: 전체 63개 생성 후 필터 → 개선: 필요한 것만 팩토리 호출)
foreach (var name in profile.EnabledToolNames)
{
if (ToolFactories.TryGetValue(name, out var factory))
registry.Register(factory());
}
await registry.RegisterMcpToolsAsync(settings.Settings.Llm.McpServers).ConfigureAwait(false);
return registry;
}
private static string BuildSubAgentSystemPrompt(string task, AgentContext parentContext)
private static string BuildSubAgentSystemPrompt(string task, AgentContext parentContext, SubAgentProfile profile)
{
var sb = new StringBuilder();
sb.AppendLine("You are a focused sub-agent for AX Copilot.");
sb.AppendLine("You are running a bounded, read-only investigation.");
sb.AppendLine("Use tools to inspect the project, gather evidence, and produce an actionable result.");
sb.AppendLine("Do not ask the user questions.");
sb.AppendLine("Do not attempt file edits, command execution, notifications, or external side effects.");
sb.AppendLine("Prefer direct evidence from files and tool results over speculation.");
sb.AppendLine("If something is uncertain, say so briefly and identify what evidence is missing.");
// P2: 프로파일별 시스템 프롬프트 접두사 사용
sb.AppendLine(profile.SystemPromptPrefix);
if (!string.IsNullOrWhiteSpace(parentContext.WorkFolder))
sb.AppendLine($"Current work folder: {parentContext.WorkFolder}");
if (!string.IsNullOrWhiteSpace(parentContext.ActiveTab))
sb.AppendLine($"Current tab: {parentContext.ActiveTab}");
// P4: 워크스페이스 컨텍스트 자동 주입
var wsContext = WorkspaceContextGenerator.LoadContext(parentContext.WorkFolder);
if (!string.IsNullOrWhiteSpace(wsContext))
{
sb.AppendLine();
sb.AppendLine("Workspace context:");
sb.AppendLine(wsContext.Length > 2000 ? wsContext[..2000] + "\n...(truncated)" : wsContext);
}
sb.AppendLine();
sb.AppendLine("Investigation rules:");
sb.AppendLine("1. Start by reading the directly relevant files, not by summarizing from memory.");
sb.AppendLine("2. If the task mentions a code path, type, method, or feature, use grep/glob to find references and callers.");
sb.AppendLine("3. If the task is about bugs, trace likely cause -> affected files -> validation evidence.");
sb.AppendLine("4. If the task is about implementation planning, identify the minimum file set and the main risk.");
sb.AppendLine("5. If the task is about review, prioritize concrete defects, regressions, and missing tests.");
// 프로파일별 작업 규칙
switch (profile.Name)
{
case "coder":
sb.AppendLine("Coding rules:");
sb.AppendLine("1. Read the relevant files first to understand existing patterns.");
sb.AppendLine("2. Make the minimal correct change — do not refactor unrelated code.");
sb.AppendLine("3. After editing, verify with build_run or test_loop.");
sb.AppendLine("4. If the build fails, fix the issue immediately.");
sb.AppendLine("5. Report what was changed and the verification result.");
break;
case "writer":
sb.AppendLine("Document creation rules:");
sb.AppendLine("1. Inspect existing documents or source files for context.");
sb.AppendLine("2. Produce well-structured, complete documents.");
sb.AppendLine("3. Use appropriate formatting for the target format.");
sb.AppendLine("4. Verify file was created successfully.");
break;
case "reviewer":
sb.AppendLine("Review rules:");
sb.AppendLine("1. Start by reading the directly relevant files.");
sb.AppendLine("2. Rate each finding P0 (critical) through P3 (minor).");
sb.AppendLine("3. Prioritize concrete defects, regressions, and missing tests.");
sb.AppendLine("4. Cite exact file paths and line ranges as evidence.");
sb.AppendLine("5. Do not suggest edits — only report findings.");
break;
case "planner":
sb.AppendLine("Planning rules:");
sb.AppendLine("1. Inspect the codebase to understand the current architecture.");
sb.AppendLine("2. Decompose the task into ordered steps with clear dependencies.");
sb.AppendLine("3. Identify the minimum file set for each step.");
sb.AppendLine("4. Highlight the primary risk for each step.");
sb.AppendLine("5. Suggest a validation strategy.");
break;
default: // researcher
sb.AppendLine("Investigation rules:");
sb.AppendLine("1. Start by reading the directly relevant files, not by summarizing from memory.");
sb.AppendLine("2. If the task mentions a code path, type, method, or feature, use grep/glob to find references and callers.");
sb.AppendLine("3. If the task is about bugs, trace likely cause -> affected files -> validation evidence.");
sb.AppendLine("4. If the task is about implementation planning, identify the minimum file set and the main risk.");
sb.AppendLine("5. If the task is about review, prioritize concrete defects, regressions, and missing tests.");
break;
}
var workflowHints = BuildSubAgentWorkflowHints(task, parentContext.ActiveTab);
if (!string.IsNullOrWhiteSpace(workflowHints))
{