코드 탭 빌드 실패 조기 종료를 복구 경로로 보강하고 다중 오류 파일 조사 유도를 추가한다

- Code 탭의 review 전용 실행 결과 게이트를 bugfix/feature/refactor에도 확대 적용해 build_run/test_loop 실패 후 읽기만 하고 종료되는 회귀를 막음

- balanced/reasoning_first/tool_call_strict 프로파일에 최근 실행 근거 게이트와 실행 성공 게이트 재시도 1회를 부여해 최소 한 번의 수정·재검증 루프를 보장함

- 빌드 로그에서 오류 파일이 여러 개 잡히면 BuildFailureInvestigationPrompt가 multi_read를 우선 쓰도록 유도해 Themes\\ControlStyles.xaml, Themes\\Effects.xaml 같은 동시 오류를 더 빠르게 좁히도록 개선함

- AgentLoopCodeQualityTests에 다중 오류 파일 조사, 코드 작업 실행 게이트 범위, 코드 중심 프로파일 회귀 테스트를 추가함

- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal / dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter AgentLoopCodeQualityTests (경고 0 / 오류 0, 134 통과)
This commit is contained in:
2026-04-15 23:06:29 +09:00
parent 16e136107c
commit 4980113b99
6 changed files with 161 additions and 11 deletions

View File

@@ -206,6 +206,25 @@ public class AgentLoopCodeQualityTests
refactorPrompt.Should().Contain("behavior-compatible");
}
[Fact]
public void BuildFailureInvestigationPrompt_PrefersMultiReadWhenBuildOutputMentionsMultipleFiles()
{
var prompt = InvokePrivateStatic<string>(
"BuildFailureInvestigationPrompt",
"build_run",
@"E:\code\MainWindow.xaml.cs",
false,
TaskTypePolicy.FromTaskType("feature"),
"""
E:\code\Themes\ControlStyles.xaml(14,17): error MC4005
E:\code\Themes\Effects.xaml(166,50): error MC3072
""");
prompt.Should().Contain("multi_read");
prompt.Should().Contain(@"Themes\ControlStyles.xaml");
prompt.Should().Contain(@"Themes\Effects.xaml");
}
[Fact]
public void BuildFailureReflectionMessage_IncludesFallbackSequenceForBuildFailures()
{
@@ -255,6 +274,33 @@ public class AgentLoopCodeQualityTests
prompt.Should().Contain("고영향 변경");
}
[Theory]
[InlineData("bugfix", true)]
[InlineData("feature", true)]
[InlineData("refactor", true)]
[InlineData("review", true)]
[InlineData("docs", false)]
public void ShouldApplyExecutionResultGate_MatchesCodeExecutionTasks(string taskType, bool expected)
{
var result = InvokePrivateStatic<bool>(
"ShouldApplyExecutionResultGate",
TaskTypePolicy.FromTaskType(taskType));
result.Should().Be(expected);
}
[Theory]
[InlineData("balanced")]
[InlineData("reasoning_first")]
[InlineData("tool_call_strict")]
public void CodeFocusedExecutionProfiles_EnableExecutionRecoveryGates(string profileKey)
{
var policy = ModelExecutionProfileCatalog.Get(profileKey);
policy.RecentExecutionGateMaxRetries.Should().BeGreaterThan(0);
policy.ExecutionSuccessGateMaxRetries.Should().BeGreaterThan(0);
}
[Fact]
public void ComputeAdaptiveMaxRetry_AdjustsByTaskType()
{

View File

@@ -2988,9 +2988,24 @@ public partial class AgentLoopService
private static string BuildFailureInvestigationPrompt(string toolName, string? lastModifiedCodeFilePath, bool highImpactChange, TaskTypePolicy taskPolicy, string? toolOutput = null)
{
var fileLine = string.IsNullOrWhiteSpace(lastModifiedCodeFilePath)
? "1. 최근 수정한 파일을 file_read로 다시 읽습니다.\n"
: $"1. 최근 수정한 파일 '{lastModifiedCodeFilePath}'를 file_read로 다시 읽습니다.\n";
var failurePathHints = ExtractFailurePathHintsFromToolOutput(toolOutput);
string fileLine;
if (failurePathHints.Count >= 2)
{
fileLine =
$"1. 빌드 로그에 나온 오류 파일 {string.Join(", ", failurePathHints.Select(path => $"'{path}'"))}를 multi_read로 함께 읽습니다.\n";
}
else if (failurePathHints.Count == 1)
{
fileLine =
$"1. 빌드 로그에 나온 오류 파일 '{failurePathHints[0]}'를 먼저 file_read로 확인합니다.\n";
}
else
{
fileLine = string.IsNullOrWhiteSpace(lastModifiedCodeFilePath)
? "1. 최근 수정한 파일을 file_read로 다시 읽습니다.\n"
: $"1. 최근 수정한 파일 '{lastModifiedCodeFilePath}'를 file_read로 다시 읽습니다.\n";
}
var taskTypeLine = taskPolicy.FailureInvestigationTaskLine;
var failureHint = BuildFailureTypeRecoveryHint(ClassifyFailureRecoveryKind(toolName, toolOutput), toolName);
var highImpactLine = highImpactChange
@@ -3011,6 +3026,65 @@ public partial class AgentLoopService
highImpactLine;
}
private static List<string> ExtractFailurePathHintsFromToolOutput(string? toolOutput, int maxHints = 4)
{
var hints = new List<string>();
if (string.IsNullOrWhiteSpace(toolOutput))
return hints;
var allowedExt = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
".cs", ".xaml", ".csproj", ".props", ".targets", ".json", ".xml", ".config", ".resx"
};
var tokens = toolOutput.Split(
[' ', '\t', '\r', '\n', '"', '\'', '(', ')', '[', ']', '{', '}', ',', ';'],
StringSplitOptions.RemoveEmptyEntries);
foreach (var raw in tokens)
{
var token = raw.Trim().Replace('/', '\\').TrimEnd(':', '.', ',');
if (string.IsNullOrWhiteSpace(token))
continue;
token = System.Text.RegularExpressions.Regex.Replace(token, @"\(\d+,\d+\):?$", "");
if (token.Length < 4)
continue;
var ext = Path.GetExtension(token);
if (string.IsNullOrWhiteSpace(ext) || !allowedExt.Contains(ext))
continue;
string condensed;
if (Path.IsPathRooted(token))
{
var segments = token.Split(['\\'], StringSplitOptions.RemoveEmptyEntries);
condensed = segments.Length >= 2
? $@"{segments[^2]}\{segments[^1]}"
: Path.GetFileName(token);
}
else
{
var segments = token.Split(['\\'], StringSplitOptions.RemoveEmptyEntries);
condensed = segments.Length >= 2
? $@"{segments[^2]}\{segments[^1]}"
: token;
}
if (string.IsNullOrWhiteSpace(condensed)
|| hints.Contains(condensed, StringComparer.OrdinalIgnoreCase))
{
continue;
}
hints.Add(condensed);
if (hints.Count >= maxHints)
break;
}
return hints;
}
private static string BuildFailureInvestigationPrompt(string toolName, string? lastModifiedCodeFilePath, bool highImpactChange, string taskType)
=> BuildFailureInvestigationPrompt(toolName, lastModifiedCodeFilePath, highImpactChange, TaskTypePolicy.FromTaskType(taskType), null);

View File

@@ -163,7 +163,7 @@ public partial class AgentLoopService
if (!string.Equals(ActiveTab, "Code", StringComparison.OrdinalIgnoreCase))
return false;
if (!taskPolicy.IsReviewTask)
if (!ShouldApplyExecutionResultGate(taskPolicy))
return false;
if (executionPolicy.RecentExecutionGateMaxRetries <= 0 || runState.RecentExecutionGateRetry >= executionPolicy.RecentExecutionGateMaxRetries)
@@ -200,7 +200,7 @@ public partial class AgentLoopService
if (!string.Equals(ActiveTab, "Code", StringComparison.OrdinalIgnoreCase))
return false;
if (!taskPolicy.IsReviewTask)
if (!ShouldApplyExecutionResultGate(taskPolicy))
return false;
if (executionPolicy.ExecutionSuccessGateMaxRetries <= 0 || runState.ExecutionSuccessGateRetry >= executionPolicy.ExecutionSuccessGateMaxRetries)
@@ -227,6 +227,12 @@ public partial class AgentLoopService
return true;
}
private static bool ShouldApplyExecutionResultGate(TaskTypePolicy taskPolicy)
{
return taskPolicy.IsReviewTask
|| taskPolicy.TaskType is "bugfix" or "feature" or "refactor";
}
private bool TryApplyTerminalEvidenceGateTransition(
List<ChatMessage> messages,
string? textResponse,

View File

@@ -70,8 +70,8 @@ public static class ModelExecutionProfileCatalog
HighImpactBuildTestGateMaxRetries: 1,
FinalReportGateMaxRetries: 1,
CodeDiffGateMaxRetries: 1,
RecentExecutionGateMaxRetries: 0,
ExecutionSuccessGateMaxRetries: 0,
RecentExecutionGateMaxRetries: 1,
ExecutionSuccessGateMaxRetries: 1,
DocumentVerificationGateMaxRetries: 0,
TerminalEvidenceGateMaxRetries: 1,
InjectPreCallToolReminder: true), // IBM/Qwen: 첫 호출 직전 reminder 주입으로 이중 강제
@@ -96,8 +96,8 @@ public static class ModelExecutionProfileCatalog
HighImpactBuildTestGateMaxRetries: 1,
FinalReportGateMaxRetries: 1,
CodeDiffGateMaxRetries: 0,
RecentExecutionGateMaxRetries: 0,
ExecutionSuccessGateMaxRetries: 0,
RecentExecutionGateMaxRetries: 1,
ExecutionSuccessGateMaxRetries: 1,
DocumentVerificationGateMaxRetries: 1,
TerminalEvidenceGateMaxRetries: 1),
"fast_readonly" => new ExecutionPolicy(
@@ -171,8 +171,8 @@ public static class ModelExecutionProfileCatalog
HighImpactBuildTestGateMaxRetries: 1,
FinalReportGateMaxRetries: 1,
CodeDiffGateMaxRetries: 0,
RecentExecutionGateMaxRetries: 0,
ExecutionSuccessGateMaxRetries: 0,
RecentExecutionGateMaxRetries: 1,
ExecutionSuccessGateMaxRetries: 1,
DocumentVerificationGateMaxRetries: 1,
TerminalEvidenceGateMaxRetries: 1),
};