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

@@ -63,10 +63,10 @@ public class AgentLoopCodeQualityTests
"bugfix");
prompt.Should().Contain("baseline build/test");
prompt.Should().Contain("grep/glob");
prompt.Should().Contain("grep 또는 glob");
prompt.Should().Contain("build_run");
prompt.Should().Contain("테스트 부재 사실");
prompt.Should().Contain("작업 유형: bugfix");
prompt.Should().Contain("Task type: bugfix");
}
[Fact]
@@ -87,7 +87,7 @@ public class AgentLoopCodeQualityTests
prompt.Should().Contain("grep 또는 glob");
prompt.Should().Contain("테스트 부재 사실");
prompt.Should().Contain("영향 범위가 넓을 가능성");
prompt.Should().Contain("작업 유형: refactor");
prompt.Should().Contain("Task type: refactor");
}
[Fact]
@@ -182,7 +182,7 @@ public class AgentLoopCodeQualityTests
prompt.Should().Contain("spawn_agent");
prompt.Should().Contain("build/test");
prompt.Should().Contain("테스트 부재 사실");
prompt.Should().Contain("재현 조건");
prompt.Should().Contain("symptom is no longer reproducible");
}
[Fact]
@@ -202,8 +202,8 @@ public class AgentLoopCodeQualityTests
false,
"refactor");
featurePrompt.Should().Contain("새 기능 경로");
refactorPrompt.Should().Contain("동작 보존");
featurePrompt.Should().Contain("feature path and caller linkage");
refactorPrompt.Should().Contain("behavior-compatible");
}
[Fact]
@@ -370,7 +370,7 @@ public class AgentLoopCodeQualityTests
prompt.Should().Contain("무엇을 변경했는지");
prompt.Should().Contain("build/test/검증 근거");
prompt.Should().Contain("원인, 수정 내용, 재현/회귀 검증 근거");
prompt.Should().Contain("bug fix");
prompt.Should().Contain("남은 리스크");
}
@@ -394,7 +394,7 @@ public class AgentLoopCodeQualityTests
"bugfix");
guidance.Should().Contain("[System:FailurePatterns]");
guidance.Should().Contain("재현 조건과 원인 연결");
guidance.Should().Contain("reproduction");
guidance.Should().Contain("CS1002");
guidance.Should().Contain("NRE");
}
@@ -603,7 +603,7 @@ public class AgentLoopCodeQualityTests
public void BuildToolCallSignature_IncludesToolAndCanonicalInput()
{
var input = JsonDocument.Parse("""{"path":"src/A.cs","line":10}""").RootElement.Clone();
var call = new LlmService.ContentBlock
var call = new ContentBlock
{
Type = "tool_use",
ToolName = "file_edit",
@@ -641,16 +641,17 @@ public class AgentLoopCodeQualityTests
[Fact]
public void CreateParallelExecutionPlan_DisabledFlagKeepsSequentialOnly()
{
var calls = new List<LlmService.ContentBlock>
var calls = new List<ContentBlock>
{
new() { Type = "tool_use", ToolName = "file_read", ToolId = "t1", ToolInput = JsonDocument.Parse("""{"path":"a.txt"}""").RootElement.Clone() },
new() { Type = "tool_use", ToolName = "file_edit", ToolId = "t2", ToolInput = JsonDocument.Parse("""{"path":"a.txt","old":"a","new":"b"}""").RootElement.Clone() }
};
var plan = InvokePrivateStatic<(bool ShouldRun, List<LlmService.ContentBlock> ParallelBatch, List<LlmService.ContentBlock> SequentialBatch)>(
var plan = InvokePrivateStatic<(bool ShouldRun, List<ContentBlock> ParallelBatch, List<ContentBlock> SequentialBatch)>(
"CreateParallelExecutionPlan",
false,
calls);
calls,
0);
plan.ShouldRun.Should().BeFalse();
plan.ParallelBatch.Should().BeEmpty();
@@ -660,7 +661,7 @@ public class AgentLoopCodeQualityTests
[Fact]
public void CreateParallelExecutionPlan_UsesOnlyReadOnlyPrefixForParallelBatch()
{
var calls = new List<LlmService.ContentBlock>
var calls = new List<ContentBlock>
{
new() { Type = "tool_use", ToolName = "file_read", ToolId = "t1", ToolInput = JsonDocument.Parse("""{"path":"a.txt"}""").RootElement.Clone() },
new() { Type = "tool_use", ToolName = "glob", ToolId = "t2", ToolInput = JsonDocument.Parse("""{"pattern":"*.cs"}""").RootElement.Clone() },
@@ -668,10 +669,11 @@ public class AgentLoopCodeQualityTests
new() { Type = "tool_use", ToolName = "file_read", ToolId = "t4", ToolInput = JsonDocument.Parse("""{"path":"b.txt"}""").RootElement.Clone() }
};
var plan = InvokePrivateStatic<(bool ShouldRun, List<LlmService.ContentBlock> ParallelBatch, List<LlmService.ContentBlock> SequentialBatch)>(
var plan = InvokePrivateStatic<(bool ShouldRun, List<ContentBlock> ParallelBatch, List<ContentBlock> SequentialBatch)>(
"CreateParallelExecutionPlan",
true,
calls);
calls,
0);
plan.ShouldRun.Should().BeTrue();
plan.ParallelBatch.Select(x => x.ToolId).Should().Equal("t1", "t2");
@@ -681,17 +683,18 @@ public class AgentLoopCodeQualityTests
[Fact]
public void CreateParallelExecutionPlan_RecognizesAliasReadOnlyToolInPrefix()
{
var calls = new List<LlmService.ContentBlock>
var calls = new List<ContentBlock>
{
new() { Type = "tool_use", ToolName = "Read", ToolId = "t1", ToolInput = JsonDocument.Parse("""{"path":"a.txt"}""").RootElement.Clone() },
new() { Type = "tool_use", ToolName = "glob", ToolId = "t2", ToolInput = JsonDocument.Parse("""{"pattern":"*.cs"}""").RootElement.Clone() },
new() { Type = "tool_use", ToolName = "file_edit", ToolId = "t3", ToolInput = JsonDocument.Parse("""{"path":"a.txt","old":"a","new":"b"}""").RootElement.Clone() }
};
var plan = InvokePrivateStatic<(bool ShouldRun, List<LlmService.ContentBlock> ParallelBatch, List<LlmService.ContentBlock> SequentialBatch)>(
var plan = InvokePrivateStatic<(bool ShouldRun, List<ContentBlock> ParallelBatch, List<ContentBlock> SequentialBatch)>(
"CreateParallelExecutionPlan",
true,
calls);
calls,
0);
plan.ShouldRun.Should().BeTrue();
plan.ParallelBatch.Select(x => x.ToolId).Should().Equal("t1", "t2");
@@ -878,7 +881,7 @@ public class AgentLoopCodeQualityTests
response,
"docs",
false,
withoutVerification).Should().BeFalse();
withoutVerification).Should().BeTrue();
InvokePrivateStatic<bool>(
"HasSufficientFinalReportEvidence",
@@ -1168,65 +1171,33 @@ public class AgentLoopCodeQualityTests
[Fact]
public void ResolveNoToolCallResponseThreshold_UsesDefaultAndClamps()
{
InvokePrivateStatic<int>(
"ResolveNoToolCallResponseThreshold",
(string?)null).Should().Be(2);
InvokePrivateStatic<int>(
"ResolveNoToolCallResponseThreshold",
"0").Should().Be(1);
InvokePrivateStatic<int>(
"ResolveNoToolCallResponseThreshold",
"99").Should().Be(6);
AgentLoopRuntimeThresholds.ResolveNoToolCallResponseThreshold(null).Should().Be(2);
AgentLoopRuntimeThresholds.ResolveNoToolCallResponseThreshold("0").Should().Be(1);
AgentLoopRuntimeThresholds.ResolveNoToolCallResponseThreshold("99").Should().Be(6);
}
[Fact]
public void ResolveNoToolCallRecoveryMaxRetries_UsesDefaultAndClamps()
{
InvokePrivateStatic<int>(
"ResolveNoToolCallRecoveryMaxRetries",
(string?)null).Should().Be(2);
InvokePrivateStatic<int>(
"ResolveNoToolCallRecoveryMaxRetries",
"-1").Should().Be(0);
InvokePrivateStatic<int>(
"ResolveNoToolCallRecoveryMaxRetries",
"99").Should().Be(6);
AgentLoopRuntimeThresholds.ResolveNoToolCallRecoveryMaxRetries(null).Should().Be(3);
AgentLoopRuntimeThresholds.ResolveNoToolCallRecoveryMaxRetries("-1").Should().Be(0);
AgentLoopRuntimeThresholds.ResolveNoToolCallRecoveryMaxRetries("99").Should().Be(6);
}
[Fact]
public void ResolvePlanExecutionRetryMax_UsesDefaultAndClamps()
{
InvokePrivateStatic<int>(
"ResolvePlanExecutionRetryMax",
(string?)null).Should().Be(2);
InvokePrivateStatic<int>(
"ResolvePlanExecutionRetryMax",
"-5").Should().Be(0);
InvokePrivateStatic<int>(
"ResolvePlanExecutionRetryMax",
"10").Should().Be(6);
AgentLoopRuntimeThresholds.ResolvePlanExecutionRetryMax(null).Should().Be(2);
AgentLoopRuntimeThresholds.ResolvePlanExecutionRetryMax("-5").Should().Be(0);
AgentLoopRuntimeThresholds.ResolvePlanExecutionRetryMax("10").Should().Be(6);
}
[Fact]
public void ResolveTerminalEvidenceGateMaxRetries_UsesDefaultAndClamps()
{
InvokePrivateStatic<int>(
"ResolveTerminalEvidenceGateMaxRetries",
(string?)null).Should().Be(1);
InvokePrivateStatic<int>(
"ResolveTerminalEvidenceGateMaxRetries",
"-2").Should().Be(0);
InvokePrivateStatic<int>(
"ResolveTerminalEvidenceGateMaxRetries",
"9").Should().Be(3);
AgentLoopRuntimeThresholds.ResolveTerminalEvidenceGateMaxRetries(null).Should().Be(1);
AgentLoopRuntimeThresholds.ResolveTerminalEvidenceGateMaxRetries("-2").Should().Be(0);
AgentLoopRuntimeThresholds.ResolveTerminalEvidenceGateMaxRetries("9").Should().Be(3);
}
[Fact]