코드 탭 빌드 실패 조기 종료를 복구 경로로 보강하고 다중 오류 파일 조사 유도를 추가한다
- 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:
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user